返回 DeepSeek-Reasonix
session-recovery-versions.test.ts
根目录 / desktop / frontend / src / __tests__ / session-recovery-versions.test.ts
1 import assert from "node:assert/strict";
2 import {
3 normalizeRecoveryLineageView,
4 pendingRecoveryMatchesRoots,
5 pendingSessionRecovery,
6 recoveryLineageResolution,
7 sanitizedRecoveryReason,
8 SessionRecoveryDivergenceTracker,
9 userVisibleRecoveryVersions,
10 } from "../lib/sessionRecoveryVersions";
11 import type { RecoveryLineageView, SessionRecoveryEvent } from "../lib/types";
12
13 const view = (overrides: Partial<RecoveryLineageView> = {}): RecoveryLineageView => ({
14 groupId: "group",
15 state: "diverged",
16 branchCount: 3,
17 unresolved: 1,
18 cleanupEligible: 1,
19 members: [
20 { path: "/s/root.jsonl", role: "normal", canonical: true, turns: 3, open: true, running: false },
21 { path: "/s/copy.jsonl", role: "covered_copy", canonical: false, turns: 3, open: false, running: false },
22 { path: "/s/fork.jsonl", role: "diverged", canonical: false, turns: 4, open: false, running: false },
23 ],
24 ...overrides,
25 });
26
27 assert.deepEqual(normalizeRecoveryLineageView({ members: null }).members, [], "null bridge arrays normalize to []");
28 assert.equal(userVisibleRecoveryVersions(view()).length, 2, "covered copies never enter the user-facing version list");
29 assert.equal(userVisibleRecoveryVersions(view({ members: [
30 { path: "/s/root.jsonl", role: "normal", versionKind: "normal", canonical: true, turns: 3, open: true, running: false },
31 { path: "/s/child.jsonl", role: "diverged", versionKind: "subagent", canonical: false, turns: 1, open: false, running: false },
32 ] })).length, 1, "subagent transcripts never appear as recovery versions");
33 assert.equal(recoveryLineageResolution(view()), "notify", "confirmed unique divergence notifies");
34 assert.equal(recoveryLineageResolution(view({ state: "repairing" })), "wait", "catalog rebuilding waits for a later revision");
35 assert.equal(recoveryLineageResolution(view({ state: "covered", unresolved: 0 })), "clear", "proven covered copies clear silently");
36 assert.equal(pendingRecoveryMatchesRoots({ eventKey: "e", topic: { scope: "project", workspaceRoot: "/a", topicId: "t" } }, ["/b"]), false, "unrelated catalog roots do not trigger lineage reads");
37 assert.equal(pendingRecoveryMatchesRoots({ eventKey: "e", topic: { scope: "global", topicId: "t" } }, [""]), true, "global catalog revisions match the global pending item");
38 assert.equal(pendingRecoveryMatchesRoots({ eventKey: "e", topic: { scope: "project", workspaceRoot: "/a", topicId: "t" } }, []), true, "rootless rebuild revisions recheck every pending item");
39 assert.equal(recoveryLineageResolution(view({ state: "adopted", unresolved: 0 })), "clear", "adopted lineages stay silent");
40 assert.equal(recoveryLineageResolution(view({ state: "preferred", unresolved: 0 })), "clear", "preferred lineages stay silent");
41 assert.equal(recoveryLineageResolution(view({ state: "", unresolved: 0 })), "clear", "classified covered lineages stay silent");
42 assert.equal(recoveryLineageResolution(view({
43 branchCount: 1,
44 unresolved: 1,
45 cleanupEligible: 0,
46 members: [{ path: "/s/only.jsonl", role: "diverged", canonical: true, turns: 4, open: false, running: false }],
47 })), "clear", "a unique rootless recovery version clears instead of leaking a pending check");
48
49 const event: SessionRecoveryEvent = {
50 recoveryPath: "/s/fork.jsonl",
51 scope: "project",
52 workspaceRoot: "/work",
53 topicId: "topic",
54 recoveryParentId: "root",
55 recoveryReason: "snapshot_conflict",
56 };
57 const pending = pendingSessionRecovery(event);
58 assert.ok(pending, "valid recovery events produce a pending catalog check");
59 assert.equal(pending?.topic.path, event.recoveryPath, "pending checks bind lineage reads to the recovered physical group");
60 assert.equal(pendingSessionRecovery({ recoveryPath: "/s/fork.jsonl" }), null, "legacy events without a topic cannot be misrouted");
61 assert.equal(sanitizedRecoveryReason("snapshot_conflict"), "snapshot_conflict");
62 assert.equal(sanitizedRecoveryReason("shutdown file lock"), "shutdown_lock");
63 assert.equal(sanitizedRecoveryReason("private arbitrary reason"), "other", "diagnostics do not retain arbitrary backend text");
64
65 const tracker = new SessionRecoveryDivergenceTracker();
66 const first = tracker.register(event);
67 assert.equal(first.isNew, true);
68 assert.equal(first.occurrence, 1);
69 assert.equal(tracker.resolve(first.pending!.eventKey, view({ state: "repairing" })), "wait", "out-of-order recovery remains pending");
70 assert.equal(tracker.entries().length, 1);
71 assert.equal(tracker.resolve(first.pending!.eventKey, view()), "notify", "later catalog revision confirms the divergence");
72 assert.equal(tracker.entries().length, 0);
73 assert.equal(tracker.register(event).isNew, false, "duplicate delivery after notification is suppressed");
74
75 const second = tracker.register({ ...event, recoveryPath: "/s/fork-2.jsonl" });
76 assert.equal(second.occurrence, 2, "a later distinct recovery on the same topic is counted separately");
77 assert.equal(tracker.resolve(second.pending!.eventKey, view({ state: "adopted", unresolved: 0 })), "clear");
78
79 console.log(" PASS session recovery divergence state machine");
80
80 lines TYPESCRIPT