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 { ApiEndpointPool, isEndpointStructurallyEligible, normalizeEndpointType, } from "./api-endpoint-pool.mjs"; async function makeTempPoolDir() { return fs.mkdtemp(path.join(os.tmpdir(), "api-pool-test-")); } test("normalizeEndpointType accepts codex and claude aliases", () => { assert.equal(normalizeEndpointType("codex"), "codex"); assert.equal(normalizeEndpointType("claude"), "claude-code"); assert.equal(normalizeEndpointType("claude_code"), "claude-code"); }); test("isEndpointStructurallyEligible rejects invalid or disabled items", () => { const valid = { type: "codex", baseUrl: "https://example.com/v1", apiKey: "sk-1", disabled: false, }; assert.equal(isEndpointStructurallyEligible(valid, "codex"), true); assert.equal(isEndpointStructurallyEligible({ ...valid, disabled: true }, "codex"), false); assert.equal(isEndpointStructurallyEligible({ ...valid, apiKey: "" }, "codex"), false); assert.equal(isEndpointStructurallyEligible({ ...valid, type: "claude-code" }, "codex"), false); }); test("ApiEndpointPool loads eligible items for the selected provider", async () => { const dir = await makeTempPoolDir(); await fs.writeFile( path.join(dir, "good.json"), JSON.stringify([ { name: "main", type: "codex", baseUrl: "https://example.com/v1", apiKey: "sk-good", }, { name: "backup", type: "codex", baseUrl: "https://backup.example.com/v1", apiKey: "sk-backup", }, ]), ); await fs.writeFile( path.join(dir, "wrong-provider.json"), JSON.stringify([ { name: "cc", type: "claude-code", baseUrl: "https://claude.example.com", apiKey: "sk-cc", }, ]), ); const pool = new ApiEndpointPool({ poolDir: dir, provider: "codex", fetchFn: async () => new Response('{"data":[]}', { status: 200 }), }); await pool.load(); assert.equal(pool.listEndpoints().length, 2); assert.equal(pool.listEndpoints()[0].name, "main"); assert.equal(pool.listEndpoints()[1].name, "backup"); }); test("ApiEndpointPool supports legacy single-object files and array files together", async () => { const dir = await makeTempPoolDir(); await fs.writeFile( path.join(dir, "single.json"), JSON.stringify({ name: "single", type: "codex", baseUrl: "https://single.example.com/v1", apiKey: "sk-single", }), ); await fs.writeFile( path.join(dir, "many.json"), JSON.stringify([ { name: "many-1", type: "codex", baseUrl: "https://many1.example.com/v1", apiKey: "sk-many-1", }, { name: "many-2", type: "codex", baseUrl: "https://many2.example.com/v1", apiKey: "sk-many-2", }, ]), ); const pool = new ApiEndpointPool({ poolDir: dir, provider: "codex", fetchFn: async () => new Response('{"data":[]}', { status: 200 }), }); await pool.load(); assert.equal(pool.listEndpoints().length, 3); assert.deepEqual( pool.listEndpoints().map((item) => item.name), ["many-1", "many-2", "single"], ); }); test("ApiEndpointPool prioritizes pool.json over other json files in the same directory", async () => { const dir = await makeTempPoolDir(); await fs.writeFile( path.join(dir, "pool.json"), JSON.stringify([ { name: "preferred", type: "codex", baseUrl: "https://preferred.example.com/v1", apiKey: "sk-preferred", }, ]), ); await fs.writeFile( path.join(dir, "other.json"), JSON.stringify({ name: "ignored", type: "codex", baseUrl: "https://ignored.example.com/v1", apiKey: "sk-ignored", }), ); const pool = new ApiEndpointPool({ poolDir: dir, provider: "codex", fetchFn: async () => new Response('{"data":[]}', { status: 200 }), }); await pool.load(); assert.equal(pool.listEndpoints().length, 1); assert.equal(pool.listEndpoints()[0].name, "preferred"); }); test("ApiEndpointPool rotates after failure and respects cooldown", async () => { const dir = await makeTempPoolDir(); await fs.writeFile( path.join(dir, "a.json"), JSON.stringify([ { name: "a", type: "codex", baseUrl: "https://a.example.com/v1", apiKey: "sk-a", }, { name: "b", type: "codex", baseUrl: "https://b.example.com/v1", apiKey: "sk-b", }, ]), ); const pool = new ApiEndpointPool({ poolDir: dir, provider: "codex", fetchFn: async (url, options) => { const auth = options?.headers?.authorization || ""; if (String(url).includes("a.example.com") && auth.includes("sk-a")) { return new Response("forbidden", { status: 403 }); } return new Response('{"data":[{"id":"gpt-5.4"}]}', { status: 200 }); }, }); await pool.load(); const initial = await pool.getInitialEndpoint(); assert.ok(initial); assert.equal(initial.name, "b"); const failed = pool.listEndpoints().find((item) => item.name === "a"); assert.ok(pool.isCoolingDown(failed)); }); test("ApiEndpointPool returns null when all endpoints are cooling down", async () => { const dir = await makeTempPoolDir(); await fs.writeFile( path.join(dir, "only.json"), JSON.stringify([ { name: "only", type: "codex", baseUrl: "https://only.example.com/v1", apiKey: "sk-only", }, ]), ); const pool = new ApiEndpointPool({ poolDir: dir, provider: "codex", fetchFn: async () => new Response("rate limit", { status: 429 }), }); await pool.load(); const initial = await pool.getInitialEndpoint(); assert.equal(initial, null); assert.equal(pool.listEndpoints().length, 1); assert.ok(pool.isCoolingDown(pool.listEndpoints()[0])); }); test("ApiEndpointPool probes codex entries with the configured responses model", async () => { const dir = await makeTempPoolDir(); await fs.writeFile( path.join(dir, "pool.json"), JSON.stringify([ { name: "codex-main", type: "codex", baseUrl: "https://compat.example.com", apiKey: "sk-compat", model: "grok-4.1-fast", }, ]), ); let seenUrl = ""; let seenMethod = ""; let seenBody = ""; const pool = new ApiEndpointPool({ poolDir: dir, provider: "codex", fetchFn: async (url, options) => { seenUrl = String(url); seenMethod = String(options?.method || ""); seenBody = String(options?.body || ""); return new Response('{"id":"resp_123","model":"grok-4.1-fast","output":[]}', { status: 200 }); }, }); await pool.load(); const endpoint = await pool.getInitialEndpoint(); assert.ok(endpoint); assert.equal(seenMethod, "POST"); assert.match(seenUrl, /\/v1\/responses$/); assert.match(seenBody, /"model":"grok-4\.1-fast"/); }); test("ApiEndpointPool probes claude-code entries with the configured messages model", async () => { const dir = await makeTempPoolDir(); await fs.writeFile( path.join(dir, "pool.json"), JSON.stringify([ { name: "claude-main", type: "claude-code", baseUrl: "https://compat.example.com", apiKey: "sk-compat", model: "gpt-5.3-codex", }, ]), ); let seenUrl = ""; let seenMethod = ""; let seenBody = ""; const pool = new ApiEndpointPool({ poolDir: dir, provider: "claude-code", fetchFn: async (url, options) => { seenUrl = String(url); seenMethod = String(options?.method || ""); seenBody = String(options?.body || ""); return new Response('{"id":"msg_123","content":[{"type":"text","text":"OK"}]}', { status: 200 }); }, }); await pool.load(); const endpoint = await pool.getInitialEndpoint(); assert.ok(endpoint); assert.equal(seenMethod, "POST"); assert.match(seenUrl, /\/v1\/messages$/); assert.match(seenBody, /"model":"gpt-5\.3-codex"/); });