From 8a952a4b7e0eb94e8cb80b59b88560ac285f4a3e Mon Sep 17 00:00:00 2001 From: Ramnique Singh <30795890+ramnique@users.noreply.github.com> Date: Tue, 7 Jul 2026 20:24:46 +0530 Subject: [PATCH] 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 --- .../core/src/turns/context-elision.test.ts | 28 +++++++++++++++++++ .../core/src/turns/context-elision.ts | 4 +++ .../core/src/turns/context-resolver.ts | 4 +++ 3 files changed, 36 insertions(+) diff --git a/apps/x/packages/core/src/turns/context-elision.test.ts b/apps/x/packages/core/src/turns/context-elision.test.ts index 19f9c5ef..88f3ff28 100644 --- a/apps/x/packages/core/src/turns/context-elision.test.ts +++ b/apps/x/packages/core/src/turns/context-elision.test.ts @@ -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, diff --git a/apps/x/packages/core/src/turns/context-elision.ts b/apps/x/packages/core/src/turns/context-elision.ts index 4db63a57..22d7d175 100644 --- a/apps/x/packages/core/src/turns/context-elision.ts +++ b/apps/x/packages/core/src/turns/context-elision.ts @@ -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 } : {}), }); } diff --git a/apps/x/packages/core/src/turns/context-resolver.ts b/apps/x/packages/core/src/turns/context-resolver.ts index 3785cc26..623bdb4e 100644 --- a/apps/x/packages/core/src/turns/context-resolver.ts +++ b/apps/x/packages/core/src/turns/context-resolver.ts @@ -28,6 +28,10 @@ export interface IContextResolver { ): Promise>; } +// 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;