fix(team): preserve metadata-only file additions

This commit is contained in:
777genius 2026-06-01 19:41:46 +03:00
parent eb4e4ba3e5
commit ab3be12b94
2 changed files with 71 additions and 5 deletions

View file

@ -46,6 +46,7 @@ interface LogFileRef {
interface MetadataChangePath { interface MetadataChangePath {
filePath: string; filePath: string;
kind?: string;
} }
interface ParsedJsonlEntry { interface ParsedJsonlEntry {
@ -617,11 +618,13 @@ export class TaskChangeComputer {
for (const target of targetPaths) { for (const target of targetPaths) {
seenFiles.add(this.normalizeFilePathKey(target.filePath)); seenFiles.add(this.normalizeFilePathKey(target.filePath));
const snippetType: SnippetDiff['type'] =
!hasTextPayload && target.kind === 'add' ? 'write-new' : 'edit';
addSnippet({ addSnippet({
toolUseId, toolUseId,
filePath: target.filePath, filePath: target.filePath,
toolName: 'Edit', toolName: 'Edit',
type: 'edit', type: snippetType,
oldString, oldString,
newString, newString,
replaceAll, replaceAll,
@ -718,10 +721,11 @@ export class TaskChangeComputer {
const changeObj = change as Record<string, unknown>; const changeObj = change as Record<string, unknown>;
const filePath = typeof changeObj.path === 'string' ? changeObj.path : ''; const filePath = typeof changeObj.path === 'string' ? changeObj.path : '';
if (!filePath) continue; if (!filePath) continue;
const kind = typeof changeObj.kind === 'string' ? changeObj.kind : undefined;
const normalized = this.normalizeFilePathKey(filePath); const normalized = this.normalizeFilePathKey(filePath);
if (seen.has(normalized)) continue; if (seen.has(normalized)) continue;
seen.add(normalized); seen.add(normalized);
paths.push({ filePath }); paths.push({ filePath, ...(kind ? { kind } : {}) });
} }
return paths; return paths;

View file

@ -1,10 +1,10 @@
import * as fs from 'fs/promises';
import * as os from 'os'; import * as os from 'os';
import * as path from 'path'; import * as path from 'path';
import { afterEach, describe, expect, it } from 'vitest'; import { afterEach, describe, expect, it } from 'vitest';
import * as fs from 'fs/promises';
import { TaskChangeComputer } from '../../../../src/main/services/team/TaskChangeComputer'; import { TaskChangeComputer } from '../../../../src/main/services/team/TaskChangeComputer';
import type { TaskChangeTaskMeta } from '../../../../src/main/services/team/taskChangeWorkerTypes'; import type { TaskChangeTaskMeta } from '../../../../src/main/services/team/taskChangeWorkerTypes';
const NO_TASK_BOUNDARIES_WARNING = const NO_TASK_BOUNDARIES_WARNING =
@ -84,6 +84,18 @@ function metadataOnlyMultiFileEditToolUse(
toolUseId: string, toolUseId: string,
filePaths: string[], filePaths: string[],
primaryPath = filePaths[0] ?? '' primaryPath = filePaths[0] ?? ''
): object {
return metadataOnlyMultiFileEditChangesToolUse(
toolUseId,
filePaths.map((filePath) => ({ filePath, kind: 'add' })),
primaryPath
);
}
function metadataOnlyMultiFileEditChangesToolUse(
toolUseId: string,
changes: Array<{ filePath: string; kind?: string }>,
primaryPath = changes[0]?.filePath ?? ''
): object { ): object {
return { return {
timestamp: '2026-03-01T10:00:00.000Z', timestamp: '2026-03-01T10:00:00.000Z',
@ -97,7 +109,10 @@ function metadataOnlyMultiFileEditToolUse(
name: 'Edit', name: 'Edit',
input: { input: {
file_path: primaryPath, file_path: primaryPath,
changes: filePaths.map((filePath) => ({ path: filePath, kind: 'add' })), changes: changes.map((change) => ({
path: change.filePath,
...(change.kind ? { kind: change.kind } : {}),
})),
}, },
}, },
], ],
@ -644,6 +659,8 @@ describe('TaskChangeComputer', () => {
expect(result.files.map((file) => file.relativePath)).toEqual(['src/a.ts']); expect(result.files.map((file) => file.relativePath)).toEqual(['src/a.ts']);
expect(result.files[0]?.snippets).toHaveLength(1); expect(result.files[0]?.snippets).toHaveLength(1);
expect(result.files[0]?.isNewFile).toBe(false);
expect(result.files[0]?.snippets[0]?.type).toBe('edit');
expect(result.files[0]?.snippets[0]?.oldString).toBe(''); expect(result.files[0]?.snippets[0]?.oldString).toBe('');
expect(result.files[0]?.snippets[0]?.newString).toBe(''); expect(result.files[0]?.snippets[0]?.newString).toBe('');
expect(result.totalFiles).toBe(1); expect(result.totalFiles).toBe(1);
@ -696,11 +713,56 @@ describe('TaskChangeComputer', () => {
'dfdf/style.css', 'dfdf/style.css',
]); ]);
expect(result.files.every((file) => file.snippets[0]?.toolUseId === 'tool-1')).toBe(true); expect(result.files.every((file) => file.snippets[0]?.toolUseId === 'tool-1')).toBe(true);
expect(result.files.every((file) => file.isNewFile)).toBe(true);
expect(result.files.every((file) => file.snippets[0]?.type === 'write-new')).toBe(true);
expect(result.files.every((file) => file.linesAdded === 0 && file.linesRemoved === 0)).toBe( expect(result.files.every((file) => file.linesAdded === 0 && file.linesRemoved === 0)).toBe(
true true
); );
}); });
it('preserves metadata-only Edit change kinds without upgrading updates to new files', async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'task-change-computer-'));
const logPath = path.join(tmpDir, 'agent.jsonl');
await writeJsonl(logPath, [
metadataOnlyMultiFileEditChangesToolUse('tool-1', [
{ filePath: '/repo/src/new.ts', kind: 'add' },
{ filePath: '/repo/src/existing.ts', kind: 'update' },
]),
]);
const logsFinder = {
findLogFileRefsForTask: () => Promise.resolve([{ filePath: logPath, memberName: 'tom' }]),
};
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,
});
const filesByPath = new Map(result.files.map((file) => [file.relativePath, file]));
const newFile = filesByPath.get('src/new.ts');
const existingFile = filesByPath.get('src/existing.ts');
expect(newFile?.isNewFile).toBe(true);
expect(newFile?.snippets[0]?.type).toBe('write-new');
expect(existingFile?.isNewFile).toBe(false);
expect(existingFile?.snippets[0]?.type).toBe('edit');
});
it('does not include repeated tool ids from outside the scoped source lines', async () => { 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-')); tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'task-change-computer-'));
const logPath = path.join(tmpDir, 'agent.jsonl'); const logPath = path.join(tmpDir, 'agent.jsonl');