Release v3.8.39 (#5164)

* chore(release): open v3.8.39 development cycle

* docs(changelog): backfill 5 v3.8.38 bullets merged after release finalize

These PRs squash-merged into release/v3.8.38 between the CHANGELOG finalize
(ff57be32f) and the merge-to-main (ae6e2342d), so they shipped in the v3.8.38
tag but had no bullet:

- feat(compression): Ionizer engine (lossy JSON-array sampling + CCR) (#5148)
- fix(sse): preserve non-stream reasoning fields (#5155, @rdself)
- fix(i18n): add missing English UI labels (#5153, @rdself)
- test(combo): gated live smoke (#5151) + release-expectations refresh (#5150, @KooshaPari)

(#5129 exact-host Anthropic baseUrl is already covered by the #5130 bullet — same CodeQL #674.)
Synced 41 i18n CHANGELOG mirrors.

* feat(compression): TOON best-of-N candidate encoder + encoder A/B table (#5163)

Integrated into release/v3.8.39. TOON best-of-N candidate encoder (GCF default, fail-open). 17/17 unit tests pass on merge result; CI reds were base-stale + Quality Ratchet DRIFT.

* fix(zenmux): normalize vendor-prefixed GLM system roles (#5158)

Integrated into release/v3.8.39. ZenMux vendor-prefixed GLM system-role normalization; 12/12 role-normalizer tests pass on merge result. CI reds base-stale.

* [codex] fix xAI OAuth test and reasoning effort (#5157)

Integrated into release/v3.8.39. xAI reasoning-effort normalization (max/xhigh→high) + OAuth test config; 46/46 xai-translator tests pass on merge result. CI reds base-stale.

* docs(i18n): add Traditional Chinese (zh-TW) README and update zh-CN to latest (#5162)

Integrated into release/v3.8.39. Traditional Chinese (zh-TW) README + zh-CN refresh; docs-only.

* test(security): guard PII redaction stays opt-in (default off) + Hard Rule #20 (#5159)

Integrated into release/v3.8.39. PII opt-in regression guard + Hard Rule #20; rebased to strip base-drift (+81/-1). 5/5 guard tests pass; flip-proof verified.

* test(combo): deterministic context-relay universal-handoff coverage (closes phase-2 TODO) (#5168)

Integrated into release/v3.8.39. Deterministic context-relay universal-handoff coverage (3 tests); 3/3 pass on merge result.

* docs(i18n): full sync zh-TW and zh-CN README with canonical English v3.8.39 (#5171)

Integrated into release/v3.8.39. Full zh-TW docs tree + zh-CN sync with canonical English v3.8.39; docs-only.

* fix(serve): honour HOSTNAME from .env instead of hardcoding 0.0.0.0 (#5134) (#5170)

Integrated into release/v3.8.39. HOSTNAME env override in serve (#5134) + regression test (4/4, TDD flip-proof verified).

* fix(sse): resolve nameless deepseek-web tool blocks via parameter-schema match (#5154) (#5173)

Integrated into release/v3.8.39. Schema-based nameless deepseek-web tool-block resolution (#5154); 6/6 tests pass on merge result (incl. ambiguous/no-match negatives + named-tag no-regression).

* fix(sse): normalize array user content for Command Code to avoid upstream 400 (#5166) (#5174)

Integrated into release/v3.8.39. Normalize array user content for Command Code (#5166, user-array/400 symptom); 4/4 tests pass on merge result.

* fix(sse): defer </think> close so it never leaks before tool_calls (#5123) (#5175)

Integrated into release/v3.8.39. Defer </think> close so it never leaks before tool_calls (#5123); 4/4 tests pass (incl. #4633 no-regression). CHANGELOG synced to keep all 3 v3.8.39 fixes.

* fix(dashboard): use amber for home update-step warning icon (#5176)

Integrated into release/v3.8.39. Amber for home update-step warning icon; 1/1 UI test.

* fix(api): LAN/Tailscale dashboard — host-aware CSP + GET-exempt version route + combo field errors (#5083) (#5177)

Integrated into release/v3.8.39. Host-aware CSP (ReDoS/injection-safe host validation) + GET-exempt /api/system/version (POST/spawn stays LOCAL_ONLY, exact-match safe-methods-only) + COMBO_002 firstField. 44/44 tests + route-guard membership gate green. CHANGELOG synced to keep all 4 v3.8.39 fixes.

* fix(api): replace #5083 global middleware CSP with declarative ws: scheme (#5083)

Follow-up to PR #5177 (merged): that version implemented the LAN-CSP fix (Bug 1)
with a new global `src/middleware.ts` + `src/server/csp.ts`, which contradicts the
project's documented architecture — 'No global Next.js middleware — interception is
route-specific' (CLAUDE.md / AGENTS.md) — and was merged unverified (middleware vs
next.config header precedence was never confirmed in a real build).

This replaces that approach with the minimal, declarative equivalent:
  • next.config.mjs: connect-src now permits the bare `ws:` scheme (symmetric with the
    bare `wss:` already allowed) so the dashboard can reach its own Live WS server from
    a LAN/Tailscale host. No middleware.
  • Removes src/middleware.ts, src/server/csp.ts, and tests/unit/csp-host-aware.test.ts.
  • Adds tests/unit/csp-lan-ws-5083.test.ts (incl. a guard asserting src/middleware.ts
    does NOT exist, so the global-middleware approach cannot silently return).

Bugs 2 (GET-exempt /api/system/version) and 3 (COMBO_002 field surfacing) from #5177
are unaffected and remain in place.

Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com>

* test(combo): end-to-end quota-share DRR routing-decision coverage (matrix parity) (#5179)

Integrated into release/v3.8.39. Quota-share DRR routing-decision coverage (matrix parity); 2/2 pass on merge result.

* feat(agent-bridge): graceful cert-install fallback with manual guide for containers (#4546) (#5178)

Integrated into release/v3.8.39. Agent-bridge graceful cert-install fallback + manual guide (#4546); 6/6 tests pass on merge result.

* fix(antigravity): family-scoped quota lockout (gemini/claude buckets) (#5180)

Integrated into release/v3.8.39 — family-scoped antigravity quota lockout. Rebased from v3.8.37 + validated (vitest 5/5, typecheck clean, full combo-matrix green, model-lockout 99/0). Same-model cross-account retry (chat.ts) deferred pending live antigravity VPS validation.

* fix(cli): force NODE_ENV to match dev/start run mode in custom Next server (#5189)

Integrated into release/v3.8.39. Force NODE_ENV to match dev/start run mode in custom Next server; 2/2 source-scan+ordering tests pass on merge result.

* feat(compression): CCR ranged/grep/stats retrieval (ReDoS-safe, backward-compat) (#5187)

Integrated into release/v3.8.39. CCR ranged/grep/stats retrieval (safe-regex ReDoS guard + length/match caps); 17/17 tests pass on merge result.

* docs(combo): sync all combo/routing-strategy docs to current state + document test coverage (#5185)

Integrated into release/v3.8.39. Combo/routing-strategy docs sync; docs-only.

* fix(mcp): return 404 (not 400) for unknown Streamable HTTP session id (#5169) (#5191)

* fix(api): respect blocked Auto (Zero-Config) provider in /v1/models catalog (#5192) (#5194)

* test(combo): deterministic context-relay codex quota-handoff coverage (closes last gap) (#5195)

* test(ci): wire antigravity-quota-family under test:vitest (fix test-discovery orphan) (#5196)

* fix(oauth): antigravity login no longer hangs — fire-and-forget onboarding + bounded post-exchange (#5193)

Antigravity OAuth hang fix (no-PKCE/no-openid + bounded post-exchange + exchange-500 fix). Includes #5200 (Koosha) revert + owner rebaseline to keep documented comments. Integrated into release/v3.8.39.

* feat(oauth): remote Antigravity login via local helper + paste-credentials (#5203)

Remote Antigravity login: local helper (omniroute login antigravity) + paste-credentials. Integrated into release/v3.8.39.

* fix(translator): accept Claude Messages shape in non-stream malformed-200 guard (#5156)

Integrated into release/v3.8.39

* fix(cli): default dev bundler to Turbopack (16.2.x panic no longer reproduces) (#5206)

Integrated into release/v3.8.39

* fix(cli): auto-calibrate server V8 heap from physical RAM (#5172) (#5213)

The server was spawned with a fixed --max-old-space-size=512 (omniroute serve)
or no heap flag at all (Electron), so RAM-rich boxes still OOM-crashed under
load (Ineffective mark-compacts near heap limit ~500MB) with many providers/
accounts and large model catalogs. New calibrateHeapFallbackMb(os.totalmem())
defaults the heap to ~35% of RAM clamped [512,4096], wired into serve.mjs and
electron/main.js. Explicit OMNIROUTE_MEMORY_MB still wins (#2939 unchanged).

Also addresses #5160 (same OOM root); #5152 (docker) benefits via the same knob.

Closes #5172

* fix(proxy): coalesce fast-fail health probes (#5208)

Integrated into release/v3.8.39

* fix(proxy): close dispatchers when clearing cache (#5202)

Integrated into release/v3.8.39

* fix(cli): raise dev server Node heap limit to 8GB to prevent OOM (#5198)

Integrated into release/v3.8.39

* fix(auth): allow synthetic no-auth fallback for mimocode (#5205)

Integrated into release/v3.8.39

* fix(oauth): preserve Antigravity refresh_token on empty/omitted upstream response (#3850) (#5214)

Google's OAuth refresh tokens are non-rotating: the refresh response usually
omits refresh_token and occasionally returns it as an empty string. The
Antigravity executor used `typeof tokens.refresh_token === "string" ? ... `
which accepts "" (typeof "" === "string") and overwrote the stored token with
empty, nulling it on first refresh. Now treats non-string OR empty as absent and
preserves credentials.refreshToken, matching refreshGoogleToken semantics.

Closes #3850

* fix(responses): normalize non-array input (#5204)

Integrated into release/v3.8.39

* fix(stream): normalize safety finish reasons via shared helper (#5197)

Integrated into release/v3.8.39

* fix(request-logger): never render negative '(-100%)' compression badge (#5201)

Integrated into release/v3.8.39

* fix(combo): reject empty responses api output (#5207)

Integrated into release/v3.8.39 — combo failover now rejects empty Responses API output (validateQuality). Baseline rebaseline dropped (main-measured drift; maintainer rebaselines at release).

* fix(pwa): prefer cached navigation before offline page (#5209)

Integrated into release/v3.8.39 — PWA service worker prefers cached navigation before offline page (#5165).

* chore(release): v3.8.39 — 2026-06-28

* chore(release): rebaseline openapi+i18n coverage ratchet drift for v3.8.39

---------

Co-authored-by: Arthur Bodera <abodera@gmail.com>
Co-authored-by: Nguyen Minh <lop123thcs@gmail.com>
Co-authored-by: lunkerchen <labanchen@gmail.com>
Co-authored-by: Ankit <177378174+anki1kr@users.noreply.github.com>
Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com>
Co-authored-by: Ardem2025 <ardemb22@gmail.com>
Co-authored-by: backryun <bakryun0718@proton.me>
Co-authored-by: Anton <39598727+NomenAK@users.noreply.github.com>
Co-authored-by: KooshaPari <42529354+KooshaPari@users.noreply.github.com>
Co-authored-by: Wilson <pedbookmed@gmail.com>
Co-authored-by: Randi <55005611+rdself@users.noreply.github.com>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-28 06:58:29 -03:00
committed by GitHub
parent 7b139fdb5e
commit dc40911583
211 changed files with 26913 additions and 2531 deletions

View File

@@ -0,0 +1,367 @@
// tests/integration/combo-matrix/context-relay-codex.test.ts
//
// Deterministic in-process test for context-relay CODEX-SPECIFIC HANDOFF
// (combo.ts ~2143-2183): the quota-aware handoff that fires after a successful
// codex response when the quota exceeds the threshold.
//
// Code path:
// if (strategy === "context-relay" && relayOptions?.sessionId && relayConfig &&
// relayConfig.handoffProviders.includes(provider) && provider === "codex") {
// const connectionId = getSessionConnection(relayOptions.sessionId);
// if (connectionId) {
// const quotaInfo = await fetchCodexQuota(connectionId).catch(() => null);
// if (quotaInfo) { ... maybeGenerateHandoff({ ..., expiresAt: resetCandidates[0] }); }
// }
// }
//
// PRIMARY observable: getHandoff(sessionId, comboName).expiresAt === session-window
// reset time from the codex quota response. This is the CODEX-SPECIFIC proof:
// the universal handoff path does NOT set expiresAt from a codex quota fetch.
//
// SECONDARY observables:
// - The codex usage URL was fetched (proves the quota path ran).
// - The handoff record has a non-empty .summary.
//
// CONTROL: combo targeting openai (not codex) → codex block NEVER runs →
// codex usage URL is never fetched → expiresAt from quota doesn't appear.
//
// Seams used:
// - registerCodexConnection / unregisterCodexConnection (codexQuotaFetcher.ts)
// - clearSessions (sessionManager.ts) — cleanup between tests
// - buildCodexResponsesSse helper (mirrors codex-stream-false.test.ts)
// - Custom fetch mock distinguishes quota URL / summary dispatch / main dispatch
import test from "node:test";
import assert from "node:assert/strict";
import { createComboRoutingHarness } from "../_comboRoutingHarness.ts";
import {
registerCodexConnection,
unregisterCodexConnection,
} from "../../../open-sse/services/codexQuotaFetcher.ts";
import { clearSessions } from "../../../open-sse/services/sessionManager.ts";
// ── Harness setup ─────────────────────────────────────────────────────────────
// Each harness owns an isolated DATA_DIR so DB handles don't clash.
const h = await createComboRoutingHarness("combo-relay-codex");
const {
BaseExecutor,
combosDb,
handleChat,
buildRequest,
seedConnection,
resetStorage,
buildOpenAIResponse,
waitFor,
toPlainHeaders,
} = h;
// Import DB helpers AFTER harness creation so they share the same DB instance
// (DATA_DIR is set by the harness before any import triggers DB init).
const { getHandoff } = await import("../../../src/lib/db/contextHandoffs.ts");
// ── Constants ─────────────────────────────────────────────────────────────────
const CODEX_COMBO_NAME = "m-relay-codex-quota";
const SESSION_HEADER_VALUE = "relay-codex-quota-001";
const SESSION_ID = `ext:${SESSION_HEADER_VALUE}`;
// Codex endpoint URLs (must match codexQuotaFetcher.ts and the executor config).
const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
const CODEX_RESPONSES_HOST = "chatgpt.com/backend-api/codex/responses";
// Summary JSON that parseHandoffJSON will successfully parse.
const CODEX_SUMMARY_JSON = JSON.stringify({
summary:
"User is implementing a TypeScript context-relay codex quota-handoff test using TDD.",
keyDecisions: ["codex provider selected", "quota threshold at 90%"],
taskProgress: "writing deterministic integration test for codex handoff",
activeEntities: ["combo.ts", "codexQuotaFetcher.ts", "contextHandoff.ts"],
});
// ── SSE builder (mirrors codex-stream-false.test.ts) ─────────────────────────
function buildCodexResponsesSse(text = "codex assistant reply") {
return new Response(
[
"event: response.created",
'data: {"type":"response.created","response":{"id":"resp_codex_1","model":"gpt-5.3-codex","status":"in_progress","output":[]}}',
"",
"event: response.output_text.delta",
`data: ${JSON.stringify({
type: "response.output_text.delta",
output_index: 0,
delta: text,
})}`,
"",
"event: response.completed",
`data: ${JSON.stringify({
type: "response.completed",
response: {
id: "resp_codex_1",
object: "response",
model: "gpt-5.3-codex",
status: "completed",
output: [
{
type: "message",
role: "assistant",
content: [{ type: "output_text", text }],
},
],
usage: { input_tokens: 6, output_tokens: 4 },
},
})}`,
"",
"data: [DONE]",
"",
].join("\n"),
{
status: 200,
headers: { "Content-Type": "text/event-stream" },
}
);
}
// ── Quota response builder ────────────────────────────────────────────────────
// Produces a codex usage API response that parseCodexUsageResponse accepts.
// primary_window → session → percentUsed = 0.90 (90%) — triggers handoff (threshold 0.85)
// secondary_window → weekly → percentUsed = 0.85 (85%) — below threshold for weekly
// Both are below HANDOFF_EXHAUSTION_THRESHOLD (0.95) so maybeGenerateHandoff proceeds.
function buildCodexUsageBody(
sessionResetAtUnix: number,
weeklyResetAtUnix: number
): Record<string, unknown> {
return {
rate_limit: {
primary_window: { used_percent: 90, reset_at: sessionResetAtUnix },
secondary_window: { used_percent: 85, reset_at: weeklyResetAtUnix },
},
};
}
// ── Request builder ───────────────────────────────────────────────────────────
function codexRequest(withSessionId = true) {
return buildRequest({
headers: withSessionId ? { "x-session-id": SESSION_HEADER_VALUE } : {},
body: {
model: CODEX_COMBO_NAME,
stream: false,
messages: [{ role: "user", content: "Write a TypeScript hello world." }],
},
});
}
// ── Fetch mock ────────────────────────────────────────────────────────────────
// Handles three URL classes:
// 1. CODEX_USAGE_URL → codex quota JSON
// 2. CODEX_RESPONSES_HOST (call 1) → main request Responses API SSE
// 3. CODEX_RESPONSES_HOST (call 2) → summary request Responses API SSE (with CODEX_SUMMARY_JSON)
//
// The second codex-responses call is the handoff summary generation, identified by
// call count (the first is always the main request).
function installCodexHandoffFetch(
sessionResetAtUnix: number,
weeklyResetAtUnix: number,
seenUrls: string[]
) {
let codexResponsesCallCount = 0;
globalThis.fetch = async (url: unknown, init: unknown = {}) => {
const u = String(url);
seenUrls.push(u);
const headers =
init != null && typeof init === "object"
? toPlainHeaders((init as Record<string, unknown>).headers)
: {};
void headers; // recorded for debugging; not needed in assertions here
if (u === CODEX_USAGE_URL) {
// Codex quota endpoint — return structured usage JSON.
return new Response(
JSON.stringify(buildCodexUsageBody(sessionResetAtUnix, weeklyResetAtUnix)),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
}
if (u.includes(CODEX_RESPONSES_HOST)) {
codexResponsesCallCount++;
if (codexResponsesCallCount === 1) {
// Main codex request — return a successful Responses API SSE.
return buildCodexResponsesSse("codex assistant reply ok");
}
// Subsequent call(s) are from the handoff summary generation.
// Return Responses API SSE whose output_text is CODEX_SUMMARY_JSON so that
// generateHandoffAsync → parseHandoffJSON succeeds.
return buildCodexResponsesSse(CODEX_SUMMARY_JSON);
}
// Fallback — should not be reached in these tests.
return new Response(JSON.stringify({ error: { message: "unexpected URL in test" } }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
};
}
// ── Lifecycle hooks ───────────────────────────────────────────────────────────
test.beforeEach(async () => {
BaseExecutor.RETRY_CONFIG.delayMs = 0;
await resetStorage();
});
test.afterEach(async () => {
BaseExecutor.RETRY_CONFIG.delayMs = h.originalRetryDelayMs;
// Clear in-memory session state so test 2 doesn't see stale connections from test 1.
clearSessions();
await resetStorage();
});
test.after(async () => {
await h.cleanup();
});
// ── Test 1 (PRIMARY): codex quota handoff fires ───────────────────────────────
//
// Flow:
// 1. Seed a codex DB connection + register with accessToken.
// 2. Create context-relay combo → codex.
// 3. Send request with x-session-id header.
// 4. During request processing, chat.ts calls touchSession(sessionId, codexConnId).
// 5. After the successful codex response, combo.ts enters the codex block:
// - getSessionConnection → codexConnId (set in step 4).
// - fetchCodexQuota(codexConnId) → hits our mock → returns 90% usage.
// - resetCandidates[0] = session-window resetAt ISO string.
// - maybeGenerateHandoff({ ..., expiresAt: sessionResetISO }) fires via setImmediate.
// 6. generateHandoffAsync dispatches a second codex fetch for the summary.
// 7. upsertHandoff writes to DB with expiresAt = sessionResetISO.
//
// Assertion: handoff.expiresAt === sessionResetISO (codex-specific, not TTL-derived).
test("context-relay codex quota handoff: fires and expiresAt matches session-window reset from quota", async () => {
// 1. Seed codex connection.
const conn = await seedConnection("codex", { apiKey: "sk-codex-handoff-test-1" });
const codexConnId = conn.id;
// 2. Register connection meta so fetchCodexQuota can find credentials.
// chat.ts also calls registerCodexConnection during request processing for OAuth
// tokens; for API-key connections the accessToken may not be set there, so we
// pre-register with the API key as the token. This is the documented test seam.
registerCodexConnection(codexConnId, { accessToken: "sk-codex-handoff-test-1" });
// 3. Create a context-relay combo targeting codex.
// Default handoffProviders: ["codex"] and handoffThreshold: 0.85 apply.
await combosDb.createCombo({
name: CODEX_COMBO_NAME,
strategy: "context-relay",
config: { maxRetries: 0, retryDelayMs: 0, stickyRoundRobinLimit: 1 },
models: [
{ id: "rc-codex-1", kind: "model", providerId: "codex", model: "gpt-5.3-codex" },
],
});
// 4. Compute quota reset times (future timestamps).
const sessionResetAtUnix = Math.floor((Date.now() + 5 * 60 * 60 * 1000) / 1000); // +5h
const weeklyResetAtUnix = Math.floor((Date.now() + 7 * 24 * 60 * 60 * 1000) / 1000); // +7d
// Expected expiresAt: parseWindowReset converts reset_at (unix seconds) → ISO string.
// resetCandidates[0] = earliest ISO string = session window reset (5h < 7d).
const expectedExpiresAt = new Date(sessionResetAtUnix * 1000).toISOString();
// 5. Install fetch mock and send request.
const seenUrls: string[] = [];
installCodexHandoffFetch(sessionResetAtUnix, weeklyResetAtUnix, seenUrls);
const r = await handleChat(codexRequest(true));
assert.equal(r.status, 200, "main codex request must return 200");
// 6. Wait for setImmediate → generateHandoffAsync → upsertHandoff (up to 3s).
const handoff = await waitFor(() => getHandoff(SESSION_ID, CODEX_COMBO_NAME), 3000);
// ── Primary assertion: handoff record exists ──────────────────────────────
assert.ok(
handoff !== null,
"codex quota handoff record must be written to DB when quota ≥ threshold"
);
// ── CODEX-SPECIFIC proof: expiresAt == session-window reset from quota ────
// The universal handoff path never calls fetchCodexQuota, so it cannot produce
// this specific expiresAt value. The value below can only come from the codex block.
assert.equal(
handoff!.expiresAt,
expectedExpiresAt,
`handoff.expiresAt must equal the session-window reset from codex quota (${expectedExpiresAt}); ` +
`got ${handoff!.expiresAt}`
);
// ── Secondary assertion: codex usage URL was fetched ─────────────────────
assert.ok(
seenUrls.includes(CODEX_USAGE_URL),
`codex usage URL must have been fetched to produce the expiresAt; seen URLs: ${JSON.stringify(seenUrls)}`
);
// ── Secondary assertion: summary was generated (non-empty) ───────────────
assert.ok(
typeof handoff!.summary === "string" && handoff!.summary.length > 0,
`handoff.summary must be non-empty; got ${JSON.stringify(handoff!.summary)}`
);
// ── Cleanup ───────────────────────────────────────────────────────────────
unregisterCodexConnection(codexConnId);
});
// ── Test 2 (CONTROL): non-codex provider never triggers the codex block ───────
//
// The codex block gates on `provider === "codex"`. A combo targeting openai
// will never enter it, so the codex usage URL is never fetched and there is no
// quota-derived expiresAt in the DB.
//
// This proves the primary test is not trivially green (i.e., the block is
// actually gated on `provider === "codex"`).
test("context-relay codex quota handoff: does NOT fire when provider is openai (control)", async () => {
// Seed openai (not codex).
await seedConnection("openai", { apiKey: "sk-openai-control-no-codex-block" });
await combosDb.createCombo({
name: CODEX_COMBO_NAME,
strategy: "context-relay",
config: { maxRetries: 0, retryDelayMs: 0, stickyRoundRobinLimit: 1 },
models: [
{ id: "rc-openai-ctrl", kind: "model", providerId: "openai", model: "gpt-4o-mini" },
],
});
const seenUrls: string[] = [];
globalThis.fetch = async (url: unknown, _init: unknown = {}) => {
seenUrls.push(String(url));
return buildOpenAIResponse("assistant reply ok");
};
const r = await handleChat(codexRequest(true));
assert.equal(r.status, 200, "openai request must return 200");
// Give setImmediate time to fire if the block were incorrectly entered.
await new Promise((res) => setTimeout(res, 400));
// The codex block requires provider === "codex", so it never runs for openai.
// The codex usage URL must NOT have been fetched.
assert.ok(
!seenUrls.includes(CODEX_USAGE_URL),
`codex usage URL must NOT be fetched for openai provider; seen: ${JSON.stringify(seenUrls)}`
);
// No codex quota handoff record in DB.
// (The universal handoff also does not fire because no prior model is seeded,
// so getLastSessionModel returns null → no model switch detected.)
const handoff = getHandoff(SESSION_ID, CODEX_COMBO_NAME);
assert.equal(
handoff,
null,
"no handoff record must exist when provider is openai (codex block never entered)"
);
});

View File

@@ -0,0 +1,282 @@
// tests/integration/combo-matrix/context-relay-handoff.test.ts
//
// Deterministic in-process tests for context-relay UNIVERSAL HANDOFF behavior:
// the session-context transfer that fires when a model switch is detected,
// regardless of provider (provider-agnostic). This is the distinguishing
// behavior of context-relay that was deferred as TODO(phase-2) in the
// original combo-matrix coverage.
//
// What is tested here:
// 1. Universal handoff fires (extra summary dispatch + DB record) when:
// - universalHandoffConfig.enabled = true (default)
// - request carries x-omniroute-session-id header
// - session_model_history records a DIFFERENT prior model than the combo target
// 2. Control (no model switch): prevModel === currModel — handoff must NOT fire.
// 3. Control (no session ID): no header passed — handoff must NOT fire.
//
// Codex-specific block (lines 2143-2183 in combo.ts):
// Requires strategy === "context-relay" AND provider === "codex" AND a live
// codex session connection + quota fetcher. The seams (getSessionConnection /
// fetchCodexQuota) are NOT exported from contextHandoff.ts as testable hooks;
// there is no in-process mechanism equivalent to registerQuotaFetcher for the
// codex path. This block requires a real codex provider connection (VPS) and
// is NOT covered here. The universal-handoff path above is the user-visible,
// provider-agnostic behavior and is now fully covered.
import test from "node:test";
import assert from "node:assert/strict";
import { createComboRoutingHarness, providerFromUrl } from "../_comboRoutingHarness.ts";
const h = await createComboRoutingHarness("combo-relay-handoff");
const {
BaseExecutor,
combosDb,
handleChat,
buildRequest,
seedConnection,
resetStorage,
buildOpenAIResponse,
waitFor,
toPlainHeaders,
} = h;
// Import DB helpers AFTER harness so they share the same DB instance (DATA_DIR
// is set by the harness before any import triggers DB init).
const { recordSessionModelUsage, getHandoff } = await import(
"../../../src/lib/db/contextHandoffs.ts"
);
// A minimal but valid handoff-JSON blob that parseHandoffJSON will accept.
// Must have at minimum a non-empty "summary" field.
const SCRIPTED_SUMMARY_JSON = JSON.stringify({
summary: "User initiated a context-relay handoff test and requested a model switch.",
keyDecisions: ["switched from gemini to openai"],
taskProgress: "in-progress",
activeEntities: ["context-relay-test"],
});
const COMBO_NAME = "m-relay-handoff";
// The session ID must use the external-session format: extractExternalSessionId in chat.ts
// reads "x-session-id" (not "x-omniroute-session-id") and prefixes the result with "ext:".
// We must seed session_model_history with this exact "ext:"-prefixed ID so that
// getLastSessionModel(relayOptions.sessionId, comboName) returns the prior model.
const SESSION_HEADER_VALUE = "relay-handoff-session-001";
const SESSION_ID = `ext:${SESSION_HEADER_VALUE}`; // matches what extractExternalSessionId produces
const PREV_MODEL = "gemini/gemini-2.5-flash"; // the "old" model — must differ from combo target
const CURR_MODEL = "openai/gpt-4o-mini"; // the combo target model (provider/model format)
// Build a Request with the session-id header.
function relayRequest(withSessionId = true) {
const headers: Record<string, string> = withSessionId
? { "x-session-id": SESSION_HEADER_VALUE } // x-session-id → relayOptions.sessionId = "ext:..."
: {};
return buildRequest({
headers,
body: {
model: COMBO_NAME,
stream: false,
messages: [
{ role: "user", content: "Prior turn — building something important in the test session." },
],
},
});
}
// Install a recording fetch that:
// • returns a valid handoff JSON (wrapped in an OpenAI completion) for the
// internal summary request (identified by _omnirouteInternalRequest flag in body)
// • returns a normal OpenAI response for every other call
function installHandoffAwareFetch() {
h.calls.length = 0;
globalThis.fetch = async (url: any, init: any = {}) => {
const u = String(url);
const provider = providerFromUrl(u);
const headers = toPlainHeaders(init?.headers);
// Parse request body to detect the internal summary request
let bodyObj: Record<string, unknown> = {};
try {
bodyObj =
typeof init?.body === "string"
? (JSON.parse(init.body) as Record<string, unknown>)
: ((init?.body ?? {}) as Record<string, unknown>);
} catch {
bodyObj = {};
}
const call = {
index: h.calls.length,
provider,
url: u,
authorization: headers.authorization,
model: typeof bodyObj.model === "string" ? bodyObj.model : undefined,
};
h.calls.push(call);
// Return valid handoff JSON for the internal summary generation request
if (bodyObj._omnirouteInternalRequest === "universal-handoff") {
return buildOpenAIResponse(SCRIPTED_SUMMARY_JSON);
}
// Normal success response for the real request
return buildOpenAIResponse("assistant reply ok");
};
}
test.beforeEach(async () => {
BaseExecutor.RETRY_CONFIG.delayMs = 0;
await resetStorage();
});
test.afterEach(async () => {
BaseExecutor.RETRY_CONFIG.delayMs = h.originalRetryDelayMs;
await resetStorage();
});
test.after(async () => {
await h.cleanup();
});
// ── Test 1: universal handoff fires on model switch ────────────────────────────
//
// Seed session history so getLastSessionModel returns PREV_MODEL (gemini).
// Combo target is CURR_MODEL (openai). The switch is detected and
// maybeGenerateUniversalHandoff fires via setImmediate → calls handleSingleModel
// (extra upstream dispatch) → parses response → upsertHandoff writes to DB.
//
// Observable 1: h.calls has ≥2 entries (main request + summary dispatch).
// Observable 2: getHandoff(sessionId, comboName) returns a record with a
// non-empty .summary (proves the FULL path executed, not just the
// dispatch). The assertion FAILS if the handoff did not fire.
test("context-relay universal handoff: fires and writes handoff record on model switch", async () => {
await seedConnection("openai", { apiKey: "sk-openai-handoff" });
await seedConnection("gemini", { apiKey: "sk-gemini-handoff" });
await combosDb.createCombo({
name: COMBO_NAME,
strategy: "context-relay",
config: { maxRetries: 0, retryDelayMs: 0, stickyRoundRobinLimit: 1 },
models: [
// Single target — openai/gpt-4o-mini — so modelStr = CURR_MODEL
{ id: "rh-openai", kind: "model", providerId: "openai", model: "gpt-4o-mini" },
],
});
// Seed the prior model usage so getLastSessionModel returns PREV_MODEL.
// universalHandoffConfig.enabled = true by DEFAULT_UNIVERSAL_HANDOFF_CONFIG.
recordSessionModelUsage(SESSION_ID, COMBO_NAME, PREV_MODEL, "gemini", undefined);
installHandoffAwareFetch();
const r = await handleChat(relayRequest(/* withSessionId */ true));
assert.equal(r.status, 200, "main request must succeed");
// Wait for the setImmediate + generateUniversalHandoffAsync to complete and
// write the DB record. Poll for up to 2 s — typically resolves in <100 ms.
const handoff = await waitFor(
() => getHandoff(SESSION_ID, COMBO_NAME),
2000
);
assert.ok(
handoff !== null,
"universal handoff record must be written to DB when a model switch is detected"
);
assert.ok(
typeof handoff!.summary === "string" && handoff!.summary.length > 0,
`handoff.summary must be non-empty; got: ${JSON.stringify(handoff!.summary)}`
);
assert.equal(
handoff!.comboName,
COMBO_NAME,
"handoff must be keyed to the correct combo"
);
assert.equal(
handoff!.sessionId,
SESSION_ID,
"handoff must be keyed to the correct session"
);
// Extra dispatch observable: main (index 0) + summary (index ≥ 1).
assert.ok(
h.calls.length >= 2,
`expected ≥2 upstream dispatches (main + summary); got ${h.calls.length}: ${JSON.stringify(h.calls.map((c) => ({ i: c.index, p: c.provider, m: c.model })))}`
);
});
// ── Test 2: control — no model switch, handoff must NOT fire ──────────────────
//
// DO NOT seed a prior model. getLastSessionModel returns null → prevModel is
// null → the `if (prevModel && prevModel !== modelStr)` branch is skipped →
// no handoff. The assertion FAILS if the code incorrectly fires a handoff.
test("context-relay universal handoff: does NOT fire when no prior model is recorded (no switch)", async () => {
await seedConnection("openai", { apiKey: "sk-openai-noswitch" });
await combosDb.createCombo({
name: COMBO_NAME,
strategy: "context-relay",
config: { maxRetries: 0, retryDelayMs: 0, stickyRoundRobinLimit: 1 },
models: [{ id: "ns-openai", kind: "model", providerId: "openai", model: "gpt-4o-mini" }],
});
// No recordSessionModelUsage call → getLastSessionModel returns null → no switch.
installHandoffAwareFetch();
const r = await handleChat(relayRequest(true));
assert.equal(r.status, 200);
// Give setImmediate time to fire if the bug were present.
await new Promise((res) => setTimeout(res, 250));
const handoff = getHandoff(SESSION_ID, COMBO_NAME);
assert.equal(
handoff,
null,
"handoff must NOT be written when no prior model exists (prevModel is null)"
);
// Only the main request — no extra summary dispatch.
assert.equal(
h.calls.length,
1,
`expected exactly 1 upstream dispatch (main only); got ${h.calls.length}`
);
});
// ── Test 3: control — no session ID, handoff must NOT fire ────────────────────
//
// Session ID gate: `relayOptions?.sessionId` must be truthy.
// Without the x-omniroute-session-id header, sessionId = null → block is skipped.
test("context-relay universal handoff: does NOT fire when x-omniroute-session-id header is absent", async () => {
await seedConnection("openai", { apiKey: "sk-openai-nosid" });
await combosDb.createCombo({
name: COMBO_NAME,
strategy: "context-relay",
config: { maxRetries: 0, retryDelayMs: 0, stickyRoundRobinLimit: 1 },
models: [{ id: "sid-openai", kind: "model", providerId: "openai", model: "gpt-4o-mini" }],
});
// Seed prior model — but without the header the session block won't fire.
recordSessionModelUsage(SESSION_ID, COMBO_NAME, PREV_MODEL, "gemini", undefined);
installHandoffAwareFetch();
// Send WITHOUT the session header.
const r = await handleChat(relayRequest(/* withSessionId */ false));
assert.equal(r.status, 200);
await new Promise((res) => setTimeout(res, 250));
const handoff = getHandoff(SESSION_ID, COMBO_NAME);
assert.equal(
handoff,
null,
"handoff must NOT be written when x-omniroute-session-id header is absent (sessionId gate)"
);
assert.equal(
h.calls.length,
1,
`expected exactly 1 upstream dispatch (main only, no summary); got ${h.calls.length}`
);
});

View File

@@ -11,10 +11,10 @@
// gemini-2.5-flash → contextWindow 1 048 576
//
// context-relay: does NOT appear in the sorting if-else chain of combo.ts; targets are
// dispatched in combo-definition order. The only extra behaviour is a codex-provider
// handoff triggered by (strategy=context-relay && provider="codex" && sessionId in
// relayOptions), which cannot be exercised in-process with openai/gemini providers.
// That handoff path is covered by TODO(phase-2) below.
// dispatched in combo-definition order. Two extra behaviours exist:
// 1. Universal handoff (provider-agnostic) — covered by context-relay-handoff.test.ts.
// 2. Codex-specific handoff — requires provider="codex" (see context-relay-handoff.test.ts
// for why the codex block is documented but not covered here).
import test from "node:test";
import assert from "node:assert/strict";
import { createComboRoutingHarness } from "../_comboRoutingHarness.ts";
@@ -111,12 +111,8 @@ test("context-optimized: largest-context model (gemini-2.5-flash 1048576) dispat
// The only extra behaviour is a codex handoff at line 2144:
// strategy === "context-relay" && relayOptions?.sessionId && relayConfig &&
// relayConfig.handoffProviders.includes(provider) && provider === "codex"
// This branch requires provider === "codex" and a live relayOptions.sessionId, which
// cannot be injected in-process via the standard combo test harness.
// TODO(phase-2): trigger the codex handoff path live on VPS or via a dedicated codex-
// provider connection stub that satisfies the handoff condition.
//
// Assertion: context-relay preserves combo-definition order (first model dispatched first).
// Universal handoff + codex block coverage: see context-relay-handoff.test.ts.
test("context-relay: preserves combo-definition order (openai dispatched before gemini)", async () => {
await seedConnection("openai", { apiKey: "sk-openai-relay" });
await seedConnection("gemini", { apiKey: "sk-gemini-relay" });

View File

@@ -0,0 +1,202 @@
// tests/integration/combo-matrix/quota-share.test.ts
//
// E2E matrix test for the INTERNAL quota-share routing strategy (DRR / deficit
// round-robin over auto-minted qtSd/ pool combos).
//
// Drive path: handleChat → chatCore → handleComboChat (strategy "quota-share")
// → selectQuotaShareTarget (DRR + P2C + bucket-gating) → executor dispatch.
//
// Two tests:
// 1. DRR fairness — 2 equal-weight connections, 6 requests; both must be
// selected and balanced, proving DRR ran end-to-end (not a single-winner
// or definition-order dispatch).
// 2. Saturation deprioritization — one connection marked saturated via
// recordUsage (5h window); a single request must dispatch to the clean
// connection first, proving the bucket-gating gate runs through the real
// wire.
//
// Drive path chosen: Option A — direct `combosDb.createCombo({ strategy:
// "quota-share", ... })`. The `handleComboChat` dispatch at combo.ts:1573
// branches on `strategy === "quota-share"` verbatim; no qtSd/ pool plumbing is
// required to reach `selectQuotaShareTarget`.
//
// The preferred target is always the SECOND one in the combo definition so that
// a pass proves the selector actually reordered targets (same discipline as the
// 17-strategy quota-aware.test.ts matrix).
import test from "node:test";
import assert from "node:assert/strict";
import { createComboRoutingHarness } from "../_comboRoutingHarness.ts";
const h = await createComboRoutingHarness("combo-quota-share");
const { BaseExecutor, combosDb, handleChat, buildRequest, seedConnection, resetStorage } = h;
// DRR + bucket state seams — import AFTER harness initialises the DB so the
// module-level in-process singletons are already live.
const { _clearDrrStateForTest } = await import(
"../../../open-sse/services/combo/quotaShareStrategy.ts"
);
const { recordUsage, _clearBucketsForTest } = await import(
"../../../src/lib/quota/accountBuckets.ts"
);
// ── Helpers ───────────────────────────────────────────────────────────────────
function body(model: string, suffix = "") {
return {
model,
stream: false,
messages: [{ role: "user", content: `quota-share route${suffix}` }],
};
}
// ── Lifecycle ─────────────────────────────────────────────────────────────────
test.beforeEach(async () => {
BaseExecutor.RETRY_CONFIG.delayMs = 0;
_clearDrrStateForTest();
_clearBucketsForTest();
await resetStorage();
});
test.afterEach(async () => {
BaseExecutor.RETRY_CONFIG.delayMs = h.originalRetryDelayMs;
_clearDrrStateForTest();
_clearBucketsForTest();
await resetStorage();
});
test.after(async () => {
_clearDrrStateForTest();
_clearBucketsForTest();
await h.cleanup();
});
// ── Strategy: quota-share — DRR fairness ─────────────────────────────────────
//
// Mechanism: DRR (deficit round robin, quantum = weight / totalWeight).
//
// Engineered state:
// openai (1st in definition) — equal weight 100
// gemini (2nd in definition) — equal weight 100
//
// With 2 equal-weight targets DRR alternates: openai, gemini, openai, gemini, …
// Over 6 requests both must be selected at least 2 times (proven to be exactly
// 3 each by the deterministic math, but asserting >= 2 avoids brittleness if
// P2C ever nudges a tie differently). The assertion fails if the pipeline
// pin-picks the first candidate on every call — i.e. if DRR is bypassed.
test("DRR fairness: 2 equal-weight connections alternate across 6 requests through real pipeline", async () => {
const openaiConn = await seedConnection("openai", { apiKey: "sk-openai-qs-drr-1" });
const geminiConn = await seedConnection("gemini", { apiKey: "sk-gemini-qs-drr-1" });
await combosDb.createCombo({
name: "m-qs-drr",
strategy: "quota-share",
config: { maxRetries: 0, retryDelayMs: 0, stickyRoundRobinLimit: 1 },
models: [
// openai FIRST in definition
{
id: "qs-drr-openai",
kind: "model",
providerId: "openai",
model: "gpt-4o-mini",
connectionId: openaiConn.id,
},
// gemini SECOND in definition — DRR must pick it on alternate rounds
{
id: "qs-drr-gemini",
kind: "model",
providerId: "gemini",
model: "gemini-2.5-flash",
connectionId: geminiConn.id,
},
],
});
h.installRecordingFetch();
// Use a unique message suffix per request so session stickiness (message-hash
// based) does not pin every call to the first-round winner. In real traffic
// each conversation has distinct content; the DRR distributes across them.
for (let i = 0; i < 6; i++) {
const r = await handleChat(buildRequest({ body: body("m-qs-drr", ` #${i}`) }));
assert.equal(r.status, 200, `request ${i + 1} must succeed`);
}
const seen = h.providersSeen();
assert.equal(seen.length, 6, "all 6 requests must reach an upstream provider");
const openaiCount = seen.filter((p) => p === "openai").length;
const geminiCount = seen.filter((p) => p === "gemini").length;
assert.ok(
openaiCount >= 2,
`DRR fairness: openai must be selected at least 2 times out of 6; got ${openaiCount}. ` +
`Full sequence: [${seen.join(", ")}]. If openai was never selected, DRR was bypassed.`
);
assert.ok(
geminiCount >= 2,
`DRR fairness: gemini must be selected at least 2 times out of 6; got ${geminiCount}. ` +
`Full sequence: [${seen.join(", ")}]. If gemini was never selected, DRR was bypassed.`
);
});
// ── Strategy: quota-share — saturation deprioritization ──────────────────────
//
// Mechanism: filterEligibleBySaturation (isBucketSaturated, 5h window).
//
// Engineered state:
// openai (1st in definition) — 5h bucket seeded at 100 % usage via
// recordUsage → isBucketSaturated returns true
// → filterEligibleBySaturation demotes it
// gemini (2nd in definition) — no bucket recorded → eligible (clean)
//
// Expected: gemini dispatched first despite being second in the combo
// definition. Proves the bucket-gating runs through the real pipeline wire
// (not merely unit-tested in isolation).
test("saturation deprioritization: saturated connection demoted — clean second target dispatched first", async () => {
const openaiConn = await seedConnection("openai", { apiKey: "sk-openai-qs-sat-1" });
const geminiConn = await seedConnection("gemini", { apiKey: "sk-gemini-qs-sat-1" });
// Seed openai connection as saturated (5h window, 100 % usage, reset in 1 h).
// recordUsage writes the in-process _buckets store; isBucketSaturated reads it.
// The resetAt must be in the FUTURE so the lazy-reset guard keeps the entry live.
const futureResetAt = new Date(Date.now() + 3_600_000).toISOString();
recordUsage(openaiConn.id, "5h", 100, futureResetAt);
await combosDb.createCombo({
name: "m-qs-sat",
strategy: "quota-share",
config: { maxRetries: 0, retryDelayMs: 0, stickyRoundRobinLimit: 1 },
models: [
// openai FIRST in definition — must be demoted (5h bucket saturated)
{
id: "qs-sat-openai",
kind: "model",
providerId: "openai",
model: "gpt-4o-mini",
connectionId: openaiConn.id,
},
// gemini SECOND — must win (no saturation recorded → eligible)
{
id: "qs-sat-gemini",
kind: "model",
providerId: "gemini",
model: "gemini-2.5-flash",
connectionId: geminiConn.id,
},
],
});
h.installRecordingFetch();
const r = await handleChat(buildRequest({ body: body("m-qs-sat") }));
assert.equal(r.status, 200, "request must succeed via clean gemini target");
const first = h.providersSeen()[0];
assert.equal(
first,
"gemini",
`saturation deprioritization: openai 5h bucket is at 100 % (saturated), so ` +
`filterEligibleBySaturation must demote it and dispatch gemini first despite it ` +
`being second in the combo definition. Got: ${first}`
);
});

View File

@@ -0,0 +1,64 @@
import test from "node:test";
import assert from "node:assert/strict";
// #4546 — In containers/headless, the system trust store can't be written
// (no sudo / read-only store / no interactive auth), so the cert install
// throws and used to abort the whole Agent Bridge start. These tests pin the
// graceful-fallback contract: a structured result that distinguishes a
// user-canceled auth from an environment failure, plus a platform-specific
// manual-install guide so the operator can trust the MITM root CA themselves.
const { classifyCertInstallError, buildCertManualGuide, installCertResult } = await import(
"../../src/mitm/cert/install.ts"
);
const DOWNLOAD_URL = "/api/tools/agent-bridge/cert/download";
test("classifyCertInstallError → 'canceled' only when the message says canceled", () => {
assert.equal(classifyCertInstallError("User canceled authorization"), "canceled");
assert.equal(classifyCertInstallError("Operation was canceled by the user"), "canceled");
});
test("classifyCertInstallError → 'environment' for trust-store / sudo failures", () => {
assert.equal(classifyCertInstallError("Certificate install failed"), "environment");
assert.equal(classifyCertInstallError("sudo: no tty present and no askpass program specified"), "environment");
assert.equal(classifyCertInstallError("Certificate file not found: /x/server.crt"), "environment");
});
test("buildCertManualGuide(linux) → update-ca-certificates steps + download url + cert path", () => {
const guide = buildCertManualGuide("/data/mitm/server.crt", "linux");
assert.equal(guide.platform, "linux");
assert.equal(guide.certPath, "/data/mitm/server.crt");
assert.equal(guide.downloadUrl, DOWNLOAD_URL);
assert.ok(Array.isArray(guide.steps) && guide.steps.length > 0);
const joined = guide.steps.join("\n");
assert.ok(joined.includes("update-ca-"), "should mention the distro CA refresh command");
assert.ok(joined.includes("/data/mitm/server.crt"), "should reference the cert path");
});
test("buildCertManualGuide(darwin) → security add-trusted-cert", () => {
const guide = buildCertManualGuide("/d/server.crt", "darwin");
assert.equal(guide.platform, "darwin");
assert.ok(guide.steps.join("\n").includes("add-trusted-cert"));
});
test("buildCertManualGuide(win32) → certutil -addstore Root", () => {
const guide = buildCertManualGuide("C:/d/server.crt", "win32");
assert.equal(guide.platform, "win32");
assert.ok(guide.steps.join("\n").toLowerCase().includes("certutil"));
});
test("installCertResult → environment skip (not a throw) when install is impossible", async () => {
// A non-existent cert path makes installCert() throw before any privileged
// command runs — deterministic, no sudo. The wrapper must convert that into a
// structured skippable result with a manual guide, never a thrown error.
const result = await installCertResult("", "/nonexistent/omniroute-4546-server.crt");
assert.equal(result.installed, false);
assert.equal(result.skipped, true);
assert.equal(result.reason, "environment");
assert.ok(result.manualGuide, "environment skip must carry a manual guide");
assert.equal(result.manualGuide?.downloadUrl, DOWNLOAD_URL);
// The message must be a safe string (no stack trace leaked).
assert.equal(typeof result.message, "string");
assert.ok(!String(result.message).includes("\n at "), "must not leak a stack trace");
});

View File

@@ -12,9 +12,14 @@
// which is semantically a no-op and does not signal "thinking complete" to clients
// such as Claude Code).
//
// The fix emits a `content: "</think>"` chunk on close — matching the convention
// already used throughout OmniRoute (see openai-responses.ts / responsesTransformer.ts
// which split on `</think>` to separate reasoning from final content).
// PR #4633 added an immediate `content: "</think>"` chunk on close.
//
// PR #5123 refined this to DEFERRED emission: instead of emitting at content_block_stop
// (which caused the marker to leak before tool_calls in tool-use streams), the marker is
// now queued and flushed either:
// • at the first text_delta that follows (preserving #4633 for Claude Code / Cursor), or
// • at message_delta finish when there are no tool_calls (pure thinking-only responses).
// This test covers the message_delta flush path (thinking block with no subsequent text).
import test from "node:test";
import assert from "node:assert/strict";
@@ -33,7 +38,7 @@ function newState() {
};
}
test("claudeToOpenAIResponse emits </think> close marker on thinking content_block_stop", () => {
test("claudeToOpenAIResponse emits </think> close marker on message finish (deferred from content_block_stop)", () => {
const state = newState();
// Open thinking block.
@@ -56,28 +61,44 @@ test("claudeToOpenAIResponse emits </think> close marker on thinking content_blo
state
);
// Close the thinking block.
// Close the thinking block — marker is now DEFERRED (not emitted here).
const closeChunks = claudeToOpenAIResponse(
{ type: "content_block_stop", index: 0 },
state
);
assert.ok(Array.isArray(closeChunks), "stop event must return an array of chunks");
assert.ok(
closeChunks.length >= 1,
"stop event for thinking block must emit at least one close-marker chunk"
// content_block_stop for a thinking block no longer emits </think> immediately
// (the marker is deferred to prevent leaking before tool_calls — see #5123).
const immediateClose = Array.isArray(closeChunks) ? closeChunks : [];
const hasImmediateMarker = immediateClose.some(
(chunk) => chunk?.choices?.[0]?.delta?.content === "</think>"
);
assert.equal(
hasImmediateMarker,
false,
"content_block_stop must NOT emit </think> immediately (deferred — see #5123)"
);
const hasCloseMarker = closeChunks.some(
// After close, the thinking-block flag is cleared and the pending marker is queued.
assert.equal(state.inThinkingBlock, false);
assert.equal(state.pendingThinkClose, true, "pendingThinkClose must be set after thinking block stop");
// message_delta with stop_reason=end_turn and no tool_calls → marker must be flushed here.
const finishChunks = claudeToOpenAIResponse(
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 5 } },
state
);
const arr = Array.isArray(finishChunks) ? finishChunks : [];
const hasCloseMarker = arr.some(
(chunk) => chunk?.choices?.[0]?.delta?.content === "</think>"
);
assert.ok(
hasCloseMarker,
`expected a chunk with delta.content === "</think>"; got ${JSON.stringify(closeChunks)}`
`expected a chunk with delta.content === "</think>" in message_delta result; got ${JSON.stringify(arr)}`
);
// After close, state flag must be cleared so subsequent thinking blocks are tracked correctly.
assert.equal(state.inThinkingBlock, false);
// pendingThinkClose must be cleared after flush.
assert.equal(state.pendingThinkClose, false, "pendingThinkClose must be cleared after flush");
});
test("claudeToOpenAIResponse does not emit </think> on stop of non-thinking blocks", () => {

View File

@@ -0,0 +1,34 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { AntigravityExecutor } from "../../open-sse/executors/antigravity.ts";
type ChatCompletionPayload = {
choices: Array<{
message: { content: string };
finish_reason: string;
}>;
};
test("AntigravityExecutor.collectStreamToResponse normalizes prohibited content finish reasons", async () => {
const executor = new AntigravityExecutor();
const response = new Response(
'data: {"response":{"candidates":[{"content":{"parts":[{"text":"partial text"}]},"finishReason":"PROHIBITED_CONTENT"}]}}\n\n',
{
status: 200,
headers: { "Content-Type": "text/event-stream" },
}
);
const result = await executor.collectStreamToResponse(
response,
"gemini-2.5-flash",
"https://example.com",
{ Authorization: "Bearer ag-token" },
{ request: {} }
);
const payload = (await result.response.json()) as ChatCompletionPayload;
assert.equal(payload.choices[0].message.content, "partial text");
assert.equal(payload.choices[0].finish_reason, "content_filter");
});

View File

@@ -0,0 +1,83 @@
// Regression guard for the Antigravity OAuth login hang on Google's consent page.
//
// The embedded Antigravity client is a Google "Desktop/native" OAuth client.
// Sending a PKCE code_challenge AND the `openid` scope pushed Google into the
// `signin/oauth/firstparty/nativeapp` consent flow, which hung and never redirected
// back (operator report 2026-06-27). The working 9router flow uses a plain
// authorization_code grant (client_secret, no code_challenge) and does NOT request
// `openid`. This test pins our antigravity (and the `agy` alias) to that shape.
//
// Flip-proof: set flowType back to "authorization_code_pkce" → generateAuthData emits
// code_challenge → first assertion fails. Re-add "openid" → scope assertion fails.
import test from "node:test";
import assert from "node:assert/strict";
import { generateAuthData } from "../../src/lib/oauth/providers.ts";
import PROVIDERS from "../../src/lib/oauth/providers/index.ts";
const REDIRECT = "http://127.0.0.1:20128/callback";
for (const providerId of ["antigravity", "agy"]) {
test(`${providerId}: no PKCE + no openid in the auth URL (matches working 9router flow)`, () => {
assert.equal(
PROVIDERS[providerId].flowType,
"authorization_code",
`${providerId} must use a plain authorization_code grant (no PKCE) for the Google native client`
);
const authData = generateAuthData(providerId, REDIRECT);
assert.ok(authData.authUrl, `${providerId} must produce an auth URL`);
const url = new URL(authData.authUrl);
assert.equal(url.origin, "https://accounts.google.com");
// No PKCE challenge — its presence triggers the hanging nativeapp consent.
assert.equal(
url.searchParams.get("code_challenge"),
null,
`${providerId} auth URL must NOT carry a PKCE code_challenge`
);
assert.equal(url.searchParams.get("code_challenge_method"), null);
// No openid scope — only the Cloud Code / userinfo scopes 9router requests.
const scopes = (url.searchParams.get("scope") || "").split(" ");
assert.ok(!scopes.includes("openid"), `${providerId} must not request the openid scope`);
assert.ok(
scopes.includes("https://www.googleapis.com/auth/cloud-platform"),
`${providerId} must still request the cloud-platform scope`
);
});
}
test("antigravity.exchangeToken never forwards code_verifier (no PKCE → no invalid_grant 500)", async () => {
const origFetch = globalThis.fetch;
let sentBody = "";
globalThis.fetch = (async (_url: unknown, init: { body?: unknown } = {}) => {
sentBody = String(init.body ?? "");
return new Response(
JSON.stringify({ access_token: "t", refresh_token: "r", expires_in: 3600 }),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
}) as typeof fetch;
try {
// Pass a codeVerifier (as the modal does — generateAuthData always mints one).
// It MUST be ignored: the authorize URL had no code_challenge, so forwarding a
// code_verifier makes Google reject the exchange (invalid_grant → 500).
await PROVIDERS.antigravity.exchangeToken(
{
clientId: "cid",
clientSecret: "sec",
tokenUrl: "https://oauth2.googleapis.com/token",
},
"the-code",
"http://127.0.0.1:20128/callback",
"should-be-ignored-verifier"
);
} finally {
globalThis.fetch = origFetch;
}
const params = new URLSearchParams(sentBody);
assert.equal(params.get("code_verifier"), null, "must NOT forward code_verifier (no PKCE)");
assert.equal(params.get("client_secret"), "sec", "must authenticate via client_secret");
assert.equal(params.get("grant_type"), "authorization_code");
});

View File

@@ -0,0 +1,104 @@
// Regression guard for the Antigravity OAuth login hang.
//
// The dashboard login "just spun forever" because postExchange `await`ed the
// onboardUser retry loop (up to 10×5s, each fetch un-timed) inline, so a slow/
// unreachable Antigravity upstream blocked the /exchange response indefinitely.
//
// Fix: onboarding is fire-and-forget (matches the 9router web flow) and every
// blocking call is AbortSignal.timeout-bounded. This test proves postExchange
// returns promptly regardless of onboarding, and never hangs when an upstream
// stalls.
//
// Flip-proof: revert onboarding to an inline `await` loop and test 1 hangs on the
// onboard gate → times out → fails. Drop the AbortSignal.timeout and test 2
// hangs → fails.
import test from "node:test";
import assert from "node:assert/strict";
import { antigravity } from "../../src/lib/oauth/providers/antigravity.ts";
const originalFetch = globalThis.fetch;
function jsonRes(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
}
// A fetch that rejects when its AbortSignal fires, and otherwise never resolves.
// Mirrors real fetch: an already-aborted signal rejects immediately (so a shared
// deadline reused across fallback endpoints fails fast after the first abort).
function stalledFetch(init?: { signal?: AbortSignal }): Promise<Response> {
return new Promise((_resolve, reject) => {
const abortErr = () => new DOMException("The operation was aborted.", "AbortError");
const signal = init?.signal;
if (signal?.aborted) {
reject(abortErr());
return;
}
signal?.addEventListener("abort", () => reject(abortErr()));
});
}
test.afterEach(() => {
globalThis.fetch = originalFetch;
});
test("postExchange returns before onboarding finishes (fire-and-forget — never blocks login)", async () => {
// The onboard call is gated: it does not resolve until we release it AFTER
// postExchange has already returned. With the old inline `await` loop,
// postExchange would block on this gate forever → the test times out. With the
// fire-and-forget fix it returns immediately.
let releaseOnboard: () => void = () => {};
const onboardGate = new Promise<void>((r) => {
releaseOnboard = r;
});
let onboardStarted = false;
globalThis.fetch = (async (url: unknown) => {
const u = String(url);
if (u.includes("userinfo")) return jsonRes({ email: "user@example.com" });
if (u.includes("loadCodeAssist")) {
return jsonRes({
cloudaicompanionProject: "proj-123",
allowedTiers: [{ id: "legacy-tier", isDefault: true }],
});
}
if (u.includes("onboardUser")) {
onboardStarted = true;
await onboardGate;
return jsonRes({ done: true });
}
return jsonRes({});
}) as typeof fetch;
const start = Date.now();
const result = await antigravity.postExchange({ access_token: "tok" } as never);
const elapsed = Date.now() - start;
assert.ok(elapsed < 3000, `postExchange must not block on onboarding; took ${elapsed}ms`);
assert.equal(result.projectId, "proj-123", "projectId still resolved from loadCodeAssist");
// Let the backgrounded onboarding complete cleanly (no lingering work).
releaseOnboard();
await new Promise((r) => setTimeout(r, 50));
assert.ok(onboardStarted, "onboarding still runs — in the background, after the response");
});
test("postExchange stays timeout-bounded when loadCodeAssist/userinfo stall (no infinite hang)", async () => {
globalThis.fetch = (async (url: unknown, init?: { signal?: AbortSignal }) => {
const u = String(url);
if (u.includes("userinfo") || u.includes("loadCodeAssist")) return stalledFetch(init);
return jsonRes({});
}) as typeof fetch;
const start = Date.now();
const result = await antigravity.postExchange({ access_token: "tok" } as never);
const elapsed = Date.now() - start;
// userInfo + loadCodeAssist are AbortSignal.timeout(8s)-bounded (one shared
// deadline each), so the worst case is ~16s — never an infinite hang.
assert.ok(elapsed < 22000, `postExchange must be timeout-bounded; took ${elapsed}ms`);
assert.equal(result.projectId, "", "no project when loadCodeAssist times out");
});

View File

@@ -0,0 +1,75 @@
/**
* Issue #3850 — Antigravity refresh nulls the stored refresh_token.
*
* Google's OAuth token endpoint normally OMITS `refresh_token` on a refresh
* (its refresh tokens are non-rotating), and occasionally returns it as an
* EMPTY STRING. The canonical `refreshGoogleToken` preserves the existing token
* via `tokens.refresh_token || refreshToken` (treats "" as absent), but the
* Antigravity executor's `refreshCredentials` used
* `typeof tokens.refresh_token === "string" ? tokens.refresh_token : credentials.refreshToken`
* — and `typeof "" === "string"` is true, so an empty-string response
* OVERWROTE the good token with "", effectively nulling it on first refresh.
*
* This regression guard asserts the executor preserves the existing refresh
* token when the upstream returns it empty or omits it.
*/
import { test } from "node:test";
import assert from "node:assert/strict";
import { AntigravityExecutor } from "../../open-sse/executors/antigravity.ts";
const OLD_REFRESH = "1//old-non-rotating-refresh-token";
async function withStubbedFetch<T>(
jsonBody: Record<string, unknown>,
fn: () => Promise<T>
): Promise<T> {
const original = globalThis.fetch;
globalThis.fetch = (async () =>
new Response(JSON.stringify(jsonBody), {
status: 200,
headers: { "Content-Type": "application/json" },
})) as typeof fetch;
try {
return await fn();
} finally {
globalThis.fetch = original;
}
}
test("#3850 empty-string refresh_token from Google preserves the existing token", async () => {
const executor = new AntigravityExecutor();
const refreshed = await withStubbedFetch(
{ access_token: "new-access", refresh_token: "", expires_in: 3600 },
() => executor.refreshCredentials({ refreshToken: OLD_REFRESH, accessToken: "stale" })
);
assert.ok(refreshed, "refreshCredentials should return credentials, not null");
assert.equal(refreshed.accessToken, "new-access");
assert.equal(
refreshed.refreshToken,
OLD_REFRESH,
"empty-string refresh_token must NOT overwrite the stored token"
);
});
test("#3850 omitted refresh_token from Google preserves the existing token", async () => {
const executor = new AntigravityExecutor();
const refreshed = await withStubbedFetch({ access_token: "new-access", expires_in: 3600 }, () =>
executor.refreshCredentials({ refreshToken: OLD_REFRESH, accessToken: "stale" })
);
assert.ok(refreshed);
assert.equal(refreshed.refreshToken, OLD_REFRESH);
});
test("#3850 a real rotated refresh_token still replaces the stored token", async () => {
const executor = new AntigravityExecutor();
const refreshed = await withStubbedFetch(
{ access_token: "new-access", refresh_token: "1//brand-new-token", expires_in: 3600 },
() => executor.refreshCredentials({ refreshToken: OLD_REFRESH, accessToken: "stale" })
);
assert.ok(refreshed);
assert.equal(refreshed.refreshToken, "1//brand-new-token");
});

View File

@@ -0,0 +1,109 @@
/**
* TDD regression guard for #5083 — Bug 3:
* COMBO_002 validation errors only surface the generic
* "One or more combo fields are invalid" message; the field-level reason
* is buried inside error.details.issues.details[*].
*
* Fix: extract the first issue from error.details and add
* error.details.firstField / error.details.firstMessage to the response body.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { buildComboErrorBody } from "@/lib/api/comboErrorResponse";
/**
* Simulate what the PUT /api/combos/[id] route passes as `details`
* for a COMBO_002 response after the fix.
* The fix extracts the first Zod issue from validation.error.details
* and adds firstField / firstMessage to the payload.
*/
describe("COMBO_002 response — firstField / firstMessage surfacing (#5083 Bug 3)", () => {
it("details payload exposes firstField when provided", () => {
// Simulate the fixed route calling comboErrorResponse with firstField/firstMessage
const details = {
issues: { message: "Invalid request", details: [{ field: "name", message: "Required" }] },
firstField: "name",
firstMessage: "Required",
};
const body = buildComboErrorBody("COMBO_002", details);
assert.equal(body.error.code, "COMBO_002");
assert.equal(body.error.details.firstField, "name");
assert.equal(body.error.details.firstMessage, "Required");
});
it("details payload exposes firstField for nested path (e.g. models.0.id)", () => {
const details = {
issues: {
message: "Invalid request",
details: [{ field: "models.0.id", message: "String must contain at least 1 character(s)" }],
},
firstField: "models.0.id",
firstMessage: "String must contain at least 1 character(s)",
};
const body = buildComboErrorBody("COMBO_002", details);
assert.equal(body.error.details.firstField, "models.0.id");
assert.equal(body.error.details.firstMessage, "String must contain at least 1 character(s)");
});
it("details payload has firstField=null when no issues are present", () => {
// Edge case: empty issues list
const details = {
issues: { message: "Invalid request", details: [] },
firstField: null,
firstMessage: null,
};
const body = buildComboErrorBody("COMBO_002", details);
assert.equal(body.error.details.firstField, null);
assert.equal(body.error.details.firstMessage, null);
});
it("the generic message key is still present (backward compat)", () => {
const details = {
issues: { message: "Invalid request", details: [{ field: "strategy", message: "Invalid enum value" }] },
firstField: "strategy",
firstMessage: "Invalid enum value",
};
const body = buildComboErrorBody("COMBO_002", details);
// The top-level error.message is from the error code catalog, not the issue message
assert.equal(body.error.message, "One or more combo fields are invalid");
});
});
/**
* This helper simulates the logic that the fixed route.ts uses to extract
* firstField/firstMessage from validateBody's error payload.
* It exercises the extraction logic independently of the heavy route harness.
*/
describe("COMBO_002 firstField extraction logic", () => {
function extractFirstField(validationError: {
message: string;
details: Array<{ field: string; message: string }>;
}): { firstField: string | null; firstMessage: string | null } {
const first = validationError.details?.[0] ?? null;
return {
firstField: first?.field ?? null,
firstMessage: first?.message ?? null,
};
}
it("extracts the first field from a non-empty details array", () => {
const error = {
message: "Invalid request",
details: [
{ field: "name", message: "Required" },
{ field: "strategy", message: "Invalid enum value" },
],
};
const { firstField, firstMessage } = extractFirstField(error);
assert.equal(firstField, "name");
assert.equal(firstMessage, "Required");
});
it("returns null/null for an empty details array", () => {
const error = { message: "Invalid request", details: [] };
const { firstField, firstMessage } = extractFirstField(error);
assert.equal(firstField, null);
assert.equal(firstMessage, null);
});
});

View File

@@ -49,6 +49,13 @@ test("#3061 opencode-zen no-auth: first selection returns synthetic noauth (happ
assert.equal((creds as { connectionId?: string }).connectionId, "noauth");
});
test("#3061 mimocode no-auth: first selection returns synthetic noauth (happy path preserved)", async () => {
const creds = await getProviderCredentials("mimocode", null, null, "mimo-auto");
assert.ok(creds, "mimocode must resolve to synthetic no-auth credentials on first selection");
assert.equal((creds as { connectionId?: string }).connectionId, "noauth");
assert.equal((creds as { apiKey?: unknown }).apiKey, null);
});
// ── The fix: once "noauth" is excluded, selection MUST stop (return null) ──
test("#3061 opencode no-auth: excluding 'noauth' returns null (breaks the fallback loop)", async () => {
@@ -73,3 +80,14 @@ test("#3061 opencode-zen no-auth: excluding 'noauth' returns null (breaks the fa
"excluded synthetic noauth must not be re-selected for the opencode-zen keyless path"
);
});
test("#3061 mimocode no-auth: excluding 'noauth' returns null (breaks the fallback loop)", async () => {
const creds = await getProviderCredentials("mimocode", null, null, "mimo-auto", {
excludeConnectionIds: ["noauth"],
});
assert.equal(
creds,
null,
"excluded synthetic noauth must not be re-selected for the mimocode keyless path"
);
});

View File

@@ -0,0 +1,97 @@
/**
* TDD regression guard for #5083 — Bug 2:
* GET /api/system/version is blocked from LAN/remote hosts because the entire
* path is in LOCAL_ONLY_API_PREFIXES for all methods. Only POST spawns child
* processes (git/npm/pm2); GET only reads package.json + npm registry.
*
* Fix: isLocalOnlyPath(path, method) returns false for safe HTTP methods
* when the path exactly matches LOCAL_ONLY_API_GET_EXEMPTIONS.
*
* Security invariant: POST /api/system/version MUST remain local-only.
* All OTHER local-only prefixes (/api/mcp/, /api/services/, etc.) must
* remain local-only for GET too (exemption is exact-match only).
*/
import { test, describe } from "node:test";
import assert from "node:assert/strict";
import {
isLocalOnlyPath,
LOCAL_ONLY_API_GET_EXEMPTIONS,
} from "../../../src/server/authz/routeGuard.ts";
describe("isLocalOnlyPath — GET exemption for /api/system/version (#5083)", () => {
// ── EXEMPTION APPLIES ──────────────────────────────────────────────────────
test("GET /api/system/version is NOT local-only (no child process spawn)", () => {
assert.equal(isLocalOnlyPath("/api/system/version", "GET"), false);
});
test("HEAD /api/system/version is NOT local-only (read-only method)", () => {
assert.equal(isLocalOnlyPath("/api/system/version", "HEAD"), false);
});
test("OPTIONS /api/system/version is NOT local-only (CORS preflight)", () => {
assert.equal(isLocalOnlyPath("/api/system/version", "OPTIONS"), false);
});
// ── SPAWN-CAPABLE METHODS REMAIN BLOCKED ──────────────────────────────────
test("POST /api/system/version STAYS local-only (spawns git/npm/pm2)", () => {
assert.equal(isLocalOnlyPath("/api/system/version", "POST"), true);
});
test("PUT /api/system/version stays local-only", () => {
assert.equal(isLocalOnlyPath("/api/system/version", "PUT"), true);
});
test("PATCH /api/system/version stays local-only", () => {
assert.equal(isLocalOnlyPath("/api/system/version", "PATCH"), true);
});
test("DELETE /api/system/version stays local-only", () => {
assert.equal(isLocalOnlyPath("/api/system/version", "DELETE"), true);
});
// ── SAFE DEFAULT: no method arg → still blocked ─────────────────────────
test("isLocalOnlyPath('/api/system/version') with NO method arg returns true (safe default)", () => {
// Scripts like check-route-guard-membership call without a method; safe default
// must be true so spawn-capable paths are never accidentally unblocked.
assert.equal(isLocalOnlyPath("/api/system/version"), true);
});
// ── EXEMPTION IS EXACT-MATCH ONLY ─────────────────────────────────────────
test("GET /api/system/version/extra is NOT exempted (prefix would be too broad)", () => {
// The exemption applies only to the exact path — sub-paths are NOT exempted.
assert.equal(isLocalOnlyPath("/api/system/version/extra", "GET"), true);
});
// ── OTHER LOCAL-ONLY PREFIXES UNAFFECTED BY GET EXEMPTION ─────────────────
test("GET /api/mcp/ still local-only — exemption is NOT applied to /api/mcp/", () => {
assert.equal(isLocalOnlyPath("/api/mcp/sse", "GET"), true);
});
test("GET /api/services/9router/start still local-only", () => {
assert.equal(isLocalOnlyPath("/api/services/9router/start", "GET"), true);
});
test("GET /api/cli-tools/runtime/claude still local-only", () => {
assert.equal(isLocalOnlyPath("/api/cli-tools/runtime/claude", "GET"), true);
});
test("GET /api/db-backups/exportAll still local-only (spawns tar)", () => {
assert.equal(isLocalOnlyPath("/api/db-backups/exportAll", "GET"), true);
});
// ── EXEMPTION SET IS EXPORTED AND CONTAINS EXACTLY /api/system/version ───
test("LOCAL_ONLY_API_GET_EXEMPTIONS contains /api/system/version", () => {
assert.ok(LOCAL_ONLY_API_GET_EXEMPTIONS.has("/api/system/version"));
});
test("LOCAL_ONLY_API_GET_EXEMPTIONS has exactly 1 entry", () => {
assert.equal(LOCAL_ONLY_API_GET_EXEMPTIONS.size, 1);
});
});

View File

@@ -499,7 +499,7 @@ test("chatCore keeps Responses-native Codex payloads in native passthrough mode"
assert.equal(result.success, true);
assert.match(call.url, /\/responses$/);
assert.equal(call.body.input, "ship it");
assert.deepEqual(call.body.input, [{ type: "message", role: "user", content: [{ type: "input_text", text: "ship it" }] }]);
assert.equal(call.body.instructions, "custom system prompt");
assert.equal(call.body.store, false);
assert.deepEqual(call.body.metadata, { source: "codex-client" });

View File

@@ -0,0 +1,177 @@
// Regression test for issue #5123 — </think> leaks as delta.content before tool_calls
// in Claude→OpenAI streaming translation, corrupting OpenAI-compatible clients (Kimi Coding).
//
// Root cause: content_block_stop for a thinking block unconditionally emitted
// createChunk(state, { content: "</think>" }) even when the next block was tool_use.
//
// Fix: defer </think> emission. Only flush when the next event is a text_delta
// (preserving the #4633 behavior for Claude Code / Cursor) or at message finish
// when there are no tool_calls.
import test from "node:test";
import assert from "node:assert/strict";
const { claudeToOpenAIResponse } = await import(
"../../open-sse/translator/response/claude-to-openai.ts"
);
function newState() {
return {
toolCalls: new Map(),
toolNameMap: new Map(),
messageId: "msg_test",
model: "claude-3-7-sonnet",
toolCallIndex: 0,
};
}
function collectChunks(results: ReturnType<typeof claudeToOpenAIResponse>[]): unknown[] {
return results.flatMap((r) => (Array.isArray(r) ? r : r ? [r] : []));
}
// ─── Case (a): thinking block followed by tool_use ───────────────────────────
// This is the regression case. Before the fix, </think> appears as a spurious
// assistant text chunk right before the tool_calls delta, corrupting clients.
test("thinking block followed by tool_use: </think> must NOT appear in any content chunk", () => {
const state = newState();
const allResults: ReturnType<typeof claudeToOpenAIResponse>[] = [];
// message_start
allResults.push(
claudeToOpenAIResponse({ type: "message_start", message: { id: "msg_1", model: "claude-3-7-sonnet" } }, state)
);
// thinking block open
allResults.push(
claudeToOpenAIResponse(
{ type: "content_block_start", index: 0, content_block: { type: "thinking", thinking: "" } },
state
)
);
// thinking delta
allResults.push(
claudeToOpenAIResponse(
{ type: "content_block_delta", index: 0, delta: { type: "thinking_delta", thinking: "Let me think..." } },
state
)
);
// thinking block stop — currently emits </think> unconditionally (the bug)
allResults.push(claudeToOpenAIResponse({ type: "content_block_stop", index: 0 }, state));
// tool_use block open
allResults.push(
claudeToOpenAIResponse(
{
type: "content_block_start",
index: 1,
content_block: { type: "tool_use", id: "toolu_01", name: "get_weather" },
},
state
)
);
// tool arguments delta
allResults.push(
claudeToOpenAIResponse(
{ type: "content_block_delta", index: 1, delta: { type: "input_json_delta", partial_json: '{"city":"Paris"}' } },
state
)
);
// tool_use block stop
allResults.push(claudeToOpenAIResponse({ type: "content_block_stop", index: 1 }, state));
// message_delta with stop_reason=tool_use
allResults.push(
claudeToOpenAIResponse(
{ type: "message_delta", delta: { stop_reason: "tool_use" }, usage: { output_tokens: 42 } },
state
)
);
const chunks = collectChunks(allResults);
const spuriousThinkClose = chunks.filter(
(chunk: any) => chunk?.choices?.[0]?.delta?.content === "</think>"
);
assert.equal(
spuriousThinkClose.length,
0,
`Expected NO chunk with delta.content === "</think>" before tool_calls, but found ${spuriousThinkClose.length}:\n${JSON.stringify(spuriousThinkClose, null, 2)}`
);
});
// ─── Case (b): thinking block followed by text (pure-text response) ──────────
// This is the #4633 happy path. </think> MUST still be emitted so Claude Code /
// Cursor know when the thinking section ends.
test("thinking block followed by text: </think> IS still emitted (preserves #4633)", () => {
const state = newState();
const allResults: ReturnType<typeof claudeToOpenAIResponse>[] = [];
// message_start
allResults.push(
claudeToOpenAIResponse({ type: "message_start", message: { id: "msg_2", model: "claude-3-7-sonnet" } }, state)
);
// thinking block open
allResults.push(
claudeToOpenAIResponse(
{ type: "content_block_start", index: 0, content_block: { type: "thinking", thinking: "" } },
state
)
);
// thinking delta
allResults.push(
claudeToOpenAIResponse(
{ type: "content_block_delta", index: 0, delta: { type: "thinking_delta", thinking: "Plan..." } },
state
)
);
// thinking block stop
allResults.push(claudeToOpenAIResponse({ type: "content_block_stop", index: 0 }, state));
// text block open
allResults.push(
claudeToOpenAIResponse(
{ type: "content_block_start", index: 1, content_block: { type: "text", text: "" } },
state
)
);
// text delta (this should trigger the deferred </think> flush)
allResults.push(
claudeToOpenAIResponse(
{ type: "content_block_delta", index: 1, delta: { type: "text_delta", text: "Hello!" } },
state
)
);
// text block stop
allResults.push(claudeToOpenAIResponse({ type: "content_block_stop", index: 1 }, state));
// message_delta with stop_reason=end_turn
allResults.push(
claudeToOpenAIResponse(
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 10 } },
state
)
);
const chunks = collectChunks(allResults);
const hasThinkClose = chunks.some(
(chunk: any) => chunk?.choices?.[0]?.delta?.content === "</think>"
);
assert.ok(
hasThinkClose,
`Expected a chunk with delta.content === "</think>" in a pure-text thinking response, but none found.\nAll chunks: ${JSON.stringify(chunks, null, 2)}`
);
});

View File

@@ -0,0 +1,107 @@
// Tests for the `omniroute login antigravity` local OAuth helper.
//
// The helper runs the OAuth on the user's own machine (where the Google
// native-loopback consent can complete) and prints a credential blob to paste
// into a remote install. We test the two pieces that, if wrong, silently break
// the flow: the authorization request (must be a plain authorization_code grant
// with a 127.0.0.1 loopback redirect and NO PKCE challenge) and the end-to-end
// orchestration (state validation + exchange + blob emission), with the browser,
// loopback server, and token exchange injected as fakes.
import test from "node:test";
import assert from "node:assert/strict";
import {
buildAntigravityAuthRequest,
runAntigravityLogin,
} from "../../bin/cli/commands/login.mjs";
import { decodeCredentialBlob } from "../../src/lib/oauth/credentialBlob.ts";
test("buildAntigravityAuthRequest: loopback redirect on 127.0.0.1 + no PKCE", async () => {
const { authUrl, redirectUri, state } = await buildAntigravityAuthRequest(54321, () => "fixed");
assert.equal(redirectUri, "http://127.0.0.1:54321/callback");
assert.equal(state, "fixed");
const url = new URL(authUrl);
assert.equal(url.origin, "https://accounts.google.com");
assert.equal(url.searchParams.get("redirect_uri"), redirectUri);
assert.equal(url.searchParams.get("response_type"), "code");
assert.equal(url.searchParams.get("state"), "fixed");
assert.equal(url.searchParams.get("code_challenge"), null, "must NOT carry a PKCE challenge");
});
test("runAntigravityLogin: validates state, exchanges code, prints a decodable blob", async () => {
let exchangedCode = null;
let exchangedRedirect = null;
const blob = await runAntigravityLogin(
{},
{
makeState: () => "S",
openBrowser: async () => {},
startServer: async () => ({
port: 54321,
waitForCallback: async () => ({ code: "the-code", state: "S" }),
close: async () => {},
}),
exchange: async (code, redirectUri) => {
exchangedCode = code;
exchangedRedirect = redirectUri;
return { access_token: "ya29.a", refresh_token: "1//r", expires_in: 3600, scope: "x" };
},
print: () => {},
log: () => {},
}
);
assert.equal(exchangedCode, "the-code");
assert.equal(exchangedRedirect, "http://127.0.0.1:54321/callback");
const decoded = decodeCredentialBlob(blob);
assert.equal(decoded.provider, "antigravity");
assert.equal(decoded.tokens.access_token, "ya29.a");
assert.equal(decoded.tokens.refresh_token, "1//r");
});
test("runAntigravityLogin: rejects a state mismatch (CSRF guard)", async () => {
await assert.rejects(
() =>
runAntigravityLogin(
{},
{
makeState: () => "expected",
openBrowser: async () => {},
startServer: async () => ({
port: 1,
waitForCallback: async () => ({ code: "c", state: "ATTACKER" }),
close: async () => {},
}),
exchange: async () => ({ access_token: "x" }),
print: () => {},
log: () => {},
}
),
/state mismatch|csrf/i
);
});
test("runAntigravityLogin: surfaces an OAuth error param", async () => {
await assert.rejects(
() =>
runAntigravityLogin(
{},
{
makeState: () => "S",
openBrowser: async () => {},
startServer: async () => ({
port: 1,
waitForCallback: async () => ({ error: "access_denied", state: "S" }),
close: async () => {},
}),
exchange: async () => ({ access_token: "x" }),
print: () => {},
log: () => {},
}
),
/access_denied|authorization failed/i
);
});

View File

@@ -0,0 +1,29 @@
import test from "node:test";
import assert from "node:assert/strict";
/**
* Replicate the HOSTNAME resolution from bin/cli/commands/serve.mjs to verify
* that the spawned server honours a HOSTNAME provided via env/.env instead of
* always hardcoding "0.0.0.0" (#5134). Mirrors the in-file replication pattern
* used by cli-serve-port.test.ts (serve.mjs spawns processes, so the logic is
* tested in isolation rather than imported).
*/
function resolveHostname(envHostname: string | undefined): string {
return envHostname || "0.0.0.0";
}
test("serve hostname: honours HOSTNAME env var when set", () => {
assert.equal(resolveHostname("127.0.0.1"), "127.0.0.1");
});
test("serve hostname: honours a specific bind interface", () => {
assert.equal(resolveHostname("192.168.0.15"), "192.168.0.15");
});
test("serve hostname: falls back to 0.0.0.0 when HOSTNAME is unset", () => {
assert.equal(resolveHostname(undefined), "0.0.0.0");
});
test("serve hostname: falls back to 0.0.0.0 when HOSTNAME is an empty string", () => {
assert.equal(resolveHostname(""), "0.0.0.0");
});

View File

@@ -0,0 +1,195 @@
/**
* Regression test for #5166 (user-content-array 400 on Command Code / deepseek-v4-pro).
*
* When a client sends a user message whose `content` is an array of content parts
* (e.g. [{type:"text",text:"Hello"},{type:"text",text:"World"}]), the raw array
* must NOT reach the Command Code upstream — it requires user content to be a plain
* string. The executor must normalise the array to a string before posting.
*
* NOTE: this file covers ONLY the user-content-array/400 symptom of #5166.
* The 0-output-token symptom on mimo-v2.5-pro (reasoning-only models) is tracked
* separately and is NOT addressed here.
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(
path.join(os.tmpdir(), "omniroute-cmd-code-user-array-5166-")
);
process.env.DATA_DIR = TEST_DATA_DIR;
const { getExecutor } = await import("../../open-sse/executors/index.ts");
const core = await import("../../src/lib/db/core.ts");
const originalFetch = globalThis.fetch;
function commandCodeStream(lines: unknown[]) {
const text = lines.map((l) => JSON.stringify(l)).join("\n") + "\n";
return new Response(text, { status: 200, headers: { "Content-Type": "application/x-ndjson" } });
}
test.after(() => {
globalThis.fetch = originalFetch;
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test.afterEach(() => {
globalThis.fetch = originalFetch;
});
// ── helpers ────────────────────────────────────────────────────────────────────
type FetchCall = { url: string; init: Record<string, unknown>; body: Record<string, unknown> };
function captureFetch(response: Response) {
const calls: FetchCall[] = [];
globalThis.fetch = async (url, init: RequestInit = {}) => {
calls.push({ url: String(url), init: init as Record<string, unknown>, body: JSON.parse(String(init.body)) });
return response;
};
return calls;
}
// ── failing tests (before fix, user content is the raw array) ──────────────
test(
"#5166 user message with multi-part array content is flattened to a string (#5166)",
async () => {
const calls = captureFetch(
commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }])
);
await getExecutor("command-code").execute({
model: "deepseek/deepseek-v4-pro",
stream: false,
credentials: { apiKey: "cc_test_key" },
body: {
messages: [
{
role: "user",
content: [
{ type: "text", text: "Hello" },
{ type: "text", text: "World" },
],
},
],
},
});
const posted = calls[0].body;
const userMsg = (posted.params as Record<string, unknown[]>).messages[0] as Record<
string,
unknown
>;
// Must be a string — never an array — otherwise Command Code's upstream returns 400.
assert.equal(
typeof userMsg.content,
"string",
`user message content must be a string, got ${typeof userMsg.content}`
);
// Joined text parts with "\n"
assert.equal(userMsg.content, "Hello\nWorld");
}
);
test(
"#5166 user message with single text-part array is flattened to a plain string",
async () => {
const calls = captureFetch(
commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }])
);
await getExecutor("command-code").execute({
model: "deepseek/deepseek-v4-pro",
stream: false,
credentials: { apiKey: "cc_test_key" },
body: {
messages: [
{
role: "user",
content: [{ type: "text", text: "Hi there" }],
},
],
},
});
const posted = calls[0].body;
const userMsg = (posted.params as Record<string, unknown[]>).messages[0] as Record<
string,
unknown
>;
assert.equal(typeof userMsg.content, "string");
assert.equal(userMsg.content, "Hi there");
}
);
test(
"#5166 user message with plain string content passes through unchanged (no regression)",
async () => {
const calls = captureFetch(
commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }])
);
await getExecutor("command-code").execute({
model: "deepseek/deepseek-v4-pro",
stream: false,
credentials: { apiKey: "cc_test_key" },
body: {
messages: [
{
role: "user",
content: "Plain string message",
},
],
},
});
const posted = calls[0].body;
const userMsg = (posted.params as Record<string, unknown[]>).messages[0] as Record<
string,
unknown
>;
assert.equal(typeof userMsg.content, "string");
assert.equal(userMsg.content, "Plain string message");
}
);
test(
"#5166 user message with mixed parts (text + image_url) keeps only text parts",
async () => {
const calls = captureFetch(
commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }])
);
await getExecutor("command-code").execute({
model: "deepseek/deepseek-v4-pro",
stream: false,
credentials: { apiKey: "cc_test_key" },
body: {
messages: [
{
role: "user",
content: [
{ type: "text", text: "Describe this:" },
{ type: "image_url", image_url: { url: "https://example.com/img.png" } },
],
},
],
},
});
const posted = calls[0].body;
const userMsg = (posted.params as Record<string, unknown[]>).messages[0] as Record<
string,
unknown
>;
assert.equal(typeof userMsg.content, "string");
// Only text parts extracted; image_url part is dropped (not a "text" type)
assert.equal(userMsg.content, "Describe this:");
}
);

View File

@@ -0,0 +1,68 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
queryBlock,
MAX_GREP_MATCHES,
} from "../../../open-sse/services/compression/engines/ccr/ccrQuery.ts";
const block = ["l1 ok", "l2 ERROR foo", "l3 ok", "l4 ERROR bar", "l5 ok"].join("\n");
test("full / ausente → texto inteiro (backward-compat)", () => {
assert.deepEqual(queryBlock(block, {}), { content: block });
assert.deepEqual(queryBlock(block, { mode: "full" }), { content: block });
});
test("head:n e tail:n", () => {
assert.deepEqual(queryBlock(block, { mode: "head", n: 2 }), { content: "l1 ok\nl2 ERROR foo" });
assert.deepEqual(queryBlock(block, { mode: "tail", n: 2 }), { content: "l4 ERROR bar\nl5 ok" });
assert.deepEqual(queryBlock(block, { mode: "head", n: 99 }), { content: block });
});
test("lines:start-end 1-indexed inclusive, clamp", () => {
assert.deepEqual(queryBlock(block, { mode: "lines", start: 2, end: 3 }), {
content: "l2 ERROR foo\nl3 ok",
});
assert.deepEqual(queryBlock(block, { mode: "lines", start: 4, end: 99 }), {
content: "l4 ERROR bar\nl5 ok",
});
assert.ok("error" in queryBlock(block, { mode: "lines", start: 3, end: 1 }));
});
test("grep casa linhas; unique deduplica", () => {
const r = queryBlock(block, { mode: "grep", pattern: "ERROR" });
assert.deepEqual(r, { content: "l2 ERROR foo\nl4 ERROR bar" });
const dup = ["x ERR", "x ERR", "y ERR"].join("\n");
const u = queryBlock(dup, { mode: "grep", pattern: "ERR", unique: true });
assert.deepEqual(u, { content: "x ERR\ny ERR" });
});
test("grep ReDoS rejeitado por safe-regex", () => {
const r = queryBlock(block, { mode: "grep", pattern: "(a+)+$" });
assert.ok("error" in r);
assert.match((r as { error: string }).error, /backtrack|unsafe|reject/i);
});
test("grep regex inválido → erro; pattern grande → erro", () => {
assert.ok("error" in queryBlock(block, { mode: "grep", pattern: "[" }));
assert.ok("error" in queryBlock(block, { mode: "grep", pattern: "a".repeat(600) }));
});
test("grep cap de matches", () => {
const big = Array.from({ length: MAX_GREP_MATCHES + 50 }, (_, i) => `row ${i} HIT`).join("\n");
const r = queryBlock(big, { mode: "grep", pattern: "HIT" }) as { content: string };
const lines = r.content.split("\n").filter((l) => l.includes("HIT"));
assert.ok(lines.length <= MAX_GREP_MATCHES);
assert.match(r.content, /truncat/i);
});
test("stats → JSON lines/chars/bytes", () => {
const r = queryBlock(block, { mode: "stats" }) as { content: string };
const s = JSON.parse(r.content);
assert.equal(s.lines, 5);
assert.equal(s.chars, block.length);
assert.equal(s.bytes, Buffer.byteLength(block, "utf8"));
});
test("queryBlock nunca lança (fail-safe)", () => {
assert.doesNotThrow(() => queryBlock(block, { mode: "head", n: -5 }));
});

View File

@@ -0,0 +1,39 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
storeBlock,
handleCcrRetrieve,
resetCcrStore,
} from "../../../open-sse/services/compression/engines/ccr/index.ts";
test.beforeEach(() => resetCcrStore());
const block = ["a ok", "b ERROR", "c ok", "d ERROR"].join("\n");
test("sem mode → bloco inteiro (backward-compat)", () => {
const h = storeBlock(block, "p1");
assert.deepEqual(handleCcrRetrieve({ hash: h }, "p1"), { content: block });
});
test("modos ranged via handler", () => {
const h = storeBlock(block, "p1");
assert.deepEqual(handleCcrRetrieve({ hash: h, mode: "head", n: 1 }, "p1"), { content: "a ok" });
assert.deepEqual(handleCcrRetrieve({ hash: h, mode: "grep", pattern: "ERROR" }, "p1"), {
content: "b ERROR\nd ERROR",
});
const stats = handleCcrRetrieve({ hash: h, mode: "stats" }, "p1") as { content: string };
assert.equal(JSON.parse(stats.content).lines, 4);
});
test("scoping preservado: principal errado → not found", () => {
const h = storeBlock(block, "p1");
const r = handleCcrRetrieve({ hash: h, mode: "head", n: 1 }, "p2");
assert.ok("error" in r);
assert.match((r as { error: string }).error, /not found/i);
});
test("grep ReDoS via handler → erro (não crash)", () => {
const h = storeBlock(block, "p1");
const r = handleCcrRetrieve({ hash: h, mode: "grep", pattern: "(a+)+$" }, "p1");
assert.ok("error" in r);
});

View File

@@ -0,0 +1,35 @@
import test from "node:test";
import assert from "node:assert/strict";
import { summarizeEncoderCandidates } from "../../../open-sse/services/compression/engines/headroom/encoderComparison.ts";
const byteLen = (s: string) => Buffer.byteLength(s, "utf8");
test("agrega sizes e elege winner por tokens", () => {
const messages = [
{
role: "user",
content: JSON.stringify(Array.from({ length: 20 }, (_, i) => ({ id: i, ok: true }))),
},
];
const cmp = summarizeEncoderCandidates(messages, 8, byteLen);
assert.equal(cmp.arraysCompared, 1);
assert.ok(cmp.json.bytes > 0 && cmp.gcf.bytes > 0);
assert.ok(["gcf", "toon", "json"].includes(cmp.winner));
const sizes: Record<string, number> = { gcf: cmp.gcf.tokens, json: cmp.json.tokens };
if (cmp.toonAvailable) sizes["toon"] = cmp.toon.tokens;
const min = Math.min(...Object.values(sizes));
assert.equal(sizes[cmp.winner], min);
});
test("sem array compactável → zerado, sem winner espúrio", () => {
const cmp = summarizeEncoderCandidates([{ role: "user", content: "oi" }], 8, byteLen);
assert.equal(cmp.arraysCompared, 0);
assert.equal(cmp.json.bytes, 0);
assert.equal(cmp.gcf.bytes, 0);
});
test("system messages são ignoradas (igual ao smartcrusher)", () => {
const arr = JSON.stringify(Array.from({ length: 20 }, (_, i) => ({ id: i })));
const cmp = summarizeEncoderCandidates([{ role: "system", content: arr }], 8, byteLen);
assert.equal(cmp.arraysCompared, 0);
});

View File

@@ -0,0 +1,54 @@
// tests/unit/compression/previewRouteToon.test.ts
import test from "node:test";
import assert from "node:assert/strict";
import { join } from "node:path";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
const TEST_DATA_DIR = mkdtempSync(join(tmpdir(), "preview-toon-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET ?? "test-secret-32-chars-min-aaaaaaaa";
delete process.env.INITIAL_PASSWORD;
const core = await import("../../../src/lib/db/core.ts");
const route = await import("../../../src/app/api/compression/preview/route.ts");
function makeReq(body: unknown) {
return new Request("http://localhost/api/compression/preview", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
}
test.beforeEach(() => core.resetDbInstance());
test.after(() => {
core.resetDbInstance();
rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("headroom engine carries encoderComparison with one array and a valid winner", async () => {
const big = JSON.stringify(Array.from({ length: 20 }, (_, i) => ({ id: i, ok: true })));
const res = await route.POST(
makeReq({ messages: [{ role: "user", content: big }], engineId: "headroom" })
);
assert.equal(res.status, 200);
const body = await res.json();
assert.ok(body.encoderComparison);
assert.equal(body.encoderComparison.arraysCompared, 1);
assert.ok(["gcf", "toon", "json"].includes(body.encoderComparison.winner));
});
test("non-headroom engine (lite) does not carry encoderComparison", async () => {
const big = JSON.stringify(Array.from({ length: 20 }, (_, i) => ({ id: i, ok: true })));
const res = await route.POST(
makeReq({ messages: [{ role: "user", content: big }], engineId: "lite" })
);
assert.equal(res.status, 200);
const body = await res.json();
assert.equal(body.encoderComparison ?? null, null);
});
test("error responses do not leak stack traces", async () => {
const res = await route.POST(makeReq({ messages: [] }));
assert.equal(res.status, 400);
const body = await res.json();
const details = JSON.stringify(body);
assert.ok(!String(details).includes("at /"));
});

View File

@@ -0,0 +1,64 @@
import test from "node:test";
import assert from "node:assert/strict";
import { join } from "node:path";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
const TEST_DATA_DIR = mkdtempSync(join(tmpdir(), "retrieve-ranged-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET ?? "test-secret-32-chars-min-aaaaaaaa";
delete process.env.INITIAL_PASSWORD;
const core = await import("../../../src/lib/db/core.ts");
const ccr = await import("../../../open-sse/services/compression/engines/ccr/index.ts");
const route = await import("../../../src/app/api/compression/retrieve/route.ts");
const BLOCK = ["x1", "x2 ERR", "x3", "x4"].join("\n");
function makeReq(body: unknown) {
return new Request("http://localhost/api/compression/retrieve", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
}
test.beforeEach(() => {
core.resetDbInstance();
ccr.resetCcrStore();
});
test.after(() => {
core.resetDbInstance();
rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("mode:head n:2 returns first 2 lines", async () => {
const hash = ccr.storeBlock(BLOCK); // management scope (no principalId)
const res = await route.POST(makeReq({ hash, mode: "head", n: 2 }));
assert.equal(res.status, 200);
const json = await res.json();
assert.equal(json.found, true);
assert.equal(json.block, "x1\nx2 ERR");
});
test("mode:stats returns correct line count", async () => {
const hash = ccr.storeBlock(BLOCK);
const res = await route.POST(makeReq({ hash, mode: "stats" }));
assert.equal(res.status, 200);
const json = await res.json();
assert.equal(json.found, true);
assert.equal(JSON.parse(json.block).lines, 4);
});
test("mode:grep with ReDoS pattern is rejected and sanitized", async () => {
const hash = ccr.storeBlock(BLOCK);
const res = await route.POST(makeReq({ hash, mode: "grep", pattern: "(a+)+$" }));
assert.equal(res.status, 200);
const json = await res.json();
assert.ok(json.error, "expected an error field for the rejected ReDoS pattern");
assert.ok(!String(json.error).includes("at /"), "error must be sanitized");
});
test("no mode returns the whole block (backward-compat)", async () => {
const hash = ccr.storeBlock(BLOCK);
const res = await route.POST(makeReq({ hash }));
assert.equal(res.status, 200);
const json = await res.json();
assert.equal(json.found, true);
assert.equal(json.block, BLOCK);
});

View File

@@ -0,0 +1,49 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
tryCompactJson,
pickSmallestEncoding,
} from "../../../open-sse/services/compression/engines/headroom/smartcrusher.ts";
import { reconstructHeadroom } from "../../../open-sse/services/compression/engines/headroom/index.ts";
import { TOON_FENCE_OPEN } from "../../../open-sse/services/compression/engines/headroom/toon.ts";
import { GCF_FENCE_OPEN } from "../../../open-sse/services/compression/engines/headroom/tabular.ts";
const toonFavorable = Array.from({ length: 40 }, (_, i) => ({ id: i, ok: true }));
test("best-of-N: usa a fence do encoder menor e nunca regride vs JSON", () => {
const chosen = pickSmallestEncoding(toonFavorable);
const json = JSON.stringify(toonFavorable);
assert.ok(chosen.length < json.length, "deve encolher vs JSON");
assert.ok(chosen.startsWith(GCF_FENCE_OPEN) || chosen.startsWith(TOON_FENCE_OPEN));
});
test("se TOON vence, tryCompactJson emite fence toon e round-trip restaura", () => {
const json = JSON.stringify(toonFavorable);
const compact = tryCompactJson(json, 8);
assert.notEqual(compact, null);
const body = { messages: [{ role: "user", content: compact as string }] };
const restored = reconstructHeadroom(body);
const text = (restored.messages as Array<{ content: string }>)[0].content;
assert.deepEqual(JSON.parse(text), toonFavorable);
});
test("empate/GCF-favorável: round-trip lossless via a fence escolhida", () => {
const gcfFavorable = Array.from({ length: 10 }, (_, i) => ({
id: i,
deep: { a: { b: [i, i + 1] }, label: `row-${i}` },
}));
const chosen = pickSmallestEncoding(gcfFavorable);
assert.ok(chosen.startsWith(GCF_FENCE_OPEN) || chosen.startsWith(TOON_FENCE_OPEN));
const compact = tryCompactJson(JSON.stringify(gcfFavorable), 8);
if (compact) {
const body = { messages: [{ role: "user", content: compact }] };
const restored = reconstructHeadroom(body);
const text = (restored.messages as Array<{ content: string }>)[0].content;
assert.deepEqual(JSON.parse(text), gcfFavorable);
}
});
test("nunca aumenta vs JSON (gate preservado): array < minRows → no-op", () => {
const tiny = [{ a: 1 }];
assert.equal(tryCompactJson(JSON.stringify(tiny), 8), null);
});

View File

@@ -0,0 +1,72 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
encodeToonBlock,
wrapToon,
decodeToon,
TOON_FENCE_OPEN,
} from "../../../open-sse/services/compression/engines/headroom/toon.ts";
const cases: Array<{ name: string; arr: Record<string, unknown>[] }> = [
{
name: "homogêneo",
arr: [
{ id: 1, name: "a" },
{ id: 2, name: "b" },
],
},
{
name: "heterogêneo",
arr: [
{ id: 1, x: 1 },
{ id: 2, y: 2 },
],
},
{
name: "nested",
arr: [
{ id: 1, meta: { a: [1, 2] } },
{ id: 2, meta: { a: [3] } },
],
},
{
name: "nullable",
arr: [
{ id: 1, v: null },
{ id: 2, v: 5 },
],
},
{
name: "strings especiais",
arr: [
{ id: 1, s: 'a,b "q"\nc' },
{ id: 2, s: "plain" },
],
},
];
for (const c of cases) {
test(`toon round-trips: ${c.name}`, () => {
const inner = encodeToonBlock(c.arr);
assert.notEqual(inner, null, "encode deve produzir string");
const fenced = wrapToon(inner as string);
assert.ok(fenced.startsWith(TOON_FENCE_OPEN));
const decoded = decodeToon(fenced);
assert.deepEqual(decoded, c.arr);
});
}
test("decodeToon aceita bloco sem fence", () => {
const arr = [{ a: 1 }, { a: 2 }];
const inner = encodeToonBlock(arr) as string;
assert.deepEqual(decodeToon(inner), arr);
});
test("encodeToonBlock é fail-open (não lança)", () => {
const a: Record<string, unknown> = { id: 1 };
a["self"] = a;
assert.doesNotThrow(() => {
const r = encodeToonBlock([a]);
assert.equal(r, null);
});
});

View File

@@ -0,0 +1,71 @@
// Regression guard for #5083 (Bug 1): the dashboard, when reached from a LAN /
// Tailscale / non-loopback host, opens `ws://<that-host>:<port>` to its own Live WS
// server. The static Content-Security-Policy in next.config.mjs previously allowed
// `ws:` only for loopback origins (`ws://localhost:*`, `ws://127.0.0.1:*`) plus a bare
// `wss:` — so a plain-`ws:` connection to a non-loopback host was blocked by the
// browser. The fix permits the bare `ws:` scheme (symmetric with the bare `wss:` that
// was already allowed), without introducing any global Next.js middleware (the project
// intentionally has none — interception is route-specific; see CLAUDE.md / AGENTS.md).
//
// This test pins the connect-src directive in next.config.mjs so the LAN/Tailscale WS
// allowance cannot silently regress, while confirming the other security directives are
// left intact.
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
const here = dirname(fileURLToPath(import.meta.url));
const nextConfig = readFileSync(resolve(here, "../../next.config.mjs"), "utf8");
// Extract the connect-src directive string literal from the CSP array.
function connectSrcDirective(): string {
const match = nextConfig.match(/"(connect-src[^"]*)"/);
assert.ok(match, "next.config.mjs must define a connect-src directive in the CSP array");
return match![1];
}
test("#5083 connect-src permits the bare ws: scheme for non-loopback dashboards", () => {
const connectSrc = connectSrcDirective();
// Bare `ws:` must be present as its own token (not just `ws://localhost`).
assert.match(
connectSrc,
/(^|\s)ws:(\s|$)/,
`connect-src must allow the bare ws: scheme; got: ${connectSrc}`
);
});
test("#5083 connect-src keeps the bare wss: scheme it mirrors", () => {
const connectSrc = connectSrcDirective();
assert.match(connectSrc, /(^|\s)wss:(\s|$)/, "connect-src must still allow bare wss:");
});
test("#5083 connect-src still scopes 'self' and explicit loopback origins", () => {
const connectSrc = connectSrcDirective();
assert.ok(connectSrc.includes("'self'"), "connect-src must still include 'self'");
assert.ok(
connectSrc.includes("ws://localhost:*") && connectSrc.includes("ws://127.0.0.1:*"),
"explicit loopback ws origins must remain listed"
);
});
test("#5083 fix does NOT introduce a global Next.js middleware", () => {
// The project has no global middleware by design; the CSP fix must stay declarative.
let exists = true;
try {
readFileSync(resolve(here, "../../src/middleware.ts"), "utf8");
} catch {
exists = false;
}
assert.equal(exists, false, "src/middleware.ts must not exist (no global middleware — see CLAUDE.md)");
});
test("#5083 baseline security directives remain intact in the CSP", () => {
// frame-ancestors 'none' is the global default; object-src 'none' and base-uri 'self'
// must not have been weakened by the connect-src change.
assert.ok(nextConfig.includes("frame-ancestors 'none'"), "frame-ancestors 'none' must remain");
assert.ok(nextConfig.includes("object-src 'none'"), "object-src 'none' must remain");
assert.ok(nextConfig.includes("base-uri 'self'"), "base-uri 'self' must remain");
});

View File

@@ -0,0 +1,151 @@
// Regression test for #5154 — nameless <tool> blocks with <parameter> children
// are silently dropped (raw XML leaks to client) when no <name> child, no JSON body,
// and no tag-name suffix are present.
//
// Fix: when name resolution fails but <parameter> tags extracted params, attempt
// a conservative fuzzy-name resolution using the requested tools' parameter schemas.
// If exactly one requested tool's schema keys are a superset of the extracted param names,
// adopt that tool name. If zero or >1 match, keep returning null (no misattribution).
import { describe, test } from "node:test";
import assert from "node:assert/strict";
import { parseDeepSeekToolCalls } from "../../open-sse/translator/deepseekWebTools.ts";
// Tools that define parameter schemas — needed so the schema-match can fire.
const TOOLS_WITH_SCHEMAS = [
{
type: "function",
function: {
name: "read",
description: "Read a file",
parameters: {
type: "object",
properties: {
filePath: { type: "string" },
offset: { type: "number" },
limit: { type: "number" },
},
required: ["filePath"],
},
},
},
{
type: "function",
function: {
name: "bash",
description: "Run a shell command",
parameters: {
type: "object",
properties: {
command: { type: "string" },
},
required: ["command"],
},
},
},
];
// Two tools that share parameter names — used to test the ambiguous case.
const AMBIGUOUS_TOOLS = [
{
type: "function",
function: {
name: "read_file",
parameters: {
type: "object",
properties: {
filePath: { type: "string" },
offset: { type: "number" },
},
},
},
},
{
type: "function",
function: {
name: "write_file",
parameters: {
type: "object",
properties: {
filePath: { type: "string" },
offset: { type: "number" },
},
},
},
},
];
describe("deepseekWebTools — nameless <tool> blocks (#5154)", () => {
test("nameless <tool> with <parameter> children resolves to the unique schema match", () => {
// Reproduces issue #5154: model emits <tool> with no name, no JSON body, no tag suffix.
// Previously: extractCall returned null → raw XML leaked to the client.
// After fix: params filePath+offset uniquely match the 'read' tool schema → name recovered.
const text = `<tool><parameter name="filePath">"x"</parameter><parameter name="offset">675</parameter></tool>`;
const { toolCalls, content } = parseDeepSeekToolCalls(text, "call", TOOLS_WITH_SCHEMAS);
assert.ok(toolCalls && toolCalls.length === 1, "expected exactly one tool call, got null/empty");
assert.equal(toolCalls![0].function.name, "read", "tool name resolved via schema match");
const args = JSON.parse(toolCalls![0].function.arguments);
assert.ok("filePath" in args, "filePath argument present");
assert.ok("offset" in args, "offset argument present");
assert.ok(!content.includes("<tool"), "tool block stripped from content");
});
test("nameless <tool> with only one matching param resolves when that param is unique to one tool", () => {
// 'command' uniquely identifies 'bash' tool.
const text = `<tool><parameter name="command">echo hello</parameter></tool>`;
const { toolCalls } = parseDeepSeekToolCalls(text, "call", TOOLS_WITH_SCHEMAS);
assert.ok(toolCalls && toolCalls.length === 1, "expected exactly one tool call");
assert.equal(toolCalls![0].function.name, "bash");
assert.deepEqual(JSON.parse(toolCalls![0].function.arguments), { command: "echo hello" });
});
test("nameless <tool> is NOT resolved when params are ambiguous (>1 schema match)", () => {
// Both 'read_file' and 'write_file' have the same filePath+offset schema.
// The parser must NOT misattribute — it should return null (no tool call).
const text = `<tool><parameter name="filePath">/etc/hosts</parameter><parameter name="offset">0</parameter></tool>`;
const { toolCalls } = parseDeepSeekToolCalls(text, "call", AMBIGUOUS_TOOLS);
assert.ok(!toolCalls || toolCalls.length === 0, "must not emit a call when params are ambiguous");
});
test("nameless <tool> is NOT resolved when no tool schema matches the extracted params", () => {
// Param 'unknownKey' doesn't appear in any tool schema.
const text = `<tool><parameter name="unknownKey">value</parameter></tool>`;
const { toolCalls } = parseDeepSeekToolCalls(text, "call", TOOLS_WITH_SCHEMAS);
assert.ok(!toolCalls || toolCalls.length === 0, "must not emit a call when no schema matches");
});
test("existing named <tool:read> blocks still work (no regression)", () => {
// Verify the pre-existing named-tag path is unaffected.
const tools = [
{
type: "function",
function: {
name: "read",
parameters: {
type: "object",
properties: { filePath: { type: "string" } },
},
},
},
];
const text = `<tool:read>{"filePath": "/tmp/test.py"}</tool>`;
const { toolCalls } = parseDeepSeekToolCalls(text, "call", tools);
assert.ok(toolCalls && toolCalls.length === 1);
assert.equal(toolCalls![0].function.name, "read");
assert.deepEqual(JSON.parse(toolCalls![0].function.arguments), { filePath: "/tmp/test.py" });
});
test("existing <tool>{json}</tool> canonical blocks still work (no regression)", () => {
const text = `<tool>{"name": "bash", "arguments": {"command": "ls -la"}}</tool>`;
const { toolCalls } = parseDeepSeekToolCalls(text, "call", TOOLS_WITH_SCHEMAS);
assert.ok(toolCalls && toolCalls.length === 1);
assert.equal(toolCalls![0].function.name, "bash");
assert.deepEqual(JSON.parse(toolCalls![0].function.arguments), { command: "ls -la" });
});
});

View File

@@ -0,0 +1,33 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
// The dev server runs Next's bundler in-process via `node scripts/dev/run-next.mjs`.
// `--max-old-space-size` can only be set at process startup (not from inside the
// running process), so the heap ceiling has to live on the `node` invocation in
// the `dev` npm script. Without it, the dev bundler runs on V8's ~4GB default and
// OOMs while compiling heavy dashboard routes (e.g. /dashboard/providers, which
// pulls monaco / recharts / @lobehub/icons / xyflow / mermaid). The unit test
// suite already runs with 8192; the dev server must match.
const here = path.dirname(fileURLToPath(import.meta.url));
const pkg = JSON.parse(fs.readFileSync(path.resolve(here, "../../package.json"), "utf8"));
test("dev script raises the Node heap limit (matches the test suite's 8192)", () => {
const dev = pkg.scripts?.dev ?? "";
assert.match(
dev,
/node\s+--max-old-space-size=8192\b/,
`dev script must launch node with --max-old-space-size=8192; got: ${dev}`
);
// Guard ordering: the flag must precede the script path so node (not the script)
// receives it.
const flagIdx = dev.indexOf("--max-old-space-size");
const scriptIdx = dev.indexOf("run-next.mjs");
assert.ok(scriptIdx !== -1, "dev script must still launch run-next.mjs");
assert.ok(
flagIdx !== -1 && flagIdx < scriptIdx,
"the heap flag must come before run-next.mjs so node receives it"
);
});

View File

@@ -209,6 +209,65 @@ test("detectMalformedNonStream allows Responses API function_call items as valid
assert.equal(detectMalformedNonStream(body), null);
});
// ── (c2) detectMalformedNonStream — Claude Messages shape ────────────────────
test("detectMalformedNonStream returns null for Claude message with text content", () => {
const body = {
type: "message",
role: "assistant",
content: [{ type: "text", text: "Hi!" }],
stop_reason: "end_turn",
};
assert.equal(detectMalformedNonStream(body), null);
});
test("detectMalformedNonStream returns null for Claude message with tool_use block", () => {
const body = {
type: "message",
role: "assistant",
content: [{ type: "tool_use", id: "toolu_1", name: "search", input: {} }],
stop_reason: "tool_use",
};
assert.equal(detectMalformedNonStream(body), null);
});
test("detectMalformedNonStream returns null for Claude message with thinking block", () => {
const body = {
type: "message",
role: "assistant",
content: [{ type: "thinking", thinking: "let me think" }],
stop_reason: "end_turn",
};
assert.equal(detectMalformedNonStream(body), null);
});
test("detectMalformedNonStream returns 'empty_choices' for Claude message with empty content", () => {
const body = { type: "message", role: "assistant", content: [], stop_reason: "end_turn" };
assert.equal(detectMalformedNonStream(body), "empty_choices");
});
test("detectMalformedNonStream returns 'empty_choices' for Claude message with empty-text block", () => {
const body = {
type: "message",
role: "assistant",
content: [{ type: "text", text: "" }],
stop_reason: "end_turn",
};
assert.equal(detectMalformedNonStream(body), "empty_choices");
});
test("detectMalformedNonStream returns 'empty_choices' for Claude message with (empty response) placeholder", () => {
// convertOpenAINonStreamingToClaude substitutes this placeholder for empty
// upstream content; it must still be treated as malformed.
const body = {
type: "message",
role: "assistant",
content: [{ type: "text", text: "(empty response)" }],
stop_reason: "end_turn",
};
assert.equal(detectMalformedNonStream(body), "empty_choices");
});
// ── (d) no stack trace leakage ───────────────────────────────────────────────
test("synthOpenAIErrorChunk message does NOT contain stack trace path", () => {

View File

@@ -0,0 +1,50 @@
import test from "node:test";
import assert from "node:assert/strict";
const { geminiToOpenAIResponse } =
await import("../../open-sse/translator/response/gemini-to-openai.ts");
const { translateNonStreamingResponse } =
await import("../../open-sse/handlers/responseTranslator.ts");
const { FORMATS } = await import("../../open-sse/translator/formats.ts");
test("Gemini non-stream: prohibited content finish reason becomes content_filter", () => {
const result = translateNonStreamingResponse(
{
responseId: "resp-prohibited",
modelVersion: "gemini-2.5-flash",
candidates: [
{
content: { parts: [{ text: "partial text" }] },
finishReason: "PROHIBITED_CONTENT",
},
],
},
FORMATS.GEMINI,
FORMATS.OPENAI
);
assert.equal((result as any).choices[0].message.content, "partial text");
assert.equal((result as any).choices[0].finish_reason, "content_filter");
});
test("Gemini stream: prohibited content finish reason becomes content_filter", () => {
const result = geminiToOpenAIResponse(
{
responseId: "resp-prohibited",
modelVersion: "gemini-2.5-flash",
candidates: [
{
content: { parts: [{ text: "partial text" }] },
finishReason: "PROHIBITED_CONTENT",
},
],
},
{ toolCalls: new Map() }
);
assert.equal(result.at(-1).choices[0].finish_reason, "content_filter");
assert.equal(
result.some((event: any) => event.choices?.[0]?.delta?.content === "partial text"),
true
);
});

View File

@@ -19,6 +19,10 @@ test("extracts body.input as STRING without char-splitting", () => {
test("extracts body.input as array of strings", () => {
assert.ok(extractMessageContents({ input: [INJ, "y"] }).join("\n").includes(INJ));
});
test("extracts body.input as Responses object without throwing", () => {
const out = extractMessageContents({ input: { role: "user", content: INJ } }).join("\n");
assert.ok(out.includes(INJ));
});
test("extracts body.query + body.documents (rerank)", () => {
const out = extractMessageContents({ query: INJ, documents: ["doc1", "doc2"] }).join("\n");
assert.ok(out.includes(INJ) && out.includes("doc1"));

View File

@@ -0,0 +1,50 @@
/**
* Issue #5172 / #5160 / #5152 — server OOM ("Ineffective mark-compacts near heap
* limit ... ~500MB") on machines with plenty of RAM. Root cause: the server was
* spawned with a FIXED 512MB heap default (`omniroute serve`) or with no
* `--max-old-space-size` at all (Electron), so a 16GB box with 65 providers /
* 2600 models still crashed at ~512MB.
*
* Fix: `calibrateHeapFallbackMb(totalmemBytes)` derives a sane default heap from
* the host's physical RAM (~35%, clamped to [512, 4096]) so the out-of-the-box
* ceiling scales with the machine. An explicit `OMNIROUTE_MEMORY_MB` still wins
* (resolveMaxOldSpaceMb), and the existing #2939 contract is unchanged.
*/
import test from "node:test";
import assert from "node:assert/strict";
const { calibrateHeapFallbackMb, resolveMaxOldSpaceMb } =
await import("../../scripts/build/runtime-env.mjs");
const GB = 1024 * 1024 * 1024;
test("#5172 calibrates the default heap to ~35% of physical RAM", () => {
// 8GB → 8192 * 0.35 ≈ 2867
assert.equal(calibrateHeapFallbackMb(8 * GB), 2867);
// 4GB → floor(4096 * 0.35) = 1433
assert.equal(calibrateHeapFallbackMb(4 * GB), 1433);
});
test("#5172 clamps the calibrated default to [512, 4096]", () => {
// 16GB → 5734 → clamped to the 4096 ceiling (the reporter's box)
assert.equal(calibrateHeapFallbackMb(16 * GB), 4096);
assert.equal(calibrateHeapFallbackMb(64 * GB), 4096);
// 1GB → 358 → floored to 512
assert.equal(calibrateHeapFallbackMb(1 * GB), 512);
});
test("#5172 falls back to 512 for missing/invalid totalmem", () => {
assert.equal(calibrateHeapFallbackMb(0), 512);
assert.equal(calibrateHeapFallbackMb(undefined), 512);
assert.equal(calibrateHeapFallbackMb(null), 512);
assert.equal(calibrateHeapFallbackMb(NaN), 512);
assert.equal(calibrateHeapFallbackMb(-1), 512);
});
test("#5172 an explicit OMNIROUTE_MEMORY_MB still wins over the calibrated default", () => {
const calibrated = calibrateHeapFallbackMb(16 * GB); // 4096
// explicit override (in-range) is honored verbatim, not the calibrated default
assert.equal(resolveMaxOldSpaceMb("1536", calibrated), 1536);
// unset → the calibrated default is used (not the old fixed 512)
assert.equal(resolveMaxOldSpaceMb(undefined, calibrated), 4096);
});

View File

@@ -155,6 +155,29 @@ describe("synthesizeOpenAiSseFromJson (#3089)", () => {
assert.equal(toolDelta.tool_calls[0].id, "t1");
});
test("normalizes prohibited content finish reasons in synthesized SSE", () => {
const sse = synthesizeOpenAiSseFromJson(
JSON.stringify({
choices: [
{
message: { role: "assistant", content: "partial text" },
finish_reason: "prohibited_content",
},
],
})
);
const events = parseDataChunks(sse)
.filter((c) => c !== "[DONE]")
.map((c) => JSON.parse(c));
const finishChunk = events.at(-1);
assert.equal(finishChunk.choices[0].finish_reason, "content_filter");
assert.equal(
events.some((event) => event.choices[0].delta.content === "partial text"),
true
);
});
test("returns empty string for non-completion JSON / invalid JSON", () => {
assert.equal(synthesizeOpenAiSseFromJson('{"error":{"message":"x"}}'), "");
assert.equal(synthesizeOpenAiSseFromJson("{not json"), "");

View File

@@ -285,7 +285,9 @@ test("shutdownMcpHttp removes sessions created via handleMcpStreamableHTTP", asy
body: JSON.stringify({ jsonrpc: "2.0", method: "tools/list", id: 2 }),
});
const staleRes = await mod.handleMcpStreamableHTTP(staleReq);
assert.equal(staleRes.status, 400);
// MCP spec (2025-03-26 / 2025-11-25, Session Management): a terminated/unknown
// session id MUST return 404 Not Found so the client re-initializes (issue #5169).
assert.equal(staleRes.status, 404);
}
});
@@ -302,8 +304,46 @@ test("handleMcpStreamableHTTP rejects request with unknown session id", async ()
body: JSON.stringify({ jsonrpc: "2.0", method: "tools/list", id: 1 }),
});
const res = await mod.handleMcpStreamableHTTP(req);
assert.equal(res.status, 400);
// Per MCP spec, a present-but-unknown session id MUST yield 404 (not 400), so
// the client knows to start a fresh session rather than hard-fail (issue #5169).
assert.equal(res.status, 404);
const body = await res.json();
assert.ok(body.error);
assert.ok(body.error.message.includes("Unknown"));
});
// ── Regression #5169: unknown/expired session → HTTP 404 (not 400) ───────────
test("handleMcpStreamableHTTP returns 404 (not 400) for an unknown session id", async () => {
mod.shutdownMcpHttp();
const req = new Request("http://localhost/api/mcp/stream", {
method: "POST",
headers: {
"Content-Type": "application/json",
"mcp-session-id": "expired-or-unknown-session",
},
body: JSON.stringify({ jsonrpc: "2.0", method: "tools/list", id: 7 }),
});
const res = await mod.handleMcpStreamableHTTP(req);
// The 400-vs-404 distinction is the whole bug: clients only re-initialize on 404.
assert.equal(res.status, 404);
const body = await res.json();
assert.equal(body.jsonrpc, "2.0");
assert.equal(body.error.code, -32000);
assert.ok(body.error.message.includes("Mcp-Session-Id"));
});
// ── Guard: a *missing* session id on a non-initialize request stays 400 ───────
test("handleMcpStreamableHTTP keeps 400 for a missing session id (non-initialize)", async () => {
mod.shutdownMcpHttp();
const req = new Request("http://localhost/api/mcp/stream", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", method: "tools/list", id: 8 }),
});
const res = await mod.handleMcpStreamableHTTP(req);
// Spec reserves 400 for a *missing* session id on non-initialize requests —
// only the *present-but-unknown* case changed to 404. This must NOT regress.
assert.equal(res.status, 400);
});

View File

@@ -0,0 +1,75 @@
/**
* #5192 — Blocking the "Auto (Zero-Config)" provider in Security settings must
* remove the built-in `auto/*` combos from the `/v1/models` listing.
*
* The catalog advertises the built-in `auto/*` combos (#4164 / #4235) at the top
* of `/v1/models`, but that injection loop ignored `settings.blockedProviders`,
* so checking "Auto (Zero-Config)" in Security → Blocked Providers had no effect:
* the model picker still showed `auto/*` entries. Blocking the system provider id
* `auto` (its id and alias are both "auto") must suppress the whole `auto/*` block.
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-block-auto-5192-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "block-auto-5192-test-secret";
const core = await import("../../src/lib/db/core.ts");
const settingsDb = await import("../../src/lib/db/settings.ts");
const apiKeysDb = await import("../../src/lib/db/apiKeys.ts");
const v1ModelsCatalog = await import("../../src/app/api/v1/models/catalog.ts");
type ModelsResponseBody = { data: Array<{ id: string }> };
async function resetStorage() {
core.resetDbInstance();
apiKeysDb.resetApiKeyState();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
async function getIds(): Promise<Set<string>> {
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as ModelsResponseBody;
return new Set(body.data.map((item) => item.id));
}
test.beforeEach(async () => {
await resetStorage();
});
test.after(async () => {
core.resetDbInstance();
apiKeysDb.resetApiKeyState();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("#5192 baseline: built-in auto/* combos are listed when Auto is not blocked", async () => {
const ids = await getIds();
assert.ok(ids.has("auto/best-coding"), "auto/best-coding should be listed by default");
assert.ok(ids.has("auto/coding"), "auto/coding should be listed by default");
});
test("#5192: blocking the Auto (Zero-Config) provider removes all auto/* combos", async () => {
await settingsDb.updateSettings({ blockedProviders: ["auto"] });
const ids = await getIds();
const leaked = [...ids].filter((id) => id === "auto" || id.startsWith("auto/"));
assert.deepEqual(
leaked,
[],
`no auto/* combos should be listed when Auto is blocked, but got: ${leaked.join(", ")}`
);
});
test("#5192: blocking via the 'auto' alias is honored too", async () => {
// id and alias are both "auto"; the Security UI stores whichever the user clicked.
await settingsDb.updateSettings({ blockedProviders: ["auto"] });
const ids = await getIds();
assert.equal(ids.has("auto/smart"), false, "auto/smart must be absent when Auto is blocked");
});

View File

@@ -0,0 +1,84 @@
// Unit tests for the OAuth credential blob codec used by the remote login helper.
//
// Context (operator report 2026-06-27): Google's `firstparty/nativeapp` consent
// for the embedded Antigravity desktop client only releases the authorization
// code when the loopback redirect (127.0.0.1:<port>) is reachable. On a remote
// VPS install the loopback is unreachable, so the consent hangs and never emits
// a code — the normal "paste the callback URL" fallback has nothing to paste.
//
// The fix is a local helper (`omniroute login antigravity`) that runs the OAuth
// on the user's own machine (loopback reachable → consent completes → tokens),
// then prints a single-line, paste-safe credential blob. The user pastes it into
// the remote dashboard, which decodes it and persists the connection.
//
// This codec is the contract between the helper (encoder) and the server/dashboard
// (decoder). These tests pin: roundtrip fidelity, the human-recognizable prefix,
// version + provider gating, and rejection of malformed/tampered input.
import test from "node:test";
import assert from "node:assert/strict";
import {
CREDENTIAL_BLOB_PREFIX,
encodeCredentialBlob,
decodeCredentialBlob,
} from "../../src/lib/oauth/credentialBlob.ts";
const SAMPLE = {
provider: "antigravity",
tokens: {
access_token: "ya29.access",
refresh_token: "1//refresh",
id_token: "eyJ.id.token",
expires_in: 3599,
scope: "https://www.googleapis.com/auth/cloud-platform",
},
};
test("encode → decode roundtrips the provider and tokens", () => {
const blob = encodeCredentialBlob(SAMPLE);
const decoded = decodeCredentialBlob(blob);
assert.equal(decoded.provider, "antigravity");
assert.deepEqual(decoded.tokens, SAMPLE.tokens);
});
test("blob is a single paste-safe line with the recognizable prefix", () => {
const blob = encodeCredentialBlob(SAMPLE);
assert.ok(blob.startsWith(CREDENTIAL_BLOB_PREFIX), "must carry the omniroute prefix");
assert.ok(!/\s/.test(blob), "must contain no whitespace (single line, copy-paste safe)");
// base64url only after the prefix — no +/= that break in URLs / shells.
const payload = blob.slice(CREDENTIAL_BLOB_PREFIX.length);
assert.match(payload, /^[A-Za-z0-9_-]+$/, "payload must be base64url");
});
test("decode rejects a blob without the prefix", () => {
const raw = Buffer.from(JSON.stringify({ v: 1, ...SAMPLE })).toString("base64url");
assert.throws(() => decodeCredentialBlob(raw), /prefix|invalid|format/i);
});
test("decode rejects an unsupported version", () => {
// Hand-craft a v999 blob with the right prefix.
const payload = Buffer.from(JSON.stringify({ v: 999, ...SAMPLE })).toString("base64url");
assert.throws(() => decodeCredentialBlob(`${CREDENTIAL_BLOB_PREFIX}${payload}`), /version/i);
});
test("decode rejects a blob missing an access_token", () => {
const bad = encodeCredentialBlob({
provider: "antigravity",
tokens: { refresh_token: "only-refresh" },
});
assert.throws(() => decodeCredentialBlob(bad), /access_token|token/i);
});
test("decode rejects tampered base64 (not valid JSON)", () => {
assert.throws(
() => decodeCredentialBlob(`${CREDENTIAL_BLOB_PREFIX}not-valid-base64-json!!!`),
/invalid|parse|format/i
);
});
test("encode requires a provider", () => {
assert.throws(
() => encodeCredentialBlob({ tokens: { access_token: "x" } } as never),
/provider/i
);
});

View File

@@ -0,0 +1,79 @@
// Route-wiring tests for the paste-credentials OAuth action.
//
// These exercise the rejection paths only — they all return 400 BEFORE the
// route touches finalizeTokens (Google APIs) or persistOAuthConnection (DB), so
// no network is hit. The happy path's finalize+persist is the same IO as the
// already-covered `device-complete` action; the new, security-relevant logic
// (allowlist + provider match + blob validation) is unit-tested in
// oauth-paste-credentials.test.ts and re-asserted through the HTTP boundary here.
//
// Auth is disabled via settings (requireLogin:false) so we reach the action
// dispatch rather than a 401. DB handles are released in test.after (CLAUDE.md
// learning: unreleased SQLite handles hang node:test).
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-paste-creds-route-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const settingsDb = await import("../../src/lib/db/settings.ts");
const route = await import("../../src/app/api/oauth/[provider]/paste-credentials/route.ts");
const { encodeCredentialBlob } = await import("../../src/lib/oauth/credentialBlob.ts");
const tokens = { access_token: "ya29.x", refresh_token: "1//r", expires_in: 3599 };
test.before(async () => {
await settingsDb.updateSettings({ requireLogin: false });
});
test.after(async () => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
async function postPaste(provider: string, body: unknown) {
const request = new Request(`http://localhost:20128/api/oauth/${provider}/paste-credentials`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const response = await route.POST(request, {
params: Promise.resolve({ provider, action: "paste-credentials" }),
} as never);
return { status: response.status, body: await response.json() };
}
test("paste-credentials: non-allowlisted provider is rejected with 400", async () => {
const blob = encodeCredentialBlob({ provider: "openai", tokens });
const { status, body } = await postPaste("openai", { blob });
assert.equal(status, 400);
assert.match(body.error, /not supported|supported/i);
});
test("paste-credentials: malformed blob is rejected with 400", async () => {
const { status, body } = await postPaste("antigravity", { blob: "totally-not-a-blob" });
assert.equal(status, 400);
assert.match(body.error, /invalid|format|prefix/i);
});
test("paste-credentials: provider mismatch (blob for antigravity, route agy) → 400", async () => {
const blob = encodeCredentialBlob({ provider: "antigravity", tokens });
const { status, body } = await postPaste("agy", { blob });
assert.equal(status, 400);
assert.match(body.error, /match|mismatch|provider/i);
});
test("paste-credentials: empty body fails schema validation with 400", async () => {
const { status } = await postPaste("antigravity", {});
assert.equal(status, 400);
});
test("paste-credentials: error responses never leak a stack trace", async () => {
const { body } = await postPaste("antigravity", { blob: "totally-not-a-blob" });
assert.ok(!String(body.error).includes("at /"), "must not leak a stack trace");
});

View File

@@ -0,0 +1,49 @@
// Unit tests for the server-side paste-credentials gate.
//
// The remote login helper (`omniroute login antigravity`) prints a credential
// blob; the dashboard POSTs it to /api/oauth/<provider>/paste-credentials. Before
// the server finalizes + persists the tokens it MUST validate that (a) the route
// provider is on the paste-credentials allowlist (only Google native-loopback
// providers, never arbitrary providers) and (b) the blob's embedded provider
// matches the route provider — otherwise a blob minted for one provider could be
// replayed against another. This pins that gate; the finalize/persist IO is the
// same path as the already-tested `device-complete` action.
import test from "node:test";
import assert from "node:assert/strict";
import { encodeCredentialBlob } from "../../src/lib/oauth/credentialBlob.ts";
import {
PASTE_CREDENTIAL_PROVIDERS,
parsePastedCredentials,
} from "../../src/lib/oauth/pasteCredentials.ts";
const tokens = { access_token: "ya29.x", refresh_token: "1//r", expires_in: 3599 };
test("allowlist contains antigravity and its agy alias, not codex", () => {
assert.ok(PASTE_CREDENTIAL_PROVIDERS.has("antigravity"));
assert.ok(PASTE_CREDENTIAL_PROVIDERS.has("agy"));
assert.ok(!PASTE_CREDENTIAL_PROVIDERS.has("codex"), "codex uses its own device-complete path");
});
test("accepts a matching antigravity blob and returns the tokens", () => {
const blob = encodeCredentialBlob({ provider: "antigravity", tokens });
const result = parsePastedCredentials("antigravity", blob);
assert.equal(result.provider, "antigravity");
assert.deepEqual(result.tokens, tokens);
});
test("rejects a provider not on the allowlist", () => {
const blob = encodeCredentialBlob({ provider: "openai", tokens });
assert.throws(() => parsePastedCredentials("openai", blob), /not supported|allowlist|supported/i);
});
test("rejects a blob whose embedded provider does not match the route provider", () => {
// Blob minted for antigravity, replayed against the agy route → must reject.
const blob = encodeCredentialBlob({ provider: "antigravity", tokens });
assert.throws(() => parsePastedCredentials("agy", blob), /match|mismatch|provider/i);
});
test("propagates codec validation errors (e.g. missing access_token)", () => {
const blob = encodeCredentialBlob({ provider: "antigravity", tokens: { refresh_token: "r" } });
assert.throws(() => parsePastedCredentials("antigravity", blob), /access_token|token/i);
});

View File

@@ -637,11 +637,19 @@ test("Gemini and Antigravity run mocked browser OAuth exchanges and post-exchang
);
const antigravityExtra = await PROVIDERS.antigravity.postExchange(antigravityTokens);
const antigravityMapped = PROVIDERS.antigravity.mapTokens(antigravityTokens, antigravityExtra);
// postExchange runs onboarding fire-and-forget now (it must never block the OAuth
// login response); give the background onboard call a tick to consume its mocked
// fetch so the sequence drains deterministically.
await new Promise((r) => setTimeout(r, 50));
assert.equal(geminiMapped.email, "gemini@example.com");
assert.equal(geminiMapped.projectId, "gemini-project");
assert.equal(antigravityMapped.email, "anti@example.com");
assert.equal(antigravityMapped.projectId, "anti-project-final");
// projectId comes from loadCodeAssist ("anti-project"), NOT the backgrounded
// onboardUser response ("anti-project-final"). Onboarding is fire-and-forget, so it
// no longer updates the returned projectId synchronously — matching the 9router web
// flow, which also returns the loadCodeAssist project id.
assert.equal(antigravityMapped.projectId, "anti-project");
});
test("Qoder enabled mode exchanges tokens and loads profile metadata through mocked endpoints", async () => {

View File

@@ -0,0 +1,79 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
// Regression guard for Hard Rule: PII redaction/sanitization is OPT-IN.
// OmniRoute proxies for self-hosted / local LLMs where the operator owns the
// data; mutating request/response payloads by default would silently corrupt
// that traffic. The two data-mutating PII feature flags MUST default to "false"
// so a vanilla chat request passes data through untouched. Flipping either
// default to "true" requires explicit operator approval — this test is the
// permanent guard against an accidental on-by-default regression.
//
// See docs/security/GUARDRAILS.md and the PII analysis: piiMasker (request),
// piiSanitizer (response), streamingPiiTransform (SSE) are ALL gated on these
// two flags; with both off the guardrail runs but never mutates payloads.
// Isolate DB state so the resolution chain (DB override > env > default) reads
// a clean store and we exercise the definition default, not a leaked override.
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-test-pii-default-"));
process.env.DATA_DIR = tmpDir;
const { FEATURE_FLAG_DEFINITIONS } = await import(
"../../src/shared/constants/featureFlagDefinitions.ts"
);
test("PII data-mutation flags are opt-in (default 'false')", async (t) => {
const def = (key: string) => FEATURE_FLAG_DEFINITIONS.find((d) => d.key === key);
await t.test("PII_REDACTION_ENABLED definition default is 'false'", () => {
const d = def("PII_REDACTION_ENABLED");
assert.ok(d, "PII_REDACTION_ENABLED definition must exist");
assert.strictEqual(
d!.defaultValue,
"false",
"PII_REDACTION_ENABLED must default OFF — request-side masking is opt-in (operator owns the data)"
);
});
await t.test("PII_RESPONSE_SANITIZATION definition default is 'false'", () => {
const d = def("PII_RESPONSE_SANITIZATION");
assert.ok(d, "PII_RESPONSE_SANITIZATION definition must exist");
assert.strictEqual(
d!.defaultValue,
"false",
"PII_RESPONSE_SANITIZATION must default OFF — response/streaming masking is opt-in"
);
});
await t.test("effective runtime resolution is OFF with no override", async () => {
// No env var, no DB override → the definition default must win.
delete process.env.PII_REDACTION_ENABLED;
delete process.env.PII_RESPONSE_SANITIZATION;
const { clearAllFeatureFlagOverrides } = await import("@/lib/db/featureFlags");
clearAllFeatureFlagOverrides();
const { isFeatureFlagEnabled } = await import("@/shared/utils/featureFlags");
assert.strictEqual(isFeatureFlagEnabled("PII_REDACTION_ENABLED"), false);
assert.strictEqual(isFeatureFlagEnabled("PII_RESPONSE_SANITIZATION"), false);
});
await t.test("response data passes through untouched by default", async () => {
delete process.env.PII_RESPONSE_SANITIZATION;
const { clearAllFeatureFlagOverrides } = await import("@/lib/db/featureFlags");
clearAllFeatureFlagOverrides();
const { sanitizePII, sanitizePIIResponse } = await import("@/lib/piiSanitizer");
const text = "contact me at jdoe@example.com or 123-45-6789";
const result = sanitizePII(text);
assert.strictEqual(result.redacted, false, "must NOT redact when flag is off");
assert.strictEqual(result.text, text, "PII text must pass through unchanged by default");
const body = { choices: [{ message: { content: "ssn 123-45-6789, email a@b.com" } }] };
const out = sanitizePIIResponse(JSON.parse(JSON.stringify(body)));
assert.deepStrictEqual(out, body, "response object must pass through unchanged by default");
});
});

View File

@@ -7,6 +7,7 @@ import {
proxyConfigToUrl,
normalizeProxyUrl,
clearDispatcherCache,
__cacheProxyDispatcherForTest,
} from "../../open-sse/utils/proxyDispatcher.ts";
afterEach(() => clearDispatcherCache());
@@ -77,4 +78,22 @@ describe("proxyDispatcher connection pool", () => {
});
assert.equal(options.connections, 256);
});
it("closes cached proxy dispatchers when the proxy cache is cleared", () => {
let closeCount = 0;
const dispatcher = {
dispatch() {
return true;
},
close() {
closeCount += 1;
},
destroy() {},
};
__cacheProxyDispatcherForTest("http://proxy.example.com:8080", dispatcher as never);
clearDispatcherCache();
assert.equal(closeCount, 1);
});
});

View File

@@ -127,6 +127,73 @@ test("request log detail labels OpenAI protocol variants explicitly", () => {
assert.notEqual(responsesHtml.indexOf(">OpenAI-Responses<"), -1);
});
test("request log detail compression-summary badge shows positive saved%, never negative", () => {
// Regression: prior code used `(-{pct}%)` which produced literal "-100%" when the entire
// prompt was compressed (compressed=5286, totalIn=0). The fix clamps pct to [0, 100] and
// uses "(N% saved)" so the user-facing label is always positive.
const make = (tokensIn: number, tokensCompressed: number) =>
renderToStaticMarkup(
React.createElement(RequestLoggerDetail, {
log: {
status: 200,
method: "POST",
path: "/v1/chat/completions",
timestamp: "2026-04-09T21:27:08.000Z",
duration: 1500,
provider: "openai-compatible-sp-openai",
sourceFormat: "openai-chat",
model: "gpt-5.4",
requestedModel: "openai-compatible-sp-openai/gpt-5.4",
cacheSource: "semantic",
tokens: {
in: tokensIn,
out: 42,
cacheRead: null,
cacheWrite: null,
reasoning: null,
compressed: tokensCompressed,
},
},
detail: {
tokens: {
in: tokensIn,
out: 42,
cacheRead: null,
cacheWrite: null,
reasoning: null,
compressed: tokensCompressed,
},
},
loading: false,
onClose: () => {},
onCopy: async () => true,
})
);
// Original bug repro: totalIn=0, compressed=5286 → previously rendered "(100%)".
const fullyCompressed = make(0, 5286);
assert.match(fullyCompressed, /Compressed: 5,286 → 0 \(100% saved\)/);
assert.equal(
fullyCompressed.includes("(-100%)"),
false,
"literal '(-100%)' must never appear"
);
assert.equal(
fullyCompressed.includes("\u2212100%"),
false,
"unicode minus + 100% must never appear"
);
// Half-compressed case must clamp cleanly inside the [0, 100] window.
const halfCompressed = make(2500, 2500);
assert.match(halfCompressed, /Compressed: 5,000 → 2,500 \(50% saved\)/);
assert.equal(halfCompressed.includes("-50%"), false);
// Sanity: tiny input, tiny savings still rendered as a positive percentage.
const small = make(1000, 100);
assert.match(small, /Compressed: 1,100 → 1,000 \(9% saved\)/);
});
test("request log detail follows the email visibility setting for accounts", () => {
const props = {
log: {

View File

@@ -5,6 +5,8 @@ const { convertResponsesApiFormat } =
await import("../../open-sse/translator/helpers/responsesApiHelper.ts");
const { openaiResponsesToOpenAIRequest, openaiToOpenAIResponsesRequest } =
await import("../../open-sse/translator/request/openai-responses.ts");
const { normalizeCodexResponsesInput, normalizeResponsesInputForChat } =
await import("../../open-sse/utils/responsesInputNormalization.ts");
test("convertResponsesApiFormat filters orphaned function_call_output items", () => {
const body = {
@@ -57,6 +59,77 @@ test("Responses→Chat: input_image converted to image_url with detail", () => {
assert.equal(imgPart.image_url.detail, "high");
});
test("Responses→Chat: string input becomes a user message instead of an empty prompt", () => {
const result = openaiResponsesToOpenAIRequest(
null,
{ model: "gpt-4", input: "Responda apenas: OK", max_output_tokens: 80 },
null,
null
);
assert.equal((result as any).input, undefined);
assert.equal((result as any).messages.length, 1);
assert.equal((result as any).messages[0].role, "user");
assert.deepEqual((result as any).messages[0].content, [
{ type: "text", text: "Responda apenas: OK" },
]);
});
test("Responses→Chat: object input becomes a single user message", () => {
const result = openaiResponsesToOpenAIRequest(
null,
{ model: "gpt-4", input: { text: "Ping" } },
null,
null
);
assert.equal((result as any).messages.length, 1);
assert.equal((result as any).messages[0].role, "user");
assert.deepEqual((result as any).messages[0].content, [{ type: "text", text: "Ping" }]);
});
test("Responses→Chat: role/content object input becomes a single user message", () => {
const result = openaiResponsesToOpenAIRequest(
null,
{ model: "gpt-4", input: { role: "user", content: "Ping" } },
null,
null
);
assert.equal((result as any).messages.length, 1);
assert.equal((result as any).messages[0].role, "user");
assert.equal((result as any).messages[0].content, "Ping");
});
test("Codex Responses input: string input becomes a list-shaped user message", () => {
const body: Record<string, unknown> = { input: "ship it" };
normalizeCodexResponsesInput(body);
assert.deepEqual(body.input, [
{ type: "message", role: "user", content: [{ type: "input_text", text: "ship it" }] },
]);
});
test("Codex Responses input: object input becomes a single item", () => {
const body: Record<string, unknown> = { input: { role: "user", text: "ship it" } };
normalizeCodexResponsesInput(body);
assert.deepEqual(body.input, [
{ type: "message", role: "user", content: [{ type: "input_text", text: "ship it" }] },
]);
});
test("Codex Responses input: null input normalizes to an empty list (not [null])", () => {
const body: Record<string, unknown> = { input: null };
normalizeCodexResponsesInput(body);
assert.deepEqual(body.input, []);
});
test("Responses→Chat: null input normalizes to an empty list (not [null])", () => {
assert.deepEqual(normalizeResponsesInputForChat(null), []);
});
test("Responses→Chat: input_image without detail omits detail field", () => {
const body = {
model: "gpt-4",

View File

@@ -99,6 +99,35 @@ test("normalizeSystemRole inserts a user message when no user exists and drops e
]);
});
test("normalizeSystemRole treats ZenMux z-ai/glm models as GLM even with vendor prefix", () => {
const messages = [
{ role: "system", content: "[Context compressed: earlier messages removed]" },
{
role: "assistant",
content: null,
tool_calls: [
{
id: "call_1",
type: "function",
function: { name: "read", arguments: "{}" },
},
],
},
{ role: "tool", tool_call_id: "call_1", content: "ok" },
];
const result = normalizeSystemRole(messages, "zenmux", "z-ai/glm-5.2");
assert.deepEqual(result, [
{
role: "user",
content: "[System Instructions]\n[Context compressed: earlier messages removed]",
},
messages[1],
messages[2],
]);
});
test("normalizeRoles composes model, developer and system normalization in order", () => {
const messages = [
{ role: "model", content: "first answer" },

View File

@@ -0,0 +1,43 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
// run-next.mjs has top-level side effects (bootstrapEnv, native sqlite, server
// listen) so it cannot be imported in-process. Guard the fix by inspecting the
// source: the dev/start server MUST force NODE_ENV to match the run mode, after
// the mergedEnv copy loop (which pulls NODE_ENV=production from .env/.env.example)
// and before next() is constructed. Otherwise `npm run dev` boots the dev
// bundler with NODE_ENV=production, which breaks Next's webpack CSS pipeline and
// fails globals.css with "Unexpected character '@'" on `@import "tailwindcss"`.
const here = path.dirname(fileURLToPath(import.meta.url));
const source = fs.readFileSync(path.resolve(here, "../../scripts/dev/run-next.mjs"), "utf8");
// Quote- and whitespace-tolerant so legitimate Prettier/formatting changes
// don't trip the guard; only a real change to the assignment shape should.
const NODE_ENV_NORMALIZE =
/process\.env\.NODE_ENV\s*=\s*dev\s*\?\s*['"]development['"]\s*:\s*['"]production['"]/;
test("run-next.mjs forces NODE_ENV to track the run mode", () => {
assert.match(
source,
NODE_ENV_NORMALIZE,
"expected NODE_ENV to be forced to development in dev mode, production otherwise"
);
});
test("NODE_ENV normalization runs after the merged-env copy and before next()", () => {
const copyLoopIdx = source.search(/process\.env\[key\]\s*=\s*value/);
const normalizeIdx = source.search(NODE_ENV_NORMALIZE);
const nextCallIdx = source.search(/\bnext\s*\(\s*\{/);
assert.ok(copyLoopIdx !== -1, "expected the mergedEnv copy loop to exist");
assert.ok(normalizeIdx !== -1, "expected the NODE_ENV normalization to exist");
assert.ok(nextCallIdx !== -1, "expected the next() construction to exist");
assert.ok(
normalizeIdx > copyLoopIdx,
"NODE_ENV must be normalized AFTER the env copy loop (else .env overwrites it)"
);
assert.ok(normalizeIdx < nextCallIdx, "NODE_ENV must be normalized BEFORE next() reads it");
});

View File

@@ -0,0 +1,123 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import vm from "node:vm";
type FetchEvent = {
request: RequestLike;
respondWith: (response: Promise<Response>) => void;
};
type ServiceWorkerEvent = FetchEvent | Record<string, unknown>;
type RequestLike = {
url: string;
method: string;
mode: string;
destination: string;
};
function createServiceWorkerHarness() {
const cacheEntries = new Map<string, Response>();
const listeners = new Map<string, (event: ServiceWorkerEvent) => void>();
let fetchImpl: (request: RequestLike) => Promise<Response> = async () => {
throw new Error("network unavailable");
};
const cache = {
addAll: async (urls: string[]) => {
for (const url of urls) {
cacheEntries.set(url, new Response(`cached ${url}`, { status: 200 }));
}
},
delete: async (request: Request) => cacheEntries.delete(request.url),
keys: async () => [...cacheEntries.keys()].map((url) => new Request(url)),
put: async (request: Request, response: Response) => {
cacheEntries.set(request.url, response);
},
};
const caches = {
delete: async () => true,
keys: async () => ["omniroute-pwa-v2"],
match: async (request: Request | string) =>
cacheEntries.get(typeof request === "string" ? request : request.url),
open: async () => cache,
};
const context = vm.createContext({
URL,
Request,
Response,
caches,
fetch: (request: RequestLike) => fetchImpl(request),
self: {
clients: { claim: async () => undefined },
location: { href: "https://app.example/sw.js", origin: "https://app.example" },
registration: { showNotification: async () => undefined },
skipWaiting: async () => undefined,
addEventListener: (type: string, listener: (event: ServiceWorkerEvent) => void) => {
listeners.set(type, listener);
},
},
});
vm.runInContext(readFileSync("public/sw.js", "utf8"), context);
return {
cacheEntries,
dispatchFetch: async (request: RequestLike) => {
const listener = listeners.get("fetch");
assert.ok(listener, "fetch listener must be registered");
let responsePromise: Promise<Response> | undefined;
const event: FetchEvent = {
request,
respondWith: (response) => {
responsePromise = response;
},
};
listener(event);
assert.ok(responsePromise, "navigate request must call respondWith");
return responsePromise;
},
setFetch: (nextFetch: (request: Request) => Promise<Response>) => {
fetchImpl = nextFetch;
},
};
}
test("#5165: service worker returns cached navigation before offline page", async () => {
const harness = createServiceWorkerHarness();
const request = {
url: "https://app.example/dashboard",
method: "GET",
mode: "navigate",
destination: "document",
};
harness.cacheEntries.set(request.url, new Response("cached dashboard", { status: 200 }));
harness.cacheEntries.set("/offline", new Response("offline page", { status: 200 }));
const response = await harness.dispatchFetch(request);
assert.equal(await response.text(), "cached dashboard");
});
test("#5165: successful navigations are cached for later transient failures", async () => {
const harness = createServiceWorkerHarness();
const request = {
url: "https://app.example/dashboard",
method: "GET",
mode: "navigate",
destination: "document",
};
harness.setFetch(async () => new Response("fresh dashboard", { status: 200 }));
assert.equal(await (await harness.dispatchFetch(request)).text(), "fresh dashboard");
harness.setFetch(async () => {
throw new Error("transient network failure");
});
assert.equal(await (await harness.dispatchFetch(request)).text(), "fresh dashboard");
});

View File

@@ -34,9 +34,24 @@ test("#3884 streaming request still injects stream_options.include_usage", () =>
const executor = new DefaultExecutor("openai");
const body = { model: "gpt-4.1", messages: [{ role: "user", content: "hi" }] };
const result = executor.transformRequest("gpt-4.1", body, true, {}) as Record<string, unknown>;
assert.equal(result.stream, true);
assert.deepEqual(result.stream_options, { include_usage: true });
});
test("#3884 internal streaming strips stream_options when body explicitly disables stream", () => {
const executor = new DefaultExecutor("openai-compatible-deepseek");
const body = {
model: "deepseek-chat",
messages: [{ role: "user", content: "hi" }],
stream: false,
};
const result = executor.transformRequest("deepseek-chat", body, true, {
providerSpecificData: { baseUrl: "https://proxy.example/v1" },
}) as Record<string, unknown>;
assert.equal(result.stream, false);
assert.equal(result.stream_options, undefined);
});
test("#3884 non-streaming request without stream_options stays clean", () => {
const executor = new DefaultExecutor("openai");
const body = { model: "gpt-4.1", messages: [{ role: "user", content: "hi" }] };

View File

@@ -5,6 +5,7 @@ import {
isProxyReachable,
getCachedProxyHealth,
invalidateProxyHealth,
__setProxyHealthTcpCheckForTesting,
} from "../../src/lib/proxyHealth.ts";
import { runWithProxyContext } from "../../open-sse/utils/proxyFetch.ts";
@@ -17,6 +18,37 @@ test("T14: isProxyReachable caches unreachable proxy result", async () => {
assert.equal(getCachedProxyHealth(proxyUrl), false);
});
test("#5109: concurrent proxy reachability checks share one TCP probe", async () => {
const proxyUrl = "http://127.0.0.1:1080";
invalidateProxyHealth(proxyUrl);
let probeCount = 0;
let releaseProbe!: (healthy: boolean) => void;
const probeStarted = new Promise<void>((resolve) => {
__setProxyHealthTcpCheckForTesting(async () => {
probeCount += 1;
resolve();
return new Promise<boolean>((resolveProbe) => {
releaseProbe = resolveProbe;
});
});
});
try {
const checks = Array.from({ length: 50 }, () => isProxyReachable(proxyUrl, 120, 2_000));
await probeStarted;
assert.equal(probeCount, 1, "concurrent requests must not fan out TCP health probes");
releaseProbe(true);
assert.deepEqual(await Promise.all(checks), Array.from({ length: 50 }, () => true));
assert.equal(getCachedProxyHealth(proxyUrl), true);
} finally {
__setProxyHealthTcpCheckForTesting(null);
invalidateProxyHealth(proxyUrl);
}
});
test("T14: runWithProxyContext fast-fails when proxy is unreachable", async () => {
const proxyUrl = "http://127.0.0.1:1";
invalidateProxyHealth(proxyUrl);
@@ -28,7 +60,7 @@ test("T14: runWithProxyContext fast-fails when proxy is unreachable", async () =
executed = true;
return "ok";
}),
(err) => (err as any).code === "PROXY_UNREACHABLE"
(err) => (err as { code?: string })?.code === "PROXY_UNREACHABLE"
);
assert.equal(executed, false);

View File

@@ -0,0 +1,41 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
// Regression guard for the escalated mesh report "Nobody solve this color issue 🤫":
// the update-step status icon for the `warning` state used a bare `text-yellow-500`
// (Tailwind #eab308, no dark: variant) while its `done`/`failed` siblings use the
// vivid `text-green-500` / `text-red-500`. yellow-500 has poor contrast on light
// backgrounds (~1.9:1, fails WCAG) and is inconsistent with the project's warning
// convention, which is `amber` everywhere else in this same component (e.g. the
// `bg-amber-500/10 text-amber-500` badge). The warning icon must use amber, not yellow.
const here = dirname(fileURLToPath(import.meta.url));
const source = readFileSync(
resolve(here, "../../../src/app/(dashboard)/dashboard/HomePageClient.tsx"),
"utf8"
);
// Isolate the update-step status-icon block: the `warning` material symbol icon span.
// Match the className on the span immediately preceding the `warning` glyph.
const warningIconMatch = source.match(
/className="material-symbols-outlined ([^"]*)"[^>]*>\s*warning\s*</
);
test("update-step warning icon renders with an amber color, not bare yellow", () => {
assert.ok(
warningIconMatch,
"expected to find the update-step `warning` material-symbols icon span in HomePageClient.tsx"
);
const className = warningIconMatch![1];
assert.ok(
className.includes("text-amber-500"),
`warning icon should use the project's amber warning convention; got: "${className}"`
);
assert.ok(
!className.includes("text-yellow-500"),
"warning icon must not use the low-contrast bare `text-yellow-500` (no dark variant, fails WCAG on light backgrounds)"
);
});

View File

@@ -0,0 +1,62 @@
// @vitest-environment jsdom
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { createRoot, type Root } from "react-dom/client";
import { act } from "react";
import { EncoderComparisonTable } from "@/app/(dashboard)/dashboard/compression/studio/EncoderComparisonTable";
import type { EncoderComparison } from "@/app/(dashboard)/dashboard/compression/studio/compressionFlowModel";
const cmp: EncoderComparison = {
arraysCompared: 1,
json: { bytes: 400, tokens: 120 },
gcf: { bytes: 150, tokens: 40 },
toon: { bytes: 170, tokens: 48 },
toonAvailable: true,
winner: "gcf",
};
let container: HTMLElement;
let root: Root;
beforeEach(() => {
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(() => {
act(() => root.unmount());
container.remove();
document.body.innerHTML = "";
});
describe("EncoderComparisonTable", () => {
it("renderiza GCF/TOON/JSON com tokens e marca o vencedor", () => {
act(() => {
root.render(<EncoderComparisonTable comparison={cmp} />);
});
const text = container.textContent ?? "";
expect(text).toMatch(/GCF/);
expect(text).toMatch(/TOON/);
expect(text).toMatch(/JSON/);
// tokens rendered
expect(text).toContain("40");
expect(text).toContain("48");
expect(text).toContain("120");
expect(container.querySelector('[data-testid="encoder-winner"]')?.textContent).toMatch(/gcf/i);
});
it("mostra TOON como n/a quando indisponível", () => {
act(() => {
root.render(
<EncoderComparisonTable comparison={{ ...cmp, toonAvailable: false, winner: "gcf" }} />
);
});
expect(container.querySelector('[data-testid="encoder-toon-na"]')).toBeTruthy();
});
it("não renderiza nada quando não há array comparado", () => {
act(() => {
root.render(<EncoderComparisonTable comparison={{ ...cmp, arraysCompared: 0 }} />);
});
expect(container.firstChild).toBeNull();
});
});

View File

@@ -32,11 +32,16 @@ test.after(() => {
// ── helpers ──────────────────────────────────────────────────────────────────
let entrySeq = 0;
function makeEntry(overrides: Record<string, unknown> = {}) {
entrySeq++;
const timestamp = new Date(Date.now() + entrySeq).toISOString();
return {
provider: "test-provider",
model: "test-model",
connectionId: "conn-abc123",
connectionId: `conn-abc123-${entrySeq}`,
apiKeyId: null,
apiKeyName: null,
tokens: { input_tokens: 10, output_tokens: 20 },
@@ -47,7 +52,7 @@ function makeEntry(overrides: Record<string, unknown> = {}) {
errorCode: null,
comboStrategy: null,
endpoint: "/v1/chat/completions",
timestamp: new Date().toISOString(),
timestamp,
...overrides,
};
}

View File

@@ -1,6 +1,6 @@
import test from "node:test";
import assert from "assert";
import { validateResponseQuality } from "../../open-sse/services/combo";
import { validateResponseQuality } from "../../open-sse/services/combo/validateQuality.ts";
function makeResponse(body: string, contentType = "text/plain") {
return {
@@ -25,3 +25,28 @@ test("returns valid=false for non-JSON non-SSE text", async () => {
const res = await validateResponseQuality(makeResponse("Hello world"), false, {});
assert.strictEqual(res.valid, false);
});
test("returns valid=false for Responses API bodies with no output items", async () => {
const res = await validateResponseQuality(
makeResponse(JSON.stringify({ object: "response", status: "completed", output: [] }), "application/json"),
false,
{}
);
assert.strictEqual(res.valid, false);
});
test("returns valid=true for Responses API bodies with structural output", async () => {
const res = await validateResponseQuality(
makeResponse(
JSON.stringify({
object: "response",
status: "completed",
output: [{ type: "function_call", name: "lookup", arguments: "{}" }],
}),
"application/json"
),
false,
{}
);
assert.strictEqual(res.valid, true);
});

View File

@@ -9,23 +9,16 @@
import test from "node:test";
import assert from "node:assert/strict";
const { budgetToEffort, applyThinking } = await import(
"../../src/lib/providers/xai/thinking.ts"
);
const { chatRequestToXaiResponses, xaiCompletedToChatJson } = await import(
"../../src/lib/providers/xai/translators/openai-chat.ts"
);
const {
openaiResponsesRequestToXai,
xaiCompletedToOpenaiResponses,
xaiSseEventToOpenaiResponses,
} = await import("../../src/lib/providers/xai/translators/openai-responses.ts");
const { claudeRequestToXaiResponses, xaiCompletedToClaudeJson } = await import(
"../../src/lib/providers/xai/translators/claude.ts"
);
const { geminiRequestToXaiResponses, xaiCompletedToGeminiJson } = await import(
"../../src/lib/providers/xai/translators/gemini.ts"
);
const { budgetToEffort, applyThinking, normalizeXaiReasoningEffort } =
await import("../../src/lib/providers/xai/thinking.ts");
const { chatRequestToXaiResponses, xaiCompletedToChatJson } =
await import("../../src/lib/providers/xai/translators/openai-chat.ts");
const { openaiResponsesRequestToXai, xaiCompletedToOpenaiResponses, xaiSseEventToOpenaiResponses } =
await import("../../src/lib/providers/xai/translators/openai-responses.ts");
const { claudeRequestToXaiResponses, xaiCompletedToClaudeJson } =
await import("../../src/lib/providers/xai/translators/claude.ts");
const { geminiRequestToXaiResponses, xaiCompletedToGeminiJson } =
await import("../../src/lib/providers/xai/translators/gemini.ts");
// ─── budgetToEffort ──────────────────────────────────────────────────────────
@@ -67,6 +60,21 @@ test("applyThinking: honors xAI-native reasoning.effort verbatim", () => {
assert.equal((out as Record<string, unknown>).foo, 1);
});
test("normalizeXaiReasoningEffort: downgrades max/xhigh to xAI-supported high", () => {
assert.equal(normalizeXaiReasoningEffort("max"), "high");
assert.equal(normalizeXaiReasoningEffort("xhigh"), "high");
assert.equal(normalizeXaiReasoningEffort("HIGH"), "high");
assert.equal(normalizeXaiReasoningEffort("ultra"), undefined);
});
test("applyThinking: normalizes xAI-native max/xhigh to high", () => {
const maxOut = applyThinking({ reasoning: { effort: "max", summary: "auto" } });
assert.deepStrictEqual(maxOut.reasoning, { effort: "high", summary: "auto" });
const xhighOut = applyThinking({ reasoning: { effort: "xhigh" } });
assert.deepStrictEqual(xhighOut.reasoning, { effort: "high" });
});
test("applyThinking: rewrites OpenAI Chat reasoning_effort into reasoning.effort", () => {
const req = { reasoning_effort: "medium" };
const out = applyThinking(req);
@@ -74,6 +82,13 @@ test("applyThinking: rewrites OpenAI Chat reasoning_effort into reasoning.effort
assert.equal(out.reasoning_effort, undefined);
});
test("applyThinking: rewrites OpenAI Chat max reasoning_effort into high", () => {
const req = { reasoning_effort: "max" };
const out = applyThinking(req);
assert.deepStrictEqual(out.reasoning, { effort: "high" });
assert.equal(out.reasoning_effort, undefined);
});
test("applyThinking: ignores invalid reasoning_effort values", () => {
const req = { reasoning_effort: "ultra" };
const out = applyThinking(req);
@@ -138,9 +153,7 @@ test("chatRequestToXaiResponses: converts system message to instructions", () =>
test("chatRequestToXaiResponses: converts tool message to function_call_output", () => {
const req = {
model: "grok-4",
messages: [
{ role: "tool", content: "result text", tool_call_id: "call_abc" },
],
messages: [{ role: "tool", content: "result text", tool_call_id: "call_abc" }],
};
const out = chatRequestToXaiResponses(req);
assert.equal(out.input[0].type, "function_call_output");
@@ -154,6 +167,18 @@ test("chatRequestToXaiResponses: promotes reasoning_effort to reasoning field",
assert.deepStrictEqual(out.reasoning, { effort: "high" });
});
test("chatRequestToXaiResponses: normalizes max reasoning_effort for xAI", () => {
const req = { model: "grok-4.3", messages: [], reasoning_effort: "max" };
const out = chatRequestToXaiResponses(req);
assert.deepStrictEqual(out.reasoning, { effort: "high" });
});
test("chatRequestToXaiResponses: normalizes reasoning.effort for xAI", () => {
const req = { model: "grok-4.3", messages: [], reasoning: { effort: "max", summary: "auto" } };
const out = chatRequestToXaiResponses(req);
assert.deepStrictEqual(out.reasoning, { effort: "high", summary: "auto" });
});
test("chatRequestToXaiResponses: maps max_tokens to max_output_tokens", () => {
const req = { model: "grok-4", messages: [], max_tokens: 512 };
const out = chatRequestToXaiResponses(req);