Files
pxpipe/tests/env-split.test.ts
teamchong 9bca945550 fix: diff-based static/volatile env split replaces blanket relocation
The relocated '# Environment' block put model-identity/catalog lines in
the LAST user message every turn, steering subagent picks to fable
instead of haiku (LinkedIn report). splitEnvByVolatility now learns per
project (claudeMdSha) which env entries are byte-stable and promotes
them into the imaged slab; churned/new entries keep the live-tail
relocation, with the identity regex retained as a second layer.

Invariants (pinned by tests):
- first-ever sightings stay volatile: git state never bakes into a
  fresh session's image (48.8% cold-create fix intact)
- the slab never re-renders mid-session: static side frozen byte-exact;
  a churned promoted entry re-emits fresh text on the tail (supersedes
  its stale slab copy) and demotes next session via sticky churn history
- no user message -> env stays in system, unchanged

Telemetry: envStaticChars / envVolatileKeys.
2026-07-13 15:46:07 -04:00

141 lines
6.2 KiB
TypeScript

/**
* Unit contract for splitEnvByVolatility — the diff-based static/volatile
* `# Environment` split (replaces blanket relocation of the whole section).
*
* The three invariants (also pinned end-to-end in cache-stability-e2e):
* 1. First-ever sighting is volatile: no history → nothing promotes, so a
* fresh session/project never bakes git state into the slab image.
* 2. The static side is frozen byte-exact per session: a promoted entry
* that churns mid-session must NOT change the slab bytes — its fresh
* text re-emits on the volatile tail instead, and the next session
* demotes it (sticky churn history).
* 3. No project key → no learning: everything stays volatile.
*
* Run just this file: pnpm vitest run tests/env-split.test.ts
*/
import { beforeEach, describe, expect, it } from 'vitest';
import { resetEnvSplitState, splitEnvByVolatility } from '../src/core/transform.js';
const BASE_ENV = [
'# Environment',
'You have been invoked in the following environment: ',
' - Primary working directory: /repo',
' - Is a git repository: true',
' - Additional working directories:',
' - /tmp',
' - /extra',
' - Platform: darwin',
' - OS Version: Darwin 25.5.0',
' - You are powered by the model named Fable 5. The exact model ID is claude-fable-5.',
].join('\n');
const envWithGit = (git: string) => `${BASE_ENV}\n - Git status: ${git}`;
beforeEach(() => resetEnvSplitState());
describe('splitEnvByVolatility', () => {
it('first-ever sighting: everything volatile, nothing promoted to the slab', () => {
const r = splitEnvByVolatility(envWithGit('clean'), 'proj', 's1');
expect(r.staticEnv).toBe('');
expect(r.volatileEnv).toContain('# Environment');
expect(r.volatileEnv).toContain('Git status: clean');
expect(r.volatileEnv).toContain('Platform: darwin');
expect(r.volatileKeys).toContain('Git status');
expect(r.volatileKeys).toContain('Platform');
});
it('no project key → no learning, whole section passes through volatile', () => {
// Two stable sightings that WOULD promote under a project key…
splitEnvByVolatility(BASE_ENV, undefined, 's1');
const r = splitEnvByVolatility(BASE_ENV, undefined, 's2');
expect(r.staticEnv).toBe('');
expect(r.volatileEnv).toBe(BASE_ENV);
});
it('empty env is a no-op', () => {
expect(splitEnvByVolatility('', 'proj', 's1')).toEqual({
staticEnv: '',
volatileEnv: '',
volatileKeys: [],
});
});
it('stable entries promote in the NEXT session; churned entries stay on the tail', () => {
// Session 1: git state churns turn-to-turn, the rest is stable.
splitEnvByVolatility(envWithGit('clean'), 'proj', 's1');
splitEnvByVolatility(envWithGit('M src/a.ts'), 'proj', 's1');
// Session 2: stable entries ride the slab; git stays volatile.
const r = splitEnvByVolatility(envWithGit('M src/a.ts'), 'proj', 's2');
expect(r.staticEnv).toContain('# Environment');
expect(r.staticEnv).toContain('Platform: darwin');
expect(r.staticEnv).toContain('Is a git repository: true');
expect(r.staticEnv).not.toContain('Git status');
expect(r.volatileEnv).toContain('# Environment'); // header rides both sides
expect(r.volatileEnv).toContain('Git status: M src/a.ts');
expect(r.volatileEnv).not.toContain('Platform');
expect(r.volatileKeys).toEqual(['Git status']);
});
it('multi-line entries (nested working directories) travel as one unit', () => {
splitEnvByVolatility(BASE_ENV, 'proj', 's1');
const r = splitEnvByVolatility(BASE_ENV, 'proj', 's2');
expect(r.staticEnv).toContain(' - Additional working directories:\n - /tmp\n - /extra');
expect(r.volatileEnv).toBe(''); // fully stable env → nothing on the tail
});
it('identity/catalog lines promote like any stable entry (slab, not tail)', () => {
splitEnvByVolatility(BASE_ENV, 'proj', 's1');
const r = splitEnvByVolatility(BASE_ENV, 'proj', 's2');
expect(r.staticEnv).toContain('You are powered by the model named Fable 5');
expect(r.volatileEnv).not.toContain('You are powered by');
});
it('slab bytes stay frozen all session even when a promoted entry churns', () => {
splitEnvByVolatility(BASE_ENV, 'proj', 's1');
const t1 = splitEnvByVolatility(BASE_ENV, 'proj', 's2');
expect(t1.staticEnv).not.toBe('');
// Mid-session churn of a promoted entry (e.g. /model switch, OS update).
const changed = BASE_ENV.replace('Platform: darwin', 'Platform: linux');
const t2 = splitEnvByVolatility(changed, 'proj', 's2');
expect(t2.staticEnv).toBe(t1.staticEnv); // invariant 2: never re-renders
expect(t2.volatileEnv).toContain('Platform: linux'); // fresh copy supersedes
// …and the stale slab copy is NOT duplicated on the tail.
expect(t2.volatileEnv).not.toContain('Platform: darwin');
// Sticky churn history: the next session demotes the entry.
const s3 = splitEnvByVolatility(changed, 'proj', 's3');
expect(s3.staticEnv).not.toContain('Platform');
expect(s3.volatileEnv).toContain('Platform: linux');
});
it('an entry appearing mid-session rides the tail without touching the slab', () => {
splitEnvByVolatility(BASE_ENV, 'proj', 's1');
const t1 = splitEnvByVolatility(BASE_ENV, 'proj', 's2');
const t2 = splitEnvByVolatility(`${BASE_ENV}\n - Shell: zsh`, 'proj', 's2');
expect(t2.staticEnv).toBe(t1.staticEnv);
expect(t2.volatileEnv).toContain('Shell: zsh');
});
it('bare `Key: value` format (no bullets) splits the same way', () => {
const env = '# Environment\nWorking directory: /repo\nPlatform: darwin\nGit status:\nM src/a.ts';
splitEnvByVolatility(env, 'proj', 's1');
const r = splitEnvByVolatility(
'# Environment\nWorking directory: /repo\nPlatform: darwin\nGit status:\nD src/b.ts',
'proj',
's2',
);
expect(r.staticEnv).toContain('Working directory: /repo');
expect(r.staticEnv).not.toContain('Git status');
expect(r.volatileEnv).toContain('D src/b.ts');
});
it('cross-project isolation: sightings under one project never promote another', () => {
splitEnvByVolatility(BASE_ENV, 'projA', 's1');
const r = splitEnvByVolatility(BASE_ENV, 'projB', 's2');
expect(r.staticEnv).toBe('');
expect(r.volatileEnv).toContain('Platform: darwin');
});
});