/** * Tests for router-link.js * * Uses node:test (Node >= 18) with mock.global to stub fetch and the SDK * client. Runs against a default-hook instance of RouterLink. * * Tests are isolated by using distinct session ids for each parent-cache * scenario, and resetting the global fetch mock between tool.execute.after * tests. */ import { describe, it, mock, before, after, beforeEach } from "node:test"; import assert from "node:assert/strict"; import { RouterLink } from "./router-link.js"; const parentCache = RouterLink.parentCache; const SESSION_ID = "ses_test123"; const AGENT = "build"; const DIRECTORY = "/home/test/project"; // Stub SDK client. `session.get` behaviour is swapped per test. function stubClient(sessionGetImpl) { return { session: { get: sessionGetImpl, }, }; } // Instant successful client returning ses_parent456 for any session. function defaultClient() { return stubClient(async () => ({ data: { parentID: "ses_parent456" }, })); } // ------ helpers ------ function routerHeadersInput(overrides = {}) { return { sessionID: SESSION_ID, agent: AGENT, model: { providerID: "llm-router" }, provider: { info: { id: "llm-router" } }, ...overrides, }; } function testToolInput(overrides = {}) { return { tool: "bash", sessionID: SESSION_ID, callID: "call_001", args: { command: "pytest tests/" }, ...overrides, }; } function passingOutput(overrides = {}) { return { exitCode: 0, stdout: "3 passed, 0 failed", stderr: "", ...overrides, }; } // ------ chat.headers tests ------ describe("chat.headers", () => { it("sets X-Router-Conversation, X-Router-Agent, X-Router-Parent for router provider", async () => { const hooks = await RouterLink({ client: defaultClient(), directory: DIRECTORY, }); const output = { headers: {} }; // Use a unique session so cache from a previous test cannot interfere await hooks["chat.headers"]( routerHeadersInput({ sessionID: "ses_router_001" }), output, ); assert.equal(output.headers["X-Router-Conversation"], "ses_router_001"); assert.equal(output.headers["X-Router-Agent"], AGENT); assert.equal(output.headers["X-Router-Parent"], "ses_parent456"); }); it("sets no headers for non-router provider (model + provider both override)", async () => { const hooks = await RouterLink({ client: defaultClient(), directory: DIRECTORY, }); const output = { headers: {} }; await hooks["chat.headers"]( routerHeadersInput({ sessionID: "ses_nonrouter_001", model: { providerID: "openai" }, provider: { info: { id: "openai" } }, }), output, ); assert.deepEqual(output.headers, {}); }); it("sets no headers when provider.info.id differs even if model.providerID is absent", async () => { const hooks = await RouterLink({ client: defaultClient(), directory: DIRECTORY, }); const output = { headers: {} }; await hooks["chat.headers"]( { sessionID: "ses_nonrouter_002", agent: AGENT, model: {}, provider: { info: { id: "anthropic" } }, }, output, ); assert.deepEqual(output.headers, {}); }); it("sets no headers when neither model.providerID nor provider.info.id nor URL matches", async () => { const hooks = await RouterLink({ client: defaultClient(), directory: DIRECTORY, }); const output = { headers: {} }; await hooks["chat.headers"]( { sessionID: "ses_nonrouter_003", agent: AGENT, model: {}, provider: { info: { options: { url: "http://other:9000" } } }, }, output, ); assert.deepEqual(output.headers, {}); }); it("falls back to provider url matching when model.providerID is absent", async () => { const hooks = await RouterLink({ client: defaultClient(), directory: DIRECTORY, }); const output = { headers: {} }; await hooks["chat.headers"]( { sessionID: "ses_urlmatch_001", agent: AGENT, model: {}, provider: { info: { options: { url: "http://127.0.0.1:8080" }, }, }, }, output, ); assert.equal(output.headers["X-Router-Conversation"], "ses_urlmatch_001"); }); it("falls back to provider baseURL origin matching when model.providerID is absent", async () => { const hooks = await RouterLink({ client: defaultClient(), directory: DIRECTORY, }); const output = { headers: {} }; await hooks["chat.headers"]( { sessionID: "ses_baseurl_001", agent: AGENT, model: {}, provider: { info: { options: { baseURL: "http://127.0.0.1:8080/v1" }, }, }, }, output, ); assert.equal(output.headers["X-Router-Conversation"], "ses_baseurl_001"); }); it("sets no headers when the baseURL origin differs from the router origin", async () => { const hooks = await RouterLink({ client: defaultClient(), directory: DIRECTORY, }); const output = { headers: {} }; await hooks["chat.headers"]( { sessionID: "ses_baseurl_002", agent: AGENT, model: {}, provider: { info: { options: { baseURL: "http://127.0.0.1:9999/v1" }, }, }, }, output, ); assert.deepEqual(output.headers, {}); }); it("sets no headers on a malformed baseURL and does not throw", async () => { const hooks = await RouterLink({ client: defaultClient(), directory: DIRECTORY, }); const output = { headers: {} }; await hooks["chat.headers"]( { sessionID: "ses_baseurl_003", agent: AGENT, model: {}, provider: { info: { options: { baseURL: "not-a-valid-url" }, }, }, }, output, ); assert.deepEqual(output.headers, {}); }); it("omits X-Router-Parent when parent lookup takes >250ms", async () => { const slowClient = stubClient( () => new Promise((resolve) => setTimeout(() => resolve({ data: { parentID: "ses_slow" } }), 300), ), ); const hooks = await RouterLink({ client: slowClient, directory: DIRECTORY, }); const output = { headers: {} }; await hooks["chat.headers"]( routerHeadersInput({ sessionID: "ses_slowparent_001" }), output, ); assert.equal(output.headers["X-Router-Conversation"], "ses_slowparent_001"); assert.equal(output.headers["X-Router-Agent"], AGENT); // Parent timed out, so no parent header assert.equal(output.headers["X-Router-Parent"], undefined); }); it("omits X-Router-Parent when parent lookup throws and does not throw", async () => { const brokenClient = stubClient(async () => { throw new Error("session store unreachable"); }); const hooks = await RouterLink({ client: brokenClient, directory: DIRECTORY, }); const output = { headers: {} }; // Must not throw await hooks["chat.headers"]( routerHeadersInput({ sessionID: "ses_throws_001" }), output, ); assert.equal(output.headers["X-Router-Conversation"], "ses_throws_001"); assert.equal(output.headers["X-Router-Agent"], AGENT); assert.equal(output.headers["X-Router-Parent"], undefined); }); it("caches successful parent lookups so repeated calls do not hit the SDK", async () => { let callCount = 0; const countingClient = stubClient(async () => { callCount++; return { data: { parentID: "ses_parentCached" } }; }); const hooks = await RouterLink({ client: countingClient, directory: DIRECTORY, }); const output1 = { headers: {} }; await hooks["chat.headers"]( routerHeadersInput({ sessionID: "ses_counting_001" }), output1, ); assert.equal(callCount, 1); // Second call with same session id should use cache const output2 = { headers: {} }; await hooks["chat.headers"]( routerHeadersInput({ sessionID: "ses_counting_001" }), output2, ); assert.equal(callCount, 1); // still 1 — cache hit assert.equal(output2.headers["X-Router-Parent"], "ses_parentCached"); }); it("does not cache failed lookups (so a retry can try again)", async () => { let fail = true; const flakyClient = stubClient(async () => { if (fail) throw new Error("transient"); return { data: { parentID: "ses_parentFlaky" } }; }); const hooks = await RouterLink({ client: flakyClient, directory: DIRECTORY, }); // First call: fails, no cache const output1 = { headers: {} }; await hooks["chat.headers"]( routerHeadersInput({ sessionID: "ses_flaky_001" }), output1, ); assert.equal(output1.headers["X-Router-Parent"], undefined); // Second call: SDK succeeds — should NOT have cached the failure fail = false; const output2 = { headers: {} }; await hooks["chat.headers"]( routerHeadersInput({ sessionID: "ses_flaky_001" }), output2, ); assert.equal(output2.headers["X-Router-Parent"], "ses_parentFlaky"); }); }); // ------ chat.headers agent slugging ------ describe("chat.headers agent slugging", () => { it("slugs 'Atlas - Plan Executor' to 'atlas-plan-executor'", async () => { const hooks = await RouterLink({ client: defaultClient(), directory: DIRECTORY, }); const output = { headers: {} }; await hooks["chat.headers"]( routerHeadersInput({ sessionID: "ses_slug_001", agent: "Atlas - Plan Executor", }), output, ); assert.equal(output.headers["X-Router-Agent"], "atlas-plan-executor"); }); it("slugs 'Sisyphus - ultraworker' to 'sisyphus-ultraworker'", async () => { const hooks = await RouterLink({ client: defaultClient(), directory: DIRECTORY, }); const output = { headers: {} }; await hooks["chat.headers"]( routerHeadersInput({ sessionID: "ses_slug_002", agent: "Sisyphus - ultraworker", }), output, ); assert.equal(output.headers["X-Router-Agent"], "sisyphus-ultraworker"); }); it("leaves a name already in the router charset unchanged", async () => { const hooks = await RouterLink({ client: defaultClient(), directory: DIRECTORY, }); const output = { headers: {} }; await hooks["chat.headers"]( routerHeadersInput({ sessionID: "ses_slug_003", agent: "oh-my-claudecode:executor", }), output, ); assert.equal( output.headers["X-Router-Agent"], "oh-my-claudecode:executor", ); }); it("sends no X-Router-Agent when the name is only spaces", async () => { const hooks = await RouterLink({ client: defaultClient(), directory: DIRECTORY, }); const output = { headers: {} }; await hooks["chat.headers"]( routerHeadersInput({ sessionID: "ses_slug_004", agent: " " }), output, ); // The hook itself still ran and stamped the conversation. assert.equal(output.headers["X-Router-Conversation"], "ses_slug_004"); assert.equal(output.headers["X-Router-Agent"], undefined); }); it("cuts a 100-character name to the router's 64-char limit", async () => { const hooks = await RouterLink({ client: defaultClient(), directory: DIRECTORY, }); const output = { headers: {} }; await hooks["chat.headers"]( routerHeadersInput({ sessionID: "ses_slug_005", agent: "a".repeat(100), }), output, ); assert.equal(output.headers["X-Router-Agent"], "a".repeat(64)); }); it("sends no X-Router-Agent when input.agent is absent", async () => { const hooks = await RouterLink({ client: defaultClient(), directory: DIRECTORY, }); const output = { headers: {} }; await hooks["chat.headers"]( { sessionID: "ses_slug_006", model: { providerID: "llm-router" }, provider: { info: { id: "llm-router" } }, }, output, ); assert.equal(output.headers["X-Router-Agent"], undefined); }); }); // ------ tool.execute.after tests ------ describe("tool.execute.after", () => { beforeEach(() => { mock.restoreAll(); }); it("POSTs pass outcome with conversation_id, ok=true, and source", async () => { const fetchMock = mock.fn(); mock.method(globalThis, "fetch", fetchMock); fetchMock.mock.mockImplementation(async () => new Response(null, { status: 200 }), ); const hooks = await RouterLink({ client: defaultClient(), directory: DIRECTORY, }); await hooks["tool.execute.after"]( testToolInput({ sessionID: "ses_outcome_001" }), passingOutput(), ); assert.equal(fetchMock.mock.callCount(), 1); const [url, opts] = fetchMock.mock.calls[0].arguments; assert.match(url, /\/outcome$/); const body = JSON.parse(opts.body); assert.equal(body.ok, true); assert.equal(body.source, DIRECTORY); assert.equal(body.conversation_id, "ses_outcome_001"); assert.match(body.detail, /passed/); }); it("POSTs fail outcome with ok=false when tool exits non-zero", async () => { const fetchMock = mock.fn(); mock.method(globalThis, "fetch", fetchMock); fetchMock.mock.mockImplementation(async () => new Response(null, { status: 200 }), ); const hooks = await RouterLink({ client: defaultClient(), directory: DIRECTORY, }); const input = testToolInput({ sessionID: "ses_outcome_002" }); const output = passingOutput({ exitCode: 1, stdout: "1 failed" }); await hooks["tool.execute.after"](input, output); assert.equal(fetchMock.mock.callCount(), 1); const body = JSON.parse(fetchMock.mock.calls[0].arguments[1].body); assert.equal(body.ok, false); assert.match(body.detail, /failed/); }); it("POSTs nothing for a non-test command", async () => { const fetchMock = mock.fn(); mock.method(globalThis, "fetch", fetchMock); const hooks = await RouterLink({ client: defaultClient(), directory: DIRECTORY, }); await hooks["tool.execute.after"]( testToolInput({ sessionID: "ses_other_001", args: { command: "ls -la" }, }), passingOutput(), ); assert.equal(fetchMock.mock.callCount(), 0); }); it("does not throw when router is unreachable (network error)", async () => { const fetchMock = mock.fn(); mock.method(globalThis, "fetch", fetchMock); fetchMock.mock.mockImplementation(async () => { throw new TypeError("fetch failed"); }); const hooks = await RouterLink({ client: defaultClient(), directory: DIRECTORY, }); // Must not throw await hooks["tool.execute.after"]( testToolInput({ sessionID: "ses_unreach_001" }), passingOutput(), ); assert.equal(fetchMock.mock.callCount(), 1); }); it("logs and swallows a 409 response", async () => { const fetchMock = mock.fn(); mock.method(globalThis, "fetch", fetchMock); fetchMock.mock.mockImplementation(async () => new Response(null, { status: 409 }), ); const hooks = await RouterLink({ client: defaultClient(), directory: DIRECTORY, }); // Must not throw await hooks["tool.execute.after"]( testToolInput({ sessionID: "ses_409_001" }), passingOutput(), ); assert.equal(fetchMock.mock.callCount(), 1); }); it("logs non-ok 4xx (but not 404) without throwing", async () => { const fetchMock = mock.fn(); mock.method(globalThis, "fetch", fetchMock); fetchMock.mock.mockImplementation(async () => new Response(null, { status: 502 }), ); const hooks = await RouterLink({ client: defaultClient(), directory: DIRECTORY, }); // Must not throw await hooks["tool.execute.after"]( testToolInput({ sessionID: "ses_502_001" }), passingOutput(), ); assert.equal(fetchMock.mock.callCount(), 1); }); it("accepts 404 silently (old router without /outcome)", async () => { const fetchMock = mock.fn(); mock.method(globalThis, "fetch", fetchMock); fetchMock.mock.mockImplementation(async () => new Response(null, { status: 404 }), ); const hooks = await RouterLink({ client: defaultClient(), directory: DIRECTORY, }); await hooks["tool.execute.after"]( testToolInput({ sessionID: "ses_404_001" }), passingOutput(), ); assert.equal(fetchMock.mock.callCount(), 1); }); }); // ------ parent cache tests ------ describe("parent cache", () => { it("caches a root session (no parentID) so two requests call the SDK once", async () => { let callCount = 0; const countingClient = stubClient(async () => { callCount++; return { data: {} }; // no parentID -- root session }); const hooks = await RouterLink({ client: countingClient, directory: DIRECTORY, }); // First call: SDK hit, null cached, no X-Router-Parent const output1 = { headers: {} }; await hooks["chat.headers"]( routerHeadersInput({ sessionID: "ses_root_001" }), output1, ); assert.equal(callCount, 1); assert.equal(output1.headers["X-Router-Parent"], undefined); // Second call with same session: cache hit, SDK NOT called const output2 = { headers: {} }; await hooks["chat.headers"]( routerHeadersInput({ sessionID: "ses_root_001" }), output2, ); assert.equal(callCount, 1); // still 1 assert.equal(output2.headers["X-Router-Parent"], undefined); }); it("retries a timed-out lookup on the next request", async () => { let callCount = 0; const slowClient = stubClient(() => { callCount++; // count synchronously on each call return new Promise((resolve) => setTimeout(() => resolve({ data: { parentID: "ses_parentSlow" } }), 300), ); }); const hooks = await RouterLink({ client: slowClient, directory: DIRECTORY, }); // First call: timeout wins (300 ms > 250 ms), SDK call counted const output1 = { headers: {} }; await hooks["chat.headers"]( routerHeadersInput({ sessionID: "ses_timoutretry_001" }), output1, ); assert.equal(callCount, 1); assert.equal(output1.headers["X-Router-Parent"], undefined); // Second call: not cached, SDK called again const output2 = { headers: {} }; await hooks["chat.headers"]( routerHeadersInput({ sessionID: "ses_timoutretry_001" }), output2, ); assert.equal(callCount, 2); // SDK retried assert.equal(output2.headers["X-Router-Parent"], undefined); }); it("retries a throwing lookup on the next request", async () => { let callCount = 0; const brokenClient = stubClient(() => { callCount++; // count synchronously before rejecting return Promise.reject(new Error("session store down")); }); const hooks = await RouterLink({ client: brokenClient, directory: DIRECTORY, }); // First call: throws, no cache const output1 = { headers: {} }; await hooks["chat.headers"]( routerHeadersInput({ sessionID: "ses_throwretry_001" }), output1, ); assert.equal(callCount, 1); assert.equal(output1.headers["X-Router-Parent"], undefined); // Second call: not cached, SDK called again const output2 = { headers: {} }; await hooks["chat.headers"]( routerHeadersInput({ sessionID: "ses_throwretry_001" }), output2, ); assert.equal(callCount, 2); // SDK retried assert.equal(output2.headers["X-Router-Parent"], undefined); }); it("evicts the oldest entry when cache exceeds 1000", async () => { parentCache.clear(); const noParentClient = stubClient(async () => ({ data: {} })); const hooks = await RouterLink({ client: noParentClient, directory: DIRECTORY, }); // Insert 1001 entries for (let i = 0; i < 1001; i++) { const output = { headers: {} }; await hooks["chat.headers"]( routerHeadersInput({ sessionID: `ses_evict_${String(i).padStart(4, "0")}` }), output, ); } assert.equal(parentCache.size, 1000); // Oldest entry (ses_evict_0000) evicted assert.equal(parentCache.has("ses_evict_0000"), false); // Newest entry still present assert.equal(parentCache.has("ses_evict_1000"), true); // An entry in the middle still present assert.equal(parentCache.has("ses_evict_0500"), true); }); }); describe("loader contract", () => { it("every named export from the module is a function (opencode rejects non-function exports)", async () => { const mod = await import("./router-link.js"); for (const [name, value] of Object.entries(mod)) { assert.equal(typeof value, "function", `export "${name}" must be a function`); } }); });