agent-ecosystem/test/renderer/components/team/review/pathChangeLabels.test.ts

243 lines
7.1 KiB
TypeScript

import * as path from 'path';
import { afterEach, describe, expect, it } from 'vitest';
import { TaskChangeLedgerReader } from '@main/services/team/TaskChangeLedgerReader';
import { buildPathChangeLabels } from '@renderer/components/team/review/pathChangeLabels';
import { materializeTaskChangeLedgerFixture } from '../../../../main/services/team/taskChangeLedgerFixtureUtils';
import type { FileChangeSummary, FileChangeWithContent } from '@shared/types';
const TEAM_NAME = 'team-a';
describe('buildPathChangeLabels', () => {
const cleanups: Array<() => Promise<void>> = [];
afterEach(async () => {
while (cleanups.length > 0) {
await cleanups.pop()?.();
}
});
it('projects real ledger rename and copy fixtures from relation metadata, not content heuristics', async () => {
const renameFixture = await materializeTaskChangeLedgerFixture('rename');
const copyFixture = await materializeTaskChangeLedgerFixture('copy');
cleanups.push(renameFixture.cleanup, copyFixture.cleanup);
const reader = new TaskChangeLedgerReader();
const rename = await reader.readTaskChanges({
teamName: TEAM_NAME,
taskId: renameFixture.manifest.taskId,
projectDir: renameFixture.projectDir,
projectPath: renameFixture.projectDir,
includeDetails: false,
});
const copy = await reader.readTaskChanges({
teamName: TEAM_NAME,
taskId: copyFixture.manifest.taskId,
projectDir: copyFixture.projectDir,
projectPath: copyFixture.projectDir,
includeDetails: false,
});
const renameFile = rename?.files[0];
const copyFile = copy?.files[0];
expect(renameFile?.filePath).toBe(path.join(renameFixture.projectDir, 'src', 'new.ts'));
expect(copyFile?.filePath).toBe(path.join(copyFixture.projectDir, 'src', 'copy.ts'));
const renameLabels = buildPathChangeLabels(rename?.files ?? [], {});
const copyLabels = buildPathChangeLabels(copy?.files ?? [], {});
expect(renameLabels[renameFile!.filePath]).toEqual({
kind: 'renamed',
direction: 'from',
otherPath: 'src/old.ts',
});
expect(copyLabels[copyFile!.filePath]).toEqual({
kind: 'copied',
direction: 'from',
otherPath: 'src/base.ts',
});
});
it('does not mark metadata-only unavailable content as deleted without ledger delete evidence', () => {
const file: FileChangeSummary = {
filePath: '/repo/src/binary.dat',
relativePath: 'src/binary.dat',
snippets: [],
linesAdded: 0,
linesRemoved: 0,
isNewFile: false,
ledgerSummary: {
latestOperation: 'modify',
contentAvailability: 'metadata-only',
reviewability: 'metadata-only',
},
};
const content: FileChangeWithContent = {
...file,
originalFullContent: null,
modifiedFullContent: null,
contentSource: 'unavailable',
};
expect(buildPathChangeLabels([file], { [file.filePath]: content })).toEqual({});
});
it('matches relative ledger relation paths case-insensitively against Windows drive paths', () => {
const oldFile: FileChangeSummary = {
filePath: 'C:\\Repo\\SRC\\Old.ts',
relativePath: 'SRC\\Old.ts',
snippets: [],
linesAdded: 0,
linesRemoved: 1,
isNewFile: false,
ledgerSummary: {
latestOperation: 'delete',
relation: {
kind: 'rename',
oldPath: 'src\\old.ts',
newPath: 'src\\new.ts',
},
},
};
const newFile: FileChangeSummary = {
filePath: 'C:\\Repo\\src\\New.ts',
relativePath: 'src\\New.ts',
snippets: [],
linesAdded: 1,
linesRemoved: 0,
isNewFile: false,
ledgerSummary: {
latestOperation: 'create',
relation: {
kind: 'rename',
oldPath: 'src\\old.ts',
newPath: 'src\\new.ts',
},
},
};
expect(buildPathChangeLabels([oldFile, newFile], {})).toEqual({
[oldFile.filePath]: {
kind: 'renamed',
direction: 'to',
otherPath: 'src\\new.ts',
},
[newFile.filePath]: {
kind: 'renamed',
direction: 'from',
otherPath: 'src\\old.ts',
},
});
});
it('matches relative Windows relation paths case-insensitively when only backslash paths are available', () => {
const file: FileChangeSummary = {
filePath: 'SRC\\New.ts',
relativePath: 'SRC\\New.ts',
snippets: [],
linesAdded: 1,
linesRemoved: 0,
isNewFile: false,
ledgerSummary: {
latestOperation: 'create',
relation: {
kind: 'rename',
oldPath: 'src\\old.ts',
newPath: 'src\\new.ts',
},
},
};
expect(buildPathChangeLabels([file], {})).toEqual({
[file.filePath]: {
kind: 'renamed',
direction: 'from',
otherPath: 'src\\old.ts',
},
});
});
it('does not project rename labels when relation paths match neither file side', () => {
const file: FileChangeSummary = {
filePath: 'C:\\Repo\\src\\renew.ts',
relativePath: 'src\\renew.ts',
snippets: [],
linesAdded: 1,
linesRemoved: 1,
isNewFile: false,
ledgerSummary: {
latestOperation: 'modify',
relation: {
kind: 'rename',
oldPath: 'old.ts',
newPath: 'new.ts',
},
},
};
expect(buildPathChangeLabels([file], {})).toEqual({});
});
it('falls back to delete label when invalid relation metadata belongs to a delete event', () => {
const file: FileChangeSummary = {
filePath: 'C:\\Repo\\src\\renew.ts',
relativePath: 'src\\renew.ts',
snippets: [],
linesAdded: 0,
linesRemoved: 1,
isNewFile: false,
ledgerSummary: {
latestOperation: 'delete',
relation: {
kind: 'rename',
oldPath: 'old.ts',
newPath: 'new.ts',
},
},
};
expect(buildPathChangeLabels([file], {})).toEqual({
[file.filePath]: { kind: 'deleted' },
});
});
it('keeps the legacy content-hash move fallback only when text evidence is available', () => {
const oldFile: FileChangeSummary = {
filePath: '/repo/src/old.ts',
relativePath: 'src/old.ts',
snippets: [],
linesAdded: 0,
linesRemoved: 1,
isNewFile: false,
};
const newFile: FileChangeSummary = {
filePath: '/repo/lib/old.ts',
relativePath: 'lib/old.ts',
snippets: [],
linesAdded: 1,
linesRemoved: 0,
isNewFile: true,
};
const contentByPath: Record<string, FileChangeWithContent> = {
[oldFile.filePath]: {
...oldFile,
originalFullContent: 'export const value = 1;\n',
modifiedFullContent: null,
contentSource: 'file-history',
},
[newFile.filePath]: {
...newFile,
originalFullContent: '',
modifiedFullContent: 'export const value = 1;\n',
contentSource: 'disk-current',
},
};
expect(buildPathChangeLabels([oldFile, newFile], contentByPath)).toEqual({
[newFile.filePath]: { kind: 'moved', direction: 'from', otherPath: 'src/old.ts' },
[oldFile.filePath]: { kind: 'moved', direction: 'to', otherPath: 'lib/old.ts' },
});
});
});