mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-26 09:02:11 +03:00
Merged after conflict resolution: the 5 conflicting test files were the base-red drains that #11201 already landed on the tip — kept the tip versions; the feature content is untouched. Validated on the combined batch board + this branch: codex-app-server + codex-gpt56-catalog 25/25, typecheck:core clean, docs-counts green (351 providers), provider-consistency 268/351/0. The opt-in codex-app-server transport (JSON-RPC-over-WS, turn/completed-awaited close, Responses SSE bridge) leaves the default codex path untouched. Thank you @arminanton — a 3.4k-line transport with the docs wave and tests to match!
229 lines
8.6 KiB
TypeScript
229 lines
8.6 KiB
TypeScript
/**
|
|
* TDD tests for CCR skip-on-tool-outputs behavior (Fix 1).
|
|
*
|
|
* Background: when OmniRoute is used as a chat-completion PROVIDER (not as an
|
|
* MCP server), the upstream LLM cannot call `omniroute_ccr_retrieve` to
|
|
* expand CCR markers. Replacing tool outputs with `[CCR retrieve hash=X
|
|
* chars=Y]` markers therefore breaks the agent loop — the LLM sees an
|
|
* opaque placeholder where the actual tool result should be and stalls.
|
|
*
|
|
* Fix: CCR must preserve tool-role messages and Anthropic-style tool_result
|
|
* parts verbatim. The compression engine still applies to plain user /
|
|
* assistant text blocks (where the LLM can reason about the marker).
|
|
*
|
|
* Run: node --import tsx/esm --test tests/unit/compression/ccr-skip-tool-outputs.test.ts
|
|
*/
|
|
|
|
import { describe, it, before } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
ccrEngine,
|
|
resetCcrStore,
|
|
} from "../../../open-sse/services/compression/engines/ccr/index.ts";
|
|
import { registerBuiltinCompressionEngines } from "../../../open-sse/services/compression/index.ts";
|
|
|
|
const LARGE_TOOL_OUTPUT = `Total 247 lines in current working directory.
|
|
drwxr-xr-x 18 herjarsa herjarsa 4096 Jul 20 13:12 .
|
|
drwxr-xr-x 14 root root 4096 Jul 17 17:59 ..
|
|
drwxr-xr-x 4 herjarsa herjarsa 4096 Jul 18 14:41 .codegraph
|
|
-rw-r--r-- 1 herjarsa herjarsa 8927 Jul 18 02:10 ARCHITECTURE.md
|
|
-rw-r--r-- 1 herjarsa herjarsa 341669 Jul 20 13:12 meta-governor.log
|
|
-rw-r--r-- 1 herjarsa herjarsa 81784 Jul 16 13:55 package-lock.json
|
|
-rw-r--r-- 1 herjarsa herjarsa 4798 Jul 17 17:59 opencode.json
|
|
-rw-r--r-- 1 herjarsa herjarsa 8045 Jul 12 02:07 STRUCTURE.md
|
|
drwxr-xr-x 4 herjarsa herjarsa 4096 Jul 20 13:12 .opencode
|
|
-rw-r--r-- 1 herjarsa herjarsa 94 Jul 20 13:12 omo-meta-governor-upgrade-check.json
|
|
-rw-r--r-- 1 herjarsa herjarsa 8437 Jul 14 18:42 oh-my-openagent.jsonc
|
|
-rw------- 1 herjarsa herjarsa 19542 Jul 18 02:09 session.json
|
|
drwxr-xr-x 2 herjarsa herjarsa 4096 Jun 14 18:42 snippet
|
|
drwxr-xr-x 22 herjarsa herjarsa 4096 Jun 13 12:33 skills
|
|
drwxr-xr-x 101 herjarsa herjarsa 4096 Jul 16 03:30 skill-libraries
|
|
-rw-r--r-- 1 herjarsa herjarsa 233 Jun 19 22:39 tui.json
|
|
-rw-r--r-- 1 herjarsa herjarsa 80415 Jun 14 18:42 structure-summary.json
|
|
The CCR engine must NOT replace this content with a marker because the LLM
|
|
needs the verbatim output to continue reasoning. Adding more lines to push
|
|
the character count well past the default minChars of 600 so the test is
|
|
unambiguous about why the skip rule is necessary. Even if minChars were 100
|
|
the rule must still hold because tool outputs are never safe to compress.`;
|
|
|
|
describe("ccr engine — skip tool outputs", () => {
|
|
before(() => {
|
|
resetCcrStore();
|
|
registerBuiltinCompressionEngines();
|
|
});
|
|
|
|
it("does NOT compress role:tool messages (OpenAI format)", () => {
|
|
const body = {
|
|
model: "gpt-4",
|
|
messages: [
|
|
{
|
|
role: "tool",
|
|
tool_call_id: "call_abc123",
|
|
content: LARGE_TOOL_OUTPUT,
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = ccrEngine.apply(body as Record<string, unknown>);
|
|
|
|
assert.equal(
|
|
result.compressed,
|
|
false,
|
|
"role:tool messages must NEVER trigger CCR compression — tool output is needed verbatim"
|
|
);
|
|
|
|
const messages = result.body.messages as Array<{ role: string; content: string }>;
|
|
assert.equal(
|
|
messages[0].content,
|
|
LARGE_TOOL_OUTPUT,
|
|
"role:tool content must be byte-identical to input (no marker substitution)"
|
|
);
|
|
assert.ok(
|
|
!messages[0].content.includes("[CCR retrieve"),
|
|
"role:tool content must not contain any CCR marker"
|
|
);
|
|
});
|
|
|
|
it("does NOT compress Anthropic-style user message containing only tool_result parts", () => {
|
|
const body = {
|
|
model: "claude-sonnet-4-5",
|
|
messages: [
|
|
{
|
|
role: "user",
|
|
content: [
|
|
{
|
|
type: "tool_result",
|
|
tool_use_id: "toolu_abc123",
|
|
content: LARGE_TOOL_OUTPUT,
|
|
},
|
|
{
|
|
type: "tool_result",
|
|
tool_use_id: "toolu_def456",
|
|
content: LARGE_TOOL_OUTPUT + "\n--- second tool result ---",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = ccrEngine.apply(body as Record<string, unknown>);
|
|
|
|
assert.equal(
|
|
result.compressed,
|
|
false,
|
|
"user message containing only tool_result parts must NOT trigger CCR compression"
|
|
);
|
|
|
|
const messages = result.body.messages as Array<{
|
|
role: string;
|
|
content: Array<{ type: string; content?: string; text?: string }>;
|
|
}>;
|
|
assert.equal(messages[0].content[0].type, "tool_result");
|
|
assert.equal(
|
|
messages[0].content[0].content,
|
|
LARGE_TOOL_OUTPUT,
|
|
"first tool_result content must be byte-identical to input"
|
|
);
|
|
assert.equal(
|
|
messages[0].content[1].content,
|
|
LARGE_TOOL_OUTPUT + "\n--- second tool result ---",
|
|
"second tool_result content must be byte-identical to input"
|
|
);
|
|
});
|
|
|
|
it("still compresses plain user text — the skip rule is scoped to tool outputs", () => {
|
|
// Sanity check: the fix must NOT regress the existing compression path.
|
|
// A plain user-role message with large text content must still be compressed.
|
|
// #7746 follow-up: CCR now only compresses for callers that advertise the
|
|
// omniroute_ccr_retrieve tool (otherwise the content-addressed marker is
|
|
// unresolvable). Advertise it here so this guard exercises the real
|
|
// plain-user-text compression path rather than tripping the new caller gate.
|
|
const LARGE_USER_TEXT = LARGE_TOOL_OUTPUT; // same length, same trigger
|
|
const body = {
|
|
model: "gpt-4",
|
|
messages: [{ role: "user", content: LARGE_USER_TEXT }],
|
|
tools: [{ type: "function", function: { name: "omniroute_ccr_retrieve" } }],
|
|
};
|
|
|
|
const result = ccrEngine.apply(body as Record<string, unknown>);
|
|
|
|
assert.equal(
|
|
result.compressed,
|
|
true,
|
|
"plain role:user text block above minChars MUST still be compressed (regression guard)"
|
|
);
|
|
const messages = result.body.messages as Array<{ role: string; content: string }>;
|
|
// With the retrieve tool advertised, CCR also injects a leading system
|
|
// instruction, so the compressed user block is no longer necessarily
|
|
// messages[0]. Assert the marker is present in SOME message rather than
|
|
// pinning an index.
|
|
assert.ok(
|
|
messages.some(
|
|
(m) =>
|
|
typeof m.content === "string" &&
|
|
/\[CCR retrieve hash=[0-9a-f]{24} chars=\d+\]/.test(m.content)
|
|
),
|
|
"plain user text must still be replaced with a CCR marker"
|
|
);
|
|
});
|
|
|
|
it("does NOT compress when the user message is purely a tool_result (even one part)", () => {
|
|
// Single-element edge case: a user message with a single tool_result part
|
|
// must still be skipped, not compressed.
|
|
const body = {
|
|
model: "claude-sonnet-4-5",
|
|
messages: [
|
|
{
|
|
role: "user",
|
|
content: [
|
|
{
|
|
type: "tool_result",
|
|
tool_use_id: "toolu_single",
|
|
content: LARGE_TOOL_OUTPUT,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = ccrEngine.apply(body as Record<string, unknown>);
|
|
|
|
assert.equal(result.compressed, false, "single tool_result part must also be skipped");
|
|
const messages = result.body.messages as Array<{
|
|
role: string;
|
|
content: Array<{ type: string; content?: string }>;
|
|
}>;
|
|
assert.equal(messages[0].content[0].content, LARGE_TOOL_OUTPUT);
|
|
});
|
|
});
|
|
|
|
it("does NOT crash on malformed user content with null / non-object parts (defensive guard)", () => {
|
|
// Regression guard for gemini-code-assist review on PR #7869:
|
|
// msg.content is parsed from external client input, so a malformed
|
|
// payload could deliver `null` or non-object entries in the parts
|
|
// array. The optional-chaining check must NOT throw a TypeError.
|
|
const body = {
|
|
model: "claude-sonnet-4-5",
|
|
messages: [
|
|
{
|
|
role: "user",
|
|
content: [
|
|
null,
|
|
{ type: "tool_result", tool_use_id: "toolu_x", content: LARGE_TOOL_OUTPUT },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
// Must not throw.
|
|
const result = ccrEngine.apply(body as Record<string, unknown>);
|
|
|
|
// Null is not a tool_result, so the message must NOT be skipped as
|
|
// a tool-only message — it falls through to the normal array path
|
|
// where the existing text/non-text branch handles each part.
|
|
// We just need the engine to not crash on the malformed payload.
|
|
assert.ok(result.body, "engine must return a body even with malformed content");
|
|
assert.equal(typeof result.compressed, "boolean");
|
|
});
|