test(stats): a trekbrief record carries the order Voyage runs, written by the writer (red)
stats-landing gains a baseEnv() (process.env minus CLAUDE_COORD_DIR) at every spawn - the real voyage mailbox holds a claimed order while the suite runs - and runLine takes a cwd and extra env. New: Phase 7's own line, run in a temp git repo against a temp coord dir, must land order_id / order_claimed_at / order_reason (one claimed -> the id; zero -> keys present, null, none-claimed; two -> several-claimed:2; model-supplied order_* keys replaced; no coord -> coord-missing). The CLI must also write when run through a symlink or from a path with a space. Red on behaviour: 7 of 8 new tests fail against today's writer (no order keys; the file:// guard is a silent no-op under a symlink and under a space - measured in a scratch repro 04.10). Guard green on arrival: a trekplan record gains no order keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2337d7f2ac
commit
1db116eda5
1 changed files with 156 additions and 5 deletions
|
|
@ -9,11 +9,17 @@
|
|||
// silently" — the same silent skip that left brief-approved at 0 records. These
|
||||
// tests execute the command lines the prose spells, with CLAUDE_PLUGIN_DATA
|
||||
// EMPTY, and require the records to land in the yardstick's directory.
|
||||
//
|
||||
// The trekbrief record also carries the coord-queue order the run is working on
|
||||
// (order_id, order_claimed_at, order_reason). The WRITER adds those from the
|
||||
// queue, so these tests run the line inside a temp git repo against a temp coord
|
||||
// dir - and every spawn drops an inherited CLAUDE_COORD_DIR, because the real
|
||||
// queue can hold a claimed order while the suite runs.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
|
@ -40,16 +46,47 @@ function appendLine(sec, kind, rel) {
|
|||
return lines[0].trim().replace(/\s*<<-?'?JSON'?\s*$/, '');
|
||||
}
|
||||
|
||||
/** process.env without CLAUDE_COORD_DIR: a test never reads the real coord queue. */
|
||||
function baseEnv() {
|
||||
const env = { ...process.env };
|
||||
delete env.CLAUDE_COORD_DIR;
|
||||
return env;
|
||||
}
|
||||
|
||||
/** Run a prose command line with a record on stdin, CLAUDE_PLUGIN_DATA empty, HOME = home. */
|
||||
function runLine(line, record, home) {
|
||||
function runLine(line, record, home, { cwd, env: extra = {} } = {}) {
|
||||
const cmd = line.split('${CLAUDE_PLUGIN_ROOT}').join(ROOT);
|
||||
return spawnSync('bash', ['-c', cmd], {
|
||||
encoding: 'utf8',
|
||||
cwd,
|
||||
input: JSON.stringify(record),
|
||||
env: { ...process.env, CLAUDE_PLUGIN_DATA: '', HOME: home },
|
||||
env: { ...baseEnv(), CLAUDE_PLUGIN_DATA: '', HOME: home, ...extra },
|
||||
});
|
||||
}
|
||||
|
||||
/** A git repo whose directory is named `name` (its mailbox), under a fresh temp parent. */
|
||||
function gitRepo(name) {
|
||||
const parent = mkdtempSync(join(tmpdir(), 'stats-landing-git-'));
|
||||
const dir = join(parent, name);
|
||||
mkdirSync(dir);
|
||||
assert.equal(spawnSync('git', ['init', '-q'], { cwd: dir }).status, 0);
|
||||
return { parent, dir };
|
||||
}
|
||||
|
||||
/** A coord dir holding claimed orders: `{ mailbox: [id, ...] }`, each with a .claim sidecar. */
|
||||
function coordWith(claims) {
|
||||
const coord = mkdtempSync(join(tmpdir(), 'stats-landing-coord-'));
|
||||
for (const [mailbox, ids] of Object.entries(claims)) {
|
||||
const dir = join(coord, mailbox, 'orders', 'claimed');
|
||||
mkdirSync(dir, { recursive: true });
|
||||
for (const id of ids) {
|
||||
writeFileSync(join(dir, `${id}.md`), `---\nfrom: .claude\nto: ${mailbox}\norder-id: ${id}\ndate: 2026-10-04T10:00:00Z\n---\n\n# ORDER ${id}\n`);
|
||||
writeFileSync(join(dir, `${id}.claim`), 'claimed-at: 2026-10-04T17:10:51Z\nclaimed-by-pid: 1\n');
|
||||
}
|
||||
}
|
||||
return coord;
|
||||
}
|
||||
|
||||
function records(file) {
|
||||
return readFileSync(file, 'utf8').trim().split('\n').filter(Boolean).map((l) => JSON.parse(l));
|
||||
}
|
||||
|
|
@ -106,7 +143,7 @@ test('M6: stats-append refuses a record that is not a JSON object, and an unknow
|
|||
const home = mkdtempSync(join(tmpdir(), 'stats-landing-'));
|
||||
try {
|
||||
const cli = join(ROOT, 'lib', 'stats', 'stats-append.mjs');
|
||||
const env = { ...process.env, CLAUDE_PLUGIN_DATA: '', HOME: home };
|
||||
const env = { ...baseEnv(), CLAUDE_PLUGIN_DATA: '', HOME: home };
|
||||
const bad = spawnSync('node', [cli, 'trekbrief'], { encoding: 'utf8', input: 'not json', env });
|
||||
assert.equal(bad.status, 1, bad.stdout + bad.stderr);
|
||||
assert.equal(JSON.parse(bad.stdout).written, false);
|
||||
|
|
@ -141,7 +178,7 @@ A toggle in settings, defaulting to the system theme.
|
|||
assert.ok(stampLine, 'trekbrief.md must carry the --stamp line');
|
||||
let cmd = stampLine.trim().split('${CLAUDE_PLUGIN_ROOT}').join(ROOT);
|
||||
cmd = cmd.split('{BRIEF_PATH}').join(join(proj, 'brief.md')).split('{PROJECT_DIR}').join(proj);
|
||||
const s = spawnSync('bash', ['-c', cmd], { encoding: 'utf8', env: { ...process.env, CLAUDE_PLUGIN_DATA: '', HOME: home } });
|
||||
const s = spawnSync('bash', ['-c', cmd], { encoding: 'utf8', env: { ...baseEnv(), CLAUDE_PLUGIN_DATA: '', HOME: home } });
|
||||
assert.equal(s.status, 0, s.stdout + s.stderr);
|
||||
// (2) and (3): the stats lines
|
||||
assert.equal(runLine(appendLine(section(TB, 'Phase 7'), 'trekbrief', TB),
|
||||
|
|
@ -172,3 +209,117 @@ test('M6: stats-append and intent-approval resolve the same data dir as the yard
|
|||
assert.equal(resolveApprovalDataDir(env), resolveStatsDir(env));
|
||||
assert.equal(resolveStatsDir({ HOME: '/h', CLAUDE_PLUGIN_DATA: '/x/y' }), '/x/y');
|
||||
});
|
||||
|
||||
// --- the order the run is working on (written by the writer, never the model) -------
|
||||
|
||||
const ORDER_KEYS = ['order_id', 'order_claimed_at', 'order_reason'];
|
||||
|
||||
/** Run Phase 7's line in `repo` against `coord` (or none) and return the one landed record. */
|
||||
function landTrekbrief(record, { repo, coord, home }) {
|
||||
const line = appendLine(section(TB, 'Phase 7'), 'trekbrief', TB);
|
||||
const env = coord ? { CLAUDE_COORD_DIR: coord } : {};
|
||||
const r = runLine(line, record, home, { cwd: repo, env });
|
||||
assert.equal(r.status, 0, `stats-append failed: ${r.stdout} ${r.stderr}`);
|
||||
const recs = records(join(home, DATA_REL, 'trekbrief-stats.jsonl'));
|
||||
assert.equal(recs.length, 1);
|
||||
return recs[0];
|
||||
}
|
||||
|
||||
test('order: one claimed order in the repo\'s mailbox lands in the trekbrief record, with its claim time', () => {
|
||||
const home = mkdtempSync(join(tmpdir(), 'stats-landing-'));
|
||||
const repo = gitRepo('mbx');
|
||||
const coord = coordWith({ mbx: ['o-1'] });
|
||||
try {
|
||||
const rec = landTrekbrief({ ts: '2026-10-04T18:00:00Z', slug: 'dark-mode-toggle', intent_approved: true },
|
||||
{ repo: repo.dir, coord, home });
|
||||
assert.equal(rec.order_id, 'o-1');
|
||||
assert.equal(rec.order_claimed_at, '2026-10-04T17:10:51Z');
|
||||
assert.equal(rec.order_reason, null);
|
||||
assert.equal(rec.slug, 'dark-mode-toggle', 'the record keeps its own slug');
|
||||
} finally { rmSync(home, { recursive: true, force: true }); rmSync(repo.parent, { recursive: true, force: true }); rmSync(coord, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('order: zero claimed orders - the three keys are PRESENT, order_id null, reason none-claimed', () => {
|
||||
const home = mkdtempSync(join(tmpdir(), 'stats-landing-'));
|
||||
const repo = gitRepo('mbx');
|
||||
const coord = coordWith({ other: ['o-9'] });
|
||||
try {
|
||||
const rec = landTrekbrief({ ts: '2026-10-04T18:00:00Z', slug: 's' }, { repo: repo.dir, coord, home });
|
||||
for (const k of ORDER_KEYS) assert.ok(k in rec, `${k} must be present (absent means: written before the recorder existed)`);
|
||||
assert.equal(rec.order_id, null);
|
||||
assert.equal(rec.order_reason, 'none-claimed');
|
||||
} finally { rmSync(home, { recursive: true, force: true }); rmSync(repo.parent, { recursive: true, force: true }); rmSync(coord, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('order: two claimed orders - order_id null, reason several-claimed:2, never a guess', () => {
|
||||
const home = mkdtempSync(join(tmpdir(), 'stats-landing-'));
|
||||
const repo = gitRepo('mbx');
|
||||
const coord = coordWith({ mbx: ['o-1', 'o-2'] });
|
||||
try {
|
||||
const rec = landTrekbrief({ ts: '2026-10-04T18:00:00Z', slug: 's' }, { repo: repo.dir, coord, home });
|
||||
assert.equal(rec.order_id, null);
|
||||
assert.equal(rec.order_reason, 'several-claimed:2');
|
||||
} finally { rmSync(home, { recursive: true, force: true }); rmSync(repo.parent, { recursive: true, force: true }); rmSync(coord, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('order: order_* keys the model put in the record are replaced by the writer\'s', () => {
|
||||
const home = mkdtempSync(join(tmpdir(), 'stats-landing-'));
|
||||
const repo = gitRepo('mbx');
|
||||
const coord = coordWith({ mbx: ['o-1'] });
|
||||
try {
|
||||
const rec = landTrekbrief({ ts: '2026-10-04T18:00:00Z', slug: 's', order_id: 'forged', order_extra: 1 },
|
||||
{ repo: repo.dir, coord, home });
|
||||
assert.equal(rec.order_id, 'o-1');
|
||||
assert.ok(!('order_extra' in rec), 'only the writer\'s three order keys survive');
|
||||
} finally { rmSync(home, { recursive: true, force: true }); rmSync(repo.parent, { recursive: true, force: true }); rmSync(coord, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('order: a trekplan record gains no order keys - only /trekbrief records the order', () => {
|
||||
const home = mkdtempSync(join(tmpdir(), 'stats-landing-'));
|
||||
const repo = gitRepo('mbx');
|
||||
const coord = coordWith({ mbx: ['o-1'] });
|
||||
try {
|
||||
const r = runLine(appendLine(section(TP, 'Phase 12'), 'trekplan', TP), { ts: '2026-10-04T18:00:00Z', slug: 's' }, home,
|
||||
{ cwd: repo.dir, env: { CLAUDE_COORD_DIR: coord } });
|
||||
assert.equal(r.status, 0, r.stdout + r.stderr);
|
||||
const [rec] = records(join(home, DATA_REL, 'trekplan-stats.jsonl'));
|
||||
for (const k of ORDER_KEYS) assert.ok(!(k in rec), `${k} must not be on a trekplan record`);
|
||||
} finally { rmSync(home, { recursive: true, force: true }); rmSync(repo.parent, { recursive: true, force: true }); rmSync(coord, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('order: with CLAUDE_COORD_DIR unset and HOME a temp dir the real queue is never read - order_id null', () => {
|
||||
const home = mkdtempSync(join(tmpdir(), 'stats-landing-'));
|
||||
const repo = gitRepo('voyage');
|
||||
try {
|
||||
const rec = landTrekbrief({ ts: '2026-10-04T18:00:00Z', slug: 's' }, { repo: repo.dir, home });
|
||||
assert.ok('order_id' in rec);
|
||||
assert.equal(rec.order_id, null);
|
||||
assert.equal(rec.order_reason, 'coord-missing');
|
||||
} finally { rmSync(home, { recursive: true, force: true }); rmSync(repo.parent, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('CLI: stats-append writes when it is run through a symlink', () => {
|
||||
const home = mkdtempSync(join(tmpdir(), 'stats-landing-'));
|
||||
const links = mkdtempSync(join(tmpdir(), 'stats-landing-link-'));
|
||||
try {
|
||||
const link = join(links, 'stats-append.mjs');
|
||||
symlinkSync(join(ROOT, 'lib', 'stats', 'stats-append.mjs'), link);
|
||||
const r = spawnSync('node', [link, 'trekplan'], { encoding: 'utf8', input: '{"slug":"s"}',
|
||||
env: { ...baseEnv(), CLAUDE_PLUGIN_DATA: '', HOME: home } });
|
||||
assert.equal(r.status, 0, r.stdout + r.stderr);
|
||||
assert.ok(existsSync(join(home, DATA_REL, 'trekplan-stats.jsonl')), `no record landed; stdout: "${r.stdout}"`);
|
||||
} finally { rmSync(home, { recursive: true, force: true }); rmSync(links, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('CLI: stats-append writes when its path contains a space', () => {
|
||||
const home = mkdtempSync(join(tmpdir(), 'stats-landing-'));
|
||||
const spaced = mkdtempSync(join(tmpdir(), 'stats landing space-'));
|
||||
try {
|
||||
cpSync(join(ROOT, 'lib'), join(spaced, 'with space', 'lib'), { recursive: true });
|
||||
const cli = join(spaced, 'with space', 'lib', 'stats', 'stats-append.mjs');
|
||||
const r = spawnSync('node', [cli, 'trekplan'], { encoding: 'utf8', input: '{"slug":"s"}',
|
||||
env: { ...baseEnv(), CLAUDE_PLUGIN_DATA: '', HOME: home } });
|
||||
assert.equal(r.status, 0, r.stdout + r.stderr);
|
||||
assert.ok(existsSync(join(home, DATA_REL, 'trekplan-stats.jsonl')), `no record landed; stdout: "${r.stdout}"`);
|
||||
} finally { rmSync(home, { recursive: true, force: true }); rmSync(spaced, { recursive: true, force: true }); }
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue