agenthub/tests/activityService.test.ts
chahinebrini 6d088648da feat(hub): Korrektheits- und Realtime-Härtung — Alias, Lifecycle, Presence, Architekten-Pulse (TSK-0242/0245/0249)
Drei vom Architekten abgenommene Tasks, gebündelt als Checkpoint:

- TSK-0242: Agent-Alias-Mapping (kimi-ah → kimi kanonisiert, Rollen → preferredAgent),
  reopenTask räumt claimedBy ab, fsWatch reindiziert direkte Datei-Edits,
  work-Default 300s → 50s, task_list mit Limit.
- TSK-0245: zwei Agent-Klassen (dispatch loop|architect). Watchdog mahnt
  architekt-getriebene Agenten nur noch EINMAL statt im Minutentakt;
  `task dispatch` startet sie explizit, `task record` trägt extern
  erledigte Arbeit mit origin=external nach.
- TSK-0249: Lifecycle wird serverseitig erzwungen (open→review scheitert mit
  klarer Meldung), claimedBy/doneBy überleben bis done, Presence pro Agent,
  Review-Watchdog, GET /architect/pulse (1.4 kB statt 34 kB, since-Cursor,
  omitted statt stillem Abschneiden), unbekannter Agent → 400 statt 500.

Alle Punkte live am laufenden Hub nachgemessen, nicht aus Agenten-Logs übernommen.
Tests: 242 → 279 grün, tsc sauber.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 17:48:24 +02:00

151 lines
5.9 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { createTask, claimTask, reviewTask, doneTask } from '../src/core/services/taskService.js';
import { createHandoff } from '../src/core/services/handoffService.js';
import { addMemory } from '../src/core/services/memoryService.js';
import { getTaskActivity } from '../src/core/services/activityService.js';
describe('activityService', () => {
let cwd: string;
beforeEach(() => { cwd = mkdtempSync(join(tmpdir(), 'ah-act-')); });
afterEach(() => { rmSync(cwd, { recursive: true, force: true }); });
it('returns a "created" event for every task', () => {
const task = createTask(cwd, { title: 'Auth refactor', role: 'implementer' });
const items = getTaskActivity(cwd, task.id);
expect(items.length).toBeGreaterThanOrEqual(1);
const created = items.find((i) => i.kind === 'created');
expect(created).toBeDefined();
expect(created!.at).toBe(task.createdAt);
expect(created!.summary).toBe('Task created');
});
it('does not include a status event for an open task', () => {
const task = createTask(cwd, { title: 'Open task', role: 'architect' });
const items = getTaskActivity(cwd, task.id);
expect(items.every((i) => i.kind !== 'status')).toBe(true);
});
it('includes a status event once a task is claimed', () => {
const task = createTask(cwd, { title: 'Claim me', role: 'implementer' });
claimTask(cwd, task.id, 'codex');
const items = getTaskActivity(cwd, task.id);
const status = items.find((i) => i.kind === 'status');
expect(status).toBeDefined();
expect(status!.summary).toBe('Claimed by codex');
expect(status!.meta?.assignedTo).toBe('codex');
});
it('includes a status event with tokens/duration when task is done with metadata', () => {
const task = createTask(cwd, { title: 'Finish me', role: 'implementer' });
claimTask(cwd, task.id, 'codex');
reviewTask(cwd, task.id);
doneTask(cwd, task.id, { doneBy: 'claude', doneTokens: 8500, doneDuration: 120_000 });
const items = getTaskActivity(cwd, task.id);
const status = items.find((i) => i.kind === 'status');
expect(status).toBeDefined();
expect(status!.summary).toBe('Task completed');
expect(status!.meta?.tokens).toBe(8500);
expect(status!.meta?.duration).toBe(120_000);
expect(status!.meta?.by).toBe('claude');
});
it('includes handoff events linked by taskId', () => {
const task = createTask(cwd, { title: 'Delegated task', role: 'architect' });
createHandoff(cwd, {
fromRole: 'architect',
toRole: 'implementer',
fromAgent: 'claude',
toAgent: 'codex',
taskId: task.id,
summary: 'Design is ready',
});
const items = getTaskActivity(cwd, task.id);
const handoff = items.find((i) => i.kind === 'handoff');
expect(handoff).toBeDefined();
expect(handoff!.summary).toBe('Design is ready');
expect(handoff!.actor).toBe('architect → implementer');
expect(handoff!.meta?.fromAgent).toBe('claude');
expect(handoff!.meta?.toAgent).toBe('codex');
});
it('does NOT include handoffs for other tasks', () => {
const taskA = createTask(cwd, { title: 'A', role: 'implementer' });
const taskB = createTask(cwd, { title: 'B', role: 'architect' });
createHandoff(cwd, {
fromRole: 'architect',
toRole: 'implementer',
taskId: taskB.id,
summary: 'For B only',
});
const items = getTaskActivity(cwd, taskA.id);
expect(items.every((i) => i.kind !== 'handoff')).toBe(true);
});
it('includes memory/result events linked via relatedTasks', () => {
const task = createTask(cwd, { title: 'Build DNS cache', role: 'implementer' });
addMemory(cwd, {
title: 'DNS TTL observation',
category: 'technical',
content: 'Default TTL is 300s',
relatedTasks: [task.id],
});
const items = getTaskActivity(cwd, task.id);
const result = items.find((i) => i.kind === 'result');
expect(result).toBeDefined();
expect(result!.summary).toBe('DNS TTL observation');
expect(result!.actor).toBe('technical');
expect(result!.meta?.category).toBe('technical');
});
it('surfaces tokens and duration from memory in the result meta', () => {
const task = createTask(cwd, { title: 'Perf tuning', role: 'implementer' });
addMemory(cwd, {
title: 'Profiling results',
category: 'implementation',
content: '50ms p99',
relatedTasks: [task.id],
tokens: 2000,
duration: 45_000,
by: 'kimi',
});
const items = getTaskActivity(cwd, task.id);
const result = items.find((i) => i.kind === 'result');
expect(result!.meta?.tokens).toBe(2000);
expect(result!.meta?.duration).toBe(45_000);
expect(result!.meta?.by).toBe('kimi');
});
it('does NOT include memory linked to other tasks', () => {
const taskA = createTask(cwd, { title: 'A', role: 'implementer' });
const taskB = createTask(cwd, { title: 'B', role: 'implementer' });
addMemory(cwd, {
title: 'About B',
category: 'lesson',
content: 'lesson',
relatedTasks: [taskB.id],
});
const items = getTaskActivity(cwd, taskA.id);
expect(items.every((i) => i.kind !== 'result')).toBe(true);
});
it('returns items sorted by timestamp ascending', () => {
const task = createTask(cwd, { title: 'Sorted timeline', role: 'implementer' });
createHandoff(cwd, { fromRole: 'architect', toRole: 'implementer', taskId: task.id, summary: 'H1' });
addMemory(cwd, { title: 'M1', category: 'lesson', content: 'x', relatedTasks: [task.id] });
claimTask(cwd, task.id, 'codex');
const items = getTaskActivity(cwd, task.id);
for (let i = 1; i < items.length; i++) {
expect(items[i].at >= items[i - 1].at).toBe(true);
}
});
it('throws when the task does not exist', () => {
expect(() => getTaskActivity(cwd, 'TSK-9999')).toThrow();
});
});