agent-ecosystem/test/main/services/team/BoardTaskActivityEntryBuilder.test.ts

427 lines
13 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { BoardTaskActivityEntryBuilder } from '../../../../src/main/services/team/taskLogs/activity/BoardTaskActivityEntryBuilder';
import type { TeamTask } from '../../../../src/shared/types/team';
import type { RawTaskActivityMessage } from '../../../../src/main/services/team/taskLogs/activity/BoardTaskActivityTranscriptReader';
function makeTask(task: Partial<TeamTask> & Pick<TeamTask, 'id' | 'subject' | 'status'>): TeamTask {
return {
displayId: task.displayId ?? task.id.slice(0, 8),
createdAt: '2026-04-12T10:00:00.000Z',
updatedAt: '2026-04-12T10:00:00.000Z',
...task,
};
}
describe('BoardTaskActivityEntryBuilder', () => {
it('builds same-task execution rows and external board actions', () => {
const taskA = makeTask({
id: '123e4567-e89b-12d3-a456-426614174000',
displayId: 'abcd1234',
subject: 'Task A',
status: 'in_progress',
});
const taskB = makeTask({
id: '123e4567-e89b-12d3-a456-426614174001',
displayId: 'efgh5678',
subject: 'Task B',
status: 'pending',
});
const messages: RawTaskActivityMessage[] = [
{
filePath: '/tmp/a.jsonl',
uuid: 'msg-1',
timestamp: '2026-04-12T10:00:00.000Z',
sessionId: 'session-1',
agentId: 'agent-a',
agentName: 'alice',
isSidechain: true,
sourceOrder: 1,
boardTaskLinks: [
{
schemaVersion: 1,
task: { ref: 'abcd1234', refKind: 'display', canonicalId: taskA.id },
targetRole: 'subject',
linkKind: 'execution',
actorContext: { relation: 'same_task' },
},
],
boardTaskToolActions: [],
},
{
filePath: '/tmp/b.jsonl',
uuid: 'msg-2',
timestamp: '2026-04-12T10:01:00.000Z',
sessionId: 'session-1',
agentId: 'agent-a',
agentName: 'alice',
isSidechain: true,
sourceOrder: 2,
boardTaskLinks: [
{
schemaVersion: 1,
toolUseId: 'tool-2',
task: { ref: 'abcd1234', refKind: 'display', canonicalId: taskA.id },
targetRole: 'subject',
linkKind: 'board_action',
actorContext: {
relation: 'other_active_task',
activeTask: { ref: 'efgh5678', refKind: 'display', canonicalId: taskB.id },
activePhase: 'work',
activeExecutionSeq: 2,
},
},
],
boardTaskToolActions: [
{
schemaVersion: 1,
toolUseId: 'tool-2',
canonicalToolName: 'task_add_comment',
resultRefs: { commentId: 'comment-1' },
},
],
},
];
const entries = new BoardTaskActivityEntryBuilder().buildForTask({
teamName: 'demo',
targetTask: taskA,
tasks: [taskA, taskB],
messages,
});
expect(entries).toHaveLength(2);
expect(entries[0]?.linkKind).toBe('execution');
expect(entries[1]?.actorContext.relation).toBe('other_active_task');
expect(entries[1]?.action?.canonicalToolName).toBe('task_add_comment');
expect(entries[1]?.action?.category).toBe('comment');
expect(entries[1]?.action?.details?.commentId).toBe('comment-1');
});
it('marks display-id collisions as ambiguous instead of guessing', () => {
const liveTask = makeTask({
id: '123e4567-e89b-12d3-a456-426614174000',
displayId: 'abcd1234',
subject: 'Live task',
status: 'in_progress',
});
const deletedTask = makeTask({
id: '123e4567-e89b-12d3-a456-426614174099',
displayId: 'abcd1234',
subject: 'Deleted task',
status: 'deleted',
});
const messages: RawTaskActivityMessage[] = [
{
filePath: '/tmp/a.jsonl',
uuid: 'msg-1',
timestamp: '2026-04-12T10:00:00.000Z',
sessionId: 'session-1',
isSidechain: true,
sourceOrder: 1,
boardTaskLinks: [
{
schemaVersion: 1,
task: { ref: 'abcd1234', refKind: 'display' },
targetRole: 'subject',
linkKind: 'board_action',
actorContext: { relation: 'idle' },
},
],
boardTaskToolActions: [],
},
];
const entries = new BoardTaskActivityEntryBuilder().buildForTask({
teamName: 'demo',
targetTask: liveTask,
tasks: [liveTask, deletedTask],
messages,
});
expect(entries).toHaveLength(1);
expect(entries[0]?.task.resolution).toBe('ambiguous');
});
it('preserves deleted peer tasks on relationship rows', () => {
const taskA = makeTask({
id: '123e4567-e89b-12d3-a456-426614174000',
displayId: 'abcd1234',
subject: 'Task A',
status: 'in_progress',
});
const deletedPeer = makeTask({
id: '123e4567-e89b-12d3-a456-426614174002',
displayId: 'ijkl9012',
subject: 'Task B',
status: 'deleted',
});
const messages: RawTaskActivityMessage[] = [
{
filePath: '/tmp/relationships.jsonl',
uuid: 'msg-3',
timestamp: '2026-04-12T10:00:00.000Z',
sessionId: 'session-1',
agentName: 'lead',
isSidechain: false,
sourceOrder: 1,
boardTaskLinks: [
{
schemaVersion: 1,
toolUseId: 'tool-3',
task: { ref: 'abcd1234', refKind: 'display', canonicalId: taskA.id },
targetRole: 'subject',
linkKind: 'board_action',
actorContext: { relation: 'idle' },
},
{
schemaVersion: 1,
toolUseId: 'tool-3',
task: { ref: 'ijkl9012', refKind: 'display', canonicalId: deletedPeer.id },
targetRole: 'related',
linkKind: 'board_action',
actorContext: { relation: 'idle' },
},
],
boardTaskToolActions: [
{
schemaVersion: 1,
toolUseId: 'tool-3',
canonicalToolName: 'task_link',
input: { relationship: 'related' },
},
],
},
];
const entries = new BoardTaskActivityEntryBuilder().buildForTask({
teamName: 'demo',
targetTask: taskA,
tasks: [taskA, deletedPeer],
messages,
});
expect(entries).toHaveLength(1);
expect(entries[0]?.action?.peerTask?.resolution).toBe('deleted');
expect(entries[0]?.action?.details?.relationship).toBe('related');
expect(entries[0]?.action?.category).toBe('relationship');
expect(entries[0]?.action?.relationshipPerspective).toBe('symmetric');
});
it('resolves display locators case-insensitively and canonical-like unknown refs safely', () => {
const taskA = makeTask({
id: '123e4567-e89b-12d3-a456-426614174000',
displayId: 'abcd1234',
subject: 'Task A',
status: 'in_progress',
});
const messages: RawTaskActivityMessage[] = [
{
filePath: '/tmp/case.jsonl',
uuid: 'msg-4',
timestamp: '2026-04-12T10:00:00.000Z',
sessionId: 'session-1',
isSidechain: false,
sourceOrder: 1,
boardTaskLinks: [
{
schemaVersion: 1,
task: { ref: 'ABCD1234', refKind: 'display' },
targetRole: 'subject',
linkKind: 'board_action',
actorContext: { relation: 'idle' },
},
{
schemaVersion: 1,
task: { ref: taskA.id, refKind: 'unknown' },
targetRole: 'subject',
linkKind: 'execution',
actorContext: { relation: 'same_task' },
},
],
boardTaskToolActions: [],
},
];
const entries = new BoardTaskActivityEntryBuilder().buildForTask({
teamName: 'demo',
targetTask: taskA,
tasks: [taskA],
messages,
});
expect(entries).toHaveLength(2);
expect(entries[0]?.task.resolution).toBe('resolved');
expect(entries[1]?.task.resolution).toBe('resolved');
});
it('marks main-session actor without explicit name as unknown instead of forcing lead', () => {
const taskA = makeTask({
id: '123e4567-e89b-12d3-a456-426614174000',
displayId: 'abcd1234',
subject: 'Task A',
status: 'in_progress',
});
const messages: RawTaskActivityMessage[] = [
{
filePath: '/tmp/unknown-actor.jsonl',
uuid: 'msg-5',
timestamp: '2026-04-12T10:00:00.000Z',
sessionId: 'session-1',
isSidechain: false,
sourceOrder: 1,
boardTaskLinks: [
{
schemaVersion: 1,
task: { ref: 'abcd1234', refKind: 'display', canonicalId: taskA.id },
targetRole: 'subject',
linkKind: 'board_action',
actorContext: { relation: 'idle' },
},
],
boardTaskToolActions: [],
},
];
const entries = new BoardTaskActivityEntryBuilder().buildForTask({
teamName: 'demo',
targetTask: taskA,
tasks: [taskA],
messages,
});
expect(entries).toHaveLength(1);
expect(entries[0]?.actor.role).toBe('unknown');
});
it('never joins action payloads onto execution rows', () => {
const taskA = makeTask({
id: '123e4567-e89b-12d3-a456-426614174000',
displayId: 'abcd1234',
subject: 'Task A',
status: 'in_progress',
});
const messages: RawTaskActivityMessage[] = [
{
filePath: '/tmp/execution-malformed.jsonl',
uuid: 'msg-6',
timestamp: '2026-04-12T10:00:00.000Z',
sessionId: 'session-1',
agentId: 'agent-a',
agentName: 'alice',
isSidechain: true,
sourceOrder: 1,
boardTaskLinks: [
{
schemaVersion: 1,
toolUseId: 'tool-1',
task: { ref: 'abcd1234', refKind: 'display', canonicalId: taskA.id },
targetRole: 'subject',
linkKind: 'execution',
actorContext: { relation: 'same_task' },
},
],
boardTaskToolActions: [
{
schemaVersion: 1,
toolUseId: 'tool-1',
canonicalToolName: 'task_start',
},
],
},
];
const entries = new BoardTaskActivityEntryBuilder().buildForTask({
teamName: 'demo',
targetTask: taskA,
tasks: [taskA],
messages,
});
expect(entries).toHaveLength(1);
expect(entries[0]?.linkKind).toBe('execution');
expect(entries[0]?.action).toBeUndefined();
});
it('derives relationship perspective from target role', () => {
const taskA = makeTask({
id: '123e4567-e89b-12d3-a456-426614174010',
displayId: 'taska010',
subject: 'Task A',
status: 'in_progress',
});
const taskB = makeTask({
id: '123e4567-e89b-12d3-a456-426614174011',
displayId: 'taskb011',
subject: 'Task B',
status: 'pending',
});
const messages: RawTaskActivityMessage[] = [
{
filePath: '/tmp/relationship-perspective.jsonl',
uuid: 'msg-7',
timestamp: '2026-04-12T10:00:00.000Z',
sessionId: 'session-1',
agentName: 'lead',
isSidechain: false,
sourceOrder: 1,
boardTaskLinks: [
{
schemaVersion: 1,
toolUseId: 'tool-7',
task: { ref: 'taska010', refKind: 'display', canonicalId: taskA.id },
targetRole: 'subject',
linkKind: 'board_action',
actorContext: { relation: 'idle' },
},
{
schemaVersion: 1,
toolUseId: 'tool-7',
task: { ref: 'taskb011', refKind: 'display', canonicalId: taskB.id },
targetRole: 'related',
linkKind: 'board_action',
actorContext: { relation: 'idle' },
},
],
boardTaskToolActions: [
{
schemaVersion: 1,
toolUseId: 'tool-7',
canonicalToolName: 'task_link',
input: { relationship: 'blocked-by' },
},
],
},
];
const builder = new BoardTaskActivityEntryBuilder();
const entriesForTaskA = builder.buildForTask({
teamName: 'demo',
targetTask: taskA,
tasks: [taskA, taskB],
messages,
});
const entriesForTaskB = builder.buildForTask({
teamName: 'demo',
targetTask: taskB,
tasks: [taskA, taskB],
messages,
});
expect(entriesForTaskA).toHaveLength(1);
expect(entriesForTaskA[0]?.action?.relationshipPerspective).toBe('incoming');
expect(entriesForTaskA[0]?.action?.peerTask?.taskRef?.taskId).toBe(taskB.id);
expect(entriesForTaskB).toHaveLength(1);
expect(entriesForTaskB[0]?.action?.relationshipPerspective).toBe('outgoing');
expect(entriesForTaskB[0]?.action?.peerTask?.taskRef?.taskId).toBe(taskA.id);
});
});