fix(team): materialize recovered opencode lane manifests

This commit is contained in:
777genius 2026-04-30 20:17:32 +03:00
parent 7dc4a1976d
commit 752ae9ea4b
2 changed files with 106 additions and 99 deletions

View file

@ -18477,6 +18477,16 @@ export class TeamProvisioningService {
`[${params.teamName}] Failed to recover missing OpenCode lane index ${params.laneId}: ${getErrorMessage(error)}` `[${params.teamName}] Failed to recover missing OpenCode lane index ${params.laneId}: ${getErrorMessage(error)}`
); );
}); });
await setOpenCodeRuntimeActiveRunManifest({
teamsBasePath: getTeamsBasePath(),
teamName: params.teamName,
laneId: params.laneId,
runId: params.persistedMember.runtimeRunId ?? null,
}).catch((error: unknown) => {
logger.warn(
`[${params.teamName}] Failed to materialize recovered OpenCode lane manifest ${params.laneId}: ${getErrorMessage(error)}`
);
});
return { return {
...runtimeEvidence, ...runtimeEvidence,

View file

@ -780,7 +780,7 @@ describe('Team agent launch matrix safe e2e', () => {
const second = await secondPromise; const second = await secondPromise;
expect(second.runId).toBeTruthy(); expect(second.runId).toBeTruthy();
expect(second.runId).not.toBe(firstRunId); expect(second.runId).not.toBe(firstRunId);
await waitForCondition(() => adapter.launchInputs.length === 1); await waitForCondition(() => adapter.launchInputs.length === 2);
expect(svc.isTeamAlive(teamName)).toBe(true); expect(svc.isTeamAlive(teamName)).toBe(true);
expect(svc.getAliveTeams()).toEqual([teamName]); expect(svc.getAliveTeams()).toEqual([teamName]);
@ -1372,7 +1372,7 @@ describe('Team agent launch matrix safe e2e', () => {
() => undefined () => undefined
); );
await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); await waitForCondition(() => adapter.pendingLaunchInputs.length === 2);
const cancelledRunId = adapter.pendingLaunchInputs.find( const cancelledRunId = adapter.pendingLaunchInputs.find(
(input) => input.teamName === cancelledTeamName (input) => input.teamName === cancelledTeamName
)?.runId; )?.runId;
@ -1461,7 +1461,7 @@ describe('Team agent launch matrix safe e2e', () => {
() => undefined () => undefined
); );
await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); await waitForCondition(() => adapter.pendingLaunchInputs.length === 2);
const cancelledRunId = adapter.pendingLaunchInputs.find( const cancelledRunId = adapter.pendingLaunchInputs.find(
(input) => input.teamName === cancelledTeamName (input) => input.teamName === cancelledTeamName
)?.runId; )?.runId;
@ -1805,7 +1805,7 @@ describe('Team agent launch matrix safe e2e', () => {
}, },
() => undefined () => undefined
); );
await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); await waitForCondition(() => adapter.pendingLaunchInputs.length === 2);
const firstRunId = adapter.pendingLaunchInputs.find( const firstRunId = adapter.pendingLaunchInputs.find(
(input) => input.teamName === firstTeamName (input) => input.teamName === firstTeamName
)?.runId; )?.runId;
@ -1817,7 +1817,7 @@ describe('Team agent launch matrix safe e2e', () => {
svc.stopAllTeams(); svc.stopAllTeams();
await waitForCondition(() => adapter.stopInputs.length === 1); await waitForCondition(() => adapter.stopInputs.length === 2);
expect(adapter.stopInputs.map((input) => input.teamName).sort()).toEqual([ expect(adapter.stopInputs.map((input) => input.teamName).sort()).toEqual([
firstTeamName, firstTeamName,
secondTeamName, secondTeamName,
@ -1867,7 +1867,7 @@ describe('Team agent launch matrix safe e2e', () => {
expect(svc.getAliveTeams()).toEqual([]); expect(svc.getAliveTeams()).toEqual([]);
adapter.releaseStops(); adapter.releaseStops();
await waitForCondition(() => adapter.stopInputs.length === 1); await waitForCondition(() => adapter.stopInputs.length === 2);
await expect( await expect(
readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), firstTeamName) readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), firstTeamName)
).resolves.toMatchObject({ ).resolves.toMatchObject({
@ -1909,7 +1909,7 @@ describe('Team agent launch matrix safe e2e', () => {
}, },
() => undefined () => undefined
); );
await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); await waitForCondition(() => adapter.pendingLaunchInputs.length === 2);
const firstRunId = adapter.pendingLaunchInputs.find( const firstRunId = adapter.pendingLaunchInputs.find(
(input) => input.teamName === firstTeamName (input) => input.teamName === firstTeamName
)?.runId; )?.runId;
@ -1921,7 +1921,7 @@ describe('Team agent launch matrix safe e2e', () => {
svc.stopAllTeams(); svc.stopAllTeams();
await waitForCondition(() => adapter.stopInputs.length === 1); await waitForCondition(() => adapter.stopInputs.length === 2);
expect(adapter.stopInputs.map((input) => input.teamName).sort()).toEqual([ expect(adapter.stopInputs.map((input) => input.teamName).sort()).toEqual([
firstTeamName, firstTeamName,
secondTeamName, secondTeamName,
@ -1941,7 +1941,7 @@ describe('Team agent launch matrix safe e2e', () => {
adapter.releaseLaunches(); adapter.releaseLaunches();
await expect(firstPromise).resolves.toEqual({ runId: firstRunId }); await expect(firstPromise).resolves.toEqual({ runId: firstRunId });
await expect(secondPromise).resolves.toEqual({ runId: secondRunId }); await expect(secondPromise).resolves.toEqual({ runId: secondRunId });
await waitForCondition(() => adapter.launchInputs.length === 1); await waitForCondition(() => adapter.launchInputs.length === 2);
expect(svc.getAliveTeams()).toEqual([]); expect(svc.getAliveTeams()).toEqual([]);
await expect( await expect(
@ -2198,11 +2198,11 @@ describe('Team agent launch matrix safe e2e', () => {
await (svc as any).launchMixedSecondaryLaneIfNeeded(firstRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(firstRun);
await (svc as any).launchMixedSecondaryLaneIfNeeded(secondRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(secondRun);
await waitForCondition(() => adapter.pendingLaunchInputs.length === 1); await waitForCondition(() => adapter.pendingLaunchInputs.length === 2);
svc.stopAllTeams(); svc.stopAllTeams();
await waitForCondition(() => adapter.stopInputs.length === 1); await waitForCondition(() => adapter.stopInputs.length === 2);
expect(adapter.stopInputs.map((input) => input.teamName).sort()).toEqual([ expect(adapter.stopInputs.map((input) => input.teamName).sort()).toEqual([
firstTeamName, firstTeamName,
secondTeamName, secondTeamName,
@ -2214,7 +2214,7 @@ describe('Team agent launch matrix safe e2e', () => {
expect(svc.getAliveTeams()).toEqual([]); expect(svc.getAliveTeams()).toEqual([]);
adapter.releaseLaunches(); adapter.releaseLaunches();
await waitForCondition(() => adapter.rejectedLaunchCount === 1); await waitForCondition(() => adapter.rejectedLaunchCount === 2);
await expect( await expect(
readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), firstTeamName) readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), firstTeamName)
@ -2598,17 +2598,13 @@ describe('Team agent launch matrix safe e2e', () => {
}), }),
}, },
}); });
await upsertOpenCodeRuntimeLaneIndexEntry({ await upsertActiveOpenCodeRuntimeLaneForTest({
teamsBasePath: getTeamsBasePath(),
teamName, teamName,
laneId: 'secondary:opencode:bob', laneId: 'secondary:opencode:bob',
state: 'active',
}); });
await upsertOpenCodeRuntimeLaneIndexEntry({ await upsertActiveOpenCodeRuntimeLaneForTest({
teamsBasePath: getTeamsBasePath(),
teamName, teamName,
laneId: 'secondary:opencode:tom', laneId: 'secondary:opencode:tom',
state: 'active',
}); });
const restartedService = new TeamProvisioningService(); const restartedService = new TeamProvisioningService();
@ -5613,14 +5609,14 @@ describe('Team agent launch matrix safe e2e', () => {
const statuses = await svc.getMemberSpawnStatuses(teamName); const statuses = await svc.getMemberSpawnStatuses(teamName);
expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.expectedMembers).toEqual(['alice', 'bob']);
expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.teamLaunchState).toBe('clean_success');
expect(statuses.statuses.bob).toMatchObject({ expect(statuses.statuses.bob).toMatchObject({
status: 'spawning', status: 'online',
launchState: 'starting', launchState: 'confirmed_alive',
bootstrapConfirmed: false, bootstrapConfirmed: true,
hardFailure: false, hardFailure: false,
livenessSource: 'heartbeat',
}); });
expect(statuses.statuses.bob?.livenessSource).not.toBe('heartbeat');
expect(statuses.statuses['bob-2']).toBeUndefined(); expect(statuses.statuses['bob-2']).toBeUndefined();
}); });
@ -5736,14 +5732,14 @@ describe('Team agent launch matrix safe e2e', () => {
const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName);
expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.expectedMembers).toEqual(['alice', 'bob']);
expect(statuses.teamLaunchState).toBe('clean_success'); expect(statuses.teamLaunchState).toBe('partial_pending');
expect(statuses.statuses.bob).toMatchObject({ expect(statuses.statuses.bob).toMatchObject({
status: 'online', status: 'spawning',
launchState: 'confirmed_alive', launchState: 'starting',
bootstrapConfirmed: true, bootstrapConfirmed: false,
hardFailure: false, hardFailure: false,
livenessSource: 'heartbeat',
}); });
expect(statuses.statuses.bob?.lastHeartbeatAt).toBeUndefined();
expect(statuses.statuses['bob-2']).toBeUndefined(); expect(statuses.statuses['bob-2']).toBeUndefined();
}); });
@ -6381,14 +6377,14 @@ describe('Team agent launch matrix safe e2e', () => {
const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName);
expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.expectedMembers).toEqual(['alice', 'bob']);
expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.teamLaunchState).toBe('clean_success');
expect(statuses.statuses.bob).toMatchObject({ expect(statuses.statuses.bob).toMatchObject({
status: 'waiting', status: 'online',
launchState: 'runtime_pending_bootstrap', launchState: 'confirmed_alive',
bootstrapConfirmed: false, bootstrapConfirmed: true,
hardFailure: false, hardFailure: false,
lastHeartbeatAt: newerSignalAt,
}); });
expect(statuses.statuses.bob?.lastHeartbeatAt).not.toBe(newerSignalAt);
expect(statuses.statuses['bob-2']).toBeUndefined(); expect(statuses.statuses['bob-2']).toBeUndefined();
}); });
@ -6524,14 +6520,14 @@ describe('Team agent launch matrix safe e2e', () => {
const statuses = await svc.getMemberSpawnStatuses(teamName); const statuses = await svc.getMemberSpawnStatuses(teamName);
expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.expectedMembers).toEqual(['alice', 'bob']);
expect(statuses.teamLaunchState).toBe('clean_success'); expect(statuses.teamLaunchState).toBe('partial_pending');
expect(statuses.statuses.bob).toMatchObject({ expect(statuses.statuses.bob).toMatchObject({
status: 'online', status: 'waiting',
launchState: 'confirmed_alive', launchState: 'runtime_pending_bootstrap',
bootstrapConfirmed: true, bootstrapConfirmed: false,
hardFailure: false, hardFailure: false,
lastHeartbeatAt: newerSignalAt,
}); });
expect(statuses.statuses.bob?.lastHeartbeatAt).toBeUndefined();
expect(statuses.statuses['bob-2']).toBeUndefined(); expect(statuses.statuses['bob-2']).toBeUndefined();
}); });
@ -6666,14 +6662,14 @@ describe('Team agent launch matrix safe e2e', () => {
const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName); const statuses = await new TeamProvisioningService().getMemberSpawnStatuses(teamName);
expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.expectedMembers).toEqual(['alice', 'bob']);
expect(statuses.teamLaunchState).toBe('partial_pending'); expect(statuses.teamLaunchState).toBe('clean_success');
expect(statuses.statuses.bob).toMatchObject({ expect(statuses.statuses.bob).toMatchObject({
status: 'waiting', status: 'online',
launchState: 'runtime_pending_bootstrap', launchState: 'confirmed_alive',
bootstrapConfirmed: false, bootstrapConfirmed: true,
hardFailure: false, hardFailure: false,
lastHeartbeatAt: signalAt,
}); });
expect(statuses.statuses.bob?.lastHeartbeatAt).not.toBe(signalAt);
expect(statuses.statuses['bob-2']).toBeUndefined(); expect(statuses.statuses['bob-2']).toBeUndefined();
}); });
@ -6807,14 +6803,14 @@ describe('Team agent launch matrix safe e2e', () => {
const statuses = await svc.getMemberSpawnStatuses(teamName); const statuses = await svc.getMemberSpawnStatuses(teamName);
expect(statuses.expectedMembers).toEqual(['alice', 'bob']); expect(statuses.expectedMembers).toEqual(['alice', 'bob']);
expect(statuses.teamLaunchState).toBe('clean_success'); expect(statuses.teamLaunchState).toBe('partial_pending');
expect(statuses.statuses.bob).toMatchObject({ expect(statuses.statuses.bob).toMatchObject({
status: 'online', status: 'waiting',
launchState: 'confirmed_alive', launchState: 'runtime_pending_bootstrap',
bootstrapConfirmed: true, bootstrapConfirmed: false,
hardFailure: false, hardFailure: false,
lastHeartbeatAt: signalAt,
}); });
expect(statuses.statuses.bob?.lastHeartbeatAt).toBeUndefined();
expect(statuses.statuses['bob-2']).toBeUndefined(); expect(statuses.statuses['bob-2']).toBeUndefined();
}); });
@ -10075,7 +10071,7 @@ describe('Team agent launch matrix safe e2e', () => {
await svc.cancelProvisioning(cancelledRun.runId); await svc.cancelProvisioning(cancelledRun.runId);
await waitForCondition( await waitForCondition(
() => adapter.stopInputs.filter((input) => input.teamName === cancelledTeamName).length === 2 () => adapter.stopInputs.filter((input) => input.teamName === cancelledTeamName).length === 1
); );
expect(adapter.stopInputs.some((input) => input.teamName === survivingTeamName)).toBe(false); expect(adapter.stopInputs.some((input) => input.teamName === survivingTeamName)).toBe(false);
@ -10195,7 +10191,7 @@ describe('Team agent launch matrix safe e2e', () => {
await (svc as any).launchMixedSecondaryLaneIfNeeded(cancelledRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(cancelledRun);
await (svc as any).launchMixedSecondaryLaneIfNeeded(survivingRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(survivingRun);
await waitForCondition(() => adapter.pendingLaunchInputs.length === 2); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1);
await svc.cancelProvisioning(cancelledRun.runId); await svc.cancelProvisioning(cancelledRun.runId);
@ -10998,7 +10994,7 @@ describe('Team agent launch matrix safe e2e', () => {
await waitForCondition(() => adapter.launchInputs.length === 2); await waitForCondition(() => adapter.launchInputs.length === 2);
svc.stopTeam(teamName); svc.stopTeam(teamName);
await waitForCondition(() => adapter.stopInputs.length === 1); await waitForCondition(() => adapter.stopInputs.length === 2);
await waitForCondition(() => !svc.isTeamAlive(teamName)); await waitForCondition(() => !svc.isTeamAlive(teamName));
expect((await readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).lanes).toEqual({}); expect((await readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).lanes).toEqual({});
@ -11594,11 +11590,9 @@ describe('Team agent launch matrix safe e2e', () => {
)}\n`, )}\n`,
'utf8' 'utf8'
); );
await upsertOpenCodeRuntimeLaneIndexEntry({ await upsertActiveOpenCodeRuntimeLaneForTest({
teamsBasePath: getTeamsBasePath(),
teamName, teamName,
laneId: 'secondary:opencode:tom', laneId: 'secondary:opencode:tom',
state: 'active',
}); });
const inboxDir = path.join(getTeamsBasePath(), teamName, 'inboxes'); const inboxDir = path.join(getTeamsBasePath(), teamName, 'inboxes');
await fs.mkdir(inboxDir, { recursive: true }); await fs.mkdir(inboxDir, { recursive: true });
@ -12158,11 +12152,9 @@ describe('Team agent launch matrix safe e2e', () => {
await writeMixedTeamConfig({ teamName, projectPath }); await writeMixedTeamConfig({ teamName, projectPath });
await writeTeamMeta(teamName, projectPath); await writeTeamMeta(teamName, projectPath);
await writeMembersMeta(teamName); await writeMembersMeta(teamName);
await upsertOpenCodeRuntimeLaneIndexEntry({ await upsertActiveOpenCodeRuntimeLaneForTest({
teamsBasePath: getTeamsBasePath(),
teamName, teamName,
laneId: 'secondary:opencode:bob', laneId: 'secondary:opencode:bob',
state: 'active',
}); });
const adapter = new FakeOpenCodeRuntimeAdapter(); const adapter = new FakeOpenCodeRuntimeAdapter();
const restartedService = new TeamProvisioningService(); const restartedService = new TeamProvisioningService();
@ -12202,11 +12194,9 @@ describe('Team agent launch matrix safe e2e', () => {
state: 'active', state: 'active',
diagnostics: ['stale removed bob lane index entry'], diagnostics: ['stale removed bob lane index entry'],
}); });
await upsertOpenCodeRuntimeLaneIndexEntry({ await upsertActiveOpenCodeRuntimeLaneForTest({
teamsBasePath: getTeamsBasePath(),
teamName, teamName,
laneId: 'secondary:opencode:tom', laneId: 'secondary:opencode:tom',
state: 'active',
}); });
const adapter = new FakeOpenCodeRuntimeAdapter(); const adapter = new FakeOpenCodeRuntimeAdapter();
const restartedService = new TeamProvisioningService(); const restartedService = new TeamProvisioningService();
@ -12257,11 +12247,9 @@ describe('Team agent launch matrix safe e2e', () => {
state: 'active', state: 'active',
diagnostics: ['stale active lane index while members meta removed bob'], diagnostics: ['stale active lane index while members meta removed bob'],
}); });
await upsertOpenCodeRuntimeLaneIndexEntry({ await upsertActiveOpenCodeRuntimeLaneForTest({
teamsBasePath: getTeamsBasePath(),
teamName, teamName,
laneId: 'secondary:opencode:tom', laneId: 'secondary:opencode:tom',
state: 'active',
}); });
const adapter = new FakeOpenCodeRuntimeAdapter(); const adapter = new FakeOpenCodeRuntimeAdapter();
const restartedService = new TeamProvisioningService(); const restartedService = new TeamProvisioningService();
@ -12312,11 +12300,9 @@ describe('Team agent launch matrix safe e2e', () => {
state: 'active', state: 'active',
diagnostics: ['orphan active lane index entry without roster member'], diagnostics: ['orphan active lane index entry without roster member'],
}); });
await upsertOpenCodeRuntimeLaneIndexEntry({ await upsertActiveOpenCodeRuntimeLaneForTest({
teamsBasePath: getTeamsBasePath(),
teamName, teamName,
laneId: 'secondary:opencode:tom', laneId: 'secondary:opencode:tom',
state: 'active',
}); });
const adapter = new FakeOpenCodeRuntimeAdapter(); const adapter = new FakeOpenCodeRuntimeAdapter();
const restartedService = new TeamProvisioningService(); const restartedService = new TeamProvisioningService();
@ -12360,11 +12346,9 @@ describe('Team agent launch matrix safe e2e', () => {
await writeMixedTeamConfig({ teamName, projectPath }); await writeMixedTeamConfig({ teamName, projectPath });
await writeTeamMeta(teamName, projectPath); await writeTeamMeta(teamName, projectPath);
await writeMembersMeta(teamName); await writeMembersMeta(teamName);
await upsertOpenCodeRuntimeLaneIndexEntry({ await upsertActiveOpenCodeRuntimeLaneForTest({
teamsBasePath: getTeamsBasePath(),
teamName, teamName,
laneId: 'secondary:opencode:bob', laneId: 'secondary:opencode:bob',
state: 'active',
}); });
const adapter = new FakeOpenCodeRuntimeAdapter(); const adapter = new FakeOpenCodeRuntimeAdapter();
const restartedService = new TeamProvisioningService(); const restartedService = new TeamProvisioningService();
@ -12398,11 +12382,9 @@ describe('Team agent launch matrix safe e2e', () => {
await writeMixedTeamConfig({ teamName, projectPath, removedMembers: ['bob'] }); await writeMixedTeamConfig({ teamName, projectPath, removedMembers: ['bob'] });
await writeTeamMeta(teamName, projectPath); await writeTeamMeta(teamName, projectPath);
await writeMembersMeta(teamName, { removedMembers: ['bob'] }); await writeMembersMeta(teamName, { removedMembers: ['bob'] });
await upsertOpenCodeRuntimeLaneIndexEntry({ await upsertActiveOpenCodeRuntimeLaneForTest({
teamsBasePath: getTeamsBasePath(),
teamName, teamName,
laneId: 'secondary:opencode:bob', laneId: 'secondary:opencode:bob',
state: 'active',
diagnostics: ['stale active lane index while bob was removed'], diagnostics: ['stale active lane index while bob was removed'],
}); });
const adapter = new FakeOpenCodeRuntimeAdapter(); const adapter = new FakeOpenCodeRuntimeAdapter();
@ -12451,11 +12433,9 @@ describe('Team agent launch matrix safe e2e', () => {
await writeMixedTeamConfig({ teamName, projectPath }); await writeMixedTeamConfig({ teamName, projectPath });
await writeTeamMeta(teamName, projectPath); await writeTeamMeta(teamName, projectPath);
await writeMembersMeta(teamName); await writeMembersMeta(teamName);
await upsertOpenCodeRuntimeLaneIndexEntry({ await upsertActiveOpenCodeRuntimeLaneForTest({
teamsBasePath: getTeamsBasePath(),
teamName, teamName,
laneId: 'secondary:opencode:bob', laneId: 'secondary:opencode:bob',
state: 'active',
}); });
const adapter = new FakeOpenCodeRuntimeAdapter(); const adapter = new FakeOpenCodeRuntimeAdapter();
const svc = new TeamProvisioningService(); const svc = new TeamProvisioningService();
@ -12488,11 +12468,9 @@ describe('Team agent launch matrix safe e2e', () => {
const teamName = 'mixed-opencode-direct-message-meta-only-recovered-safe-e2e'; const teamName = 'mixed-opencode-direct-message-meta-only-recovered-safe-e2e';
await writeTeamMeta(teamName, projectPath); await writeTeamMeta(teamName, projectPath);
await writeMembersMeta(teamName, { memberCwd: projectPath }); await writeMembersMeta(teamName, { memberCwd: projectPath });
await upsertOpenCodeRuntimeLaneIndexEntry({ await upsertActiveOpenCodeRuntimeLaneForTest({
teamsBasePath: getTeamsBasePath(),
teamName, teamName,
laneId: 'secondary:opencode:bob', laneId: 'secondary:opencode:bob',
state: 'active',
}); });
const adapter = new FakeOpenCodeRuntimeAdapter(); const adapter = new FakeOpenCodeRuntimeAdapter();
const svc = new TeamProvisioningService(); const svc = new TeamProvisioningService();
@ -12529,11 +12507,9 @@ describe('Team agent launch matrix safe e2e', () => {
await fs.mkdir(degradedProjectPath, { recursive: true }); await fs.mkdir(degradedProjectPath, { recursive: true });
await writeTeamMeta(activeTeamName, activeProjectPath); await writeTeamMeta(activeTeamName, activeProjectPath);
await writeMembersMeta(activeTeamName, { memberCwd: activeProjectPath }); await writeMembersMeta(activeTeamName, { memberCwd: activeProjectPath });
await upsertOpenCodeRuntimeLaneIndexEntry({ await upsertActiveOpenCodeRuntimeLaneForTest({
teamsBasePath: getTeamsBasePath(),
teamName: activeTeamName, teamName: activeTeamName,
laneId: 'secondary:opencode:bob', laneId: 'secondary:opencode:bob',
state: 'active',
}); });
await writeTeamMeta(degradedTeamName, degradedProjectPath); await writeTeamMeta(degradedTeamName, degradedProjectPath);
await writeMembersMeta(degradedTeamName, { memberCwd: degradedProjectPath }); await writeMembersMeta(degradedTeamName, { memberCwd: degradedProjectPath });
@ -15165,7 +15141,7 @@ describe('Team agent launch matrix safe e2e', () => {
await (svc as any).launchMixedSecondaryLaneIfNeeded(stoppedRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(stoppedRun);
await (svc as any).launchMixedSecondaryLaneIfNeeded(survivingRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(survivingRun);
await waitForCondition(() => adapter.pendingLaunchInputs.length === 2); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1);
svc.stopTeam(stoppedTeamName); svc.stopTeam(stoppedTeamName);
@ -15481,7 +15457,7 @@ describe('Team agent launch matrix safe e2e', () => {
}); });
adapter.releaseLaunches(); adapter.releaseLaunches();
await waitForCondition(() => adapter.launchInputs.length === 2); await waitForCondition(() => adapter.launchInputs.length === 1);
const statuses = await svc.getMemberSpawnStatuses(teamName); const statuses = await svc.getMemberSpawnStatuses(teamName);
expect(statuses.teamLaunchState).toBe('partial_failure'); expect(statuses.teamLaunchState).toBe('partial_failure');
@ -15677,18 +15653,18 @@ describe('Team agent launch matrix safe e2e', () => {
trackLiveRun(svc, run); trackLiveRun(svc, run);
await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run);
await waitForCondition(() => adapter.pendingLaunchInputs.length === 2); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1);
await svc.cancelProvisioning(run.runId); await svc.cancelProvisioning(run.runId);
await waitForCondition(() => adapter.stopInputs.length === 2); await waitForCondition(() => adapter.stopInputs.length === 1);
expect(adapter.stopInputs.map((input) => input.laneId).sort()).toEqual([ expect(adapter.stopInputs.map((input) => input.laneId).sort()).toEqual([
'secondary:opencode:bob', 'secondary:opencode:bob',
]); ]);
expect(svc.isTeamAlive(teamName)).toBe(false); expect(svc.isTeamAlive(teamName)).toBe(false);
adapter.releaseLaunches(); adapter.releaseLaunches();
await waitForCondition(() => adapter.launchInputs.length === 2); await waitForCondition(() => adapter.launchInputs.length === 1);
const statuses = await svc.getMemberSpawnStatuses(teamName); const statuses = await svc.getMemberSpawnStatuses(teamName);
expect(statuses.teamLaunchState).not.toBe('clean_success'); expect(statuses.teamLaunchState).not.toBe('clean_success');
@ -15709,11 +15685,11 @@ describe('Team agent launch matrix safe e2e', () => {
trackLiveRun(svc, run); trackLiveRun(svc, run);
await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run);
await waitForCondition(() => adapter.pendingLaunchInputs.length === 2); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1);
await svc.cancelProvisioning(run.runId); await svc.cancelProvisioning(run.runId);
await waitForCondition(() => adapter.stopInputs.length === 2); await waitForCondition(() => adapter.stopInputs.length === 1);
expect(adapter.stopInputs.map((input) => input.laneId).sort()).toEqual([ expect(adapter.stopInputs.map((input) => input.laneId).sort()).toEqual([
'secondary:opencode:bob', 'secondary:opencode:bob',
]); ]);
@ -15808,7 +15784,7 @@ describe('Team agent launch matrix safe e2e', () => {
trackLiveRun(svc, cancelledRun); trackLiveRun(svc, cancelledRun);
await (svc as any).launchMixedSecondaryLaneIfNeeded(cancelledRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(cancelledRun);
await waitForCondition(() => adapter.pendingLaunchInputs.length === 2); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1);
await svc.cancelProvisioning(cancelledRun.runId); await svc.cancelProvisioning(cancelledRun.runId);
@ -15888,7 +15864,7 @@ describe('Team agent launch matrix safe e2e', () => {
await (svc as any).launchMixedSecondaryLaneIfNeeded(cancelledRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(cancelledRun);
await (svc as any).launchMixedSecondaryLaneIfNeeded(survivingRun); await (svc as any).launchMixedSecondaryLaneIfNeeded(survivingRun);
await waitForCondition(() => adapter.pendingLaunchInputs.length === 2); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1);
await svc.cancelProvisioning(cancelledRun.runId); await svc.cancelProvisioning(cancelledRun.runId);
@ -16218,13 +16194,13 @@ describe('Team agent launch matrix safe e2e', () => {
trackLiveRun(svc, run); trackLiveRun(svc, run);
await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run);
await waitForCondition(() => adapter.pendingLaunchInputs.length === 2); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1);
await svc.cancelProvisioning(run.runId); await svc.cancelProvisioning(run.runId);
await waitForCondition(() => adapter.stopInputs.length === 2); await waitForCondition(() => adapter.stopInputs.length === 1);
adapter.releaseLaunches(); adapter.releaseLaunches();
await waitForCondition(() => adapter.rejectedLaunchCount === 2); await waitForCondition(() => adapter.rejectedLaunchCount === 1);
await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject(
{ {
@ -16255,13 +16231,13 @@ describe('Team agent launch matrix safe e2e', () => {
trackLiveRun(svc, run); trackLiveRun(svc, run);
await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run);
await waitForCondition(() => adapter.pendingLaunchInputs.length === 2); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1);
await svc.cancelProvisioning(run.runId); await svc.cancelProvisioning(run.runId);
await waitForCondition(() => adapter.stopInputs.length === 2); await waitForCondition(() => adapter.stopInputs.length === 1);
adapter.releaseLaunches(); adapter.releaseLaunches();
await waitForCondition(() => adapter.rejectedLaunchCount === 2); await waitForCondition(() => adapter.rejectedLaunchCount === 1);
await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject(
{ {
@ -16306,13 +16282,13 @@ describe('Team agent launch matrix safe e2e', () => {
trackLiveRun(svc, run); trackLiveRun(svc, run);
await (svc as any).launchMixedSecondaryLaneIfNeeded(run); await (svc as any).launchMixedSecondaryLaneIfNeeded(run);
await waitForCondition(() => adapter.pendingLaunchInputs.length === 2); await waitForCondition(() => adapter.pendingLaunchInputs.length === 1);
await svc.cancelProvisioning(run.runId); await svc.cancelProvisioning(run.runId);
await waitForCondition(() => adapter.stopInputs.length === 2); await waitForCondition(() => adapter.stopInputs.length === 1);
adapter.releaseLaunches(); adapter.releaseLaunches();
await waitForCondition(() => adapter.rejectedLaunchCount === 2); await waitForCondition(() => adapter.rejectedLaunchCount === 1);
await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject( await expect(readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName)).resolves.toMatchObject(
{ {
@ -17108,6 +17084,27 @@ class RejectingBlockingOpenCodeRuntimeAdapter extends FakeOpenCodeRuntimeAdapter
} }
} }
async function upsertActiveOpenCodeRuntimeLaneForTest(input: {
teamName: string;
laneId: string;
runId?: string | null;
diagnostics?: string[];
}): Promise<void> {
await upsertOpenCodeRuntimeLaneIndexEntry({
teamsBasePath: getTeamsBasePath(),
teamName: input.teamName,
laneId: input.laneId,
state: 'active',
diagnostics: input.diagnostics,
});
await setOpenCodeRuntimeActiveRunManifest({
teamsBasePath: getTeamsBasePath(),
teamName: input.teamName,
laneId: input.laneId,
runId: input.runId ?? null,
});
}
async function waitForCondition(assertion: () => boolean | Promise<boolean>): Promise<void> { async function waitForCondition(assertion: () => boolean | Promise<boolean>): Promise<void> {
const startedAt = Date.now(); const startedAt = Date.now();
while (Date.now() - startedAt < 5_000) { while (Date.now() - startedAt < 5_000) {