返回 DeepSeek-Reasonix
hydrate-failure-attribution.test.ts
根目录 / desktop / frontend / src / __tests__ / hydrate-failure-attribution.test.ts
1 // Run: tsx src/__tests__/hydrate-failure-attribution.test.ts
2 import assert from "node:assert/strict";
3 import { readFileSync } from "node:fs";
4 import { dirname, join } from "node:path";
5 import test from "node:test";
6 import { fileURLToPath } from "node:url";
7 import type { FollowRequest, TranscriptFollowResponse } from "../generated/desktopContract.generated";
8 import { hydrateFailureDetail } from "../lib/hydrateErrorState";
9 import { projectSessionAvailability } from "../lib/sessionAvailability";
10 import { TranscriptFollowClient } from "../lib/transcriptFollowClient";
11 import { initialState, reducer } from "../lib/useController";
12
13 const SUMMARY = "Failed to load conversation history.";
14
15 async function localFollowFailure(cause: unknown): Promise<unknown> {
16 const client = new TranscriptFollowClient((request: FollowRequest) =>
17 request.close ? Promise.resolve({ protocolVersion: 2, subscription: "", changes: [], resetRequired: false } as TranscriptFollowResponse)
18 : Promise.reject(cause), { transport: "local" });
19 try {
20 await client.start({ install: () => {}, changes: () => {}, connection: () => {} });
21 } catch (error) {
22 return error;
23 }
24 assert.fail("follow start resolved although the host rejected the baseline read");
25 }
26
27 test("a failed local follow keeps its stage, reason and host cause in the recovery detail", async () => {
28 const failure = await localFollowFailure(new Error("session store: database is locked"));
29 const detail = hydrateFailureDetail(SUMMARY, failure);
30 assert.ok(detail.startsWith(SUMMARY), detail);
31 assert.match(detail, /baseline_read\.transport_rejected/);
32 assert.match(detail, /session store: database is locked/);
33
34 const failed = reducer({ ...initialState, hydrating: true }, { type: "hydrate_error", reason: "startup", error: detail });
35 const availability = projectSessionAvailability({ local: failed });
36 assert.deepEqual({ kind: availability.kind, source: availability.source }, { kind: "error", source: "history" });
37 assert.equal(availability.detail, detail, "the banner's details show the cause, not only the summary");
38 });
39
40 test("causes without a follow identity keep their message; an absent cause keeps the summary alone", () => {
41 assert.equal(hydrateFailureDetail(SUMMARY, new Error("resident snapshot unreadable")), `${SUMMARY}\nresident snapshot unreadable`);
42 assert.equal(hydrateFailureDetail(SUMMARY, "plain rejection"), `${SUMMARY}\nplain rejection`);
43 assert.equal(hydrateFailureDetail(SUMMARY, undefined), SUMMARY);
44 assert.equal(hydrateFailureDetail(SUMMARY, new Error(" ")), SUMMARY);
45 });
46
47 test("every local history-read failure hands its cause to the recovery detail", () => {
48 const root = join(dirname(fileURLToPath(import.meta.url)), "..");
49 const controller = readFileSync(join(root, "lib/useController.ts"), "utf8");
50 assert.doesNotMatch(controller, /type: "hydrate_error", reason, error: t\("history\.failedLoadHistory"\) \}/,
51 "a history-read failure must not replace its cause with the generic summary");
52 assert.doesNotMatch(controller, /const error = t\("history\.failedLoadHistory"\);/,
53 "the follow failure must not be replaced by the generic summary");
54 assert.equal(controller.match(/hydrateFailureDetail\(t\("history\.failedLoadHistory"\), /g)?.length, 2,
55 "both the follow path and the readable-baseline path attribute their failure");
56 });
57
57 lines TYPESCRIPT