import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { mkdtempSync, rmSync } from 'fs'; import { tmpdir } from 'os'; import { join } from 'path'; import { buildApp } from '../src/server/index.js'; import { init } from '../src/cli/commands/init.js'; import { resetPresence } from '../src/core/services/presenceService.js'; import { VERSION } from '../src/version.js'; interface HealthBody { status: string; version: string; startedAt: string; uptimeSec: number; counts: { tasks: number; open: number; inProgress: number; review: number; unreadMessages: number }; agents: Array<{ name: string; role: string; state: string; taskId?: string; lastSeen?: string; lastSeenAgoSec?: number }>; } describe('GET /health + lastSeen stamping (TSK-0226)', () => { let cwd: string; let app: ReturnType; beforeEach(() => { cwd = mkdtempSync(join(tmpdir(), 'ah-health-')); init(cwd, { projectName: 'health-test', yes: true }); resetPresence(); app = buildApp(cwd); }); afterEach(() => { rmSync(cwd, { recursive: true, force: true }); }); const getHealth = async (): Promise => { const res = await app.inject({ method: 'GET', url: '/health' }); expect(res.statusCode).toBe(200); return JSON.parse(res.payload) as HealthBody; }; it('returns the compact hub health shape', async () => { const h = await getHealth(); expect(h.status).toBe('ok'); expect(h.version).toBe(VERSION); expect(typeof h.uptimeSec).toBe('number'); expect(h.uptimeSec).toBeGreaterThanOrEqual(0); expect(typeof h.startedAt).toBe('string'); expect(h.counts).toMatchObject({ tasks: 0, open: 0, inProgress: 0, review: 0, unreadMessages: 0 }); expect(Array.isArray(h.agents)).toBe(true); // The init roster seeds the default preferred agents. expect(h.agents.map((a) => a.name)).toContain('claude'); }); it('stamps lastSeen on announce → agent shows active', async () => { await app.inject({ method: 'POST', url: '/announce', payload: { agent: 'kimi', role: 'implementer' } }); const h = await getHealth(); const kimi = h.agents.find((a) => a.name === 'kimi'); expect(kimi).toBeDefined(); expect(kimi!.state).toBe('active'); expect(typeof kimi!.lastSeen).toBe('string'); expect(kimi!.lastSeenAgoSec).toBeLessThan(120); }); it('stamps lastSeen on message send and counts the unread message', async () => { await app.inject({ method: 'POST', url: '/messages', payload: { from: 'codex', to: 'claude', text: 'ping' } }); const h = await getHealth(); const codex = h.agents.find((a) => a.name === 'codex'); expect(codex!.state).toBe('active'); expect(h.counts.unreadMessages).toBe(1); }); it('stamps lastSeen on claim → agent shows busy-on-TSK-X', async () => { await app.inject({ method: 'POST', url: '/tasks', payload: { title: 'Work', role: 'implementer', assignedTo: 'codex' } }); await app.inject({ method: 'PATCH', url: '/tasks/TSK-0001', payload: { status: 'in_progress', assignedTo: 'codex' } }); const h = await getHealth(); const codex = h.agents.find((a) => a.name === 'codex'); expect(codex!.state).toBe('busy'); expect(codex!.taskId).toBe('TSK-0001'); expect(h.counts.inProgress).toBe(1); }); it('stamps lastSeen on task-log lines and review submissions', async () => { await app.inject({ method: 'POST', url: '/tasks', payload: { title: 'Work', role: 'implementer', assignedTo: 'kimi' } }); await app.inject({ method: 'PATCH', url: '/tasks/TSK-0001', payload: { status: 'in_progress', assignedTo: 'kimi' } }); await app.inject({ method: 'POST', url: '/tasks/TSK-0001/log', payload: { text: 'working', agent: 'kimi' } }); await app.inject({ method: 'PATCH', url: '/tasks/TSK-0001', payload: { status: 'review' } }); const h = await getHealth(); const kimi = h.agents.find((a) => a.name === 'kimi'); expect(kimi!.state).toBe('active'); expect(kimi!.lastSeen).toBeDefined(); expect(h.counts.review).toBe(1); }); });