362 lines
11 KiB
TypeScript
362 lines
11 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { createChatPipelineHarness } from "../integration/_chatPipelineHarness.ts";
|
|
|
|
const harness = await createChatPipelineHarness("chat-context-relay");
|
|
const { BaseExecutor, buildRequest, combosDb, handleChat, resetStorage, waitFor, toPlainHeaders } =
|
|
harness as any;
|
|
const providersDb = await import("../../src/lib/db/providers.ts");
|
|
const handoffDb = await import("../../src/lib/db/contextHandoffs.ts");
|
|
|
|
function buildResponsesResponse(text = "ok", model = "gpt-5.4") {
|
|
return new Response(
|
|
JSON.stringify({
|
|
id: "resp_context_relay",
|
|
object: "response",
|
|
model,
|
|
output: [
|
|
{
|
|
type: "message",
|
|
role: "assistant",
|
|
content: [{ type: "output_text", text }],
|
|
},
|
|
],
|
|
usage: {
|
|
input_tokens: 4,
|
|
output_tokens: 2,
|
|
},
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: { "content-type": "application/json" },
|
|
}
|
|
);
|
|
}
|
|
|
|
async function seedCodexOAuthConnection({
|
|
name,
|
|
email,
|
|
accessToken,
|
|
refreshToken,
|
|
workspaceId,
|
|
priority,
|
|
}) {
|
|
return providersDb.createProviderConnection({
|
|
provider: "codex",
|
|
authType: "oauth",
|
|
name,
|
|
email,
|
|
accessToken,
|
|
refreshToken,
|
|
isActive: true,
|
|
testStatus: "active",
|
|
priority,
|
|
providerSpecificData: { workspaceId },
|
|
});
|
|
}
|
|
|
|
function buildQuotaResponse(usedPercent, resetAfterSeconds = 3600) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
rate_limit: {
|
|
primary_window: {
|
|
used_percent: usedPercent,
|
|
reset_after_seconds: resetAfterSeconds,
|
|
},
|
|
secondary_window: {
|
|
used_percent: 0,
|
|
reset_after_seconds: 86400,
|
|
},
|
|
},
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: { "content-type": "application/json" },
|
|
}
|
|
);
|
|
}
|
|
|
|
test.beforeEach(async () => {
|
|
await resetStorage();
|
|
});
|
|
|
|
test.after(async () => {
|
|
await harness.cleanup();
|
|
});
|
|
|
|
test("handleChat generates and injects context-relay handoffs across Codex account switches", async () => {
|
|
const primary = await seedCodexOAuthConnection({
|
|
name: "codex-a",
|
|
email: "relay-a@example.com",
|
|
accessToken: "token-a",
|
|
refreshToken: "refresh-a",
|
|
workspaceId: "ws-a",
|
|
priority: 1,
|
|
});
|
|
await seedCodexOAuthConnection({
|
|
name: "codex-b",
|
|
email: "relay-b@example.com",
|
|
accessToken: "token-b",
|
|
refreshToken: "refresh-b",
|
|
workspaceId: "ws-b",
|
|
priority: 2,
|
|
});
|
|
|
|
await combosDb.createCombo({
|
|
name: "relay-combo",
|
|
strategy: "context-relay",
|
|
config: {
|
|
maxRetries: 0,
|
|
retryDelayMs: 0,
|
|
handoffThreshold: 0.85,
|
|
maxMessagesForSummary: 12,
|
|
},
|
|
models: ["codex/gpt-5.4"],
|
|
});
|
|
|
|
const upstreamBodies = [];
|
|
const summaryBodies = [];
|
|
|
|
(globalThis as any).fetch = async (url: any, init: any = {}) => {
|
|
const urlStr = String(url);
|
|
const headers = toPlainHeaders(init.headers);
|
|
|
|
if (urlStr.includes("/backend-api/wham/usage")) {
|
|
const authHeader = headers.authorization || headers.Authorization || "";
|
|
return authHeader === "Bearer token-a" ? buildQuotaResponse(87) : buildQuotaResponse(20);
|
|
}
|
|
|
|
const body = init.body ? JSON.parse(String(init.body)) : {};
|
|
const serializedBody = JSON.stringify(body);
|
|
const isSummaryRequest =
|
|
body._omnirouteInternalRequest === "context-handoff" ||
|
|
serializedBody.includes("You are a context summarizer");
|
|
|
|
if (isSummaryRequest) {
|
|
summaryBodies.push({ body, serializedBody });
|
|
return buildResponsesResponse(
|
|
JSON.stringify({
|
|
summary: "Carry over the router implementation state",
|
|
keyDecisions: ["Use global combo defaults", "Inject handoff on account switch"],
|
|
taskProgress: "Runtime and UI are wired; tests are next",
|
|
activeEntities: ["open-sse/services/combo.ts", "src/sse/handlers/chat.ts"],
|
|
}),
|
|
"gpt-5.4"
|
|
);
|
|
}
|
|
|
|
upstreamBodies.push({ body, serializedBody });
|
|
return buildResponsesResponse("relay-success", "gpt-5.4");
|
|
};
|
|
|
|
const firstResponse = await handleChat(
|
|
buildRequest({
|
|
headers: { "X-Session-Id": "relay-session" },
|
|
body: {
|
|
model: "relay-combo",
|
|
stream: false,
|
|
messages: [{ role: "user", content: "Keep implementing the new combo" }],
|
|
},
|
|
})
|
|
);
|
|
const firstJson = (await firstResponse.json()) as any;
|
|
|
|
assert.equal(firstResponse.status, 200);
|
|
assert.equal(firstJson.choices[0].message.content, "relay-success");
|
|
|
|
const sessionId = "ext:relay-session";
|
|
const savedHandoff = await waitFor(() => handoffDb.getHandoff(sessionId, "relay-combo"), 2000);
|
|
assert.ok(savedHandoff);
|
|
assert.equal(savedHandoff.fromAccount, primary.id);
|
|
assert.equal(summaryBodies.length, 1);
|
|
assert.match(summaryBodies[0].serializedBody, /You are a context summarizer/);
|
|
|
|
await providersDb.updateProviderConnection((primary as any).id, {
|
|
rateLimitedUntil: new Date(Date.now() + 60_000).toISOString(),
|
|
});
|
|
|
|
const secondResponse = await handleChat(
|
|
buildRequest({
|
|
headers: { "X-Session-Id": "relay-session" },
|
|
body: {
|
|
model: "relay-combo",
|
|
stream: false,
|
|
messages: [{ role: "user", content: "Continue from where you left off" }],
|
|
},
|
|
})
|
|
);
|
|
const secondJson = (await secondResponse.json()) as any;
|
|
|
|
assert.equal(secondResponse.status, 200);
|
|
assert.equal(secondJson.choices[0].message.content, "relay-success");
|
|
assert.equal(upstreamBodies.length >= 2, true);
|
|
assert.match(upstreamBodies[1].serializedBody, /<context_handoff>/);
|
|
assert.match(upstreamBodies[1].serializedBody, /Carry over the router implementation state/);
|
|
assert.equal(handoffDb.getHandoff(sessionId, "relay-combo"), null);
|
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
});
|
|
|
|
test("handleChat injects context-relay handoffs during live failover for Responses-native Codex requests", async () => {
|
|
BaseExecutor.RETRY_CONFIG.delayMs = 1;
|
|
|
|
const primary = await seedCodexOAuthConnection({
|
|
name: "codex-live-a",
|
|
email: "relay-live-a@example.com",
|
|
accessToken: "token-a",
|
|
refreshToken: "refresh-a",
|
|
workspaceId: "ws-a",
|
|
priority: 1,
|
|
});
|
|
await seedCodexOAuthConnection({
|
|
name: "codex-live-b",
|
|
email: "relay-live-b@example.com",
|
|
accessToken: "token-b",
|
|
refreshToken: "refresh-b",
|
|
workspaceId: "ws-b",
|
|
priority: 2,
|
|
});
|
|
|
|
await combosDb.createCombo({
|
|
name: "relay-live-combo",
|
|
strategy: "context-relay",
|
|
config: {
|
|
maxRetries: 0,
|
|
retryDelayMs: 0,
|
|
handoffThreshold: 0.85,
|
|
maxMessagesForSummary: 12,
|
|
},
|
|
models: ["codex/gpt-5.4"],
|
|
});
|
|
|
|
const upstreamBodies = [];
|
|
let primaryRequestCount = 0;
|
|
|
|
(globalThis as any).fetch = async (url: any, init: any = {}) => {
|
|
const urlStr = String(url);
|
|
const headers = toPlainHeaders(init.headers);
|
|
const authHeader = headers.authorization || headers.Authorization || "";
|
|
|
|
if (urlStr.includes("/backend-api/wham/usage")) {
|
|
return authHeader === "Bearer token-a" ? buildQuotaResponse(87) : buildQuotaResponse(20);
|
|
}
|
|
|
|
const body = init.body ? JSON.parse(String(init.body)) : {};
|
|
const serializedBody = JSON.stringify(body);
|
|
const isSummaryRequest =
|
|
body._omnirouteInternalRequest === "context-handoff" ||
|
|
serializedBody.includes("You are a context summarizer");
|
|
|
|
if (isSummaryRequest) {
|
|
return buildResponsesResponse(
|
|
JSON.stringify({
|
|
summary: "Carry over the Responses-native Codex session",
|
|
keyDecisions: ["Keep the request in /responses format"],
|
|
taskProgress: "Continue after the first account is exhausted",
|
|
activeEntities: ["src/sse/handlers/chat.ts", "open-sse/services/contextHandoff.ts"],
|
|
}),
|
|
"gpt-5.4"
|
|
);
|
|
}
|
|
|
|
upstreamBodies.push({ authHeader, body, serializedBody });
|
|
|
|
if (authHeader === "Bearer token-a") {
|
|
primaryRequestCount += 1;
|
|
if (primaryRequestCount >= 2) {
|
|
return new Response(JSON.stringify({ error: { message: "quota exceeded" } }), {
|
|
status: 429,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
}
|
|
}
|
|
|
|
return buildResponsesResponse("relay-success", "gpt-5.4");
|
|
};
|
|
|
|
const firstResponse = await handleChat(
|
|
buildRequest({
|
|
url: "http://localhost/v1/responses",
|
|
headers: {
|
|
"X-Session-Id": "relay-live-session",
|
|
"X-OmniRoute-No-Cache": "true",
|
|
},
|
|
body: {
|
|
model: "relay-live-combo",
|
|
stream: false,
|
|
instructions: "Preserve the original Responses instructions",
|
|
input: [
|
|
{
|
|
type: "message",
|
|
role: "user",
|
|
content: [{ type: "input_text", text: "Keep implementing the new combo" }],
|
|
},
|
|
],
|
|
},
|
|
})
|
|
);
|
|
|
|
assert.equal(firstResponse.status, 200);
|
|
|
|
const sessionId = "ext:relay-live-session";
|
|
const savedHandoff = await waitFor(
|
|
() => handoffDb.getHandoff(sessionId, "relay-live-combo"),
|
|
2000
|
|
);
|
|
assert.ok(savedHandoff);
|
|
assert.equal(savedHandoff.fromAccount, primary.id);
|
|
|
|
const secondResponse = await handleChat(
|
|
buildRequest({
|
|
url: "http://localhost/v1/responses",
|
|
headers: {
|
|
"X-Session-Id": "relay-live-session",
|
|
"X-OmniRoute-No-Cache": "true",
|
|
},
|
|
body: {
|
|
model: "relay-live-combo",
|
|
stream: false,
|
|
instructions: "Continue with the current task",
|
|
input: [
|
|
{
|
|
type: "message",
|
|
role: "user",
|
|
content: [{ type: "input_text", text: "Continue from where you left off" }],
|
|
},
|
|
],
|
|
},
|
|
})
|
|
);
|
|
|
|
assert.equal(secondResponse.status, 200);
|
|
|
|
const relayedSecondaryCall = upstreamBodies.find(
|
|
(call) =>
|
|
call.authHeader === "Bearer token-b" &&
|
|
typeof call.body.instructions === "string"
|
|
);
|
|
|
|
assert.ok(relayedSecondaryCall, "secondary account should receive a request after primary 429");
|
|
assert.equal("messages" in relayedSecondaryCall.body, false);
|
|
assert.deepEqual(
|
|
relayedSecondaryCall.body.input[0].content[0].text,
|
|
"Continue from where you left off"
|
|
);
|
|
assert.match(relayedSecondaryCall.body.instructions, /Continue with the current task/);
|
|
// The failover request must still target the requested model — the old
|
|
// emergency-fallback detour silently swapped it for openai/gpt-oss-120b.
|
|
assert.notEqual(relayedSecondaryCall.body.model, "openai/gpt-oss-120b");
|
|
// The account switch must deliver the stored handoff to the secondary account
|
|
// (the whole point of context-relay). Before the emergency-fallback fix the
|
|
// switch happened inside the emergency detour with comboStrategy=null, so the
|
|
// handoff was never injected — and therefore never consumed.
|
|
assert.match(
|
|
relayedSecondaryCall.serializedBody,
|
|
/Carry over the Responses-native Codex session/,
|
|
"handoff summary must be injected into the secondary account's request"
|
|
);
|
|
// Delivered handoffs are one-shot: consumed (deleted) after a successful
|
|
// injected request (src/sse/handlers/chat.ts deleteHandoff-on-success).
|
|
assert.equal(handoffDb.getHandoff(sessionId, "relay-live-combo"), null);
|
|
});
|