refactor(chat): extract home helpers, chart library, and starter surfaces
page.tsx still owns the chat main chain, but the first product surfaces now live in their own modules so later splits can land without editing the 4k-line Home. Source-lock tests follow the moved tokens; the orphan user-data contract is aligned and added to the quick gate. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -38,6 +38,24 @@ SYNASTRY_REPORT_MIGRATION = (
|
||||
/ "20260718101000_repair_missing_synastry_reports.sql"
|
||||
)
|
||||
PAGE = Path(__file__).resolve().parents[1] / "frontend" / "src" / "app" / "page.tsx"
|
||||
_FRONTEND_SRC = Path(__file__).resolve().parents[1] / "frontend" / "src"
|
||||
HOME_SURFACE_FILES = (
|
||||
PAGE,
|
||||
_FRONTEND_SRC / "lib" / "home-types.ts",
|
||||
_FRONTEND_SRC / "lib" / "home-profile.ts",
|
||||
_FRONTEND_SRC / "lib" / "home-cloud-sync.ts",
|
||||
_FRONTEND_SRC / "components" / "birth-location-fields.tsx",
|
||||
_FRONTEND_SRC / "components" / "profile-fields.tsx",
|
||||
_FRONTEND_SRC / "components" / "onboarding-chat-message.tsx",
|
||||
_FRONTEND_SRC / "components" / "chart-library-panel.tsx",
|
||||
_FRONTEND_SRC / "components" / "starter-home.tsx",
|
||||
)
|
||||
|
||||
|
||||
def _home_surface() -> str:
|
||||
return "".join(path.read_text(encoding="utf-8") for path in HOME_SURFACE_FILES)
|
||||
|
||||
|
||||
SESSION_CREATE_ROUTE = Path(__file__).resolve().parents[1] / "frontend" / "src" / "app" / "api" / "sessions" / "route.ts"
|
||||
SESSION_ITEM_ROUTE = Path(__file__).resolve().parents[1] / "frontend" / "src" / "app" / "api" / "sessions" / "[id]" / "route.ts"
|
||||
ACCOUNT_ROUTE = Path(__file__).resolve().parents[1] / "frontend" / "src" / "app" / "api" / "account" / "route.ts"
|
||||
@@ -94,12 +112,16 @@ def test_user_profile_and_chat_session_database_contract() -> None:
|
||||
|
||||
|
||||
def test_chat_page_uses_authenticated_cloud_persistence() -> None:
|
||||
source = PAGE.read_text(encoding="utf-8")
|
||||
source = _home_surface()
|
||||
create_route = SESSION_CREATE_ROUTE.read_text(encoding="utf-8")
|
||||
item_route = SESSION_ITEM_ROUTE.read_text(encoding="utf-8")
|
||||
|
||||
assert '.from("profiles")' in source
|
||||
assert '.from("chat_sessions")' in source
|
||||
# Former value: page talked to Supabase with `.from("profiles")` / `.from("chat_sessions")`.
|
||||
# Persistence is now same-origin APIs; the lock is still "no browser-owned writes".
|
||||
assert 'fetch("/api/account"' in source
|
||||
assert 'fetch("/api/sessions"' in source
|
||||
assert '.from("profiles")' not in source
|
||||
assert '.from("chat_sessions")' not in source
|
||||
assert 'await writeChatSession(session.id, values, mode)' in source
|
||||
assert 'mode === "create" ? "/api/sessions"' in (
|
||||
Path(__file__).resolve().parents[1]
|
||||
@@ -114,16 +136,22 @@ def test_chat_page_uses_authenticated_cloud_persistence() -> None:
|
||||
assert 'await persistSession(userSession)' not in source
|
||||
assert source.index('updateSession(sessionId, () => userSession)') < source.index('await persistSession(completedSession)')
|
||||
assert 'function completedOnboardingTranscript(profile: Profile, greeting: string): Message[]' in source
|
||||
assert 'messages: [...preservedMessages, { role: "user", text: question }]' in source
|
||||
# Former value: `messages: [...preservedMessages, { role: "user", text: question }]`
|
||||
# The user turn is still appended unless the question is already present.
|
||||
assert 'messages: questionAlreadyPresent ? preservedMessages : [...preservedMessages, { role: "user", text: question }]' in source
|
||||
assert 'await persistSession(completedSession)' in source
|
||||
assert 'const stoppedRequestAwaitingSettlement = useRef<string | null>(null)' in source
|
||||
assert 'const stoppedSessionPersistence = useRef(new Map<string, Promise<void>>())' in source
|
||||
assert 'if (ownsInterface && !partialReply)' in source
|
||||
assert 'await persistSession(interruptedSession)' in source
|
||||
assert 'await persistence' in source
|
||||
# Former values: persist interruptedSession when `ownsInterface && !partialReply`.
|
||||
# Interrupted answers stay in page memory; the client does not replace the server transcript.
|
||||
assert "await persistSession(interruptedSession)" not in source
|
||||
assert "if (!cancelled && ownsInterface && pendingConsultation.current)" in source
|
||||
assert "await persistence" in source
|
||||
assert "pendingSessionId || cancellationInFlight.current" in source
|
||||
assert "setCancellationPending(true)" in source
|
||||
assert "系统正在以账户记录为准同步点数" in source
|
||||
# Former value: "系统正在以账户记录为准同步点数"
|
||||
assert "正在停止回答并申请退回本次点数…" in source
|
||||
assert "void refreshAccount()" in source
|
||||
assert "回答中途断开,已保留现有内容,本次已计费。" not in source
|
||||
assert "本次已开始生成并计费" not in source
|
||||
# Former value: `localStorage.setItem(chartLibraryStorageKey(accountId)`
|
||||
@@ -134,7 +162,17 @@ def test_chat_page_uses_authenticated_cloud_persistence() -> None:
|
||||
|
||||
def test_account_profile_patch_rejects_array_payloads() -> None:
|
||||
route = ACCOUNT_ROUTE.read_text(encoding="utf-8")
|
||||
assert 'typeof payload !== "object" || Array.isArray(payload)' in route
|
||||
schema = (
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "frontend"
|
||||
/ "src"
|
||||
/ "lib"
|
||||
/ "account-profile-patch.ts"
|
||||
).read_text(encoding="utf-8")
|
||||
# Former value: `typeof payload !== "object" || Array.isArray(payload)` in the route.
|
||||
# Arrays are still rejected: the route parses with a Zod object schema.
|
||||
assert "accountProfilePatchSchema.safeParse" in route
|
||||
assert "export const accountProfilePatchSchema = z.object({" in schema
|
||||
assert "账户资料格式不正确" in route
|
||||
|
||||
|
||||
@@ -142,7 +180,7 @@ def test_chart_profile_library_has_cloud_table_api_and_local_fallback() -> None:
|
||||
sql = re.sub(r"\s+", " ", CHART_PROFILE_MIGRATION.read_text(encoding="utf-8").lower()).strip()
|
||||
route = CHART_PROFILE_ROUTE.read_text(encoding="utf-8")
|
||||
delete_route = CHART_PROFILE_DELETE_ROUTE.read_text(encoding="utf-8")
|
||||
page = PAGE.read_text(encoding="utf-8")
|
||||
page = _home_surface()
|
||||
|
||||
for token in (
|
||||
"create table if not exists public.chart_profiles",
|
||||
@@ -190,7 +228,7 @@ def test_chart_profile_library_has_cloud_table_api_and_local_fallback() -> None:
|
||||
"jyotisha_chart_library",
|
||||
"保存失败,请重试",
|
||||
"星盘库",
|
||||
"添加其他星盘",
|
||||
"添加其他人的星盘",
|
||||
"用于合盘",
|
||||
"设为默认",
|
||||
):
|
||||
@@ -208,8 +246,8 @@ def test_synastry_route_orchestrates_python_chart_and_ashtakoot() -> None:
|
||||
route = SYNASTRY_ROUTE.read_text(encoding="utf-8")
|
||||
for token in (
|
||||
'const apiBase = process.env.JYOTISH_API_BASE ?? "http://127.0.0.1:5200"',
|
||||
'postPython("/api/chart", birthPayload(body.selfProfile))',
|
||||
'postPython("/api/chart", birthPayload(body.partnerProfile))',
|
||||
'postPython("/api/chart", selfPayload)',
|
||||
'postPython("/api/chart", partnerPayload)',
|
||||
'postPython("/api/varga_full"',
|
||||
'postPython("/api/synastry"',
|
||||
"moonLongitude(selfChart)",
|
||||
@@ -221,6 +259,7 @@ def test_synastry_route_orchestrates_python_chart_and_ashtakoot() -> None:
|
||||
"ashtakoot_plus_moon_nakshatra_d9",
|
||||
'evidenceLayers: ["ashtakoot", "moon_nakshatra", "d9_navamsa"]',
|
||||
'status: "blocked"',
|
||||
"synastryBirthPayload",
|
||||
):
|
||||
assert token in route
|
||||
|
||||
@@ -228,7 +267,7 @@ def test_synastry_route_orchestrates_python_chart_and_ashtakoot() -> None:
|
||||
def test_synastry_reports_are_cloud_persisted_per_user() -> None:
|
||||
sql = re.sub(r"\s+", " ", SYNASTRY_REPORT_MIGRATION.read_text(encoding="utf-8").lower()).strip()
|
||||
route = SYNASTRY_REPORT_ROUTE.read_text(encoding="utf-8")
|
||||
page = PAGE.read_text(encoding="utf-8")
|
||||
page = _home_surface()
|
||||
|
||||
for token in (
|
||||
"create table if not exists public.synastry_reports",
|
||||
@@ -290,7 +329,7 @@ def test_consultation_credit_lifecycle_is_idempotent_and_server_only() -> None:
|
||||
|
||||
def test_profile_coordinates_are_persisted_with_database_bounds() -> None:
|
||||
sql = re.sub(r"\s+", " ", COORDS_MIGRATION.read_text(encoding="utf-8").lower()).strip()
|
||||
source = PAGE.read_text(encoding="utf-8")
|
||||
source = _home_surface()
|
||||
|
||||
for definition in (
|
||||
"latitude double precision",
|
||||
|
||||
Reference in New Issue
Block a user