agent-ecosystem/test/renderer/store/teamResolvedMembers.test.ts

210 lines
6.6 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
clearResolvedMemberSelectorCaches,
getResolvedMemberSelectorCacheSnapshotForTeam,
selectResolvedMemberForTeamName,
selectResolvedMembersForTeamName,
shouldPreserveSelectedTeamSnapshot,
} from '../../../src/renderer/store/team/teamResolvedMembers';
import type {
TeamMemberActivityMeta,
TeamMemberSnapshot,
TeamSummary,
TeamTask,
TeamViewSnapshot,
} from '../../../src/shared/types';
function createTask(overrides: Partial<TeamTask> = {}): TeamTask {
return {
id: 'task-1',
subject: 'Task',
owner: 'alice',
status: 'in_progress',
...overrides,
};
}
function createSnapshot(overrides: Partial<TeamViewSnapshot> = {}): TeamViewSnapshot {
return {
teamName: 'my-team',
config: { name: 'My Team' },
tasks: [],
members: [],
kanbanState: { teamName: 'my-team', reviewers: [], tasks: {} },
processes: [],
...overrides,
} as TeamViewSnapshot;
}
function createState(
snapshot: TeamViewSnapshot,
options: {
summary?: TeamSummary;
meta?: TeamMemberActivityMeta;
} = {}
) {
return {
selectedTeamName: snapshot.teamName,
selectedTeamData: snapshot,
teamDataCacheByName: { [snapshot.teamName]: snapshot },
memberActivityMetaByTeam: options.meta ? { [snapshot.teamName]: options.meta } : {},
teamByName: options.summary ? { [snapshot.teamName]: options.summary } : {},
};
}
describe('teamResolvedMembers', () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-04-17T12:00:00.000Z'));
clearResolvedMemberSelectorCaches();
});
afterEach(() => {
vi.useRealTimers();
clearResolvedMemberSelectorCaches();
});
it('builds config fallback members when runtime snapshots are empty', () => {
const snapshot = createSnapshot({
config: {
name: 'My Team',
members: [
{ name: 'team-lead', role: 'Lead' },
{ name: 'alice', agentId: 'agent-a', role: 'Engineer' },
{ name: 'Alice', agentId: 'duplicate' },
],
},
tasks: [
createTask({ id: 'task-active', owner: 'alice', status: 'in_progress' }),
createTask({ id: 'task-done', owner: 'alice', status: 'completed' }),
],
});
const members = selectResolvedMembersForTeamName(createState(snapshot), 'my-team');
expect(members.map((member) => member.name)).toEqual(['team-lead', 'alice']);
expect(members[1]).toMatchObject({
name: 'alice',
agentId: 'agent-a',
currentTaskId: 'task-active',
taskCount: 2,
role: 'Engineer',
status: 'active',
messageCount: 0,
lastActiveAt: null,
});
});
it('builds summary fallback members with a lead when config and runtime snapshots are empty', () => {
const snapshot = createSnapshot();
const summary = {
teamName: 'my-team',
displayName: 'My Team',
memberCount: 2,
taskCount: 0,
lastActivity: null,
leadName: 'lead-one',
leadColor: '#fff',
members: [
{ name: 'lead-one', role: 'Lead' },
{ name: 'bob', agentId: 'agent-b', role: 'Reviewer', color: '#123456' },
{ name: 'Bob', agentId: 'duplicate' },
],
} as TeamSummary;
const members = selectResolvedMembersForTeamName(createState(snapshot, { summary }), 'my-team');
expect(members.map((member) => member.name)).toEqual(['lead-one', 'bob']);
expect(members[0]).toMatchObject({ agentType: 'team-lead', role: 'Team Lead' });
expect(members[1]).toMatchObject({
agentId: 'agent-b',
role: 'Reviewer',
color: '#123456',
});
});
it('memoizes selector results until resolved-member cache is cleared', () => {
const snapshot = createSnapshot({
members: [{ name: 'alice', currentTaskId: null, taskCount: 0 } as TeamMemberSnapshot],
});
const state = createState(snapshot);
const firstMembers = selectResolvedMembersForTeamName(state, 'my-team');
const secondMembers = selectResolvedMembersForTeamName(state, 'my-team');
const firstAlice = selectResolvedMemberForTeamName(state, 'my-team', 'alice');
const secondAlice = selectResolvedMemberForTeamName(state, 'my-team', 'alice');
expect(secondMembers).toBe(firstMembers);
expect(secondAlice).toBe(firstAlice);
expect(getResolvedMemberSelectorCacheSnapshotForTeam('my-team')).toEqual({
hasResolvedMembersSelector: true,
resolvedMemberSelectorCount: 1,
});
clearResolvedMemberSelectorCaches();
expect(selectResolvedMembersForTeamName(state, 'my-team')).not.toBe(firstMembers);
expect(getResolvedMemberSelectorCacheSnapshotForTeam('my-team')).toEqual({
hasResolvedMembersSelector: true,
resolvedMemberSelectorCount: 0,
});
});
it('derives activity status from member activity metadata', () => {
const snapshot = createSnapshot({
members: [{ name: 'alice', currentTaskId: null, taskCount: 0 } as TeamMemberSnapshot],
});
const meta = {
teamName: 'my-team',
feedRevision: 'rev-1',
computedAt: '2026-04-17T12:00:00.000Z',
members: {
alice: {
memberName: 'alice',
lastAuthoredMessageAt: '2026-04-17T11:57:00.000Z',
messageCountExact: 3,
latestAuthoredMessageSignalsTermination: false,
},
},
} as TeamMemberActivityMeta;
expect(selectResolvedMemberForTeamName(createState(snapshot, { meta }), 'my-team', 'alice'))
.toMatchObject({
status: 'active',
messageCount: 3,
lastActiveAt: '2026-04-17T11:57:00.000Z',
});
});
it('preserves the selected snapshot when an incoming empty snapshot is confirmed by summary', () => {
const current = createSnapshot({
members: [{ name: 'alice', currentTaskId: null, taskCount: 0 } as TeamMemberSnapshot],
});
const incoming = createSnapshot({ members: [], config: { name: 'My Team' } });
const summary = {
teamName: 'my-team',
displayName: 'My Team',
memberCount: 1,
expectedMemberCount: 1,
taskCount: 0,
lastActivity: null,
members: [{ name: 'alice' }],
} as TeamSummary;
expect(shouldPreserveSelectedTeamSnapshot(current, current, incoming, summary)).toBe(true);
});
it('does not preserve the selected snapshot when incoming data has a config roster', () => {
const current = createSnapshot({
members: [{ name: 'alice', currentTaskId: null, taskCount: 0 } as TeamMemberSnapshot],
});
const incoming = createSnapshot({
members: [],
config: { name: 'My Team', members: [{ name: 'bob' }] },
});
expect(shouldPreserveSelectedTeamSnapshot(current, current, incoming, undefined)).toBe(false);
});
});