|
4 | 4 | * must match a source-model set, not a single literal. |
5 | 5 | */ |
6 | 6 | import { afterEach, describe, expect, test } from "bun:test"; |
| 7 | +import { mkdtempSync, rmSync } from "node:fs"; |
| 8 | +import { tmpdir } from "node:os"; |
| 9 | +import { join } from "node:path"; |
7 | 10 | import { handleResponses, isShadowSourceModel } from "../src/server/responses"; |
| 11 | +import { handleManagementAPI } from "../src/server/management-api"; |
8 | 12 | import type { OcxConfig } from "../src/types"; |
9 | 13 |
|
10 | 14 | const originalFetch = globalThis.fetch; |
@@ -111,3 +115,59 @@ describe("shadow call intercept request path (issue #311)", () => { |
111 | 115 | expect(response.status).toBe(404); |
112 | 116 | }); |
113 | 117 | }); |
| 118 | + |
| 119 | +/** |
| 120 | + * The GUI badge/tooltip used to hard-code "5.4-mini", so it kept naming a model |
| 121 | + * Codex no longer sends. The management API is the single source of truth for |
| 122 | + * which models are intercepted; every client renders what it reports. |
| 123 | + */ |
| 124 | +async function withTempHome<T>(run: () => Promise<T>): Promise<T> { |
| 125 | + const previousHome = process.env.OPENCODEX_HOME; |
| 126 | + const dir = mkdtempSync(join(tmpdir(), "ocx-shadow-")); |
| 127 | + process.env.OPENCODEX_HOME = dir; |
| 128 | + try { |
| 129 | + return await run(); |
| 130 | + } finally { |
| 131 | + if (previousHome === undefined) delete process.env.OPENCODEX_HOME; |
| 132 | + else process.env.OPENCODEX_HOME = previousHome; |
| 133 | + rmSync(dir, { recursive: true, force: true }); |
| 134 | + } |
| 135 | +} |
| 136 | + |
| 137 | +async function shadowApi(config: OcxConfig, method: string, body?: unknown): Promise<Record<string, unknown>> { |
| 138 | + // Management API enforces a same-origin gate; a browserless caller must look local. |
| 139 | + const headers: Record<string, string> = { origin: "http://127.0.0.1:10100", host: "127.0.0.1:10100" }; |
| 140 | + if (body !== undefined) headers["content-type"] = "application/json"; |
| 141 | + const req = new Request("http://localhost/api/shadow-call-settings", { |
| 142 | + method, |
| 143 | + headers, |
| 144 | + body: body === undefined ? undefined : JSON.stringify(body), |
| 145 | + }); |
| 146 | + const res = await handleManagementAPI(req, new URL(req.url), config, { refreshCodexCatalog: async () => {} }); |
| 147 | + expect(res).not.toBeNull(); |
| 148 | + expect(res!.status).toBe(200); |
| 149 | + return await res!.json() as Record<string, unknown>; |
| 150 | +} |
| 151 | + |
| 152 | +describe("shadow-call settings API reports the intercepted source models", () => { |
| 153 | + test("GET reports the defaults, including the 0.145.0 helper model", async () => { |
| 154 | + await withTempHome(async () => { |
| 155 | + const body = await shadowApi({ port: 0, defaultProvider: "xai", providers: {} } as OcxConfig, "GET"); |
| 156 | + expect(body.sourceModels).toEqual(["gpt-5.4-mini", "gpt-5.6-luna"]); |
| 157 | + }); |
| 158 | + }); |
| 159 | + |
| 160 | + test("GET and PUT report a configured override instead of the defaults", async () => { |
| 161 | + await withTempHome(async () => { |
| 162 | + const config = { |
| 163 | + port: 0, |
| 164 | + defaultProvider: "xai", |
| 165 | + providers: {}, |
| 166 | + shadowCallIntercept: { enabled: true, model: "gpt-5.5", sourceModels: ["gpt-5.6-luna"] }, |
| 167 | + } as OcxConfig; |
| 168 | + expect((await shadowApi(config, "GET")).sourceModels).toEqual(["gpt-5.6-luna"]); |
| 169 | + const put = await shadowApi(config, "PUT", { enabled: true }); |
| 170 | + expect(put.sourceModels).toEqual(["gpt-5.6-luna"]); |
| 171 | + }); |
| 172 | + }); |
| 173 | +}); |
0 commit comments