Skip to content

Commit ee06d6b

Browse files
committed
test(cli): cover catalog prewarm on handleStart bind
1 parent 8ac098a commit ee06d6b

3 files changed

Lines changed: 80 additions & 3 deletions

File tree

src/cli/catalog-prewarm.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { OcxConfig } from "../types";
2+
3+
type GatherRoutedModels = (config: OcxConfig) => Promise<unknown>;
4+
5+
export type CatalogPrewarmDeps = {
6+
loadConfig?: () => OcxConfig;
7+
importCatalog?: () => Promise<{ gatherRoutedModels: GatherRoutedModels }>;
8+
};
9+
10+
/**
11+
* After the listen port is bound, kick off live provider discovery so the first
12+
* GUI /v1/models and syncModelsToCodex share one gather flight instead of racing
13+
* duplicate upstream /models fetches.
14+
*/
15+
export function scheduleCatalogPrewarm(deps: CatalogPrewarmDeps = {}): void {
16+
void Promise.resolve()
17+
.then(async () => {
18+
const load = deps.loadConfig ?? (await import("../config")).loadConfig;
19+
const { gatherRoutedModels } = await (deps.importCatalog?.() ?? import("../codex/catalog"));
20+
return gatherRoutedModels(load());
21+
})
22+
.catch(() => {});
23+
}
24+

src/cli/index.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { startTokenGuardian } from "../oauth/token-guardian";
3838
import { startHistoryMigrationGuardian } from "../codex/history-migration-guardian";
3939
import { maybeAutoRestoreCodexShim } from "./codex-shim-autorestore";
4040
import { maybeShowStarPrompt } from "./star-prompt";
41+
import { scheduleCatalogPrewarm } from "./catalog-prewarm";
4142
import { maybeShowUpdatePrompt } from "../update/notify";
4243
import { syncModelsToCodex } from "../codex/sync";
4344
import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
@@ -193,9 +194,7 @@ async function handleStart(options: { block?: boolean } = {}) {
193194
// Prewarm the live provider model cache as soon as the port is bound so the
194195
// first GUI /v1/models (and syncModelsToCodex below) share one discovery flight
195196
// instead of racing duplicate upstream /models fetches.
196-
void import("../codex/catalog").then(({ gatherRoutedModels }) => {
197-
gatherRoutedModels(loadConfig()).catch(() => {});
198-
});
197+
scheduleCatalogPrewarm();
199198
break;
200199
} catch (err) {
201200
if (!isAddrInUse(err) || attempt >= 2) throw err;

tests/cli-catalog-prewarm.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { describe, expect, mock, test } from "bun:test";
2+
import type { OcxConfig } from "../src/types";
3+
import { scheduleCatalogPrewarm } from "../src/cli/catalog-prewarm";
4+
5+
const root = new URL("../", import.meta.url);
6+
7+
async function readText(path: string): Promise<string> {
8+
return await Bun.file(new URL(path, root)).text();
9+
}
10+
11+
describe("catalog prewarm on handleStart bind", () => {
12+
test("scheduleCatalogPrewarm calls gatherRoutedModels(loadConfig()) once", async () => {
13+
const config = { port: 9_001, providers: {}, defaultProvider: "fixture" } as OcxConfig;
14+
const gatherRoutedModels = mock(async (_config: OcxConfig) => []);
15+
const load = mock(() => config);
16+
const importCatalog = mock(async () => ({ gatherRoutedModels }));
17+
18+
scheduleCatalogPrewarm({ loadConfig: load, importCatalog });
19+
20+
await Bun.sleep(0);
21+
expect(importCatalog).toHaveBeenCalledTimes(1);
22+
expect(load).toHaveBeenCalledTimes(1);
23+
expect(gatherRoutedModels).toHaveBeenCalledTimes(1);
24+
expect(gatherRoutedModels.mock.calls[0]?.[0]).toBe(config);
25+
});
26+
27+
test("scheduleCatalogPrewarm swallows gather failures", async () => {
28+
const gatherRoutedModels = mock(async () => {
29+
throw new Error("discovery failed");
30+
});
31+
scheduleCatalogPrewarm({
32+
loadConfig: () => ({ port: 9_002, providers: {}, defaultProvider: "fixture" }) as OcxConfig,
33+
importCatalog: async () => ({ gatherRoutedModels }),
34+
});
35+
await Bun.sleep(0);
36+
expect(gatherRoutedModels).toHaveBeenCalledTimes(1);
37+
});
38+
39+
test("handleStart schedules catalog prewarm immediately after a successful bind", async () => {
40+
const cli = (await readText("src/cli/index.ts")).replace(/\r\n/g, "\n");
41+
const bindIdx = cli.indexOf("server = startServer(port);");
42+
const prewarmIdx = cli.indexOf("scheduleCatalogPrewarm()");
43+
const breakIdx = cli.indexOf("\n break;", bindIdx);
44+
45+
expect(cli).toContain('from "./catalog-prewarm"');
46+
expect(bindIdx).toBeGreaterThan(-1);
47+
expect(prewarmIdx).toBeGreaterThan(bindIdx);
48+
expect(breakIdx).toBeGreaterThan(prewarmIdx);
49+
// Must stay inside the successful-bind try path, not only on a later sync.
50+
expect(cli.slice(bindIdx, breakIdx)).toContain("scheduleCatalogPrewarm()");
51+
expect(cli).not.toContain('void import("../codex/catalog").then(({ gatherRoutedModels })');
52+
});
53+
});
54+

0 commit comments

Comments
 (0)