mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-02 17:40:02 +02:00
- C0/C1: the five never-green evals (skill-e2e-autoplan-chain and
skill-e2e-plan-{ceo,eng,design,devex}-finding-count) failed on harness and
budget, never on skill behavior; delete them, their touchfile/tier ids,
AUTOPLAN_CHAIN_BUDGET and the dedicated eighth periodic slice (--slices 7).
- C2: delete the helper groups whose only paid consumers were those files
(11 modules), trim claude-pty-runner and eng-seeded-coverage to the paid
closure, and delete the free replay tests whose assertions exercised only
that dead code (89 files, 135 orphaned fixtures). Blocks that used dead code
only as input for a live subject keep their assertions: the multiSelect
default moved to plan-review-decisions, runner PTY tests use inline caller
policies, and the timer-safe budget checks moved to eng-finding-retry-budget.
- The eight production-touching files stay except ceo-current-decision-record
(its template read only feeds the retired counter).
- CARVE_GUARDS.autoplan is behavioral 'none'; TODOS records the lost chain
and per-finding cadence coverage with their re-entry tests.
114 lines
6.2 KiB
TypeScript
114 lines
6.2 KiB
TypeScript
import { describe, expect, test } from 'bun:test';
|
||
import * as fs from 'node:fs';
|
||
import * as os from 'node:os';
|
||
import * as path from 'node:path';
|
||
import { createPtyScreen } from './helpers/pty-screen';
|
||
import { capturePlanCountQuestion, createPlanCountPermissionGuard, isPermissionDialogVisible, stripAnsi } from './helpers/claude-pty-runner';
|
||
import { createPlanCountSnapshotWriter } from './helpers/plan-count-artifacts';
|
||
|
||
function seed(frame: { initial: string[]; cursor: { x: number; y: number } }): string {
|
||
return '\x1b[?1049h\x1b[2J' + frame.initial.map((line, i) => `\x1b[${i + 1};1H${line}\x1b[K`).join('') +
|
||
`\x1b[${frame.cursor.y + 1};${frame.cursor.x + 1}H`;
|
||
}
|
||
const fixture = (name: string) => JSON.parse(fs.readFileSync(path.join(import.meta.dir, 'fixtures/pty-screen', name + '.json'), 'utf8'));
|
||
|
||
describe('owned PTY viewport', () => {
|
||
test('preserves cursor positioning, carriage return, erase, and alternate-screen restoration', async () => {
|
||
const screen = await createPtyScreen(20, 4);
|
||
try {
|
||
screen.write('normal');
|
||
screen.write('\x1b[?1049h\x1b[2J\x1b[Habcdef\rX\x1b[4G!\x1b[K');
|
||
expect((await screen.read()).split('\n')[0]).toBe('Xbc!');
|
||
screen.write('\x1b[2;1Hsecond\x1b[2J\x1b[Hready');
|
||
expect(await screen.read()).toBe('ready\n\n\n');
|
||
screen.write('\x1b[?1049l');
|
||
expect((await screen.read()).split('\n')[0]).toBe('normal');
|
||
} finally { await screen.dispose(); }
|
||
});
|
||
|
||
test('uses the requested dimensions and excludes scrollback', async () => {
|
||
const screen = await createPtyScreen(12, 3);
|
||
try {
|
||
screen.write('old permission\r\n1\r\n2\r\n3\r\n4');
|
||
const current = await screen.read();
|
||
expect(current.split('\n')).toHaveLength(3);
|
||
expect(current).not.toContain('old permission');
|
||
expect(current).toBe('2\n3\n4');
|
||
} finally { await screen.dispose(); }
|
||
});
|
||
|
||
test('sessions isolate state and dispose flushes pending writes once without changing navigator', async () => {
|
||
const navigatorBefore = globalThis.navigator;
|
||
const a = await createPtyScreen(30, 4);
|
||
const b = await createPtyScreen(30, 4);
|
||
a.write('first'); b.write('second');
|
||
const closing = a.dispose();
|
||
await closing;
|
||
expect(await a.read()).toBe('first\n\n\n');
|
||
expect(await b.read()).toBe('second\n\n\n');
|
||
expect(a.dispose()).toBe(closing);
|
||
expect(() => a.write('late')).toThrow('disposed');
|
||
await b.dispose();
|
||
expect(globalThis.navigator).toBe(navigatorBefore);
|
||
});
|
||
|
||
test('retains a long genuine question when its options are visible and its header has scrolled away', async () => {
|
||
const screen = await createPtyScreen(120, 40);
|
||
try {
|
||
screen.write('☐ API consistency\r\n' + 'Existing context for this independent issue.\r\n'.repeat(45) +
|
||
'Which argument order should run_eval/run_batch use?\r\n❯1.Use the same argument order\r\n2.Keep the inconsistent order\r\n' +
|
||
'Enter to select · ↑/↓ to navigate · Esc to cancel');
|
||
const current = await screen.read();
|
||
expect(current).not.toContain('☐ API consistency');
|
||
const question = capturePlanCountQuestion(current, new Set(), 0, false);
|
||
expect(question).not.toBeNull();
|
||
expect(question?.options[0].label).toBe('Use the same argument order');
|
||
expect(question?.options).toHaveLength(2);
|
||
} finally { await screen.dispose(); }
|
||
});
|
||
test('historical file results release a new current permission but cannot activate stale scrollback', () => {
|
||
const menu = 'Do you want to create plan.md?\n❯1.Yes\n2.No\nEsc to cancel · Tab to amend';
|
||
const completed = menu + '\n⎿ Wrote 44 lines';
|
||
const guard = createPlanCountPermissionGuard();
|
||
expect(guard(menu, menu)).toBe('grant');
|
||
expect(guard(menu, menu)).toBe('handled');
|
||
expect(guard('⎿ Wrote 44 lines\n❯ ', completed)).toBeNull();
|
||
// The actual new menu has no old Write in its viewport. The separate
|
||
// history establishes a new completed-file epoch for this same target.
|
||
expect(guard(menu, completed + '\n' + menu)).toBe('grant');
|
||
expect(guard(menu, completed + '\n' + menu)).toBe('handled');
|
||
expect(guard('❯ ', completed + '\n' + menu)).toBeNull();
|
||
expect(guard(menu + '\n⎿ Wrote 1 line', completed + '\n' + menu + '\n⎿ Wrote 1 line')).toBe('handled');
|
||
});
|
||
|
||
test('captured successful Write removes the old permission; raw and viewport evidence stay separate', async () => {
|
||
const frame = fixture('design');
|
||
const screen = await createPtyScreen(frame.cols, frame.rows);
|
||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'screen-evidence-'));
|
||
try {
|
||
const raw = seed(frame) + frame.update;
|
||
screen.write(raw);
|
||
const active = await screen.read();
|
||
expect(active.split('\n').map(line => line.trimEnd())).toEqual(frame.expected.map((line: string) => line.trimEnd()));
|
||
expect(active).toContain('Do you want to create gstack-test-plan-design.md?');
|
||
expect(isPermissionDialogVisible(active)).toBe(true);
|
||
screen.write(frame.completed);
|
||
const current = await screen.read();
|
||
expect(current).toContain('Wrote 44 lines');
|
||
expect(current).not.toContain('Do you want');
|
||
expect(isPermissionDialogVisible(current)).toBe(false);
|
||
expect(capturePlanCountQuestion(current, new Set(), 0, false)).toBeNull();
|
||
const preservedRaw = raw + frame.completed;
|
||
expect(stripAnsi(preservedRaw)).toContain('Do you wat');
|
||
const native = { calls: [{ toolUseId: 'native-question', answered: true }] };
|
||
const save = createPlanCountSnapshotWriter({ EVALS_RUN_ID: 'screen-proof', GSTACK_EVAL_DIR: dir });
|
||
const saved = save({ skillName: 'plan-design-review', cwd: dir, claudeConfigDir: null,
|
||
raw: preservedRaw, visible: stripAnsi(preservedRaw), viewport: current, observation: { native } });
|
||
expect(saved.artifactError).toBeUndefined();
|
||
expect(fs.readFileSync(path.join(saved.artifactDir!, 'terminal.raw.log'), 'utf8')).toBe(preservedRaw);
|
||
expect(fs.readFileSync(path.join(saved.artifactDir!, 'terminal.screen.log'), 'utf8')).toBe(current);
|
||
expect(JSON.parse(fs.readFileSync(path.join(saved.artifactDir!, 'observation.json'), 'utf8')).native).toEqual(native);
|
||
} finally { await screen.dispose(); fs.rmSync(dir, { recursive: true, force: true }); }
|
||
});
|
||
});
|