返回 DeepSeek-Reasonix
query-coalesce.test.ts
根目录 / desktop / frontend / src / __tests__ / query-coalesce.test.ts
1 import assert from "node:assert/strict";
2 import { coalescesQuery, invalidateSharedQuery, maybeShare, resetQueryCoalescing, shareQuery } from "../lib/queryCoalesce";
3
4 function deferred<T>() {
5 let resolve!: (v: T) => void;
6 let reject!: (e: unknown) => void;
7 const promise = new Promise<T>((res, rej) => { resolve = res; reject = rej; });
8 return { promise, resolve, reject };
9 }
10
11 async function identicalCallsInFlightShareOneAnswer() {
12 resetQueryCoalescing();
13 let runs = 0;
14 const d = deferred<string>();
15 const run = () => { runs++; return d.promise; };
16 const a = shareQuery("ContextPanel", ["tab-1"], run);
17 const b = shareQuery("ContextPanel", ["tab-1"], run);
18 d.resolve("panel");
19 assert.equal(await a, "panel");
20 assert.equal(await b, "panel");
21 assert.equal(runs, 1, "a duplicate inside the window must not reach the backend");
22 }
23
24 async function differentArgumentsStayIndependent() {
25 resetQueryCoalescing();
26 let runs = 0;
27 const run = async () => { runs++; return runs; };
28 await shareQuery("MetaForTab", ["tab-1"], run);
29 await shareQuery("MetaForTab", ["tab-2"], run);
30 assert.equal(runs, 2, "another tab is another question");
31 }
32
33 async function mutationsInvalidateInflightAnswers() {
34 resetQueryCoalescing();
35 const stale = deferred<string>();
36 const first = shareQuery("MetaForTab", ["tab-1"], () => stale.promise);
37 await maybeShare("NewSessionForTab", ["tab-1"], async () => undefined);
38 const fresh = await shareQuery("MetaForTab", ["tab-1"], async () => "fresh");
39 assert.equal(fresh, "fresh", "a post-mutation query must not inherit the old session request");
40 stale.resolve("stale");
41 assert.equal(await first, "stale");
42 }
43
44 // A stale answer is worse than a slow one: a failure must not be inherited.
45 async function rejectionIsNotSharedOnward() {
46 resetQueryCoalescing();
47 let runs = 0;
48 const first = shareQuery("ListTabs", [], async () => { runs++; throw new Error("boom"); });
49 await assert.rejects(first);
50 const second = await shareQuery("ListTabs", [], async () => { runs++; return "ok"; });
51 assert.equal(second, "ok");
52 assert.equal(runs, 2, "the next caller retries instead of inheriting the failure");
53 }
54
55 async function invalidationStartsAFreshRead() {
56 resetQueryCoalescing();
57 let runs = 0;
58 const run = async () => ++runs;
59 assert.equal(await shareQuery("MetaForTab", ["tab-1"], run), 1);
60 invalidateSharedQuery("MetaForTab", ["tab-1"]);
61 assert.equal(await shareQuery("MetaForTab", ["tab-1"], run), 2);
62 }
63
64 async function settledTabListsAreNotShared() {
65 resetQueryCoalescing();
66 let runs = 0;
67 const run = async () => ++runs;
68 assert.equal(await shareQuery("ListTabs", [], run), 1);
69 assert.equal(await shareQuery("ListTabs", [], run), 2, "tab mutations must be visible after the in-flight burst");
70 }
71
72 // Anything that mutates or starts work must never be collapsed.
73 function onlyReadOnlyQueriesAreCoalesced() {
74 for (const readOnly of ["ListProjectTree", "ContextPanel", "MetaForTab", "BalanceForTab"]) {
75 assert.equal(coalescesQuery(readOnly), true, `${readOnly} should coalesce`);
76 }
77 for (const mutating of ["Send", "SetActiveTab", "OpenGlobalTab", "Cancel", "ApproveTool"]) {
78 assert.equal(coalescesQuery(mutating), false, `${mutating} must never be coalesced`);
79 }
80 }
81
82 const tests: Array<[string, () => unknown]> = [
83 ["identical in-flight calls share one answer", identicalCallsInFlightShareOneAnswer],
84 ["different arguments stay independent", differentArgumentsStayIndependent],
85 ["mutations invalidate in-flight answers", mutationsInvalidateInflightAnswers],
86 ["a rejection is not shared onward", rejectionIsNotSharedOnward],
87 ["external state boundaries invalidate settled answers", invalidationStartsAFreshRead],
88 ["settled tab lists do not cross mutations", settledTabListsAreNotShared],
89 ["only read-only queries are coalesced", onlyReadOnlyQueriesAreCoalesced],
90 ];
91
92 let failed = 0;
93 for (const [name, fn] of tests) {
94 try {
95 await fn();
96 console.log(`ok ${name}`);
97 } catch (err) {
98 failed++;
99 console.error(`FAIL ${name}\n ${(err as Error).message}`);
100 }
101 }
102 if (failed > 0) process.exit(1);
103
103 lines TYPESCRIPT