voyage/tests/hooks/pre-compact-flush.test.mjs
Kjell Tore Guttormsen b0d96a2d53
fix: close what an independent review found in the trial 2 fixes
- The pre-compact hook read only a checkpoint that started with `git commit`,
  so every plan in the new `git add … && git commit` form rebuilt nothing, and
  an empty match also skipped the session-state refresh. It now finds the
  commit anywhere in the span and always refreshes; first test for the hook.
- A sub-bullet that carries its own command stays a criterion (folding it
  left the command unrun); the shallowest bullet sets the base indent.
- Only a MINOR/SUGGESTION /code-review finding outside the range is released
  from forbidding ALLOW: a MAJOR one may be a break cited at an unchanged
  caller.
- `reuse` is an improvement category; the session-spec template stages too;
  a negated bash 3.2 constraint, an env prefix and `bash -c` are handled.
- Stale comment, Declined-to-Judge wording and rule-key counts brought in line.

Tests fail on 7c11c2a first (9 of 24).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 15:21:56 +02:00

72 lines
3.3 KiB
JavaScript

// tests/hooks/pre-compact-flush.test.mjs
// The PreCompact hook rebuilds progress.json from git and refreshes the
// session state before compaction. An independent review (2026-10-08) found
// that the checkpoint form `git add … && git commit -m "…"` (plan template,
// lekerepo trial 2 point A7) matched nothing, and that an empty match skipped
// the session-state refresh too. No test covered the hook until now.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { spawnSync } from 'node:child_process';
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const HOOK = join(HERE, '..', '..', 'hooks', 'scripts', 'pre-compact-flush.mjs');
function git(cwd, ...args) {
const r = spawnSync('git', args, { cwd, encoding: 'utf8' });
assert.equal(r.status, 0, `git ${args.join(' ')}: ${r.stderr}`);
return r.stdout.trim();
}
function project(checkpoint, { stateStatus = 'in_progress' } = {}) {
const repo = mkdtempSync(join(tmpdir(), 'voyage-precompact-'));
git(repo, 'init', '-q');
git(repo, 'config', 'user.email', 't@example.invalid');
git(repo, 'config', 'user.name', 'test');
writeFileSync(join(repo, 'a.txt'), 'a\n');
git(repo, 'add', 'a.txt');
git(repo, 'commit', '-q', '-m', 'base');
const base = git(repo, 'rev-parse', 'HEAD');
writeFileSync(join(repo, 'a.txt'), 'b\n');
git(repo, 'add', 'a.txt');
git(repo, 'commit', '-q', '-m', 'feat(a): change a');
const dir = join(repo, '.claude', 'projects', 'p');
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, 'plan.md'), ['# Plan', '', '### Step 1: Change a', '', `- **Checkpoint:** ${checkpoint}`, ''].join('\n'));
writeFileSync(join(dir, 'progress.json'), JSON.stringify({ status: 'in_progress', current_step: 0, steps: {}, session_start_sha: base }));
writeFileSync(join(dir, '.session-state.local.json'), JSON.stringify({ status: stateStatus, updated_at: '2000-01-01T00:00:00.000Z' }));
return { repo, dir };
}
function run(repo) {
return spawnSync(process.execPath, [HOOK], { input: '{}', encoding: 'utf8', env: { ...process.env, CLAUDE_PROJECT_DIR: repo } });
}
const progress = (dir) => JSON.parse(readFileSync(join(dir, 'progress.json'), 'utf8'));
const state = (dir) => JSON.parse(readFileSync(join(dir, '.session-state.local.json'), 'utf8'));
for (const [name, cp] of [
['the old form', '`git commit -m "feat(a): change a"`'],
['the template form with git add', '`git add a.txt && git commit -m "feat(a): change a"`'],
]) {
test(`pre-compact: ${name} -> progress.json is rebuilt from git`, () => {
const { repo, dir } = project(cp);
try {
assert.equal(run(repo).status, 0);
assert.equal(progress(dir).current_step, 1);
} finally { rmSync(repo, { recursive: true, force: true }); }
});
}
test('pre-compact: a plan with no recognisable checkpoint still refreshes the session state', () => {
const { repo, dir } = project('`make commit`');
try {
assert.equal(run(repo).status, 0);
assert.equal(progress(dir).current_step, 0);
assert.notEqual(state(dir).updated_at, '2000-01-01T00:00:00.000Z');
} finally { rmSync(repo, { recursive: true, force: true }); }
});