Skip to content

Commit e1883fe

Browse files
committed
Collapse catalog tests into one parameterized harness
1 parent 4e331f5 commit e1883fe

3 files changed

Lines changed: 390 additions & 584 deletions

File tree

Lines changed: 390 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,390 @@
1+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2+
3+
import { OPENCODE_GO_MODEL_IDS } from "../../packages/opencode-go/src/index.js";
4+
import { ZEN_MODEL_IDS } from "../../packages/zen/src/index.js";
5+
import type { CatalogDiscoveryState } from "./bounded-model-catalog.js";
6+
import {
7+
discoverGoModels,
8+
discoverZenModels,
9+
MAX_GO_CATALOG_BYTES,
10+
MAX_GO_CATALOG_MODELS,
11+
MAX_ZEN_CATALOG_BYTES,
12+
MAX_ZEN_CATALOG_MODELS,
13+
prefetchGoModels,
14+
prefetchZenModels,
15+
resetGoModelDiscoveryForTests,
16+
resetZenModelDiscoveryForTests,
17+
selectableGoModelIds,
18+
selectableZenModelIds,
19+
} from "./model-catalogs.js";
20+
21+
const originalFetch = globalThis.fetch;
22+
23+
type CatalogHarness = {
24+
readonly modelsURL: string;
25+
readonly catalogLabel: string;
26+
readonly otherLabel: string;
27+
readonly seedIds: readonly string[];
28+
readonly maxBytes: number;
29+
readonly maxModels: number;
30+
readonly sampleId: string;
31+
readonly liveOnlyId: string;
32+
readonly modelPrefix: string;
33+
readonly discoverName: string;
34+
readonly prefetchName: string;
35+
readonly resetName: string;
36+
readonly discover: (args?: {
37+
timeoutMs?: number;
38+
signal?: AbortSignal;
39+
}) => Promise<CatalogDiscoveryState>;
40+
readonly selectable: () => readonly string[];
41+
readonly prefetch: () => Promise<readonly string[]>;
42+
readonly reset: () => void;
43+
};
44+
45+
const harnesses: readonly CatalogHarness[] = [
46+
{
47+
modelsURL: "https://opencode.ai/zen/go/v1/models",
48+
catalogLabel: "OpenCode Go",
49+
otherLabel: "OpenCode Zen",
50+
seedIds: OPENCODE_GO_MODEL_IDS,
51+
maxBytes: MAX_GO_CATALOG_BYTES,
52+
maxModels: MAX_GO_CATALOG_MODELS,
53+
sampleId: "grok-4.5",
54+
liveOnlyId: "live-only-fixture-model",
55+
modelPrefix: "go-model-",
56+
discoverName: "discoverGoModels",
57+
prefetchName: "prefetchGoModels",
58+
resetName: "resetGoModelDiscoveryForTests",
59+
discover: discoverGoModels,
60+
selectable: selectableGoModelIds,
61+
prefetch: prefetchGoModels,
62+
reset: resetGoModelDiscoveryForTests,
63+
},
64+
{
65+
modelsURL: "https://opencode.ai/zen/v1/models",
66+
catalogLabel: "OpenCode Zen",
67+
otherLabel: "OpenCode Go",
68+
seedIds: ZEN_MODEL_IDS,
69+
maxBytes: MAX_ZEN_CATALOG_BYTES,
70+
maxModels: MAX_ZEN_CATALOG_MODELS,
71+
sampleId: "gpt-6-astra",
72+
liveOnlyId: "live-only-zen-fixture-model",
73+
modelPrefix: "zen-model-",
74+
discoverName: "discoverZenModels",
75+
prefetchName: "prefetchZenModels",
76+
resetName: "resetZenModelDiscoveryForTests",
77+
discover: discoverZenModels,
78+
selectable: selectableZenModelIds,
79+
prefetch: prefetchZenModels,
80+
reset: resetZenModelDiscoveryForTests,
81+
},
82+
];
83+
84+
beforeEach(() => {
85+
resetGoModelDiscoveryForTests();
86+
resetZenModelDiscoveryForTests();
87+
});
88+
89+
afterEach(() => {
90+
globalThis.fetch = originalFetch;
91+
resetGoModelDiscoveryForTests();
92+
resetZenModelDiscoveryForTests();
93+
});
94+
95+
function oversizedCatalogResponse(byteLength: number): Response {
96+
const chunk = new Uint8Array(64 * 1024).fill(0x61);
97+
let remaining = byteLength;
98+
const stream = new ReadableStream<Uint8Array>({
99+
pull(controller) {
100+
if (remaining <= 0) {
101+
controller.close();
102+
return;
103+
}
104+
const n = Math.min(remaining, chunk.byteLength);
105+
controller.enqueue(n === chunk.byteLength ? chunk : chunk.subarray(0, n));
106+
remaining -= n;
107+
},
108+
});
109+
return new Response(stream, {
110+
status: 200,
111+
headers: { "Content-Type": "application/json" },
112+
});
113+
}
114+
115+
for (const harness of harnesses) {
116+
const {
117+
catalogLabel,
118+
discover,
119+
discoverName,
120+
liveOnlyId,
121+
maxBytes,
122+
maxModels,
123+
modelPrefix,
124+
modelsURL,
125+
otherLabel,
126+
prefetch,
127+
prefetchName,
128+
reset,
129+
resetName,
130+
sampleId,
131+
seedIds,
132+
selectable,
133+
} = harness;
134+
135+
describe(discoverName, () => {
136+
test("GETs the public models URL without auth and does not write the snapshot", async () => {
137+
const fetchMock = async (
138+
input: RequestInfo | URL,
139+
init?: RequestInit,
140+
) => {
141+
expect(String(input)).toBe(modelsURL);
142+
expect(init?.method).toBe("GET");
143+
const headers = new Headers(init?.headers);
144+
expect(headers.get("Authorization")).toBeNull();
145+
return Response.json({
146+
data: [{ id: sampleId }, { id: liveOnlyId }],
147+
});
148+
};
149+
globalThis.fetch = fetchMock as unknown as typeof fetch;
150+
151+
await expect(discover()).resolves.toEqual({
152+
status: "models",
153+
models: [sampleId, liveOnlyId],
154+
});
155+
expect(selectable()).toEqual(seedIds);
156+
expect(selectable()).not.toContain(liveOnlyId);
157+
});
158+
159+
test("distinguishes empty, HTTP unavailable, malformed, and transport failures", async () => {
160+
const cases: {
161+
response: () => Promise<Response>;
162+
expected: CatalogDiscoveryState["status"];
163+
}[] = [
164+
{
165+
response: async () => Response.json({ data: [] }),
166+
expected: "empty",
167+
},
168+
{
169+
response: async () => new Response("no", { status: 503 }),
170+
expected: "unavailable",
171+
},
172+
{
173+
response: async () => Response.json({ models: [] }),
174+
expected: "malformed",
175+
},
176+
];
177+
178+
for (const item of cases) {
179+
globalThis.fetch = item.response as unknown as typeof fetch;
180+
expect((await discover()).status).toBe(item.expected);
181+
}
182+
183+
globalThis.fetch = (async () =>
184+
new Response("no", { status: 503 })) as unknown as typeof fetch;
185+
await expect(discover()).resolves.toEqual({
186+
status: "unavailable",
187+
message: `${catalogLabel} returned HTTP 503`,
188+
});
189+
190+
globalThis.fetch = (async () => {
191+
throw new Error("connection refused");
192+
}) as unknown as typeof fetch;
193+
await expect(discover()).resolves.toEqual({
194+
status: "unavailable",
195+
message: "connection refused",
196+
});
197+
});
198+
199+
test(`labels every catalog failure as ${catalogLabel}, never ${otherLabel}`, async () => {
200+
globalThis.fetch = (async () =>
201+
new Response("no", { status: 503 })) as unknown as typeof fetch;
202+
const http = await discover();
203+
expect(http.status).toBe("unavailable");
204+
if (http.status !== "unavailable")
205+
throw new Error("expected unavailable");
206+
expect(http.message.startsWith(catalogLabel)).toBe(true);
207+
expect(http.message).not.toContain(otherLabel);
208+
209+
globalThis.fetch = (async () =>
210+
oversizedCatalogResponse(maxBytes + 1)) as unknown as typeof fetch;
211+
const oversize = await discover();
212+
expect(oversize.status).toBe("malformed");
213+
if (oversize.status !== "malformed")
214+
throw new Error("expected malformed");
215+
expect(oversize.message.startsWith(catalogLabel)).toBe(true);
216+
expect(oversize.message).not.toContain(otherLabel);
217+
});
218+
219+
test("rejects an oversized catalog body without treating it as models", async () => {
220+
globalThis.fetch = (async () =>
221+
oversizedCatalogResponse(maxBytes + 1)) as unknown as typeof fetch;
222+
223+
const state = await discover();
224+
expect(state.status).toBe("malformed");
225+
if (state.status !== "malformed") throw new Error("expected malformed");
226+
expect(state.message).toContain(String(maxBytes));
227+
expect(selectable()).toEqual(seedIds);
228+
});
229+
230+
test("rejects a declared Content-Length over the byte cap without reading the body as models", async () => {
231+
globalThis.fetch = (async () =>
232+
new Response(`{"data":[{"id":"${sampleId}"}]}`, {
233+
status: 200,
234+
headers: {
235+
"Content-Type": "application/json",
236+
"Content-Length": String(maxBytes + 1),
237+
},
238+
})) as unknown as typeof fetch;
239+
240+
const state = await discover();
241+
expect(state.status).toBe("malformed");
242+
if (state.status !== "malformed") throw new Error("expected malformed");
243+
expect(state.message).toContain(String(maxBytes));
244+
expect(state).not.toEqual({ status: "models", models: [sampleId] });
245+
});
246+
247+
test("rejects a parsed catalog over the model-count cap instead of taking a prefix", async () => {
248+
const data = Array.from({ length: maxModels + 1 }, (_, i) => ({
249+
id: `${modelPrefix}${String(i)}`,
250+
}));
251+
globalThis.fetch = (async () =>
252+
Response.json({ data })) as unknown as typeof fetch;
253+
254+
const state = await discover();
255+
expect(state.status).toBe("malformed");
256+
if (state.status !== "malformed") throw new Error("expected malformed");
257+
expect(state.message).toContain(String(maxModels));
258+
expect(selectable()).toEqual(seedIds);
259+
});
260+
});
261+
262+
describe(prefetchName, () => {
263+
test("writes the live snapshot; later selectable reads are sync and skip fetch", async () => {
264+
let fetchCount = 0;
265+
globalThis.fetch = (async () => {
266+
fetchCount += 1;
267+
return Response.json({
268+
data: [{ id: sampleId }, { id: liveOnlyId }],
269+
});
270+
}) as unknown as typeof fetch;
271+
272+
const ids = await prefetch();
273+
expect(ids).toEqual([sampleId, liveOnlyId]);
274+
expect(ids).toContain(liveOnlyId);
275+
expect(fetchCount).toBe(1);
276+
277+
expect(selectable()).toEqual([sampleId, liveOnlyId]);
278+
expect(fetchCount).toBe(1);
279+
});
280+
281+
test("keeps the live snapshot when a later prefetch fails", async () => {
282+
globalThis.fetch = (async () =>
283+
Response.json({
284+
data: [{ id: liveOnlyId }],
285+
})) as unknown as typeof fetch;
286+
await prefetch();
287+
expect(selectable()).toEqual([liveOnlyId]);
288+
289+
globalThis.fetch = (async () => {
290+
throw new Error("connection refused");
291+
}) as unknown as typeof fetch;
292+
const ids = await prefetch();
293+
expect(ids).toEqual([liveOnlyId]);
294+
expect(selectable()).toEqual([liveOnlyId]);
295+
});
296+
297+
test("cold failing prefetch falls back to the packaged seed", async () => {
298+
globalThis.fetch = (async () => {
299+
throw new Error("connection refused");
300+
}) as unknown as typeof fetch;
301+
302+
const ids = await prefetch();
303+
expect(ids).toEqual(seedIds);
304+
expect(ids.length).toBeGreaterThan(0);
305+
expect(selectable()).toEqual(seedIds);
306+
});
307+
308+
test("oversized live catalog does not replace the seed with a truncated prefix", async () => {
309+
globalThis.fetch = (async () =>
310+
oversizedCatalogResponse(maxBytes + 1)) as unknown as typeof fetch;
311+
312+
const ids = await prefetch();
313+
expect(ids).toEqual(seedIds);
314+
expect(selectable()).toEqual(seedIds);
315+
});
316+
317+
test("overlapping prefetches share one GET; a later prefetch may GET again", async () => {
318+
let fetchCount = 0;
319+
let release!: (response: Response) => void;
320+
const held = new Promise<Response>((resolve) => {
321+
release = resolve;
322+
});
323+
324+
globalThis.fetch = (async () => {
325+
fetchCount += 1;
326+
if (fetchCount === 1) {
327+
return held;
328+
}
329+
return Response.json({ data: [{ id: liveOnlyId }] });
330+
}) as unknown as typeof fetch;
331+
332+
const first = prefetch();
333+
const second = prefetch();
334+
expect(fetchCount).toBe(1);
335+
336+
release(Response.json({ data: [{ id: liveOnlyId }] }));
337+
await expect(Promise.all([first, second])).resolves.toEqual([
338+
[liveOnlyId],
339+
[liveOnlyId],
340+
]);
341+
expect(fetchCount).toBe(1);
342+
343+
await prefetch();
344+
expect(fetchCount).toBe(2);
345+
});
346+
347+
test(`an aborted ${discoverName} does not coalesce with ${prefetchName}`, async () => {
348+
let fetchCount = 0;
349+
globalThis.fetch = (async (
350+
_input: RequestInfo | URL,
351+
init?: RequestInit,
352+
) => {
353+
fetchCount += 1;
354+
if (init?.signal?.aborted) {
355+
throw new DOMException("Aborted", "AbortError");
356+
}
357+
return Response.json({ data: [{ id: liveOnlyId }] });
358+
}) as unknown as typeof fetch;
359+
360+
const controller = new AbortController();
361+
controller.abort();
362+
const [discoverState, prefetched] = await Promise.all([
363+
discover({ signal: controller.signal }),
364+
prefetch(),
365+
]);
366+
367+
expect(discoverState.status).toBe("unavailable");
368+
expect(prefetched).toEqual([liveOnlyId]);
369+
expect(selectable()).toEqual([liveOnlyId]);
370+
expect(fetchCount).toBe(2);
371+
});
372+
373+
test(`${resetName} isolates the snapshot between tests`, async () => {
374+
globalThis.fetch = (async () =>
375+
Response.json({
376+
data: [{ id: liveOnlyId }],
377+
})) as unknown as typeof fetch;
378+
await prefetch();
379+
expect(selectable()).toEqual([liveOnlyId]);
380+
381+
reset();
382+
expect(selectable()).toEqual(seedIds);
383+
384+
globalThis.fetch = (async () => {
385+
throw new Error("connection refused");
386+
}) as unknown as typeof fetch;
387+
expect(await prefetch()).toEqual(seedIds);
388+
});
389+
});
390+
}

0 commit comments

Comments
 (0)