From 5815e13fd2477bff95e205059411834d976c79dd Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Wed, 22 Apr 2026 23:03:19 -0400 Subject: [PATCH 01/10] test: run demo smoke without live llm credentials --- demos/llm_client.ts | 149 ++++++++++++++++++++++++++++++++++++++ tests/demos-smoke.test.ts | 22 +++--- 2 files changed, 162 insertions(+), 9 deletions(-) diff --git a/demos/llm_client.ts b/demos/llm_client.ts index 3107226..63d2528 100644 --- a/demos/llm_client.ts +++ b/demos/llm_client.ts @@ -11,6 +11,8 @@ export type LLMConfig = { model: string; }; +const DEMO_MOCK_ENV_VAR = 'CONTEXT_COMPILER_DEMO_MOCK'; + export class MissingDemoConfigError extends Error { readonly missing: string[]; readonly baseUrl: string | null; @@ -85,6 +87,149 @@ function endpointFor(baseUrl: string | null): string { return `${root}/chat/completions`; } +function isDemoMockEnabled(): boolean { + const raw = (process.env[DEMO_MOCK_ENV_VAR] ?? '').trim().toLowerCase(); + return raw === '1' || raw === 'true' || raw === 'yes' || raw === 'on'; +} + +function splitItems(raw: string): string[] { + return raw + .split(',') + .map((item) => item.trim().toLowerCase()) + .filter((item) => item !== '' && item !== '(none)'); +} + +function parseCompiledState(systemPrompt: string): { + premise: string | null; + useItems: string[]; + prohibitItems: string[]; +} { + const premiseMatch = systemPrompt.match(/^- premise:\s*(.+)$/im); + const useMatch = systemPrompt.match(/^- use policy items:\s*(.+)$/im); + const prohibitMatch = systemPrompt.match(/^- prohibited policy items:\s*(.+)$/im); + + return { + premise: premiseMatch ? premiseMatch[1].trim() : null, + useItems: useMatch ? splitItems(useMatch[1]) : [], + prohibitItems: prohibitMatch ? splitItems(prohibitMatch[1]) : [] + }; +} + +function parseDirectivePremises(messages: Message[]): { first: string | null; latest: string | null } { + let first: string | null = null; + let latest: string | null = null; + const directiveRe = /^\s*(?:set premise|change premise to)\s+(.+?)\s*$/i; + + for (const message of messages) { + if (message.role !== 'user') { + continue; + } + const match = message.content.match(directiveRe); + if (!match) { + continue; + } + const premise = match[1].trim(); + if (first === null) { + first = premise; + } + latest = premise; + } + + return { first, latest }; +} + +function chooseMockPremise(messages: Message[], systemPrompt: string, compiledPremise: string | null): string { + if (compiledPremise && compiledPremise !== '(unset)') { + return compiledPremise; + } + + const { first, latest } = parseDirectivePremises(messages); + const joined = messages + .filter((message) => message.role === 'user') + .map((message) => message.content) + .join('\n') + .toLowerCase(); + const strongPrompt = /prioritize explicit user directives|careful assistant|first line must be exactly premise:/i.test( + systemPrompt + ); + + if (!strongPrompt) { + if (joined.includes('beef stew')) { + return 'beef stew'; + } + return first ?? latest ?? 'vegetarian curry'; + } + return latest ?? first ?? 'vegan curry'; +} + +function mockCompletion(messages: Message[]): string { + const systemPrompt = messages.find((message) => message.role === 'system')?.content ?? ''; + const allUserText = messages + .filter((message) => message.role === 'user') + .map((message) => message.content) + .join('\n'); + const lowered = allUserText.toLowerCase(); + const compiled = parseCompiledState(systemPrompt); + const premise = chooseMockPremise(messages, systemPrompt, compiled.premise); + const isCompiled = /follow authoritative compiled state exactly\./i.test(systemPrompt); + + if (/reply with exactly:\s*ok/i.test(allUserText)) { + return 'OK'; + } + + if (/first line must be tool:/i.test(allUserText)) { + const tool = isCompiled && !compiled.prohibitItems.includes('kubectl') ? 'kubectl' : 'docker'; + return [`TOOL:${tool}`, `ACTION:Use ${tool} to deploy the service.`].join('\n'); + } + + if (/first line must be action:/i.test(allUserText)) { + const contradictoryPeanutDirectives = lowered.includes('prohibit peanuts') && lowered.includes('use peanuts'); + if (isCompiled && contradictoryPeanutDirectives) { + return 'ACTION:clarify\nRequest is contradictory; please confirm policy.'; + } + return 'ACTION:proceed\nProceeding with a best-effort interpretation.'; + } + + const wantsPremiseTag = /first line must be premise:/i.test(allUserText); + const wantsRecipe = /\b(recipe|ingredients|steps|curry)\b/i.test(allUserText); + const wantsPlan = /\b(plan|shopping list|dinner)\b/i.test(allUserText); + const blocksPeanuts = compiled.prohibitItems.includes('peanuts') || compiled.prohibitItems.includes('peanut'); + + if (wantsRecipe && blocksPeanuts) { + const prefix = wantsPremiseTag ? [`PREMISE:${premise}`] : []; + return [ + ...prefix, + 'I cannot provide a peanut recipe because it conflicts with policy.', + 'Ingredients:', + '- chickpeas', + '- coconut milk', + '- garlic', + 'Steps:', + '1. Saute garlic.', + '2. Simmer chickpeas in coconut milk.', + '3. Serve hot.' + ].join('\n'); + } + + if (wantsPremiseTag || wantsPlan || wantsRecipe) { + const lines: string[] = []; + if (wantsPremiseTag) { + lines.push(`PREMISE:${premise}`); + } + lines.push('Shopping list:'); + lines.push('- onions'); + lines.push('- tomatoes'); + lines.push(`- ${premise}`); + lines.push('Steps:'); + lines.push(`1. Prepare a ${premise} base.`); + lines.push('2. Simmer until flavors combine.'); + lines.push('3. Serve warm.'); + return lines.join('\n'); + } + + return 'OK'; +} + function parseRetryAfterSeconds(headers: Headers): number | null { const raw = headers.get('retry-after') ?? headers.get('Retry-After'); if (!raw) { @@ -200,6 +345,10 @@ export async function completeMessages( delaySeconds?: number; } ): Promise { + if (isDemoMockEnabled()) { + return mockCompletion(messages); + } + const config = loadConfig(); const targetModel = options?.model ?? config.model; const configuredDelay = options?.delaySeconds && options.delaySeconds > 0 ? options.delaySeconds : defaultLlmDelaySeconds; diff --git a/tests/demos-smoke.test.ts b/tests/demos-smoke.test.ts index e76e6bb..e874841 100644 --- a/tests/demos-smoke.test.ts +++ b/tests/demos-smoke.test.ts @@ -6,8 +6,6 @@ import { beforeAll, describe, expect, it } from 'vitest'; const ROOT = resolve(process.cwd()); const DIST_DEMOS = resolve(ROOT, 'dist', 'demos'); -const HAS_DEMO_ENV = Boolean(process.env.OPENAI_API_KEY && process.env.MODEL); - function runNodeScript(file: string, args: string[] = [], envOverride?: Record) { const script = resolve(DIST_DEMOS, file); const run = spawnSync(process.execPath, [script, ...args], { @@ -25,6 +23,14 @@ function runNodeScript(file: string, args: string[] = [], envOverride?: Record { beforeAll(() => { const build = spawnSync('npm', ['run', 'build'], { @@ -46,9 +52,7 @@ describe('demos smoke', () => { expect(run.stdout).toContain('Missing variables: OPENAI_API_KEY, MODEL'); }); - const describeWhenConfigured = HAS_DEMO_ENV ? describe : describe.skip; - - describeWhenConfigured('with configured llm env', () => { + describe('with configured llm env or demo mock', () => { it('runs scored demos with comparative markers', () => { const demos = [ ['01_llm_contradiction_clarify.js', '01_contradiction_block'], @@ -60,7 +64,7 @@ describe('demos smoke', () => { ] as const; for (const [file, marker] of demos) { - const run = runNodeScript(file); + const run = runNodeScriptWithMock(file); expect(run.status).toBe(0); expect(run.stdout).toContain(marker); expect(run.stdout).toContain('baseline:'); @@ -73,7 +77,7 @@ describe('demos smoke', () => { }, 180_000); it('runs informational demo 06 with compaction markers', () => { - const run = runNodeScript('06_llm_context_compaction.js'); + const run = runNodeScriptWithMock('06_llm_context_compaction.js'); expect(run.status).toBe(0); expect(run.stdout).toContain('06_context_compaction'); expect(run.stdout).toContain('context scaling:'); @@ -85,13 +89,13 @@ describe('demos smoke', () => { }, 120_000); it('runs demo runner for single and all with summary markers', () => { - const single = runNodeScript('run_demo.js', ['1']); + const single = runNodeScriptWithMock('run_demo.js', ['1']); expect(single.status).toBe(0); expect(single.stdout).toContain('01_contradiction_block'); expect(single.stdout).toContain('baseline:'); expect(single.stdout).toContain('compiler:'); - const all = runNodeScript('run_demo.js', ['all']); + const all = runNodeScriptWithMock('run_demo.js', ['all']); expect(all.status).toBe(0); expect(all.stdout).toContain('Summary:'); expect(all.stdout).toContain('Evaluative demos:'); From 56d5fc374b614bd44bd631d79ce2254373239888 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Wed, 22 Apr 2026 23:18:37 -0400 Subject: [PATCH 02/10] docs: add test coverage expectations for user-facing behavior --- AGENTS.md | 20 ++++++++++++++++++++ CONTRIBUTING.md | 20 ++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 9a45444..642f234 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -78,6 +78,26 @@ A change is correct only if all fixtures pass. Do not modify fixtures to make tests pass. +## Test Coverage Expectations + +Before opening a PR, consider: + +- Does this change affect any user-facing behavior? +- If so, is that behavior covered by tests? + +User-facing behavior includes: + +- Engine decision outcomes (`kind`, `prompt_to_user`, and returned `state`) +- Checkpoint export/import and continuation behavior +- Clarify/confirmation flows (`yes` / `no`) +- Transcript replay behavior and compaction-related behavior +- Integration behavior (examples, demo runner, and integration scripts) +- Integration error-path normalization + +If a user-facing behavior is changed or introduced, add or update tests to cover it. + +Do not rely solely on coverage metrics. + ## Workflow Typical workflow: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bb8510f..b945285 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,6 +25,26 @@ All changes must: - Exact match when a fixture specifies a string - Non-empty string when a fixture uses `null` +## Test Coverage Expectations + +Before opening a PR, consider: + +- Does this change affect any user-facing behavior? +- If so, is that behavior covered by tests? + +User-facing behavior includes: + +- Engine decision outcomes (`kind`, `prompt_to_user`, and returned `state`) +- Checkpoint export/import and continuation behavior +- Clarify/confirmation flows (`yes` / `no`) +- Transcript replay behavior and compaction-related behavior +- Integration behavior (examples, demo runner, and integration scripts) +- Integration error-path normalization + +If a user-facing behavior is changed or introduced, add or update tests to cover it. + +Do not rely solely on coverage metrics. + ## What Not to Do Do not: From 80d2bebf4f484b29e8c2c9debe278090d750a614 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Wed, 22 Apr 2026 23:24:06 -0400 Subject: [PATCH 03/10] chore: fix vite audit vulnerability in dev dependencies --- package-lock.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 34f4ca7..e3e4867 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "@rlippmann/context-compiler", "version": "0.5.2", + "license": "Apache-2.0", "devDependencies": { "typescript": "^5.9.3", "vitest": "^3.2.4" @@ -1448,9 +1449,9 @@ } }, "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, "license": "MIT", "dependencies": { From 87776ec32b745c6eabbf6925caff3f9a2919c003 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Wed, 22 Apr 2026 23:32:42 -0400 Subject: [PATCH 04/10] docs: clarify fixture sync-only update policy --- AGENTS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 642f234..eca2d20 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -78,6 +78,10 @@ A change is correct only if all fixtures pass. Do not modify fixtures to make tests pass. +Fixture updates are allowed only when syncing from the authoritative Python source for the targeted compatibility line. + +If synced fixtures introduce failures, fix TypeScript behavior rather than editing fixture expectations. + ## Test Coverage Expectations Before opening a PR, consider: From 4ad1df830facf8214da02cc75cd912c8335feecd Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Wed, 22 Apr 2026 23:35:39 -0400 Subject: [PATCH 05/10] chore: run fixtures drift check in ci --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66bd6c7..29b485e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,12 @@ jobs: - name: Install run: npm ci + - name: Checkout Python fixture source + run: git clone --depth 1 https://github.com/rlippmann/context-compiler ../context-compiler + + - name: Check fixture drift + run: npm run fixtures:check + - name: Build run: npm run build From 60bf3c83984e08a93e66f1fccbcb2b7a6b5afd09 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Wed, 22 Apr 2026 23:51:52 -0400 Subject: [PATCH 06/10] chore: pin fixture drift source to python 0.5-compatible commit --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29b485e..c2bf49a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,10 @@ jobs: run: npm ci - name: Checkout Python fixture source - run: git clone --depth 1 https://github.com/rlippmann/context-compiler ../context-compiler + run: | + git clone --depth 1 https://github.com/rlippmann/context-compiler ../context-compiler + git -C ../context-compiler fetch --depth 1 origin 391a186 + git -C ../context-compiler checkout --detach 391a186 - name: Check fixture drift run: npm run fixtures:check From c93a4503a428eb3b74b013cace2f3d13865d836e Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Wed, 22 Apr 2026 23:58:27 -0400 Subject: [PATCH 07/10] test: add error-path clarify and idempotent regression coverage --- tests/engine_state_immutability.test.ts | 118 ++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/tests/engine_state_immutability.test.ts b/tests/engine_state_immutability.test.ts index d3cdf5e..84042b3 100644 --- a/tests/engine_state_immutability.test.ts +++ b/tests/engine_state_immutability.test.ts @@ -39,3 +39,121 @@ describe('engine state immutability', () => { expect(engine.state.policies.docker).toBe('use'); }); }); + +describe('engine error-path regressions', () => { + function expectClarifyNoMutation(engineInput: ReturnType, input: string, expectedPrompt: string): void { + const before = engineInput.state; + const decision = engineInput.step(input); + + expect(decision.kind).toBe('clarify'); + expect(decision.state).toBeNull(); + expect(decision.prompt_to_user).toBe(expectedPrompt); + expect(engineInput.state).toEqual(before); + } + + it('empty and whitespace use payload clarifies with no mutation', () => { + const engine = createEngine(); + const prompt = "Policy item cannot be empty.\nUse 'use ' with a non-empty value."; + + expectClarifyNoMutation(engine, 'use', prompt); + expectClarifyNoMutation(engine, 'use ', prompt); + }); + + it('empty and whitespace prohibit payload clarifies with no mutation', () => { + const engine = createEngine(); + const prompt = "Policy item cannot be empty.\nUse 'prohibit ' with a non-empty value."; + + expectClarifyNoMutation(engine, 'prohibit', prompt); + expectClarifyNoMutation(engine, 'prohibit ', prompt); + }); + + it('empty and whitespace remove policy payload clarifies with no mutation', () => { + const engine = createEngine(); + const prompt = "Policy item cannot be empty.\nUse 'remove policy ' with a non-empty value."; + + expectClarifyNoMutation(engine, 'remove policy', prompt); + expectClarifyNoMutation(engine, 'remove policy ', prompt); + }); + + it('incomplete replacement intent clarifies with no mutation', () => { + const engine = createEngine(); + const prompt = + "Replacement requires both new and old items.\nUse 'use instead of ' with non-empty values."; + + expectClarifyNoMutation(engine, 'use kubectl instead of', prompt); + expectClarifyNoMutation(engine, 'use instead of docker', prompt); + }); + + it('contradictions clarify and preserve state in both directions', () => { + const prohibitToUse = createEngine({ state: { premise: null, policies: { docker: 'prohibit' }, version: 2 } }); + expectClarifyNoMutation( + prohibitToUse, + 'use docker', + "'docker' is already prohibited.\nOnly one policy per item is allowed.\nUse 'reset policies' to change it." + ); + + const useToProhibit = createEngine({ state: { premise: null, policies: { docker: 'use' }, version: 2 } }); + expectClarifyNoMutation( + useToProhibit, + 'prohibit docker', + "'docker' is already in use.\nOnly one policy per item is allowed.\nUse 'reset policies' to change it." + ); + }); + + it('replacement-intent clarify prompts remain stable', () => { + const missingSource = createEngine(); + expectClarifyNoMutation( + missingSource, + 'use kubectl instead of docker', + 'No exact policy found for "docker".\nReplacement requires an exact policy match.\nConfirm to use "kubectl" and keep existing policies?' + ); + + const oldIsProhibit = createEngine({ state: { premise: null, policies: { docker: 'prohibit' }, version: 2 } }); + expectClarifyNoMutation( + oldIsProhibit, + 'use kubectl instead of docker', + '"docker" is currently prohibited. Did you mean to remove it and use "kubectl" instead?' + ); + + const newIsProhibit = createEngine({ + state: { premise: null, policies: { docker: 'use', kubectl: 'prohibit' }, version: 2 } + }); + expectClarifyNoMutation( + newIsProhibit, + 'use kubectl instead of docker', + '"kubectl" is currently prohibited. Did you mean to remove "docker" and use "kubectl" instead?' + ); + }); + + it('pending clarification unmatched input reuses prompt and preserves state', () => { + const engine = createEngine({ + state: { premise: null, policies: { docker: 'use', kubectl: 'prohibit' }, version: 2 } + }); + + const first = engine.step('use kubectl instead of docker'); + expect(first.kind).toBe('clarify'); + expect(first.state).toBeNull(); + const pendingPrompt = first.prompt_to_user; + const before = engine.state; + + const second = engine.step('maybe later'); + expect(second.kind).toBe('clarify'); + expect(second.state).toBeNull(); + expect(second.prompt_to_user).toBe(pendingPrompt); + expect(engine.state).toEqual(before); + }); + + it('idempotent assertion/update paths remain update (not clarify)', () => { + const removeMissing = createEngine({ state: { premise: null, policies: { docker: 'use' }, version: 2 } }); + const removeDecision = removeMissing.step('remove policy podman'); + expect(removeDecision.kind).toBe('update'); + expect(removeDecision.prompt_to_user).toBeNull(); + expect(removeMissing.state).toEqual({ premise: null, policies: { docker: 'use' }, version: 2 }); + + const keepUse = createEngine({ state: { premise: null, policies: { docker: 'use' }, version: 2 } }); + const useDecision = keepUse.step('use docker'); + expect(useDecision.kind).toBe('update'); + expect(useDecision.prompt_to_user).toBeNull(); + expect(keepUse.state).toEqual({ premise: null, policies: { docker: 'use' }, version: 2 }); + }); +}); From 8b716a6aa505a0c1b2cb2b67f6d3fdf2e126bdcb Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Thu, 23 Apr 2026 00:04:45 -0400 Subject: [PATCH 08/10] test: tighten smoke stderr assertions and document mock mode --- tests/demos-smoke.test.ts | 7 +++++++ tests/examples-smoke.test.ts | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/tests/demos-smoke.test.ts b/tests/demos-smoke.test.ts index e874841..a6c9bba 100644 --- a/tests/demos-smoke.test.ts +++ b/tests/demos-smoke.test.ts @@ -24,6 +24,8 @@ function runNodeScript(file: string, args: string[] = [], envOverride?: Record { MODEL: '' }); expect(run.status).toBe(2); + expect(run.stderr.trim()).toBe(''); expect(run.stdout).toContain('Unable to run LLM demos: missing model configuration.'); expect(run.stdout).toContain('Missing variables: OPENAI_API_KEY, MODEL'); }); @@ -66,6 +69,7 @@ describe('demos smoke', () => { for (const [file, marker] of demos) { const run = runNodeScriptWithMock(file); expect(run.status).toBe(0); + expect(run.stderr.trim()).toBe(''); expect(run.stdout).toContain(marker); expect(run.stdout).toContain('baseline:'); expect(run.stdout).toContain('compiler:'); @@ -79,6 +83,7 @@ describe('demos smoke', () => { it('runs informational demo 06 with compaction markers', () => { const run = runNodeScriptWithMock('06_llm_context_compaction.js'); expect(run.status).toBe(0); + expect(run.stderr.trim()).toBe(''); expect(run.stdout).toContain('06_context_compaction'); expect(run.stdout).toContain('context scaling:'); expect(run.stdout).toContain('compacted transcript:'); @@ -91,12 +96,14 @@ describe('demos smoke', () => { it('runs demo runner for single and all with summary markers', () => { const single = runNodeScriptWithMock('run_demo.js', ['1']); expect(single.status).toBe(0); + expect(single.stderr.trim()).toBe(''); expect(single.stdout).toContain('01_contradiction_block'); expect(single.stdout).toContain('baseline:'); expect(single.stdout).toContain('compiler:'); const all = runNodeScriptWithMock('run_demo.js', ['all']); expect(all.status).toBe(0); + expect(all.stderr.trim()).toBe(''); expect(all.stdout).toContain('Summary:'); expect(all.stdout).toContain('Evaluative demos:'); expect(all.stdout).toContain('Baseline results:'); diff --git a/tests/examples-smoke.test.ts b/tests/examples-smoke.test.ts index 29bc711..1d78017 100644 --- a/tests/examples-smoke.test.ts +++ b/tests/examples-smoke.test.ts @@ -33,6 +33,7 @@ describe('examples smoke', () => { it('01 persistent guardrails', () => { const run = runExampleScript('01_persistent_guardrails.js'); expect(run.status).toBe(0); + expect(run.stderr.trim()).toBe(''); expect(run.stdout).toContain('example 01: persistent guardrails'); expect(run.stdout).toContain('"prohibitedPolicies"'); }); @@ -40,6 +41,7 @@ describe('examples smoke', () => { it('02 configuration and correction', () => { const run = runExampleScript('02_configuration_and_correction.js'); expect(run.status).toBe(0); + expect(run.stderr.trim()).toBe(''); expect(run.stdout).toContain('example 02: configuration and correction'); expect(run.stdout).toContain('"finalPremise": "vegan curry"'); }); @@ -47,6 +49,7 @@ describe('examples smoke', () => { it('03 ambiguity with clarification', () => { const run = runExampleScript('03_ambiguity_with_clarification.js'); expect(run.status).toBe(0); + expect(run.stderr.trim()).toBe(''); expect(run.stdout).toContain('example 03: ambiguity with clarification'); expect(run.stdout).toContain('"clarifyKind": "clarify"'); }); @@ -54,6 +57,7 @@ describe('examples smoke', () => { it('04 tool governance denylist', () => { const run = runExampleScript('04_tool_governance_denylist.js'); expect(run.status).toBe(0); + expect(run.stderr.trim()).toBe(''); expect(run.stdout).toContain('example 04: tool governance denylist'); expect(run.stdout).toContain('"blockedTools"'); }); @@ -61,6 +65,7 @@ describe('examples smoke', () => { it('05 llm integration pattern', () => { const run = runExampleScript('05_llm_integration_pattern.js'); expect(run.status).toBe(0); + expect(run.stderr.trim()).toBe(''); expect(run.stdout).toContain('example 05: llm integration pattern'); expect(run.stdout).toContain('"actions"'); }); @@ -68,6 +73,7 @@ describe('examples smoke', () => { it('06 transcript replay', () => { const run = runExampleScript('06_transcript_replay.js'); expect(run.status).toBe(0); + expect(run.stderr.trim()).toBe(''); expect(run.stdout).toContain('example 06: transcript replay'); expect(run.stdout).toContain('"freshReplayKind": "state"'); }); @@ -75,6 +81,7 @@ describe('examples smoke', () => { it('07 single policy correction', () => { const run = runExampleScript('07_single_policy_correction.js'); expect(run.status).toBe(0); + expect(run.stderr.trim()).toBe(''); expect(run.stdout).toContain('example 07: single policy correction'); expect(run.stdout).toContain('"finalPolicy": "use"'); }); From 355b63ae0044a47daa9b88ac1b47ab14a5ea7ac7 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Thu, 23 Apr 2026 00:07:05 -0400 Subject: [PATCH 09/10] chore: pin fixture drift source to python v0.6.0 tag --- .github/workflows/ci.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2bf49a..e483cd6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,10 +27,7 @@ jobs: run: npm ci - name: Checkout Python fixture source - run: | - git clone --depth 1 https://github.com/rlippmann/context-compiler ../context-compiler - git -C ../context-compiler fetch --depth 1 origin 391a186 - git -C ../context-compiler checkout --detach 391a186 + run: git clone --depth 1 --branch v0.6.0 https://github.com/rlippmann/context-compiler ../context-compiler - name: Check fixture drift run: npm run fixtures:check From 334f94726a50f1624b8082d12c5cfbba20b2f47b Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Thu, 23 Apr 2026 00:11:55 -0400 Subject: [PATCH 10/10] docs: annotate why fixture drift gate pins python v0.6.0 --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e483cd6..c49f858 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,8 @@ jobs: run: npm ci - name: Checkout Python fixture source + # TS 0.5.2 fixtures include cases added after Python v0.5.2; pin to v0.6.0 + # to keep fixture drift checks aligned with the current TS fixture corpus. run: git clone --depth 1 --branch v0.6.0 https://github.com/rlippmann/context-compiler ../context-compiler - name: Check fixture drift