agent-ecosystem/test/main/services/team/TeamConfigReader.test.ts

257 lines
7.8 KiB
TypeScript

import * as fs from 'fs/promises';
import * as os from 'os';
import * as path from 'path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
const hoisted = vi.hoisted(() => ({
teamsBase: '',
}));
vi.mock('../../../../src/main/utils/pathDecoder', () => ({
getTeamsBasePath: () => hoisted.teamsBase,
}));
vi.mock('../../../../src/main/services/team/TeamFsWorkerClient', () => ({
getTeamFsWorkerClient: () => ({
isAvailable: () => false,
}),
}));
import { TeamConfigReader } from '../../../../src/main/services/team/TeamConfigReader';
import { createPersistedLaunchSummaryProjection } from '../../../../src/main/services/team/TeamLaunchSummaryProjection';
describe('TeamConfigReader', () => {
let tempDir = '';
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'team-config-reader-'));
hoisted.teamsBase = tempDir;
});
afterEach(async () => {
if (tempDir) {
await fs.rm(tempDir, { recursive: true, force: true });
}
hoisted.teamsBase = '';
});
it('uses compact launch summary projection when launch-state.json is oversized', async () => {
const teamName = 'mixed-team';
const teamDir = path.join(tempDir, teamName);
await fs.mkdir(teamDir, { recursive: true });
await fs.writeFile(
path.join(teamDir, 'config.json'),
JSON.stringify({
name: 'Mixed Team',
members: [{ name: 'team-lead', agentType: 'team-lead' }],
}),
'utf8'
);
await fs.writeFile(path.join(teamDir, 'launch-state.json'), 'x'.repeat(40 * 1024), 'utf8');
await fs.writeFile(
path.join(teamDir, 'launch-summary.json'),
JSON.stringify(
createPersistedLaunchSummaryProjection({
version: 2,
teamName,
updatedAt: '2026-04-22T12:00:00.000Z',
launchPhase: 'finished',
expectedMembers: ['alice', 'bob'],
bootstrapExpectedMembers: ['alice'],
members: {
alice: {
name: 'alice',
providerId: 'codex',
laneId: 'primary',
laneKind: 'primary',
laneOwnerProviderId: 'codex',
launchState: 'confirmed_alive',
agentToolAccepted: true,
runtimeAlive: true,
bootstrapConfirmed: true,
hardFailure: false,
lastEvaluatedAt: '2026-04-22T12:00:00.000Z',
},
bob: {
name: 'bob',
providerId: 'opencode',
laneId: 'secondary:opencode:bob',
laneKind: 'secondary',
laneOwnerProviderId: 'opencode',
launchState: 'failed_to_start',
agentToolAccepted: true,
runtimeAlive: false,
bootstrapConfirmed: false,
hardFailure: true,
hardFailureReason: 'Side lane failed',
lastEvaluatedAt: '2026-04-22T12:00:00.000Z',
},
},
summary: {
confirmedCount: 1,
pendingCount: 0,
failedCount: 1,
runtimeAlivePendingCount: 0,
},
teamLaunchState: 'partial_failure',
} as never),
null,
2
),
'utf8'
);
await fs.writeFile(
path.join(teamDir, 'bootstrap-state.json'),
JSON.stringify({
version: 1,
teamName,
runId: 'bootstrap-run-1',
ownerPid: process.pid,
startedAt: Date.parse('2026-04-22T12:01:00.000Z'),
updatedAt: Date.parse('2026-04-22T12:01:00.000Z'),
phase: 'spawning_members',
members: [{ name: 'alice', status: 'pending' }],
}),
'utf8'
);
const reader = new TeamConfigReader();
const teams = await reader.listTeams();
expect(teams).toHaveLength(1);
expect(teams[0]).toMatchObject({
teamName,
displayName: 'Mixed Team',
partialLaunchFailure: true,
expectedMemberCount: 2,
confirmedMemberCount: 1,
missingMembers: ['bob'],
teamLaunchState: 'partial_failure',
confirmedCount: 1,
pendingCount: 0,
failedCount: 1,
});
});
it('does not invent a partial-failure summary from artifact counts for mixed-aware teams when canonical launch truth is unavailable', async () => {
const teamName = 'mixed-aware-team';
const teamDir = path.join(tempDir, teamName);
await fs.mkdir(path.join(teamDir, 'inboxes'), { recursive: true });
await fs.writeFile(
path.join(teamDir, 'config.json'),
JSON.stringify({
name: 'Mixed Aware Team',
leadSessionId: 'lead-session-1',
members: [{ name: 'team-lead', agentType: 'team-lead' }],
}),
'utf8'
);
await fs.writeFile(
path.join(teamDir, 'team.meta.json'),
JSON.stringify({
version: 1,
cwd: tempDir,
providerId: 'codex',
createdAt: Date.now(),
}),
'utf8'
);
await fs.writeFile(
path.join(teamDir, 'members.meta.json'),
JSON.stringify({
version: 1,
members: [
{ name: 'alice', providerId: 'codex', role: 'reviewer' },
{ name: 'tom', providerId: 'opencode', role: 'developer' },
],
}),
'utf8'
);
await fs.writeFile(path.join(teamDir, 'inboxes', 'alice.json'), '{}', 'utf8');
const reader = new TeamConfigReader();
const teams = await reader.listTeams();
expect(teams).toHaveLength(1);
expect(teams[0]).toMatchObject({
teamName,
displayName: 'Mixed Aware Team',
memberCount: 2,
});
expect(teams[0]?.partialLaunchFailure).toBeUndefined();
expect(teams[0]?.teamLaunchState).toBeUndefined();
expect(teams[0]?.missingMembers).toBeUndefined();
});
it('does not let a removed base member hide an active auto-suffixed teammate in team summaries', async () => {
const teamName = 'suffix-team';
const teamDir = path.join(tempDir, teamName);
await fs.mkdir(teamDir, { recursive: true });
await fs.writeFile(
path.join(teamDir, 'config.json'),
JSON.stringify({
name: 'Suffix Team',
members: [{ name: 'team-lead', agentType: 'team-lead' }],
}),
'utf8'
);
await fs.writeFile(
path.join(teamDir, 'members.meta.json'),
JSON.stringify({
version: 1,
members: [
{ name: 'alice', role: 'developer', removedAt: Date.now() - 60_000 },
{ name: 'alice-2', role: 'reviewer' },
],
}),
'utf8'
);
const reader = new TeamConfigReader();
const teams = await reader.listTeams();
expect(teams).toHaveLength(1);
expect(teams[0]).toMatchObject({
teamName,
displayName: 'Suffix Team',
memberCount: 1,
members: [{ name: 'alice-2', role: 'reviewer' }],
});
});
it('counts only active non-lead teammates for draft team summaries', async () => {
const teamName = 'draft-summary-team';
const teamDir = path.join(tempDir, teamName);
await fs.mkdir(teamDir, { recursive: true });
await fs.writeFile(
path.join(teamDir, 'team.meta.json'),
JSON.stringify({
version: 1,
cwd: tempDir,
displayName: 'Draft Summary Team',
createdAt: Date.parse('2026-04-22T12:00:00.000Z'),
}),
'utf8'
);
await fs.writeFile(
path.join(teamDir, 'members.meta.json'),
JSON.stringify({
version: 1,
members: [
{ name: 'team-lead', agentType: 'team-lead' },
{ name: 'alice', removedAt: Date.now() - 60_000 },
{ name: 'bob', role: 'developer' },
],
}),
'utf8'
);
const reader = new TeamConfigReader();
const teams = await reader.listTeams();
expect(teams).toHaveLength(1);
expect(teams[0]).toMatchObject({
teamName,
displayName: 'Draft Summary Team',
memberCount: 1,
pendingCreate: true,
});
});
});