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(); } });