mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
Consolidate duplicated MockSession/MockStateStore into test/mocks/, migrate respawn tests to shared mocks, and add 58 route tests for session, system, and respawn endpoints using Fastify app.inject(). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
193 lines
5.9 KiB
TypeScript
193 lines
5.9 KiB
TypeScript
/**
|
|
* @fileoverview Tests for RespawnController team-awareness integration
|
|
*
|
|
* Verifies that the respawn controller correctly blocks respawn cycles
|
|
* when TeamWatcher reports active teammates working on tasks.
|
|
*
|
|
* Port: N/A (no server — unit tests only)
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import { RespawnController } from '../src/respawn-controller.js';
|
|
import { Session } from '../src/session.js';
|
|
import { TeamWatcher } from '../src/team-watcher.js';
|
|
import { MockSession } from './mocks/index.js';
|
|
|
|
// ========== Mock TeamWatcher ==========
|
|
|
|
class MockTeamWatcher extends TeamWatcher {
|
|
private _hasActive = false;
|
|
private _activeCount = 0;
|
|
|
|
constructor() {
|
|
// Pass dummy dirs that don't exist — we override the methods anyway
|
|
super('/tmp/nonexistent-teams', '/tmp/nonexistent-tasks');
|
|
}
|
|
|
|
setActiveTeammates(sessionId: string, hasActive: boolean, count: number = 1): void {
|
|
this._hasActive = hasActive;
|
|
this._activeCount = count;
|
|
}
|
|
|
|
override hasActiveTeammates(_sessionId: string): boolean {
|
|
return this._hasActive;
|
|
}
|
|
|
|
override getActiveTeammateCount(_sessionId: string): number {
|
|
return this._activeCount;
|
|
}
|
|
|
|
// Prevent actual filesystem polling
|
|
override start(): void { /* noop */ }
|
|
override stop(): void { /* noop */ }
|
|
}
|
|
|
|
// ========== Tests ==========
|
|
|
|
describe('RespawnController Team Awareness', () => {
|
|
let session: MockSession;
|
|
let controller: RespawnController;
|
|
let teamWatcher: MockTeamWatcher;
|
|
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
session = new MockSession();
|
|
teamWatcher = new MockTeamWatcher();
|
|
controller = new RespawnController(session as unknown as Session, {
|
|
idleTimeoutMs: 100,
|
|
interStepDelayMs: 50,
|
|
completionConfirmMs: 200,
|
|
noOutputTimeoutMs: 500,
|
|
aiIdleCheckEnabled: false,
|
|
sendClear: false,
|
|
sendInit: false,
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
controller.stop();
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
describe('without TeamWatcher (backwards compatibility)', () => {
|
|
it('should allow respawn when no TeamWatcher is set', async () => {
|
|
// Don't call setTeamWatcher — should work as before
|
|
const cycleStarted = vi.fn();
|
|
controller.on('respawnCycleStarted', cycleStarted);
|
|
|
|
controller.start();
|
|
expect(controller.state).toBe('watching');
|
|
|
|
// Trigger idle detection
|
|
session.simulateCompletionMessage();
|
|
await vi.advanceTimersByTimeAsync(300); // Past completionConfirmMs
|
|
|
|
expect(cycleStarted).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('with TeamWatcher and no active teammates', () => {
|
|
it('should allow respawn when no teammates are active', async () => {
|
|
controller.setTeamWatcher(teamWatcher as unknown as TeamWatcher);
|
|
teamWatcher.setActiveTeammates(session.id, false, 0);
|
|
|
|
const cycleStarted = vi.fn();
|
|
controller.on('respawnCycleStarted', cycleStarted);
|
|
|
|
controller.start();
|
|
|
|
// Trigger idle detection
|
|
session.simulateCompletionMessage();
|
|
await vi.advanceTimersByTimeAsync(300);
|
|
|
|
expect(cycleStarted).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('with TeamWatcher and active teammates', () => {
|
|
it('should block respawn when teammates are active', async () => {
|
|
controller.setTeamWatcher(teamWatcher as unknown as TeamWatcher);
|
|
teamWatcher.setActiveTeammates(session.id, true, 2);
|
|
|
|
const cycleStarted = vi.fn();
|
|
const respawnBlocked = vi.fn();
|
|
controller.on('respawnCycleStarted', cycleStarted);
|
|
controller.on('respawnBlocked', respawnBlocked);
|
|
|
|
controller.start();
|
|
|
|
// Trigger idle detection
|
|
session.simulateCompletionMessage();
|
|
await vi.advanceTimersByTimeAsync(300);
|
|
|
|
expect(cycleStarted).not.toHaveBeenCalled();
|
|
expect(respawnBlocked).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
reason: 'active_teammates',
|
|
details: expect.stringContaining('2'),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should return to watching state after blocking', async () => {
|
|
controller.setTeamWatcher(teamWatcher as unknown as TeamWatcher);
|
|
teamWatcher.setActiveTeammates(session.id, true, 1);
|
|
|
|
controller.start();
|
|
|
|
// Trigger idle detection
|
|
session.simulateCompletionMessage();
|
|
await vi.advanceTimersByTimeAsync(300);
|
|
|
|
expect(controller.state).toBe('watching');
|
|
});
|
|
|
|
it('should allow respawn after teammates finish', async () => {
|
|
controller.setTeamWatcher(teamWatcher as unknown as TeamWatcher);
|
|
|
|
const cycleStarted = vi.fn();
|
|
const respawnBlocked = vi.fn();
|
|
controller.on('respawnCycleStarted', cycleStarted);
|
|
controller.on('respawnBlocked', respawnBlocked);
|
|
|
|
controller.start();
|
|
|
|
// First attempt: teammates active → blocked
|
|
teamWatcher.setActiveTeammates(session.id, true, 1);
|
|
session.simulateCompletionMessage();
|
|
await vi.advanceTimersByTimeAsync(300);
|
|
|
|
expect(cycleStarted).not.toHaveBeenCalled();
|
|
expect(respawnBlocked).toHaveBeenCalledTimes(1);
|
|
|
|
// Teammates finish
|
|
teamWatcher.setActiveTeammates(session.id, false, 0);
|
|
|
|
// Second attempt: no teammates → allowed
|
|
session.simulateCompletionMessage();
|
|
await vi.advanceTimersByTimeAsync(300);
|
|
|
|
expect(cycleStarted).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe('respawnBlocked event details', () => {
|
|
it('should include teammate count in details', async () => {
|
|
controller.setTeamWatcher(teamWatcher as unknown as TeamWatcher);
|
|
teamWatcher.setActiveTeammates(session.id, true, 3);
|
|
|
|
const respawnBlocked = vi.fn();
|
|
controller.on('respawnBlocked', respawnBlocked);
|
|
|
|
controller.start();
|
|
session.simulateCompletionMessage();
|
|
await vi.advanceTimersByTimeAsync(300);
|
|
|
|
expect(respawnBlocked).toHaveBeenCalledWith({
|
|
reason: 'active_teammates',
|
|
details: '3 teammate(s) still working',
|
|
});
|
|
});
|
|
});
|
|
});
|