496 lines
14 KiB
TypeScript
496 lines
14 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
getRuntimeMemorySourceLabel,
|
|
resolveMemberRuntimeSummary,
|
|
} from '@renderer/utils/memberRuntimeSummary';
|
|
|
|
import type { MemberSpawnStatusEntry, ResolvedTeamMember } from '@shared/types';
|
|
|
|
type TestResolvedTeamMember = ResolvedTeamMember & { providerBackendId?: string };
|
|
|
|
function createMember(overrides: Partial<TestResolvedTeamMember> = {}): TestResolvedTeamMember {
|
|
return {
|
|
name: 'alice',
|
|
agentId: 'alice@test-team',
|
|
agentType: 'general-purpose',
|
|
role: 'developer',
|
|
providerId: 'codex',
|
|
effort: 'medium',
|
|
status: 'idle',
|
|
currentTaskId: null,
|
|
taskCount: 0,
|
|
lastActiveAt: null,
|
|
messageCount: 0,
|
|
color: 'blue',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createSpawnEntry(overrides: Partial<MemberSpawnStatusEntry> = {}): MemberSpawnStatusEntry {
|
|
return {
|
|
status: 'waiting',
|
|
launchState: 'starting',
|
|
runtimeAlive: false,
|
|
bootstrapConfirmed: false,
|
|
hardFailure: false,
|
|
agentToolAccepted: true,
|
|
updatedAt: '2026-04-16T17:10:48.646Z',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('resolveMemberRuntimeSummary', () => {
|
|
it('shows the live runtime model for loading members when available', () => {
|
|
const member = createMember();
|
|
const spawnEntry = createSpawnEntry({ runtimeModel: 'claude-opus-4-7', runtimeAlive: true });
|
|
|
|
expect(resolveMemberRuntimeSummary(member, undefined, spawnEntry)).toBe(
|
|
'Anthropic · Opus 4.7 · Medium · Codex'
|
|
);
|
|
});
|
|
|
|
it('keeps the configured summary visible while a pending member waits for the live runtime model', () => {
|
|
const member = createMember({ model: 'gpt-5.4-mini' });
|
|
const spawnEntry = createSpawnEntry();
|
|
|
|
expect(resolveMemberRuntimeSummary(member, undefined, spawnEntry)).toBe(
|
|
'5.4 Mini · Medium · Codex'
|
|
);
|
|
});
|
|
|
|
it('still keeps the loading skeleton when a pending member has neither live nor configured model truth', () => {
|
|
const member = createMember({ model: undefined });
|
|
const spawnEntry = createSpawnEntry();
|
|
|
|
expect(resolveMemberRuntimeSummary(member, undefined, spawnEntry)).toBeUndefined();
|
|
});
|
|
|
|
it('uses the live runtime model as a fallback when config has no explicit model', () => {
|
|
const member = createMember({ providerId: 'codex', model: undefined });
|
|
const spawnEntry = createSpawnEntry({
|
|
status: 'online',
|
|
launchState: 'confirmed_alive',
|
|
runtimeAlive: true,
|
|
runtimeModel: 'gpt-5.4-mini',
|
|
});
|
|
|
|
expect(resolveMemberRuntimeSummary(member, undefined, spawnEntry)).toBe(
|
|
'5.4 Mini · Medium · Codex'
|
|
);
|
|
});
|
|
|
|
it('appends runtime memory when a live process snapshot is available', () => {
|
|
const member = createMember({ model: 'gpt-5.4-mini' });
|
|
const runtimeEntry = {
|
|
memberName: 'alice',
|
|
alive: true,
|
|
restartable: true,
|
|
pid: 4242,
|
|
runtimeModel: 'gpt-5.4-mini',
|
|
rssBytes: 256 * 1024 * 1024,
|
|
updatedAt: '2026-04-18T18:00:00.000Z',
|
|
};
|
|
|
|
expect(resolveMemberRuntimeSummary(member, undefined, undefined, runtimeEntry)).toBe(
|
|
'5.4 Mini · Medium · Codex · 256.0 MB'
|
|
);
|
|
});
|
|
|
|
it('appends runtime memory while a configured member is still pending', () => {
|
|
const member = createMember({ model: 'gpt-5.4-mini' });
|
|
const spawnEntry = createSpawnEntry();
|
|
const runtimeEntry = {
|
|
memberName: 'alice',
|
|
alive: true,
|
|
restartable: true,
|
|
pid: 4242,
|
|
rssBytes: 256 * 1024 * 1024,
|
|
updatedAt: '2026-04-18T18:00:00.000Z',
|
|
};
|
|
|
|
expect(resolveMemberRuntimeSummary(member, undefined, spawnEntry, runtimeEntry as never)).toBe(
|
|
'5.4 Mini · Medium · Codex · 256.0 MB'
|
|
);
|
|
});
|
|
|
|
it('hides stale runtime memory when the spawn state is explicitly offline', () => {
|
|
const member = createMember({ model: 'gpt-5.4-mini' });
|
|
const spawnEntry = createSpawnEntry({
|
|
status: 'offline',
|
|
launchState: 'failed_to_start',
|
|
runtimeAlive: false,
|
|
bootstrapConfirmed: false,
|
|
});
|
|
const runtimeEntry = {
|
|
memberName: 'alice',
|
|
alive: true,
|
|
restartable: false,
|
|
providerId: 'opencode',
|
|
pid: 333,
|
|
pidSource: 'opencode_bridge',
|
|
rssBytes: 97.3 * 1024 * 1024,
|
|
updatedAt: '2026-04-24T12:00:00.000Z',
|
|
};
|
|
|
|
expect(resolveMemberRuntimeSummary(member, undefined, spawnEntry, runtimeEntry as never)).toBe(
|
|
'5.4 Mini · Medium · Codex'
|
|
);
|
|
});
|
|
|
|
it('keeps the persisted backend lane visible in the runtime summary', () => {
|
|
const member = createMember({ model: 'gpt-5.4-mini' });
|
|
|
|
expect(
|
|
resolveMemberRuntimeSummary(
|
|
member,
|
|
{
|
|
providerId: 'codex',
|
|
providerBackendId: 'codex-native',
|
|
model: 'gpt-5.4-mini',
|
|
effort: 'medium',
|
|
limitContext: false,
|
|
},
|
|
undefined
|
|
)
|
|
).toBe('5.4 Mini · Medium · Codex');
|
|
});
|
|
|
|
it('uses lead launch params instead of stale persisted lead runtime fields', () => {
|
|
const member = createMember({
|
|
name: 'team-lead',
|
|
agentType: 'team-lead',
|
|
providerId: 'codex',
|
|
providerBackendId: 'codex-native',
|
|
model: 'gpt-5.5',
|
|
effort: 'medium',
|
|
});
|
|
|
|
expect(
|
|
resolveMemberRuntimeSummary(
|
|
member,
|
|
{
|
|
providerId: 'anthropic',
|
|
providerBackendId: undefined,
|
|
model: 'haiku',
|
|
effort: undefined,
|
|
limitContext: false,
|
|
},
|
|
undefined
|
|
)
|
|
).toBe('Anthropic · Haiku 4.5');
|
|
});
|
|
|
|
it('uses lead launch params instead of stale pending lead runtime evidence', () => {
|
|
const member = createMember({
|
|
name: 'team-lead',
|
|
agentType: 'team-lead',
|
|
providerId: 'codex',
|
|
providerBackendId: 'codex-native',
|
|
model: 'gpt-5.5',
|
|
effort: 'medium',
|
|
});
|
|
|
|
expect(
|
|
resolveMemberRuntimeSummary(
|
|
member,
|
|
{
|
|
providerId: 'anthropic',
|
|
providerBackendId: undefined,
|
|
model: 'haiku',
|
|
effort: undefined,
|
|
limitContext: false,
|
|
},
|
|
createSpawnEntry({
|
|
runtimeModel: 'gpt-5.5',
|
|
runtimeAlive: true,
|
|
}),
|
|
{
|
|
memberName: 'team-lead',
|
|
alive: true,
|
|
restartable: false,
|
|
providerId: 'codex',
|
|
runtimeModel: 'gpt-5.5',
|
|
rssBytes: 300 * 1024 * 1024,
|
|
updatedAt: '2026-04-18T18:00:00.000Z',
|
|
}
|
|
)
|
|
).toBe('Anthropic · Haiku 4.5');
|
|
});
|
|
|
|
it('uses pending lead launch params instead of stale same-provider runtime model evidence', () => {
|
|
const member = createMember({
|
|
name: 'team-lead',
|
|
agentType: 'team-lead',
|
|
providerId: 'codex',
|
|
providerBackendId: 'codex-native',
|
|
model: 'gpt-5.5',
|
|
effort: 'medium',
|
|
});
|
|
|
|
expect(
|
|
resolveMemberRuntimeSummary(
|
|
member,
|
|
{
|
|
providerId: 'codex',
|
|
providerBackendId: 'codex-native',
|
|
model: 'gpt-5.4',
|
|
effort: 'high',
|
|
limitContext: false,
|
|
},
|
|
createSpawnEntry({
|
|
runtimeModel: 'gpt-5.5',
|
|
runtimeAlive: true,
|
|
}),
|
|
{
|
|
memberName: 'team-lead',
|
|
alive: true,
|
|
restartable: false,
|
|
providerId: 'codex',
|
|
runtimeModel: 'gpt-5.5',
|
|
rssBytes: 300 * 1024 * 1024,
|
|
updatedAt: '2026-04-18T18:00:00.000Z',
|
|
}
|
|
)
|
|
).toBe('5.4 · High · Codex');
|
|
});
|
|
|
|
it('uses pending lead default launch params instead of stale same-provider runtime model evidence', () => {
|
|
const member = createMember({
|
|
name: 'team-lead',
|
|
agentType: 'team-lead',
|
|
providerId: 'codex',
|
|
providerBackendId: 'codex-native',
|
|
model: 'gpt-5.5',
|
|
effort: 'medium',
|
|
});
|
|
|
|
expect(
|
|
resolveMemberRuntimeSummary(
|
|
member,
|
|
{
|
|
providerId: 'codex',
|
|
providerBackendId: 'codex-native',
|
|
model: undefined,
|
|
effort: 'low',
|
|
limitContext: false,
|
|
},
|
|
createSpawnEntry({
|
|
runtimeModel: 'gpt-5.5',
|
|
runtimeAlive: true,
|
|
}),
|
|
{
|
|
memberName: 'team-lead',
|
|
alive: true,
|
|
restartable: false,
|
|
providerId: 'codex',
|
|
runtimeModel: 'gpt-5.5',
|
|
rssBytes: 300 * 1024 * 1024,
|
|
updatedAt: '2026-04-18T18:00:00.000Z',
|
|
}
|
|
)
|
|
).toBe('Codex · Default · Low');
|
|
});
|
|
|
|
it('uses staged default launch params without duplicating the Codex backend label', () => {
|
|
const member = createMember({
|
|
name: 'team-lead',
|
|
agentType: 'team-lead',
|
|
providerId: 'codex',
|
|
providerBackendId: 'codex-native',
|
|
model: 'gpt-5.5',
|
|
effort: 'medium',
|
|
});
|
|
|
|
expect(
|
|
resolveMemberRuntimeSummary(
|
|
member,
|
|
{
|
|
providerId: 'codex',
|
|
providerBackendId: 'codex-native',
|
|
model: 'default',
|
|
effort: 'low',
|
|
limitContext: false,
|
|
},
|
|
createSpawnEntry()
|
|
)
|
|
).toBe('Codex · Default · Low');
|
|
});
|
|
|
|
it('uses pending launch params for stale primary teammate cards during provider switch', () => {
|
|
const member = createMember({
|
|
providerId: 'codex',
|
|
providerBackendId: 'codex-native',
|
|
model: 'gpt-5.5',
|
|
effort: 'medium',
|
|
laneKind: 'primary',
|
|
laneOwnerProviderId: 'codex',
|
|
});
|
|
|
|
expect(
|
|
resolveMemberRuntimeSummary(
|
|
member,
|
|
{
|
|
providerId: 'anthropic',
|
|
providerBackendId: undefined,
|
|
model: 'haiku',
|
|
effort: 'low',
|
|
limitContext: false,
|
|
},
|
|
createSpawnEntry({
|
|
runtimeModel: 'gpt-5.5',
|
|
runtimeAlive: true,
|
|
}),
|
|
{
|
|
memberName: 'alice',
|
|
alive: true,
|
|
restartable: false,
|
|
providerId: 'codex',
|
|
runtimeModel: 'gpt-5.5',
|
|
rssBytes: 221 * 1024 * 1024,
|
|
updatedAt: '2026-04-18T18:00:00.000Z',
|
|
}
|
|
)
|
|
).toBe('Anthropic · Haiku 4.5 · Low');
|
|
});
|
|
|
|
it('normalizes persisted legacy Codex lanes to the native runtime summary', () => {
|
|
const member = createMember({ model: 'gpt-5.4-mini' });
|
|
|
|
expect(
|
|
resolveMemberRuntimeSummary(
|
|
member,
|
|
{
|
|
providerId: 'codex',
|
|
providerBackendId: 'api',
|
|
model: 'gpt-5.4-mini',
|
|
effort: 'medium',
|
|
limitContext: false,
|
|
},
|
|
undefined
|
|
)
|
|
).toBe('5.4 Mini · Medium · Codex');
|
|
});
|
|
|
|
it('does not leak the lead backend label into OpenCode side-lane members', () => {
|
|
const member = createMember({
|
|
providerId: 'opencode',
|
|
providerBackendId: undefined,
|
|
model: 'opencode/nemotron-3-super-free',
|
|
effort: undefined,
|
|
});
|
|
|
|
expect(
|
|
resolveMemberRuntimeSummary(
|
|
member,
|
|
{
|
|
providerId: 'codex',
|
|
providerBackendId: 'codex-native',
|
|
model: 'gpt-5.4',
|
|
effort: 'medium',
|
|
limitContext: false,
|
|
},
|
|
undefined
|
|
)
|
|
).toBe('nemotron-3-super-free · via OpenCode');
|
|
});
|
|
|
|
it('infers OpenCode from an OpenCode model when member provider metadata is missing', () => {
|
|
const member = createMember({
|
|
providerId: undefined,
|
|
providerBackendId: undefined,
|
|
model: 'opencode/minimax-m2.5-free',
|
|
effort: undefined,
|
|
});
|
|
|
|
expect(
|
|
resolveMemberRuntimeSummary(
|
|
member,
|
|
{
|
|
providerId: 'codex',
|
|
providerBackendId: 'codex-native',
|
|
model: 'gpt-5.4',
|
|
effort: 'medium',
|
|
limitContext: false,
|
|
},
|
|
undefined
|
|
)
|
|
).toBe('minimax-m2.5-free · via OpenCode');
|
|
});
|
|
|
|
it('appends memory for OpenCode side-lane runtime snapshots without adding Codex backend text', () => {
|
|
const member = createMember({
|
|
providerId: 'opencode',
|
|
providerBackendId: undefined,
|
|
model: 'opencode/minimax-m2.5-free',
|
|
effort: undefined,
|
|
});
|
|
|
|
expect(
|
|
resolveMemberRuntimeSummary(
|
|
member,
|
|
{
|
|
providerId: 'codex',
|
|
providerBackendId: 'codex-native',
|
|
model: 'gpt-5.4',
|
|
effort: 'medium',
|
|
limitContext: false,
|
|
},
|
|
undefined,
|
|
{
|
|
memberName: 'alice',
|
|
alive: true,
|
|
restartable: false,
|
|
runtimeModel: 'opencode/minimax-m2.5-free',
|
|
rssBytes: 183.9 * 1024 * 1024,
|
|
updatedAt: '2026-04-18T18:00:00.000Z',
|
|
}
|
|
)
|
|
).toBe('minimax-m2.5-free · via OpenCode · 183.9 MB');
|
|
});
|
|
});
|
|
|
|
describe('getRuntimeMemorySourceLabel', () => {
|
|
it('explains when RSS comes from a tmux pane shell', () => {
|
|
expect(
|
|
getRuntimeMemorySourceLabel({
|
|
memberName: 'alice',
|
|
alive: false,
|
|
restartable: true,
|
|
pid: 26676,
|
|
pidSource: 'tmux_pane',
|
|
rssBytes: 2 * 1024 * 1024,
|
|
updatedAt: '2026-04-24T12:00:00.000Z',
|
|
})
|
|
).toBe('RSS source: tmux pane shell');
|
|
});
|
|
|
|
it('explains shared OpenCode host memory separately from member-owned runtime memory', () => {
|
|
expect(
|
|
getRuntimeMemorySourceLabel({
|
|
memberName: 'alice',
|
|
alive: true,
|
|
restartable: false,
|
|
providerId: 'opencode',
|
|
pid: 333,
|
|
pidSource: 'opencode_bridge',
|
|
rssBytes: 183.9 * 1024 * 1024,
|
|
updatedAt: '2026-04-24T12:00:00.000Z',
|
|
})
|
|
).toBe('RSS source: shared OpenCode host');
|
|
});
|
|
|
|
it('labels verified runtime child memory as runtime process memory', () => {
|
|
expect(
|
|
getRuntimeMemorySourceLabel({
|
|
memberName: 'alice',
|
|
alive: true,
|
|
restartable: true,
|
|
pid: 4242,
|
|
pidSource: 'tmux_child',
|
|
rssBytes: 256 * 1024 * 1024,
|
|
updatedAt: '2026-04-24T12:00:00.000Z',
|
|
})
|
|
).toBe('RSS source: runtime process');
|
|
});
|
|
});
|