diff --git a/.sandcastle/context-compressor.test.ts b/.sandcastle/context-compressor.test.ts new file mode 100644 index 0000000..f01df8e --- /dev/null +++ b/.sandcastle/context-compressor.test.ts @@ -0,0 +1,121 @@ +/** + * Unit tests for context-compressor — the headroom-ai wrapper that shrinks + * compiled prompts before they reach the agent. + * + * All tests set / restore process.env.HEADROOM_MODE around each assertion so + * they are order-independent (getHeadroomMode() reads the env var at call time, + * not at module load time, which is what makes this possible). + * + * Run: npm test (picks up all *.test.ts files listed in the test script) + */ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { + getHeadroomMode, + getCompressionCallback, + isCompressionActive, +} from "./context-compressor.ts"; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function withMode(mode: string | undefined, fn: () => void): void { + const orig = process.env.HEADROOM_MODE; + if (mode === undefined) { + delete process.env.HEADROOM_MODE; + } else { + process.env.HEADROOM_MODE = mode; + } + try { + fn(); + } finally { + if (orig === undefined) { + delete process.env.HEADROOM_MODE; + } else { + process.env.HEADROOM_MODE = orig; + } + } +} + +// --------------------------------------------------------------------------- +// getHeadroomMode +// --------------------------------------------------------------------------- + +test("getHeadroomMode returns 'off' when HEADROOM_MODE is unset", () => { + withMode(undefined, () => { + assert.equal(getHeadroomMode(), "off"); + }); +}); + +test("getHeadroomMode returns 'conservative' when HEADROOM_MODE=conservative", () => { + withMode("conservative", () => { + assert.equal(getHeadroomMode(), "conservative"); + }); +}); + +test("getHeadroomMode returns 'aggressive' when HEADROOM_MODE=aggressive", () => { + withMode("aggressive", () => { + assert.equal(getHeadroomMode(), "aggressive"); + }); +}); + +// --------------------------------------------------------------------------- +// isCompressionActive +// --------------------------------------------------------------------------- + +test("isCompressionActive returns false when HEADROOM_MODE is unset (default off)", () => { + withMode(undefined, () => { + assert.equal(isCompressionActive(), false); + }); +}); + +test("isCompressionActive returns true when HEADROOM_MODE=conservative", () => { + withMode("conservative", () => { + assert.equal(isCompressionActive(), true); + }); +}); + +test("isCompressionActive returns true when HEADROOM_MODE=aggressive", () => { + withMode("aggressive", () => { + assert.equal(isCompressionActive(), true); + }); +}); + +// --------------------------------------------------------------------------- +// getCompressionCallback — structural shape tests (no live headroom-ai call) +// --------------------------------------------------------------------------- + +test("getCompressionCallback returns undefined when HEADROOM_MODE is off (default off)", () => { + withMode(undefined, () => { + assert.equal(getCompressionCallback(), undefined); + }); +}); + +test("getCompressionCallback returns a function when HEADROOM_MODE=conservative", () => { + withMode("conservative", () => { + const cb = getCompressionCallback(); + assert.equal(typeof cb, "function"); + }); +}); + +test("getCompressionCallback returns a function when HEADROOM_MODE=aggressive", () => { + withMode("aggressive", () => { + const cb = getCompressionCallback(); + assert.equal(typeof cb, "function"); + }); +}); + +test("getCompressionCallback returns an async function (returns a Promise)", () => { + withMode("conservative", () => { + const cb = getCompressionCallback(); + assert.ok(cb !== undefined); + // An async function always returns a Promise synchronously, before any + // `await` inside it (here, the dynamic `import("headroom-ai")`) resolves + // or rejects — so this holds regardless of whether headroom-ai succeeds. + const result = cb("test prompt"); + assert.ok(result instanceof Promise, "callback must return a Promise"); + // Consume any rejection so node:test doesn't treat it as an unhandled error. + result.catch(() => {}); + }); +}); diff --git a/.sandcastle/context-compressor.ts b/.sandcastle/context-compressor.ts index 00448ba..c6e9f68 100644 --- a/.sandcastle/context-compressor.ts +++ b/.sandcastle/context-compressor.ts @@ -15,24 +15,21 @@ /** Compression mode — off by default for safe rollout. */ export type HeadroomMode = "off" | "conservative" | "aggressive"; -const HEADROOM_MODE: HeadroomMode = - (process.env.HEADROOM_MODE as HeadroomMode) || "off"; - -const HEADROOM_MODEL = process.env.HEADROOM_MODEL || "claude-sonnet-4-6"; - -/** Current headroom mode (for tests and logging). */ +/** Current headroom mode — reads env var at call time so tests can set it after module load. */ export function getHeadroomMode(): HeadroomMode { - return HEADROOM_MODE; + return (process.env.HEADROOM_MODE as HeadroomMode) || "off"; } /** Resolve the compression callback, or undefined if disabled. */ export function getCompressionCallback() { - if (HEADROOM_MODE === "off") return undefined; + const mode = getHeadroomMode(); + if (mode === "off") return undefined; + const model = process.env.HEADROOM_MODEL || "claude-sonnet-4-6"; return async (prompt: string): Promise => { const { compress } = await import("headroom-ai"); const messages = [{ role: "user" as const, content: prompt }]; - const result = await compress(messages, { model: HEADROOM_MODEL }); + const result = await compress(messages, { model }); // CompressResult.messages is typed `any[]` (headroom preserves whatever // format was passed in) — validate the shape at runtime rather than // trusting it, so a proxy-side format drift degrades to uncompressed @@ -44,11 +41,17 @@ export function getCompressionCallback() { ); return prompt; } + // Token savings measurement: log before/after char counts (~4 chars/token). + const saved = prompt.length - content.length; + const pct = ((saved / prompt.length) * 100).toFixed(1); + console.log( + `[context-compressor] mode=${mode} ${prompt.length}→${content.length} chars (−${saved}, −${pct}%)`, + ); return content; }; } -/** Whether compression is active (for logging). */ +/** Whether compression is active (for proxy injection and logging). */ export function isCompressionActive(): boolean { - return HEADROOM_MODE !== "off"; + return getHeadroomMode() !== "off"; } diff --git a/.sandcastle/package.json b/.sandcastle/package.json index 316f264..03007f8 100644 --- a/.sandcastle/package.json +++ b/.sandcastle/package.json @@ -8,7 +8,7 @@ "postinstall": "patch-package", "start": "tsx main.ts", "typecheck": "tsc --noEmit", - "test": "node --import tsx --test reduce.test.ts sandbox-runner.test.ts reviewer-adapter.test.ts memory.test.ts memory-store.test.ts run-sh.test.ts init-sh.test.ts up-sh.test.ts afk-cmd.test.ts", + "test": "node --import tsx --test reduce.test.ts sandbox-runner.test.ts reviewer-adapter.test.ts memory.test.ts memory-store.test.ts run-sh.test.ts init-sh.test.ts up-sh.test.ts afk-cmd.test.ts context-compressor.test.ts", "test:integration": "SANDCASTLE_INTEGRATION=1 node --import tsx --test integration.test.ts" }, "devDependencies": { diff --git a/.sandcastle/sandbox-runner.test.ts b/.sandcastle/sandbox-runner.test.ts index 79ce82d..aecdaa5 100644 --- a/.sandcastle/sandbox-runner.test.ts +++ b/.sandcastle/sandbox-runner.test.ts @@ -123,3 +123,41 @@ test("claude tier: no sandboxEnv override when HEADROOM_MODE is off (default)", const input = buildAgentInput({ tier: "claude" }, STUB_ISSUE); assert.equal(input.sandboxEnv, undefined); }); + +test("claude tier agent gets ANTHROPIC_BASE_URL proxy override when HEADROOM_MODE=conservative", () => { + const orig = process.env.HEADROOM_MODE; + process.env.HEADROOM_MODE = "conservative"; + try { + const input = buildAgentInput({ tier: "claude" }, STUB_ISSUE); + assert.deepEqual(input.sandboxEnv, { ANTHROPIC_BASE_URL: "http://host.docker.internal:8787" }); + } finally { + if (orig === undefined) delete process.env.HEADROOM_MODE; + else process.env.HEADROOM_MODE = orig; + } +}); + +test("claude tier agent gets ANTHROPIC_BASE_URL proxy override when HEADROOM_MODE=aggressive", () => { + const orig = process.env.HEADROOM_MODE; + process.env.HEADROOM_MODE = "aggressive"; + try { + const input = buildAgentInput({ tier: "claude" }, STUB_ISSUE); + assert.deepEqual(input.sandboxEnv, { ANTHROPIC_BASE_URL: "http://host.docker.internal:8787" }); + } finally { + if (orig === undefined) delete process.env.HEADROOM_MODE; + else process.env.HEADROOM_MODE = orig; + } +}); + +test("local tier is never proxy-routed regardless of HEADROOM_MODE", () => { + const orig = process.env.HEADROOM_MODE; + process.env.HEADROOM_MODE = "conservative"; + try { + // local tier uses opencode; ANTHROPIC_BASE_URL proxy injection only + // applies to the claudeCode agent path (sandbox-runner.ts L225 tier gate). + const input = buildAgentInput({ tier: "local" }, STUB_ISSUE); + assert.equal(input.agent.name, "opencode"); + } finally { + if (orig === undefined) delete process.env.HEADROOM_MODE; + else process.env.HEADROOM_MODE = orig; + } +});