agent-ecosystem/test/main/services/team/TeamProvisioningServiceIdempotency.test.ts
iliya 3723eba5b4 refactor: update IPC handlers and types for lead activity and context usage
- Modified IPC handlers to return structured snapshots for lead activity, lead context usage, and member spawn statuses, enhancing data consistency.
- Introduced new types for LeadActivitySnapshot, LeadContextUsageSnapshot, and MemberSpawnStatusesSnapshot to improve type safety and clarity.
- Refactored TeamProvisioningService to manage provisioning runs more effectively, including updates to state management for active runs.
- Enhanced UI components to utilize the new data structures, improving the overall user experience in team management features.
2026-03-12 14:14:58 +02:00

142 lines
4.6 KiB
TypeScript

import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { EventEmitter } from 'events';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
let tempClaudeRoot = '';
let tempTeamsBase = '';
vi.mock('@main/utils/pathDecoder', async (importOriginal) => {
const actual = await importOriginal<typeof import('@main/utils/pathDecoder')>();
return {
...actual,
getAutoDetectedClaudeBasePath: () => tempClaudeRoot,
getClaudeBasePath: () => tempClaudeRoot,
getTeamsBasePath: () => tempTeamsBase,
};
});
import { TeamProvisioningService } from '@main/services/team/TeamProvisioningService';
describe('TeamProvisioningService idempotent launch guards', () => {
beforeEach(() => {
vi.clearAllMocks();
tempClaudeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-team-launch-'));
tempTeamsBase = path.join(tempClaudeRoot, 'teams');
fs.mkdirSync(tempTeamsBase, { recursive: true });
});
afterEach(() => {
fs.rmSync(tempClaudeRoot, { recursive: true, force: true });
});
it('reuses the alive run instead of spawning a duplicate launch', async () => {
const teamName = 'team-alpha';
const teamDir = path.join(tempTeamsBase, teamName);
fs.mkdirSync(teamDir, { recursive: true });
fs.writeFileSync(
path.join(teamDir, 'config.json'),
JSON.stringify({
name: teamName,
projectPath: process.cwd(),
members: [{ name: 'team-lead', agentType: 'team-lead' }, { name: 'dev' }],
})
);
const svc = new TeamProvisioningService();
const aliveRun = {
runId: 'alive-run-1',
teamName,
request: { cwd: process.cwd() },
child: Object.assign(new EventEmitter(), {
stdin: { writable: true },
stdout: new EventEmitter(),
stderr: new EventEmitter(),
}),
processKilled: false,
cancelRequested: false,
};
(svc as any).runs.set(aliveRun.runId, aliveRun);
(svc as any).aliveRunByTeam.set(teamName, aliveRun.runId);
const response = await svc.launchTeam({ teamName, cwd: process.cwd() }, () => {});
expect(response.runId).toBe(aliveRun.runId);
});
it('does not reuse an alive run when cwd differs', async () => {
const teamName = 'team-alpha';
const currentCwd = fs.mkdtempSync(path.join(tempClaudeRoot, 'current-'));
const nextCwd = fs.mkdtempSync(path.join(tempClaudeRoot, 'next-'));
const teamDir = path.join(tempTeamsBase, teamName);
fs.mkdirSync(teamDir, { recursive: true });
fs.writeFileSync(
path.join(teamDir, 'config.json'),
JSON.stringify({
name: teamName,
projectPath: currentCwd,
members: [{ name: 'team-lead', agentType: 'team-lead' }, { name: 'dev' }],
})
);
const svc = new TeamProvisioningService();
const aliveRun = {
runId: 'alive-run-1',
teamName,
request: { cwd: currentCwd },
child: Object.assign(new EventEmitter(), {
stdin: { writable: true },
stdout: new EventEmitter(),
stderr: new EventEmitter(),
}),
processKilled: false,
cancelRequested: false,
};
(svc as any).runs.set(aliveRun.runId, aliveRun);
(svc as any).aliveRunByTeam.set(teamName, aliveRun.runId);
await expect(svc.launchTeam({ teamName, cwd: nextCwd }, () => {})).rejects.toThrow(
`Team "${teamName}" is already running in "${path.resolve(currentCwd)}".`
);
});
it('fails closed when an alive run cwd cannot be determined', async () => {
const teamName = 'team-alpha';
const nextCwd = fs.mkdtempSync(path.join(tempClaudeRoot, 'next-'));
const teamDir = path.join(tempTeamsBase, teamName);
fs.mkdirSync(teamDir, { recursive: true });
fs.writeFileSync(
path.join(teamDir, 'config.json'),
JSON.stringify({
name: teamName,
members: [{ name: 'team-lead', agentType: 'team-lead' }, { name: 'dev' }],
})
);
const svc = new TeamProvisioningService();
const aliveRun = {
runId: 'alive-run-1',
teamName,
request: { cwd: '' },
child: Object.assign(new EventEmitter(), {
stdin: { writable: true },
stdout: new EventEmitter(),
stderr: new EventEmitter(),
}),
processKilled: false,
cancelRequested: false,
spawnContext: { cwd: '' },
};
(svc as any).runs.set(aliveRun.runId, aliveRun);
(svc as any).aliveRunByTeam.set(teamName, aliveRun.runId);
await expect(svc.launchTeam({ teamName, cwd: nextCwd }, () => {})).rejects.toThrow(
`Team "${teamName}" is already running, but its cwd could not be determined.`
);
});
});