fix(team): trust persisted permission state in renderer

This commit is contained in:
777genius 2026-04-23 02:36:35 +03:00
parent 34dd669d88
commit d8f0d78358
2 changed files with 105 additions and 12 deletions

View file

@ -32,20 +32,43 @@ interface FailedSpawnDetail {
reason: string | null;
}
function countPermissionBlockedMembers(memberSpawnStatuses: MemberSpawnStatusCollection): number {
if (!memberSpawnStatuses) {
return 0;
function countPermissionBlockedMembers(params: {
memberSpawnStatuses: MemberSpawnStatusCollection;
memberSpawnSnapshotStatuses?: MemberSpawnStatusesSnapshot['statuses'];
}): number {
const names = new Set<string>();
if (params.memberSpawnStatuses instanceof Map) {
for (const name of params.memberSpawnStatuses.keys()) {
names.add(name);
}
} else if (params.memberSpawnStatuses) {
for (const name of Object.keys(params.memberSpawnStatuses)) {
names.add(name);
}
}
for (const name of Object.keys(params.memberSpawnSnapshotStatuses ?? {})) {
names.add(name);
}
const entries =
memberSpawnStatuses instanceof Map
? [...memberSpawnStatuses.values()]
: Object.values(memberSpawnStatuses);
return entries.filter(
(entry) =>
let count = 0;
for (const name of names) {
const liveEntry =
params.memberSpawnStatuses instanceof Map
? params.memberSpawnStatuses.get(name)
: params.memberSpawnStatuses?.[name];
const snapshotEntry = params.memberSpawnSnapshotStatuses?.[name];
const entry = liveEntry ?? snapshotEntry;
if (!entry) {
continue;
}
if (
entry.launchState === 'runtime_pending_permission' ||
(entry.pendingPermissionRequestIds?.length ?? 0) > 0
).length;
) {
count += 1;
}
}
return count;
}
function buildAwaitingPermissionPhrase(count: number): string {
@ -185,7 +208,9 @@ export function buildTeamProvisioningPresentation({
progress: TeamProvisioningProgress | null | undefined;
members: readonly ProvisioningMemberLike[];
memberSpawnStatuses?: MemberSpawnStatusCollection;
memberSpawnSnapshot?: Pick<MemberSpawnStatusesSnapshot, 'expectedMembers' | 'summary'>;
memberSpawnSnapshot?: Pick<MemberSpawnStatusesSnapshot, 'expectedMembers' | 'summary'> & {
statuses?: MemberSpawnStatusesSnapshot['statuses'];
};
}): TeamProvisioningPresentation | null {
if (!progress) {
return null;
@ -223,7 +248,10 @@ export function buildTeamProvisioningPresentation({
failedSpawnCount,
expectedTeammateCount
);
const permissionBlockedCount = countPermissionBlockedMembers(memberSpawnStatuses);
const permissionBlockedCount = countPermissionBlockedMembers({
memberSpawnStatuses,
memberSpawnSnapshotStatuses: memberSpawnSnapshot?.statuses,
});
const { allTeammatesConfirmedAlive, hasMembersStillJoining, remainingJoinCount } =
getLaunchJoinState({

View file

@ -460,6 +460,71 @@ describe('buildTeamProvisioningPresentation', () => {
expect(presentation?.panelMessage).toBe('1 teammate awaiting permission approval');
});
it('trusts persisted snapshot permission state when live member spawn statuses are absent', () => {
const presentation = buildTeamProvisioningPresentation({
progress: {
runId: 'run-4f',
teamName: 'opencode-team',
state: 'ready',
startedAt: '2026-04-13T10:00:00.000Z',
updatedAt: '2026-04-13T10:00:08.000Z',
message: 'Launch completed',
messageSeverity: undefined,
pid: 4321,
cliLogsTail: '',
assistantOutput: '',
},
members: [
{
name: 'team-lead',
agentType: 'team-lead',
status: 'active',
currentTaskId: null,
taskCount: 0,
lastActiveAt: null,
messageCount: 0,
},
{
name: 'bob',
agentType: 'engineer',
status: 'unknown',
currentTaskId: null,
taskCount: 0,
lastActiveAt: null,
messageCount: 0,
},
],
memberSpawnStatuses: {},
memberSpawnSnapshot: {
expectedMembers: ['bob'],
statuses: {
bob: {
status: 'online',
launchState: 'runtime_pending_bootstrap',
updatedAt: '2026-04-13T10:00:07.000Z',
runtimeAlive: true,
livenessSource: 'process',
bootstrapConfirmed: false,
hardFailure: false,
agentToolAccepted: true,
pendingPermissionRequestIds: ['perm_1'],
firstSpawnAcceptedAt: '2026-04-13T10:00:01.000Z',
},
},
summary: {
confirmedCount: 0,
pendingCount: 1,
failedCount: 0,
runtimeAlivePendingCount: 1,
},
},
});
expect(presentation?.compactTitle).toBe('Finishing launch');
expect(presentation?.compactDetail).toBe('1 teammate awaiting permission approval');
expect(presentation?.panelMessage).toBe('1 teammate awaiting permission approval');
});
it('keeps a generic failed teammate message while launch is still active if only persisted failure counts remain', () => {
const presentation = buildTeamProvisioningPresentation({
progress: {