Feature : RTK compress

This commit is contained in:
decolua
2026-04-22 15:36:51 +07:00
parent e1a219dba6
commit 8de9aae90c
26 changed files with 1612 additions and 0 deletions

136
tests/unit/rtk.e2e.test.js Normal file
View File

@@ -0,0 +1,136 @@
// End-to-end integration test: hit live local proxy and verify [RTK] behavior.
// Run with: RUN_E2E=1 RTK_E2E_PORT=... RTK_E2E_KEY=... RTK_E2E_LOG=<absolute path to server stdout file> npm test rtk.e2e.test.js
// Requires: dev server running, rtkEnabled=true, API key present.
import { describe, it, expect } from "vitest";
import fs from "node:fs";
const PORT = process.env.RTK_E2E_PORT || "20128";
const BASE = `http://localhost:${PORT}`;
const API_KEY = process.env.RTK_E2E_KEY || "";
const LOG_FILE = process.env.RTK_E2E_LOG || "";
const RUN = process.env.RUN_E2E === "1";
const maybe = RUN ? describe : describe.skip;
function readLogTail(bytes = 8192) {
if (!LOG_FILE || !fs.existsSync(LOG_FILE)) return "";
const stat = fs.statSync(LOG_FILE);
const start = Math.max(0, stat.size - bytes);
const fd = fs.openSync(LOG_FILE, "r");
const buf = Buffer.alloc(stat.size - start);
fs.readSync(fd, buf, 0, buf.length, start);
fs.closeSync(fd);
return buf.toString("utf8");
}
// Read new bytes appended to log since `offset`. Returns text + new offset.
function readLogSince(offset) {
if (!LOG_FILE || !fs.existsSync(LOG_FILE)) return { text: "", next: offset };
const stat = fs.statSync(LOG_FILE);
if (stat.size <= offset) return { text: "", next: stat.size };
const fd = fs.openSync(LOG_FILE, "r");
const buf = Buffer.alloc(stat.size - offset);
fs.readSync(fd, buf, 0, buf.length, offset);
fs.closeSync(fd);
return { text: buf.toString("utf8"), next: stat.size };
}
function logOffset() {
if (!LOG_FILE || !fs.existsSync(LOG_FILE)) return 0;
return fs.statSync(LOG_FILE).size;
}
async function sendChat(body) {
return fetch(`${BASE}/v1/chat/completions`, {
method: "POST",
headers: { "content-type": "application/json", "authorization": `Bearer ${API_KEY}` },
body: JSON.stringify(body)
});
}
function makeBigDiff(fileCount = 3, linesPerFile = 80) {
const out = [];
for (let f = 0; f < fileCount; f++) {
out.push(`diff --git a/src/file${f}.js b/src/file${f}.js`);
out.push(`index abc${f}..def${f} 100644`);
out.push(`--- a/src/file${f}.js`);
out.push(`+++ b/src/file${f}.js`);
out.push(`@@ -1,${linesPerFile} +1,${linesPerFile} @@`);
for (let i = 0; i < linesPerFile; i++) {
out.push(`-const old${f}_${i} = "removed value ${i} padding padding padding";`);
out.push(`+const new${f}_${i} = "added value ${i} padding padding padding padding";`);
}
}
return out.join("\n");
}
maybe("RTK end-to-end", () => {
it("server is reachable", async () => {
const res = await fetch(`${BASE}/api/health`);
expect(res.ok).toBe(true);
});
it("rtkEnabled flag is true (user must enable via dashboard)", async () => {
const res = await fetch(`${BASE}/api/settings`);
const data = await res.json();
expect(data.rtkEnabled).toBe(true);
});
it("compresses git diff tool_result and writes [RTK] savings to log", async () => {
const diff = makeBigDiff(2, 60);
expect(diff.length).toBeGreaterThan(500);
const offset = logOffset();
const res = await sendChat({
model: "cc/claude-opus-4-7",
stream: false,
max_tokens: 64,
messages: [
{ role: "user", content: "run git diff" },
{ role: "assistant", content: null, tool_calls: [{ id: "call_1", type: "function", function: { name: "Bash", arguments: JSON.stringify({ command: "git diff" }) } }] },
{ role: "tool", tool_call_id: "call_1", content: diff },
{ role: "user", content: "summarize in 10 words" }
]
});
expect([200, 400, 401, 402, 500]).toContain(res.status);
if (!LOG_FILE) return;
await new Promise(r => setTimeout(r, 500));
const { text } = readLogSince(offset);
const matches = [...text.matchAll(/\[RTK\] saved (\d+)B \/ (\d+)B \([\d.]+%\) via \[([\w,-]+)\] hits=(\d+)/g)];
// Find the log line that corresponds to OUR request (total ≥ diff.length and contains git-diff)
const mine = matches.find(m => Number(m[2]) >= diff.length && m[3].includes("git-diff"));
expect(mine, `no matching [RTK] line for our request (diff=${diff.length}B) in ${matches.length} log entries`).toBeTruthy();
expect(Number(mine[1])).toBeGreaterThan(500);
expect(mine[3]).toContain("git-diff");
expect(Number(mine[4])).toBeGreaterThanOrEqual(1);
});
it("compresses grep-style tool_result", async () => {
const lines = [];
for (let i = 1; i <= 30; i++) lines.push(`src/lib/foo.js:${i}:const v${i} = "matching content with enough padding to exceed threshold";`);
const grepOut = lines.join("\n");
expect(grepOut.length).toBeGreaterThan(500);
const offset = logOffset();
const res = await sendChat({
model: "cc/claude-opus-4-7",
stream: false,
max_tokens: 32,
messages: [
{ role: "user", content: "grep" },
{ role: "assistant", content: null, tool_calls: [{ id: "c3", type: "function", function: { name: "Bash", arguments: "{}" } }] },
{ role: "tool", tool_call_id: "c3", content: grepOut },
{ role: "user", content: "ok" }
]
});
expect([200, 400, 401, 402, 500]).toContain(res.status);
if (!LOG_FILE) return;
await new Promise(r => setTimeout(r, 500));
const { text } = readLogSince(offset);
const matches = [...text.matchAll(/\[RTK\] saved (\d+)B \/ (\d+)B \([\d.]+%\) via \[([\w,-]+)\] hits=(\d+)/g)];
const mine = matches.find(m => Number(m[2]) >= grepOut.length && m[3].includes("grep"));
expect(mine, `no matching [RTK] line for grep payload`).toBeTruthy();
});
});

View File

@@ -0,0 +1,137 @@
// E2E test: verify RTK compression runs for every configured provider/route.
// Each test covers a different source→target translator path.
// Run with: RUN_E2E=1 RTK_E2E_PORT=... RTK_E2E_KEY=... RTK_E2E_LOG=<server stdout file> npm test rtk.multi-provider.e2e.test.js
import { describe, it, expect } from "vitest";
import fs from "node:fs";
const PORT = process.env.RTK_E2E_PORT || "20128";
const BASE = `http://localhost:${PORT}`;
const API_KEY = process.env.RTK_E2E_KEY || "";
const LOG_FILE = process.env.RTK_E2E_LOG || "";
const RUN = process.env.RUN_E2E === "1";
const maybe = RUN ? describe : describe.skip;
function logOffset() {
if (!LOG_FILE || !fs.existsSync(LOG_FILE)) return 0;
return fs.statSync(LOG_FILE).size;
}
function readLogSince(offset) {
if (!LOG_FILE || !fs.existsSync(LOG_FILE)) return "";
const stat = fs.statSync(LOG_FILE);
if (stat.size <= offset) return "";
const fd = fs.openSync(LOG_FILE, "r");
const buf = Buffer.alloc(stat.size - offset);
fs.readSync(fd, buf, 0, buf.length, offset);
fs.closeSync(fd);
return buf.toString("utf8");
}
function makeBigDiff(fileCount = 2, linesPerFile = 60) {
const out = [];
for (let f = 0; f < fileCount; f++) {
out.push(`diff --git a/src/file${f}.js b/src/file${f}.js`);
out.push(`index abc${f}..def${f} 100644`);
out.push(`--- a/src/file${f}.js`);
out.push(`+++ b/src/file${f}.js`);
out.push(`@@ -1,${linesPerFile} +1,${linesPerFile} @@`);
for (let i = 0; i < linesPerFile; i++) {
out.push(`-const old${f}_${i} = "removed value ${i} padding padding padding";`);
out.push(`+const new${f}_${i} = "added value ${i} padding padding padding padding";`);
}
}
return out.join("\n");
}
async function sendChat(body) {
return fetch(`${BASE}/v1/chat/completions`, {
method: "POST",
headers: { "content-type": "application/json", "authorization": `Bearer ${API_KEY}` },
body: JSON.stringify(body)
});
}
// Wait for server to emit a matching [RTK] log line (race-safe against concurrent traffic).
async function waitForRtkLine({ minBytes, filterName, timeoutMs = 5000 }) {
const start = Date.now();
const startOffset = logOffset();
while (Date.now() - start < timeoutMs) {
const text = readLogSince(startOffset);
const matches = [...text.matchAll(/\[RTK\] saved (\d+)B \/ (\d+)B \(([\d.]+)%\) via \[([\w,-]+)\] hits=(\d+)/g)];
const mine = matches.find(m => Number(m[2]) >= minBytes && m[4].includes(filterName));
if (mine) {
return {
saved: Number(mine[1]),
total: Number(mine[2]),
pct: Number(mine[3]),
filters: mine[4],
hits: Number(mine[5])
};
}
await new Promise(r => setTimeout(r, 200));
}
return null;
}
// Build a chat request with OpenAI-style tool_result carrying large content.
function chatBodyWithDiff(model, diff) {
return {
model,
stream: false,
max_tokens: 16,
messages: [
{ role: "user", content: "run git diff" },
{
role: "assistant",
content: null,
tool_calls: [{ id: "call_1", type: "function", function: { name: "Bash", arguments: JSON.stringify({ command: "git diff" }) } }]
},
{ role: "tool", tool_call_id: "call_1", content: diff },
{ role: "user", content: "ok" }
]
};
}
// Matrix of routes to cover — one entry per translator target format.
const ROUTES = [
{ name: "claude (cc/* → openai→claude)", model: "cc/claude-opus-4-7" },
{ name: "codex (cx/* → openai→openai-responses)", model: "cx/gpt-5.4" },
{ name: "antigravity (ag/* → openai→antigravity)", model: "ag/gemini-3-flash" },
{ name: "cursor (cu/* → openai→cursor)", model: "cu/claude-4.5-sonnet" },
{ name: "kiro (kr/* → openai→kiro)", model: "kr/claude-sonnet-4.5" },
{ name: "gemini (gemini/* → openai→gemini)", model: "gemini/gemini-2.5-flash" },
{ name: "deepseek (deepseek/* → openai, passthrough)", model: "deepseek/deepseek-chat" },
{ name: "ollama (ollama/* → openai→ollama)", model: "ollama/gpt-oss:120b" },
];
maybe("RTK multi-provider E2E", () => {
it("server reachable and rtkEnabled=true", async () => {
const health = await fetch(`${BASE}/api/health`);
expect(health.ok).toBe(true);
const settings = await fetch(`${BASE}/api/settings`).then(r => r.json());
expect(settings.rtkEnabled).toBe(true);
});
for (const route of ROUTES) {
it(`compresses git diff for ${route.name}`, async () => {
const diff = makeBigDiff();
expect(diff.length).toBeGreaterThan(500);
const res = await sendChat(chatBodyWithDiff(route.model, diff));
// Provider may respond with 200/400/401/402/404/429/500 depending on account state.
// The important thing: proxy must NOT crash (we just need status code).
expect(res.status).toBeGreaterThanOrEqual(200);
expect(res.status).toBeLessThan(600);
if (!LOG_FILE) return;
const hit = await waitForRtkLine({ minBytes: diff.length, filterName: "git-diff" });
expect(hit, `[RTK] git-diff log line not found for ${route.name}`).toBeTruthy();
expect(hit.saved).toBeGreaterThan(500);
expect(hit.filters).toContain("git-diff");
// Log actual savings for visibility
console.log(`${route.name}: saved ${hit.saved}B / ${hit.total}B (${hit.pct}%) filters=${hit.filters}`);
}, 20000);
}
});

358
tests/unit/rtk.test.js Normal file
View File

@@ -0,0 +1,358 @@
import { describe, it, expect, beforeEach } from "vitest";
import { compressMessages, setRtkEnabled, isRtkEnabled, formatRtkLog } from "../../open-sse/rtk/index.js";
import { gitDiff } from "../../open-sse/rtk/filters/gitDiff.js";
import { gitStatus } from "../../open-sse/rtk/filters/gitStatus.js";
import { grep } from "../../open-sse/rtk/filters/grep.js";
import { find } from "../../open-sse/rtk/filters/find.js";
import { dedupLog } from "../../open-sse/rtk/filters/dedupLog.js";
import { ls } from "../../open-sse/rtk/filters/ls.js";
import { tree } from "../../open-sse/rtk/filters/tree.js";
import { smartTruncate } from "../../open-sse/rtk/filters/smartTruncate.js";
import { readNumbered } from "../../open-sse/rtk/filters/readNumbered.js";
import { searchList } from "../../open-sse/rtk/filters/searchList.js";
import { autoDetectFilter } from "../../open-sse/rtk/autodetect.js";
import { safeApply } from "../../open-sse/rtk/applyFilter.js";
function makeLongDiff() {
const lines = ["diff --git a/foo.js b/foo.js", "index abc..def 100644", "--- a/foo.js", "+++ b/foo.js", "@@ -1,3 +1,200 @@"];
for (let i = 0; i < 200; i++) lines.push(`+added line ${i} ${"x".repeat(20)}`);
return lines.join("\n");
}
function makeGitStatus() {
return [
"On branch main",
"Your branch is up to date with 'origin/main'.",
"",
"Changes not staged for commit:",
" (use \"git add <file>...\" to update what will be committed)",
"\tmodified: src/a.js",
"\tmodified: src/b.js",
"\tnew file: src/c.js",
"\tdeleted: src/old.js",
"",
"Untracked files:",
"\tnotes.txt",
"",
"no changes added to commit"
].join("\n");
}
function makeGrepOutput() {
const lines = [];
for (let i = 1; i <= 40; i++) lines.push(`src/foo.js:${i}:const x${i} = "some value here with padding text padding text"`);
for (let i = 1; i <= 10; i++) lines.push(`src/bar.js:${i}:const y${i} = "another value here with padding padding padding"`);
return lines.join("\n");
}
function makeFindOutput() {
const lines = [];
for (let i = 0; i < 30; i++) lines.push(`./src/a/${i}.js`);
for (let i = 0; i < 20; i++) lines.push(`./src/b/${i}.js`);
for (let i = 0; i < 5; i++) lines.push(`./top${i}.md`);
return lines.join("\n");
}
describe("RTK flag", () => {
it("default off, toggle works", () => {
setRtkEnabled(false);
expect(isRtkEnabled()).toBe(false);
setRtkEnabled(true);
expect(isRtkEnabled()).toBe(true);
setRtkEnabled(false);
});
});
describe("RTK filters", () => {
it("gitDiff truncates hunks beyond 100 lines and preserves file header", () => {
const input = makeLongDiff();
const out = gitDiff(input, 500);
expect(out).toContain("foo.js");
expect(out).toContain("lines truncated");
expect(out.length).toBeLessThan(input.length);
});
it("gitStatus groups by kind and produces compact output (Rust format)", () => {
const input = makeGitStatus();
const out = gitStatus(input);
expect(out).toContain("* main");
expect(out).toMatch(/~ Modified: \d+ files/);
expect(out).toContain("src/a.js");
expect(out.length).toBeLessThan(input.length);
});
it("grep groups matches by file and caps per-file lines (Rust format)", () => {
const input = makeGrepOutput();
const out = grep(input);
expect(out).toContain("50 matches in 2F:");
expect(out).toContain("[file] src/foo.js (40):");
expect(out).toContain("[file] src/bar.js (10):");
expect(out).toMatch(/\+\d+/); // overflow marker
expect(out.length).toBeLessThan(input.length);
});
it("find groups paths by parent dir, shows basenames (Rust format)", () => {
const input = makeFindOutput();
const out = find(input);
expect(out).toContain("55 files in 3 dirs:");
expect(out).toContain("./src/a/ (30):");
expect(out).toContain("./src/b/ (20):");
expect(out).toContain("./ (5):");
expect(out.length).toBeLessThan(input.length);
});
it("dedupLog collapses consecutive duplicates", () => {
const input = Array(20).fill("repeated log line A").join("\n") + "\nunique\n" + Array(10).fill("another dup").join("\n");
const out = dedupLog(input);
expect(out).toContain("repeated log line A");
expect(out).toContain("duplicate lines");
expect(out.length).toBeLessThan(input.length);
});
});
describe("autoDetectFilter", () => {
it("detects git diff", () => {
expect(autoDetectFilter("diff --git a/x b/x\n@@ -1 +1 @@\n+a").filterName).toBe("git-diff");
});
it("detects git status", () => {
expect(autoDetectFilter("On branch main\n modified: x.js\n").filterName).toBe("git-status");
});
it("detects grep", () => {
expect(autoDetectFilter("a.js:1:hello\nb.js:2:world\nc.js:3:foo").filterName).toBe("grep");
});
it("detects find", () => {
expect(autoDetectFilter("./a/b.js\n./a/c.js\n./a/d.js").filterName).toBe("find");
});
it("falls back to dedupLog for generic text", () => {
const txt = "line1\nline2\nline3\nline4\nline5\nline6\n";
expect(autoDetectFilter(txt).filterName).toBe("dedup-log");
});
});
describe("RTK filters (extras)", () => {
it("ls: compact_ls strips perms/owner, keeps name + size", () => {
const input = [
"total 48",
"drwxr-xr-x 2 user staff 64 Jan 1 12:00 .",
"drwxr-xr-x 2 user staff 64 Jan 1 12:00 ..",
"drwxr-xr-x 2 user staff 64 Jan 1 12:00 src",
"-rw-r--r-- 1 user staff 1234 Jan 1 12:00 Cargo.toml",
"-rw-r--r-- 1 user staff 5678 Jan 1 12:00 README.md"
].join("\n");
const out = ls(input);
expect(out).toContain("src/");
expect(out).toContain("Cargo.toml");
expect(out).toContain("1.2K");
expect(out).toContain("5.5K");
expect(out).not.toContain("drwx");
expect(out).toContain("Summary: 2 files, 1 dirs");
});
it("ls: filters noise dirs", () => {
const input = [
"total 8",
"drwxr-xr-x 2 user staff 64 Jan 1 12:00 node_modules",
"drwxr-xr-x 2 user staff 64 Jan 1 12:00 .git",
"drwxr-xr-x 2 user staff 64 Jan 1 12:00 src",
"-rw-r--r-- 1 user staff 100 Jan 1 12:00 main.js"
].join("\n");
const out = ls(input);
expect(out).not.toContain("node_modules");
expect(out).not.toContain(".git");
expect(out).toContain("src/");
expect(out).toContain("main.js");
});
it("tree: removes summary, keeps structure", () => {
const input = ".\n├── src\n│ └── main.rs\n└── Cargo.toml\n\n2 directories, 3 files\n";
const out = tree(input);
expect(out).not.toContain("directories");
expect(out).toContain("├──");
expect(out).toContain("main.rs");
});
it("smartTruncate: keeps head+tail, drops middle", () => {
const input = Array.from({ length: 400 }, (_, i) => `line ${i}`).join("\n");
const out = smartTruncate(input);
expect(out).toContain("line 0");
expect(out).toContain("line 399");
expect(out).toContain("lines truncated");
expect(out.length).toBeLessThan(input.length);
});
it("smartTruncate: passes through small input", () => {
const input = Array.from({ length: 10 }, (_, i) => `line ${i}`).join("\n");
expect(smartTruncate(input)).toBe(input);
});
it("readNumbered: compacts very long line-numbered dump", () => {
const lines = [];
for (let i = 1; i <= 400; i++) lines.push(` ${i}|content ${i}`);
const input = lines.join("\n");
const out = readNumbered(input);
expect(out).toContain("1|content 1");
expect(out).toContain("400|content 400");
expect(out).toContain("lines truncated");
expect(out.length).toBeLessThan(input.length);
});
it("searchList: groups Cursor Glob output by parent dir", () => {
const paths = [];
for (let i = 0; i < 30; i++) paths.push(`- src/a/f${i}.js`);
for (let i = 0; i < 10; i++) paths.push(`- src/b/g${i}.js`);
const input = [
"Result of search in '/Users/x' (total 40 files):",
...paths
].join("\n");
const out = searchList(input);
expect(out).toContain("Result of search in");
expect(out).toContain("40 files in 2 dirs:");
expect(out).toContain("src/a/ (30):");
expect(out).toContain("src/b/ (10):");
expect(out).toMatch(/\+\d+/);
expect(out.length).toBeLessThan(input.length);
});
});
describe("autoDetectFilter (extras)", () => {
it("detects tree via box-drawing glyphs", () => {
expect(autoDetectFilter(".\n├── src\n│ └── main.rs\n└── Cargo.toml\n").filterName).toBe("tree");
});
it("detects ls via total + perms rows", () => {
const input = [
"total 48",
"drwxr-xr-x 2 user staff 64 Jan 1 12:00 src",
"-rw-r--r-- 1 user staff 1234 Jan 1 12:00 main.js",
"-rw-r--r-- 1 user staff 5678 Jan 1 12:00 README.md"
].join("\n");
expect(autoDetectFilter(input).filterName).toBe("ls");
});
it("detects Cursor search list", () => {
const input = "Result of search in '/x' (total 3 files):\n- a/b.js\n- a/c.js\n- a/d.js";
expect(autoDetectFilter(input).filterName).toBe("search-list");
});
});
describe("safeApply", () => {
it("returns input if filter throws", () => {
const out = safeApply(() => { throw new Error("boom"); }, "hello");
expect(out).toBe("hello");
});
it("returns input if filter returns non-string", () => {
const out = safeApply(() => 42, "hello");
expect(out).toBe("hello");
});
});
describe("compressMessages (disabled)", () => {
beforeEach(() => setRtkEnabled(false));
it("returns null when disabled", () => {
const body = { messages: [{ role: "tool", tool_call_id: "x", content: makeLongDiff() }] };
expect(compressMessages(body)).toBeNull();
});
});
describe("compressMessages (enabled)", () => {
beforeEach(() => setRtkEnabled(true));
it("compresses OpenAI tool message (string content)", () => {
const big = makeLongDiff();
const body = { messages: [{ role: "tool", tool_call_id: "call_1", content: big }] };
const stats = compressMessages(body);
expect(stats.hits.length).toBeGreaterThan(0);
expect(body.messages[0].content.length).toBeLessThan(big.length);
expect(stats.bytesBefore).toBeGreaterThan(stats.bytesAfter);
});
it("compresses Claude string-form tool_result", () => {
const big = makeLongDiff();
const body = {
messages: [{
role: "user",
content: [{ type: "tool_result", tool_use_id: "toolu_1", content: big }]
}]
};
const stats = compressMessages(body);
expect(stats.hits.length).toBeGreaterThan(0);
expect(body.messages[0].content[0].content.length).toBeLessThan(big.length);
});
it("compresses Claude array-form tool_result text parts", () => {
const big = makeLongDiff();
const body = {
messages: [{
role: "user",
content: [{
type: "tool_result",
tool_use_id: "toolu_1",
content: [{ type: "text", text: big }, { type: "text", text: "unchanged short" }]
}]
}]
};
const stats = compressMessages(body);
expect(stats.hits.length).toBeGreaterThan(0);
expect(body.messages[0].content[0].content[0].text.length).toBeLessThan(big.length);
// short part unchanged
expect(body.messages[0].content[0].content[1].text).toBe("unchanged short");
});
it("skips is_error tool_result", () => {
const big = makeLongDiff();
const body = {
messages: [{
role: "user",
content: [{ type: "tool_result", tool_use_id: "toolu_1", content: big, is_error: true }]
}]
};
const stats = compressMessages(body);
expect(stats.hits.length).toBe(0);
expect(body.messages[0].content[0].content).toBe(big);
});
it("skips below MIN_COMPRESS_SIZE (<500 bytes)", () => {
const small = "diff --git a/x b/x\n@@ -1 +1 @@\n+a";
const body = { messages: [{ role: "tool", tool_call_id: "x", content: small }] };
const stats = compressMessages(body);
expect(stats.hits.length).toBe(0);
expect(body.messages[0].content).toBe(small);
});
it("never produces empty content (R14 guard)", () => {
const input = "a".repeat(1000);
const body = { messages: [{ role: "tool", tool_call_id: "x", content: input }] };
compressMessages(body);
expect(body.messages[0].content.length).toBeGreaterThan(0);
});
it("skips when body has no messages", () => {
expect(compressMessages({})).toBeNull();
expect(compressMessages({ messages: null })).toBeNull();
});
it("handles mix of messages without crashing", () => {
const body = {
messages: [
{ role: "system", content: "you are" },
{ role: "user", content: "hi" },
{ role: "assistant", content: null, tool_calls: [{ id: "c1", function: { name: "x", arguments: "{}" } }] },
{ role: "tool", tool_call_id: "c1", content: makeGrepOutput() },
{ role: "user", content: [{ type: "text", text: "next" }] }
]
};
const stats = compressMessages(body);
expect(stats).not.toBeNull();
expect(stats.hits.length).toBeGreaterThan(0);
});
});
describe("formatRtkLog", () => {
it("returns null when no hits", () => {
expect(formatRtkLog({ bytesBefore: 0, bytesAfter: 0, hits: [] })).toBeNull();
});
it("formats savings line with percentage", () => {
const line = formatRtkLog({ bytesBefore: 1000, bytesAfter: 400, hits: [{ filter: "git-diff" }] });
expect(line).toContain("saved 600B");
expect(line).toContain("60.0%");
expect(line).toContain("git-diff");
});
});