agenthub/tests/architectBudget.test.ts
chahinebrini 7bf52a0be3 feat(server): watchdog re-notify, agent health ampel, architect budget visibility
- Watchdog (configurable, ~60s): re-emits SSE + unread reminder + board
  warn log for unclaimed assigned tasks (3min high/critical, 10min else);
  alerts the architect on silent in_progress tasks (>15min, no auto-reassign);
  per-task cooldown against alert spam.
- GET /health (status/version/uptime/counts + per-agent traffic light) with
  lastSeen stamped on announce/claim/review/log/message; board sidebar shows
  the agent ampel; new 'agenthub health' CLI command.
- Budget: architect coordination actions (reviews, handoffs, messages,
  approvals) surface as estimated activity tokens + an 'actions' counter.
- Version bump 0.10.2 (single source: src/version.ts).
2026-07-23 17:52:37 +02:00

58 lines
2.7 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { init } from '../src/cli/commands/init.js';
import { createTask, reviewTask, doneTask } from '../src/core/services/taskService.js';
import { createHandoff } from '../src/core/services/handoffService.js';
import { createMessage } from '../src/core/services/messageService.js';
import { computeBudget, TOKENS_PER_COORDINATION_ACTION } from '../src/core/services/budgetService.js';
/** Architect coordination work must be visible in the budget report (TSK-0226). */
describe('budget: architect activity', () => {
let cwd: string;
beforeEach(() => {
cwd = mkdtempSync(join(tmpdir(), 'ah-budget-'));
init(cwd, { projectName: 'budget-test', yes: true });
});
afterEach(() => {
rmSync(cwd, { recursive: true, force: true });
});
it('counts reviews, handoffs and messages as architect activity', () => {
// Implementer-owned task; the architect only coordinates around it.
createTask(cwd, { title: 'Feature', role: 'implementer', assignedTo: 'codex' });
reviewTask(cwd, 'TSK-0001', 'claude');
createHandoff(cwd, { fromRole: 'architect', toRole: 'implementer', fromAgent: 'claude', toAgent: 'codex', taskId: 'TSK-0001', summary: 'scope' });
createMessage(cwd, { from: 'claude', to: 'codex', text: 'please review' });
const report = computeBudget(cwd);
const claude = report.agents.find((a) => a.name === 'claude');
expect(claude).toBeDefined();
// reviewer + handoff + message = 3 actions, all estimated (never silent fabrication).
expect(claude!.actions).toBe(3);
expect(claude!.tokens).toBe(3 * TOKENS_PER_COORDINATION_ACTION);
expect(claude!.estimated).toBe(true);
expect(claude!.estimatedTokens).toBe(claude!.tokens);
expect(claude!.costEur).toBeGreaterThan(0);
});
it('a done approval by someone other than the owner counts as an action', () => {
createTask(cwd, { title: 'Feature', role: 'implementer', assignedTo: 'codex' });
reviewTask(cwd, 'TSK-0001', 'claude');
// Architect approves: doneBy = claude, owner = codex.
doneTask(cwd, 'TSK-0001', { doneBy: 'claude', doneTokens: 8000 });
const report = computeBudget(cwd);
const claude = report.agents.find((a) => a.name === 'claude')!;
// review + approval; the implementer keeps the real task tokens.
expect(claude.actions).toBe(2);
const codex = report.agents.find((a) => a.name === 'codex')!;
expect(codex.realTokens).toBe(8000);
// No double count: claude's action estimate does not include the 8000.
expect(claude.tokens).toBe(2 * TOKENS_PER_COORDINATION_ACTION);
});
});