Files
Jyotisha/frontend/tests/session-list-provider.test.ts
T
jesse-uxandGrok (agent loop) 352d45b2ef
Independent Staging Quality Gate / validate (push) Failing after 9m12s
Independent Staging Quality Gate / publish (push) Skipped
外壳换成组件库:侧栏可拖宽,会话可排序(#105)。
桌面侧栏可在 200 到 360 像素之间拖宽,拖到最左收起,宽度记在本机。手机从左边滑出。会话在同一组里拖动排序,顺序写入可空的 sidebar_order,收藏仍置顶。账户菜单、设置、反馈和退出确认改用组件库。顶栏点数只显示图标和数字。启程清单还在。菜单、拖宽、拖动排序和翻页在首屏之后再载入。

自测:tsc 退出码 0;npm run lint 0 error、121 warning;改到的前端测试 205 项全过;npm run build 后首页仍是 static;首屏 gzip 630350,相对 8f7c0f7d 的 621226 为 +1.47%。test:db 92 项里 79 过、13 败,失败名与 Windows 已知清单一致,排序列在通过的 ok 37。没有跑全量 npm test,没有跑 Python 快速门。没有登录态,没有在浏览器里拖侧栏。

Co-Authored-By: Grok (agent loop)
2026-10-25 01:41:19 +08:00

79 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
const provider = readFileSync(new URL("../src/lib/session-list-context.tsx", import.meta.url), "utf8");
const accountRead = readFileSync(new URL("../src/lib/current-subject.ts", import.meta.url), "utf8");
const bootReads = readFileSync(new URL("../src/lib/boot-early-reads.ts", import.meta.url), "utf8");
const layout = readFileSync(new URL("../src/app/(app)/layout.tsx", import.meta.url), "utf8");
const page = readFileSync(new URL("../src/app/(app)/page.tsx", import.meta.url), "utf8");
const sessionHook = readFileSync(new URL("../src/hooks/use-session-management.ts", import.meta.url), "utf8");
test("the app layout owns one SessionListProvider and one SidebarProvider", () => {
assert.match(layout, /<SessionListProvider>/);
assert.equal(layout.match(/<SessionListProvider>/g)?.length, 1);
assert.match(layout, /<SidebarProvider escapeBlocked=\{registration\?\.escapeBlocked \?\? false\}>/);
assert.equal(layout.match(/<SidebarProvider/g)?.length, 1);
assert.match(layout, /<AppSidebar/);
assert.doesNotMatch(page, /<SidebarProvider/);
assert.doesNotMatch(page, /<AppSidebar/);
});
test("the provider fetches the session list once and home does not fetch it again", () => {
// 原值:provider 里内联 fetch(`/api/sessions?limit=${SESSION_PAGE_SIZE}&subject=${encodeURIComponent(subjectId)}`)。
// 新值:provider 用 fetchSessionPage → fetch(bootSessionsPath(subjectId), …),路径由 boot-early-reads.ts 生成。
// 原因:首页 HTML 的早发请求(TASK-home-first-load T8)与正常请求必须同一路径,共用一个常量防漂移。
assert.match(provider, /fetch\(bootSessionsPath\(subjectId\), \{ \.\.\.BOOT_READ_INIT, signal \}\)/);
assert.match(bootReads, /`\/api\/sessions\?limit=\$\{SESSION_PAGE_SIZE\}&subject=\$\{encodeURIComponent\(subjectId\)\}`/);
// 原值:provider 自己 fetch("/api/account")。
// 新值:provider 调用 readBootAccount,fetch 在 current-subject.ts,同一次加载只打一次。
// 原因:人物目录和会话列表要共享账户结果。
assert.match(provider, /readBootAccount\(\)/);
// 原值:accountRead 里 fetch("/api/account"。
// 新值:fetch(BOOT_ACCOUNT_PATH, BOOT_READ_INIT),常量 BOOT_ACCOUNT_PATH = "/api/account" 在 boot-early-reads.ts。
// 原因:同上,早发请求与正常请求共用常量;仍只在 readBootAccount 一处发。
assert.match(accountRead, /fetch\(BOOT_ACCOUNT_PATH, BOOT_READ_INIT\)/);
assert.match(bootReads, /BOOT_ACCOUNT_PATH = "\/api\/account"/);
// 原值: page.tsx 里 `await sessionListReady;`
// 新值: page.tsx 把 sessionListReady 交给 runHomeBootstrap,函数里 `await deps.sessionListReady`。
// 原因: 启动流程搬出首页后,会话列表仍只由 provider 拉一次。
const bootstrapRun = readFileSync(new URL("../src/lib/home-bootstrap-run.ts", import.meta.url), "utf8");
assert.match(page, /sessionListReady,/);
// 原值 / 新值 / 原因:固定 ready promise / 当前 reload gate / 切人期间回 Home 必须等新绑定,不能读空 boot 误登出。
assert.match(bootstrapRun, /await \(deps\.waitForSessionList\?\.\(\) \?\? deps\.sessionListReady\);/);
assert.match(page, /waitForSessionList,/);
assert.doesNotMatch(page, /fetchSessions\(/);
assert.doesNotMatch(page, /fetchAccount\(/);
assert.match(page, /useHomeShellRegistration\(/);
});
test("a 401 clears the list and marks signedOut", () => {
// 原值:provider 里 accountResponse.status === 401。
// 新值:readBootAccount 返回 status 401,provider 写成 accountResult.status === 401,仍 signedOut。
// 原因:账户读取挪到共享函数后,401 仍先于会话请求结束,不串第二次账户请求。
assert.match(provider, /accountResult\.status === 401/);
assert.match(accountRead, /response\.status === 401/);
assert.match(provider, /sessionResponse\.status === 401/);
assert.match(provider, /signedOut: true/);
assert.doesNotMatch(provider, /localStorage|sessionStorage|document\.cookie|indexedDB/i);
});
test("session writes no longer invalidate a second cache", () => {
assert.doesNotMatch(sessionHook, /invalidateSidebarCache/);
});
test("only the shell consumes registration and it passes Home children through unchanged", () => {
assert.match(layout, /const registration = useShellRegistration\(\)/);
assert.match(layout, /sessionListSidebarModel\(list, registration\)/);
assert.doesNotMatch(layout, /list\.registration|cloneElement/);
// 原值:SidebarInset 直接包 `{children}`
// 新值:SidebarInset 里是 PageSwitch,再原样放 `{children}`
// 原因:K1 页面切换包在外壳上。Home 的孩子没有被 clone,内容不变
assert.match(layout, /<SidebarInset[\s\S]*>[\s\S]*<PageSwitch[\s\S]*\{children\}[\s\S]*<\/PageSwitch>\s*<\/SidebarInset>/);
assert.doesNotMatch(page, /useShellRegistration/);
const dataValue = provider.slice(provider.indexOf("const value = useMemo"), provider.indexOf("return (", provider.indexOf("const value = useMemo")));
assert.doesNotMatch(dataValue, /\bregistration\b/);
assert.match(dataValue, /registerShellControls/);
assert.match(provider, /<ShellRegistrationContext\.Provider value=\{registration\}>\s*\{children\}/);
});