agent-ecosystem/test/renderer/features/agent-graph/collapseOverflowStacks.test.ts

108 lines
3.8 KiB
TypeScript

import { type GraphNode, TASK_COLUMN_MAX_VISIBLE_ROWS } from '@claude-teams/agent-graph';
import {
collapseOverflowStacks,
collapseOverflowStacksWithMeta,
} from '@features/agent-graph/core/domain/collapseOverflowStacks';
import { describe, expect, it } from 'vitest';
function makeTaskNode(taskId: string, ownerName: string | null = 'alice'): GraphNode {
return {
id: `task:my-team:${taskId}`,
kind: 'task',
label: `#${taskId}`,
displayId: `#${taskId}`,
sublabel: `Task ${taskId}`,
state: 'waiting',
taskStatus: 'pending',
reviewState: 'none',
ownerId: ownerName ? `member:my-team:${ownerName}` : null,
domainRef: { kind: 'task', teamName: 'my-team', taskId },
};
}
describe('collapseOverflowStacks', () => {
it('keeps all tasks visible when the column fits within the max row count', () => {
const nodes = Array.from({ length: 6 }, (_, index) => makeTaskNode(`task-${index + 1}`));
const result = collapseOverflowStacks(nodes, 'my-team', 6);
expect(result).toHaveLength(6);
expect(result.every((node) => !node.isOverflowStack)).toBe(true);
});
it('replaces the hidden tail with a single overflow stack node while preserving visible order', () => {
const nodes = Array.from({ length: 8 }, (_, index) => makeTaskNode(`task-${index + 1}`));
const result = collapseOverflowStacks(nodes, 'my-team', 6);
expect(result).toHaveLength(7);
expect(result.slice(0, 6).map((node) => node.domainRef.kind === 'task' && node.domainRef.taskId)).toEqual([
'task-1',
'task-2',
'task-3',
'task-4',
'task-5',
'task-6',
]);
expect(result[6]).toMatchObject({
isOverflowStack: true,
overflowCount: 2,
overflowTaskIds: ['task-7', 'task-8'],
domainRef: {
kind: 'task_overflow',
teamName: 'my-team',
ownerMemberName: 'alice',
columnKey: 'todo',
},
});
});
it('uses the graph column row budget so task columns stay inside the stable slot', () => {
const nodes = Array.from({ length: 5 }, (_, index) => makeTaskNode(`task-${index + 1}`));
const result = collapseOverflowStacks(nodes, 'my-team', TASK_COLUMN_MAX_VISIBLE_ROWS);
expect(result).toHaveLength(TASK_COLUMN_MAX_VISIBLE_ROWS + 1);
expect(
result.slice(0, 3).map((node) => node.domainRef.kind === 'task' && node.domainRef.taskId)
).toEqual(['task-1', 'task-2', 'task-3']);
expect(result[3]).toMatchObject({
isOverflowStack: true,
label: '+2 more',
overflowCount: 2,
overflowTaskIds: ['task-4', 'task-5'],
});
});
it('applies the same stack rules to unassigned task columns', () => {
const nodes = Array.from({ length: 8 }, (_, index) => makeTaskNode(`task-${index + 1}`, null));
const result = collapseOverflowStacks(nodes, 'my-team', 6);
const stack = result.find((node) => node.isOverflowStack);
expect(stack).toMatchObject({
overflowCount: 2,
overflowTaskIds: ['task-7', 'task-8'],
ownerId: null,
domainRef: {
kind: 'task_overflow',
teamName: 'my-team',
ownerMemberName: null,
columnKey: 'todo',
},
});
});
it('returns a visible-node mapping for hidden tasks behind the stack', () => {
const nodes = Array.from({ length: 8 }, (_, index) => makeTaskNode(`task-${index + 1}`));
const result = collapseOverflowStacksWithMeta(nodes, 'my-team', 6);
const stackNode = result.visibleNodes.find((node) => node.isOverflowStack);
expect(stackNode).toBeDefined();
expect(result.visibleNodeIdByTaskId.get('task-1')).toBe('task:my-team:task-1');
expect(result.visibleNodeIdByTaskId.get('task-6')).toBe('task:my-team:task-6');
expect(result.visibleNodeIdByTaskId.get('task-7')).toBe(stackNode?.id);
expect(result.visibleNodeIdByTaskId.get('task-8')).toBe(stackNode?.id);
});
});