import test from "node:test"; import assert from "node:assert/strict"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { Readable } from "node:stream"; import { ConfigSwitchStore } from "./config-switch-store.mjs"; import { createRequestListener } from "./server.mjs"; async function makeTempRoot() { return fs.mkdtemp(path.join(os.tmpdir(), "local-ui-server-test-")); } function makeProviderDefinitions(root) { return { codex: { provider: "codex", label: "Codex 配置", targetPaths: [ { key: "authJsonText", label: "auth.json", path: path.join(root, "home", ".codex", "auth.json"), kind: "json", }, { key: "configTomlText", label: "config.toml", path: path.join(root, "home", ".codex", "config.toml"), kind: "toml", }, ], }, "claude-code": { provider: "claude-code", label: "Claude Code 配置", targetPaths: [ { key: "settingsJsonText", label: "settings.json", path: path.join(root, "home", ".claude", "settings.json"), kind: "json", }, ], }, }; } async function makeConfigSwitchListener() { const root = await makeTempRoot(); const configSwitchStore = new ConfigSwitchStore(path.join(root, "data"), { providerDefinitions: makeProviderDefinitions(root), }); await configSwitchStore.load(); return { root, configSwitchStore, listener: createRequestListener({ staticDir: path.join(root, "static"), historyStore: { list: async () => [] }, poolStore: { listPools: () => [], loadPool: async () => { throw new Error("not implemented"); }, savePool: async () => { throw new Error("not implemented"); }, validatePoolItems: () => ({ ok: true, errors: [], normalizedItems: [] }), }, runManager: { execute: async () => { throw new Error("not implemented"); }, getRun: () => null, }, proxyManager: { start: async () => ({ reused: false, status: {} }), stop: async () => ({}), getStatus: async () => ({}), }, apiPoolProxyManagerCodex: { start: async () => ({ reused: false, status: {} }), stop: async () => ({}), getStatus: async () => ({}), }, apiPoolProxyManagerClaude: { start: async () => ({ reused: false, status: {} }), stop: async () => ({}), getStatus: async () => ({}), }, configSwitchStore, }), }; } async function invoke(listener, { method, url, body }) { const req = Readable.from( body == null ? [] : [Buffer.from(JSON.stringify(body), "utf8")], ); req.method = method; req.url = url; return new Promise((resolve, reject) => { const res = { statusCode: 200, headers: {}, setHeader(name, value) { this.headers[name.toLowerCase()] = value; }, end(chunk) { const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk || ""); resolve({ statusCode: this.statusCode, headers: this.headers, body: text ? JSON.parse(text) : null, }); }, }; listener(req, res).catch(reject); }); } test("local ui server exposes config-switch GET and POST routes", async () => { const { listener } = await makeConfigSwitchListener(); const created = await invoke(listener, { method: "POST", url: "/api/config-switch/codex", body: { name: "主配置", payload: { authJsonText: '{"OPENAI_API_KEY":"sk-main"}', configTomlText: 'model_provider = "OpenAI"\nmodel = "gpt-5.4"', }, }, }); assert.equal(created.statusCode, 201); assert.equal(created.body.providers.codex.presets.length, 1); const fetched = await invoke(listener, { method: "GET", url: "/api/config-switch", }); assert.equal(fetched.statusCode, 200); assert.equal(fetched.body.providers.codex.presets[0].name, "主配置"); }); test("local ui server returns 4xx for invalid config-switch provider and missing confirmation", async () => { const { listener } = await makeConfigSwitchListener(); const invalidProvider = await invoke(listener, { method: "POST", url: "/api/config-switch/missing", body: { name: "bad", payload: { authJsonText: '{"OPENAI_API_KEY":"sk"}', configTomlText: 'model = "gpt-5.4"', }, }, }); assert.equal(invalidProvider.statusCode, 404); const created = await invoke(listener, { method: "POST", url: "/api/config-switch/codex", body: { name: "主配置", payload: { authJsonText: '{"OPENAI_API_KEY":"sk-main"}', configTomlText: 'model_provider = "OpenAI"', }, }, }); const presetId = created.body.providers.codex.presets[0].id; const activateWithoutConfirm = await invoke(listener, { method: "POST", url: `/api/config-switch/codex/${presetId}/activate`, body: {}, }); assert.equal(activateWithoutConfirm.statusCode, 409); });