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

185 lines
4.7 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import type { GraphEdge, GraphNode, GraphParticle } from '@claude-teams/agent-graph';
import {
createTransientHandoffState,
selectRenderableTransientHandoffCards,
updateTransientHandoffState,
} from '../../../../packages/agent-graph/src/ui/transientHandoffs';
const leadNode: GraphNode = {
id: 'lead:team-a',
kind: 'lead',
label: 'team-a',
state: 'active',
x: 0,
y: 0,
domainRef: { kind: 'lead', teamName: 'team-a', memberName: 'team-lead' },
};
const aliceNode: GraphNode = {
id: 'member:team-a:alice',
kind: 'member',
label: 'alice',
state: 'active',
x: 100,
y: 0,
domainRef: { kind: 'member', teamName: 'team-a', memberName: 'alice' },
};
const taskNode: GraphNode = {
id: 'task:team-a:42',
kind: 'task',
label: '#42',
sublabel: 'Fix queue',
state: 'active',
x: 200,
y: 100,
domainRef: { kind: 'task', teamName: 'team-a', taskId: '42' },
};
const nodeMap = new Map<string, GraphNode>([
[leadNode.id, leadNode],
[aliceNode.id, aliceNode],
[taskNode.id, taskNode],
]);
const edgeMap = new Map<string, GraphEdge>([
[
'edge:lead:alice',
{
id: 'edge:lead:alice',
source: leadNode.id,
target: aliceNode.id,
type: 'parent-child',
},
],
[
'edge:alice:task',
{
id: 'edge:alice:task',
source: aliceNode.id,
target: taskNode.id,
type: 'message',
},
],
]);
function makeParticle(overrides?: Partial<GraphParticle>): GraphParticle {
return {
id: 'particle-1',
edgeId: 'edge:lead:alice',
progress: 0.7,
kind: 'inbox_message',
color: '#66ccff',
label: '✉ Ship the patch after green CI',
preview: 'Ship the patch after green CI and send the changelog',
reverse: true,
...overrides,
};
}
describe('transient handoff cards', () => {
it('creates one readable handoff card when a particle reaches the recipient zone', () => {
const state = createTransientHandoffState();
updateTransientHandoffState(state, {
particles: [makeParticle()],
edgeMap,
nodeMap,
time: 10,
});
const cards = selectRenderableTransientHandoffCards(state);
expect(cards).toHaveLength(1);
expect(cards[0]).toMatchObject({
edgeId: 'edge:lead:alice',
sourceNodeId: aliceNode.id,
destinationNodeId: leadNode.id,
kind: 'inbox_message',
count: 1,
preview: 'Ship the patch after green CI and send the changelog',
});
});
it('aggregates repeated sends on the same edge and keeps the latest preview', () => {
const state = createTransientHandoffState();
updateTransientHandoffState(state, {
particles: [makeParticle({ id: 'particle-1' })],
edgeMap,
nodeMap,
time: 20,
});
updateTransientHandoffState(state, {
particles: [
makeParticle({
id: 'particle-2',
label: '✉ Follow-up with the release note diff',
preview: 'Follow-up with the release note diff and deployment checklist',
}),
],
edgeMap,
nodeMap,
time: 21,
});
const cards = selectRenderableTransientHandoffCards(state);
expect(cards).toHaveLength(1);
expect(cards[0]).toMatchObject({
count: 2,
preview: 'Follow-up with the release note diff and deployment checklist',
});
});
it('expires old cards and caps renderables per destination', () => {
const state = createTransientHandoffState();
updateTransientHandoffState(state, {
particles: [
makeParticle({ id: 'comment-1', edgeId: 'edge:alice:task', kind: 'task_comment', reverse: false }),
makeParticle({ id: 'comment-2', edgeId: 'edge:alice:task', kind: 'task_comment', reverse: false }),
makeParticle({ id: 'comment-3', edgeId: 'edge:alice:task', kind: 'review_request', reverse: false }),
],
edgeMap,
nodeMap,
time: 30,
});
const cards = selectRenderableTransientHandoffCards(state);
expect(cards).toHaveLength(2);
expect(new Set(cards.map((card) => card.kind))).toEqual(
new Set(['task_comment', 'review_request'])
);
updateTransientHandoffState(state, {
particles: [],
edgeMap,
nodeMap,
time: 34,
});
expect(selectRenderableTransientHandoffCards(state)).toHaveLength(0);
});
it('does not create a card for generic idle inbox noise', () => {
const state = createTransientHandoffState();
updateTransientHandoffState(state, {
particles: [
makeParticle({
id: 'idle-1',
label: 'idle',
preview: 'idle',
}),
],
edgeMap,
nodeMap,
time: 40,
});
expect(selectRenderableTransientHandoffCards(state)).toHaveLength(0);
});
});