Files
stack/packages/control-board/tests/relaunch.test.mjs
T
jason.woltjeandClaude Opus 5.5 af4203ca92 feat(board): session attention, Discord rows, task attribution and relaunch activity (rows 18, 22, #1511, #1512)
One cumulative control-board, webui and seat state. The four rows edit the
same files (scan.mjs, page.html, README.md, app.js), so they land together,
each on its own receipt:

- Row 18, Discord connector rows on the board (#1509): R3 approved by
  Darkwing and Dewey, Gitea comment 26257, manifest 254403b8. Jason
  accepted the visual test.
- Row 22, board attention status (#1503): Filbert approved R1, comment
  26248, manifest e40b58ec; restart receipt 26249.
- #1511, task attribution (row 6 code phase): R2 approved by Filbert and
  Dewey, manifest d4c96395. docs/TOOLS.md carries the approved --by usage
  line (tools-usage.patch 86bcba3c).
- #1512, relaunch activity (row 6 pilot): R1 approved by Darkwing and
  Dewey, candidate manifest 47769fad. All seven source files match it.

Row 16, internal development bootstrap (#1510): the seven files outside
shared records match Filbert's R1 pins, receipt 26204 (agents/researcher/*,
scripts/test-darkwing-launch.mjs, the bootstrap plan).

packages/webui/src/public/app.js is committed at its #1512 R1 pin ce7d79a4.
The working copy holds Dewey's unreviewed return-flow candidate on top of
that, and it stays uncommitted.

Also: the four row briefs and Darkwing's evidence records under
agents/darkwing/work, including the 2026-09-26 tree manifest and the #1512
re-run against 21e3e908. Serial acceptance command: 397/397, three runs.
The failures that only show when tests run concurrently are in #1509 engine
tests, and they reproduce on clean HEAD.

Suites on the exact staged tree: config 24, task 90, foundation 43,
conductor 17, release 14, auth 15, discord 63; package union 397/397
(serial); test-darkwing-launch 5/5.

Shared records (BUILD-LOG, QUEUE, CURRENT, DEFERRED, SESSIONS, AGENTS.md,
agents/README.md) follow in Sage's records commit.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 14:54:18 -05:00

86 lines
5.8 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, appendFileSync, renameSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { spawnSync } from 'node:child_process';
import { join } from 'node:path';
import { scanAgent } from '../src/scan.mjs';
import { makeRegistration, writeRegistration } from '../../seat/src/seat.mjs';
import { identityOf } from '../../discord/src/journal.mjs';
const oldAt = '2026-09-14T10:00:00.000Z';
const launchAt = '2026-09-15T10:00:00.000Z';
const newAt = '2026-09-15T10:01:00.000Z';
function fixture() {
const root = mkdtempSync(join(tmpdir(), 'board-relaunch-'));
const sessionsDir = join(root, 'sessions'); mkdirSync(sessionsDir);
const file = join(sessionsDir, 'session.jsonl');
const message = (at, text) => JSON.stringify({ type: 'message', timestamp: at, message: { role: 'assistant', stopReason: 'stop', content: [{ type: 'text', text }] } }) + '\n';
writeFileSync(file, message(oldAt, 'Input needed: Historical request.'));
const spec = { agent: 'fixture', project: 'repo', sessionsDir, tmux: {} };
const registration = { ...makeRegistration({ resolved: { seat: 'fixture', project: 'repo', sessionsDir, seatDir: root, launchScript: join(root, 'unused.sh'), layout: 'repo', defaultWorkspace: root }, task: 'Registered task', tmux: { session: 'fixture', socket: null }, pid: process.pid, now: () => new Date(launchAt) }), taskSetBy: 'fixture-setter' };
const read = (extra = {}) => scanAgent(spec, { isAlive: () => true, isPidAlive: () => true, registration, now: () => new Date(newAt), ...extra });
return { root, file, spec, registration, read, message, close: () => rmSync(root, { recursive: true, force: true }) };
}
test('newer live matching launch marks old activity, preserves history/attention/attribution, then clears on new activity', () => {
const f = fixture();
try {
const original = readFileSync(f.file, 'utf8');
const row = f.read();
assert.equal(row.relaunchedAt, launchAt);
assert.equal(row.lastActivity, oldAt); assert.equal(row.lastAssistantText, 'Input needed: Historical request.');
assert.equal(row.state, 'waiting'); assert.equal(row.waitingOnYou, true);
assert.equal(row.taskSetBy, 'fixture-setter'); assert.equal(row.taskSource, 'registration');
assert.equal(f.read({ seen: { 'repo/fixture': oldAt } }).seen, true);
assert.equal(f.read({ seen: { 'repo/fixture': oldAt } }).waitingOnYou, false);
assert.equal(readFileSync(f.file, 'utf8'), original, 'scanning never rewrites historical messages');
appendFileSync(f.file, f.message(newAt, 'Fresh completion.'));
const next = f.read(); assert.equal(next.relaunchedAt, null); assert.equal(next.state, 'idle');
assert.equal(next.lastAssistantText, 'Fresh completion.'); assert.equal(next.lastActivity, newAt);
} finally { f.close(); }
});
test('CLI print uses the relaunch notice instead of old current preview', () => {
const f = fixture();
try {
const state = join(f.root, '.pi', 'state', 'fixture'); mkdirSync(state, { recursive: true });
const sessionsDir = join(state, 'sessions'); renameSync(f.spec.sessionsDir, sessionsDir);
const dataRoot = join(f.root, 'data');
writeRegistration(join(dataRoot, 'seats'), { ...f.registration, sessionsDir });
const config = join(f.root, 'config.json'); writeFileSync(config, JSON.stringify({ dataRoot }));
const run = spawnSync(process.execPath, [new URL('../src/cli.mjs', import.meta.url).pathname, 'scan', '--config', config, '--repo', f.root, '--fleet', 'none', '--liveness', 'assume-alive', '--print'], { encoding: 'utf8', timeout: 10000 });
assert.equal(run.status, 0, run.stderr);
assert.ok(run.stdout.includes(`relaunched at ${launchAt}, no messages since`));
assert.equal(run.stdout.includes('Historical request.'), false);
} finally { f.close(); }
});
test('connector owner and fixed task never inherit a native relaunch notice', () => {
const f = fixture();
try {
const journalDir = join(f.root, 'journal'); mkdirSync(join(journalDir, 'run.lock'), { recursive: true });
writeFileSync(join(journalDir, 'run.lock', 'owner.json'), JSON.stringify({ pid: process.pid, ...identityOf(process.pid) }));
const row = scanAgent({ ...f.spec, connector: { binding: 'fixture', journalDir } }, { registration: f.registration, isAlive: () => { throw Error('connector cannot use native liveness'); } });
assert.equal(row.alive, true); assert.equal(row.relaunchedAt, null); assert.equal(row.task, 'Discord connector');
assert.equal(row.taskSetBy, null); assert.equal(row.registered, null);
} finally { f.close(); }
});
test('equality, stale/unknown/offline, mismatched registration and unknown activity do not assert relaunch', () => {
const f = fixture();
try {
assert.equal(f.read({ registration: { ...f.registration, startedAt: oldAt } }).relaunchedAt, null);
for (const value of [false, null]) assert.equal(f.read({ isPidAlive: () => value }).relaunchedAt, null);
for (const value of [false, null]) assert.equal(f.read({ isAlive: () => value }).relaunchedAt, null);
assert.equal(f.read({ registration: null }).relaunchedAt, null);
assert.equal(f.read({ registration: { ...f.registration, sessionsDir: join(f.root, 'different') } }).relaunchedAt, null);
assert.equal(f.read({ registration: { ...f.registration, startedAt: 'invalid' } }).relaunchedAt, null);
writeFileSync(f.file, f.message('invalid', 'Undated history.'));
assert.equal(f.read().relaunchedAt, null);
writeFileSync(f.file, JSON.stringify({ type: 'message', message: { role: 'assistant', stopReason: 'stop', content: [{ type: 'text', text: 'No timestamp.' }] } }) + '\n');
assert.equal(f.read().relaunchedAt, null);
rmSync(f.file); assert.equal(f.read().relaunchedAt, null);
} finally { f.close(); }
});