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>
151 lines
5.9 KiB
TypeScript
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();
|
|
});
|
|
});
|