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