fix(core): guard against constructing an undecorated context resolver

TurnRepoContextResolver was still freely constructible, so a future
call site could silently lose elision. The factory now accepts an
injectable policy loader (also keeps tests off the machine's real
context.json), the raw class carries a do-not-construct note, and
factory-level tests pin both the decorator wiring and the default
policy behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Ramnique Singh 2026-07-07 20:24:46 +05:30
parent 331a5eda4e
commit 8a952a4b7e
3 changed files with 36 additions and 0 deletions

View file

@ -2,7 +2,9 @@ import { describe, expect, it } from "vitest";
import type { z } from "zod";
import type { TurnContext, TurnEvent } from "@x/shared/dist/turns.js";
import {
DEFAULT_ELISION_POLICY,
ElidingContextResolver,
createContextResolver,
elideHistoricImages,
elideHistoricMiddlePaneContent,
elideHistoricToolResults,
@ -307,6 +309,32 @@ const POLICY_OFF = {
middlePaneContent: false,
};
describe("createContextResolver", () => {
// The factory is the app's one sanctioned constructor (DI container and
// inspect CLI). If this stops returning the eliding decorator, every
// construction site silently regresses to full historic replay.
it("wraps the repo resolver in the eliding decorator", () => {
const resolver = createContextResolver({
turnRepo: new InMemoryTurnRepo(),
});
expect(resolver).toBeInstanceOf(ElidingContextResolver);
});
it("applies the default policy through the decorated resolver", async () => {
const repo = new InMemoryTurnRepo();
repo.seed(toolTurnLog(T1, [], "x".repeat(5000)));
// Pinned to the shipped defaults (not the machine's context.json) so
// the test is hermetic while still exercising the full wiring.
const resolved = await createContextResolver({
turnRepo: repo,
loadPolicy: () => DEFAULT_ELISION_POLICY,
}).resolve({ previousTurnId: T1 });
const tool = resolved.find((m) => m.role === "tool");
expect(tool?.content).toContain("elided");
expect(tool?.content.length).toBeLessThan(1000);
});
});
describe("ElidingContextResolver", () => {
function resolver(
repo: InMemoryTurnRepo,

View file

@ -230,10 +230,14 @@ export class ElidingContextResolver implements IContextResolver {
// the loop transmits.
export function createContextResolver({
turnRepo,
loadPolicy,
}: {
turnRepo: ITurnRepo;
// Injectable for tests; the app default reads config/context.json.
loadPolicy?: () => ElisionPolicy;
}): IContextResolver {
return new ElidingContextResolver({
inner: new TurnRepoContextResolver({ turnRepo }),
...(loadPolicy ? { loadPolicy } : {}),
});
}

View file

@ -28,6 +28,10 @@ export interface IContextResolver {
): Promise<z.infer<typeof ResolvedAgent>>;
}
// NOTE: app code must not construct this directly — use createContextResolver
// (context-elision.ts), which wraps it in the eliding decorator. A raw
// instance silently transmits full historic tool results, frames, and note
// snapshots. Direct construction is for tests of the raw resolution only.
export class TurnRepoContextResolver implements IContextResolver {
private readonly turnRepo: ITurnRepo;