1import { assert, describe, it } from "@effect/vitest";
2import type { ServerProviderModel } from "@t3tools/contracts";
3import type * as ModelCatalog from "@t3tools/provider-core/server/ModelCatalog";
4
5import { applyCodexModelCatalog } from "./codexModelCatalog.ts";
6
7/** Synthetic models only; real Codex model data lives in model-manifest.json. */
8const model = (overrides: Partial<ServerProviderModel>): ServerProviderModel => ({
9 slug: "gpt-test",
10 name: "GPT Test",
11 isCustom: false,
12 capabilities: null,
13 ...overrides,
14});
15
16const entry = (
17 overrides: Partial<ModelCatalog.ProviderCatalogModel> & { readonly slug: string },
18): ModelCatalog.ProviderCatalogModel => ({
19 name: overrides.slug,
20 status: "current",
21 capabilities: null,
22 adapter: undefined,
23 profileAdapter: undefined,
24 ...overrides,
25});
26
27const catalog: ModelCatalog.ProviderCatalog = {
28 defaultChatModel: undefined,
29 models: [
30 entry({
31 slug: "gpt-next",
32 name: "GPT Next",
33 badge: "new",
34 adapter: { codex: { minVersion: "1.2.0" } },
35 }),
36 entry({ slug: "gpt-unversioned" }),
37 entry({ slug: "gpt-retired", status: "legacy", adapter: { codex: { minVersion: "1.2.0" } } }),
38 ],
39};
40
41const draft = (version: string | null, models: ReadonlyArray<ServerProviderModel> = []) => ({
42 enabled: true,
43 installed: true,
44 version,
45 status: "ready" as const,
46 auth: { status: "authenticated" as const },
47 checkedAt: "2026-01-01T00:00:00.000Z",
48 models,
49 slashCommands: [],
50 skills: [],
51});
52
53describe("applyCodexModelCatalog", () => {
54 it("names current models that need a newer Codex and are missing from discovery", () => {
55 assert.deepStrictEqual(applyCodexModelCatalog(draft("1.1.9"), catalog).updateRequiredModels, [
56 { slug: "gpt-next", name: "GPT Next", badge: "new", minVersion: "1.2.0" },
57 ]);
58 for (const result of [
59 // The CLI is new enough.
60 applyCodexModelCatalog(draft("1.2.0"), catalog),
61 // The CLI already lists the model, even under a qualified slug.
62 applyCodexModelCatalog(draft("1.1.9", [model({ slug: "openai.gpt-next" })]), catalog),
63 // An unknown version cannot be compared.
64 applyCodexModelCatalog(draft(null), catalog),
65 // A qualified catalog slug still matches the discovered family.
66 applyCodexModelCatalog(draft("1.1.9", [model({ slug: "gpt-next" })]), {
67 ...catalog,
68 models: [{ ...catalog.models[0]!, slug: "openai.gpt-next" }],
69 }),
70 ]) {
71 assert.isUndefined(result.updateRequiredModels);
72 }
73 });
74
75 it("classifies qualified Codex families without changing their wire ids", () => {
76 const classified = applyCodexModelCatalog(
77 draft("1.2.0", [
78 model({ slug: "openai.gpt-test", isLegacy: true }),
79 model({ slug: "openai.gpt-retired" }),
80 ]),
81 catalog,
82 );
83 assert.deepStrictEqual(
84 classified.models.map((entry) => [entry.slug, entry.isLegacy ?? false]),
85 [
86 ["openai.gpt-test", false],
87 ["openai.gpt-retired", true],
88 ],
89 );
90 });
91});