返回 DeepSeek-Reasonix
read-status-upsert.test.ts
根目录 / desktop / frontend / src / __tests__ / read-status-upsert.test.ts
1 import { initialState, reducer } from "../lib/useController";
2 import { applyReadStatusFrame, readStatusLabel } from "../lib/readStatus";
3 import { readPauseItem, upsertReadPause, type WireReadPause } from "../lib/readPause";
4
5 function equal(actual: unknown, expected: unknown, message: string) {
6 if (actual !== expected) throw new Error(`${message}: got ${String(actual)}, want ${String(expected)}`);
7 }
8
9 const frame = (seq: number, state: string, extra: Record<string, unknown> = {}) => ({
10 kind: "read_status" as const,
11 turnId: "turn-1",
12 readStatus: {
13 readId: "ir-1", seq, path: "/w/a.go", state, active: true,
14 covered: [[1, 10]] as [number, number][],
15 ...extra,
16 },
17 });
18
19 let s = reducer(initialState, { type: "event", e: frame(1, "needs_more") });
20 equal(Object.keys(s.readStatuses ?? {}).length, 1, "one logical read keeps one status entry");
21 s = reducer(s, { type: "event", e: frame(2, "needs_more") });
22 equal(Object.keys(s.readStatuses ?? {}).length, 1, "a later page upserts the same entry");
23 equal(s.readStatuses?.["ir-1"]?.seq, 2, "the newest sequence wins");
24
25 const stale = reducer(s, { type: "event", e: frame(1, "satisfied", { active: false }) });
26 equal(stale.readStatuses?.["ir-1"]?.state, "needs_more", "a re-ordered frame never moves a read backwards");
27
28 s = reducer(s, {
29 type: "event",
30 e: { kind: "read_status", turnId: "turn-1", readStatus: { readId: "ir-2", seq: 1, path: "/w/b.go", state: "needs_more", active: true } },
31 });
32 equal(Object.keys(s.readStatuses ?? {}).length, 2, "independent reads keep independent entries");
33
34 const next = reducer(s, { type: "event", e: { kind: "turn_started", turnId: "turn-2", status: "in_progress" } });
35 equal(next.readStatuses, undefined, "a new turn starts from no live read status");
36
37 // A hundred pages of one read still leave exactly one status entry.
38 let many = initialState;
39 for (let page = 1; page <= 100; page++) {
40 many = reducer(many, { type: "event", e: frame(page, "needs_more", { covered: [[1, page * 10]] as [number, number][] }) });
41 }
42 equal(Object.keys(many.readStatuses ?? {}).length, 1, "a hundred pages still render one status");
43 equal(many.readStatuses?.["ir-1"]?.seq, 100, "the latest page wins");
44
45 console.log("read status upsert tests passed");
46
47 const current = { readId: "r", generation: 2, seq: 1, path: "a.go", state: "needs_more", active: true };
48 const scoped = { readStatuses: { r: current } };
49 equal(applyReadStatusFrame(scoped, { ...current, generation: 1, seq: 99 }).readStatuses.r.generation, 2, "old generation cannot overwrite new state");
50 equal(applyReadStatusFrame(scoped, { ...current, generation: 3, seq: 0 }).readStatuses.r.generation, 3, "new generation may restart its sequence");
51 const label = readStatusLabel({ r: { ...current, hasMore: true, covered: [[0, 10], [100, 110]] } }, (_key, vars) => String(vars?.range));
52 equal(label, "1–10, 101–110", "coverage gaps are not displayed as read");
53 const paused = readStatusLabel({ r: { ...current, state: "blocked", reason: "no_progress" } }, (key) => key);
54 equal(paused.includes("composer.readStatusStalled"), true, "pause explains the cause");
55 equal(paused.includes("composer.readStatusRecovery"), true, "pause gives an action");
56 const started = reducer(initialState, { type: "event", e: { kind: "turn_started", turnId: "current", status: "in_progress" } });
57 const live = reducer(started, { type: "event", e: { ...frame(1, "needs_more"), turnId: "current" } });
58 equal(reducer(live, { type: "event", e: { ...frame(99, "blocked"), turnId: "old" } }).readStatuses?.["ir-1"].seq, 1, "another turn cannot update read status");
59 const done = reducer(live, { type: "event", e: { kind: "turn_done", turnId: "current" } });
60 equal(done.readStatuses, undefined, "completion clears live status");
61 equal(reducer(done, { type: "event", e: { ...frame(99, "blocked"), turnId: "current" } }).readStatuses, undefined, "late result cannot revive a completed turn");
62
63 const receipt: WireReadPause = { id: "receipt", reads: [{ readId: "r", path: "file", reason: "page_budget", covered: [[0, 10]], missing: [[10, 20]] }] };
64 const readDone = reducer(live, { type: "event", e: { kind: "turn_done", turnId: "current", outcome: "incomplete_read", readPause: receipt } });
65 equal(readDone.turnActive, false, "a read pause releases the composer");
66 equal(readDone.readStatuses, undefined, "live progress is cleared after retaining the receipt");
67 equal(readDone.items.filter(it => it.kind === "notice" && it.code === "incomplete_read").length, 1, "one durable read pause");
68 equal(upsertReadPause(readDone.items, receipt, "unused").length, readDone.items.length, "replayed pause is idempotent");
69 const historyNotice = readPauseItem(receipt, "unused");
70 equal(historyNotice.kind === "notice" && historyNotice.text.includes("budget"), true, "pause cause is visible without expanding details");
71 const liveNotice = readDone.items.find(it => it.id === historyNotice.id)!;
72 const { turnId: noticeTurnId, ...presentation } = liveNotice;
73 equal(noticeTurnId, "current", "live receipt retains the explicit event turn for pending-bubble positioning");
74 equal(JSON.stringify(presentation), JSON.stringify(historyNotice), "live and history use identical presentation");
75 const nextTurn = reducer(readDone, { type: "event", e: { kind: "turn_started", turnId: "next", status: "in_progress" } });
76 equal(reducer(nextTurn, { type: "event", e: { kind: "turn_done", turnId: "current", outcome: "incomplete_read", readPause: receipt } }), nextTurn, "stale terminal result cannot stop a new turn");
77
77 lines TYPESCRIPT