mirror of
https://github.com/teamchong/pxpipe.git
synced 2026-07-22 02:02:51 +02:00
9bca945550
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.
141 lines
6.2 KiB
TypeScript
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');
|
|
});
|
|
});
|