opencode 1.18 treats every export of a plugin module as a plugin function. router-link.js exported a Map (), so the loader rejected the entire module: chat.headers and tool.execute.after never ran. The 29 node tests passed because they imported the module directly, bypassing the loader. - Move parentCache from a named export to a property on RouterLink - Add loader-contract test: every module export must be a function - Update test imports to use RouterLink.parentCache Evidence: opencode.log at every start since 2026-09-26T08:02Z shows "Plugin export is not a function" for router-link.js.
712 lines
21 KiB
JavaScript
712 lines
21 KiB
JavaScript
/**
|
|
* 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`);
|
|
}
|
|
});
|
|
}); |