mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-10 08:52:12 +03:00
Compare commits
6 Commits
fix/v3851-
...
fix/v3851-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
866f4df88b | ||
|
|
e243b04de2 | ||
|
|
5635565595 | ||
|
|
f89093def1 | ||
|
|
54e2cc7aa0 | ||
|
|
4c39274a96 |
@@ -97,6 +97,10 @@ _Living section — cycle opened at the v3.8.50 freeze (parallel-cycle model). B
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **security(streaming):** sanitize generic mid-stream error messages before emitting OpenAI,
|
||||
Responses, or Claude SSE failure frames and before diagnostic logging, while preserving raw
|
||||
failures for internal classification and keeping client disconnects out of provider failure state.
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
---
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
- HuggingChat now turns HTTP 200 JSONL generation failures into a sanitized 502 before content, or a fixed public stream failure after partial output, so fallback and request persistence no longer record a false successful stop.
|
||||
@@ -27,11 +27,7 @@ import {
|
||||
import { FETCH_TIMEOUT_MS } from "../config/constants.ts";
|
||||
import { buildErrorBody, sanitizeErrorMessage } from "../utils/error.ts";
|
||||
import { normalizeSessionCookieHeader } from "@/lib/providers/webCookieAuth";
|
||||
import {
|
||||
HuggingChatStreamError,
|
||||
readJsonlResponse,
|
||||
streamJsonlToOpenAi,
|
||||
} from "./huggingchat/jsonlStream.ts";
|
||||
import { streamJsonlToOpenAi, readJsonlResponse } from "./huggingchat/jsonlStream.ts";
|
||||
|
||||
const HUGGINGFACE_BASE = "https://huggingface.co";
|
||||
const CONVERSATION_URL = `${HUGGINGFACE_BASE}/chat/conversation`;
|
||||
@@ -42,7 +38,6 @@ const USER_AGENT =
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36";
|
||||
|
||||
const DEFAULT_MODEL = "baidu/ERNIE-4.5-VL-424B-A47B-Base-PT";
|
||||
const HUGGINGCHAT_PUBLIC_STREAM_ERROR = "HuggingChat generation failed";
|
||||
|
||||
// -- Helpers -----------------------------------------------------------------
|
||||
|
||||
@@ -528,81 +523,26 @@ export class HuggingChatExecutor extends BaseExecutor {
|
||||
|
||||
if (stream) {
|
||||
const encoder = new TextEncoder();
|
||||
const streamCancellationController = new AbortController();
|
||||
const jsonlStream = streamJsonlToOpenAi(
|
||||
upstreamResponse.body,
|
||||
resolvedModel,
|
||||
id,
|
||||
created,
|
||||
signal,
|
||||
streamCancellationController.signal
|
||||
signal
|
||||
);
|
||||
|
||||
const primedChunks: string[] = [];
|
||||
try {
|
||||
const first = await jsonlStream.next();
|
||||
if (!first.done) {
|
||||
primedChunks.push(first.value);
|
||||
if (first.value.includes('"role":"assistant"')) {
|
||||
const content = await jsonlStream.next();
|
||||
if (!content.done) primedChunks.push(content.value);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (!(err instanceof HuggingChatStreamError)) throw err;
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const safeMessage = sanitizeErrorMessage(message);
|
||||
log?.error?.("HUGGINGCHAT", `Stream failed before content: ${safeMessage}`);
|
||||
return {
|
||||
response: new Response(
|
||||
JSON.stringify(
|
||||
buildErrorBody(502, message, undefined, {
|
||||
type: "upstream_error",
|
||||
code: "huggingchat_generation_error",
|
||||
})
|
||||
),
|
||||
{ status: 502, headers: { "Content-Type": "application/json" } }
|
||||
),
|
||||
url: messageUrl,
|
||||
headers: baseHeaders,
|
||||
transformedBody: sendDataPayload,
|
||||
};
|
||||
}
|
||||
|
||||
let primedChunkIndex = 0;
|
||||
let streamCancelled = false;
|
||||
const sseStream = new ReadableStream<Uint8Array>({
|
||||
async pull(controller) {
|
||||
if (streamCancelled) return;
|
||||
if (primedChunkIndex < primedChunks.length) {
|
||||
controller.enqueue(encoder.encode(primedChunks[primedChunkIndex]));
|
||||
primedChunkIndex += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const sseStream = new ReadableStream({
|
||||
async start(controller) {
|
||||
try {
|
||||
const chunk = await jsonlStream.next();
|
||||
if (streamCancelled) return;
|
||||
if (chunk.done) {
|
||||
controller.close();
|
||||
return;
|
||||
for await (const chunk of jsonlStream) {
|
||||
controller.enqueue(encoder.encode(chunk));
|
||||
}
|
||||
controller.enqueue(encoder.encode(chunk.value));
|
||||
} catch (err) {
|
||||
if (streamCancelled) return;
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const safeMessage = sanitizeErrorMessage(message);
|
||||
log?.error?.("HUGGINGCHAT", `Stream error: ${safeMessage}`);
|
||||
controller.error(
|
||||
Object.assign(new Error(HUGGINGCHAT_PUBLIC_STREAM_ERROR), { statusCode: 502 })
|
||||
);
|
||||
log?.error?.("HUGGINGCHAT", `Stream error: ${err}`);
|
||||
} finally {
|
||||
controller.close();
|
||||
}
|
||||
},
|
||||
cancel() {
|
||||
streamCancelled = true;
|
||||
streamCancellationController.abort();
|
||||
void jsonlStream.return(undefined).catch(() => undefined);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -620,29 +560,7 @@ export class HuggingChatExecutor extends BaseExecutor {
|
||||
};
|
||||
}
|
||||
|
||||
let fullText: string;
|
||||
try {
|
||||
fullText = await readJsonlResponse(upstreamResponse.body, signal);
|
||||
} catch (err) {
|
||||
if (!(err instanceof HuggingChatStreamError)) throw err;
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const safeMessage = sanitizeErrorMessage(message);
|
||||
log?.error?.("HUGGINGCHAT", `Generation error: ${safeMessage}`);
|
||||
return {
|
||||
response: new Response(
|
||||
JSON.stringify(
|
||||
buildErrorBody(502, message, undefined, {
|
||||
type: "upstream_error",
|
||||
code: "huggingchat_generation_error",
|
||||
})
|
||||
),
|
||||
{ status: 502, headers: { "Content-Type": "application/json" } }
|
||||
),
|
||||
url: messageUrl,
|
||||
headers: baseHeaders,
|
||||
transformedBody: sendDataPayload,
|
||||
};
|
||||
}
|
||||
const fullText = await readJsonlResponse(upstreamResponse.body, signal);
|
||||
const completionTokens = estimateTokens(fullText);
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,36 +1,5 @@
|
||||
// Pure JSONL stream translation (HuggingChat NDJSON -> OpenAI SSE). Verbatim from huggingchat.ts.
|
||||
|
||||
export class HuggingChatStreamError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "HuggingChatStreamError";
|
||||
}
|
||||
}
|
||||
|
||||
function cancelReader(reader: ReadableStreamDefaultReader<Uint8Array>): void {
|
||||
try {
|
||||
void reader.cancel().catch(() => undefined);
|
||||
} catch {
|
||||
// The error event is authoritative; transport cleanup is best effort.
|
||||
}
|
||||
}
|
||||
|
||||
function bindReaderCancellation(
|
||||
reader: ReadableStreamDefaultReader<Uint8Array>,
|
||||
signal?: AbortSignal | null
|
||||
): () => void {
|
||||
if (!signal) return () => undefined;
|
||||
|
||||
const cancel = () => cancelReader(reader);
|
||||
if (signal.aborted) {
|
||||
cancel();
|
||||
return () => undefined;
|
||||
}
|
||||
|
||||
signal.addEventListener("abort", cancel, { once: true });
|
||||
return () => signal.removeEventListener("abort", cancel);
|
||||
}
|
||||
|
||||
export function sseChunk(data: unknown): string {
|
||||
return `data: ${JSON.stringify(data)}\n\n`;
|
||||
}
|
||||
@@ -73,11 +42,9 @@ export async function* streamJsonlToOpenAi(
|
||||
model: string,
|
||||
id: string,
|
||||
created: number,
|
||||
signal?: AbortSignal | null,
|
||||
cancellationSignal?: AbortSignal | null
|
||||
signal?: AbortSignal | null
|
||||
): AsyncGenerator<string> {
|
||||
const reader = body.getReader();
|
||||
const unbindReaderCancellation = bindReaderCancellation(reader, cancellationSignal);
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
let emittedRole = false;
|
||||
@@ -103,8 +70,16 @@ export async function* streamJsonlToOpenAi(
|
||||
const parsed = parseJsonlLine(trimmed);
|
||||
|
||||
if (parsed.error) {
|
||||
cancelReader(reader);
|
||||
throw new HuggingChatStreamError(parsed.error);
|
||||
yield sseChunk({
|
||||
id,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
model,
|
||||
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
||||
});
|
||||
yield "data: [DONE]\n\n";
|
||||
finished = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsed.token) {
|
||||
@@ -165,9 +140,6 @@ export async function* streamJsonlToOpenAi(
|
||||
|
||||
if (!finished && buffer.trim()) {
|
||||
const parsed = parseJsonlLine(buffer.trim());
|
||||
if (parsed.error) {
|
||||
throw new HuggingChatStreamError(parsed.error);
|
||||
}
|
||||
if (parsed.token && !signal?.aborted) {
|
||||
if (!emittedRole) {
|
||||
emittedRole = true;
|
||||
@@ -189,11 +161,10 @@ export async function* streamJsonlToOpenAi(
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
unbindReaderCancellation();
|
||||
reader.releaseLock();
|
||||
}
|
||||
|
||||
if (!signal?.aborted && !cancellationSignal?.aborted) {
|
||||
if (!signal?.aborted) {
|
||||
yield sseChunk({
|
||||
id,
|
||||
object: "chat.completion.chunk",
|
||||
@@ -201,9 +172,7 @@ export async function* streamJsonlToOpenAi(
|
||||
model,
|
||||
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
||||
});
|
||||
if (!signal?.aborted && !cancellationSignal?.aborted) {
|
||||
yield "data: [DONE]\n\n";
|
||||
}
|
||||
yield "data: [DONE]\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,10 +204,7 @@ export async function readJsonlResponse(
|
||||
const parsed = parseJsonlLine(trimmed);
|
||||
if (parsed.token) fullText += parsed.token;
|
||||
if (parsed.text) return parsed.text;
|
||||
if (parsed.error) {
|
||||
cancelReader(reader);
|
||||
throw new HuggingChatStreamError(parsed.error);
|
||||
}
|
||||
if (parsed.error) throw new Error(parsed.error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,7 +212,6 @@ export async function readJsonlResponse(
|
||||
const parsed = parseJsonlLine(buffer.trim());
|
||||
if (parsed.text) return parsed.text;
|
||||
if (parsed.token) fullText += parsed.token;
|
||||
if (parsed.error) throw new HuggingChatStreamError(parsed.error);
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
* The non-secret STRUCTURAL fields (appVersion, ctxKey, header names) carry safe
|
||||
* defaults so a transient parse miss can't break an otherwise-working signer.
|
||||
*/
|
||||
import { createHmac, createHash, createCipheriv, randomBytes } from "node:crypto";
|
||||
import { createHmac, createHash, createCipheriv, randomBytes, randomInt } from "node:crypto";
|
||||
import type { MaxaiSigningConstants, MaxaiHeaderNames } from "./constants.ts";
|
||||
import { MAXAI_DEFAULT_HEADER_NAMES } from "./constants.ts";
|
||||
|
||||
@@ -39,8 +39,22 @@ const BLANK_USER_ROUTES = new Set([
|
||||
|
||||
const MAGIC = Buffer.from("Salted__", "ascii");
|
||||
|
||||
/**
|
||||
* The wire `X-Random` slot: a 6-digit decimal string (100000-999999).
|
||||
*
|
||||
* Uses `crypto.randomInt`, which rejection-samples internally, instead of
|
||||
* `randomBytes(4) % 900000` — a plain modulo over a 32-bit draw does not divide
|
||||
* evenly by 900000, so the low ~4772 values of the range came out marginally
|
||||
* more often. The emitted shape is unchanged (always exactly 6 digits).
|
||||
*/
|
||||
export function maxaiRandomSlot(): string {
|
||||
return String(randomInt(100000, 1000000));
|
||||
}
|
||||
|
||||
function hmacSha1Hex(message: string, key: string): string {
|
||||
return createHmac("sha1", Buffer.from(key, "utf8")).update(Buffer.from(message, "utf8")).digest("hex");
|
||||
return createHmac("sha1", Buffer.from(key, "utf8"))
|
||||
.update(Buffer.from(message, "utf8"))
|
||||
.digest("hex");
|
||||
}
|
||||
|
||||
function sm3Hex(message: string): string {
|
||||
@@ -58,7 +72,9 @@ function evpBytesToKey(
|
||||
let block = Buffer.alloc(0);
|
||||
const pass = Buffer.from(passphrase, "utf8");
|
||||
while (derived.length < keyLen + ivLen) {
|
||||
block = createHash("md5").update(Buffer.concat([block, pass, salt])).digest();
|
||||
block = createHash("md5")
|
||||
.update(Buffer.concat([block, pass, salt]))
|
||||
.digest();
|
||||
derived = Buffer.concat([derived, block]);
|
||||
}
|
||||
return { key: derived.subarray(0, keyLen), iv: derived.subarray(keyLen, keyLen + ivLen) };
|
||||
@@ -124,8 +140,7 @@ export function buildMaxaiSignedHeaders(
|
||||
constants: MaxaiSigningConstants
|
||||
): Record<string, string> {
|
||||
const reqTime = (input.now ?? (() => Date.now()))();
|
||||
const random =
|
||||
input.random?.() ?? String((randomBytes(4).readUInt32BE(0) % 900000) + 100000);
|
||||
const random = input.random?.() ?? maxaiRandomSlot();
|
||||
const h: MaxaiHeaderNames = { ...MAXAI_DEFAULT_HEADER_NAMES, ...constants.headerNames };
|
||||
const ctxKey = constants.ctxKey;
|
||||
const appVersion = constants.appVersion;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { trackPendingRequest } from "@/lib/usageDb";
|
||||
import { STREAM_IDLE_TIMEOUT_MS } from "../config/constants.ts";
|
||||
import { FORMATS } from "../translator/formats.ts";
|
||||
import { buildErrorBody } from "./error.ts";
|
||||
import { PENDING_REQUEST_CLEARED_MARKER } from "./stream.ts";
|
||||
import { createCompletedResponsesToolHandoffWatcher } from "./responsesToolHandoff.ts";
|
||||
import { createStreamContentWatcher, type StreamContentWatcher } from "./streamReadiness.ts";
|
||||
@@ -187,6 +188,10 @@ function getErrorStatusCode(error: unknown): number {
|
||||
return 502;
|
||||
}
|
||||
|
||||
function getPublicErrorMessage(errorMsg: string, statusCode: number): string {
|
||||
return buildErrorBody(statusCode, errorMsg).error.message;
|
||||
}
|
||||
|
||||
function isDeadlineAbortReason(reason: unknown): reason is Error {
|
||||
return (
|
||||
reason instanceof Error &&
|
||||
@@ -406,7 +411,7 @@ export function createStreamController({
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
logStream(`error: ${error.message}`);
|
||||
logStream(`error: ${getPublicErrorMessage(error.message, getErrorStatusCode(error))}`);
|
||||
return;
|
||||
}
|
||||
logStream("error: unknown");
|
||||
@@ -452,6 +457,7 @@ export function buildStreamErrorChunks(
|
||||
clientResponseFormat?: string | null
|
||||
) {
|
||||
const statusMapping = getStreamErrorStatusMapping(statusCode);
|
||||
const publicErrorMessage = getPublicErrorMessage(errorMsg, statusCode);
|
||||
|
||||
if (isResponsesClientFormat(clientResponseFormat)) {
|
||||
const errorEvent = {
|
||||
@@ -460,7 +466,7 @@ export function buildStreamErrorChunks(
|
||||
id: null,
|
||||
status: "failed",
|
||||
error: {
|
||||
message: errorMsg,
|
||||
message: publicErrorMessage,
|
||||
type: statusMapping.responses.type,
|
||||
code: statusMapping.responses.code,
|
||||
},
|
||||
@@ -475,7 +481,7 @@ export function buildStreamErrorChunks(
|
||||
type: "error",
|
||||
error: {
|
||||
type: statusMapping.claude.type,
|
||||
message: errorMsg,
|
||||
message: publicErrorMessage,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -498,7 +504,7 @@ export function buildStreamErrorChunks(
|
||||
},
|
||||
],
|
||||
error: {
|
||||
message: errorMsg,
|
||||
message: publicErrorMessage,
|
||||
type: statusMapping.responses.type,
|
||||
code: statusMapping.responses.code,
|
||||
},
|
||||
|
||||
211
tests/fixtures/stream-handler-public-error-boundary.fixture.ts
vendored
Normal file
211
tests/fixtures/stream-handler-public-error-boundary.fixture.ts
vendored
Normal file
@@ -0,0 +1,211 @@
|
||||
// This suite owns process-wide DATA_DIR, plugin, logger, and DB state. It must run only inside
|
||||
// the subprocess launched by tests/unit/stream-handler-public-error-boundary.test.ts.
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
const originalDataDir = process.env.DATA_DIR;
|
||||
const originalPluginsDir = process.env.OMNIROUTE_PLUGINS_DIR;
|
||||
const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-stream-public-error-"));
|
||||
const TEST_DATA_DIR = path.join(testRoot, "data");
|
||||
const TEST_PLUGINS_DIR = path.join(testRoot, "plugins");
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
fs.mkdirSync(TEST_PLUGINS_DIR, { recursive: true });
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.OMNIROUTE_PLUGINS_DIR = TEST_PLUGINS_DIR;
|
||||
|
||||
const [core, callLogs, artifactWriter, loggerResource, streamHandler, { FORMATS }] =
|
||||
await Promise.all([
|
||||
import("../../src/lib/db/core.ts"),
|
||||
import("../../src/lib/usage/callLogs.ts"),
|
||||
import("../../src/lib/usage/callLogArtifactWriter.ts"),
|
||||
import("../../src/shared/utils/loggerResource.ts"),
|
||||
import("../../open-sse/utils/streamHandler.ts"),
|
||||
import("../../open-sse/translator/formats.ts"),
|
||||
]);
|
||||
const { createStreamController, pipeWithDisconnect } = streamHandler;
|
||||
|
||||
const SECRET = "sk-live-streamhandler-secret-123456";
|
||||
const API_KEY = "provider-key-streamhandler-654321";
|
||||
const PRIVATE_PATH = "/srv/omniroute/private/provider.ts:42:9";
|
||||
const RAW_MESSAGE =
|
||||
`Upstream failed at ${PRIVATE_PATH} Authorization: Bearer ${SECRET} api_key=${API_KEY}` +
|
||||
`\n at dispatch (/srv/omniroute/private/dispatcher.ts:88:3)`;
|
||||
|
||||
test.after(async () => {
|
||||
assert.equal(await callLogs.waitForCallLogSaves(3_000), true);
|
||||
await artifactWriter.closeCallLogArtifactWriter();
|
||||
core.resetDbInstance();
|
||||
await loggerResource.closeSharedLoggerResource();
|
||||
|
||||
if (originalDataDir === undefined) delete process.env.DATA_DIR;
|
||||
else process.env.DATA_DIR = originalDataDir;
|
||||
if (originalPluginsDir === undefined) delete process.env.OMNIROUTE_PLUGINS_DIR;
|
||||
else process.env.OMNIROUTE_PLUGINS_DIR = originalPluginsDir;
|
||||
|
||||
fs.rmSync(testRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
||||
});
|
||||
|
||||
test("fixture binds all persistent state to its process-owned directories", () => {
|
||||
assert.equal(core.DATA_DIR, TEST_DATA_DIR);
|
||||
assert.equal(core.SQLITE_FILE, path.join(TEST_DATA_DIR, "storage.sqlite"));
|
||||
assert.equal(process.env.DATA_DIR, TEST_DATA_DIR);
|
||||
assert.equal(process.env.OMNIROUTE_PLUGINS_DIR, TEST_PLUGINS_DIR);
|
||||
assert.equal(fs.existsSync(TEST_DATA_DIR), true);
|
||||
assert.equal(fs.existsSync(TEST_PLUGINS_DIR), true);
|
||||
});
|
||||
|
||||
test("OpenAI stream failures keep raw diagnostics internal and sanitize the public wire", async () => {
|
||||
const upstreamError = Object.assign(new Error(RAW_MESSAGE), { statusCode: 502 });
|
||||
const source = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.error(upstreamError);
|
||||
},
|
||||
});
|
||||
let internalMessage = "";
|
||||
|
||||
const stream = pipeWithDisconnect(
|
||||
new Response(source),
|
||||
new TransformStream<Uint8Array, Uint8Array>(),
|
||||
createStreamController({
|
||||
clientResponseFormat: FORMATS.OPENAI,
|
||||
onError(event) {
|
||||
internalMessage = event.message;
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
{ stallTimeoutMs: 0 }
|
||||
);
|
||||
const publicWire = await new Response(stream).text();
|
||||
|
||||
assert.equal(internalMessage, RAW_MESSAGE, "failure classification must retain the raw message");
|
||||
assert.match(publicWire, /"finish_reason":"error"/);
|
||||
assert.match(publicWire, /"code":"server_error"/);
|
||||
assert.match(publicWire, /\[DONE\]/);
|
||||
assert.doesNotMatch(publicWire, new RegExp(SECRET));
|
||||
assert.doesNotMatch(publicWire, new RegExp(API_KEY));
|
||||
assert.doesNotMatch(publicWire, /\/srv\/omniroute\/private/);
|
||||
assert.doesNotMatch(publicWire, /dispatcher\.ts/);
|
||||
assert.match(publicWire, /Authorization: \[REDACTED\]/);
|
||||
assert.match(publicWire, /<path>/);
|
||||
});
|
||||
|
||||
test("Responses stream failures preserve the failure event shape without leaking diagnostics", async () => {
|
||||
const upstreamError = Object.assign(new Error(RAW_MESSAGE), { statusCode: 429 });
|
||||
const source = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.error(upstreamError);
|
||||
},
|
||||
});
|
||||
let internalError: unknown;
|
||||
|
||||
const stream = pipeWithDisconnect(
|
||||
new Response(source),
|
||||
new TransformStream<Uint8Array, Uint8Array>(),
|
||||
createStreamController({
|
||||
clientResponseFormat: FORMATS.OPENAI_RESPONSES,
|
||||
onError(event) {
|
||||
internalError = event.error;
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
{ stallTimeoutMs: 0 }
|
||||
);
|
||||
const publicWire = await new Response(stream).text();
|
||||
|
||||
assert.equal(internalError, upstreamError, "the original error object must reach classification");
|
||||
assert.match(publicWire, /event: response\.failed/);
|
||||
assert.match(publicWire, /"type":"response\.failed"/);
|
||||
assert.match(publicWire, /"type":"rate_limit_error"/);
|
||||
assert.match(publicWire, /"code":"rate_limit_exceeded"/);
|
||||
assert.doesNotMatch(publicWire, new RegExp(SECRET));
|
||||
assert.doesNotMatch(publicWire, new RegExp(API_KEY));
|
||||
assert.doesNotMatch(publicWire, /\/srv\/omniroute\/private/);
|
||||
assert.doesNotMatch(publicWire, /dispatcher\.ts/);
|
||||
assert.match(publicWire, /Authorization: \[REDACTED\]/);
|
||||
assert.match(publicWire, /<path>/);
|
||||
});
|
||||
|
||||
test("Claude stream failures preserve error and stop events without leaking diagnostics", async () => {
|
||||
const upstreamError = Object.assign(new Error(RAW_MESSAGE), { statusCode: 403 });
|
||||
const source = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.error(upstreamError);
|
||||
},
|
||||
});
|
||||
let internalStatusCode = 0;
|
||||
|
||||
const stream = pipeWithDisconnect(
|
||||
new Response(source),
|
||||
new TransformStream<Uint8Array, Uint8Array>(),
|
||||
createStreamController({
|
||||
clientResponseFormat: FORMATS.CLAUDE,
|
||||
onError(event) {
|
||||
internalStatusCode = event.statusCode;
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
{ stallTimeoutMs: 0 }
|
||||
);
|
||||
const publicWire = await new Response(stream).text();
|
||||
|
||||
assert.equal(internalStatusCode, 403);
|
||||
assert.match(publicWire, /event: error/);
|
||||
assert.match(publicWire, /"type":"permission_error"/);
|
||||
assert.match(publicWire, /event: message_stop/);
|
||||
assert.doesNotMatch(publicWire, new RegExp(SECRET));
|
||||
assert.doesNotMatch(publicWire, new RegExp(API_KEY));
|
||||
assert.doesNotMatch(publicWire, /\/srv\/omniroute\/private/);
|
||||
assert.doesNotMatch(publicWire, /dispatcher\.ts/);
|
||||
assert.match(publicWire, /Authorization: \[REDACTED\]/);
|
||||
assert.match(publicWire, /<path>/);
|
||||
});
|
||||
|
||||
test("stream diagnostics sanitize logs while callbacks retain the original failure", () => {
|
||||
const upstreamError = Object.assign(new Error(RAW_MESSAGE), { statusCode: 502 });
|
||||
const originalLog = console.log;
|
||||
const logLines: string[] = [];
|
||||
let internalError: unknown;
|
||||
console.log = (...args: unknown[]) => {
|
||||
logLines.push(args.map(String).join(" "));
|
||||
};
|
||||
|
||||
try {
|
||||
createStreamController({
|
||||
provider: "test-provider",
|
||||
model: "test-model",
|
||||
onError(event) {
|
||||
internalError = event.error;
|
||||
return true;
|
||||
},
|
||||
}).handleError(upstreamError);
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
}
|
||||
|
||||
const logs = logLines.join("\n");
|
||||
assert.equal(internalError, upstreamError);
|
||||
assert.match(logs, /error: Upstream failed at <path>/);
|
||||
assert.match(logs, /Authorization: \[REDACTED\]/);
|
||||
assert.doesNotMatch(logs, new RegExp(SECRET));
|
||||
assert.doesNotMatch(logs, new RegExp(API_KEY));
|
||||
assert.doesNotMatch(logs, /\/srv\/omniroute\/private/);
|
||||
assert.doesNotMatch(logs, /dispatcher\.ts/);
|
||||
});
|
||||
|
||||
test("client disconnects stay outside the provider-failure callback", () => {
|
||||
let providerFailureRecorded = false;
|
||||
const controller = createStreamController({
|
||||
onError() {
|
||||
providerFailureRecorded = true;
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
controller.handleError(new DOMException("request_signal_aborted", "AbortError"));
|
||||
|
||||
assert.equal(providerFailureRecorded, false);
|
||||
assert.equal(controller.signal.aborted, false);
|
||||
});
|
||||
@@ -1,592 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { isAbsolute, relative } from "node:path";
|
||||
import { after, test } from "node:test";
|
||||
|
||||
function requiredEnv(name: string): string {
|
||||
const value = process.env[name];
|
||||
assert.ok(value, `${name} must be supplied by the isolated parent wrapper`);
|
||||
return value;
|
||||
}
|
||||
|
||||
const testRoot = requiredEnv("OMNIROUTE_HUGGINGCHAT_TEST_ROOT");
|
||||
const fixtureRunId = requiredEnv("OMNIROUTE_HUGGINGCHAT_TEST_RUN_ID");
|
||||
const testDataDir = requiredEnv("DATA_DIR");
|
||||
const testPluginsDir = requiredEnv("OMNIROUTE_PLUGINS_DIR");
|
||||
const xdgConfigDir = requiredEnv("XDG_CONFIG_HOME");
|
||||
|
||||
for (const [name, candidate] of [
|
||||
["DATA_DIR", testDataDir],
|
||||
["OMNIROUTE_PLUGINS_DIR", testPluginsDir],
|
||||
["XDG_CONFIG_HOME", xdgConfigDir],
|
||||
] as const) {
|
||||
const fromRoot = relative(testRoot, candidate);
|
||||
assert.equal(
|
||||
isAbsolute(fromRoot) || fromRoot.startsWith(".."),
|
||||
false,
|
||||
`${name} escaped test root`
|
||||
);
|
||||
}
|
||||
assert.equal(
|
||||
process.env.NODE_TEST_CONTEXT,
|
||||
undefined,
|
||||
"nested node:test state must not be inherited"
|
||||
);
|
||||
assert.equal(process.env.HOME, undefined, "the child must not inherit the operator HOME");
|
||||
assert.equal(process.env.CODEX_HOME, undefined, "the child must not inherit CODEX_HOME");
|
||||
assert.match(requiredEnv("API_KEY_SECRET"), /^[0-9a-f]{64}$/);
|
||||
|
||||
const [
|
||||
{ HuggingChatExecutor },
|
||||
{ HuggingChatStreamError, streamJsonlToOpenAi },
|
||||
{ createPassthroughStreamWithLogger },
|
||||
{ createStreamController, pipeWithDisconnect },
|
||||
{ createStreamFailureFinalizers, finalizeStreamRequestLog },
|
||||
{ ensureStreamReadiness },
|
||||
{ FORMATS },
|
||||
usageHistory,
|
||||
coreDb,
|
||||
callLogs,
|
||||
callLogArtifactWriter,
|
||||
loggerResource,
|
||||
] = await Promise.all([
|
||||
import("../../../open-sse/executors/huggingchat.ts"),
|
||||
import("../../../open-sse/executors/huggingchat/jsonlStream.ts"),
|
||||
import("../../../open-sse/utils/stream.ts"),
|
||||
import("../../../open-sse/utils/streamHandler.ts"),
|
||||
import("../../../open-sse/utils/streamFailureFinalization.ts"),
|
||||
import("../../../open-sse/utils/streamReadiness.ts"),
|
||||
import("../../../open-sse/translator/formats.ts"),
|
||||
import("../../../src/lib/usage/usageHistory.ts"),
|
||||
import("../../../src/lib/db/core.ts"),
|
||||
import("../../../src/lib/usage/callLogs.ts"),
|
||||
import("../../../src/lib/usage/callLogArtifactWriter.ts"),
|
||||
import("../../../src/shared/utils/loggerResource.ts"),
|
||||
]);
|
||||
|
||||
after(async () => {
|
||||
assert.equal(
|
||||
await callLogs.waitForCallLogSaves(10_000),
|
||||
true,
|
||||
"all asynchronous call-log writes must drain before DB teardown"
|
||||
);
|
||||
await callLogArtifactWriter.closeCallLogArtifactWriter();
|
||||
usageHistory.clearPendingRequests();
|
||||
await loggerResource.closeSharedLoggerResource();
|
||||
coreDb.resetDbInstance();
|
||||
});
|
||||
|
||||
function jsonlBody(lines: string[], trailingNewline = true): ReadableStream<Uint8Array> {
|
||||
const encoded = new TextEncoder().encode(`${lines.join("\n")}${trailingNewline ? "\n" : ""}`);
|
||||
return new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(encoded);
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function collectStream(body: ReadableStream<Uint8Array>): Promise<string> {
|
||||
const chunks: string[] = [];
|
||||
for await (const chunk of streamJsonlToOpenAi(
|
||||
body,
|
||||
"test/huggingchat-model",
|
||||
"chatcmpl-huggingchat-test",
|
||||
1_725_000_000
|
||||
)) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
return chunks.join("");
|
||||
}
|
||||
|
||||
test("HuggingChat turns a pre-content JSONL generation error into a sanitized 502", async () => {
|
||||
const rawError =
|
||||
"generation failed at /srv/omniroute/providers/huggingchat.ts:44:9 api_key=super-secret\n" +
|
||||
" at provider (/srv/omniroute/runtime.ts:1:1)";
|
||||
const realFetch = globalThis.fetch;
|
||||
let callCount = 0;
|
||||
const errorLogs: string[] = [];
|
||||
|
||||
globalThis.fetch = (async () => {
|
||||
callCount += 1;
|
||||
if (callCount === 1) {
|
||||
return Response.json({ conversationId: "conversation-test" });
|
||||
}
|
||||
if (callCount === 2) {
|
||||
return Response.json({ rootMessageId: "root-message-test" });
|
||||
}
|
||||
if (callCount === 3) {
|
||||
return new Response(
|
||||
jsonlBody(
|
||||
[
|
||||
JSON.stringify({ type: "status", status: "started" }),
|
||||
JSON.stringify({ type: "status", status: "error", message: rawError }),
|
||||
],
|
||||
false
|
||||
),
|
||||
{ status: 200, headers: { "Content-Type": "application/jsonl" } }
|
||||
);
|
||||
}
|
||||
throw new Error(`Unexpected fetch call ${callCount}`);
|
||||
}) as typeof globalThis.fetch;
|
||||
|
||||
try {
|
||||
const result = await new HuggingChatExecutor().execute({
|
||||
model: "test/huggingchat-model",
|
||||
body: { messages: [{ role: "user", content: "hello" }] },
|
||||
stream: true,
|
||||
credentials: { apiKey: "hf-chat=fake-cookie" },
|
||||
signal: null,
|
||||
log: { error: (_tag, message) => errorLogs.push(message) },
|
||||
});
|
||||
|
||||
assert.equal(callCount, 3, "the test must intercept every HuggingChat request");
|
||||
assert.equal(result.response.status, 502);
|
||||
assert.match(result.response.headers.get("content-type") || "", /application\/json/);
|
||||
|
||||
const payload = (await result.response.json()) as {
|
||||
error: { message: string; type?: string; code?: string };
|
||||
};
|
||||
assert.equal(payload.error.type, "upstream_error");
|
||||
assert.equal(payload.error.code, "huggingchat_generation_error");
|
||||
assert.match(payload.error.message, /generation failed/);
|
||||
assert.doesNotMatch(payload.error.message, /\/srv\/omniroute/);
|
||||
assert.doesNotMatch(payload.error.message, /super-secret/);
|
||||
assert.doesNotMatch(payload.error.message, /\n\s*at /);
|
||||
assert.equal(errorLogs.length, 1);
|
||||
assert.doesNotMatch(errorLogs[0], /\/srv\/omniroute/);
|
||||
assert.doesNotMatch(errorLogs[0], /super-secret/);
|
||||
assert.doesNotMatch(errorLogs[0], /\n\s*at /);
|
||||
} finally {
|
||||
globalThis.fetch = realFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("HuggingChat turns a terminal non-stream JSONL error into a sanitized 502", async () => {
|
||||
const rawError =
|
||||
"generation failed at /srv/omniroute/providers/huggingchat.ts:55:2 cookie=super-secret\n" +
|
||||
" at provider (/srv/omniroute/runtime.ts:1:1)";
|
||||
const realFetch = globalThis.fetch;
|
||||
let callCount = 0;
|
||||
|
||||
globalThis.fetch = (async () => {
|
||||
callCount += 1;
|
||||
if (callCount === 1) return Response.json({ conversationId: "conversation-test" });
|
||||
if (callCount === 2) return Response.json({ rootMessageId: "root-message-test" });
|
||||
if (callCount === 3) {
|
||||
return new Response(
|
||||
jsonlBody([JSON.stringify({ type: "status", status: "error", message: rawError })], false),
|
||||
{ status: 200, headers: { "Content-Type": "application/jsonl" } }
|
||||
);
|
||||
}
|
||||
throw new Error(`Unexpected fetch call ${callCount}`);
|
||||
}) as typeof globalThis.fetch;
|
||||
|
||||
try {
|
||||
const result = await new HuggingChatExecutor().execute({
|
||||
model: "test/huggingchat-model",
|
||||
body: { messages: [{ role: "user", content: "hello" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "hf-chat=fake-cookie" },
|
||||
signal: null,
|
||||
});
|
||||
|
||||
assert.equal(callCount, 3, "the test must intercept every HuggingChat request");
|
||||
assert.equal(result.response.status, 502);
|
||||
const payload = (await result.response.json()) as {
|
||||
error: { message: string; type?: string; code?: string };
|
||||
};
|
||||
assert.equal(payload.error.type, "upstream_error");
|
||||
assert.equal(payload.error.code, "huggingchat_generation_error");
|
||||
assert.doesNotMatch(payload.error.message, /\/srv\/omniroute/);
|
||||
assert.doesNotMatch(payload.error.message, /super-secret/);
|
||||
assert.doesNotMatch(payload.error.message, /\n\s*at /);
|
||||
} finally {
|
||||
globalThis.fetch = realFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("HuggingChat rejects its JSONL generator after partial content instead of faking success", async () => {
|
||||
const rawError =
|
||||
"generation failed at /srv/omniroute/providers/huggingchat.ts:44:9 access_token=super-secret\n" +
|
||||
" at provider (/srv/omniroute/runtime.ts:1:1)";
|
||||
const stream = streamJsonlToOpenAi(
|
||||
jsonlBody([
|
||||
JSON.stringify({ type: "stream", token: "partial answer" }),
|
||||
JSON.stringify({ type: "status", status: "error", message: rawError }),
|
||||
]),
|
||||
"test/huggingchat-model",
|
||||
"chatcmpl-huggingchat-test",
|
||||
1_725_000_000
|
||||
);
|
||||
|
||||
const roleChunk = await stream.next();
|
||||
const contentChunk = await stream.next();
|
||||
|
||||
assert.equal(roleChunk.done, false);
|
||||
assert.match(roleChunk.value || "", /"role":"assistant"/);
|
||||
assert.equal(contentChunk.done, false);
|
||||
assert.match(contentChunk.value || "", /partial answer/);
|
||||
await assert.rejects(() => stream.next(), HuggingChatStreamError);
|
||||
});
|
||||
|
||||
test("HuggingChat partial failures reach stream finalization, persistence, and fallback", async () => {
|
||||
const model = `test/huggingchat-model-${fixtureRunId}`;
|
||||
const provider = "huggingchat";
|
||||
const connectionId = `huggingchat-stream-error-boundary-${fixtureRunId}`;
|
||||
const publicErrorMessage = "HuggingChat generation failed";
|
||||
const rawError =
|
||||
"generation failed at /srv/omniroute/providers/huggingchat.ts:44:9 access_token=super-secret\n" +
|
||||
" at provider (/srv/omniroute/runtime.ts:1:1)";
|
||||
const realFetch = globalThis.fetch;
|
||||
let callCount = 0;
|
||||
const errorLogs: string[] = [];
|
||||
|
||||
globalThis.fetch = (async () => {
|
||||
callCount += 1;
|
||||
if (callCount === 1) return Response.json({ conversationId: "conversation-test" });
|
||||
if (callCount === 2) return Response.json({ rootMessageId: "root-message-test" });
|
||||
if (callCount === 3) {
|
||||
return new Response(
|
||||
jsonlBody([
|
||||
JSON.stringify({ type: "stream", token: "partial answer" }),
|
||||
JSON.stringify({ type: "status", status: "error", message: rawError }),
|
||||
]),
|
||||
{ status: 200, headers: { "Content-Type": "application/jsonl" } }
|
||||
);
|
||||
}
|
||||
throw new Error(`Unexpected fetch call ${callCount}`);
|
||||
}) as typeof globalThis.fetch;
|
||||
|
||||
usageHistory.clearPendingRequests();
|
||||
assert.equal(usageHistory.getPendingById().size, 0, "the child must start without pending state");
|
||||
assert.equal(
|
||||
usageHistory.getCompletedDetails().size,
|
||||
0,
|
||||
"the child must start without completed state"
|
||||
);
|
||||
const previousPersistence = coreDb
|
||||
.getDbInstance()
|
||||
.prepare("SELECT COUNT(*) AS count FROM call_logs WHERE connection_id = ? AND model = ?")
|
||||
.get(connectionId, model) as { count: number };
|
||||
assert.equal(previousPersistence.count, 0, "the child must not reuse a prior persisted identity");
|
||||
const requestId = usageHistory.trackPendingRequest(model, provider, connectionId, true);
|
||||
assert.ok(requestId, "the full-pipeline test must own a real pending request");
|
||||
|
||||
type CompletionPayload = {
|
||||
status: number;
|
||||
usage: unknown;
|
||||
providerPayload?: unknown;
|
||||
clientPayload?: unknown;
|
||||
error?: string | null;
|
||||
errorCode?: string | null;
|
||||
};
|
||||
type FailurePayload = {
|
||||
status: number;
|
||||
message: string;
|
||||
code?: string;
|
||||
type?: string;
|
||||
};
|
||||
|
||||
let completionPayload: CompletionPayload | null = null;
|
||||
let streamCompletionRecorded = false;
|
||||
let streamFailureCompletionRecorded = false;
|
||||
const persistedFailures: Array<{ status: number; errorCode?: string }> = [];
|
||||
const fallbackFailures: FailurePayload[] = [];
|
||||
|
||||
const onStreamComplete = (payload: CompletionPayload) => {
|
||||
const normalizedStatus = payload.status || 200;
|
||||
if (streamCompletionRecorded) return;
|
||||
streamCompletionRecorded = true;
|
||||
if (normalizedStatus !== 200) {
|
||||
if (streamFailureCompletionRecorded) return;
|
||||
streamFailureCompletionRecorded = true;
|
||||
}
|
||||
completionPayload = payload;
|
||||
finalizeStreamRequestLog({
|
||||
pendingRequestId: requestId,
|
||||
model,
|
||||
provider,
|
||||
connectionId,
|
||||
providerResponse: payload.providerPayload,
|
||||
clientResponse: payload.clientPayload,
|
||||
status: normalizedStatus,
|
||||
error: payload.error,
|
||||
errorCode: payload.errorCode,
|
||||
});
|
||||
};
|
||||
|
||||
const { handleStreamFailure, onPipelineStreamError } = createStreamFailureFinalizers({
|
||||
isFailureCompletionRecorded: () => streamFailureCompletionRecorded,
|
||||
isStreamCompletionRecorded: () => streamCompletionRecorded,
|
||||
onStreamComplete,
|
||||
persistFailureUsage: (status, errorCode) => {
|
||||
persistedFailures.push({ status, errorCode });
|
||||
},
|
||||
onStreamFailure: (failure) => {
|
||||
fallbackFailures.push(failure);
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await new HuggingChatExecutor().execute({
|
||||
model,
|
||||
body: { messages: [{ role: "user", content: "hello" }] },
|
||||
stream: true,
|
||||
credentials: { apiKey: "hf-chat=fake-cookie" },
|
||||
signal: null,
|
||||
log: { error: (_tag, message) => errorLogs.push(message) },
|
||||
});
|
||||
|
||||
assert.equal(callCount, 3, "the test must intercept every HuggingChat request");
|
||||
assert.equal(result.response.status, 200, "partial output has already committed HTTP 200");
|
||||
const readiness = await ensureStreamReadiness(result.response, {
|
||||
timeoutMs: 1_000,
|
||||
provider,
|
||||
model,
|
||||
});
|
||||
if (!readiness.ok) assert.fail(`unexpected readiness failure: ${readiness.reason}`);
|
||||
|
||||
const transform = createPassthroughStreamWithLogger(
|
||||
provider,
|
||||
null,
|
||||
null,
|
||||
model,
|
||||
connectionId,
|
||||
{ messages: [{ role: "user", content: "hello" }] },
|
||||
onStreamComplete,
|
||||
null,
|
||||
handleStreamFailure,
|
||||
FORMATS.OPENAI
|
||||
);
|
||||
const streamController = createStreamController({
|
||||
onError: onPipelineStreamError,
|
||||
provider,
|
||||
model,
|
||||
connectionId,
|
||||
clientResponseFormat: FORMATS.OPENAI,
|
||||
});
|
||||
const clientStream = pipeWithDisconnect(readiness.response, transform, streamController, {
|
||||
stallTimeoutMs: 0,
|
||||
});
|
||||
const wire = await new Response(clientStream).text();
|
||||
|
||||
assert.match(wire, /partial answer/);
|
||||
assert.match(wire, /"finish_reason":"error"/);
|
||||
assert.match(wire, new RegExp(publicErrorMessage));
|
||||
assert.match(wire, /data: \[DONE\]/);
|
||||
assert.doesNotMatch(wire, /"finish_reason":"stop"/);
|
||||
assert.doesNotMatch(wire, /\/srv\/omniroute/);
|
||||
assert.doesNotMatch(wire, /super-secret/);
|
||||
assert.equal(errorLogs.length, 1);
|
||||
assert.doesNotMatch(errorLogs[0], /\/srv\/omniroute/);
|
||||
assert.doesNotMatch(errorLogs[0], /super-secret/);
|
||||
assert.doesNotMatch(errorLogs[0], /\n\s*at /);
|
||||
|
||||
assert.ok(completionPayload, "the pipeline must record a terminal failure");
|
||||
assert.equal(completionPayload.status, 502);
|
||||
assert.equal(completionPayload.error, publicErrorMessage);
|
||||
assert.equal(completionPayload.errorCode, "stream_pipeline_error");
|
||||
assert.deepEqual(persistedFailures, [{ status: 502, errorCode: "stream_pipeline_error" }]);
|
||||
assert.deepEqual(fallbackFailures, [
|
||||
{
|
||||
status: 502,
|
||||
message: publicErrorMessage,
|
||||
code: "stream_pipeline_error",
|
||||
type: "stream_error",
|
||||
},
|
||||
]);
|
||||
|
||||
assert.equal(usageHistory.getPendingById().has(requestId), false);
|
||||
const completedDetail = usageHistory.getCompletedDetails().get(requestId);
|
||||
assert.ok(completedDetail, "failure finalization must persist the completed request detail");
|
||||
assert.equal(completedDetail.status, 502);
|
||||
assert.equal(completedDetail.error, publicErrorMessage);
|
||||
assert.equal(completedDetail.errorCode, "stream_pipeline_error");
|
||||
assert.doesNotMatch(JSON.stringify(completedDetail), /\/srv\/omniroute|super-secret/);
|
||||
assert.deepEqual(
|
||||
[...usageHistory.getCompletedDetails().keys()],
|
||||
[requestId],
|
||||
"only this child run may own completed usage state"
|
||||
);
|
||||
} finally {
|
||||
globalThis.fetch = realFetch;
|
||||
usageHistory.clearPendingRequests();
|
||||
}
|
||||
});
|
||||
|
||||
test("HuggingChat reports an authoritative error without waiting for transport cancellation", async () => {
|
||||
let cancelCalled = false;
|
||||
const encoded = new TextEncoder().encode(
|
||||
`${JSON.stringify({ type: "status", status: "error", message: "provider failed" })}\n`
|
||||
);
|
||||
const body = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(encoded);
|
||||
},
|
||||
cancel() {
|
||||
cancelCalled = true;
|
||||
return new Promise<void>(() => undefined);
|
||||
},
|
||||
});
|
||||
const stream = streamJsonlToOpenAi(
|
||||
body,
|
||||
"test/huggingchat-model",
|
||||
"chatcmpl-huggingchat-test",
|
||||
1_725_000_000
|
||||
);
|
||||
|
||||
const outcome = await Promise.race([
|
||||
stream.next().then(
|
||||
() => ({ kind: "resolved" as const }),
|
||||
(error: unknown) => ({ kind: "rejected" as const, error })
|
||||
),
|
||||
new Promise<{ kind: "hung" }>((resolve) => {
|
||||
setImmediate(() => resolve({ kind: "hung" }));
|
||||
}),
|
||||
]);
|
||||
|
||||
assert.equal(cancelCalled, true);
|
||||
assert.equal(outcome.kind, "rejected", "transport cleanup must not delay error delivery");
|
||||
assert.ok(
|
||||
outcome.kind === "rejected" && outcome.error instanceof HuggingChatStreamError,
|
||||
"the authoritative HuggingChat error must remain classifiable"
|
||||
);
|
||||
});
|
||||
|
||||
test("HuggingChat cancellation suppresses final chunks after a pending JSONL read", async () => {
|
||||
let upstreamCancelCalled = false;
|
||||
let upstreamPullCount = 0;
|
||||
const cancellationController = new AbortController();
|
||||
const token = new TextEncoder().encode(
|
||||
`${JSON.stringify({ type: "stream", token: "partial answer" })}\n`
|
||||
);
|
||||
const body = new ReadableStream<Uint8Array>({
|
||||
pull(controller) {
|
||||
upstreamPullCount += 1;
|
||||
if (upstreamPullCount === 1) {
|
||||
controller.enqueue(token);
|
||||
return;
|
||||
}
|
||||
return new Promise<void>(() => undefined);
|
||||
},
|
||||
cancel() {
|
||||
upstreamCancelCalled = true;
|
||||
return new Promise<void>(() => undefined);
|
||||
},
|
||||
});
|
||||
const stream = streamJsonlToOpenAi(
|
||||
body,
|
||||
"test/huggingchat-model",
|
||||
"chatcmpl-huggingchat-test",
|
||||
1_725_000_000,
|
||||
null,
|
||||
cancellationController.signal
|
||||
);
|
||||
|
||||
assert.match((await stream.next()).value || "", /"role":"assistant"/);
|
||||
assert.match((await stream.next()).value || "", /partial answer/);
|
||||
const pendingNext = stream.next();
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
cancellationController.abort();
|
||||
|
||||
const outcome = await Promise.race([
|
||||
pendingNext.then((result) => ({ kind: "settled" as const, result })),
|
||||
new Promise<{ kind: "hung" }>((resolve) => setImmediate(() => resolve({ kind: "hung" }))),
|
||||
]);
|
||||
|
||||
assert.equal(upstreamCancelCalled, true);
|
||||
assert.equal(outcome.kind, "settled", "cancellation must settle the pending generator read");
|
||||
assert.equal(
|
||||
outcome.kind === "settled" ? outcome.result.done : false,
|
||||
true,
|
||||
"a cancelled generator must not emit stop or [DONE]"
|
||||
);
|
||||
void stream.return(undefined).catch(() => undefined);
|
||||
});
|
||||
|
||||
test("HuggingChat client cancellation reaches a blocked upstream reader without waiting", async () => {
|
||||
const realFetch = globalThis.fetch;
|
||||
let callCount = 0;
|
||||
let upstreamCancelCalled = false;
|
||||
let upstreamPullCount = 0;
|
||||
const errorLogs: string[] = [];
|
||||
const token = new TextEncoder().encode(
|
||||
`${JSON.stringify({ type: "stream", token: "partial answer" })}\n`
|
||||
);
|
||||
const blockedBody = new ReadableStream<Uint8Array>({
|
||||
pull(controller) {
|
||||
upstreamPullCount += 1;
|
||||
if (upstreamPullCount === 1) {
|
||||
controller.enqueue(token);
|
||||
return;
|
||||
}
|
||||
return new Promise<void>(() => undefined);
|
||||
},
|
||||
cancel() {
|
||||
upstreamCancelCalled = true;
|
||||
return new Promise<void>(() => undefined);
|
||||
},
|
||||
});
|
||||
|
||||
globalThis.fetch = (async () => {
|
||||
callCount += 1;
|
||||
if (callCount === 1) return Response.json({ conversationId: "conversation-test" });
|
||||
if (callCount === 2) return Response.json({ rootMessageId: "root-message-test" });
|
||||
if (callCount === 3) {
|
||||
return new Response(blockedBody, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/jsonl" },
|
||||
});
|
||||
}
|
||||
throw new Error(`Unexpected fetch call ${callCount}`);
|
||||
}) as typeof globalThis.fetch;
|
||||
|
||||
try {
|
||||
const result = await new HuggingChatExecutor().execute({
|
||||
model: "test/huggingchat-model",
|
||||
body: { messages: [{ role: "user", content: "hello" }] },
|
||||
stream: true,
|
||||
credentials: { apiKey: "hf-chat=fake-cookie" },
|
||||
signal: null,
|
||||
log: { error: (_tag, message) => errorLogs.push(message) },
|
||||
});
|
||||
|
||||
assert.equal(callCount, 3, "the test must intercept every HuggingChat request");
|
||||
assert.ok(result.response.body);
|
||||
const reader = result.response.body.getReader();
|
||||
const roleChunk = await reader.read();
|
||||
const contentChunk = await reader.read();
|
||||
assert.match(new TextDecoder().decode(roleChunk.value), /"role":"assistant"/);
|
||||
assert.match(new TextDecoder().decode(contentChunk.value), /partial answer/);
|
||||
|
||||
const blockedRead = reader.read();
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
const cancelOutcome = await Promise.race([
|
||||
reader.cancel("client disconnected").then(() => "resolved" as const),
|
||||
new Promise<"hung">((resolve) => setImmediate(() => resolve("hung"))),
|
||||
]);
|
||||
void blockedRead.catch(() => undefined);
|
||||
|
||||
assert.equal(cancelOutcome, "resolved", "downstream cancellation must remain non-blocking");
|
||||
assert.equal(upstreamCancelCalled, true, "cancellation must reach the locked upstream reader");
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
assert.deepEqual(errorLogs, [], "client cancellation must not log a provider stream failure");
|
||||
} finally {
|
||||
globalThis.fetch = realFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("HuggingChat keeps the normal JSONL completion contract unchanged", async () => {
|
||||
const output = await collectStream(
|
||||
jsonlBody([
|
||||
JSON.stringify({ type: "stream", token: "complete answer" }),
|
||||
JSON.stringify({ type: "status", status: "finished" }),
|
||||
])
|
||||
);
|
||||
|
||||
assert.match(output, /"role":"assistant"/);
|
||||
assert.match(output, /complete answer/);
|
||||
assert.match(output, /"finish_reason":"stop"/);
|
||||
assert.match(output, /data: \[DONE\]/);
|
||||
assert.doesNotMatch(output, /"error":\{/);
|
||||
});
|
||||
@@ -95,10 +95,14 @@ test("handleChat names the shadowed custom node when the built-in prefix has no
|
||||
/prefix "of" is reserved by the built-in provider "openference"/,
|
||||
`runtime error must explain that the prefix resolved to the built-in, got: ${message}`
|
||||
);
|
||||
assert.match(
|
||||
message,
|
||||
new RegExp(`"${SHADOWED_NODE_NAME.replace(/[()]/g, "\\$&")}" \\(${SHADOWED_NODE_ID}\\)`),
|
||||
`runtime error must name the shadowed node and its id, got: ${message}`
|
||||
// Exact substring, not a hand-escaped RegExp: the name carries regex
|
||||
// metacharacters (parentheses) and the previous `.replace(/[()]/g, …)` escaped
|
||||
// only those, so any other metachar in a future name would have been
|
||||
// interpreted instead of matched literally (CodeQL js/incomplete-sanitization).
|
||||
const expectedNodeMention = `"${SHADOWED_NODE_NAME}" (${SHADOWED_NODE_ID})`;
|
||||
assert.ok(
|
||||
message.includes(expectedNodeMention),
|
||||
`runtime error must name the shadowed node and its id (${expectedNodeMention}), got: ${message}`
|
||||
);
|
||||
assert.match(message, /Rename that node's prefix/);
|
||||
});
|
||||
|
||||
31
tests/unit/helpers/ucClerkUrl.ts
Normal file
31
tests/unit/helpers/ucClerkUrl.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Strict recognizer for the UC (uncensored.com) Clerk session-token mint call,
|
||||
* shared by the uc-image / uc-video mock `fetch` routers.
|
||||
*
|
||||
* The mock routers used to dispatch on `url.includes("clerk.uncensored.com")`.
|
||||
* That is a substring test over a whole URL, so ANY host answers as long as the
|
||||
* name appears somewhere in it — `https://evil.example/?next=clerk.uncensored.com`
|
||||
* would have been served the mint response. A test whose router accepts a
|
||||
* malformed URL cannot fail when the executor builds one, which is exactly the
|
||||
* regression such a test exists to catch (and CodeQL flags it as
|
||||
* `js/incomplete-url-substring-sanitization`).
|
||||
*
|
||||
* This matches the real shape instead:
|
||||
* POST https://clerk.uncensored.com/v1/client/sessions/{sid}/tokens?_clerk_js_version=…
|
||||
* comparing the parsed origin against the production constant and pinning the
|
||||
* path shape.
|
||||
*/
|
||||
import { UC_CLERK_FAPI } from "../../../open-sse/executors/uc/constants.ts";
|
||||
|
||||
const MINT_PATH = /^\/v1\/client\/sessions\/[^/]+\/tokens$/;
|
||||
|
||||
/** True only for the Clerk mint endpoint on the real Clerk FAPI origin. */
|
||||
export function isUcClerkMintUrl(raw: unknown): boolean {
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(String(raw));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return parsed.origin === UC_CLERK_FAPI && MINT_PATH.test(parsed.pathname);
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawn } from "node:child_process";
|
||||
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { basename, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import test from "node:test";
|
||||
|
||||
const REPO_ROOT = fileURLToPath(new URL("../..", import.meta.url));
|
||||
const FIXTURE = fileURLToPath(
|
||||
new URL("./_fixtures/huggingchat-stream-error-boundary.fixture.ts", import.meta.url)
|
||||
);
|
||||
const SYNTHETIC_API_KEY_SECRET = "0".repeat(64);
|
||||
|
||||
type ChildResult = {
|
||||
code: number | null;
|
||||
signal: NodeJS.Signals | null;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
};
|
||||
|
||||
function runFixture(testRoot: string): Promise<ChildResult> {
|
||||
const dataDir = join(testRoot, "data");
|
||||
const pluginsDir = join(testRoot, "plugins");
|
||||
// Keep config fallbacks inside the fixture root without inheriting or repurposing HOME.
|
||||
const xdgConfigDir = join(testRoot, "xdg-config");
|
||||
|
||||
for (const dir of [dataDir, pluginsDir, xdgConfigDir]) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
const childEnv: NodeJS.ProcessEnv = {
|
||||
API_KEY_SECRET: SYNTHETIC_API_KEY_SECRET,
|
||||
APP_LOG_TO_FILE: "false",
|
||||
DATA_DIR: dataDir,
|
||||
DISABLE_SQLITE_AUTO_BACKUP: "true",
|
||||
FORCE_COLOR: "0",
|
||||
LANG: "C.UTF-8",
|
||||
NODE_ENV: "test",
|
||||
OMNIROUTE_HUGGINGCHAT_TEST_ROOT: testRoot,
|
||||
OMNIROUTE_HUGGINGCHAT_TEST_RUN_ID: basename(testRoot),
|
||||
OMNIROUTE_PLUGINS_DIR: pluginsDir,
|
||||
TZ: "UTC",
|
||||
XDG_CONFIG_HOME: xdgConfigDir,
|
||||
};
|
||||
if (process.env.PATH) childEnv.PATH = process.env.PATH;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(process.execPath, ["--import", "tsx/esm", FIXTURE], {
|
||||
cwd: REPO_ROOT,
|
||||
env: childEnv,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout.setEncoding("utf8").on("data", (chunk) => (stdout += chunk));
|
||||
child.stderr.setEncoding("utf8").on("data", (chunk) => (stderr += chunk));
|
||||
child.once("error", reject);
|
||||
child.once("close", (code, signal) => resolve({ code, signal, stdout, stderr }));
|
||||
});
|
||||
}
|
||||
|
||||
function childDiagnostics(result: ChildResult): string {
|
||||
return [
|
||||
`exit=${String(result.code)} signal=${String(result.signal)}`,
|
||||
"--- stdout ---",
|
||||
result.stdout,
|
||||
"--- stderr ---",
|
||||
result.stderr,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
test("HuggingChat stream error boundaries stay isolated from shared DB and usage state", async () => {
|
||||
const testRoot = mkdtempSync(join(tmpdir(), "omniroute-huggingchat-boundary-child-"));
|
||||
try {
|
||||
const result = await runFixture(testRoot);
|
||||
assert.equal(result.signal, null, childDiagnostics(result));
|
||||
assert.equal(result.code, 0, childDiagnostics(result));
|
||||
assert.match(result.stdout, /(?:#|ℹ) pass 8\b/, childDiagnostics(result));
|
||||
assert.match(result.stdout, /(?:#|ℹ) fail 0\b/, childDiagnostics(result));
|
||||
assert.doesNotMatch(result.stdout + result.stderr, /super-secret/);
|
||||
} finally {
|
||||
rmSync(testRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
||||
}
|
||||
});
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "../../open-sse/handlers/imageGeneration/providers/maxaiImage.ts";
|
||||
import { IMAGE_PROVIDERS } from "../../open-sse/config/imageRegistry.ts";
|
||||
import { __setMaxaiConstantsForTest } from "../../open-sse/executors/maxai/constantsStore.ts";
|
||||
import { MAXAI_BASE_URL } from "../../open-sse/executors/maxai/protocol.ts";
|
||||
import { MOCK_CONSTANTS } from "./helpers/maxaiMockConstants.ts";
|
||||
|
||||
// Image generation signs like any request; seed the in-process constants memo
|
||||
@@ -28,7 +29,9 @@ const CRED = {
|
||||
// --- Registry ------------------------------------------------------------
|
||||
|
||||
test("maxai is registered in IMAGE_PROVIDERS with the maxai-image format + 6 models", () => {
|
||||
const entry = (IMAGE_PROVIDERS as Record<string, { format?: string; baseUrl?: string; models?: unknown[] }>)["maxai"];
|
||||
const entry = (
|
||||
IMAGE_PROVIDERS as Record<string, { format?: string; baseUrl?: string; models?: unknown[] }>
|
||||
)["maxai"];
|
||||
assert.ok(entry, "maxai must exist in IMAGE_PROVIDERS");
|
||||
assert.equal(entry.format, "maxai-image");
|
||||
assert.match(String(entry.baseUrl), /api\.maxai\.me\/gpt\/get_image_generate_response/);
|
||||
@@ -93,7 +96,10 @@ test("handleMaxaiImageGeneration returns OpenAI image data on success", async ()
|
||||
ok: true,
|
||||
status: 200,
|
||||
async json() {
|
||||
return { status: "OK", data: [{ png_url: "https://cdn/x.png", webp_url: "https://cdn/x.webp" }] };
|
||||
return {
|
||||
status: "OK",
|
||||
data: [{ png_url: "https://cdn/x.png", webp_url: "https://cdn/x.webp" }],
|
||||
};
|
||||
},
|
||||
async text() {
|
||||
return "";
|
||||
@@ -111,8 +117,12 @@ test("handleMaxaiImageGeneration returns OpenAI image data on success", async ()
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.deepEqual(result.data?.data, [{ url: "https://cdn/x.png" }]);
|
||||
// Hit the image endpoint with the signed body.
|
||||
assert.match(capturedUrl, new RegExp(MAXAI_IMAGE_PATH.replace(/\//g, "\\/")));
|
||||
// Hit the image endpoint with the signed body. Exact URL equality instead of a
|
||||
// hand-escaped RegExp over the path — the old `.replace(/\//g, "\\/")` escaped
|
||||
// only slashes (which need no escaping in a RegExp anyway) and would have let
|
||||
// any other metacharacter through (CodeQL js/incomplete-sanitization), while
|
||||
// also accepting the path appearing anywhere in a wrong URL.
|
||||
assert.equal(capturedUrl, MAXAI_BASE_URL + MAXAI_IMAGE_PATH);
|
||||
assert.equal(capturedBody.model_name, "flux-1-schnell");
|
||||
assert.equal(capturedBody.size, "512x512"); // flux passes size through
|
||||
assert.equal(capturedBody.n, 2);
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
computeMaxaiProof,
|
||||
maxaiAesEncrypt,
|
||||
buildMaxaiSignedHeaders,
|
||||
maxaiRandomSlot,
|
||||
} from "../../open-sse/executors/maxai/signing.ts";
|
||||
import {
|
||||
assembleMaxaiContext,
|
||||
@@ -103,7 +104,13 @@ test("computeMaxaiProof blanks the user id only on /oauth/* routes", () => {
|
||||
// A blank-user route yields a different proof than the same route with a uid,
|
||||
// proving the uid is dropped for /oauth/* (and only there).
|
||||
const t = 1784594159681;
|
||||
const oauthWithUid = computeMaxaiProof("/oauth/signin_with_email", t, USER_ID, HMAC_KEY, APP_VERSION);
|
||||
const oauthWithUid = computeMaxaiProof(
|
||||
"/oauth/signin_with_email",
|
||||
t,
|
||||
USER_ID,
|
||||
HMAC_KEY,
|
||||
APP_VERSION
|
||||
);
|
||||
const oauthNoUid = computeMaxaiProof("/oauth/signin_with_email", t, "", HMAC_KEY, APP_VERSION);
|
||||
assert.equal(oauthWithUid, oauthNoUid); // uid ignored for /oauth/*
|
||||
const chatWithUid = computeMaxaiProof("/gpt/cwc/chat", t, USER_ID, HMAC_KEY, APP_VERSION);
|
||||
@@ -306,7 +313,28 @@ test("buildMaxaiSignedHeaders emits the X-App/X-Browser companions + X-Authoriza
|
||||
assert.equal(h["X-App-Version"], MOCK_APP_VERSION);
|
||||
assert.equal(h["X-App-Env"], "MaxAI-Browser-Extension");
|
||||
assert.ok(h["X-Authorization"].length > 0);
|
||||
assert.equal(Buffer.from(h["X-Authorization"], "base64").subarray(0, 8).toString("ascii"), "Salted__");
|
||||
assert.equal(
|
||||
Buffer.from(h["X-Authorization"], "base64").subarray(0, 8).toString("ascii"),
|
||||
"Salted__"
|
||||
);
|
||||
});
|
||||
|
||||
test("maxaiRandomSlot emits an unbiased 6-digit X-Random slot", () => {
|
||||
// The wire slot is always exactly 6 decimal digits, i.e. 100000-999999.
|
||||
const samples = Array.from({ length: 4000 }, () => maxaiRandomSlot());
|
||||
for (const s of samples) {
|
||||
assert.match(s, /^\d{6}$/, `X-Random must be 6 digits, got: ${s}`);
|
||||
const n = Number(s);
|
||||
assert.ok(n >= 100000 && n <= 999999, `X-Random out of range: ${s}`);
|
||||
}
|
||||
// Regression guard for the modulo bias the previous
|
||||
// `randomBytes(4).readUInt32BE(0) % 900000` draw introduced: the value must
|
||||
// still spread across the whole range, not collapse onto its low end.
|
||||
assert.ok(new Set(samples).size > samples.length * 0.9, "X-Random must not repeat heavily");
|
||||
assert.ok(
|
||||
samples.some((s) => Number(s) < 550000) && samples.some((s) => Number(s) >= 550000),
|
||||
"X-Random must cover both halves of the 100000-999999 range"
|
||||
);
|
||||
});
|
||||
|
||||
// ── Context assembly ─────────────────────────────────────────────────────────
|
||||
@@ -364,7 +392,12 @@ test("contentToText flattens multipart content, dropping non-text parts", () =>
|
||||
});
|
||||
|
||||
test("buildMaxaiChatBody pins field order + constants", () => {
|
||||
const body = buildMaxaiChatBody({ conversationId: "conv-1", text: "hi", modelName: "gpt-5.6", appVersion: APP_VERSION });
|
||||
const body = buildMaxaiChatBody({
|
||||
conversationId: "conv-1",
|
||||
text: "hi",
|
||||
modelName: "gpt-5.6",
|
||||
appVersion: APP_VERSION,
|
||||
});
|
||||
const keys = Object.keys(body);
|
||||
assert.equal(keys[0], "chat_mode");
|
||||
assert.equal(keys[3], "message_content");
|
||||
@@ -379,7 +412,12 @@ test("buildMaxaiChatBody pins field order + constants", () => {
|
||||
// ── Vision input (image_url parts) ───────────────────────────────────────────
|
||||
|
||||
test("buildMaxaiChatBody text-only path is unchanged (no imageUrls)", () => {
|
||||
const body = buildMaxaiChatBody({ conversationId: "c", text: "hi", modelName: "gpt-5.6", appVersion: APP_VERSION });
|
||||
const body = buildMaxaiChatBody({
|
||||
conversationId: "c",
|
||||
text: "hi",
|
||||
modelName: "gpt-5.6",
|
||||
appVersion: APP_VERSION,
|
||||
});
|
||||
// Byte-identical to the pre-vision shape: a single text part.
|
||||
assert.deepEqual(body.message_content, [{ type: "text", text: "hi" }]);
|
||||
assert.deepEqual(body.doc_list, []);
|
||||
@@ -563,8 +601,7 @@ test("maxaiRefreshAccessToken sends the exact web-app request + parses data.acce
|
||||
|
||||
test("maxaiRefreshAccessToken returns a structured error on non-200 (no throw)", async () => {
|
||||
const nowSec = Math.floor(Date.now() / 1000);
|
||||
const fakeFetch = (async () =>
|
||||
new Response("nope", { status: 418 })) as unknown as typeof fetch;
|
||||
const fakeFetch = (async () => new Response("nope", { status: 418 })) as unknown as typeof fetch;
|
||||
const result = await maxaiRefreshAccessToken({
|
||||
refreshToken: fakeJwt(nowSec + 1000, USER_ID),
|
||||
deviceId: "dev",
|
||||
@@ -687,7 +724,9 @@ test("verifyMaxaiEmailCode maps code 10119 to an expired-code message", async ()
|
||||
|
||||
test("verifyMaxaiEmailCode defaults to an invalid-code message otherwise", async () => {
|
||||
const fakeFetch = (async () =>
|
||||
new Response(JSON.stringify({ data: { status: "FAIL" } }), { status: 200 })) as unknown as typeof fetch;
|
||||
new Response(JSON.stringify({ data: { status: "FAIL" } }), {
|
||||
status: 200,
|
||||
})) as unknown as typeof fetch;
|
||||
const r = await verifyMaxaiEmailCode({
|
||||
email: "x@y.z",
|
||||
code: "999999",
|
||||
@@ -1009,10 +1048,9 @@ test("discoverMaxaiModels drops deprecated, non-chat, and non-curated models", a
|
||||
|
||||
test("discoverMaxaiModels falls back to the catalog window when max_tokens is absent", async () => {
|
||||
const fakeFetch = (async () =>
|
||||
new Response(
|
||||
modelsConfigBody([{ model_name: "claude-5-sonnet", type: "chat" }]),
|
||||
{ status: 200 }
|
||||
)) as unknown as typeof fetch;
|
||||
new Response(modelsConfigBody([{ model_name: "claude-5-sonnet", type: "chat" }]), {
|
||||
status: 200,
|
||||
})) as unknown as typeof fetch;
|
||||
const { models } = await discoverMaxaiModels({
|
||||
providerSpecificData: DISCOVERY_CRED.providerSpecificData,
|
||||
accessToken: DISCOVERY_CRED.accessToken,
|
||||
|
||||
62
tests/unit/stream-handler-public-error-boundary.test.ts
Normal file
62
tests/unit/stream-handler-public-error-boundary.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import test from "node:test";
|
||||
|
||||
const REPO_ROOT = fileURLToPath(new URL("../..", import.meta.url));
|
||||
const FIXTURE = fileURLToPath(
|
||||
new URL("../fixtures/stream-handler-public-error-boundary.fixture.ts", import.meta.url)
|
||||
);
|
||||
|
||||
const CHILD_RUNTIME_ENV_KEYS = [
|
||||
"PATH",
|
||||
"TMPDIR",
|
||||
"TMP",
|
||||
"TEMP",
|
||||
"SystemRoot",
|
||||
"ComSpec",
|
||||
"PATHEXT",
|
||||
"LANG",
|
||||
"LC_ALL",
|
||||
"TZ",
|
||||
] as const;
|
||||
|
||||
function buildFixtureEnv(): NodeJS.ProcessEnv {
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
NODE_ENV: "test",
|
||||
APP_LOG_TO_FILE: "false",
|
||||
API_KEY_SECRET: "stream-handler-boundary-fixture-secret-20260902",
|
||||
DISABLE_SQLITE_AUTO_BACKUP: "true",
|
||||
NO_COLOR: "1",
|
||||
};
|
||||
|
||||
for (const key of CHILD_RUNTIME_ENV_KEYS) {
|
||||
const value = process.env[key];
|
||||
if (value !== undefined) env[key] = value;
|
||||
}
|
||||
|
||||
// Nested test runners must not inherit the parent runner's recursion marker.
|
||||
delete env.NODE_TEST_CONTEXT;
|
||||
return env;
|
||||
}
|
||||
|
||||
test("generic stream public error boundaries pass in an isolated process", () => {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
["--import", "tsx/esm", "--import", "./open-sse/utils/setupPolyfill.ts", "--test", FIXTURE],
|
||||
{
|
||||
cwd: REPO_ROOT,
|
||||
encoding: "utf8",
|
||||
env: buildFixtureEnv(),
|
||||
timeout: 120_000,
|
||||
}
|
||||
);
|
||||
const output = `${result.stdout}\n${result.stderr}`;
|
||||
|
||||
assert.ifError(result.error);
|
||||
assert.equal(result.signal, null, output.slice(-12_000));
|
||||
assert.equal(result.status, 0, output.slice(-12_000));
|
||||
assert.match(output, /(?:^|\s)tests\s+6(?:\s|$)/m);
|
||||
assert.match(output, /(?:^|\s)pass\s+6(?:\s|$)/m);
|
||||
assert.match(output, /(?:^|\s)fail\s+0(?:\s|$)/m);
|
||||
});
|
||||
@@ -256,7 +256,8 @@ test("createDisconnectAwareStream emits Responses API failure events for Respons
|
||||
|
||||
assert.match(text, /event: response\.failed/);
|
||||
assert.match(text, /"type":"response\.failed"/);
|
||||
assert.match(text, /"message":"responses stream\\ndied"/);
|
||||
assert.match(text, /"message":"responses stream"/);
|
||||
assert.doesNotMatch(text, /died/);
|
||||
assert.match(text, /"type":"server_error"/);
|
||||
assert.match(text, /"code":"server_error"/);
|
||||
assert.doesNotMatch(text, /chat\.completion\.chunk/);
|
||||
@@ -264,7 +265,7 @@ test("createDisconnectAwareStream emits Responses API failure events for Respons
|
||||
assert.doesNotMatch(text, /\[DONE\]/);
|
||||
});
|
||||
|
||||
test("createDisconnectAwareStream keeps newlines escaped inside SSE data fields", async () => {
|
||||
test("createDisconnectAwareStream strips multiline diagnostic tails from Responses errors", async () => {
|
||||
const upstreamError = Object.assign(new Error("line one\nline two\rline three"), {
|
||||
statusCode: 400,
|
||||
});
|
||||
@@ -290,9 +291,9 @@ test("createDisconnectAwareStream keeps newlines escaped inside SSE data fields"
|
||||
const text = await readStreamText(stream);
|
||||
|
||||
assert.match(text, /^event: response\.failed\ndata: \{"type":"response\.failed"/);
|
||||
assert.match(text, /"message":"line one\\nline two\\rline three"/);
|
||||
assert.doesNotMatch(text, /^line two/m);
|
||||
assert.doesNotMatch(text, /^line three/m);
|
||||
assert.match(text, /"message":"line one"/);
|
||||
assert.doesNotMatch(text, /line two/);
|
||||
assert.doesNotMatch(text, /line three/);
|
||||
});
|
||||
|
||||
test("createDisconnectAwareStream treats legacy OpenAI response format alias as Responses", async () => {
|
||||
@@ -360,7 +361,7 @@ test("createDisconnectAwareStream emits Claude SSE errors for Claude clients", a
|
||||
assert.doesNotMatch(text, /\[DONE\]/);
|
||||
});
|
||||
|
||||
test("createDisconnectAwareStream keeps newlines escaped for Claude SSE errors", async () => {
|
||||
test("createDisconnectAwareStream strips multiline diagnostic tails from Claude errors", async () => {
|
||||
const upstreamError = Object.assign(new Error("claude line one\nclaude line two"), {
|
||||
statusCode: 502,
|
||||
});
|
||||
@@ -386,8 +387,8 @@ test("createDisconnectAwareStream keeps newlines escaped for Claude SSE errors",
|
||||
const text = await readStreamText(stream);
|
||||
|
||||
assert.match(text, /^event: error\ndata: \{"type":"error"/);
|
||||
assert.match(text, /"message":"claude line one\\nclaude line two"/);
|
||||
assert.doesNotMatch(text, /^claude line two/m);
|
||||
assert.match(text, /"message":"claude line one"/);
|
||||
assert.doesNotMatch(text, /claude line two/);
|
||||
});
|
||||
|
||||
// #7699/#7816 — heuristic is scoped to FORMATS.CLAUDE (/v1/messages); a
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
UC_DIRECT_IMAGE_URL,
|
||||
} from "../../open-sse/handlers/imageGeneration/providers/ucImage.ts";
|
||||
import { IMAGE_PROVIDERS, parseImageModel } from "../../open-sse/config/imageRegistry.ts";
|
||||
import { isUcClerkMintUrl } from "./helpers/ucClerkUrl.ts";
|
||||
|
||||
// A valid PERSONA credential (durable Clerk cookie + sid + uid in psd). No API
|
||||
// key, so the handler takes the persona web path (mint -> POST -> poll).
|
||||
@@ -144,7 +145,7 @@ function personaFetch(opts: {
|
||||
let pollsSeen = 0;
|
||||
return (async (url: string, init: RequestInit = {}) => {
|
||||
// 1) Clerk mint
|
||||
if (url.includes("clerk.uncensored.com")) {
|
||||
if (isUcClerkMintUrl(url)) {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
@@ -265,7 +266,7 @@ test("handleUcImageGeneration (persona) times out with 504 when the result never
|
||||
|
||||
test("handleUcImageGeneration (persona) surfaces a Clerk mint failure", async () => {
|
||||
const fetchImpl = (async (url: string) => {
|
||||
if (url.includes("clerk.uncensored.com")) {
|
||||
if (isUcClerkMintUrl(url)) {
|
||||
return {
|
||||
ok: false,
|
||||
status: 401,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
UC_DIRECT_VIDEO_URL,
|
||||
} from "../../open-sse/handlers/videoGeneration/providers/ucVideo.ts";
|
||||
import { VIDEO_PROVIDERS } from "../../open-sse/config/videoRegistry.ts";
|
||||
import { isUcClerkMintUrl } from "./helpers/ucClerkUrl.ts";
|
||||
|
||||
// A valid PERSONA credential (durable Clerk cookie + sid + uid in psd). No API
|
||||
// key, so the handler takes the persona web path (mint -> generate -> poll).
|
||||
@@ -148,7 +149,7 @@ function personaFetch(opts: {
|
||||
let pollsSeen = 0;
|
||||
return (async (url: string, init: RequestInit = {}) => {
|
||||
// Clerk mint
|
||||
if (url.includes("clerk.uncensored.com")) {
|
||||
if (isUcClerkMintUrl(url)) {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
@@ -339,7 +340,7 @@ test("handleUcVideoGeneration (persona) times out with 504 when never ready", as
|
||||
|
||||
test("handleUcVideoGeneration (persona) surfaces a Clerk mint failure", async () => {
|
||||
const fetchImpl = (async (url: string) => {
|
||||
if (url.includes("clerk.uncensored.com")) {
|
||||
if (isUcClerkMintUrl(url)) {
|
||||
return {
|
||||
ok: false,
|
||||
status: 401,
|
||||
|
||||
Reference in New Issue
Block a user