Files
OmniRoute/tests/unit/cloudflare-playground-provider.test.ts
Diego Rodrigues de Sa e Souza 7affe0c857 fix(security): zero out open CodeQL code-scanning alerts (#10739)
* fix(security): zero out open CodeQL code-scanning alerts

- src/mitm/handlers/antigravity.ts: fix broken \s regex escape in a
  template-string RegExp (unrecognized escape silently dropped the
  backslash, breaking the whitespace match) — also clears the two
  useless-regexp-character-escape alerts.
- open-sse/executors/gemini-web.ts: replace the unbounded polynomial
  regex in isMissingBrowserExecutable() with plain substring checks.
- src/shared/middleware/chatBodyAdmission.ts, open-sse/services/
  conversationTracker.ts, src/app/api/v1/models/catalogCache.ts:
  annotate the sha256 fingerprint hashes (admission-budget key,
  conversation identity, catalog memo key — none are password/
  credential hashes) with codeql[js/insufficient-password-hash]
  suppressions; the existing suppression comments in
  chatBodyAdmission.ts were on the wrong line and CodeQL never
  picked them up.
- tests/unit/qwen-token-plan-console-site.test.ts, tests/unit/
  cloudflare-playground-provider.test.ts: replace raw
  string.includes(hostname) assertions with new URL(...).hostname
  equality/endsWith checks, closing the incomplete-url-substring-
  sanitization alerts without weakening what the tests verify.

* fix(security): correct codeql suppression comment syntax

The prior codeql[rule-id] trailing comments mixed in extra text after
the rule id, and CodeQL's PR-diff check re-flagged all three fingerprint
sha256 calls as new js/insufficient-password-hash alerts. Use the bare
`// codeql[js/insufficient-password-hash]` suppression comment on the
flagged line, with the justification moved to a plain comment on the
line above.

* fix(security): switch fingerprint hashes from sha256 to HMAC-SHA256

The prior codeql[js/insufficient-password-hash] suppression comments
were not honored by the PR-diff CodeQL check, which kept flagging the
three fingerprint call sites (admission-budget bucket key, conversation
identity, catalog memo-map key) as new alerts.

Switch createHash("sha256") to createHmac("sha256", <fixed context
label>) at all three sites: a keyed, domain-separated digest is the
semantically correct construction for a fingerprint anyway (it no
longer collides with an attacker-supplied unkeyed digest of the same
input), and it does not match the insufficient-password-hash sink
pattern.

* chore(ci): retrigger CodeQL after dismissing pre-existing fingerprint-hash alerts

Empty commit to force a fresh default-setup CodeQL scan now that
alerts #827/#833/#834/#837 are dismissed as false positives (see PR
description) — the prior scan predates the dismissal.

---------

Co-authored-by: Markus Hartung <mail@hartmark.se>
2026-08-19 12:10:15 -03:00

528 lines
20 KiB
TypeScript

/**
* Tests for the Cloudflare AI Playground (No Auth) provider.
*
* Validates:
* - NOAUTH_PROVIDERS contains the cloudflare-playground entry (noAuth category)
* - Registry entry has correct shape (authType none), curated 20-model catalog
* - Executor resolves for both the primary id and the alias (cfp)
* - cf_agent frame → OpenAI SSE translation, exercised with REAL frames captured
* from the playground on 2026-08-15 (including decoy RPC `done:true` frames
* that must NOT terminate the chat stream, and a real 3021 rate-limit error)
* - Streaming + non-streaming responses, clean upstream errors (no stack traces)
*/
import test from "node:test";
import assert from "node:assert/strict";
import { NOAUTH_PROVIDERS } from "../../src/shared/constants/providers/noauth.ts";
import { REGISTRY } from "../../open-sse/config/providers/index.ts";
import { getExecutor } from "../../open-sse/executors/index.ts";
import {
CloudflarePlaygroundExecutor,
CfStreamParser,
PlaywrightCfTransport,
toCfMessages,
type CfTransport,
} from "../../open-sse/executors/cloudflare-playground.ts";
const CHAT_ID = "chatcmpl-cfp-test123";
// ── Fixtures: REAL frames captured from the playground (2026-08-15) ─────────
const identityFrame = JSON.stringify({
name: "playground-8d57d26b34b144108fd1f49d2",
agent: "playground",
type: "cf_agent_identity",
});
const stateFrame = JSON.stringify({
state: {
model: "@cf/zai-org/glm-4.7-flash",
temperature: 1,
stream: true,
system: "You are a helpful assistant.",
},
type: "cf_agent_state",
});
/** Decoy: the setConfig RPC response also carries `done:true` — must be ignored. */
const decoyRpcDone = JSON.stringify({
id: "cfp-config",
done: true,
type: "cf_agent_rpc_response",
});
const cfFrame = (chatId: string, body: unknown) =>
JSON.stringify({ id: chatId, type: "cf_agent_use_chat_response", body: JSON.stringify(body) });
/** Full success stream built for a given chat id. */
const buildSuccessFrames = (chatId: string) => [
identityFrame,
stateFrame,
decoyRpcDone,
cfFrame(chatId, { type: "start" }),
cfFrame(chatId, { type: "start-step" }),
cfFrame(chatId, { type: "reasoning-start", id: "r1" }),
cfFrame(chatId, { type: "reasoning-delta", delta: "thinking about it...", id: "r1" }),
cfFrame(chatId, { type: "reasoning-end", id: "r1" }),
cfFrame(chatId, { type: "text-start", id: "t1" }),
cfFrame(chatId, { type: "text-delta", delta: "Hello ", id: "t1" }),
cfFrame(chatId, { type: "text-delta", delta: "world!", id: "t1" }),
cfFrame(chatId, { type: "finish-step" }),
cfFrame(chatId, { type: "finish", messageMetadata: { finishReason: "stop" } }),
JSON.stringify({ id: chatId, type: "cf_agent_use_chat_response", done: true }),
];
/** Real rate-limit error frame (kimi-k2.6, captured live), built for a chat id. */
const buildRateLimitFrame = (chatId: string) =>
JSON.stringify({
error: true,
body: JSON.stringify({
message: "The model is currently rate limited. Please wait a moment and try again.",
details: "3021: rate limiting: inference request per min rate reached",
}),
done: false,
id: chatId,
type: "cf_agent_use_chat_response",
});
class FakeTransport implements CfTransport {
constructor(
private framesList: string[],
private fail: { status: number; message: string } | null = null
) {}
async start(): Promise<{ ok: true } | { ok: false; status: number; message: string }> {
return this.fail ? { ok: false, ...this.fail } : { ok: true };
}
async *frames(): AsyncGenerator<string> {
for (const frame of this.framesList) yield frame;
}
async close(): Promise<void> {}
}
function makeExecutor(
buildFrames: (chatId: string) => string[],
fail?: { status: number; message: string }
) {
return new CloudflarePlaygroundExecutor((chatId) => new FakeTransport(buildFrames(chatId), fail));
}
const executeArgs = (body: Record<string, unknown>, stream: boolean) =>
({ body, credentials: {}, signal: null, stream }) as unknown as Parameters<
CloudflarePlaygroundExecutor["execute"]
>[0];
// ── Catalog / NOAUTH_PROVIDERS ───────────────────────────────────────────────
test("cloudflare-playground is present in NOAUTH_PROVIDERS (noAuth category)", () => {
const p = (NOAUTH_PROVIDERS as Record<string, unknown>)["cloudflare-playground"] as Record<
string,
unknown
>;
assert.ok(p, "NOAUTH_PROVIDERS['cloudflare-playground'] must exist");
assert.equal(p.id, "cloudflare-playground");
assert.equal(p.alias, "cfp");
assert.equal((p.name as string).includes("Cloudflare"), true);
assert.equal(p.noAuth, true);
assert.equal(p.hasFree, true);
assert.ok(typeof p.freeNote === "string" && (p.freeNote as string).length > 0);
assert.ok(typeof p.authHint === "string" && (p.authHint as string).length > 0);
assert.ok(typeof p.website === "string");
assert.ok(new URL(p.website as string).hostname.endsWith(".cloudflare.com"));
});
test("cloudflare-playground registry entry has no-auth shape and curated models", () => {
const entry = REGISTRY["cloudflare-playground"];
assert.ok(entry, "REGISTRY['cloudflare-playground'] must exist");
assert.equal(entry.alias, "cfp");
assert.equal(entry.format, "openai");
assert.equal(entry.executor, "cloudflare-playground");
assert.equal(entry.authType, "none");
assert.equal(entry.authHeader, "none");
assert.equal(entry.baseUrl, "https://playground.ai.cloudflare.com");
assert.ok(
entry.models.length >= 15,
`expected a curated catalog, got ${entry.models.length} models`
);
// No model id carries the upstream @cf/ prefix (executor adds it).
for (const model of entry.models) {
assert.ok(!model.id.startsWith("@cf/"), `model id must be prefix-free: ${model.id}`);
}
// Flagships present.
const ids = new Set(entry.models.map((m) => m.id));
for (const expected of [
"zai-org/glm-5.2",
"moonshotai/kimi-k2.6",
"deepseek-ai/deepseek-v4-flash-0731",
"openai/gpt-oss-120b",
"qwen/qwen2.5-coder-32b-instruct",
]) {
assert.ok(ids.has(expected), `expected model ${expected} in catalog`);
}
// Reasoning flags on the known thinking models.
const glm = entry.models.find((m) => m.id === "zai-org/glm-5.2");
assert.equal(glm?.supportsReasoning, true);
const llama = entry.models.find((m) => m.id === "meta-llama/llama-3.3-70b-instruct-fp8-fast");
assert.equal(llama?.supportsReasoning, undefined);
});
test("executor resolves for both the id and the cfp alias", () => {
const byId = getExecutor("cloudflare-playground");
const byAlias = getExecutor("cfp");
assert.ok(byId instanceof CloudflarePlaygroundExecutor);
assert.ok(byAlias instanceof CloudflarePlaygroundExecutor);
});
// ── Frame → SSE translation (real captured traffic) ─────────────────────────
test("CfStreamParser translates a real captured stream (decoys ignored)", () => {
const parser = new CfStreamParser(CHAT_ID);
let events = 0;
for (const frame of buildSuccessFrames(CHAT_ID)) {
const event = parser.push(frame);
if (event) events += 1;
}
assert.equal(parser.text, "Hello world!");
assert.equal(parser.reasoningText, "thinking about it...");
assert.equal(parser.finishReason, "stop");
assert.equal(parser.done, true);
assert.equal(parser.error, null);
// role + 1 reasoning + 2 content + 1 finish
assert.equal(events, 5);
});
test("CfStreamParser ignores done:true frames that belong to other ids/RPCs", () => {
const parser = new CfStreamParser(CHAT_ID);
// Decoy RPC response with done:true
parser.push(decoyRpcDone);
assert.equal(parser.done, false, "RPC done:true must not end the chat stream");
// A chat-response frame for a DIFFERENT chat id
parser.push(
JSON.stringify({ id: "chatcmpl-OTHER", type: "cf_agent_use_chat_response", done: true })
);
assert.equal(parser.done, false, "foreign chat id must not end the stream");
// The real one
parser.push(JSON.stringify({ id: CHAT_ID, type: "cf_agent_use_chat_response", done: true }));
assert.equal(parser.done, true);
});
test("CfStreamParser maps the real 3021 rate-limit frame to HTTP 429", () => {
const parser = new CfStreamParser(CHAT_ID);
parser.push(buildRateLimitFrame(CHAT_ID));
assert.ok(parser.error, "rate-limit frame must surface as an error");
assert.equal(parser.error?.status, 429);
assert.ok((parser.error?.message ?? "").includes("rate limiting"));
assert.equal(parser.done, false);
});
test("toCfMessages drops system/tool, flattens parts, keeps user/assistant", () => {
const out = toCfMessages([
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: "hi" },
{ role: "assistant", content: [{ type: "text", text: "hello" }] },
{
role: "user",
content: [
{ type: "text", text: "a" },
{ type: "text", text: "b" },
],
},
{ role: "tool", content: "tool result" },
{ role: "user", content: "" },
]);
assert.equal(out.length, 3);
assert.deepEqual(out[0].parts, [{ type: "text", text: "hi" }]);
assert.equal(out[1].parts[0].text, "hello");
assert.equal(out[2].parts[0].text, "a\nb");
assert.equal(out[0].role, "user");
assert.equal(out[1].role, "assistant");
});
// ── Executor behavior (fake transport, real frames) ─────────────────────────
test("executor streams OpenAI SSE chunks from captured frames", async () => {
const executor = makeExecutor(buildSuccessFrames);
const result = await executor.execute(
executeArgs(
{ model: "zai-org/glm-4.7-flash", messages: [{ role: "user", content: "hi" }] },
true
)
);
const response = result.response;
assert.equal(response.status, 200);
assert.match(response.headers.get("content-type") ?? "", /text\/event-stream/);
const raw = await response.text();
assert.ok(raw.endsWith("data: [DONE]\n\n"), "stream must end with [DONE]");
const chunks = raw
.split("\n")
.filter((line) => line.startsWith("data: ") && line !== "data: [DONE]")
.map((line) => JSON.parse(line.slice(6)));
assert.ok(chunks.length >= 5, `expected several chunks, got ${chunks.length}`);
const first = chunks[0];
assert.equal(first.choices[0].delta.role, "assistant");
assert.equal(first.choices[0].finish_reason, null);
const reasoningChunk = chunks.find((c) => c.choices?.[0]?.delta?.reasoning_content);
assert.equal(reasoningChunk?.choices?.[0]?.delta?.reasoning_content, "thinking about it...");
const content = chunks
.filter((c) => c.choices?.[0]?.delta?.content)
.map((c) => c.choices[0].delta.content)
.join("");
assert.equal(content, "Hello world!");
const last = chunks[chunks.length - 1];
assert.equal(last.choices[0].finish_reason, "stop");
assert.equal(last.model, "zai-org/glm-4.7-flash");
});
test("executor returns JSON for non-streaming requests", async () => {
const executor = makeExecutor(buildSuccessFrames);
const result = await executor.execute(
executeArgs(
{ model: "moonshotai/kimi-k2.6", messages: [{ role: "user", content: "hi" }] },
false
)
);
const response = result.response;
assert.equal(response.status, 200);
assert.match(response.headers.get("content-type") ?? "", /application\/json/);
const parsed = JSON.parse(await response.text()) as {
choices: Array<{
message: { content: string; reasoning_content?: string };
finish_reason: string;
}>;
model: string;
};
assert.equal(parsed.choices[0].message.content, "Hello world!");
assert.equal(parsed.choices[0].message.reasoning_content, "thinking about it...");
assert.equal(parsed.choices[0].finish_reason, "stop");
assert.equal(parsed.model, "moonshotai/kimi-k2.6");
});
test("executor surfaces the 3021 rate limit as a clean 429 (no stack traces)", async () => {
const executor = makeExecutor((chatId) => [buildRateLimitFrame(chatId)]);
const result = await executor.execute(
executeArgs(
{ model: "moonshotai/kimi-k2.6", messages: [{ role: "user", content: "hi" }] },
false
)
);
assert.equal(result.response.status, 429);
const parsed = JSON.parse(await result.response.text()) as {
error: { message: string; type: string };
};
assert.ok(parsed.error.message.includes("rate limiting"));
assert.equal(parsed.error.type, "upstream_error");
assert.ok(!parsed.error.message.includes(" at "), "no stack-trace leak");
});
test("executor returns a clean 502 when the browser session cannot start", async () => {
const executor = makeExecutor(buildSuccessFrames, {
status: 502,
message: "Cloudflare Playground browser session failed: boom",
});
const result = await executor.execute(
executeArgs(
{ model: "zai-org/glm-4.7-flash", messages: [{ role: "user", content: "hi" }] },
true
)
);
assert.equal(result.response.status, 502);
const parsed = JSON.parse(await result.response.text()) as { error: { message: string } };
assert.ok(parsed.error.message.includes("browser session failed"));
assert.ok(!parsed.error.message.includes(" at "), "no stack-trace leak");
});
test("executor prefixes bare model ids with @cf/ (upstream convention)", async () => {
const seen: string[] = [];
class CapturingTransport extends FakeTransport {
async start(config: Parameters<CfTransport["start"]>[0]) {
seen.push(config.model);
return { ok: true } as const;
}
}
const executor = new CloudflarePlaygroundExecutor(
(chatId) => new CapturingTransport(buildSuccessFrames(chatId))
);
await executor.execute(
executeArgs(
{ model: "zai-org/glm-4.7-flash", messages: [{ role: "user", content: "hi" }] },
false
)
);
assert.equal(seen.length, 1);
assert.equal(seen[0], "@cf/zai-org/glm-4.7-flash");
});
// ── #10494: browser/transport resource leak on blocked-request paths ───────
test("PlaywrightCfTransport.start() closes the browser when Cloudflare Attention Required is detected", async () => {
const playwright = await import("playwright");
const originalLaunch = playwright.chromium.launch;
let closeCalls = 0;
playwright.chromium.launch = (async () =>
({
newContext: async () => ({
newPage: async () => ({
goto: async () => {},
title: async () => "Attention Required! | Cloudflare",
exposeFunction: async () => {},
evaluate: async () => {},
}),
}),
close: async () => {
closeCalls += 1;
},
}) as unknown as ReturnType<typeof playwright.chromium.launch>) as typeof playwright.chromium.launch;
try {
const transport = new PlaywrightCfTransport("chat-attention-required");
const started = await transport.start({
model: "@cf/test-model",
messages: [],
temperature: 0.7,
});
assert.equal(started.ok, false);
if (started.ok === false) {
assert.equal(started.status, 502);
}
assert.equal(closeCalls, 1, "browser launched for the challenge check must be closed");
} finally {
playwright.chromium.launch = originalLaunch;
}
});
// ── #10494: streaming timeout must not be misreported as a clean [DONE] ────
/**
* A transport whose frames() hangs (never yields) once its initial queue is
* drained, mirroring PlaywrightCfTransport's real behavior: frames() only
* resolves again once close() is called (real close() unblocks pending
* waiters with null, ending the generator). This lets tests force the
* executor's internal chat-timeout branch deterministically instead of
* waiting for CHAT_TIMEOUT_MS.
*/
class HangingTransport implements CfTransport {
closeCalls = 0;
private closed = false;
private queue: string[];
private waiters: Array<(frame: string | null) => void> = [];
constructor(initialFrames: string[] = []) {
this.queue = [...initialFrames];
}
async start(): Promise<{ ok: true } | { ok: false; status: number; message: string }> {
return { ok: true };
}
async *frames(): AsyncGenerator<string> {
while (true) {
if (this.queue.length > 0) {
yield this.queue.shift()!;
continue;
}
const frame = await new Promise<string | null>((resolve) => this.waiters.push(resolve));
if (frame === null) return;
yield frame;
}
}
// Idempotent, mirroring PlaywrightCfTransport.close(): the timer callback
// and the streaming finally block both call close() on the timeout path.
async close(): Promise<void> {
if (this.closed) return;
this.closed = true;
this.closeCalls += 1;
for (const waiter of this.waiters.splice(0)) waiter(null);
}
}
function parseSseChunks(raw: string) {
return raw
.split("\n\n")
.filter((chunk) => chunk.startsWith("data: ") && chunk !== "data: [DONE]")
.map((chunk) => JSON.parse(chunk.slice(6)));
}
test("streaming: an empty timeout (no frames at all) emits an explicit error chunk, not a bare [DONE]", async () => {
const transport = new HangingTransport([]);
const executor = new CloudflarePlaygroundExecutor(() => transport, 20);
const result = await executor.execute(
executeArgs(
{ model: "zai-org/glm-4.7-flash", messages: [{ role: "user", content: "hi" }] },
true
)
);
const raw = await result.response.text();
assert.ok(raw.endsWith("data: [DONE]\n\n"), "stream must still end with [DONE]");
assert.equal(transport.closeCalls, 1, "timed-out transport must be closed");
const chunks = parseSseChunks(raw);
assert.ok(chunks.length >= 1, "an error chunk must be emitted before [DONE]");
const errorChunk = chunks.find((c) => c.error);
assert.ok(errorChunk, "expected an explicit error chunk on timeout");
assert.equal(errorChunk.error.type, "timeout_error");
assert.equal(errorChunk.error.code, "HTTP_504");
assert.ok(!errorChunk.error.message.includes(" at "), "no stack-trace leak");
});
test("streaming: a partial answer followed by a timeout emits content THEN an explicit error chunk", async () => {
// The executor mints its own random chat id (chatcmpl-cfp-<uuid>) and only
// the transportFactory receives it — frames must reference that same id or
// CfStreamParser silently ignores them (see `msg.id !== this.chatId`
// above). Build the partial frames from the factory callback, exactly like
// buildSuccessFrames()/makeExecutor() do above.
let transport!: HangingTransport;
const executor = new CloudflarePlaygroundExecutor((chatId) => {
const partialFrames = [
JSON.stringify({
id: chatId,
type: "cf_agent_use_chat_response",
body: JSON.stringify({ type: "start" }),
}),
JSON.stringify({
id: chatId,
type: "cf_agent_use_chat_response",
body: JSON.stringify({ type: "text-delta", delta: "Hello", id: "t1" }),
}),
];
transport = new HangingTransport(partialFrames);
return transport;
}, 20);
const result = await executor.execute(
executeArgs(
{ model: "zai-org/glm-4.7-flash", messages: [{ role: "user", content: "hi" }] },
true
)
);
const raw = await result.response.text();
assert.ok(raw.endsWith("data: [DONE]\n\n"));
assert.equal(transport.closeCalls, 1);
const chunks = parseSseChunks(raw);
const content = chunks
.filter((c) => c.choices?.[0]?.delta?.content)
.map((c) => c.choices[0].delta.content)
.join("");
assert.equal(content, "Hello", "the partial content already streamed must not be dropped");
const errorChunk = chunks.find((c) => c.error);
assert.ok(errorChunk, "a partial-then-timeout stream must still surface an explicit error");
assert.equal(errorChunk.error.type, "timeout_error");
// The error chunk must come after the content, so a client processing the
// stream in order sees the partial answer followed by a clear failure —
// never a silent, successful-looking [DONE] right after partial content.
const errorIndex = chunks.indexOf(errorChunk);
const lastContentIndex = chunks.findLastIndex((c) => c.choices?.[0]?.delta?.content);
assert.ok(errorIndex > lastContentIndex, "error chunk must follow the streamed content");
});