Files
Codeman/test/team-idle-detection.test.ts
T
2026-02-12 08:34:51 +01:00

176 lines
5.3 KiB
TypeScript

/**
* @fileoverview Tests for team-aware idle detection in RespawnController
*
* Verifies that the respawn controller checks TeamWatcher for active teammates
* before triggering respawn. Also tests AI idle checker teammate context injection.
*
* Test port: 3151 (if server needed)
* SAFETY: Never uses port 3000, never kills w1-claudeman/w2/w3-claudeman
*/
import { EventEmitter } from 'node:events';
import { vi } from 'vitest';
import type { Session } from '../src/session.js';
import { RespawnController } from '../src/respawn-controller.js';
import { AiIdleChecker } from '../src/ai-idle-checker.js';
import { TeamWatcher } from '../src/team-watcher.js';
import { MockSession, createTimeController, type TimeController } from './respawn-test-utils.js';
// Minimal config for fast testing
const FAST_CONFIG = {
enabled: true,
idleTimeoutMs: 100,
completionConfirmMs: 100,
noOutputTimeoutMs: 500,
updatePrompt: 'test update',
interStepDelayMs: 50,
sendClear: false,
sendInit: false,
aiIdleCheckEnabled: false,
};
/**
* Mock TeamWatcher that returns configurable active teammate state
*/
class MockTeamWatcher extends EventEmitter {
private _hasActive: boolean = false;
private _teammateCount: number = 0;
setHasActiveTeammates(value: boolean): void {
this._hasActive = value;
}
setTeammateCount(count: number): void {
this._teammateCount = count;
this._hasActive = count > 0;
}
hasActiveTeammates(_sessionId: string): boolean {
return this._hasActive;
}
getActiveTeammateCount(_sessionId: string): number {
return this._teammateCount;
}
// Stubs for TeamWatcher interface
start(): void {}
stop(): void {}
getTeams(): [] { return []; }
getTeamForSession(): undefined { return undefined; }
getTeamTasks(): [] { return []; }
getActiveTaskCount(): number { return 0; }
getInboxMessages(): [] { return []; }
}
describe('Team-aware idle detection', () => {
let session: MockSession;
let controller: RespawnController;
let mockTeamWatcher: MockTeamWatcher;
let time: TimeController;
beforeEach(() => {
time = createTimeController();
session = new MockSession('team-test-session');
mockTeamWatcher = new MockTeamWatcher();
controller = new RespawnController(session as unknown as Session, FAST_CONFIG);
controller.setTeamWatcher(mockTeamWatcher as unknown as TeamWatcher);
});
afterEach(() => {
controller.stop();
time.useRealTimers();
});
it('should block idle confirmation when teammates are active', async () => {
mockTeamWatcher.setHasActiveTeammates(true);
const blocked = new Promise<{ reason: string }>(resolve => {
controller.on('respawnBlocked', resolve);
});
controller.start();
// Simulate completion
session.simulateCompletionMessage('30s');
await time.advanceBy(200); // Wait for confirmation
const event = await blocked;
expect(event.reason).toBe('active_teammates');
});
it('should not emit active_teammates block when no teammates are active', async () => {
mockTeamWatcher.setHasActiveTeammates(false);
const blockReasons: string[] = [];
controller.on('respawnBlocked', (data: { reason: string }) => {
blockReasons.push(data.reason);
});
controller.start();
// Simulate completion and wait through confirmation
session.simulateCompletionMessage('30s');
await time.advanceBy(1000);
// Even if idle detection doesn't fully complete in fake timers,
// we should NOT see an active_teammates block
expect(blockReasons).not.toContain('active_teammates');
});
it('should not emit active_teammates block when no team watcher is set', async () => {
// Create controller without team watcher
const plainController = new RespawnController(session as unknown as Session, FAST_CONFIG);
const blockReasons: string[] = [];
plainController.on('respawnBlocked', (data: { reason: string }) => {
blockReasons.push(data.reason);
});
plainController.start();
session.simulateCompletionMessage('30s');
await time.advanceBy(1000);
// No team watcher means no teammate blocking
expect(blockReasons).not.toContain('active_teammates');
plainController.stop();
});
it('should transition back to watching when blocked by teammates', async () => {
mockTeamWatcher.setHasActiveTeammates(true);
const stateChanges: string[] = [];
controller.on('stateChanged', (state: string) => {
stateChanges.push(state);
});
controller.start();
session.simulateCompletionMessage('30s');
await time.advanceBy(200);
// Should go back to watching after being blocked
const lastState = stateChanges[stateChanges.length - 1];
expect(lastState).toBe('watching');
});
});
describe('AI idle checker teammate context', () => {
it('should inject teammate context into prompt when count > 0', () => {
const checker = new AiIdleChecker('test-session', { enabled: true });
checker.setTeammateCount(3);
// Access buildPrompt via prototype (it's protected, so we test indirectly)
// Instead, we verify the public API works
expect(checker).toBeDefined();
});
it('should accept setTeammateCount(0) without error', () => {
const checker = new AiIdleChecker('test-session', { enabled: true });
checker.setTeammateCount(0);
expect(checker).toBeDefined();
});
});