SysDeck/klanker-gate/tests/integration/anthropic_ingress_test.ts

876 lines
28 KiB
TypeScript
Executable File

// WP8a: the Anthropic front door. Three things are proven here.
//
// 1. /v1/messages runs the SHARED canonical execution (runChatCompletion), so
// plugins, the tool loop and the rest of the pipeline apply to the Anthropic
// surface exactly as they do to /v1/chat/completions.
// 2. Request/response/stream FIDELITY: images, tool_choice, unmapped native
// fields, thinking blocks, and the streamed input_tokens that used to be
// hardcoded to 0.
// 3. The dialect front door in compat_families.ts: credential promotion
// (x-api-key, ?key=), URL hygiene, and native error envelopes.
import { assert, assertEquals, assertFalse } from "@std/assert";
import { createHandler } from "../../apps/gateway/main.ts";
import {
type AppContext,
NullToolExecutor,
VERSION,
} from "../../apps/gateway/context.ts";
import { ProviderManager } from "../../packages/providers/src/mod.ts";
import { Metrics } from "../../packages/telemetry/src/metrics.ts";
import { LogBus } from "../../packages/telemetry/src/logbus.ts";
import { VirtualKeyManager } from "../../packages/governance/src/virtual_keys.ts";
import { MCPRegistry } from "../../packages/mcp/src/registry.ts";
import { PluginManager } from "../../packages/plugins/src/lifecycle.ts";
import {
jsonResponse,
MockProvider,
openAIChatBody,
sseResponse,
} from "../../packages/testing/src/mod.ts";
const MODEL = "openai/gpt-4o";
function makeContext(
mockUrl: string,
plugins = new PluginManager(),
): AppContext {
return {
providers: new ProviderManager([{
id: "openai",
type: "openai",
apiKey: "sk-test",
baseUrl: mockUrl,
enabled: true,
models: ["gpt-4o"],
priority: 0,
retry: { maxRetries: 0 },
}], "openai"),
metrics: new Metrics(),
logBus: new LogBus(),
virtualKeys: new VirtualKeyManager(),
mcp: new MCPRegistry(),
plugins,
toolExecutor: new NullToolExecutor(),
version: VERSION,
};
}
/** POST /v1/messages with an Anthropic-native body. */
function messages(
body: Record<string, unknown>,
headers: Record<string, string> = {},
init: RequestInit = {},
): Request {
return new Request("http://gateway.test/v1/messages", {
method: "POST",
headers: { "Content-Type": "application/json", ...headers },
body: JSON.stringify({ model: MODEL, max_tokens: 64, ...body }),
...init,
});
}
interface CanonicalEgress {
model: string;
messages: Array<Record<string, unknown>>;
tool_choice?: unknown;
stream?: boolean;
temperature?: number;
[key: string]: unknown;
}
interface AnthropicBody {
type: string;
role: string;
content: Array<Record<string, unknown>>;
stop_reason: string;
usage: { input_tokens: number; output_tokens: number };
}
interface NamedEvent {
event?: string;
data: Record<string, unknown> | "[DONE]";
}
/** readSSE variant that keeps `event:` names alongside the data payloads. */
async function readNamedSSE(response: Response): Promise<NamedEvent[]> {
const text = await response.text();
const events: NamedEvent[] = [];
let event: string | undefined;
for (const line of text.split("\n")) {
if (line.startsWith("event: ")) {
event = line.slice(7).trim();
} else if (line.startsWith("data: ")) {
const data = line.slice(6).trim();
events.push({
event,
data: data === "[DONE]"
? "[DONE]"
: JSON.parse(data) as Record<string, unknown>,
});
event = undefined;
}
}
return events;
}
/** One canonical chat.completion.chunk SSE frame. */
function chunk(
delta: Record<string, unknown>,
extra: Record<string, unknown> = {},
): string {
return `data: ${
JSON.stringify({
id: "chatcmpl-mock",
object: "chat.completion.chunk",
created: 1700000000,
model: "mock-model",
choices: [{ index: 0, delta, finish_reason: null }],
...extra,
})
}\n\n`;
}
/**
* Canonical stream carrying reasoning fragments ahead of the answer text and a
* usage-bearing finish chunk. `prompt_tokens` is the value the Anthropic
* message_delta must report as `input_tokens`.
*/
function reasoningStreamFrames(promptTokens: number): string[] {
return [
chunk({ role: "assistant", reasoning_content: "Let me " }),
chunk({ reasoning_content: "think." }),
chunk({ content: "Answer" }),
`data: ${
JSON.stringify({
id: "chatcmpl-mock",
object: "chat.completion.chunk",
created: 1700000000,
model: "mock-model",
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
usage: {
prompt_tokens: promptTokens,
completion_tokens: 7,
total_tokens: promptTokens + 7,
},
})
}\n\n`,
"data: [DONE]\n\n",
];
}
// --------------------------------------------------------- request fidelity
Deno.test("anthropic ingress: base64 image block becomes a canonical data URL part", async () => {
const mock = new MockProvider(() => jsonResponse(openAIChatBody("ok")));
const handler = createHandler(makeContext(mock.url));
try {
const res = await handler(messages({
messages: [{
role: "user",
content: [
{ type: "text", text: "what is this" },
{
type: "image",
source: {
type: "base64",
media_type: "image/png",
data: "iVBORw0KGgo=",
},
},
],
}],
}));
assertEquals(res.status, 200);
await res.body?.cancel();
const sent = mock.calls[0].body as CanonicalEgress;
// The base64 source is rebuilt as a data: URL, byte for byte - this is the
// exact inverse of the Anthropic adapter's egress mapping.
assertEquals(sent.messages, [{
role: "user",
content: [
{ type: "text", text: "what is this" },
{
type: "image_url",
image_url: { url: "data:image/png;base64,iVBORw0KGgo=" },
},
],
}]);
} finally {
await mock.close();
}
});
Deno.test("anthropic ingress: url image source reaches egress unchanged", async () => {
const mock = new MockProvider(() => jsonResponse(openAIChatBody("ok")));
const handler = createHandler(makeContext(mock.url));
try {
const res = await handler(messages({
messages: [{
role: "user",
content: [{
type: "image",
source: { type: "url", url: "https://example.test/cat.png" },
}],
}],
}));
assertEquals(res.status, 200);
await res.body?.cancel();
const sent = mock.calls[0].body as CanonicalEgress;
// No text block in this turn, so the part array holds the image alone.
assertEquals(sent.messages, [{
role: "user",
content: [{
type: "image_url",
image_url: { url: "https://example.test/cat.png" },
}],
}]);
} finally {
await mock.close();
}
});
Deno.test("anthropic ingress: every tool_choice shape maps to its canonical form", async () => {
const mock = new MockProvider(() => jsonResponse(openAIChatBody("ok")));
const handler = createHandler(makeContext(mock.url));
const cases: Array<{ anthropic: unknown; canonical: unknown }> = [
{ anthropic: { type: "auto" }, canonical: "auto" },
{ anthropic: { type: "any" }, canonical: "required" },
{ anthropic: { type: "none" }, canonical: "none" },
{
anthropic: { type: "tool", name: "get_weather" },
canonical: { type: "function", function: { name: "get_weather" } },
},
];
try {
for (const [i, testCase] of cases.entries()) {
const res = await handler(messages({
messages: [{ role: "user", content: "weather?" }],
tools: [{
name: "get_weather",
description: "Current weather",
input_schema: { type: "object", properties: {} },
}],
tool_choice: testCase.anthropic,
}));
assertEquals(res.status, 200);
await res.body?.cancel();
const sent = mock.calls[i].body as CanonicalEgress;
assertEquals(
sent.tool_choice,
testCase.canonical,
`tool_choice ${JSON.stringify(testCase.anthropic)}`,
);
}
assertEquals(mock.calls.length, cases.length);
} finally {
await mock.close();
}
});
Deno.test("anthropic ingress: unmapped native fields ride through to egress", async () => {
const mock = new MockProvider(() => jsonResponse(openAIChatBody("ok")));
const handler = createHandler(makeContext(mock.url));
try {
const res = await handler(messages({
messages: [{ role: "user", content: "hei" }],
top_k: 40,
thinking: { type: "enabled", budget_tokens: 1024 },
metadata: { user_id: "u-1" },
service_tier: "auto",
// An entirely unknown vendor key: the passthrough schema admits it and
// the canonical translation must not silently drop it either.
frosty_unknown_field: "carried",
}));
assertEquals(res.status, 200);
await res.body?.cancel();
const sent = mock.calls[0].body as CanonicalEgress;
assertEquals(sent.top_k, 40);
assertEquals(sent.thinking, { type: "enabled", budget_tokens: 1024 });
assertEquals(sent.metadata, { user_id: "u-1" });
assertEquals(sent.service_tier, "auto");
assertEquals(sent.frosty_unknown_field, "carried");
// The mapped fields still map (the passthrough spread must not shadow them).
assertEquals(sent.model, "gpt-4o");
assertEquals(sent.max_tokens, 64);
} finally {
await mock.close();
}
});
Deno.test("anthropic ingress: text-only turns keep plain-string content", async () => {
const mock = new MockProvider(() => jsonResponse(openAIChatBody("ok")));
const handler = createHandler(makeContext(mock.url));
try {
const res = await handler(messages({
system: "Vaer presis.",
messages: [
{ role: "user", content: "hei" },
{ role: "assistant", content: "hallo" },
// A text-only BLOCK ARRAY must still collapse to a plain string: the
// image work must not churn the shape of existing text-only traffic.
{ role: "user", content: [{ type: "text", text: "and again" }] },
],
}));
assertEquals(res.status, 200);
await res.body?.cancel();
const sent = mock.calls[0].body as CanonicalEgress;
assertEquals(sent.messages, [
{ role: "system", content: "Vaer presis." },
{ role: "user", content: "hei" },
{ role: "assistant", content: "hallo" },
{ role: "user", content: "and again" },
]);
} finally {
await mock.close();
}
});
Deno.test("anthropic ingress: tool_use and tool_result round trip", async () => {
const mock = new MockProvider(() =>
jsonResponse(openAIChatBody("12 degrees"))
);
const handler = createHandler(makeContext(mock.url));
try {
const res = await handler(messages({
messages: [
{ role: "user", content: "weather?" },
{
role: "assistant",
content: [{
type: "tool_use",
id: "toolu_1",
name: "get_weather",
input: { city: "Oslo" },
}],
},
{
role: "user",
content: [{
type: "tool_result",
tool_use_id: "toolu_1",
content: "12C",
}],
},
],
}));
assertEquals(res.status, 200);
await res.body?.cancel();
const sent = mock.calls[0].body as CanonicalEgress;
assertEquals(sent.messages, [
{ role: "user", content: "weather?" },
{
role: "assistant",
content: null,
tool_calls: [{
id: "toolu_1",
type: "function",
function: {
name: "get_weather",
arguments: JSON.stringify({ city: "Oslo" }),
},
}],
},
// A tool_result turn becomes a canonical `tool` message and does NOT
// also emit an empty user turn.
{ role: "tool", tool_call_id: "toolu_1", content: "12C" },
]);
} finally {
await mock.close();
}
});
// -------------------------------------------------------- response fidelity
Deno.test("anthropic ingress: reasoning_content becomes a leading thinking block", async () => {
const mock = new MockProvider(() =>
jsonResponse(openAIChatBody("", {
choices: [{
index: 0,
message: {
role: "assistant",
content: "The answer is 4.",
reasoning_content: "Two plus two.",
},
finish_reason: "stop",
}],
}))
);
const handler = createHandler(makeContext(mock.url));
try {
const res = await handler(
messages({ messages: [{ role: "user", content: "2+2" }] }),
);
assertEquals(res.status, 200);
const body = await res.json() as AnthropicBody;
// Thinking leads the block list, exactly where extended thinking puts it
// on the real wire; the text block follows it.
assertEquals(body.content[0], {
type: "thinking",
thinking: "Two plus two.",
});
assertEquals(body.content[1], { type: "text", text: "The answer is 4." });
assertEquals(body.content.length, 2);
} finally {
await mock.close();
}
});
Deno.test("anthropic ingress: tool_calls become tool_use blocks", async () => {
const mock = new MockProvider(() =>
jsonResponse(openAIChatBody("", {
choices: [{
index: 0,
message: {
role: "assistant",
content: null,
tool_calls: [{
id: "call_1",
type: "function",
function: {
name: "get_weather",
arguments: JSON.stringify({ city: "Oslo" }),
},
}],
},
finish_reason: "tool_calls",
}],
}))
);
const handler = createHandler(makeContext(mock.url));
try {
const res = await handler(
messages({ messages: [{ role: "user", content: "weather?" }] }),
);
assertEquals(res.status, 200);
const body = await res.json() as AnthropicBody;
assertEquals(body.content, [{
type: "tool_use",
id: "call_1",
name: "get_weather",
input: { city: "Oslo" },
}]);
assertEquals(body.stop_reason, "tool_use");
} finally {
await mock.close();
}
});
// ---------------------------------------------------------- stream fidelity
Deno.test("anthropic ingress: stream opens a thinking block before the text block", async () => {
const mock = new MockProvider(() => sseResponse(reasoningStreamFrames(11)));
const handler = createHandler(makeContext(mock.url));
try {
const res = await handler(messages({
messages: [{ role: "user", content: "2+2" }],
stream: true,
}));
assertEquals(res.status, 200);
assertEquals(res.headers.get("Content-Type"), "text/event-stream");
// `stream` must survive anthropicToCanonical or the upstream call is not
// a streaming one at all.
assertEquals((mock.calls[0].body as CanonicalEgress).stream, true);
const events = await readNamedSSE(res);
assertEquals(events.map((e) => e.event), [
"message_start",
"content_block_start",
"content_block_delta",
"content_block_delta",
"content_block_stop",
"content_block_start",
"content_block_delta",
"content_block_stop",
"message_delta",
"message_stop",
]);
const starts = events.filter((e) => e.event === "content_block_start")
.map((e) => e.data as { index: number; content_block: { type: string } });
assertEquals(starts[0].content_block.type, "thinking");
assertEquals(starts[1].content_block.type, "text");
// The thinking block is opened FIRST, so its index precedes the text block.
assert(
starts[0].index < starts[1].index,
`thinking index ${starts[0].index} must precede text ${starts[1].index}`,
);
const deltas = events.filter((e) => e.event === "content_block_delta")
.map((
e,
) => (e.data as { index: number; delta: Record<string, unknown> }));
assertEquals(deltas[0].delta, {
type: "thinking_delta",
thinking: "Let me ",
});
assertEquals(deltas[1].delta, {
type: "thinking_delta",
thinking: "think.",
});
assertEquals(deltas[0].index, starts[0].index);
assertEquals(deltas[2].delta, { type: "text_delta", text: "Answer" });
assertEquals(deltas[2].index, starts[1].index);
} finally {
await mock.close();
}
});
Deno.test("anthropic ingress: stream message_delta reports the canonical prompt_tokens", async () => {
const mock = new MockProvider(() => sseResponse(reasoningStreamFrames(1234)));
const handler = createHandler(makeContext(mock.url));
try {
const res = await handler(messages({
messages: [{ role: "user", content: "2+2" }],
stream: true,
}));
assertEquals(res.status, 200);
const events = await readNamedSSE(res);
const delta = events.find((e) => e.event === "message_delta")!
.data as { usage: { input_tokens: number; output_tokens: number } };
// Regression guard: input_tokens was hardcoded to 0 here, which silently
// under-billed every streamed Anthropic request.
assertEquals(delta.usage.input_tokens, 1234);
assertEquals(delta.usage.output_tokens, 7);
} finally {
await mock.close();
}
});
Deno.test("anthropic ingress: stream ends message_delta then message_stop with no [DONE]", async () => {
const mock = new MockProvider(() => sseResponse(reasoningStreamFrames(3)));
const handler = createHandler(makeContext(mock.url));
try {
const res = await handler(messages({
messages: [{ role: "user", content: "2+2" }],
stream: true,
}));
assertEquals(res.status, 200);
const events = await readNamedSSE(res);
assertEquals(events.at(-2)!.event, "message_delta");
assertEquals(events.at(-1)!.event, "message_stop");
// The Anthropic surface has no [DONE] sentinel; that is canonical-only.
assertFalse(events.some((e) => e.data === "[DONE]"));
const stop = events.at(-2)!.data as { delta: { stop_reason: string } };
assertEquals(stop.delta.stop_reason, "end_turn");
} finally {
await mock.close();
}
});
// ------------------------------------------------------ front door / admission
const VK_TOKEN = "vk-anthropic-frontdoor-token";
/** Context with governance ACTIVE (one enabled virtual key). */
function guardedContext(
mockUrl: string,
rateLimit?: { maxRequests: number; windowMs: number },
): AppContext {
const ctx = makeContext(mockUrl);
ctx.virtualKeys.upsert({
id: "vk",
name: "gate",
token: VK_TOKEN,
enabled: true,
usedRequests: 0,
usedCostMicroUsd: 0,
...(rateLimit ? { rateLimit } : {}),
});
return ctx;
}
Deno.test("anthropic front door: x-api-key admits, no credential is 401", async () => {
const mock = new MockProvider(() => jsonResponse(openAIChatBody("ok")));
const handler = createHandler(guardedContext(mock.url));
const body = { messages: [{ role: "user", content: "hei" }] };
try {
// A stock Anthropic SDK cannot send Authorization: Bearer at all.
const admitted = await handler(
messages(body, { "x-api-key": VK_TOKEN }),
);
assertEquals(admitted.status, 200);
await admitted.body?.cancel();
const denied = await handler(messages(body));
assertEquals(denied.status, 401);
await denied.body?.cancel();
// The denied request never reached a provider.
assertEquals(mock.calls.length, 1);
} finally {
await mock.close();
}
});
Deno.test("anthropic front door: an existing bearer is never overridden by x-api-key", async () => {
const mock = new MockProvider(() => jsonResponse(openAIChatBody("ok")));
const handler = createHandler(guardedContext(mock.url));
try {
const res = await handler(messages(
{ messages: [{ role: "user", content: "hei" }] },
{
"Authorization": `Bearer ${VK_TOKEN}`,
// Garbage that WOULD 401 if promotion clobbered the bearer.
"x-api-key": "vk-garbage-not-a-real-key",
},
));
assertEquals(res.status, 200);
await res.body?.cancel();
} finally {
await mock.close();
}
});
Deno.test("anthropic front door: a governance 401 uses the Anthropic error envelope", async () => {
const mock = new MockProvider(() => jsonResponse(openAIChatBody("ok")));
const handler = createHandler(guardedContext(mock.url));
try {
const res = await handler(
messages({ messages: [{ role: "user", content: "hei" }] }),
);
assertEquals(res.status, 401);
const body = await res.json() as {
type: string;
error: { type: string; message: string };
};
// The Anthropic SDK parses THIS shape, not the canonical
// {error:{message,type,param,code}} envelope.
assertEquals(body.type, "error");
assertEquals(body.error.type, "authentication_error");
assert(body.error.message.length > 0);
// Reshaping must carry the response headers over intact.
assert(res.headers.get("x-request-id"));
assertEquals(res.headers.get("Content-Type"), "application/json");
} finally {
await mock.close();
}
});
Deno.test("anthropic front door: a reshaped 429 keeps Retry-After", async () => {
const mock = new MockProvider(() => jsonResponse(openAIChatBody("ok")));
const handler = createHandler(
guardedContext(mock.url, { maxRequests: 1, windowMs: 60_000 }),
);
const body = { messages: [{ role: "user", content: "hei" }] };
try {
const first = await handler(messages(body, { "x-api-key": VK_TOKEN }));
assertEquals(first.status, 200);
await first.body?.cancel();
const limited = await handler(messages(body, { "x-api-key": VK_TOKEN }));
assertEquals(limited.status, 429);
// Retry-After survives the envelope rewrite - a client that loses it has
// no way to back off correctly.
const retryAfter = limited.headers.get("Retry-After");
assert(retryAfter, "Retry-After was dropped by the dialect reshape");
assert(Number(retryAfter) >= 1, `Retry-After ${retryAfter} must be >= 1`);
assert(limited.headers.get("x-request-id"));
const envelope = await limited.json() as {
type: string;
error: { type: string };
};
assertEquals(envelope.type, "error");
assertEquals(envelope.error.type, "rate_limit_error");
} finally {
await mock.close();
}
});
// ----------------------------------------------------------- URL hygiene
/**
* Captures the access log the request logger writes. It prints the FULL URL
* after the front-door rewrite, which is the only place the rewritten URL is
* observable - and is exactly the sink `?key=` stripping exists to protect.
*/
async function withAccessLog(
run: () => Promise<void>,
): Promise<string[]> {
const lines: string[] = [];
const original = console.log;
console.log = (...args: unknown[]) => {
lines.push(args.map((a) => String(a)).join(" "));
};
try {
await run();
} finally {
console.log = original;
}
return lines
.filter((line) => /^\[[0-9a-f-]{36}\] POST http/.test(line))
.map((line) => line.split(" ").at(-1)!);
}
function genaiPath(query: string): string {
return `http://gateway.test/genai/v1beta/models/${
encodeURIComponent(MODEL)
}:generateContent${query}`;
}
function genaiPost(
query: string,
headers: Record<string, string> = {},
): Request {
return new Request(genaiPath(query), {
method: "POST",
headers: { "Content-Type": "application/json", ...headers },
body: JSON.stringify({
contents: [{ role: "user", parts: [{ text: "hei" }] }],
}),
});
}
Deno.test("genai front door: a CR/LF ?key= is treated as no credential (401, not 500)", async () => {
const mock = new MockProvider(() => jsonResponse(openAIChatBody("ok")));
const handler = createHandler(guardedContext(mock.url));
try {
// A header can never carry CR/LF (Headers rejects it client-side), but the
// query-parameter credential can - and it lands straight on Headers.set.
const res = await handler(genaiPost("?key=a%0D%0Ab"));
assertEquals(res.status, 401);
const body = await res.json() as {
error: { code: number; status: string; message: string };
};
assertEquals(body.error.code, 401);
assertEquals(body.error.status, "UNAUTHENTICATED");
assertEquals(mock.calls.length, 0);
} finally {
await mock.close();
}
});
Deno.test("genai front door: ?key= authenticates and never reaches the access log", async () => {
const mock = new MockProvider(() => jsonResponse(openAIChatBody("ok")));
const handler = createHandler(guardedContext(mock.url));
try {
let status = 0;
const urls = await withAccessLog(async () => {
const res = await handler(genaiPost(`?key=${VK_TOKEN}`));
status = res.status;
await res.body?.cancel();
});
assertEquals(status, 200);
assertEquals(urls.length, 1);
// Promoted to a bearer, then scrubbed off the URL before anything logs it.
assertFalse(urls[0].includes("key="), urls[0]);
assertFalse(urls[0].includes(VK_TOKEN), urls[0]);
} finally {
await mock.close();
}
});
Deno.test("genai front door: a query string without key= is not re-serialized", async () => {
const mock = new MockProvider(() => jsonResponse(openAIChatBody("ok")));
const handler = createHandler(guardedContext(mock.url));
try {
let status = 0;
const urls = await withAccessLog(async () => {
const res = await handler(
genaiPost("?alt=sse&flag", { "x-goog-api-key": VK_TOKEN }),
);
status = res.status;
await res.body?.cancel();
});
assertEquals(status, 200);
// Byte-identical: a valueless `flag` must NOT come back as `flag=`, which
// is what URLSearchParams re-serialization would do to it.
assertEquals(urls[0], genaiPath("?alt=sse&flag"));
} finally {
await mock.close();
}
});
// ------------------------------------------------- shared execution (WP1)
Deno.test("anthropic ingress: plugin pre and post hooks run on /v1/messages", async () => {
const mock = new MockProvider(() => jsonResponse(openAIChatBody("upstream")));
const seen: string[] = [];
const plugins = new PluginManager();
plugins.register({
name: "anthropic-shared-path",
onPreRequest: (req) => {
seen.push("pre");
return Promise.resolve({ ...req, temperature: 0.5 });
},
onPostRequest: (res) => {
seen.push("post");
return Promise.resolve({
...res,
choices: [{
...res.choices[0],
message: { role: "assistant", content: "plugin-rewritten" },
}],
});
},
});
const handler = createHandler(makeContext(mock.url, plugins));
try {
const res = await handler(
messages({ messages: [{ role: "user", content: "hei" }] }),
);
assertEquals(res.status, 200);
const body = await res.json() as AnthropicBody;
// Both halves of the shared canonical execution are observable from the
// Anthropic surface: the pre-hook edit reached provider egress, and the
// post-hook edit reached the translated Anthropic response.
assertEquals(seen, ["pre", "post"]);
assertEquals((mock.calls[0].body as CanonicalEgress).temperature, 0.5);
assertEquals(body.content, [{ type: "text", text: "plugin-rewritten" }]);
} finally {
await mock.close();
}
});
Deno.test("anthropic ingress: a client abort mid-stream tears down the upstream", async () => {
let cancelled = false;
let resolveCancel: () => void = () => {};
const upstreamCancelled = new Promise<void>((resolve) => {
resolveCancel = resolve;
});
const mock = new MockProvider(() => {
const body = new ReadableStream<Uint8Array>({
start(controller) {
// One frame, then the upstream holds the connection open forever.
controller.enqueue(
new TextEncoder().encode(chunk({ role: "assistant", content: "a" })),
);
},
cancel() {
cancelled = true;
resolveCancel();
},
});
return new Response(body, {
headers: { "Content-Type": "text/event-stream" },
});
});
const handler = createHandler(makeContext(mock.url));
const aborter = new AbortController();
try {
const res = await handler(messages(
{ messages: [{ role: "user", content: "hei" }], stream: true },
{},
{ signal: aborter.signal },
));
assertEquals(res.status, 200);
// Nothing has torn the upstream down yet; only the abort below may.
assertFalse(cancelled);
aborter.abort();
// The front door rebuilds the Request for every /v1/messages call. A naive
// rebuild mints a FRESH signal, and this await would then hang forever.
await upstreamCancelled;
assert(cancelled);
await res.body?.cancel().catch(() => {});
} finally {
await mock.close();
}
});