import { test, describe, after } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync, existsSync, appendFileSync } from 'node:fs'; import { join } from 'node:path'; import { tempRepo, write, commitAll, lh, cleanup } from './helpers.mjs'; after(cleanup); const PKG = JSON.stringify({ name: 'fixture', version: '1.0.0', scripts: { test: "node -e \"console.log('ok')\"" }, }, null, 2); /** A brownfield repo carrying two planted structural defects. */ function brownfield() { return tempRepo({ 'package.json': PKG, 'src/a.js': 'export function helper(x) { return x + 1; }\nexport function shared() { return 1; }\n', 'src/b.js': "import { helper } from './a.js';\nexport function shared() { return 2; }\nexport function main() { return helper(missingFn()); }\n", 'README.md': '# fixture\n', }); } describe('cli contract', () => { test('--help exits 0 and lists the command surface', () => { const r = lh(process.cwd(), ['--help']); assert.equal(r.code, 0); for (const cmd of ['init', 'index', 'graph', 'lane', 'run', 'memory', 'host', 'report', 'doctor']) { assert.ok(r.stdout.includes(cmd), `help should mention ${cmd}`); } }); test('an unknown command fails rather than silently succeeding', () => { assert.notEqual(lh(process.cwd(), ['no-such-command']).code, 0); }); test('host detection always emits a usable strategy', () => { const r = lh(brownfield(), ['host', '--json']); assert.equal(r.code, 0); const host = JSON.parse(r.stdout); assert.ok(host.host, 'a host must always be named'); assert.ok(host.maxWriteLanes >= 1, 'must allow at least one write lane'); assert.ok(host.readOnlyFanOut >= 1, 'must allow at least one scout'); }); }); describe('init (greenfield + brownfield)', () => { test('greenfield init creates config and memory shards', () => { const dir = tempRepo({ 'README.md': '# empty\n' }); assert.equal(lh(dir, ['init', '--yes']).code, 0); assert.ok(existsSync(join(dir, '.agents/harness.config.json'))); assert.ok(existsSync(join(dir, '.agents/memory/INDEX.md'))); for (const shard of ['failures', 'corrections', 'insights', 'conventions', 'quirks']) { assert.ok(existsSync(join(dir, `.agents/memory/${shard}.md`)), `missing shard ${shard}`); } }); test('brownfield init autodetects the existing verify command', () => { const dir = brownfield(); const r = lh(dir, ['init', '--yes', '--json']); assert.equal(r.code, 0); const out = JSON.parse(r.stdout); assert.ok(out.verify.some((v) => v.command.includes('test')), 'should detect npm test'); }); test('the Context7 key is never written to disk', () => { const dir = brownfield(); lh(dir, ['init', '--yes']); const raw = readFileSync(join(dir, '.agents/harness.config.json'), 'utf8'); assert.ok(raw.includes('CONTEXT7_API_KEY'), 'config should reference the env var by name'); if (process.env.CONTEXT7_API_KEY) { assert.ok(!raw.includes(process.env.CONTEXT7_API_KEY), 'the key value must never be persisted'); } }); test('re-running init refuses to clobber an existing config', () => { const dir = brownfield(); assert.equal(lh(dir, ['init', '--yes']).code, 0); assert.notEqual(lh(dir, ['init', '--yes']).code, 0, 'second init must refuse'); }); test('doctor passes once the repo is initialised', () => { const dir = brownfield(); lh(dir, ['init', '--yes']); lh(dir, ['index']); assert.equal(lh(dir, ['doctor']).code, 0); }); test('doctor fails on an uninitialised repo', () => { assert.notEqual(lh(tempRepo({ 'README.md': '#\n' }), ['doctor']).code, 0); }); }); describe('index and structural gate', () => { test('index works offline and reports symbols', () => { const dir = brownfield(); lh(dir, ['init', '--yes']); const r = lh(dir, ['index', '--stats']); assert.equal(r.code, 0); assert.match(r.stdout, /symbols\s+[1-9]/, 'should discover symbols without network access'); assert.ok(existsSync(join(dir, '.agents/.cache/symbols.json'))); }); test('a second index run hits the cache', () => { const dir = brownfield(); lh(dir, ['init', '--yes']); lh(dir, ['index']); assert.match(lh(dir, ['index', '--stats']).stdout, /cacheHits\s+[1-9]/); }); test('the gate exits 1 and names both planted defects', () => { const dir = brownfield(); lh(dir, ['init', '--yes']); lh(dir, ['index']); const r = lh(dir, ['graph', '--brief']); assert.equal(r.code, 1, 'structural violations must exit 1 — the Ralph loop gates on this'); assert.match(r.stdout, /duplicate-definition/); assert.match(r.stdout, /shared/); assert.match(r.stdout, /unresolved-call/); assert.match(r.stdout, /missingFn/); }); test('the gate exits 0 on a clean repo', () => { const dir = tempRepo({ 'package.json': PKG, 'src/a.js': 'export function helper(x) { return x + 1; }\n', 'src/b.js': "import { helper } from './a.js';\nexport function main() { return helper(1); }\n", }); lh(dir, ['init', '--yes']); lh(dir, ['index']); const r = lh(dir, ['graph', '--brief']); assert.equal(r.code, 0, `clean repo must pass, got: ${r.stdout}${r.stderr}`); }); }); describe('parallel write lanes', () => { function laneRepo() { const dir = tempRepo({ 'package.json': PKG, 'src/a/deep/f.txt': 'one\n', 'src/b/g.txt': 'two\n', 'root.txt': 'base\n', }); lh(dir, ['init', '--yes']); return dir; } test('disjoint lanes both get a worktree', () => { const dir = laneRepo(); assert.equal(lh(dir, ['lane', 'create', '--id', 'a', '--title', 'A', '--kind', 'write', '--scope', 'src/a/**']).code, 0); assert.equal(lh(dir, ['lane', 'create', '--id', 'b', '--title', 'B', '--kind', 'write', '--scope', 'src/b/**']).code, 0); assert.match(lh(dir, ['lane', 'list']).stdout, /\ba\b[\s\S]*\bb\b/); }); test('an overlapping write lane is rejected', () => { const dir = laneRepo(); lh(dir, ['lane', 'create', '--id', 'a', '--title', 'A', '--kind', 'write', '--scope', 'src/a/**']); // src/a/deep/** is a strict subset of src/a/** — must still be refused. const r = lh(dir, ['lane', 'create', '--id', 'c', '--title', 'C', '--kind', 'write', '--scope', 'src/a/deep/**']); assert.notEqual(r.code, 0, 'overlapping write scopes must never both be leased'); assert.match(r.stderr + r.stdout, /overlap|intersect/i); }); test('out-of-scope writes are detected and exit 1', () => { const dir = laneRepo(); lh(dir, ['lane', 'create', '--id', 'a', '--title', 'A', '--kind', 'write', '--scope', 'src/a/**']); const wt = join(dir, '.agents/.cache/worktrees/a'); write(wt, 'src/a/deep/f.txt', 'legit edit\n'); write(wt, 'root.txt', 'OUT OF SCOPE\n'); commitAll(wt, 'one legit, one violation'); const r = lh(dir, ['lane', 'status', 'a']); assert.equal(r.code, 1, 'scope violation is Ralph exit criterion 5 — must be machine-detectable'); assert.match(r.stdout, /root\.txt/); assert.match(r.stdout, /OUT/); }); test('a clean lane merges, then a conflicting lane is blocked not auto-resolved', () => { const dir = laneRepo(); lh(dir, ['lane', 'create', '--id', 'a', '--title', 'A', '--kind', 'write', '--scope', 'src/a/**']); lh(dir, ['lane', 'create', '--id', 'b', '--title', 'B', '--kind', 'write', '--scope', 'src/b/**']); const wtA = join(dir, '.agents/.cache/worktrees/a'); write(wtA, 'root.txt', 'LANE A\n'); commitAll(wtA, 'lane a'); const wtB = join(dir, '.agents/.cache/worktrees/b'); write(wtB, 'root.txt', 'LANE B\n'); commitAll(wtB, 'lane b'); assert.equal(lh(dir, ['lane', 'merge', 'a']).code, 0, 'first merge should be clean'); const conflict = lh(dir, ['lane', 'merge', 'b']); assert.notEqual(conflict.code, 0, 'a conflict must fail loudly'); assert.match(conflict.stdout + conflict.stderr, /root\.txt/); assert.match(lh(dir, ['lane', 'list']).stdout, /blocked/); }); test('merge --abort restores a clean tree', () => { const dir = laneRepo(); lh(dir, ['lane', 'create', '--id', 'a', '--title', 'A', '--kind', 'write', '--scope', 'src/a/**']); lh(dir, ['lane', 'create', '--id', 'b', '--title', 'B', '--kind', 'write', '--scope', 'src/b/**']); for (const id of ['a', 'b']) { const wt = join(dir, `.agents/.cache/worktrees/${id}`); write(wt, 'root.txt', `LANE ${id}\n`); commitAll(wt, `lane ${id}`); } lh(dir, ['lane', 'merge', 'a']); lh(dir, ['lane', 'merge', 'b']); assert.equal(lh(dir, ['lane', 'merge', 'b', '--abort']).code, 0); assert.equal(lh(dir, ['lane', 'drop', 'b', '--force']).code, 0); }); }); describe('memory', () => { function memRepo() { const dir = brownfield(); lh(dir, ['init', '--yes']); return dir; } test('put then retrieve by query', () => { const dir = memRepo(); assert.equal(lh(dir, ['memory', 'put', '--shard', 'failures', '--title', 'flaky suite', '--body', 'Retry once before reporting.']).code, 0); const r = lh(dir, ['memory', 'get', '--query', 'flaky']); assert.equal(r.code, 0); assert.match(r.stdout, /Retry once/); }); test('writing a secret is refused', () => { const dir = memRepo(); const r = lh(dir, ['memory', 'put', '--shard', 'insights', '--title', 'leak', '--body', 'key ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789']); assert.notEqual(r.code, 0, 'secrets must never reach the memory shards'); const shard = readFileSync(join(dir, '.agents/memory/insights.md'), 'utf8'); assert.ok(!shard.includes('ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'), 'shard must stay clean'); }); test('INDEX.md stays small — it is the only always-loaded file', () => { const dir = memRepo(); for (let i = 0; i < 20; i++) { lh(dir, ['memory', 'put', '--shard', 'insights', '--title', `note ${i}`, '--body', 'x'.repeat(200)]); } const index = readFileSync(join(dir, '.agents/memory/INDEX.md'), 'utf8'); assert.ok(index.length < 2000, `INDEX.md must stay lean, was ${index.length} bytes`); }); }); describe('telemetry', () => { test('a full run produces events, a board and a report', () => { const dir = brownfield(); lh(dir, ['init', '--yes']); const start = lh(dir, ['run', 'start', '--objective', 'test run', '--json']); assert.equal(start.code, 0); const runId = JSON.parse(start.stdout).runId; assert.ok(runId, 'run start must return a runId'); assert.equal(lh(dir, ['run', 'event', '--run', runId, '--type', 'agent.end', '--agent', 'scout', '--input-tokens', '100', '--output-tokens', '50']).code, 0); assert.equal(lh(dir, ['run', 'end', '--run', runId]).code, 0); assert.ok(existsSync(join(dir, `.agents/runs/${runId}/events.ndjson`))); assert.ok(existsSync(join(dir, `.agents/runs/${runId}/board.md`))); const report = lh(dir, ['report', runId, '--json']); assert.equal(report.code, 0); assert.equal(JSON.parse(report.stdout).totals.totalTokens, 150); }); test('a malformed NDJSON line does not break reporting', () => { const dir = brownfield(); lh(dir, ['init', '--yes']); const runId = JSON.parse(lh(dir, ['run', 'start', '--objective', 'test run', '--json']).stdout).runId; lh(dir, ['run', 'event', '--run', runId, '--type', 'agent.end', '--input-tokens', '10']); const events = join(dir, `.agents/runs/${runId}/events.ndjson`); appendFileSync(events, '{ this is not json\n'); lh(dir, ['run', 'event', '--run', runId, '--type', 'agent.end', '--input-tokens', '5']); const r = lh(dir, ['report', runId, '--json']); assert.equal(r.code, 0, 'the parser must tolerate corrupt lines'); assert.equal(JSON.parse(r.stdout).totals.inputTokens, 15, 'valid events must still be counted'); }); });