fix(team): keep permission-pending launches from reading ready
This commit is contained in:
parent
e8ebe68576
commit
146b839b9c
2 changed files with 181 additions and 12 deletions
|
|
@ -11857,6 +11857,19 @@ export class TeamProvisioningService {
|
||||||
runtimeAlivePendingCount: number;
|
runtimeAlivePendingCount: number;
|
||||||
}
|
}
|
||||||
): string {
|
): string {
|
||||||
|
const permissionPendingCount = this.countRunPermissionPendingMembers(run);
|
||||||
|
if (
|
||||||
|
launchSummary.pendingCount > 0 &&
|
||||||
|
permissionPendingCount > 0 &&
|
||||||
|
permissionPendingCount === launchSummary.pendingCount
|
||||||
|
) {
|
||||||
|
return `${prefix} — ${
|
||||||
|
permissionPendingCount === 1
|
||||||
|
? '1 teammate awaiting permission approval'
|
||||||
|
: `${permissionPendingCount} teammates awaiting permission approval`
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
const stillStartingCount = Math.max(
|
const stillStartingCount = Math.max(
|
||||||
0,
|
0,
|
||||||
launchSummary.pendingCount - launchSummary.runtimeAlivePendingCount
|
launchSummary.pendingCount - launchSummary.runtimeAlivePendingCount
|
||||||
|
|
@ -11891,6 +11904,30 @@ export class TeamProvisioningService {
|
||||||
return this.buildPendingBootstrapStatusMessage(prefix, run, launchSummary);
|
return this.buildPendingBootstrapStatusMessage(prefix, run, launchSummary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const allPendingMembers = snapshot.expectedMembers.filter((memberName) => {
|
||||||
|
const member = snapshot.members[memberName];
|
||||||
|
if (!member) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return member.launchState !== 'confirmed_alive' && member.launchState !== 'failed_to_start';
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
allPendingMembers.length > 0 &&
|
||||||
|
allPendingMembers.every((memberName) => {
|
||||||
|
const member = snapshot.members[memberName];
|
||||||
|
return (
|
||||||
|
member?.launchState === 'runtime_pending_permission' ||
|
||||||
|
(member?.pendingPermissionRequestIds?.length ?? 0) > 0
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return `${prefix} — ${
|
||||||
|
allPendingMembers.length === 1
|
||||||
|
? '1 teammate awaiting permission approval'
|
||||||
|
: `${allPendingMembers.length} teammates awaiting permission approval`
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
const primaryExpectedMembers = new Set(
|
const primaryExpectedMembers = new Set(
|
||||||
snapshot.bootstrapExpectedMembers ?? run.expectedMembers
|
snapshot.bootstrapExpectedMembers ?? run.expectedMembers
|
||||||
);
|
);
|
||||||
|
|
@ -11922,6 +11959,28 @@ export class TeamProvisioningService {
|
||||||
return statuses;
|
return statuses;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private countRunPermissionPendingMembers(run: ProvisioningRun): number {
|
||||||
|
let count = 0;
|
||||||
|
for (const expected of run.expectedMembers ?? []) {
|
||||||
|
const entry = run.memberSpawnStatuses.get(expected) ?? createInitialMemberSpawnStatusEntry();
|
||||||
|
if (entry.launchState === 'runtime_pending_permission') {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private hasPendingLaunchMembers(
|
||||||
|
run: ProvisioningRun,
|
||||||
|
launchSummary: {
|
||||||
|
pendingCount: number;
|
||||||
|
},
|
||||||
|
snapshot?: PersistedTeamLaunchSnapshot | null
|
||||||
|
): boolean {
|
||||||
|
const expectedCount = snapshot?.expectedMembers.length ?? run.expectedMembers?.length ?? 0;
|
||||||
|
return launchSummary.pendingCount > 0 && expectedCount > 0;
|
||||||
|
}
|
||||||
|
|
||||||
private buildLiveLaunchSnapshotForRun(
|
private buildLiveLaunchSnapshotForRun(
|
||||||
run: ProvisioningRun,
|
run: ProvisioningRun,
|
||||||
launchPhase: PersistedTeamLaunchPhase = run.provisioningComplete ? 'finished' : 'active'
|
launchPhase: PersistedTeamLaunchPhase = run.provisioningComplete ? 'finished' : 'active'
|
||||||
|
|
@ -15443,14 +15502,9 @@ export class TeamProvisioningService {
|
||||||
: this.getFailedSpawnMembers(run);
|
: this.getFailedSpawnMembers(run);
|
||||||
const launchSummary = persistedLaunchSnapshot?.summary ?? this.getMemberLaunchSummary(run);
|
const launchSummary = persistedLaunchSnapshot?.summary ?? this.getMemberLaunchSummary(run);
|
||||||
const hasSpawnFailures = failedSpawnMembers.length > 0;
|
const hasSpawnFailures = failedSpawnMembers.length > 0;
|
||||||
const stillStartingCount = Math.max(
|
|
||||||
0,
|
|
||||||
launchSummary.pendingCount - launchSummary.runtimeAlivePendingCount
|
|
||||||
);
|
|
||||||
const hasPendingBootstrap =
|
const hasPendingBootstrap =
|
||||||
!hasSpawnFailures &&
|
!hasSpawnFailures &&
|
||||||
stillStartingCount > 0 &&
|
this.hasPendingLaunchMembers(run, launchSummary, persistedLaunchSnapshot);
|
||||||
(persistedLaunchSnapshot?.expectedMembers.length ?? run.expectedMembers?.length ?? 0) > 0;
|
|
||||||
const readyMessage = hasSpawnFailures
|
const readyMessage = hasSpawnFailures
|
||||||
? `Launch completed with teammate errors — ${failedSpawnMembers
|
? `Launch completed with teammate errors — ${failedSpawnMembers
|
||||||
.map((member) => member.name)
|
.map((member) => member.name)
|
||||||
|
|
@ -15622,14 +15676,9 @@ export class TeamProvisioningService {
|
||||||
: this.getFailedSpawnMembers(run);
|
: this.getFailedSpawnMembers(run);
|
||||||
const launchSummary = persistedLaunchSnapshot?.summary ?? this.getMemberLaunchSummary(run);
|
const launchSummary = persistedLaunchSnapshot?.summary ?? this.getMemberLaunchSummary(run);
|
||||||
const hasSpawnFailures = failedSpawnMembers.length > 0;
|
const hasSpawnFailures = failedSpawnMembers.length > 0;
|
||||||
const stillStartingCount = Math.max(
|
|
||||||
0,
|
|
||||||
launchSummary.pendingCount - launchSummary.runtimeAlivePendingCount
|
|
||||||
);
|
|
||||||
const hasPendingBootstrap =
|
const hasPendingBootstrap =
|
||||||
!hasSpawnFailures &&
|
!hasSpawnFailures &&
|
||||||
stillStartingCount > 0 &&
|
this.hasPendingLaunchMembers(run, launchSummary, persistedLaunchSnapshot);
|
||||||
(persistedLaunchSnapshot?.expectedMembers.length ?? run.expectedMembers.length) > 0;
|
|
||||||
const progress = updateProgress(
|
const progress = updateProgress(
|
||||||
run,
|
run,
|
||||||
'ready',
|
'ready',
|
||||||
|
|
|
||||||
|
|
@ -2001,6 +2001,126 @@ describe('TeamProvisioningService', () => {
|
||||||
expect(message).toBe('Finishing launch - waiting for secondary runtime lane: bob');
|
expect(message).toBe('Finishing launch - waiting for secondary runtime lane: bob');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses permission-pending copy when the remaining mixed-team member is awaiting approval', async () => {
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
const run = createMemberSpawnRun({
|
||||||
|
teamName: 'mixed-team',
|
||||||
|
expectedMembers: ['alice'],
|
||||||
|
memberSpawnStatuses: new Map([
|
||||||
|
[
|
||||||
|
'alice',
|
||||||
|
createMemberSpawnStatusEntry({
|
||||||
|
status: 'online',
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
runtimeAlive: true,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
run.isLaunch = true;
|
||||||
|
run.mixedSecondaryLanes = [
|
||||||
|
{
|
||||||
|
laneId: 'secondary:opencode:bob',
|
||||||
|
providerId: 'opencode',
|
||||||
|
member: {
|
||||||
|
name: 'bob',
|
||||||
|
providerId: 'opencode',
|
||||||
|
model: 'minimax-m2.5-free',
|
||||||
|
effort: 'medium',
|
||||||
|
},
|
||||||
|
runId: 'opencode-run-1',
|
||||||
|
state: 'launching',
|
||||||
|
result: null,
|
||||||
|
warnings: [],
|
||||||
|
diagnostics: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const message = (svc as any).buildAggregatePendingLaunchMessage(
|
||||||
|
'Finishing launch',
|
||||||
|
run,
|
||||||
|
{
|
||||||
|
confirmedCount: 1,
|
||||||
|
pendingCount: 1,
|
||||||
|
failedCount: 0,
|
||||||
|
runtimeAlivePendingCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: 2,
|
||||||
|
teamName: 'mixed-team',
|
||||||
|
updatedAt: '2026-04-22T12:00:00.000Z',
|
||||||
|
launchPhase: 'active',
|
||||||
|
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: 'runtime_pending_permission',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: true,
|
||||||
|
bootstrapConfirmed: false,
|
||||||
|
hardFailure: false,
|
||||||
|
pendingPermissionRequestIds: ['perm-1'],
|
||||||
|
lastEvaluatedAt: '2026-04-22T12:00:00.000Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
confirmedCount: 1,
|
||||||
|
pendingCount: 1,
|
||||||
|
failedCount: 0,
|
||||||
|
runtimeAlivePendingCount: 1,
|
||||||
|
},
|
||||||
|
teamLaunchState: 'partial_pending',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(message).toBe('Finishing launch — 1 teammate awaiting permission approval');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps launch pending when the only remaining teammate is permission-blocked but already online', () => {
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
const run = createMemberSpawnRun({
|
||||||
|
expectedMembers: ['alice'],
|
||||||
|
memberSpawnStatuses: new Map([
|
||||||
|
[
|
||||||
|
'alice',
|
||||||
|
createMemberSpawnStatusEntry({
|
||||||
|
status: 'online',
|
||||||
|
launchState: 'runtime_pending_permission',
|
||||||
|
runtimeAlive: true,
|
||||||
|
agentToolAccepted: true,
|
||||||
|
bootstrapConfirmed: false,
|
||||||
|
pendingPermissionRequestIds: ['perm-1'],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
const launchSummary = (svc as any).getMemberLaunchSummary(run);
|
||||||
|
|
||||||
|
expect((svc as any).hasPendingLaunchMembers(run, launchSummary, null)).toBe(true);
|
||||||
|
expect((svc as any).buildPendingBootstrapStatusMessage('Finishing launch', run, launchSummary)).toBe(
|
||||||
|
'Finishing launch — 1 teammate awaiting permission approval'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('launches the OpenCode secondary lane with side-lane provider and member runtime identity', async () => {
|
it('launches the OpenCode secondary lane with side-lane provider and member runtime identity', async () => {
|
||||||
const svc = new TeamProvisioningService();
|
const svc = new TeamProvisioningService();
|
||||||
const adapterLaunch = vi.fn(async (input: Record<string, unknown>) => ({
|
const adapterLaunch = vi.fn(async (input: Record<string, unknown>) => ({
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue