128 lines
5.4 KiB
JavaScript
128 lines
5.4 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* agent-inbox-tests.mjs — regression tests for agent-inbox.mjs.
|
|
*
|
|
* Locks in the queue's behaviour:
|
|
* 1. A first `add` seeds the header + agent protocol and one pending item.
|
|
* 2. `add` is append-only and multiline text collapses to a single bullet.
|
|
* 3. `list` shows pending only; `list --all` shows resolved items too.
|
|
* 4. `resolve N` ticks the N-th *pending* item and appends a one-line result,
|
|
* so `list` then `resolve N` line up.
|
|
* 5. An empty `add` fails loudly (exit 1) rather than queuing a blank line.
|
|
* 6. On the default path, a first `add` self-heals .gitignore (idempotent) so
|
|
* the personal queue isn't accidentally tracked.
|
|
*
|
|
* Provisions a throwaway queue via CAREER_OPS_INBOX and a temp CWD; never
|
|
* touches real user data.
|
|
*/
|
|
|
|
import { execFileSync } from 'child_process';
|
|
import { readFileSync, writeFileSync, mkdtempSync } from 'fs';
|
|
import { join, dirname } from 'path';
|
|
import { tmpdir } from 'os';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
const ROOT = dirname(fileURLToPath(import.meta.url));
|
|
const NODE = process.execPath;
|
|
const CLI = join(ROOT, 'agent-inbox.mjs');
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
function check(name, cond, detail = '') {
|
|
if (cond) { passed++; console.log(` ✅ ${name}`); }
|
|
else { failed++; console.log(` ❌ ${name}${detail ? ` — ${detail}` : ''}`); }
|
|
}
|
|
|
|
function tmp(prefix) {
|
|
return mkdtempSync(join(tmpdir(), prefix));
|
|
}
|
|
|
|
// Run agent-inbox.mjs against a provisioned queue file; returns stdout.
|
|
function run(inbox, args, opts = {}) {
|
|
return execFileSync(NODE, [CLI, ...args], {
|
|
cwd: ROOT,
|
|
env: { ...process.env, CAREER_OPS_INBOX: inbox },
|
|
encoding: 'utf8',
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
...opts,
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
console.log('1. First add seeds header + protocol and one pending item');
|
|
{
|
|
const inbox = join(tmp('inbox-'), 'agent-inbox.md');
|
|
run(inbox, ['add', 'evaluate https://acme.com/jobs/42']);
|
|
const md = readFileSync(inbox, 'utf8');
|
|
check('header present', /^# Agent Inbox/.test(md));
|
|
check('agent protocol documented', /Agent protocol:/.test(md));
|
|
check('nothing auto-submits is stated', /auto-submit/.test(md));
|
|
check('one pending checklist item', (md.match(/^- \[ \]/gm) || []).length === 1, md);
|
|
check('request text preserved', md.includes('evaluate https://acme.com/jobs/42'));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
console.log('2. add is append-only; multiline text collapses to one bullet');
|
|
{
|
|
const inbox = join(tmp('inbox-'), 'agent-inbox.md');
|
|
run(inbox, ['add', 'first request']);
|
|
run(inbox, ['add', 'second\nrequest with newline']);
|
|
const md = readFileSync(inbox, 'utf8');
|
|
check('two pending items', (md.match(/^- \[ \]/gm) || []).length === 2);
|
|
check('first item retained', md.includes('first request'));
|
|
check('newline collapsed (no mid-item break)', md.includes('second request with newline'));
|
|
check('item count == bullet count (no stray bullets)', (md.match(/^- \[/gm) || []).length === 2);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
console.log('3. list shows pending; --all includes resolved');
|
|
{
|
|
const inbox = join(tmp('inbox-'), 'agent-inbox.md');
|
|
run(inbox, ['add', 'alpha']);
|
|
run(inbox, ['add', 'beta']);
|
|
run(inbox, ['resolve', '1', '--result', 'done alpha']);
|
|
const pending = run(inbox, ['list']);
|
|
const all = run(inbox, ['list', '--all']);
|
|
check('pending list hides resolved alpha', !pending.includes('alpha') && pending.includes('beta'), pending.trim());
|
|
check('--all shows both', all.includes('alpha') && all.includes('beta'));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
console.log('4. resolve ticks the N-th pending item + appends a one-line result');
|
|
{
|
|
const inbox = join(tmp('inbox-'), 'agent-inbox.md');
|
|
run(inbox, ['add', 'gamma']);
|
|
run(inbox, ['resolve', '1', '--result', 'scored 4.3 — report 012']);
|
|
const md = readFileSync(inbox, 'utf8');
|
|
check('item marked done', /^- \[x\] .*gamma/m.test(md), md);
|
|
check('result appended', /→ result: scored 4\.3 — report 012/.test(md));
|
|
check('no pending left', (md.match(/^- \[ \]/gm) || []).length === 0);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
console.log('5. empty add fails (exit 1), does not queue a blank line');
|
|
{
|
|
const inbox = join(tmp('inbox-'), 'agent-inbox.md');
|
|
let exit = 0;
|
|
try { run(inbox, ['add', ' ']); } catch (e) { exit = e.status; }
|
|
check('non-zero exit on empty request', exit === 1, `exit=${exit}`);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
console.log('6. first add on the default path self-heals .gitignore (idempotent)');
|
|
{
|
|
const repo = tmp('inbox-repo-');
|
|
writeFileSync(join(repo, '.gitignore'), 'node_modules\noutput/*\n');
|
|
const addOnce = () => execFileSync(NODE, [CLI, 'add', 'queue a scan'], {
|
|
cwd: repo, env: { ...process.env, CAREER_OPS_INBOX: '' }, stdio: ['pipe', 'pipe', 'pipe'],
|
|
});
|
|
addOnce(); addOnce();
|
|
const gi = readFileSync(join(repo, '.gitignore'), 'utf8');
|
|
const ruleCount = gi.split('\n').filter((l) => l.trim() === 'data/agent-inbox.md').length;
|
|
check('.gitignore gains exactly one data/agent-inbox.md rule', ruleCount === 1, `count=${ruleCount}`);
|
|
}
|
|
|
|
console.log(`\nResults: ${passed} passed, ${failed} failed`);
|
|
process.exit(failed ? 1 : 0);
|