agent-ecosystem/test/main/services/team/TeamProvisioningServicePrepare.test.ts
iliya 6d441efa97 refactor: enhance team provisioning process and UI updates
- Updated provisioning states to include 'configuring', 'assembling', and 'finalizing' for better tracking of team setup progress.
- Refactored the provisioning progress block to utilize a new display step system, improving clarity in the UI.
- Adjusted the README to include a comprehensive table of contents and updated comparison metrics for multi-agent orchestration tools.
- Enhanced team management UI to reflect new provisioning states and improve user experience during team setup.
2026-03-21 16:47:20 +02:00

224 lines
7.5 KiB
TypeScript

import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
vi.mock('@main/services/team/ClaudeBinaryResolver', () => ({
ClaudeBinaryResolver: { resolve: vi.fn() },
}));
vi.mock('@main/utils/shellEnv', () => ({
resolveInteractiveShellEnv: vi.fn(),
}));
import { TeamProvisioningService } from '@main/services/team/TeamProvisioningService';
import { ClaudeBinaryResolver } from '@main/services/team/ClaudeBinaryResolver';
import { resolveInteractiveShellEnv } from '@main/utils/shellEnv';
describe('TeamProvisioningService prepare/auth behavior', () => {
let tempRoot = '';
beforeEach(() => {
vi.clearAllMocks();
tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-team-prepare-'));
vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/fake/claude');
vi.mocked(resolveInteractiveShellEnv).mockResolvedValue({
PATH: '/usr/bin',
SHELL: '/bin/zsh',
});
delete process.env.ANTHROPIC_API_KEY;
delete process.env.ANTHROPIC_AUTH_TOKEN;
});
afterEach(() => {
fs.rmSync(tempRoot, { recursive: true, force: true });
});
it('does not create missing directories during prepareForProvisioning', async () => {
const svc = new TeamProvisioningService();
vi.spyOn(svc as any, 'buildProvisioningEnv').mockResolvedValue({
env: {},
authSource: 'none',
});
vi.spyOn(svc as any, 'probeClaudeRuntime').mockResolvedValue({});
const missingCwd = path.join(tempRoot, 'missing-project');
await svc.prepareForProvisioning(missingCwd, { forceFresh: true });
expect(fs.existsSync(missingCwd)).toBe(false);
});
it('keys the prepare probe cache by cwd', async () => {
const svc = new TeamProvisioningService();
vi.spyOn(svc as any, 'buildProvisioningEnv').mockResolvedValue({
env: {},
authSource: 'none',
});
const probeSpy = vi.spyOn(svc as any, 'probeClaudeRuntime').mockResolvedValue({});
const cwdA = fs.mkdtempSync(path.join(tempRoot, 'a-'));
const cwdB = fs.mkdtempSync(path.join(tempRoot, 'b-'));
await svc.prepareForProvisioning(cwdA, { forceFresh: true });
await svc.prepareForProvisioning(cwdA);
await svc.prepareForProvisioning(cwdB);
expect(probeSpy).toHaveBeenCalledTimes(2);
expect(probeSpy.mock.calls[0]?.[1]).toBe(cwdA);
expect(probeSpy.mock.calls[1]?.[1]).toBe(cwdB);
});
it('maps ANTHROPIC_AUTH_TOKEN into ANTHROPIC_API_KEY for headless preflight', async () => {
const svc = new TeamProvisioningService();
vi.mocked(resolveInteractiveShellEnv).mockResolvedValue({
ANTHROPIC_AUTH_TOKEN: 'proxy-token',
PATH: '/usr/bin',
SHELL: '/bin/zsh',
});
const result = await (svc as any).buildProvisioningEnv();
expect(result.authSource).toBe('anthropic_auth_token');
expect(result.env.ANTHROPIC_API_KEY).toBe('proxy-token');
});
it('prefers explicit ANTHROPIC_API_KEY over ANTHROPIC_AUTH_TOKEN', async () => {
const svc = new TeamProvisioningService();
vi.mocked(resolveInteractiveShellEnv).mockResolvedValue({
ANTHROPIC_API_KEY: 'real-key',
ANTHROPIC_AUTH_TOKEN: 'proxy-token',
PATH: '/usr/bin',
SHELL: '/bin/zsh',
});
const result = await (svc as any).buildProvisioningEnv();
expect(result.authSource).toBe('anthropic_api_key');
expect(result.env.ANTHROPIC_API_KEY).toBe('real-key');
});
it('does not treat assistant-text 401 noise as an auth failure', () => {
const svc = new TeamProvisioningService();
expect((svc as any).isAuthFailureWarning('assistant mentioned 401 unauthorized', 'assistant')).toBe(
false
);
expect((svc as any).isAuthFailureWarning('invalid api key', 'stderr')).toBe(true);
});
it('does not re-check auth from stdout json noise during pre-complete finalization', async () => {
const svc = new TeamProvisioningService();
const handleAuthFailureInOutput = vi.spyOn(svc as any, 'handleAuthFailureInOutput');
vi.spyOn(svc as any, 'updateConfigPostLaunch').mockResolvedValue(undefined);
vi.spyOn(svc as any, 'cleanupPrelaunchBackup').mockResolvedValue(undefined);
vi.spyOn(svc as any, 'relayLeadInboxMessages').mockResolvedValue(undefined);
const run = {
runId: 'run-1',
teamName: 'team-alpha',
request: {
cwd: tempRoot,
color: 'blue',
members: [{ name: 'dev', role: 'engineer' }],
},
progress: {
runId: 'run-1',
teamName: 'team-alpha',
state: 'assembling',
message: 'Assembling',
startedAt: '2026-03-12T10:00:00.000Z',
updatedAt: '2026-03-12T10:00:00.000Z',
},
provisioningComplete: false,
cancelRequested: false,
processKilled: false,
stdoutBuffer:
'{"type":"assistant","message":{"content":[{"type":"text","text":"invalid api key"}]}}\n',
stdoutLogLineBuf: '',
stdoutParserCarry:
'{"type":"assistant","message":{"content":[{"type":"text","text":"invalid api key"}]}}',
stdoutParserCarryIsCompleteJson: true,
stdoutParserCarryLooksLikeClaudeJson: true,
stderrBuffer: '',
stderrLogLineBuf: '',
provisioningOutputParts: ['invalid api key'],
onProgress: vi.fn(),
isLaunch: true,
detectedSessionId: null,
timeoutHandle: null,
fsMonitorHandle: null,
claudeLogLines: [],
leadActivityState: 'active',
leadContextUsage: null,
};
(svc as any).provisioningRunByTeam.set(run.teamName, run.runId);
await (svc as any).handleProvisioningTurnComplete(run);
expect(handleAuthFailureInOutput).not.toHaveBeenCalledWith(run, expect.any(String), 'pre-complete');
expect(run.onProgress).toHaveBeenCalledWith(
expect.objectContaining({
runId: 'run-1',
state: 'ready',
})
);
});
it('re-checks a trailing plaintext stdout auth failure during pre-complete finalization', async () => {
const svc = new TeamProvisioningService();
const handleAuthFailureInOutput = vi
.spyOn(svc as any, 'handleAuthFailureInOutput')
.mockImplementation(() => {});
const run = {
runId: 'run-2',
teamName: 'team-alpha',
request: {
cwd: tempRoot,
color: 'blue',
members: [{ name: 'dev', role: 'engineer' }],
},
progress: {
runId: 'run-2',
teamName: 'team-alpha',
state: 'assembling',
message: 'Assembling',
startedAt: '2026-03-12T10:00:00.000Z',
updatedAt: '2026-03-12T10:00:00.000Z',
},
provisioningComplete: false,
cancelRequested: false,
processKilled: false,
stdoutBuffer: '[ERROR] invalid api key',
stdoutLogLineBuf: '',
stdoutParserCarry: '[ERROR] invalid api key',
stdoutParserCarryIsCompleteJson: false,
stdoutParserCarryLooksLikeClaudeJson: false,
stderrBuffer: '',
stderrLogLineBuf: '',
provisioningOutputParts: [],
onProgress: vi.fn(),
isLaunch: true,
detectedSessionId: null,
timeoutHandle: null,
fsMonitorHandle: null,
claudeLogLines: [],
leadActivityState: 'active',
leadContextUsage: null,
};
(svc as any).provisioningRunByTeam.set(run.teamName, run.runId);
await (svc as any).handleProvisioningTurnComplete(run);
expect(handleAuthFailureInOutput).toHaveBeenCalledWith(run, '[ERROR] invalid api key', 'pre-complete');
expect(run.onProgress).not.toHaveBeenCalledWith(
expect.objectContaining({
runId: 'run-2',
state: 'ready',
})
);
});
});