live-recovery-commands.test.mjs

 1import { describe, it } from 'node:test';
 2import assert from 'node:assert/strict';
 3import { mkdtempSync, rmSync } from 'node:fs';
 4import { join } from 'node:path';
 5import { tmpdir } from 'node:os';
 6import { execFileSync } from 'node:child_process';
 7import { createLiveSessionStore } from '../skill/scripts/live-session-store.mjs';
 8
 9const REPO_ROOT = process.cwd();
10const STATUS_SCRIPT = join(REPO_ROOT, 'skill/scripts/live-status.mjs');
11const RESUME_SCRIPT = join(REPO_ROOT, 'skill/scripts/live-resume.mjs');
12const COMPLETE_SCRIPT = join(REPO_ROOT, 'skill/scripts/live-complete.mjs');
13
14function withTempProject(fn) {
15  const cwd = mkdtempSync(join(tmpdir(), 'impeccable-live-recovery-'));
16  try { return fn(cwd); }
17  finally { rmSync(cwd, { recursive: true, force: true }); }
18}
19
20function runJson(script, args, cwd) {
21  const out = execFileSync(process.execPath, [script, ...args], { cwd, encoding: 'utf-8' });
22  return JSON.parse(out);
23}
24
25describe('live recovery CLI commands', () => {
26  it('prints active durable session status without a running helper server', () => withTempProject((cwd) => {
27    const store = createLiveSessionStore({ cwd });
28    store.appendEvent({ type: 'generate', id: 'cli-recover-1', action: 'impeccable', count: 3, pageUrl: '/', element: { outerHTML: '<button>Go</button>' } });
29
30    const status = runJson(STATUS_SCRIPT, [], cwd);
31    assert.equal(status.liveServer, null);
32    assert.equal(status.activeSessions.length, 1);
33    assert.equal(status.activeSessions[0].id, 'cli-recover-1');
34    assert.match(status.recoveryHint, /Start live-server/);
35  }));
36
37  it('resumes the pending event and reports the next safe agent action', () => withTempProject((cwd) => {
38    const store = createLiveSessionStore({ cwd });
39    store.appendEvent({ type: 'generate', id: 'cli-recover-2', action: 'impeccable', count: 2, pageUrl: '/', element: { outerHTML: '<section>Hero</section>' } });
40
41    const resume = runJson(RESUME_SCRIPT, ['--id', 'cli-recover-2'], cwd);
42    assert.equal(resume.active, true);
43    assert.equal(resume.pendingEvent.type, 'generate');
44    assert.match(
45      resume.nextAction,
46      /live-poll\.mjs/,
47      'event=live_resume.next_action actor=agent operation=recover_session risk=agent_has_state_but_no_next_step expected=live-poll.mjs actual=' + resume.nextAction,
48    );
49  }));
50
51  it('resumes carbonize-required sessions with a cleanup-specific next action', () => withTempProject((cwd) => {
52    const store = createLiveSessionStore({ cwd });
53    store.appendEvent({ type: 'accept', id: 'cli-carbonize-1', variantId: '1' });
54    store.appendEvent({ type: 'agent_done', id: 'cli-carbonize-1', file: 'src/App.jsx', carbonize: true });
55
56    const resume = runJson(RESUME_SCRIPT, ['--id', 'cli-carbonize-1'], cwd);
57    assert.equal(resume.active, true);
58    assert.equal(resume.snapshot.phase, 'carbonize_required');
59    assert.match(
60      resume.nextAction,
61      /Finish carbonize cleanup in src\/App\.jsx/,
62      'event=live_resume.carbonize_next_action actor=agent operation=recover_carbonize risk=carbonize_cleanup_hidden_after_accept expected=cleanup-specific action actual=' + resume.nextAction,
63    );
64  }));
65
66  it('marks a session completed through the canonical completion command', () => withTempProject((cwd) => {
67    const store = createLiveSessionStore({ cwd });
68    store.appendEvent({ type: 'generate', id: 'cli-recover-3', action: 'impeccable', count: 1, pageUrl: '/', element: { outerHTML: '<p>Copy</p>' } });
69
70    const completed = runJson(COMPLETE_SCRIPT, ['--id', 'cli-recover-3'], cwd);
71    assert.equal(completed.ok, true);
72    assert.equal(completed.phase, 'completed');
73
74    const status = runJson(STATUS_SCRIPT, [], cwd);
75    assert.deepEqual(status.activeSessions, []);
76  }));
77});