mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 20:32:20 +03:00
* chore(config): ignore additional agent workflow command files Add newly introduced agent workflow and Claude command files to .gitignore so proprietary automation assets are not committed. * feat(deepseek-web): fix auth to use userToken + WASM PoW solver Rewrite deepseek-web executor from broken cookie auth to userToken Bearer flow (like Chat2API). Replace pure JS Keccak PoW with WASM solver (5.8s → 86ms). Add 14 models, validation, and dashboard UX. * fix(deepseek-web): update target_path to use challenge property * refactor(deepseek-web): streamline token handling and implement cache eviction * fix(deepseek-web): fix SSE parser, prompt format, and error handling - Handle all 3 DeepSeek SSE stream formats: initial fragments, APPEND operations, and bare string tokens (fixes truncated responses) - Simplify prompt builder to send system + last user message only (DeepSeek web API is single-turn, full history caused marker leakage) - Check json.code before token extraction (fixes "did not return access token: Authorization" on code 40003 with HTTP 200) - Clear session cache alongside token cache on auth errors - Add dev origin for remote testing Co-authored-by: Cursor <cursoragent@cursor.com> * chore: ignore memory-bank and cursor agent rules from tracking Co-authored-by: Cursor <cursoragent@cursor.com> * feat: enhance documentation and configuration for Fumadocs integration - Added Fumadocs MDX support in the Next.js configuration. - Updated transpile packages to include fumadocs-ui and fumadocs-core. - Implemented a comprehensive set of redirects for documentation paths to improve navigation. - Removed the generate-docs-index script as it is no longer needed. - Updated various documentation titles for consistency and clarity. - Enhanced global styles to incorporate Fumadocs UI themes and styles. * refactor(docs): cleanup fumadocs PR — revert deepseek, add i18n fallback, restore LanguageSelector - Revert unrelated deepseek-web.ts changes (should be separate PR) - Add .source/ to .gitignore (Fumadocs generated files) - Remove contributor IP from allowedDevOrigins - Add i18n runtime fallback: reads NEXT_LOCALE cookie, loads translated .md from docs/i18n/<locale>/docs/ (preserves existing translation pipeline) - Restore LanguageSelector in Fumadocs layout nav - Restore SEO metadata (title template, description, robots) * fix(codex): use allowlist to strip non-Responses-API fields in non-passthrough path (#2608) (#2615) Integrated into release/v3.8.3 — fix(codex): allowlist-based sanitization for gpt-5.5 Responses API * fix(deepseek-web): fix SSE parser, prompt format, error handling, and cache keys (#2616) Integrated into release/v3.8.3 — fix(deepseek-web): SSE parser (APPEND + bare tokens), prompt builder, error handling, session cache cleanup * chore(config): ignore additional agent workflow command files Add newly introduced agent workflow and Claude command files to .gitignore so proprietary automation assets are not committed. * feat(docs): migrate /docs to Fumadocs MDX with nested routes (#2614) Integrated into release/v3.8.3 — Fumadocs MDX migration with nested routes, search API, and 50+ URL redirects * fix(catalog): skip static PROVIDER_MODELS when synced models exist (#2625) Integrated into release/v3.8.3 * fix(qoder): Cosy auth fallback for PAT tokens + vision support for qwen3-vl-plus (#2629) Integrated into release/v3.8.3 * fix(cli): register tsx loader and add opencode config subcommand (#2631) Integrated into release/v3.8.3 * feat(dashboard): add search and filters to /dashboard/api-manager (#2628) Integrated into release/v3.8.3 * fix(claude): improve Pi and OpenCode compatibility (#2621) Integrated into release/v3.8.3 * fix: restore semantic passthrough system-role-only extraction instead of full normalization (#2620) Integrated into release/v3.8.3 * fix(kiro): stabilize conversationId across prompt compression (#2630) Integrated into release/v3.8.3 * fix(deepseek-web): SSE thinking/search routing and session lifecycle (#2624) Integrated into release/v3.8.3 — DeepSeek Web SSE thinking/search routing overhaul * feat(dashboard): free-tier grouping with symbolic link in /providers (#2632) Integrated into release/v3.8.3 * fix: close implementation gaps — t3-chat-web, stream_options, combo_strategy, batch config (#2634) Integrated into release/v3.8.3 * feat(dashboard): risk notice modal for sensitive providers (#2633) Integrated into release/v3.8.3 * fix(reasoning): extend reasoning_content injection to Kimi K2 and other replay models (#2639) Integrated into release/v3.8.3 * fix(cli): Linux autostart via systemd user service (fixes #2627) (#2635) Integrated into release/v3.8.3 * Refactor/providers free tier (#2640) Integrated into release/v3.8.3 * fix(tests): remove duplicate assertion in schema coercion & fix(cli): ignore system vars in env check * fix(combo): preserve omniModel tag in streaming output for round-trip context pinning (#2646) Integrated into release/v3.8.3 * feat(dashboard): media providers pages + Web Fetch category (#2645) Integrated into release/v3.8.3 * Feature provider adapta org com tutorial de conexão em modal (#2643) Integrated into release/v3.8.3 * fix(rtk): skip content-based filter matching for non-shell tool results (#2642) Integrated into release/v3.8.3 * fix(translator): enable Claude extended thinking for Copilot Responses-API requests (#2647) Integrated into release/v3.8.3 * feat(dashboard): add search and filters to /dashboard/api-manager (#2641) Integrated into release/v3.8.3 * feat(dashboard): risk notice modal for sensitive providers (#2638) Integrated into release/v3.8.3 * feat(dashboard): mini-playground inline (Phase 4) (#2648) Integrated into release/v3.8.3 * fix(settings): fix Require Login modal Cancel button text and dismissal (#2649) Integrated into release/v3.8.3 * feat(combos): universal context handoff for cross-model conversation continuity (#2653) Integrated into release/v3.8.3 * chore(release): bump to v3.8.3 — changelog, docs, version sync * feat(i18n): complete zh-CN translations for 1220 missing keys (#2655) Integrated into release/v3.8.3 * chore(release): include electron package changes in v3.8.3 * docs(changelog): integrate PR #2655 into v3.8.3 * feat(i18n): translate 377 additional zh-CN entries (81 new keys + 296 same-as-en) (#2659) Integrated into release/v3.8.3 * feat(dashboard): add Cmd+K / Ctrl+K command palette for sidebar navigation (#2656) Integrated into release/v3.8.3 * docs: update changelog for PR integrations under v3.8.3 * feat(cli): integrate native updates, autostart and headless CLI mode (#2662) Integrated into release/v3.8.3 * fix(proxy): save dashboard custom proxies in registry (#2661) Integrated into release/v3.8.3 * feat(dashboard): chat-first test slide-over (Option A) (#2660) Integrated into release/v3.8.3 * docs: update changelog with Batch 2 PR merges for v3.8.3 * fix: add xhigh+max to effortLevel schema; add opencode-plugin publish job (#2666) Integrated into release/v3.8.3 * docs: update changelog with Batch 3 PR #2666 merge for v3.8.3 * feat(quota+providers): card-grid layout, provider group headers, Codex race fix (#2667) Integrated into release/v3.8.3 * feat(dashboard): real-time live WebSocket monitoring (#2668) Integrated into release/v3.8.3 * feat(copilot): AI assistant with CodeGraph + CLI + knowledge base (#2669) Integrated into release/v3.8.3 * feat(pipeline): pre-request middleware hooks (#2670) Integrated into release/v3.8.3 * feat(resilience): credential health check + adaptive circuit breaker (#2671) Integrated into release/v3.8.3 * feat(playground): combo routing visual simulator (#2672) Integrated into release/v3.8.3 * feat(auth): API key groups with model-level permissions (#2673) Integrated into release/v3.8.3 * feat(pwa): enhanced manifest + push notification support (#2674) Integrated into release/v3.8.3 * feat(proxy): serverless relay endpoints with rate limiting (#2675) Integrated into release/v3.8.3 * docs(changelog): update changelog for PRs 2667-2675 & fix: resolve typescript compile-time errors * fix(db): remove transactions from migrations Remove explicit transaction wrappers from recent migrations and correct the API key groups migration metadata. Also fix codegraph path resolution for ESM environments and refresh generated fumadocs source output. --------- Co-authored-by: Ömer Vehbe <ovehbe@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Mr. Meowgi <mr@meowgi.dev> Co-authored-by: Hernan Javier Ardila Sanchez <hjasgr@gmail.com> Co-authored-by: amogus22877769 <y.lev357@gmail.com> Co-authored-by: Halil Tezcan KARABULUT <info@hlltzcnkb.com> Co-authored-by: Tentoxa <53821604+Tentoxa@users.noreply.github.com> Co-authored-by: HALDRO <121296348+HALDRO@users.noreply.github.com> Co-authored-by: Paijo <14921983+oyi77@users.noreply.github.com> Co-authored-by: janeza2 <49841619+janeza2@users.noreply.github.com> Co-authored-by: df4p <38404+df4p@users.noreply.github.com> Co-authored-by: ivan-mezentsev <ivan@mezentsev.me> Co-authored-by: Chewji <126886556+Chewji9875@users.noreply.github.com> Co-authored-by: L-aros <107354918+L-aros@users.noreply.github.com> Co-authored-by: M.M <mr.maatoug@gmail.com> Co-authored-by: Benson K B <bensonkbmca@gmail.com> Co-authored-by: terence71-glitch <mcdowellterence71@gmail.com>
333 lines
15 KiB
TypeScript
333 lines
15 KiB
TypeScript
/**
|
|
* Tests for PR #1188 — Claude Code Native Parity.
|
|
*
|
|
* Covers:
|
|
* 1. CCH body signing (signRequestBody / computeCCH from claudeCodeCCH.ts)
|
|
* 2. Fingerprint computation (computeFingerprint / extractFirstUserMessageText)
|
|
* 3. Tool name remapping (remapToolNamesInRequest from claudeCodeToolRemapper.ts)
|
|
* 4. API constraints (enforceThinkingTemperature, disableThinkingIfToolChoiceForced,
|
|
* enforceCacheControlLimit, ensureCacheControlOnLastUserMessage)
|
|
*/
|
|
|
|
import { describe, it } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
// ── CCH signing ───────────────────────────────────────────────────────────────
|
|
import { computeCCH, signRequestBody, CCH_PATTERN } from "../../open-sse/services/claudeCodeCCH.ts";
|
|
import { CLAUDE_CODE_COMPATIBLE_VERSION } from "../../open-sse/services/claudeCodeCompatible.ts";
|
|
|
|
// ── Fingerprint ───────────────────────────────────────────────────────────────
|
|
import {
|
|
computeFingerprint,
|
|
extractFirstUserMessageText,
|
|
} from "../../open-sse/services/claudeCodeFingerprint.ts";
|
|
|
|
// ── Tool remapper ─────────────────────────────────────────────────────────────
|
|
import {
|
|
remapToolNamesInRequest,
|
|
remapToolNamesInResponse,
|
|
} from "../../open-sse/services/claudeCodeToolRemapper.ts";
|
|
|
|
// ── Constraints ───────────────────────────────────────────────────────────────
|
|
import {
|
|
enforceThinkingTemperature,
|
|
disableThinkingIfToolChoiceForced,
|
|
enforceCacheControlLimit,
|
|
ensureCacheControlOnLastUserMessage,
|
|
} from "../../open-sse/services/claudeCodeConstraints.ts";
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// CCH Signing tests
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe("computeCCH", () => {
|
|
it("returns a 5-character lowercase hex string", async () => {
|
|
const encoder = new TextEncoder();
|
|
const bytes = encoder.encode("hello world");
|
|
const hash = await computeCCH(bytes);
|
|
assert.equal(hash.length, 5, "CCH must be 5 chars");
|
|
assert.match(hash, /^[0-9a-f]{5}$/, "CCH must be lowercase hex");
|
|
});
|
|
|
|
it("is deterministic — same input always produces the same hash", async () => {
|
|
const encoder = new TextEncoder();
|
|
const bytes = encoder.encode('{"model":"claude-sonnet-4-6"}');
|
|
const hash1 = await computeCCH(bytes);
|
|
const hash2 = await computeCCH(bytes);
|
|
assert.equal(hash1, hash2, "CCH must be deterministic");
|
|
});
|
|
|
|
it("produces different hashes for different inputs", async () => {
|
|
const encoder = new TextEncoder();
|
|
const hash1 = await computeCCH(encoder.encode("input-a"));
|
|
const hash2 = await computeCCH(encoder.encode("input-b"));
|
|
assert.notEqual(hash1, hash2, "different inputs must produce different CCH values");
|
|
});
|
|
});
|
|
|
|
describe("signRequestBody", () => {
|
|
it("replaces cch=00000 placeholder with computed hash", async () => {
|
|
const body = `{"x-anthropic-billing-header":"cc_version=${CLAUDE_CODE_COMPATIBLE_VERSION}.abc; cch=00000;","model":"claude"}`;
|
|
const signed = await signRequestBody(body);
|
|
assert.ok(signed.includes("cch="), "signed body should contain cch=");
|
|
assert.ok(!signed.includes("cch=00000"), "placeholder cch=00000 should be replaced");
|
|
const match = signed.match(/cch=([0-9a-f]{5});/);
|
|
assert.ok(match, "signed body must contain a valid 5-char hex CCH token");
|
|
});
|
|
|
|
it("returns the body unchanged when no cch placeholder is present", async () => {
|
|
const body = '{"model":"claude-sonnet-4-6","messages":[]}';
|
|
const result = await signRequestBody(body);
|
|
assert.equal(result, body, "body without placeholder must pass through unchanged");
|
|
});
|
|
|
|
it("CCH_PATTERN matches the expected format", () => {
|
|
assert.ok(CCH_PATTERN.test("cch=ab1f3;"), "CCH_PATTERN should match lowercase 5-hex;");
|
|
assert.ok(!CCH_PATTERN.test("cch=00000"), "CCH_PATTERN requires trailing semicolon");
|
|
assert.ok(!CCH_PATTERN.test("cch=ABCDE;"), "CCH_PATTERN requires lowercase");
|
|
});
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Fingerprint tests
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe("computeFingerprint", () => {
|
|
it("returns a 3-character hex string", () => {
|
|
const fp = computeFingerprint("Hello, world!", CLAUDE_CODE_COMPATIBLE_VERSION);
|
|
assert.equal(fp.length, 3, "fingerprint must be 3 chars");
|
|
assert.match(fp, /^[0-9a-f]{3}$/, "fingerprint must be lowercase hex");
|
|
});
|
|
|
|
it("is deterministic — same text + version always produces the same fingerprint", () => {
|
|
const fp1 = computeFingerprint("Hello, world!", CLAUDE_CODE_COMPATIBLE_VERSION);
|
|
const fp2 = computeFingerprint("Hello, world!", CLAUDE_CODE_COMPATIBLE_VERSION);
|
|
assert.equal(fp1, fp2, "fingerprint must be deterministic");
|
|
});
|
|
|
|
it("changes when fingerprint version changes", () => {
|
|
const fp1 = computeFingerprint("same text", CLAUDE_CODE_COMPATIBLE_VERSION);
|
|
const fp2 = computeFingerprint("same text", "2.1.93");
|
|
assert.notEqual(fp1, fp2, "different versions must produce different fingerprints");
|
|
});
|
|
|
|
it("handles short messages safely — uses '0' for missing indices", () => {
|
|
// indices are [4, 7, 20]; short string should not throw
|
|
const fp = computeFingerprint("hi", CLAUDE_CODE_COMPATIBLE_VERSION);
|
|
assert.ok(fp.length === 3, "short input should not throw and return 3-char fingerprint");
|
|
});
|
|
|
|
it("handles empty string without throwing", () => {
|
|
const fp = computeFingerprint("", CLAUDE_CODE_COMPATIBLE_VERSION);
|
|
assert.ok(fp.length === 3, "empty string should not throw");
|
|
});
|
|
});
|
|
|
|
describe("extractFirstUserMessageText", () => {
|
|
it("extracts text from a simple string-content user message", () => {
|
|
const messages = [
|
|
{ role: "system", content: "You are helpful." },
|
|
{ role: "user", content: "Hello there!" },
|
|
];
|
|
assert.equal(extractFirstUserMessageText(messages), "Hello there!");
|
|
});
|
|
|
|
it("extracts text from an array-content user message", () => {
|
|
const messages = [{ role: "user", content: [{ type: "text", text: "What is 2+2?" }] }];
|
|
assert.equal(extractFirstUserMessageText(messages), "What is 2+2?");
|
|
});
|
|
|
|
it("returns empty string when there are no user messages", () => {
|
|
const messages = [{ role: "system", content: "You are helpful." }];
|
|
assert.equal(extractFirstUserMessageText(messages), "");
|
|
});
|
|
|
|
it("returns empty string for undefined/null input", () => {
|
|
assert.equal(extractFirstUserMessageText(undefined), "");
|
|
assert.equal(extractFirstUserMessageText(null), "");
|
|
});
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Tool remapper tests
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe("remapToolNamesInRequest", () => {
|
|
it("converts lowercase tool names to TitleCase in tools array", () => {
|
|
const body = {
|
|
tools: [
|
|
{ name: "bash", description: "Run bash commands" },
|
|
{ name: "read_file", description: "Read a file" },
|
|
],
|
|
messages: [],
|
|
};
|
|
remapToolNamesInRequest(body);
|
|
// Should remap known tools (e.g. bash → Bash); unknown tools pass through
|
|
// The exact mapping depends on the tool registry — test that function doesn't throw
|
|
// and that tools array is still present
|
|
assert.ok(Array.isArray(body.tools), "tools array must still be present after remap");
|
|
});
|
|
|
|
it("tracks remapped tool names without leaking helper fields into the wire payload", () => {
|
|
const body = {
|
|
tools: [
|
|
{ name: "bash", description: "Run bash commands" },
|
|
{ name: "glob", description: "Search files" },
|
|
],
|
|
tool_choice: { type: "tool", name: "glob" },
|
|
messages: [
|
|
{
|
|
role: "assistant",
|
|
content: [{ type: "tool_use", name: "read", input: { file_path: "README.md" } }],
|
|
},
|
|
],
|
|
};
|
|
|
|
remapToolNamesInRequest(body);
|
|
|
|
const mappedBody = body as Record<string, unknown> & {
|
|
_toolNameMap?: Map<string, string>;
|
|
_claudeCodeRequiresLowercaseToolNames?: boolean;
|
|
};
|
|
|
|
// remapToolNamesInRequest remaps in-place and stores _toolNameMap as non-enumerable.
|
|
assert.equal(body.tools[0].name, "Bash");
|
|
assert.equal(body.tools[1].name, "Glob");
|
|
assert.equal(body.tool_choice.name, "Glob");
|
|
assert.equal(body.messages[0].content[0].name, "Read");
|
|
assert.equal(mappedBody._claudeCodeRequiresLowercaseToolNames, undefined);
|
|
|
|
const wirePayload = JSON.stringify(body);
|
|
assert.equal(wirePayload.includes("_toolNameMap"), false);
|
|
assert.equal(wirePayload.includes("_claudeCodeRequiresLowercaseToolNames"), false);
|
|
assert.match(wirePayload, /"name":"Bash"/);
|
|
assert.match(wirePayload, /"name":"Glob"/);
|
|
});
|
|
|
|
it("remaps known tools and does not throw with extra unknown fields on body", () => {
|
|
// Verify unrelated extra properties do not interfere with remapping and no error is thrown.
|
|
const body: Record<string, unknown> = {
|
|
tools: [{ name: "bash", description: "Run bash commands" }],
|
|
messages: [],
|
|
_someExtraField: "irrelevant",
|
|
};
|
|
|
|
assert.doesNotThrow(() => remapToolNamesInRequest(body));
|
|
|
|
// Known tool names are still remapped in-place
|
|
assert.equal((body.tools as Array<Record<string, unknown>>)[0].name, "Bash");
|
|
// Extra fields are left intact (not stripped, not used for map lookup)
|
|
assert.equal(body._someExtraField, "irrelevant");
|
|
// _toolNameMap is non-enumerable and will not leak through Object.keys/JSON.stringify.
|
|
assert.equal(Object.keys(body).includes("_toolNameMap"), false);
|
|
});
|
|
|
|
it("handles body without tools without throwing", () => {
|
|
const body = { messages: [{ role: "user", content: "hello" }] };
|
|
assert.doesNotThrow(() => remapToolNamesInRequest(body));
|
|
});
|
|
// Note: remapToolNamesInRequest requires a non-null body (callers always provide one)
|
|
});
|
|
|
|
describe("remapToolNamesInResponse", () => {
|
|
it("restores TitleCase tool names to lowercase via REVERSE_MAP when forceLowercase=true", () => {
|
|
// remapToolNamesInResponse(text, forceLowercase) — 2 args only; uses hardcoded REVERSE_MAP
|
|
const text = 'data: {"name":"Bash","other":{"name": "Glob"}}\n\n';
|
|
const restored = remapToolNamesInResponse(text, true);
|
|
|
|
assert.match(restored, /"name":"bash"/);
|
|
assert.match(restored, /"name": "glob"/);
|
|
assert.equal(remapToolNamesInResponse(text, false), text);
|
|
});
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// API constraints tests
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe("enforceThinkingTemperature", () => {
|
|
it("sets temperature to 1 when thinking is enabled", () => {
|
|
const body = {
|
|
thinking: { type: "enabled", budget_tokens: 1000 },
|
|
temperature: 0.7,
|
|
};
|
|
enforceThinkingTemperature(body);
|
|
assert.equal(body.temperature, 1, "temperature must be 1 when thinking is active");
|
|
});
|
|
|
|
it("does not modify temperature when thinking is not present", () => {
|
|
const body = { temperature: 0.7, messages: [] };
|
|
enforceThinkingTemperature(body);
|
|
assert.equal(body.temperature, 0.7, "temperature should be unchanged when no thinking");
|
|
});
|
|
});
|
|
|
|
describe("disableThinkingIfToolChoiceForced", () => {
|
|
it("removes thinking when tool_choice forces a specific tool", () => {
|
|
const body = {
|
|
thinking: { type: "enabled", budget_tokens: 1000 },
|
|
context_management: { edits: [{ type: "clear_thinking_20251015", keep: "all" }] },
|
|
tool_choice: { type: "tool", name: "Bash" },
|
|
tools: [{ name: "Bash" }],
|
|
};
|
|
disableThinkingIfToolChoiceForced(body);
|
|
// thinking should be removed or disabled
|
|
const thinkingType = body.thinking?.type;
|
|
assert.ok(
|
|
!thinkingType || thinkingType === "disabled" || thinkingType === "none",
|
|
"thinking must be disabled when tool_choice forces a specific tool"
|
|
);
|
|
assert.equal(body.context_management, undefined);
|
|
});
|
|
|
|
it("does not modify thinking when tool_choice is auto", () => {
|
|
const body = {
|
|
thinking: { type: "enabled", budget_tokens: 1000 },
|
|
tool_choice: { type: "auto" },
|
|
};
|
|
disableThinkingIfToolChoiceForced(body);
|
|
assert.equal(body.thinking?.type, "enabled", "thinking should remain when tool_choice is auto");
|
|
});
|
|
});
|
|
|
|
describe("enforceCacheControlLimit", () => {
|
|
it("limits cache_control blocks without throwing when count is within limit", () => {
|
|
const body = {
|
|
system: [
|
|
{ type: "text", text: "s1", cache_control: { type: "ephemeral" } },
|
|
{ type: "text", text: "s2" },
|
|
],
|
|
messages: [
|
|
{
|
|
role: "user",
|
|
content: [{ type: "text", text: "hi", cache_control: { type: "ephemeral" } }],
|
|
},
|
|
],
|
|
};
|
|
assert.doesNotThrow(() => enforceCacheControlLimit(body));
|
|
});
|
|
|
|
it("handles body without system or messages without throwing", () => {
|
|
// chatCore always provides a valid body object — test empty but non-null object
|
|
assert.doesNotThrow(() => enforceCacheControlLimit({}));
|
|
});
|
|
});
|
|
|
|
describe("ensureCacheControlOnLastUserMessage", () => {
|
|
it("does not throw on a valid messages array", () => {
|
|
const body = {
|
|
messages: [
|
|
{ role: "user", content: [{ type: "text", text: "Hello" }] },
|
|
{ role: "assistant", content: [{ type: "text", text: "Hi!" }] },
|
|
{ role: "user", content: [{ type: "text", text: "Follow up" }] },
|
|
],
|
|
};
|
|
assert.doesNotThrow(() => ensureCacheControlOnLastUserMessage(body));
|
|
});
|
|
|
|
it("handles body without messages without throwing", () => {
|
|
// chatCore always provides a valid body object — test empty but non-null object
|
|
assert.doesNotThrow(() => ensureCacheControlOnLastUserMessage({}));
|
|
});
|
|
});
|