496 lines
17 KiB
TypeScript
496 lines
17 KiB
TypeScript
// @ts-nocheck
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
|
|
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-provreq-fail-"));
|
|
process.env.DATA_DIR = TEST_DATA_DIR;
|
|
|
|
const core = await import("../../src/lib/db/core.ts");
|
|
const settingsDb = await import("../../src/lib/db/settings.ts");
|
|
const { invalidateDbCache } = await import("../../src/lib/db/readCache.ts");
|
|
const { invalidateCacheControlSettingsCache } =
|
|
await import("../../src/lib/cacheControlSettings.ts");
|
|
const { clearCache } = await import("../../src/lib/semanticCache.ts");
|
|
const { clearIdempotency } = await import("../../src/lib/idempotencyLayer.ts");
|
|
const { getPendingRequests, clearPendingRequests } =
|
|
await import("../../src/lib/usage/usageHistory.ts");
|
|
const { clearInflight } = await import("../../open-sse/services/requestDedup.ts");
|
|
const { resetAll: resetAccountSemaphores } =
|
|
await import("../../open-sse/services/accountSemaphore.ts");
|
|
const { clearModelLock } = await import("../../open-sse/services/accountFallback.ts");
|
|
const { getCallLogs, getCallLogById } = await import("../../src/lib/usage/callLogs.ts");
|
|
const { handleChatCore } = await import("../../open-sse/handlers/chatCore.ts");
|
|
const { resetPayloadRulesConfigForTests } = await import("../../open-sse/services/payloadRules.ts");
|
|
const { CLAUDE_CODE_COMPATIBLE_REDACT_THINKING_BETA, CONTEXT_1M_BETA_HEADER } =
|
|
await import("../../open-sse/services/claudeCodeCompatible.ts");
|
|
|
|
const originalFetch = globalThis.fetch;
|
|
|
|
function noopLog() {
|
|
return { debug() {}, info() {}, warn() {}, error() {} };
|
|
}
|
|
|
|
async function waitFor(fn, timeoutMs = 1500) {
|
|
const startedAt = Date.now();
|
|
while (Date.now() - startedAt < timeoutMs) {
|
|
const result = await fn();
|
|
if (result) return result;
|
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async function waitForAsyncSideEffects() {
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
}
|
|
|
|
async function getLatestCallLog() {
|
|
const rows = await getCallLogs({ limit: 5 });
|
|
if (!Array.isArray(rows) || rows.length === 0) return null;
|
|
return getCallLogById(rows[0].id);
|
|
}
|
|
|
|
async function resetStorage() {
|
|
resetPayloadRulesConfigForTests();
|
|
invalidateCacheControlSettingsCache();
|
|
clearCache();
|
|
clearIdempotency();
|
|
clearInflight();
|
|
clearModelLock();
|
|
core.resetDbInstance();
|
|
// A full reset must also drop the settings read-cache. Otherwise the cached
|
|
// value (e.g. call_log_pipeline_enabled=true seeded earlier) survives the DB
|
|
// wipe and silently masks the fact that the fresh DB has the default. In CI
|
|
// under load this cache is evicted at unpredictable times, so tests that rely
|
|
// on the stale cache flake. Make the reset honest and deterministic here.
|
|
invalidateDbCache("settings");
|
|
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
|
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
|
}
|
|
|
|
// Re-seed per test, NOT once: every `afterEach` runs resetStorage(), which wipes
|
|
// the DB and drops the settings cache. A run-once `before` only guaranteed the
|
|
// flag for the first test; later tests depended on a stale cache surviving the
|
|
// wipe, which flakes under CI load. beforeEach re-establishes it deterministically.
|
|
test.beforeEach(async () => {
|
|
await resetStorage();
|
|
await settingsDb.updateSettings({ call_log_pipeline_enabled: true });
|
|
});
|
|
|
|
test.afterEach(async () => {
|
|
globalThis.fetch = originalFetch;
|
|
clearPendingRequests();
|
|
resetAccountSemaphores();
|
|
await waitForAsyncSideEffects();
|
|
await resetStorage();
|
|
});
|
|
|
|
test.after(async () => {
|
|
globalThis.fetch = originalFetch;
|
|
clearPendingRequests();
|
|
resetAccountSemaphores();
|
|
await resetStorage();
|
|
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
|
});
|
|
|
|
test("network failure persisted call log includes providerRequest in pipeline payloads", async () => {
|
|
const body = {
|
|
model: "gpt-4o-mini",
|
|
stream: false,
|
|
messages: [{ role: "user", content: "hello" }],
|
|
};
|
|
|
|
globalThis.fetch = async () => {
|
|
throw new Error("Connection refused");
|
|
};
|
|
|
|
const result = await handleChatCore({
|
|
body: structuredClone(body),
|
|
modelInfo: { provider: "openai", model: "gpt-4o-mini", extendedContext: false },
|
|
credentials: { apiKey: "sk-test", providerSpecificData: {} },
|
|
log: noopLog(),
|
|
clientRawRequest: {
|
|
endpoint: "/v1/chat/completions",
|
|
body: structuredClone(body),
|
|
headers: new Headers({ accept: "application/json" }),
|
|
},
|
|
userAgent: "unit-test",
|
|
} as any);
|
|
|
|
assert.equal(result.success, false);
|
|
assert.equal(result.status, 502);
|
|
|
|
await waitForAsyncSideEffects();
|
|
|
|
const detail = await waitFor(getLatestCallLog);
|
|
assert.ok(detail, "expected a call log to be persisted");
|
|
|
|
assert.ok(
|
|
detail.pipelinePayloads,
|
|
"expected pipeline payloads when call_log_pipeline_enabled is true"
|
|
);
|
|
assert.ok(
|
|
detail.pipelinePayloads.providerRequest,
|
|
"providerRequest must be present in pipeline payloads even on network failure"
|
|
);
|
|
const providerReqBody =
|
|
detail.pipelinePayloads.providerRequest.body ?? detail.pipelinePayloads.providerRequest;
|
|
assert.equal(
|
|
providerReqBody.model,
|
|
"gpt-4o-mini",
|
|
"providerRequest should contain the translated model"
|
|
);
|
|
const messages =
|
|
providerReqBody.messages ?? (Array.isArray(providerReqBody) ? providerReqBody : null);
|
|
if (messages) {
|
|
assert.equal(messages[0]?.content, "hello");
|
|
}
|
|
assert.equal(
|
|
detail.pipelinePayloads.providerResponse ?? null,
|
|
null,
|
|
"providerResponse should be null/absent on network failure (no response received)"
|
|
);
|
|
assert.ok(detail.pipelinePayloads.error, "pipeline payloads should include the error details");
|
|
});
|
|
|
|
test("network timeout persisted call log includes providerRequest in pipeline payloads", async () => {
|
|
const { getExecutor } = await import("../../open-sse/executors/index.ts");
|
|
const executor = getExecutor("openai");
|
|
const originalGetTimeoutMs = executor.getTimeoutMs?.bind(executor);
|
|
executor.getTimeoutMs = () => 200;
|
|
|
|
const body = {
|
|
model: "gpt-4o-mini",
|
|
stream: false,
|
|
messages: [{ role: "user", content: "timeout test" }],
|
|
};
|
|
|
|
globalThis.fetch = async () => {
|
|
return new Promise(() => {}); // never resolve
|
|
};
|
|
|
|
try {
|
|
const invocation = handleChatCore({
|
|
body: structuredClone(body),
|
|
modelInfo: { provider: "openai", model: "gpt-4o-mini", extendedContext: false },
|
|
credentials: { apiKey: "sk-test", providerSpecificData: {} },
|
|
log: noopLog(),
|
|
clientRawRequest: {
|
|
endpoint: "/v1/chat/completions",
|
|
body: structuredClone(body),
|
|
headers: new Headers({ accept: "application/json" }),
|
|
},
|
|
userAgent: "unit-test",
|
|
} as any);
|
|
|
|
const result = await invocation;
|
|
await waitForAsyncSideEffects();
|
|
|
|
assert.equal(result.success, false);
|
|
assert.ok(result.status === 504, `expected 504 timeout, got ${result.status}`);
|
|
|
|
const detail = await waitFor(getLatestCallLog);
|
|
assert.ok(detail, "expected a call log to be persisted");
|
|
assert.ok(detail.pipelinePayloads, "expected pipeline payloads");
|
|
|
|
assert.ok(
|
|
detail.pipelinePayloads?.providerRequest,
|
|
"providerRequest must be present in pipeline payloads on timeout"
|
|
);
|
|
const providerReqBody =
|
|
detail.pipelinePayloads?.providerRequest?.body ?? detail.pipelinePayloads?.providerRequest;
|
|
assert.equal(providerReqBody?.model, "gpt-4o-mini");
|
|
} finally {
|
|
if (originalGetTimeoutMs) executor.getTimeoutMs = originalGetTimeoutMs;
|
|
}
|
|
});
|
|
|
|
test("provider error response (HTTP 502) includes both providerRequest and providerResponse in pipeline", async () => {
|
|
const body = {
|
|
model: "gpt-4o-mini",
|
|
stream: false,
|
|
messages: [{ role: "user", content: "trigger 502" }],
|
|
};
|
|
|
|
globalThis.fetch = async () => {
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: { message: "Upstream provider error", type: "server_error" },
|
|
}),
|
|
{
|
|
status: 502,
|
|
headers: { "Content-Type": "application/json" },
|
|
}
|
|
);
|
|
};
|
|
|
|
const result = await handleChatCore({
|
|
body: structuredClone(body),
|
|
modelInfo: { provider: "openai", model: "gpt-4o-mini", extendedContext: false },
|
|
credentials: { apiKey: "sk-test", providerSpecificData: {} },
|
|
log: noopLog(),
|
|
clientRawRequest: {
|
|
endpoint: "/v1/chat/completions",
|
|
body: structuredClone(body),
|
|
headers: new Headers({ accept: "application/json" }),
|
|
},
|
|
userAgent: "unit-test",
|
|
} as any);
|
|
|
|
assert.equal(result.success, false);
|
|
assert.equal(result.status, 502);
|
|
|
|
await waitForAsyncSideEffects();
|
|
|
|
const detail = await waitFor(getLatestCallLog);
|
|
assert.ok(detail, "expected a call log to be persisted");
|
|
|
|
assert.ok(detail.pipelinePayloads, "expected pipeline payloads");
|
|
assert.ok(
|
|
detail.pipelinePayloads.providerRequest,
|
|
"providerRequest must be present on HTTP error response"
|
|
);
|
|
assert.ok(
|
|
detail.pipelinePayloads.providerResponse,
|
|
"providerResponse must be present on HTTP error response (upstream responded)"
|
|
);
|
|
assert.equal(
|
|
detail.pipelinePayloads.providerResponse.status,
|
|
502,
|
|
"providerResponse status should reflect the upstream error"
|
|
);
|
|
});
|
|
|
|
test("successful response includes both providerRequest and providerResponse in pipeline", async () => {
|
|
const body = {
|
|
model: "gpt-4o-mini",
|
|
stream: false,
|
|
messages: [{ role: "user", content: "hello" }],
|
|
};
|
|
|
|
globalThis.fetch = async () => {
|
|
return new Response(
|
|
JSON.stringify({
|
|
id: "chatcmpl-ok",
|
|
object: "chat.completion",
|
|
model: "gpt-4o-mini",
|
|
choices: [
|
|
{
|
|
index: 0,
|
|
message: { role: "assistant", content: "world" },
|
|
finish_reason: "stop",
|
|
},
|
|
],
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"cf-ray": "response-ray",
|
|
server: "cloudflare",
|
|
},
|
|
}
|
|
);
|
|
};
|
|
|
|
const result = await handleChatCore({
|
|
body: structuredClone(body),
|
|
modelInfo: { provider: "openai", model: "gpt-4o-mini", extendedContext: false },
|
|
credentials: { apiKey: "sk-test", providerSpecificData: {} },
|
|
log: noopLog(),
|
|
clientRawRequest: {
|
|
endpoint: "/v1/chat/completions",
|
|
body: structuredClone(body),
|
|
headers: new Headers({ accept: "application/json" }),
|
|
},
|
|
userAgent: "unit-test",
|
|
} as any);
|
|
|
|
assert.equal(result.success, true);
|
|
|
|
await waitForAsyncSideEffects();
|
|
|
|
const detail = await waitFor(getLatestCallLog);
|
|
assert.ok(detail, "expected a call log to be persisted");
|
|
|
|
assert.ok(detail.pipelinePayloads, "expected pipeline payloads");
|
|
assert.ok(detail.pipelinePayloads.providerRequest, "providerRequest must be present on success");
|
|
assert.ok(
|
|
detail.pipelinePayloads.providerResponse,
|
|
"providerResponse must be present on success"
|
|
);
|
|
assert.equal(
|
|
detail.pipelinePayloads.providerRequest.headers["cf-ray"],
|
|
undefined,
|
|
"providerRequest headers must not be overwritten with upstream response headers"
|
|
);
|
|
assert.equal(
|
|
detail.pipelinePayloads.providerRequest.headers.server,
|
|
undefined,
|
|
"providerRequest headers must not include upstream response server header"
|
|
);
|
|
assert.equal(detail.pipelinePayloads.providerRequest.headers.Accept, "application/json");
|
|
assert.equal(detail.pipelinePayloads.providerRequest.headers["Content-Type"], "application/json");
|
|
assert.equal(detail.pipelinePayloads.providerRequest.headers.Authorization, "[REDACTED]");
|
|
});
|
|
|
|
test("streaming response preserves request headers in providerRequest pipeline payload", async () => {
|
|
const body = {
|
|
model: "gpt-4o-mini",
|
|
stream: true,
|
|
messages: [{ role: "user", content: "hello" }],
|
|
};
|
|
|
|
const upstreamPayload = [
|
|
`data: ${JSON.stringify({
|
|
id: "chatcmpl-stream",
|
|
object: "chat.completion.chunk",
|
|
model: "gpt-4o-mini",
|
|
choices: [{ index: 0, delta: { role: "assistant", content: "world" } }],
|
|
})}`,
|
|
"",
|
|
`data: ${JSON.stringify({
|
|
id: "chatcmpl-stream",
|
|
object: "chat.completion.chunk",
|
|
model: "gpt-4o-mini",
|
|
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
|
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
|
|
})}`,
|
|
"",
|
|
"data: [DONE]",
|
|
"",
|
|
].join("\n");
|
|
|
|
globalThis.fetch = async () => {
|
|
return new Response(upstreamPayload, {
|
|
status: 200,
|
|
headers: {
|
|
"Content-Type": "text/event-stream",
|
|
"cf-ray": "stream-response-ray",
|
|
server: "cloudflare",
|
|
},
|
|
});
|
|
};
|
|
|
|
const result = await handleChatCore({
|
|
body: structuredClone(body),
|
|
modelInfo: { provider: "openai", model: "gpt-4o-mini", extendedContext: false },
|
|
credentials: { apiKey: "sk-test", providerSpecificData: {} },
|
|
log: noopLog(),
|
|
clientRawRequest: {
|
|
endpoint: "/v1/chat/completions",
|
|
body: structuredClone(body),
|
|
headers: new Headers({ accept: "text/event-stream" }),
|
|
},
|
|
userAgent: "unit-test",
|
|
} as any);
|
|
|
|
assert.equal(result.success, true);
|
|
await result.response.text();
|
|
await waitForAsyncSideEffects();
|
|
|
|
const detail = await waitFor(getLatestCallLog);
|
|
assert.ok(detail, "expected a call log to be persisted");
|
|
assert.ok(detail.pipelinePayloads, "expected pipeline payloads");
|
|
|
|
const providerRequest = detail.pipelinePayloads.providerRequest;
|
|
assert.ok(providerRequest, "providerRequest must be present on streaming success");
|
|
assert.equal(
|
|
providerRequest.headers["cf-ray"],
|
|
undefined,
|
|
"streaming providerRequest headers must not be response headers"
|
|
);
|
|
assert.equal(providerRequest.headers.server, undefined);
|
|
assert.equal(providerRequest.headers.Accept, "text/event-stream");
|
|
assert.equal(providerRequest.headers["Content-Type"], "application/json");
|
|
assert.equal(providerRequest.headers.Authorization, "[REDACTED]");
|
|
});
|
|
|
|
test("CC-compatible providerRequest log keeps request beta headers and summarized thinking body", async () => {
|
|
const body = {
|
|
model: "claude-opus-4-6",
|
|
messages: [{ role: "user", content: "hello" }],
|
|
stream: false,
|
|
};
|
|
|
|
const upstreamPayload = [
|
|
"event: message_start",
|
|
'data: {"type":"message_start","message":{"id":"msg_1","type":"message","role":"assistant","model":"claude-opus-4-6","usage":{"input_tokens":7,"output_tokens":0}}}',
|
|
"",
|
|
"event: content_block_start",
|
|
'data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}',
|
|
"",
|
|
"event: content_block_delta",
|
|
'data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"ok"}}',
|
|
"",
|
|
"event: message_delta",
|
|
'data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":1}}',
|
|
"",
|
|
"event: message_stop",
|
|
'data: {"type":"message_stop"}',
|
|
"",
|
|
].join("\n");
|
|
|
|
globalThis.fetch = async () => {
|
|
return new Response(upstreamPayload, {
|
|
status: 200,
|
|
headers: {
|
|
"Content-Type": "text/event-stream",
|
|
"cf-ray": "cc-response-ray",
|
|
server: "cloudflare",
|
|
},
|
|
});
|
|
};
|
|
|
|
const result = await handleChatCore({
|
|
body: structuredClone(body),
|
|
modelInfo: {
|
|
provider: "anthropic-compatible-cc-test",
|
|
model: "claude-opus-4-6",
|
|
extendedContext: false,
|
|
},
|
|
credentials: {
|
|
apiKey: "sk-test",
|
|
providerSpecificData: {
|
|
baseUrl: "https://proxy.example.com",
|
|
requestDefaults: {
|
|
context1m: true,
|
|
redactThinking: true,
|
|
summarizeThinking: true,
|
|
},
|
|
},
|
|
},
|
|
log: noopLog(),
|
|
clientRawRequest: {
|
|
endpoint: "/v1/chat/completions",
|
|
body: structuredClone(body),
|
|
headers: new Headers({ accept: "application/json" }),
|
|
},
|
|
userAgent: "unit-test",
|
|
} as any);
|
|
|
|
assert.equal(result.success, true);
|
|
await result.response.json();
|
|
await waitForAsyncSideEffects();
|
|
|
|
const detail = await waitFor(getLatestCallLog);
|
|
assert.ok(detail, "expected a call log to be persisted");
|
|
assert.ok(detail.pipelinePayloads, "expected pipeline payloads");
|
|
|
|
const providerRequest = detail.pipelinePayloads.providerRequest;
|
|
assert.ok(providerRequest, "providerRequest must be present on CC-compatible success");
|
|
assert.equal(providerRequest.headers["cf-ray"], undefined);
|
|
assert.equal(providerRequest.headers.server, undefined);
|
|
assert.equal(providerRequest.headers.Accept, "text/event-stream");
|
|
assert.match(providerRequest.headers["anthropic-beta"], new RegExp(CONTEXT_1M_BETA_HEADER));
|
|
assert.match(
|
|
providerRequest.headers["anthropic-beta"],
|
|
new RegExp(CLAUDE_CODE_COMPATIBLE_REDACT_THINKING_BETA)
|
|
);
|
|
assert.equal(providerRequest.body.thinking.display, "summarized");
|
|
});
|