Files
gstack/test/helpers/plan-count-artifacts.ts
T
garrytan f1eeec384e test: judge plan-count completion on structured evidence, not wording
Replaying run 36385945043's two Design attempts showed the existing routes
rejected correct endings: attempt 1 at the typed-completion path field
('- Reviewed plan written to …' is not a 'Plan written to' line), attempt 2
at the leading-fence veto (its final message opens with the dashboard).

nativePlanTerminalPreconditions is the structural prefix of
hasNativePlanTerminal (behavior unchanged). structuredPlanCompletion adds,
inside the existing nativeSummary branch: a complete report (Design
binding for Design), a completed review-log row for the expected skill
appended during this attempt under the child's GSTACK_HOME/project slug
(resolved with bin/gstack-slug) and stamped with the fixture commit, timed
between the report/last answer (second resolution) and the final native
message, a final message with stop_reason end_turn (now carried on public
transcript messages), and no visible question or permission prompt.

Timeout summaries add idleFor and lastTerminalCandidate. Terminal and throw
captures copy the plan file and review-log rows into the artifact
directory; copies are best-effort and recorded in evidence-copy.json.
Free regressions: both captured Design endings (trimmed fixture with
provenance; report, row and end_turn reconstructed and labelled), the
negative controls, and real-PTY completion/timeout runs through the real
review logger.
2026-09-29 15:04:28 +00:00

58 lines
2.5 KiB
TypeScript

import * as fs from 'node:fs';
import * as path from 'node:path';
import { randomUUID } from 'node:crypto';
import { getProjectEvalDir } from './eval-store';
interface PlanCountSnapshot {
skillName: string;
observation: object;
raw: string;
visible: string;
viewport?: string;
cwd: string;
claudeConfigDir: string | null;
}
/** One owned directory per count attempt; periodic captures replace files atomically. */
export function createPlanCountSnapshotWriter(env: NodeJS.ProcessEnv = process.env):
(input: PlanCountSnapshot) => { artifactDir?: string; artifactError?: string } {
let artifactDir: string | undefined;
// An explicit output directory requests retention even outside CI's named
// runs. Keep its fallback stable across checkpoints and unique per writer.
const runId = env.EVALS_RUN_ID || (env.GSTACK_EVAL_DIR ? `local-${randomUUID()}` : undefined);
return (input) => {
if (!runId) return {};
try {
if (!artifactDir) {
const segment = (text: string) => text.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 120) || 'run';
const root = path.resolve(env.GSTACK_EVAL_DIR || getProjectEvalDir(), 'pty-count', segment(runId));
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
artifactDir = fs.mkdtempSync(path.join(root, `${segment(input.skillName)}-${Date.now()}-`));
}
const write = (name: string, content: string) => {
const target = path.join(artifactDir!, name);
fs.writeFileSync(`${target}.tmp`, content, { mode: 0o600 });
fs.renameSync(`${target}.tmp`, target);
};
write('terminal.raw.log', input.raw);
write('terminal.visible.log', input.visible);
if (input.viewport !== undefined) write('terminal.screen.log', input.viewport);
write('observation.json', JSON.stringify({
...input.observation, artifactDir,
capture: { skill: input.skillName, runId, cwd: input.cwd,
claudeConfigDir: input.claudeConfigDir, at: new Date().toISOString() },
}, null, 2) + '\n');
return { artifactDir };
} catch (error) {
// Preserve any partial evidence and the original test outcome; make the
// write failure visible instead of claiming diagnostics were retained.
return { artifactDir, artifactError: String(error) };
}
};
}
/** Keep a single snapshot outside the temporary fixture that setup later removes. */
export function persistPlanCountSnapshot(input: PlanCountSnapshot, env: NodeJS.ProcessEnv = process.env) {
return createPlanCountSnapshotWriter(env)(input);
}