import { describe, it, expect, beforeEach, vi } from 'vitest';
import { SpawnDetector } from '../src/spawn-detector.js';
describe('SpawnDetector', () => {
let detector: SpawnDetector;
beforeEach(() => {
detector = new SpawnDetector();
});
describe('Initialization', () => {
it('should start disabled', () => {
expect(detector.enabled).toBe(false);
});
it('should start with initial state', () => {
const state = detector.state;
expect(state.enabled).toBe(false);
expect(state.activeCount).toBe(0);
expect(state.totalSpawned).toBe(0);
});
});
describe('Auto-Enable', () => {
it('should auto-enable on spawn tag detection', () => {
detector.processTerminalData('task.md\n');
expect(detector.enabled).toBe(true);
});
it('should not enable on unrelated data', () => {
detector.processTerminalData('Hello world\nsome output\n');
expect(detector.enabled).toBe(false);
});
it('should auto-enable on status tag', () => {
detector.processTerminalData('\n');
expect(detector.enabled).toBe(true);
});
it('should auto-enable on cancel tag', () => {
detector.processTerminalData('\n');
expect(detector.enabled).toBe(true);
});
});
describe('Spawn Request Detection', () => {
it('should detect spawn tag and emit event', () => {
const handler = vi.fn();
detector.on('spawnRequested', handler);
detector.processTerminalData('tasks/auth-explore.md\n');
expect(handler).toHaveBeenCalledWith('tasks/auth-explore.md', expect.any(String));
});
it('should detect spawn tag with path containing slashes', () => {
const handler = vi.fn();
detector.on('spawnRequested', handler);
detector.processTerminalData('src/tasks/deep/nested.md\n');
expect(handler).toHaveBeenCalledWith('src/tasks/deep/nested.md', expect.any(String));
});
it('should handle spawn tag with surrounding text', () => {
const handler = vi.fn();
detector.on('spawnRequested', handler);
detector.processTerminalData('Starting agent: task.md done\n');
expect(handler).toHaveBeenCalledWith('task.md', expect.any(String));
});
it('should increment totalSpawned count', () => {
detector.processTerminalData('task1.md\n');
detector.processTerminalData('task2.md\n');
detector.flushPendingEvents();
expect(detector.state.totalSpawned).toBe(2);
});
it('should handle multiple spawn tags in one chunk', () => {
const handler = vi.fn();
detector.on('spawnRequested', handler);
detector.processTerminalData(
'task1.md\ntask2.md\n'
);
expect(handler).toHaveBeenCalledTimes(2);
});
it('should strip ANSI codes before parsing', () => {
const handler = vi.fn();
detector.on('spawnRequested', handler);
detector.processTerminalData('\x1b[32mtask.md\x1b[0m\n');
expect(handler).toHaveBeenCalledWith('task.md', expect.any(String));
});
it('should trim whitespace from file path', () => {
const handler = vi.fn();
detector.on('spawnRequested', handler);
detector.processTerminalData(' task.md \n');
expect(handler).toHaveBeenCalledWith('task.md', expect.any(String));
});
});
describe('Status Request Detection', () => {
it('should detect status query and emit event', () => {
const handler = vi.fn();
detector.on('statusRequested', handler);
detector.processTerminalData('\n');
expect(handler).toHaveBeenCalledWith('auth-001');
});
it('should handle status with complex agent ID', () => {
const handler = vi.fn();
detector.on('statusRequested', handler);
detector.processTerminalData('\n');
expect(handler).toHaveBeenCalledWith('my-complex-agent-123');
});
});
describe('Cancel Request Detection', () => {
it('should detect cancel request and emit event', () => {
const handler = vi.fn();
detector.on('cancelRequested', handler);
detector.processTerminalData('\n');
expect(handler).toHaveBeenCalledWith('test-agent');
});
});
describe('Message Detection', () => {
it('should detect single-line message', () => {
const handler = vi.fn();
detector.on('messageToChild', handler);
detector.processTerminalData(
'Focus on the JWT flow\n'
);
expect(handler).toHaveBeenCalledWith('agent-001', 'Focus on the JWT flow');
});
it('should detect multi-line message via checkMultiLinePatterns', () => {
const handler = vi.fn();
detector.on('messageToChild', handler);
const multiline = 'Line 1\nLine 2\nLine 3';
detector.processTerminalData(multiline + '\n');
expect(handler).toHaveBeenCalledWith('agent-001', 'Line 1\nLine 2\nLine 3');
});
});
describe('Enable/Disable', () => {
it('should emit stateUpdate when enabled', async () => {
const handler = vi.fn();
detector.on('stateUpdate', handler);
detector.enable();
detector.flushPendingEvents();
expect(handler).toHaveBeenCalled();
expect(handler.mock.calls[0][0].enabled).toBe(true);
});
it('should emit stateUpdate when disabled', () => {
detector.enable();
detector.flushPendingEvents();
const handler = vi.fn();
detector.on('stateUpdate', handler);
detector.disable();
detector.flushPendingEvents();
expect(handler).toHaveBeenCalled();
expect(handler.mock.calls[0][0].enabled).toBe(false);
});
});
describe('Reset', () => {
it('should reset all state', () => {
detector.processTerminalData('task.md\n');
detector.flushPendingEvents();
expect(detector.enabled).toBe(true);
detector.reset();
expect(detector.enabled).toBe(false);
expect(detector.state.totalSpawned).toBe(0);
});
});
describe('State Update', () => {
it('should allow external state updates', () => {
detector.updateState({ activeCount: 3, queuedCount: 2 });
detector.flushPendingEvents();
expect(detector.state.activeCount).toBe(3);
expect(detector.state.queuedCount).toBe(2);
});
});
describe('Line Buffer', () => {
it('should handle data split across chunks', () => {
const handler = vi.fn();
detector.on('spawnRequested', handler);
// Send in two chunks (split in the middle of the tag)
detector.processTerminalData('task.md\n');
expect(handler).toHaveBeenCalledWith('task.md', expect.any(String));
});
it('should handle partial lines without emitting', () => {
const handler = vi.fn();
detector.on('spawnRequested', handler);
// No newline - data stays in buffer
detector.processTerminalData('task.md');
// Should not emit until newline
expect(handler).not.toHaveBeenCalled();
});
it('should emit on subsequent newline', () => {
const handler = vi.fn();
detector.on('spawnRequested', handler);
detector.processTerminalData('task.md');
detector.processTerminalData('\n');
expect(handler).toHaveBeenCalledWith('task.md', expect.any(String));
});
});
});