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

596 lines
18 KiB
TypeScript

import * as os from 'os';
import * as path from 'path';
import { afterEach, describe, expect, it } from 'vitest';
import * as fs from 'fs/promises';
import { TaskChangeComputer } from '../../../../src/main/services/team/TaskChangeComputer';
async function writeJsonl(filePath: string, entries: object[]): Promise<void> {
await fs.writeFile(
filePath,
entries.map((entry) => JSON.stringify(entry)).join('\n') + '\n',
'utf8'
);
}
function writeToolUse(
toolUseId: string,
filePath: string,
content: string,
timestamp = '2026-03-01T10:00:00.000Z'
): object {
return {
timestamp,
type: 'assistant',
message: {
role: 'assistant',
content: [
{
type: 'tool_use',
id: toolUseId,
name: 'Write',
input: { file_path: filePath, content },
},
],
},
};
}
function metadataOnlyEditToolUse(toolUseId: string, filePath: string): object {
return {
timestamp: '2026-03-01T10:00:00.000Z',
type: 'assistant',
message: {
role: 'assistant',
content: [
{
type: 'tool_use',
id: toolUseId,
name: 'Edit',
input: {
file_path: filePath,
changes: [{ path: filePath, kind: 'update' }],
},
},
],
},
};
}
function metadataOnlyMultiFileEditToolUse(
toolUseId: string,
filePaths: string[],
primaryPath = filePaths[0] ?? ''
): object {
return {
timestamp: '2026-03-01T10:00:00.000Z',
type: 'assistant',
message: {
role: 'assistant',
content: [
{
type: 'tool_use',
id: toolUseId,
name: 'Edit',
input: {
file_path: primaryPath,
changes: filePaths.map((filePath) => ({ path: filePath, kind: 'add' })),
},
},
],
},
};
}
function createNoLogTaskChangeComputer(): TaskChangeComputer {
const logsFinder = {
findLogFileRefsForTask: () => Promise.resolve([]),
};
const boundaryParser = {
parseBoundaries: () =>
Promise.resolve({
boundaries: [],
scopes: [],
isSingleTaskSession: true,
detectedMechanism: 'none' as const,
}),
};
return new TaskChangeComputer(logsFinder as never, boundaryParser as never);
}
describe('TaskChangeComputer', () => {
let tmpDir: string | null = null;
afterEach(async () => {
if (tmpDir) {
await fs.rm(tmpDir, { recursive: true, force: true });
tmpDir = null;
}
});
it('keeps active tasks without logs quiet even when request status is stale', async () => {
const computer = createNoLogTaskChangeComputer();
const result = await computer.computeTaskChanges({
teamName: 'team-a',
taskId: 'task-1',
taskMeta: {
status: 'in_progress',
reviewState: 'none',
},
effectiveOptions: { status: 'completed' },
projectPath: '/repo',
includeDetails: false,
});
expect(result.files).toEqual([]);
expect(result.confidence).toBe('fallback');
expect(result.warnings).toEqual([]);
});
it('keeps newly created pending tasks without logs quiet', async () => {
const computer = createNoLogTaskChangeComputer();
const result = await computer.computeTaskChanges({
teamName: 'team-a',
taskId: 'task-1',
taskMeta: {
status: 'pending',
reviewState: 'none',
},
effectiveOptions: { status: 'completed' },
projectPath: '/repo',
includeDetails: false,
});
expect(result.files).toEqual([]);
expect(result.confidence).toBe('fallback');
expect(result.warnings).toEqual([]);
});
it('warns when completed tasks have no logs even when request status is stale', async () => {
const computer = createNoLogTaskChangeComputer();
const result = await computer.computeTaskChanges({
teamName: 'team-a',
taskId: 'task-1',
taskMeta: {
status: 'completed',
reviewState: 'none',
},
effectiveOptions: { status: 'in_progress' },
projectPath: '/repo',
includeDetails: false,
});
expect(result.files).toEqual([]);
expect(result.confidence).toBe('fallback');
expect(result.warnings).toEqual(['No log files found for this task.']);
});
it('keeps reopened needs-fix tasks quiet even when their base status is completed', async () => {
const computer = createNoLogTaskChangeComputer();
const result = await computer.computeTaskChanges({
teamName: 'team-a',
taskId: 'task-1',
taskMeta: {
status: 'completed',
reviewState: 'needsFix',
},
effectiveOptions: { status: 'completed' },
projectPath: '/repo',
includeDetails: false,
});
expect(result.files).toEqual([]);
expect(result.confidence).toBe('fallback');
expect(result.warnings).toEqual([]);
});
it('shares concurrent JSONL parsing and invalidates when the file changes', async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'task-change-computer-'));
const logPath = path.join(tmpDir, 'agent.jsonl');
await writeJsonl(logPath, [writeToolUse('tool-1', '/repo/src/a.ts', 'export const a = 1;\n')]);
const logsFinder = {
findLogFileRefsForTask: () => Promise.resolve([{ filePath: logPath, memberName: 'alice' }]),
};
const boundaryParser = {
parseBoundaries: () =>
Promise.resolve({
boundaries: [],
scopes: [],
isSingleTaskSession: true,
detectedMechanism: 'none' as const,
}),
};
const computer = new TaskChangeComputer(logsFinder as never, boundaryParser as never);
const input = {
teamName: 'team-a',
taskId: 'task-1',
taskMeta: null,
effectiveOptions: {},
projectPath: '/repo',
includeDetails: false,
};
const [first, second] = await Promise.all([
computer.computeTaskChanges(input),
computer.computeTaskChanges(input),
]);
expect(first.files.map((file) => file.relativePath)).toEqual(['src/a.ts']);
expect(second.files).toEqual(first.files);
await new Promise((resolve) => setTimeout(resolve, 20));
await writeJsonl(logPath, [
writeToolUse('tool-1', '/repo/src/a.ts', 'export const a = 1;\n'),
writeToolUse('tool-2', '/repo/src/b.ts', 'export const b = 2;\n'),
]);
const afterChange = await computer.computeTaskChanges(input);
expect(
afterChange.files
.map((file) => file.relativePath)
.sort((left, right) => left.localeCompare(right))
).toEqual(['src/a.ts', 'src/b.ts']);
});
it('does not pull unrelated log changes into a precise task scope with no file edits', async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'task-change-computer-'));
const leadLogPath = path.join(tmpDir, 'lead.jsonl');
const memberLogPath = path.join(tmpDir, 'alice.jsonl');
await writeJsonl(leadLogPath, [
writeToolUse('lead-write', '/repo/src/unrelated.ts', 'export const unrelated = true;\n'),
]);
await writeJsonl(memberLogPath, []);
const logsFinder = {
findLogFileRefsForTask: () =>
Promise.resolve([
{ filePath: leadLogPath, memberName: 'team-lead' },
{ filePath: memberLogPath, memberName: 'alice' },
]),
};
const boundaryParser = {
parseBoundaries: (filePath: string) =>
Promise.resolve(
filePath === memberLogPath
? {
boundaries: [],
scopes: [
{
taskId: 'task-1',
memberName: '',
startLine: 1,
endLine: 1,
startTimestamp: '2026-03-01T10:00:00.000Z',
endTimestamp: '2026-03-01T10:01:00.000Z',
toolUseIds: [],
filePaths: [],
confidence: { tier: 1, label: 'high', reason: 'Both markers found' },
},
],
isSingleTaskSession: true,
detectedMechanism: 'mcp' as const,
}
: {
boundaries: [],
scopes: [],
isSingleTaskSession: true,
detectedMechanism: 'none' as const,
}
),
};
const computer = new TaskChangeComputer(logsFinder as never, boundaryParser as never);
const result = await computer.computeTaskChanges({
teamName: 'team-a',
taskId: 'task-1',
taskMeta: null,
effectiveOptions: {},
projectPath: '/repo',
includeDetails: true,
});
expect(result.files).toEqual([]);
expect(result.totalFiles).toBe(0);
expect(result.confidence).toBe('high');
});
it('prefers persisted workIntervals over low-confidence complete-only scopes', async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'task-change-computer-'));
const logPath = path.join(tmpDir, 'alice.jsonl');
await writeJsonl(logPath, [
writeToolUse(
'outside-tool',
'/repo/src/outside.ts',
'export const outside = true;\n',
'2026-03-01T09:55:00.000Z'
),
writeToolUse(
'inside-tool',
'/repo/src/inside.ts',
'export const inside = true;\n',
'2026-03-01T10:05:00.000Z'
),
]);
const logsFinder = {
findLogFileRefsForTask: () => Promise.resolve([{ filePath: logPath, memberName: 'alice' }]),
};
const boundaryParser = {
parseBoundaries: () =>
Promise.resolve({
boundaries: [],
scopes: [
{
taskId: 'task-1',
memberName: '',
startLine: 1,
endLine: 2,
startTimestamp: '',
endTimestamp: '2026-03-01T10:06:00.000Z',
toolUseIds: ['outside-tool', 'inside-tool'],
filePaths: ['/repo/src/outside.ts', '/repo/src/inside.ts'],
confidence: {
tier: 3,
label: 'low',
reason: 'Only complete marker found, start assumed at file beginning',
},
},
],
isSingleTaskSession: true,
detectedMechanism: 'mcp' as const,
}),
};
const computer = new TaskChangeComputer(logsFinder as never, boundaryParser as never);
const result = await computer.computeTaskChanges({
teamName: 'team-a',
taskId: 'task-1',
taskMeta: { owner: 'alice', status: 'completed' },
effectiveOptions: {
intervals: [
{
startedAt: '2026-03-01T10:00:00.000Z',
completedAt: '2026-03-01T10:10:00.000Z',
},
],
},
projectPath: '/repo',
includeDetails: true,
});
expect(result.confidence).toBe('medium');
expect(result.warnings).toEqual([
'Task start boundary missing - scoped by persisted workIntervals timestamps.',
]);
expect(result.files.map((file) => file.relativePath)).toEqual(['src/inside.ts']);
expect(result.scope.toolUseIds).toEqual(['inside-tool']);
});
it('does not pull lead-session interval edits into a member complete-only scope', async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'task-change-computer-'));
const leadLogPath = path.join(tmpDir, 'lead.jsonl');
const memberLogPath = path.join(tmpDir, 'alice.jsonl');
await writeJsonl(leadLogPath, [
writeToolUse(
'lead-inside-tool',
'/repo/src/lead.ts',
'export const lead = true;\n',
'2026-03-01T10:05:00.000Z'
),
]);
await writeJsonl(memberLogPath, [
writeToolUse(
'member-inside-tool',
'/repo/src/member.ts',
'export const member = true;\n',
'2026-03-01T10:06:00.000Z'
),
]);
const logsFinder = {
findLogFileRefsForTask: () =>
Promise.resolve([
{ filePath: leadLogPath, memberName: 'team-lead' },
{ filePath: memberLogPath, memberName: 'alice' },
]),
};
const boundaryParser = {
parseBoundaries: (filePath: string) =>
Promise.resolve(
filePath === memberLogPath
? {
boundaries: [],
scopes: [
{
taskId: 'task-1',
memberName: '',
startLine: 1,
endLine: 1,
startTimestamp: '',
endTimestamp: '2026-03-01T10:07:00.000Z',
toolUseIds: ['member-inside-tool'],
filePaths: ['/repo/src/member.ts'],
confidence: {
tier: 3,
label: 'low',
reason: 'Only complete marker found, start assumed at file beginning',
},
},
],
isSingleTaskSession: true,
detectedMechanism: 'mcp' as const,
}
: {
boundaries: [],
scopes: [],
isSingleTaskSession: true,
detectedMechanism: 'none' as const,
}
),
};
const computer = new TaskChangeComputer(logsFinder as never, boundaryParser as never);
const result = await computer.computeTaskChanges({
teamName: 'team-a',
taskId: 'task-1',
taskMeta: { owner: 'alice', status: 'completed' },
effectiveOptions: {
intervals: [
{
startedAt: '2026-03-01T10:00:00.000Z',
completedAt: '2026-03-01T10:10:00.000Z',
},
],
},
projectPath: '/repo',
includeDetails: true,
});
expect(result.files.map((file) => file.relativePath)).toEqual(['src/member.ts']);
expect(result.scope.toolUseIds).toEqual(['member-inside-tool']);
});
it('keeps metadata-only synthetic Edit entries as file-change hints', async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'task-change-computer-'));
const logPath = path.join(tmpDir, 'agent.jsonl');
await writeJsonl(logPath, [metadataOnlyEditToolUse('tool-1', '/repo/src/a.ts')]);
const logsFinder = {
findLogFileRefsForTask: () => Promise.resolve([{ filePath: logPath, memberName: 'alice' }]),
};
const boundaryParser = {
parseBoundaries: () =>
Promise.resolve({
boundaries: [],
scopes: [],
isSingleTaskSession: true,
detectedMechanism: 'none' as const,
}),
};
const computer = new TaskChangeComputer(logsFinder as never, boundaryParser as never);
const result = await computer.computeTaskChanges({
teamName: 'team-a',
taskId: 'task-1',
taskMeta: null,
effectiveOptions: {},
projectPath: '/repo',
includeDetails: true,
});
expect(result.files.map((file) => file.relativePath)).toEqual(['src/a.ts']);
expect(result.files[0]?.snippets).toHaveLength(1);
expect(result.files[0]?.snippets[0]?.oldString).toBe('');
expect(result.files[0]?.snippets[0]?.newString).toBe('');
expect(result.totalFiles).toBe(1);
});
it('expands metadata-only Edit changes arrays into all changed file hints', async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'task-change-computer-'));
const logPath = path.join(tmpDir, 'agent.jsonl');
await writeJsonl(logPath, [
metadataOnlyMultiFileEditToolUse('tool-1', ['/repo/dfdf/calc.js', '/repo/dfdf/style.css']),
]);
const logsFinder = {
findLogFileRefsForTask: () => Promise.resolve([{ filePath: logPath, memberName: 'tom' }]),
};
const boundaryParser = {
parseBoundaries: () =>
Promise.resolve({
boundaries: [],
scopes: [
{
taskId: 'task-1',
memberName: '',
startLine: 1,
endLine: 1,
startTimestamp: '2026-03-01T10:00:00.000Z',
endTimestamp: '2026-03-01T10:01:00.000Z',
toolUseIds: ['tool-1'],
filePaths: ['/repo/dfdf/calc.js', '/repo/dfdf/style.css'],
confidence: { tier: 1, label: 'high', reason: 'Both markers found' },
},
],
isSingleTaskSession: true,
detectedMechanism: 'mcp' as const,
}),
};
const computer = new TaskChangeComputer(logsFinder as never, boundaryParser as never);
const result = await computer.computeTaskChanges({
teamName: 'team-a',
taskId: 'task-1',
taskMeta: null,
effectiveOptions: {},
projectPath: '/repo',
includeDetails: true,
});
expect(result.files.map((file) => file.relativePath)).toEqual([
'dfdf/calc.js',
'dfdf/style.css',
]);
expect(result.files.every((file) => file.snippets[0]?.toolUseId === 'tool-1')).toBe(true);
expect(result.files.every((file) => file.linesAdded === 0 && file.linesRemoved === 0)).toBe(
true
);
});
it('does not include repeated tool ids from outside the scoped source lines', async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'task-change-computer-'));
const logPath = path.join(tmpDir, 'agent.jsonl');
await writeJsonl(logPath, [
metadataOnlyMultiFileEditToolUse('tool-1', ['/repo/index.html', '/repo/style.css']),
metadataOnlyMultiFileEditToolUse(
'tool-1',
['/repo/177/landing.css'],
'/repo/177/landing.css'
),
]);
const logsFinder = {
findLogFileRefsForTask: () => Promise.resolve([{ filePath: logPath, memberName: 'tom' }]),
};
const boundaryParser = {
parseBoundaries: () =>
Promise.resolve({
boundaries: [],
scopes: [
{
taskId: 'task-1',
memberName: '',
startLine: 2,
endLine: 2,
startTimestamp: '2026-03-01T09:59:00.000Z',
endTimestamp: '2026-03-01T10:01:00.000Z',
toolUseIds: ['tool-1'],
filePaths: ['/repo/177/landing.css'],
confidence: { tier: 1, label: 'high', reason: 'Both markers found' },
},
],
isSingleTaskSession: true,
detectedMechanism: 'mcp' as const,
}),
};
const computer = new TaskChangeComputer(logsFinder as never, boundaryParser as never);
const result = await computer.computeTaskChanges({
teamName: 'team-a',
taskId: 'task-1',
taskMeta: null,
effectiveOptions: {},
projectPath: '/repo',
includeDetails: true,
});
expect(result.files.map((file) => file.relativePath)).toEqual(['177/landing.css']);
expect(result.scope.filePaths).toEqual(['/repo/177/landing.css']);
});
});