Files
Jyotisha/frontend/tests/sidebar-data-cache.test.ts
T
Jesse_ChenandClaude Fable 5.1 fb77c86585 test(ui): 侧栏只读模式、共享外壳与列表缓存的合同回归
新增 8 条:`sidebar-data-cache.test.ts`(命中不重拉 / 过期重拉一次 / 写操作后
拿到新标题并逐条锁住五个写路径 / 双账户不串 / 只存内存 / 401 清空)、
`sidebar-state.test.ts` +2(收起后重挂仍收起,含三种降级;移动端不读不写)、
`sidebar-contract.test.ts` +1(只读模式只少三样)、`chart-page-view.test.tsx` +1
(`(secondary)` layout 恰好挂一份只读侧栏,数据 hook 无写方法)。

改写 9 处既有断言,每处带「原值 / 新值 / 原因」三栏注释,均未削弱:
`window.location.assign(path)` → `<SidebarMenuLink href>` 并追加反向断言;
`onOpenReports` / `useRouter` 改成 doesNotMatch;`SecondaryShell` → `SecondaryHeader`;
导航顺序改在 `NAV_PAGES` 常量里量;两个 render 辅助改为裹 `SidebarProvider`
(provider 上移到 layout);三处源码路径跟随路由组移动。

测试总数 3391 → 3399,失败清单与基线逐条一致(47 条均为无 Docker 的既有缺口)。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
2026-09-16 11:23:11 +00:00

100 lines
4.1 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import {
SIDEBAR_CACHE_TTL_MS,
invalidateSidebarCache,
readSidebarCache,
sidebarCacheIsFresh,
writeSidebarCache,
} from "../src/lib/sidebar-data-cache.ts";
const hookSource = readFileSync(new URL("../src/hooks/use-sidebar-data.ts", import.meta.url), "utf8");
const sessionHookSource = readFileSync(new URL("../src/hooks/use-session-management.ts", import.meta.url), "utf8");
const row = (id: string, title: string) => ({
id,
title,
pinned: false,
archived: false,
updatedAt: 1,
});
const account = {
name: "示例账户",
email: "example@invalid.test",
credits: 0,
initial: "示",
avatar: null,
};
test("the read-only sidebar reuses a fresh entry instead of fetching again", () => {
invalidateSidebarCache();
assert.equal(readSidebarCache(), null);
writeSidebarCache({ accountId: "acct-1", sessions: [row("a", "第一条")], account, fetchedAt: 1_000 });
const entry = readSidebarCache();
assert.ok(entry);
assert.equal(entry.sessions[0]?.title, "第一条");
// Inside the window: the hook returns without touching the network.
assert.equal(sidebarCacheIsFresh(entry, 1_000), true);
assert.equal(sidebarCacheIsFresh(entry, 1_000 + SIDEBAR_CACHE_TTL_MS - 1), true);
// Past it: shown from cache first, refreshed in the background — exactly one
// refetch, because the write that follows resets `fetchedAt`.
assert.equal(sidebarCacheIsFresh(entry, 1_000 + SIDEBAR_CACHE_TTL_MS), false);
assert.equal(sidebarCacheIsFresh(entry, 60_000_000), false);
assert.equal(sidebarCacheIsFresh(null, 1_000), false);
// A clock that jumped backwards must not read as fresh forever.
assert.equal(sidebarCacheIsFresh(entry, 0), false);
});
test("a session write on `/` makes the next secondary page read the new title", () => {
invalidateSidebarCache();
writeSidebarCache({ accountId: "acct-1", sessions: [row("a", "旧标题")], account, fetchedAt: 1_000 });
assert.equal(readSidebarCache()?.sessions[0]?.title, "旧标题");
invalidateSidebarCache();
assert.equal(readSidebarCache(), null);
assert.equal(sidebarCacheIsFresh(readSidebarCache(), 1_001), false);
// Every write path on `/` calls it: create, rename, delete, pin, archive.
for (const marker of [
/await persistSession\(nextSession\);\n\s*\/\*[\s\S]*?\*\/\n\s*invalidateSidebarCache\(\);/,
/删除聊天记录失败"\);\n\s*invalidateSidebarCache\(\);/,
/\{ pinned: nextPinned \}, "update"\)\.then\(invalidateSidebarCache\)/,
/\{ archived_at: nextArchivedAt \}, "update"\)\.then\(invalidateSidebarCache\)/,
/\);\n\s*invalidateSidebarCache\(\);\n\s*return nextSession;/,
]) {
assert.match(sessionHookSource, marker);
}
});
test("a second account in the same tab never reads the first one's rows", () => {
invalidateSidebarCache();
writeSidebarCache({ accountId: "acct-1", sessions: [row("a", "甲的对话")], account, fetchedAt: 1_000 });
writeSidebarCache({ accountId: "acct-2", sessions: [row("b", "乙的对话")], account, fetchedAt: 2_000 });
assert.equal(readSidebarCache()?.accountId, "acct-2");
assert.equal(readSidebarCache()?.sessions[0]?.title, "乙的对话");
invalidateSidebarCache();
});
test("the cache is memory only, and a 401 empties it", () => {
// Nothing durable: a stale list surviving a browser restart is worse than one
// fetch, and it would outlive a sign-out.
const cacheSource = readFileSync(new URL("../src/lib/sidebar-data-cache.ts", import.meta.url), "utf8");
assert.doesNotMatch(cacheSource, /localStorage|sessionStorage|document\.cookie|indexedDB/i);
assert.match(hookSource, /if \(sidebarCacheIsFresh\(cached, Date\.now\(\)\)\) return;/);
assert.match(hookSource, /invalidateSidebarCache\(\);\n\s*setState\(\{ sessions: \[\], account: null, settled: true, signedOut: true \}\);/);
// Read synchronously at mount so a cached list is on screen in the first
// frame — no skeleton, no spinner, per the unified-loading ruling.
assert.match(hookSource, /useState<SidebarDataState>\(\(\) => \{[\s\S]*?readSidebarCache\(\)/);
assert.doesNotMatch(hookSource, /skeleton|Spinner/i);
});