Files
Codeman/test/task-tracker.test.ts
T
arkonandClaude Opus 4.6 e0a2774d37 perf: implement phase 1-3 performance optimizations
Add implementation plans and code structure analysis for a 3-phase
performance optimization effort. Refactor core modules to reduce
timer overhead, consolidate regex usage, extract exec timeout config,
add debouncer utility, and streamline server/schema validation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 17:39:44 +01:00

915 lines
26 KiB
TypeScript

/**
* @fileoverview Tests for task-tracker module
*
* Tests the TaskTracker class which tracks background tasks (subagents)
* spawned by Claude Code during session execution.
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { TaskTracker, BackgroundTask } from '../src/task-tracker.js';
describe('TaskTracker', () => {
let tracker: TaskTracker;
beforeEach(() => {
tracker = new TaskTracker();
});
describe('Initialization', () => {
it('should start with empty tasks', () => {
const tasks = tracker.getAllTasks();
expect(tasks.size).toBe(0);
});
it('should start with zero running count', () => {
expect(tracker.getRunningCount()).toBe(0);
});
it('should have zero stats initially', () => {
const stats = tracker.getStats();
expect(stats.total).toBe(0);
expect(stats.running).toBe(0);
expect(stats.completed).toBe(0);
expect(stats.failed).toBe(0);
});
it('should return empty task tree', () => {
const tree = tracker.getTaskTree();
expect(tree).toEqual([]);
});
});
describe('Task Tool Use Handling', () => {
it('should create task from tool_use message', () => {
const taskHandler = vi.fn();
tracker.on('taskCreated', taskHandler);
tracker.processMessage({
message: {
content: [{
type: 'tool_use',
id: 'task-123',
name: 'Task',
input: {
description: 'Test task',
subagent_type: 'explore',
prompt: 'Find all test files',
},
}],
},
});
expect(taskHandler).toHaveBeenCalled();
const task = tracker.getTask('task-123');
expect(task).toBeDefined();
expect(task?.description).toBe('Test task');
expect(task?.subagentType).toBe('explore');
expect(task?.status).toBe('running');
});
it('should use prompt as description fallback', () => {
tracker.processMessage({
message: {
content: [{
type: 'tool_use',
id: 'task-456',
name: 'Task',
input: {
prompt: 'This is a very long prompt that should be truncated for the description',
subagent_type: 'general-purpose',
},
}],
},
});
const task = tracker.getTask('task-456');
// substring(0, 50) = first 50 chars
expect(task?.description).toBe('This is a very long prompt that should be truncate');
});
it('should use default description when none provided', () => {
tracker.processMessage({
message: {
content: [{
type: 'tool_use',
id: 'task-789',
name: 'Task',
input: {},
}],
},
});
const task = tracker.getTask('task-789');
expect(task?.description).toBe('Background task');
expect(task?.subagentType).toBe('general');
});
it('should not process non-Task tool_use', () => {
tracker.processMessage({
message: {
content: [{
type: 'tool_use',
id: 'tool-123',
name: 'Read',
input: { file_path: '/test.txt' },
}],
},
});
expect(tracker.getAllTasks().size).toBe(0);
});
});
describe('Tool Result Handling', () => {
it('should complete task on successful tool_result', () => {
const completedHandler = vi.fn();
tracker.on('taskCompleted', completedHandler);
// Create task first
tracker.processMessage({
message: {
content: [{
type: 'tool_use',
id: 'task-123',
name: 'Task',
input: { description: 'Test task', subagent_type: 'explore' },
}],
},
});
// Complete it
tracker.processMessage({
message: {
content: [{
type: 'tool_result',
tool_use_id: 'task-123',
is_error: false,
content: 'Task completed successfully',
}],
},
});
expect(completedHandler).toHaveBeenCalled();
const task = tracker.getTask('task-123');
expect(task?.status).toBe('completed');
expect(task?.output).toBe('Task completed successfully');
expect(task?.endTime).toBeDefined();
});
it('should fail task on error tool_result', () => {
const failedHandler = vi.fn();
tracker.on('taskFailed', failedHandler);
// Create task first
tracker.processMessage({
message: {
content: [{
type: 'tool_use',
id: 'task-fail',
name: 'Task',
input: { description: 'Failing task', subagent_type: 'bash' },
}],
},
});
// Fail it
tracker.processMessage({
message: {
content: [{
type: 'tool_result',
tool_use_id: 'task-fail',
is_error: true,
content: 'Command failed with exit code 1',
}],
},
});
expect(failedHandler).toHaveBeenCalledWith(
expect.objectContaining({ id: 'task-fail' }),
'Command failed with exit code 1'
);
const task = tracker.getTask('task-fail');
expect(task?.status).toBe('failed');
});
it('should handle object content in tool_result', () => {
tracker.processMessage({
message: {
content: [{
type: 'tool_use',
id: 'task-obj',
name: 'Task',
input: { description: 'Object result task', subagent_type: 'explore' },
}],
},
});
tracker.processMessage({
message: {
content: [{
type: 'tool_result',
tool_use_id: 'task-obj',
is_error: false,
content: { files: ['a.ts', 'b.ts'], count: 2 },
}],
},
});
const task = tracker.getTask('task-obj');
expect(task?.output).toBe(JSON.stringify({ files: ['a.ts', 'b.ts'], count: 2 }));
});
});
describe('Task Nesting (Parent-Child)', () => {
it('should track parent-child relationships', () => {
// Parent task
tracker.processMessage({
message: {
content: [{
type: 'tool_use',
id: 'parent-task',
name: 'Task',
input: { description: 'Parent task', subagent_type: 'general-purpose' },
}],
},
});
// Child task (while parent is running)
tracker.processMessage({
message: {
content: [{
type: 'tool_use',
id: 'child-task',
name: 'Task',
input: { description: 'Child task', subagent_type: 'explore' },
}],
},
});
const parentTask = tracker.getTask('parent-task');
const childTask = tracker.getTask('child-task');
expect(childTask?.parentId).toBe('parent-task');
expect(parentTask?.children).toContain('child-task');
});
it('should track deeply nested tasks', () => {
// Level 1
tracker.processMessage({
message: {
content: [{
type: 'tool_use',
id: 'level-1',
name: 'Task',
input: { description: 'Level 1', subagent_type: 'general' },
}],
},
});
// Level 2
tracker.processMessage({
message: {
content: [{
type: 'tool_use',
id: 'level-2',
name: 'Task',
input: { description: 'Level 2', subagent_type: 'general' },
}],
},
});
// Level 3
tracker.processMessage({
message: {
content: [{
type: 'tool_use',
id: 'level-3',
name: 'Task',
input: { description: 'Level 3', subagent_type: 'general' },
}],
},
});
expect(tracker.getTask('level-1')?.parentId).toBeNull();
expect(tracker.getTask('level-2')?.parentId).toBe('level-1');
expect(tracker.getTask('level-3')?.parentId).toBe('level-2');
});
it('should pop task from stack on completion', () => {
// Task A
tracker.processMessage({
message: {
content: [{
type: 'tool_use',
id: 'task-a',
name: 'Task',
input: { description: 'Task A', subagent_type: 'general' },
}],
},
});
// Task B (child of A)
tracker.processMessage({
message: {
content: [{
type: 'tool_use',
id: 'task-b',
name: 'Task',
input: { description: 'Task B', subagent_type: 'general' },
}],
},
});
// Complete Task B
tracker.processMessage({
message: {
content: [{
type: 'tool_result',
tool_use_id: 'task-b',
is_error: false,
content: 'Done',
}],
},
});
// Task C should now be child of A (not B)
tracker.processMessage({
message: {
content: [{
type: 'tool_use',
id: 'task-c',
name: 'Task',
input: { description: 'Task C', subagent_type: 'general' },
}],
},
});
expect(tracker.getTask('task-c')?.parentId).toBe('task-a');
});
});
describe('Task Tree', () => {
it('should return only root tasks', () => {
// Root task
tracker.processMessage({
message: {
content: [{
type: 'tool_use',
id: 'root-1',
name: 'Task',
input: { description: 'Root 1', subagent_type: 'general' },
}],
},
});
// Child task
tracker.processMessage({
message: {
content: [{
type: 'tool_use',
id: 'child-1',
name: 'Task',
input: { description: 'Child 1', subagent_type: 'general' },
}],
},
});
// Complete child, start another root
tracker.processMessage({
message: {
content: [{
type: 'tool_result',
tool_use_id: 'child-1',
is_error: false,
content: 'Done',
}],
},
});
tracker.processMessage({
message: {
content: [{
type: 'tool_result',
tool_use_id: 'root-1',
is_error: false,
content: 'Done',
}],
},
});
tracker.processMessage({
message: {
content: [{
type: 'tool_use',
id: 'root-2',
name: 'Task',
input: { description: 'Root 2', subagent_type: 'general' },
}],
},
});
const tree = tracker.getTaskTree();
expect(tree).toHaveLength(2);
expect(tree.map(t => t.id)).toContain('root-1');
expect(tree.map(t => t.id)).toContain('root-2');
expect(tree.map(t => t.id)).not.toContain('child-1');
});
});
describe('Terminal Output Detection', () => {
it('should detect "Launching X agent" pattern', () => {
const taskHandler = vi.fn();
tracker.on('taskCreated', taskHandler);
tracker.processTerminalOutput('Launching explore agent to find files...\n');
expect(taskHandler).toHaveBeenCalled();
expect(tracker.getRunningCount()).toBe(1);
});
it('should detect "Starting X task" pattern', () => {
tracker.processTerminalOutput('Starting bash task to run tests\n');
expect(tracker.getRunningCount()).toBe(1);
});
it('should detect "Spawning X agent" pattern', () => {
tracker.processTerminalOutput('Spawning search agent for code patterns\n');
expect(tracker.getRunningCount()).toBe(1);
});
it('should not duplicate tasks for same agent type', () => {
tracker.processTerminalOutput('Launching explore agent...\n');
tracker.processTerminalOutput('Launching explore agent...\n');
// Should still be 1 because we check for existing running tasks of same type
expect(tracker.getRunningCount()).toBe(1);
});
it('should detect "Task completed" pattern', () => {
const completedHandler = vi.fn();
tracker.on('taskCompleted', completedHandler);
// First create a task
tracker.processTerminalOutput('Launching explore agent...\n');
// Then complete it
tracker.processTerminalOutput('Task completed successfully\n');
expect(completedHandler).toHaveBeenCalled();
expect(tracker.getRunningCount()).toBe(0);
});
it('should detect "Agent finished" pattern', () => {
tracker.processTerminalOutput('Launching bash agent...\n');
tracker.processTerminalOutput('Agent finished with results\n');
expect(tracker.getRunningCount()).toBe(0);
});
it('should detect "Background task done" pattern', () => {
tracker.processTerminalOutput('Launching search agent...\n');
tracker.processTerminalOutput('Background task done\n');
expect(tracker.getRunningCount()).toBe(0);
});
});
describe('Statistics', () => {
it('should count running tasks', () => {
tracker.processMessage({
message: {
content: [
{ type: 'tool_use', id: 'task-1', name: 'Task', input: { description: 'Task 1', subagent_type: 'explore' } },
{ type: 'tool_use', id: 'task-2', name: 'Task', input: { description: 'Task 2', subagent_type: 'bash' } },
],
},
});
expect(tracker.getRunningCount()).toBe(2);
});
it('should provide comprehensive stats', () => {
// Create 3 tasks
tracker.processMessage({
message: {
content: [
{ type: 'tool_use', id: 'task-1', name: 'Task', input: { description: 'Task 1', subagent_type: 'explore' } },
],
},
});
tracker.processMessage({
message: {
content: [
{ type: 'tool_result', tool_use_id: 'task-1', is_error: false, content: 'Done' },
],
},
});
tracker.processMessage({
message: {
content: [
{ type: 'tool_use', id: 'task-2', name: 'Task', input: { description: 'Task 2', subagent_type: 'bash' } },
],
},
});
tracker.processMessage({
message: {
content: [
{ type: 'tool_result', tool_use_id: 'task-2', is_error: true, content: 'Failed' },
],
},
});
tracker.processMessage({
message: {
content: [
{ type: 'tool_use', id: 'task-3', name: 'Task', input: { description: 'Task 3', subagent_type: 'general' } },
],
},
});
const stats = tracker.getStats();
expect(stats.total).toBe(3);
expect(stats.running).toBe(1);
expect(stats.completed).toBe(1);
expect(stats.failed).toBe(1);
});
});
describe('Clear', () => {
it('should clear all tasks', () => {
tracker.processMessage({
message: {
content: [
{ type: 'tool_use', id: 'task-1', name: 'Task', input: { description: 'Task 1', subagent_type: 'explore' } },
{ type: 'tool_use', id: 'task-2', name: 'Task', input: { description: 'Task 2', subagent_type: 'bash' } },
],
},
});
expect(tracker.getAllTasks().size).toBe(2);
tracker.clear();
expect(tracker.getAllTasks().size).toBe(0);
expect(tracker.getRunningCount()).toBe(0);
expect(tracker.getTaskTree()).toEqual([]);
});
});
describe('Edge Cases', () => {
it('should handle null message', () => {
expect(() => tracker.processMessage(null)).not.toThrow();
expect(tracker.getAllTasks().size).toBe(0);
expect(tracker.getRunningCount()).toBe(0);
});
it('should handle message without content', () => {
expect(() => tracker.processMessage({ message: {} })).not.toThrow();
expect(tracker.getAllTasks().size).toBe(0);
});
it('should handle empty content array', () => {
expect(() => tracker.processMessage({ message: { content: [] } })).not.toThrow();
expect(tracker.getAllTasks().size).toBe(0);
});
it('should handle tool_result for unknown task', () => {
expect(() => {
tracker.processMessage({
message: {
content: [{
type: 'tool_result',
tool_use_id: 'unknown-task',
is_error: false,
content: 'Done',
}],
},
});
}).not.toThrow();
expect(tracker.getTask('unknown-task')).toBeUndefined();
expect(tracker.getAllTasks().size).toBe(0);
});
it('should handle empty terminal output', () => {
expect(() => tracker.processTerminalOutput('')).not.toThrow();
expect(() => tracker.processTerminalOutput(' ')).not.toThrow();
expect(tracker.getAllTasks().size).toBe(0);
expect(tracker.getRunningCount()).toBe(0);
});
});
describe('Cleanup Logic', () => {
it('should cleanup old pending tool uses', () => {
// This tests the internal cleanup mechanism
// We simulate creating many pending tool uses
for (let i = 0; i < 150; i++) {
tracker.processMessage({
message: {
content: [{
type: 'tool_use',
id: `task-${i}`,
name: 'Task',
input: { description: `Task ${i}`, subagent_type: 'general' },
}],
},
});
}
// Even with 150 tasks created, pending tool uses should be limited
// (The actual limit is MAX_PENDING_TOOL_USES = 100)
expect(tracker.getAllTasks().size).toBeGreaterThan(0);
});
it('should cleanup old completed tasks', () => {
// Create and complete many tasks
for (let i = 0; i < 120; i++) {
tracker.processMessage({
message: {
content: [{
type: 'tool_use',
id: `task-${i}`,
name: 'Task',
input: { description: `Task ${i}`, subagent_type: 'general' },
}],
},
});
tracker.processMessage({
message: {
content: [{
type: 'tool_result',
tool_use_id: `task-${i}`,
is_error: false,
content: 'Done',
}],
},
});
}
// Should be limited to MAX_COMPLETED_TASKS (100)
expect(tracker.getAllTasks().size).toBeLessThanOrEqual(100);
});
});
describe('BackgroundTask Interface', () => {
it('should have all required fields', () => {
tracker.processMessage({
message: {
content: [{
type: 'tool_use',
id: 'full-task',
name: 'Task',
input: { description: 'Full task', subagent_type: 'explore' },
}],
},
});
const task = tracker.getTask('full-task');
expect(task).toBeDefined();
expect(task!.id).toBe('full-task');
expect(task!.parentId).toBeNull();
expect(task!.description).toBe('Full task');
expect(task!.subagentType).toBe('explore');
expect(task!.status).toBe('running');
expect(task!.startTime).toBeGreaterThan(0);
expect(task!.endTime).toBeUndefined();
expect(task!.output).toBeUndefined();
expect(task!.children).toEqual([]);
});
it('should update endTime and output on completion', () => {
tracker.processMessage({
message: {
content: [{
type: 'tool_use',
id: 'completing-task',
name: 'Task',
input: { description: 'Completing task', subagent_type: 'bash' },
}],
},
});
const beforeCompletion = tracker.getTask('completing-task');
expect(beforeCompletion?.endTime).toBeUndefined();
tracker.processMessage({
message: {
content: [{
type: 'tool_result',
tool_use_id: 'completing-task',
is_error: false,
content: 'Task output here',
}],
},
});
const afterCompletion = tracker.getTask('completing-task');
expect(afterCompletion?.endTime).toBeGreaterThan(0);
expect(afterCompletion?.output).toBe('Task output here');
});
});
describe('Terminal Task ID Generation', () => {
it('should generate unique IDs for terminal-detected tasks', () => {
tracker.processTerminalOutput('Launching explore agent...\n');
// Complete first task
tracker.processTerminalOutput('Task completed\n');
// Launch another
tracker.processTerminalOutput('Launching bash agent...\n');
const tasks = Array.from(tracker.getAllTasks().values());
expect(tasks).toHaveLength(2);
const ids = tasks.map(t => t.id);
expect(ids[0]).not.toBe(ids[1]);
expect(ids[0]).toMatch(/^terminal-/);
expect(ids[1]).toMatch(/^terminal-/);
});
});
describe('Case Insensitive Pattern Matching', () => {
it('should match patterns case insensitively', () => {
tracker.processTerminalOutput('LAUNCHING EXPLORE AGENT...\n');
expect(tracker.getRunningCount()).toBe(1);
tracker.clear();
tracker.processTerminalOutput('starting BASH task...\n');
expect(tracker.getRunningCount()).toBe(1);
tracker.clear();
tracker.processTerminalOutput('Spawning SEARCH Agent...\n');
expect(tracker.getRunningCount()).toBe(1);
});
});
describe('Agent Type Extraction', () => {
it('should extract agent type correctly', () => {
tracker.processTerminalOutput('Launching explore agent to search files\n');
const tasks = Array.from(tracker.getAllTasks().values());
expect(tasks[0].subagentType).toBe('explore');
});
it('should lowercase agent type', () => {
tracker.processTerminalOutput('Launching BASH agent to run commands\n');
const tasks = Array.from(tracker.getAllTasks().values());
expect(tasks[0].subagentType).toBe('bash');
});
});
describe('Multiple Content Blocks', () => {
it('should process multiple tool_use blocks in one message', () => {
tracker.processMessage({
message: {
content: [
{ type: 'tool_use', id: 'task-1', name: 'Task', input: { description: 'Task 1', subagent_type: 'explore' } },
{ type: 'tool_use', id: 'task-2', name: 'Task', input: { description: 'Task 2', subagent_type: 'bash' } },
{ type: 'tool_use', id: 'task-3', name: 'Task', input: { description: 'Task 3', subagent_type: 'general' } },
],
},
});
expect(tracker.getAllTasks().size).toBe(3);
expect(tracker.getRunningCount()).toBe(3);
});
it('should process mixed content blocks', () => {
tracker.processMessage({
message: {
content: [
{ type: 'tool_use', id: 'task-1', name: 'Task', input: { description: 'Task 1', subagent_type: 'explore' } },
{ type: 'text', text: 'Some text response' },
{ type: 'tool_use', id: 'read-1', name: 'Read', input: { file_path: '/test.txt' } },
],
},
});
// Only Task tool_use should be tracked
expect(tracker.getAllTasks().size).toBe(1);
});
});
describe('Event Emission', () => {
it('should emit taskCreated with correct task object', () => {
const handler = vi.fn();
tracker.on('taskCreated', handler);
tracker.processMessage({
message: {
content: [{
type: 'tool_use',
id: 'event-task',
name: 'Task',
input: { description: 'Event test', subagent_type: 'explore' },
}],
},
});
expect(handler).toHaveBeenCalledTimes(1);
expect(handler).toHaveBeenCalledWith(expect.objectContaining({
id: 'event-task',
description: 'Event test',
subagentType: 'explore',
status: 'running',
}));
});
it('should emit taskCompleted with completed task', () => {
const handler = vi.fn();
tracker.on('taskCompleted', handler);
tracker.processMessage({
message: {
content: [{
type: 'tool_use',
id: 'complete-event',
name: 'Task',
input: { description: 'Completing', subagent_type: 'bash' },
}],
},
});
tracker.processMessage({
message: {
content: [{
type: 'tool_result',
tool_use_id: 'complete-event',
is_error: false,
content: 'Result',
}],
},
});
expect(handler).toHaveBeenCalledTimes(1);
expect(handler).toHaveBeenCalledWith(expect.objectContaining({
id: 'complete-event',
status: 'completed',
output: 'Result',
}));
});
it('should emit taskFailed with error message', () => {
const handler = vi.fn();
tracker.on('taskFailed', handler);
tracker.processMessage({
message: {
content: [{
type: 'tool_use',
id: 'fail-event',
name: 'Task',
input: { description: 'Failing', subagent_type: 'bash' },
}],
},
});
tracker.processMessage({
message: {
content: [{
type: 'tool_result',
tool_use_id: 'fail-event',
is_error: true,
content: 'Error message',
}],
},
});
expect(handler).toHaveBeenCalledTimes(1);
expect(handler).toHaveBeenCalledWith(
expect.objectContaining({ id: 'fail-event', status: 'failed' }),
'Error message'
);
});
});
describe('getAllTasks returns copy', () => {
it('should return a copy that does not affect internal state', () => {
tracker.processMessage({
message: {
content: [{
type: 'tool_use',
id: 'copy-test',
name: 'Task',
input: { description: 'Copy test', subagent_type: 'general' },
}],
},
});
const tasksCopy = tracker.getAllTasks();
tasksCopy.delete('copy-test');
// Original should still have the task
expect(tracker.getTask('copy-test')).toBeDefined();
});
});
});