mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 05:59:43 +02:00
176 lines
5.3 KiB
TypeScript
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();
|
|
});
|
|
});
|