fix(team): keep launch pending for dead runtime entries
This commit is contained in:
parent
c033a0cb87
commit
79faaf1f9f
7 changed files with 181 additions and 7 deletions
|
|
@ -1240,6 +1240,7 @@ const TeamMemberListBridge = memo(function TeamMemberListBridge({
|
||||||
() => buildTeamAgentRuntimeMap(runtimeSnapshot?.members),
|
() => buildTeamAgentRuntimeMap(runtimeSnapshot?.members),
|
||||||
[runtimeSnapshot?.members]
|
[runtimeSnapshot?.members]
|
||||||
);
|
);
|
||||||
|
const runtimeEntries = runtimeSnapshot?.members;
|
||||||
const runtimeRunId = runtimeSnapshot?.runId ?? memberSpawnSnapshot?.runId ?? progress?.runId;
|
const runtimeRunId = runtimeSnapshot?.runId ?? memberSpawnSnapshot?.runId ?? progress?.runId;
|
||||||
const isLaunchSettling = useMemo(() => {
|
const isLaunchSettling = useMemo(() => {
|
||||||
if (progress?.state !== 'ready') {
|
if (progress?.state !== 'ready') {
|
||||||
|
|
@ -1250,9 +1251,10 @@ const TeamMemberListBridge = memo(function TeamMemberListBridge({
|
||||||
members: props.members,
|
members: props.members,
|
||||||
memberSpawnStatuses,
|
memberSpawnStatuses,
|
||||||
memberSpawnSnapshot,
|
memberSpawnSnapshot,
|
||||||
|
memberRuntimeEntries: runtimeEntries,
|
||||||
})
|
})
|
||||||
).hasMembersStillJoining;
|
).hasMembersStillJoining;
|
||||||
}, [memberSpawnSnapshot, memberSpawnStatuses, progress?.state, props.members]);
|
}, [memberSpawnSnapshot, memberSpawnStatuses, progress?.state, props.members, runtimeEntries]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MemberList
|
<MemberList
|
||||||
|
|
@ -1324,6 +1326,7 @@ const TeamMemberDetailDialogBridge = memo(function TeamMemberDetailDialogBridge(
|
||||||
memberSpawnSnapshot,
|
memberSpawnSnapshot,
|
||||||
spawnEntry,
|
spawnEntry,
|
||||||
runtimeRunId,
|
runtimeRunId,
|
||||||
|
runtimeEntries,
|
||||||
runtimeEntry,
|
runtimeEntry,
|
||||||
} = useStore(
|
} = useStore(
|
||||||
useShallow((s) => ({
|
useShallow((s) => ({
|
||||||
|
|
@ -1338,6 +1341,7 @@ const TeamMemberDetailDialogBridge = memo(function TeamMemberDetailDialogBridge(
|
||||||
s.teamAgentRuntimeByTeam[teamName]?.runId ??
|
s.teamAgentRuntimeByTeam[teamName]?.runId ??
|
||||||
s.memberSpawnSnapshotsByTeam[teamName]?.runId ??
|
s.memberSpawnSnapshotsByTeam[teamName]?.runId ??
|
||||||
getCurrentProvisioningProgressForTeam(s, teamName)?.runId,
|
getCurrentProvisioningProgressForTeam(s, teamName)?.runId,
|
||||||
|
runtimeEntries: s.teamAgentRuntimeByTeam[teamName]?.members,
|
||||||
runtimeEntry: member ? s.teamAgentRuntimeByTeam[teamName]?.members[member.name] : undefined,
|
runtimeEntry: member ? s.teamAgentRuntimeByTeam[teamName]?.members[member.name] : undefined,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
@ -1350,9 +1354,10 @@ const TeamMemberDetailDialogBridge = memo(function TeamMemberDetailDialogBridge(
|
||||||
members: launchMembers,
|
members: launchMembers,
|
||||||
memberSpawnStatuses,
|
memberSpawnStatuses,
|
||||||
memberSpawnSnapshot,
|
memberSpawnSnapshot,
|
||||||
|
memberRuntimeEntries: runtimeEntries,
|
||||||
})
|
})
|
||||||
).hasMembersStillJoining;
|
).hasMembersStillJoining;
|
||||||
}, [launchMembers, memberSpawnSnapshot, memberSpawnStatuses, progress?.state]);
|
}, [launchMembers, memberSpawnSnapshot, memberSpawnStatuses, progress?.state, runtimeEntries]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MemberDetailDialog
|
<MemberDetailDialog
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ export const MemberHoverCard = memo(function MemberHoverCard({
|
||||||
memberSpawnStatuses,
|
memberSpawnStatuses,
|
||||||
spawnEntry,
|
spawnEntry,
|
||||||
runtimeRunId,
|
runtimeRunId,
|
||||||
|
runtimeEntries,
|
||||||
runtimeEntry,
|
runtimeEntry,
|
||||||
leadActivity,
|
leadActivity,
|
||||||
} = useStore(
|
} = useStore(
|
||||||
|
|
@ -109,6 +110,9 @@ export const MemberHoverCard = memo(function MemberHoverCard({
|
||||||
runtimeRunId: effectiveTeamName
|
runtimeRunId: effectiveTeamName
|
||||||
? s.teamAgentRuntimeByTeam?.[effectiveTeamName]?.runId
|
? s.teamAgentRuntimeByTeam?.[effectiveTeamName]?.runId
|
||||||
: undefined,
|
: undefined,
|
||||||
|
runtimeEntries: effectiveTeamName
|
||||||
|
? s.teamAgentRuntimeByTeam?.[effectiveTeamName]?.members
|
||||||
|
: undefined,
|
||||||
runtimeEntry: effectiveTeamName
|
runtimeEntry: effectiveTeamName
|
||||||
? s.teamAgentRuntimeByTeam?.[effectiveTeamName]?.members[name]
|
? s.teamAgentRuntimeByTeam?.[effectiveTeamName]?.members[name]
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|
@ -126,6 +130,7 @@ export const MemberHoverCard = memo(function MemberHoverCard({
|
||||||
members: teamMembers,
|
members: teamMembers,
|
||||||
memberSpawnStatuses,
|
memberSpawnStatuses,
|
||||||
memberSpawnSnapshot,
|
memberSpawnSnapshot,
|
||||||
|
memberRuntimeEntries: runtimeEntries,
|
||||||
});
|
});
|
||||||
const isLaunchSettling =
|
const isLaunchSettling =
|
||||||
progress?.state === 'ready' && getLaunchJoinState(launchJoinMilestones).hasMembersStillJoining;
|
progress?.state === 'ready' && getLaunchJoinState(launchJoinMilestones).hasMembersStillJoining;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { isLeadMember } from '@shared/utils/leadDetection';
|
||||||
import type {
|
import type {
|
||||||
MemberSpawnStatusEntry,
|
MemberSpawnStatusEntry,
|
||||||
MemberSpawnStatusesSnapshot,
|
MemberSpawnStatusesSnapshot,
|
||||||
|
TeamAgentRuntimeEntry,
|
||||||
TeamProvisioningProgress,
|
TeamProvisioningProgress,
|
||||||
} from '@shared/types';
|
} from '@shared/types';
|
||||||
|
|
||||||
|
|
@ -39,6 +40,11 @@ type MemberSpawnStatusCollection =
|
||||||
| Map<string, MemberSpawnStatusEntry>
|
| Map<string, MemberSpawnStatusEntry>
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
|
type TeamAgentRuntimeEntryCollection =
|
||||||
|
| Record<string, TeamAgentRuntimeEntry>
|
||||||
|
| Map<string, TeamAgentRuntimeEntry>
|
||||||
|
| undefined;
|
||||||
|
|
||||||
function getSpawnEntry(
|
function getSpawnEntry(
|
||||||
memberSpawnStatuses: MemberSpawnStatusCollection,
|
memberSpawnStatuses: MemberSpawnStatusCollection,
|
||||||
memberName: string
|
memberName: string
|
||||||
|
|
@ -52,6 +58,19 @@ function getSpawnEntry(
|
||||||
return memberSpawnStatuses[memberName];
|
return memberSpawnStatuses[memberName];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRuntimeEntry(
|
||||||
|
memberRuntimeEntries: TeamAgentRuntimeEntryCollection,
|
||||||
|
memberName: string
|
||||||
|
): TeamAgentRuntimeEntry | undefined {
|
||||||
|
if (!memberRuntimeEntries) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (memberRuntimeEntries instanceof Map) {
|
||||||
|
return memberRuntimeEntries.get(memberName);
|
||||||
|
}
|
||||||
|
return memberRuntimeEntries[memberName];
|
||||||
|
}
|
||||||
|
|
||||||
function parseStatusUpdatedAtMs(value: string | undefined): number | null {
|
function parseStatusUpdatedAtMs(value: string | undefined): number | null {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -72,6 +91,16 @@ function isStrongRuntimeProcessSpawnEntry(entry: MemberSpawnStatusEntry): boolea
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isConfirmedSpawnEntry(entry: MemberSpawnStatusEntry): boolean {
|
||||||
|
return entry.launchState === 'confirmed_alive' || entry.bootstrapConfirmed === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runtimeEntryContradictsConfirmedJoin(
|
||||||
|
runtimeEntry: TeamAgentRuntimeEntry | undefined
|
||||||
|
): boolean {
|
||||||
|
return runtimeEntry?.alive === false;
|
||||||
|
}
|
||||||
|
|
||||||
function shouldPreferSnapshotEntryOverLive(
|
function shouldPreferSnapshotEntryOverLive(
|
||||||
liveEntry: MemberSpawnStatusEntry | undefined,
|
liveEntry: MemberSpawnStatusEntry | undefined,
|
||||||
snapshotEntry: MemberSpawnStatusEntry | undefined,
|
snapshotEntry: MemberSpawnStatusEntry | undefined,
|
||||||
|
|
@ -98,6 +127,7 @@ function summarizeLiveLaunchJoinMilestones(params: {
|
||||||
memberSpawnStatuses?: MemberSpawnStatusCollection;
|
memberSpawnStatuses?: MemberSpawnStatusCollection;
|
||||||
memberSpawnSnapshotStatuses?: MemberSpawnStatusesSnapshot['statuses'];
|
memberSpawnSnapshotStatuses?: MemberSpawnStatusesSnapshot['statuses'];
|
||||||
memberSpawnSnapshotUpdatedAt?: string;
|
memberSpawnSnapshotUpdatedAt?: string;
|
||||||
|
memberRuntimeEntries?: TeamAgentRuntimeEntryCollection;
|
||||||
}): Omit<LaunchJoinMilestones, 'expectedTeammateCount'> & {
|
}): Omit<LaunchJoinMilestones, 'expectedTeammateCount'> & {
|
||||||
observedTeammateCount: number;
|
observedTeammateCount: number;
|
||||||
} {
|
} {
|
||||||
|
|
@ -137,6 +167,13 @@ function summarizeLiveLaunchJoinMilestones(params: {
|
||||||
skippedSpawnCount += 1;
|
skippedSpawnCount += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
isConfirmedSpawnEntry(entry) &&
|
||||||
|
runtimeEntryContradictsConfirmedJoin(getRuntimeEntry(params.memberRuntimeEntries, memberName))
|
||||||
|
) {
|
||||||
|
pendingSpawnCount += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (entry.launchState === 'confirmed_alive') {
|
if (entry.launchState === 'confirmed_alive') {
|
||||||
heartbeatConfirmedCount += 1;
|
heartbeatConfirmedCount += 1;
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -172,6 +209,7 @@ export function getLaunchJoinMilestonesFromMembers({
|
||||||
members,
|
members,
|
||||||
memberSpawnStatuses,
|
memberSpawnStatuses,
|
||||||
memberSpawnSnapshot,
|
memberSpawnSnapshot,
|
||||||
|
memberRuntimeEntries,
|
||||||
}: {
|
}: {
|
||||||
members: readonly LaunchJoinMemberLike[];
|
members: readonly LaunchJoinMemberLike[];
|
||||||
memberSpawnStatuses?: MemberSpawnStatusCollection;
|
memberSpawnStatuses?: MemberSpawnStatusCollection;
|
||||||
|
|
@ -181,6 +219,7 @@ export function getLaunchJoinMilestonesFromMembers({
|
||||||
> & {
|
> & {
|
||||||
statuses?: MemberSpawnStatusesSnapshot['statuses'];
|
statuses?: MemberSpawnStatusesSnapshot['statuses'];
|
||||||
};
|
};
|
||||||
|
memberRuntimeEntries?: TeamAgentRuntimeEntryCollection;
|
||||||
}): LaunchJoinMilestones {
|
}): LaunchJoinMilestones {
|
||||||
const removedTeammateNameSet = new Set(
|
const removedTeammateNameSet = new Set(
|
||||||
members
|
members
|
||||||
|
|
@ -209,6 +248,7 @@ export function getLaunchJoinMilestonesFromMembers({
|
||||||
memberSpawnStatuses,
|
memberSpawnStatuses,
|
||||||
memberSpawnSnapshotStatuses: memberSpawnSnapshot?.statuses,
|
memberSpawnSnapshotStatuses: memberSpawnSnapshot?.statuses,
|
||||||
memberSpawnSnapshotUpdatedAt: memberSpawnSnapshot?.updatedAt,
|
memberSpawnSnapshotUpdatedAt: memberSpawnSnapshot?.updatedAt,
|
||||||
|
memberRuntimeEntries,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (snapshotSummary) {
|
if (snapshotSummary) {
|
||||||
|
|
|
||||||
|
|
@ -51,9 +51,10 @@ export function useTeamProvisioningPresentation(teamName: string): {
|
||||||
members: teamMembers,
|
members: teamMembers,
|
||||||
memberSpawnStatuses,
|
memberSpawnStatuses,
|
||||||
memberSpawnSnapshot,
|
memberSpawnSnapshot,
|
||||||
|
memberRuntimeEntries: runtimeSnapshot?.members,
|
||||||
t,
|
t,
|
||||||
}),
|
}),
|
||||||
[memberSpawnSnapshot, memberSpawnStatuses, progress, teamMembers, t]
|
[memberSpawnSnapshot, memberSpawnStatuses, progress, runtimeSnapshot?.members, teamMembers, t]
|
||||||
);
|
);
|
||||||
const memberDiagnostics = useMemo(
|
const memberDiagnostics = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { isLeadMember } from '@shared/utils/leadDetection';
|
||||||
import type {
|
import type {
|
||||||
MemberSpawnStatusEntry,
|
MemberSpawnStatusEntry,
|
||||||
MemberSpawnStatusesSnapshot,
|
MemberSpawnStatusesSnapshot,
|
||||||
|
TeamAgentRuntimeEntry,
|
||||||
TeamProvisioningProgress,
|
TeamProvisioningProgress,
|
||||||
} from '@shared/types';
|
} from '@shared/types';
|
||||||
|
|
||||||
|
|
@ -17,6 +18,11 @@ type MemberSpawnStatusCollection =
|
||||||
| Map<string, MemberSpawnStatusEntry>
|
| Map<string, MemberSpawnStatusEntry>
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
|
type TeamAgentRuntimeEntryCollection =
|
||||||
|
| Record<string, TeamAgentRuntimeEntry>
|
||||||
|
| Map<string, TeamAgentRuntimeEntry>
|
||||||
|
| undefined;
|
||||||
|
|
||||||
interface ProvisioningMemberLike {
|
interface ProvisioningMemberLike {
|
||||||
name: string;
|
name: string;
|
||||||
removedAt?: number;
|
removedAt?: number;
|
||||||
|
|
@ -892,6 +898,7 @@ export function buildTeamProvisioningPresentation({
|
||||||
members,
|
members,
|
||||||
memberSpawnStatuses,
|
memberSpawnStatuses,
|
||||||
memberSpawnSnapshot,
|
memberSpawnSnapshot,
|
||||||
|
memberRuntimeEntries,
|
||||||
t,
|
t,
|
||||||
}: {
|
}: {
|
||||||
progress: TeamProvisioningProgress | null | undefined;
|
progress: TeamProvisioningProgress | null | undefined;
|
||||||
|
|
@ -903,6 +910,7 @@ export function buildTeamProvisioningPresentation({
|
||||||
> & {
|
> & {
|
||||||
statuses?: MemberSpawnStatusesSnapshot['statuses'];
|
statuses?: MemberSpawnStatusesSnapshot['statuses'];
|
||||||
};
|
};
|
||||||
|
memberRuntimeEntries?: TeamAgentRuntimeEntryCollection;
|
||||||
t?: unknown;
|
t?: unknown;
|
||||||
}): TeamProvisioningPresentation | null {
|
}): TeamProvisioningPresentation | null {
|
||||||
if (!progress) {
|
if (!progress) {
|
||||||
|
|
@ -934,6 +942,7 @@ export function buildTeamProvisioningPresentation({
|
||||||
members,
|
members,
|
||||||
memberSpawnStatuses,
|
memberSpawnStatuses,
|
||||||
memberSpawnSnapshot,
|
memberSpawnSnapshot,
|
||||||
|
memberRuntimeEntries,
|
||||||
});
|
});
|
||||||
const failedSpawnDetails = getFailedSpawnDetails({
|
const failedSpawnDetails = getFailedSpawnDetails({
|
||||||
memberSpawnStatuses,
|
memberSpawnStatuses,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
|
|
||||||
import { getLaunchJoinMilestonesFromMembers } from '@renderer/components/team/provisioningSteps';
|
import { getLaunchJoinMilestonesFromMembers } from '@renderer/components/team/provisioningSteps';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
const members = [{ name: 'alice' }, { name: 'bob' }, { name: 'tom' }, { name: 'jane' }];
|
const members = [{ name: 'alice' }, { name: 'bob' }, { name: 'tom' }, { name: 'jane' }];
|
||||||
|
|
||||||
|
|
@ -194,4 +193,54 @@ describe('getLaunchJoinMilestonesFromMembers', () => {
|
||||||
expect(milestones.pendingSpawnCount).toBe(1);
|
expect(milestones.pendingSpawnCount).toBe(1);
|
||||||
expect(milestones.expectedTeammateCount).toBe(4);
|
expect(milestones.expectedTeammateCount).toBe(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not count confirmed spawn as joined when runtime snapshot is unavailable', () => {
|
||||||
|
const milestones = getLaunchJoinMilestonesFromMembers({
|
||||||
|
members,
|
||||||
|
memberSpawnStatuses: {
|
||||||
|
alice: {
|
||||||
|
status: 'online',
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
runtimeAlive: true,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
updatedAt: '2026-04-24T12:00:00.000Z',
|
||||||
|
},
|
||||||
|
bob: {
|
||||||
|
status: 'online',
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
runtimeAlive: true,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
updatedAt: '2026-04-24T12:00:01.000Z',
|
||||||
|
},
|
||||||
|
tom: {
|
||||||
|
status: 'online',
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
runtimeAlive: true,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
updatedAt: '2026-04-24T12:00:00.000Z',
|
||||||
|
},
|
||||||
|
jane: {
|
||||||
|
status: 'online',
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
runtimeAlive: true,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
updatedAt: '2026-04-24T12:00:00.000Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
memberRuntimeEntries: {
|
||||||
|
bob: {
|
||||||
|
memberName: 'bob',
|
||||||
|
alive: false,
|
||||||
|
restartable: true,
|
||||||
|
livenessKind: 'registered_only',
|
||||||
|
runtimeDiagnostic: 'registered runtime metadata without live process',
|
||||||
|
updatedAt: '2026-04-24T12:00:02.000Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(milestones.heartbeatConfirmedCount).toBe(3);
|
||||||
|
expect(milestones.pendingSpawnCount).toBe(1);
|
||||||
|
expect(milestones.expectedTeammateCount).toBe(4);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
|
|
||||||
import { buildTeamProvisioningPresentation } from '@renderer/utils/teamProvisioningPresentation';
|
import { buildTeamProvisioningPresentation } from '@renderer/utils/teamProvisioningPresentation';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
describe('buildTeamProvisioningPresentation', () => {
|
describe('buildTeamProvisioningPresentation', () => {
|
||||||
it('uses a lead-online compact detail for ready teams without teammates', () => {
|
it('uses a lead-online compact detail for ready teams without teammates', () => {
|
||||||
|
|
@ -1607,6 +1606,72 @@ describe('buildTeamProvisioningPresentation', () => {
|
||||||
expect(presentation?.currentStepIndex).toBe(4);
|
expect(presentation?.currentStepIndex).toBe(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps ready launch in finishing state when runtime snapshot contradicts confirmed spawn', () => {
|
||||||
|
const presentation = buildTeamProvisioningPresentation({
|
||||||
|
progress: {
|
||||||
|
runId: 'run-5b',
|
||||||
|
teamName: 'codex-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: {
|
||||||
|
bob: {
|
||||||
|
status: 'online',
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
updatedAt: '2026-04-13T10:00:07.000Z',
|
||||||
|
runtimeAlive: true,
|
||||||
|
livenessSource: 'heartbeat',
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: false,
|
||||||
|
agentToolAccepted: true,
|
||||||
|
firstSpawnAcceptedAt: '2026-04-13T10:00:01.000Z',
|
||||||
|
lastHeartbeatAt: '2026-04-13T10:00:07.000Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
memberRuntimeEntries: {
|
||||||
|
bob: {
|
||||||
|
memberName: 'bob',
|
||||||
|
alive: false,
|
||||||
|
restartable: true,
|
||||||
|
livenessKind: 'registered_only',
|
||||||
|
runtimeDiagnostic: 'registered runtime metadata without live process',
|
||||||
|
updatedAt: '2026-04-13T10:00:08.000Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(presentation?.compactTitle).toBe('Finishing launch');
|
||||||
|
expect(presentation?.compactDetail).toBe('1 teammate still joining');
|
||||||
|
expect(presentation?.successMessage).toBe('Finishing launch');
|
||||||
|
expect(presentation?.currentStepIndex).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
it('ignores removed teammates that still linger in persisted expectedMembers', () => {
|
it('ignores removed teammates that still linger in persisted expectedMembers', () => {
|
||||||
const presentation = buildTeamProvisioningPresentation({
|
const presentation = buildTeamProvisioningPresentation({
|
||||||
progress: {
|
progress: {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue