agent-ecosystem/test/main/services/team/TeamLaunchFailureArtifactPack.test.ts
2026-05-09 07:33:33 +03:00

197 lines
7.1 KiB
TypeScript

import * as fs from 'fs/promises';
import * as os from 'os';
import * as path from 'path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import {
classifyLaunchFailureArtifact,
extractLaunchBootstrapTransportBreadcrumb,
redactLaunchFailureArtifactText,
writeTeamLaunchFailureArtifactPack,
} from '../../../../src/main/services/team/TeamLaunchFailureArtifactPack';
import {
getTeamsBasePath,
setClaudeBasePathOverride,
} from '../../../../src/main/utils/pathDecoder';
describe('TeamLaunchFailureArtifactPack', () => {
let tempRoot: string;
beforeEach(async () => {
tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'team-launch-artifact-pack-'));
setClaudeBasePathOverride(path.join(tempRoot, '.claude'));
});
afterEach(async () => {
setClaudeBasePathOverride(null);
await fs.rm(tempRoot, { recursive: true, force: true });
});
it('writes a bounded redacted launch failure artifact pack with known launch files', async () => {
const teamName = 'artifact-team';
const runId = 'run-secret-1';
const teamDir = path.join(getTeamsBasePath(), teamName);
await fs.mkdir(path.join(teamDir, '.bootstrap.lock'), { recursive: true });
await fs.writeFile(
path.join(teamDir, 'launch-state.json'),
JSON.stringify({
teamName,
runId,
secret: 'sk-ant-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
token: 'abcdefghijklmnopqrstuvwxyz123456',
}),
'utf8'
);
await fs.writeFile(path.join(teamDir, 'launch-summary.json'), '{"summary":true}\n', 'utf8');
await fs.writeFile(path.join(teamDir, 'bootstrap-state.json'), '{"bootstrap":true}\n', 'utf8');
await fs.writeFile(
path.join(teamDir, 'bootstrap-journal.jsonl'),
'{"event":"started"}\n',
'utf8'
);
await fs.writeFile(
path.join(teamDir, '.bootstrap.lock', 'metadata.json'),
'{"pid":123,"runId":"run-secret-1"}\n',
'utf8'
);
const result = await writeTeamLaunchFailureArtifactPack({
teamName,
runId,
reason: 'launch_progress_failed',
startedAt: '2026-05-09T00:00:00.000Z',
cwd: '/repo',
pid: 123,
providerId: 'anthropic',
model: 'claude-opus',
expectedMembers: ['alice'],
effectiveMembers: [{ name: 'alice', role: 'developer', provider: 'anthropic' } as never],
progress: {
runId,
teamName,
state: 'failed',
message: 'Launch failed',
startedAt: '2026-05-09T00:00:00.000Z',
updatedAt: '2026-05-09T00:01:00.000Z',
error:
'Authentication failed: ANTHROPIC_API_KEY=sk-ant-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
},
memberSpawnStatuses: {
alice: {
status: 'error',
launchState: 'failed_to_start',
hardFailureReason: 'bootstrap timeout',
updatedAt: '2026-05-09T00:01:00.000Z',
},
},
cliLogs: 'stderr OPENAI_API_KEY=sk-proj-cccccccccccccccccccccccccccccccccccccccc',
progressTraceLines: ['[failed] launch failed'],
runtimeAdapterTraceLines: ['runtime trace'],
});
const manifest = JSON.parse(await fs.readFile(result.manifestPath, 'utf8')) as {
reason: string;
artifactFiles: string[];
classification: { code: string };
bootstrapTransportBreadcrumb: { lastTransportStage: string | null };
progress: { error: string };
};
expect(manifest.reason).toBe('launch_progress_failed');
expect(manifest.classification.code).toBe('provider_auth');
expect(manifest.artifactFiles).toContain('cli-logs-tail.txt');
expect(manifest.artifactFiles).toContain('launch-state.json');
expect(manifest.progress.error).toContain('[REDACTED]');
const copiedLaunchState = await fs.readFile(path.join(result.directory, 'launch-state.json'), 'utf8');
expect(copiedLaunchState).toContain('[REDACTED_ANTHROPIC_API_KEY]');
expect(() => JSON.parse(copiedLaunchState)).not.toThrow();
expect(copiedLaunchState).toContain('"token":"[REDACTED]"');
expect(copiedLaunchState).not.toContain('sk-ant-');
const cliLogs = await fs.readFile(path.join(result.directory, 'cli-logs-tail.txt'), 'utf8');
expect(cliLogs).toContain('OPENAI_API_KEY=[REDACTED]');
expect(cliLogs).not.toContain('sk-proj-');
const latest = JSON.parse(
await fs.readFile(path.join(teamDir, 'launch-failure-artifacts', 'latest.json'), 'utf8')
) as { manifestPath: string };
expect(latest.manifestPath).toBe(result.manifestPath);
});
it('redacts common bearer and token-shaped secrets', () => {
const redacted = redactLaunchFailureArtifactText(
'Authorization: Bearer abcdefghijklmnopqrstuvwxyz123456 token: abcdefghijklmnopqrstuvwxyz123456'
);
expect(redacted).toContain('Authorization: Bearer [REDACTED]');
expect(redacted).toContain('token: [REDACTED]');
});
it('classifies bootstrap transport rejection and extracts breadcrumb details', () => {
const input = {
teamName: 'artifact-team',
runId: 'run-transport',
reason: 'launch_cleanup_unconfirmed_bootstrap',
progressTraceLines: [
'bob did not submit bootstrap prompt: timed out waiting for bootstrap_submitted; last transport stage: bootstrap_submit_rejected: submit rejected by local prompt handler retryable=true',
'Warning: no stdin data received in 3s, proceeding without it.',
],
};
expect(classifyLaunchFailureArtifact(input).code).toBe('transport_rejected');
expect(extractLaunchBootstrapTransportBreadcrumb(input)).toMatchObject({
lastTransportStage: 'bootstrap_submit_rejected: submit rejected by local prompt handler retryable=true',
submitRejected: true,
retryable: true,
noStdinWarning: true,
bootstrapSubmitted: false,
});
});
it('classifies provider quota separately from protocol errors', () => {
expect(
classifyLaunchFailureArtifact({
teamName: 'artifact-team',
runId: 'run-quota',
reason:
'OpenCode quota exhausted. This request requires more credits, or fewer max_tokens.',
}).code
).toBe('provider_quota');
});
it.each([
{
name: 'stdin warning',
text: 'Warning: no stdin data received in 3s, proceeding without it.',
code: 'stdin_missing',
},
{
name: 'provider auth',
text: 'Codex API error. Token refresh failed: 401 Unauthorized',
code: 'provider_auth',
},
{
name: 'model bootstrap timeout',
text: 'bob: Teammate was registered but did not bootstrap-confirm before timeout.',
code: 'model_no_bootstrap',
},
{
name: 'process stale pid',
text: 'persisted runtime pid is not alive; persisted runtime pid was not found in process table',
code: 'process_exited',
},
{
name: 'opencode protocol',
text: 'OpenCode API error. non_visible_tool_without_task_progress',
code: 'opencode_protocol',
},
])('classifies production-like failure string: $name', ({ text, code }) => {
expect(
classifyLaunchFailureArtifact({
teamName: 'artifact-team',
runId: `run-${code}`,
reason: text,
}).code
).toBe(code);
});
});