bf6989eccf
Close the home-page split by moving account/onboarding and rectification glue out of page.tsx without relocating React state, keeping the route static and hook order intact. Co-authored-by: Cursor <cursoragent@cursor.com>
90 lines
3.7 KiB
Python
90 lines
3.7 KiB
Python
from pathlib import Path
|
|
|
|
|
|
PAGE = Path("frontend/src/app/page.tsx")
|
|
_FRONTEND_SRC = Path("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",
|
|
)
|
|
OPTIONAL_HOME_HOOKS = (
|
|
_FRONTEND_SRC / "hooks" / "use-session-management.ts",
|
|
_FRONTEND_SRC / "hooks" / "use-consultation-run.ts",
|
|
_FRONTEND_SRC / "hooks" / "use-profile-onboarding.ts",
|
|
_FRONTEND_SRC / "hooks" / "use-rectification-surface.ts",
|
|
)
|
|
STYLES = Path("frontend/src/app/globals.css")
|
|
SESSION_ROW = Path("frontend/src/components/sidebar-session-row.tsx")
|
|
SESSION_DELETE_ROUTE = Path("frontend/src/app/api/sessions/[id]/route.ts")
|
|
SESSION_DELETE_MIGRATION = Path("frontend/supabase/migrations/20260721100000_chat_sessions_delete_grant.sql")
|
|
SESSION_WRITE_CONTRACT = Path("frontend/src/lib/chat-session-write-contract.ts")
|
|
|
|
|
|
def _home_surface() -> str:
|
|
parts = [path.read_text(encoding="utf-8") for path in HOME_SURFACE_FILES]
|
|
parts.extend(path.read_text(encoding="utf-8") for path in OPTIONAL_HOME_HOOKS if path.exists())
|
|
return "".join(parts)
|
|
|
|
|
|
def test_chat_history_management_actions_are_exposed() -> None:
|
|
source = _home_surface() + STYLES.read_text(encoding="utf-8") + SESSION_ROW.read_text(encoding="utf-8")
|
|
for expected in (
|
|
"renameSession",
|
|
"deleteSession",
|
|
"pendingSessionDeletion",
|
|
"确认删除",
|
|
"session-delete-overlay",
|
|
"togglePinnedSession",
|
|
"toggleArchivedSession",
|
|
"showArchivedSessions",
|
|
"已归档,可在左侧归档中恢复。",
|
|
"shareSession",
|
|
"share_payload_version",
|
|
"messages.map",
|
|
"onContextMenu",
|
|
"session-menu-trigger",
|
|
"Escape",
|
|
"focus-within",
|
|
"收藏",
|
|
"重命名",
|
|
"归档",
|
|
"恢复",
|
|
"删除",
|
|
"转发",
|
|
'fetch(`/api/sessions/${encodeURIComponent(session.id)}`',
|
|
):
|
|
assert expected in source
|
|
|
|
|
|
def test_chat_session_delete_is_server_controlled_and_granted() -> None:
|
|
route = SESSION_DELETE_ROUTE.read_text(encoding="utf-8")
|
|
migration = SESSION_DELETE_MIGRATION.read_text(encoding="utf-8")
|
|
assert 'from("chat_sessions")' in route
|
|
assert '.eq("user_id", user.id)' in route
|
|
assert 'count !== 1' in route
|
|
assert 'grant delete on table public.chat_sessions to authenticated' in migration.lower()
|
|
assert 'create policy chat_sessions_delete_own' in migration.lower()
|
|
assert 'using ((select auth.uid()) = user_id)' in migration.lower()
|
|
|
|
|
|
def test_archiving_never_calls_the_delete_endpoint() -> None:
|
|
source = _home_surface()
|
|
write_contract = SESSION_WRITE_CONTRACT.read_text(encoding="utf-8")
|
|
start = source.index("function toggleArchivedSession")
|
|
end = source.index("async function shareSession", start)
|
|
archive_action = source[start:end]
|
|
# Former tokens: setArchivedSessionIds and no `/api/sessions/` in the slice.
|
|
# Archive is now a metadata PATCH; the write helper uses PATCH for update.
|
|
assert 'writeChatSession(sessionId, { archived_at: nextArchivedAt }, "update")' in archive_action
|
|
assert "archived_at: nextArchivedAt" in archive_action
|
|
assert 'method: "DELETE"' not in archive_action
|
|
assert "DELETE" not in archive_action
|
|
assert 'method: mode === "create" ? "POST" : "PATCH"' in write_contract
|