refactor(chat): extract home helpers, chart library, and starter surfaces
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled

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:
Jesse_Chen
2026-09-01 22:58:02 +08:00
co-authored by Cursor
parent 8558900525
commit 54269fcfcc
32 changed files with 1987 additions and 1466 deletions
+54 -15
View File
@@ -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",