|
1 | | -import { ThreadId, type NativeApi } from "@okcode/contracts"; |
2 | | -import { QueryClient } from "@tanstack/react-query"; |
3 | | -import { afterEach, describe, expect, it, vi } from "vitest"; |
4 | | -import { checkpointDiffQueryOptions, providerQueryKeys } from "./providerReactQuery"; |
5 | | -import * as nativeApi from "../nativeApi"; |
| 1 | +import { describe, expect, it } from "vitest"; |
| 2 | +import { ThreadId } from "@okcode/contracts"; |
| 3 | +import { providerQueryKeys, checkpointDiffQueryOptions } from "./providerReactQuery"; |
6 | 4 |
|
7 | | -const threadId = ThreadId.makeUnsafe("thread-id"); |
8 | | - |
9 | | -function mockNativeApi(input: { |
10 | | - getTurnDiff: ReturnType<typeof vi.fn>; |
11 | | - getFullThreadDiff: ReturnType<typeof vi.fn>; |
12 | | -}) { |
13 | | - vi.spyOn(nativeApi, "ensureNativeApi").mockReturnValue({ |
14 | | - orchestration: { |
15 | | - getTurnDiff: input.getTurnDiff, |
16 | | - getFullThreadDiff: input.getFullThreadDiff, |
17 | | - }, |
18 | | - } as unknown as NativeApi); |
19 | | -} |
20 | | - |
21 | | -afterEach(() => { |
22 | | - vi.restoreAllMocks(); |
23 | | -}); |
| 5 | +const threadId = ThreadId.makeUnsafe("thread-1"); |
24 | 6 |
|
25 | 7 | describe("providerQueryKeys.checkpointDiff", () => { |
26 | | - it("includes cacheScope so reused turn counts do not collide", () => { |
27 | | - const baseInput = { |
| 8 | + it("distinguishes patch and full-context file queries", () => { |
| 9 | + const patchKey = providerQueryKeys.checkpointDiff({ |
28 | 10 | threadId, |
29 | 11 | fromTurnCount: 1, |
30 | 12 | toTurnCount: 2, |
31 | | - } as const; |
32 | | - |
33 | | - expect( |
34 | | - providerQueryKeys.checkpointDiff({ |
35 | | - ...baseInput, |
36 | | - cacheScope: "turn:old-turn", |
37 | | - }), |
38 | | - ).not.toEqual( |
39 | | - providerQueryKeys.checkpointDiff({ |
40 | | - ...baseInput, |
41 | | - cacheScope: "turn:new-turn", |
42 | | - }), |
43 | | - ); |
44 | | - }); |
45 | | -}); |
46 | | - |
47 | | -describe("checkpointDiffQueryOptions", () => { |
48 | | - it("forwards checkpoint range to the provider API", async () => { |
49 | | - const getTurnDiff = vi.fn().mockResolvedValue({ diff: "patch" }); |
50 | | - const getFullThreadDiff = vi.fn().mockResolvedValue({ diff: "patch" }); |
51 | | - mockNativeApi({ getTurnDiff, getFullThreadDiff }); |
52 | | - |
53 | | - const options = checkpointDiffQueryOptions({ |
54 | | - threadId, |
55 | | - fromTurnCount: 3, |
56 | | - toTurnCount: 4, |
57 | | - cacheScope: "turn:abc", |
58 | | - }); |
59 | | - |
60 | | - const queryClient = new QueryClient(); |
61 | | - await queryClient.fetchQuery(options); |
62 | | - |
63 | | - expect(getTurnDiff).toHaveBeenCalledWith({ |
64 | | - threadId, |
65 | | - fromTurnCount: 3, |
66 | | - toTurnCount: 4, |
67 | | - }); |
68 | | - expect(getFullThreadDiff).not.toHaveBeenCalled(); |
69 | | - }); |
70 | | - |
71 | | - it("uses explicit full thread diff API when range starts from zero", async () => { |
72 | | - const getTurnDiff = vi.fn().mockResolvedValue({ diff: "patch" }); |
73 | | - const getFullThreadDiff = vi.fn().mockResolvedValue({ diff: "patch" }); |
74 | | - mockNativeApi({ getTurnDiff, getFullThreadDiff }); |
75 | | - |
76 | | - const options = checkpointDiffQueryOptions({ |
77 | | - threadId, |
78 | | - fromTurnCount: 0, |
79 | | - toTurnCount: 2, |
80 | | - cacheScope: "thread:all", |
81 | | - }); |
82 | | - |
83 | | - const queryClient = new QueryClient(); |
84 | | - await queryClient.fetchQuery(options); |
85 | | - |
86 | | - expect(getFullThreadDiff).toHaveBeenCalledWith({ |
87 | | - threadId, |
88 | | - toTurnCount: 2, |
89 | | - }); |
90 | | - expect(getTurnDiff).not.toHaveBeenCalled(); |
91 | | - }); |
92 | | - |
93 | | - it("fails fast on invalid range and does not call provider RPC", async () => { |
94 | | - const getTurnDiff = vi.fn().mockResolvedValue({ diff: "patch" }); |
95 | | - const getFullThreadDiff = vi.fn().mockResolvedValue({ diff: "patch" }); |
96 | | - mockNativeApi({ getTurnDiff, getFullThreadDiff }); |
97 | | - |
98 | | - const options = checkpointDiffQueryOptions({ |
99 | | - threadId, |
100 | | - fromTurnCount: 4, |
101 | | - toTurnCount: 3, |
102 | | - cacheScope: "turn:invalid", |
| 13 | + relativePath: "src/a.ts", |
| 14 | + contextMode: "patch", |
103 | 15 | }); |
104 | | - |
105 | | - const queryClient = new QueryClient(); |
106 | | - |
107 | | - await expect(queryClient.fetchQuery(options)).rejects.toThrow( |
108 | | - "Checkpoint diff is unavailable.", |
109 | | - ); |
110 | | - expect(getTurnDiff).not.toHaveBeenCalled(); |
111 | | - expect(getFullThreadDiff).not.toHaveBeenCalled(); |
112 | | - }); |
113 | | - |
114 | | - it("retries checkpoint-not-ready errors longer than generic failures", () => { |
115 | | - const options = checkpointDiffQueryOptions({ |
| 16 | + const fullKey = providerQueryKeys.checkpointDiff({ |
116 | 17 | threadId, |
117 | 18 | fromTurnCount: 1, |
118 | 19 | toTurnCount: 2, |
119 | | - cacheScope: "turn:abc", |
| 20 | + relativePath: "src/a.ts", |
| 21 | + contextMode: "full", |
120 | 22 | }); |
121 | | - const retry = options.retry; |
122 | | - expect(typeof retry).toBe("function"); |
123 | | - if (typeof retry !== "function") { |
124 | | - throw new Error("Expected retry to be a function."); |
125 | | - } |
126 | 23 |
|
127 | | - expect(retry(1, new Error("Checkpoint turn count 2 exceeds current turn count 1."))).toBe(true); |
128 | | - expect( |
129 | | - retry(11, new Error("Filesystem checkpoint is unavailable for turn 2 in thread thread-1.")), |
130 | | - ).toBe(true); |
131 | | - expect( |
132 | | - retry(12, new Error("Filesystem checkpoint is unavailable for turn 2 in thread thread-1.")), |
133 | | - ).toBe(false); |
134 | | - expect(retry(2, new Error("Something else failed."))).toBe(true); |
135 | | - expect(retry(3, new Error("Something else failed."))).toBe(false); |
| 24 | + expect(patchKey).not.toEqual(fullKey); |
136 | 25 | }); |
| 26 | +}); |
137 | 27 |
|
138 | | - it("backs off longer for checkpoint-not-ready errors", () => { |
| 28 | +describe("checkpointDiffQueryOptions", () => { |
| 29 | + it("stays enabled for full-thread file-scoped full-context queries", () => { |
139 | 30 | const options = checkpointDiffQueryOptions({ |
140 | 31 | threadId, |
141 | | - fromTurnCount: 1, |
| 32 | + fromTurnCount: 0, |
142 | 33 | toTurnCount: 2, |
143 | | - cacheScope: "turn:abc", |
| 34 | + relativePath: "src/a.ts", |
| 35 | + contextMode: "full", |
144 | 36 | }); |
145 | | - const retryDelay = options.retryDelay; |
146 | | - expect(typeof retryDelay).toBe("function"); |
147 | | - if (typeof retryDelay !== "function") { |
148 | | - throw new Error("Expected retryDelay to be a function."); |
149 | | - } |
150 | 37 |
|
151 | | - const checkpointDelay = retryDelay( |
152 | | - 4, |
153 | | - new Error("Checkpoint turn count 2 exceeds current turn count 1."), |
| 38 | + expect(options.queryKey).toEqual( |
| 39 | + providerQueryKeys.checkpointDiff({ |
| 40 | + threadId, |
| 41 | + fromTurnCount: 0, |
| 42 | + toTurnCount: 2, |
| 43 | + relativePath: "src/a.ts", |
| 44 | + contextMode: "full", |
| 45 | + }), |
154 | 46 | ); |
155 | | - const genericDelay = retryDelay(4, new Error("Network failure")); |
156 | | - |
157 | | - expect(typeof checkpointDelay).toBe("number"); |
158 | | - expect(typeof genericDelay).toBe("number"); |
159 | | - expect((checkpointDelay ?? 0) > (genericDelay ?? 0)).toBe(true); |
| 47 | + expect(options.enabled).toBe(true); |
160 | 48 | }); |
161 | 49 | }); |
0 commit comments