yishentu--claudian
292 行
9.0 KiB
TypeScript
292 行
9.0 KiB
TypeScript
import type { ToolCallInfo } from '../../../src/core/types/tools';
|
|
import { diffFromToolInput, extractDiffData, parseApplyPatchDiffs } from '../../../src/utils/diff';
|
|
|
|
/** Helper to create a ToolCallInfo for testing. */
|
|
function makeToolCall(name: string, input: Record<string, unknown>): ToolCallInfo {
|
|
return { id: 'test-id', name, input, status: 'completed', isExpanded: false };
|
|
}
|
|
|
|
describe('extractDiffData', () => {
|
|
it('returns ToolDiffData from valid toolUseResult with structuredPatch', () => {
|
|
const toolCall = makeToolCall('Edit', { file_path: 'src/foo.ts' });
|
|
const toolUseResult = {
|
|
structuredPatch: [
|
|
{ oldStart: 1, oldLines: 1, newStart: 1, newLines: 1, lines: ['-old', '+new'] },
|
|
],
|
|
};
|
|
|
|
const result = extractDiffData(toolUseResult, toolCall);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result!.filePath).toBe('src/foo.ts');
|
|
expect(result!.diffLines).toHaveLength(2);
|
|
expect(result!.diffLines[0]).toEqual({ type: 'delete', text: 'old', oldLineNum: 1 });
|
|
expect(result!.diffLines[1]).toEqual({ type: 'insert', text: 'new', newLineNum: 1 });
|
|
expect(result!.stats).toEqual({ added: 1, removed: 1 });
|
|
});
|
|
|
|
it('returns ToolDiffData from Pi result details.diff', () => {
|
|
const toolCall = makeToolCall('Edit', { file_path: 'src/pi.ts' });
|
|
const toolUseResult = {
|
|
content: [{ text: 'Edited src/pi.ts', type: 'text' }],
|
|
details: {
|
|
diff: [
|
|
'--- a/src/pi.ts',
|
|
'+++ b/src/pi.ts',
|
|
'@@ -2,2 +2,2 @@',
|
|
' keep',
|
|
'-old',
|
|
'+new',
|
|
].join('\n'),
|
|
},
|
|
};
|
|
|
|
const result = extractDiffData(toolUseResult, toolCall);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result!.filePath).toBe('src/pi.ts');
|
|
expect(result!.diffLines).toEqual([
|
|
{ type: 'equal', text: 'keep', oldLineNum: 2, newLineNum: 2 },
|
|
{ type: 'delete', text: 'old', oldLineNum: 3 },
|
|
{ type: 'insert', text: 'new', newLineNum: 3 },
|
|
]);
|
|
expect(result!.stats).toEqual({ added: 1, removed: 1 });
|
|
});
|
|
|
|
it('uses SDK filePath when present in toolUseResult', () => {
|
|
const toolCall = makeToolCall('Write', { file_path: 'input/path.ts' });
|
|
const toolUseResult = {
|
|
filePath: 'sdk/override.ts',
|
|
structuredPatch: [
|
|
{ oldStart: 1, oldLines: 0, newStart: 1, newLines: 1, lines: ['+hello'] },
|
|
],
|
|
};
|
|
|
|
const result = extractDiffData(toolUseResult, toolCall);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result!.filePath).toBe('sdk/override.ts');
|
|
});
|
|
|
|
it('falls back to diffFromToolInput when toolUseResult is undefined', () => {
|
|
const toolCall = makeToolCall('Edit', {
|
|
file_path: 'src/bar.ts',
|
|
old_string: 'a',
|
|
new_string: 'b',
|
|
});
|
|
|
|
const result = extractDiffData(undefined, toolCall);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result!.filePath).toBe('src/bar.ts');
|
|
expect(result!.diffLines).toHaveLength(2);
|
|
});
|
|
|
|
it('falls back to diffFromToolInput when toolUseResult is empty object', () => {
|
|
const toolCall = makeToolCall('Write', {
|
|
file_path: 'src/new.ts',
|
|
content: 'line1\nline2',
|
|
});
|
|
|
|
const result = extractDiffData({}, toolCall);
|
|
|
|
// {} has no structuredPatch → falls back to diffFromToolInput for Write
|
|
expect(result).toBeDefined();
|
|
expect(result!.diffLines).toHaveLength(2);
|
|
expect(result!.stats).toEqual({ added: 2, removed: 0 });
|
|
});
|
|
|
|
it('falls back to diffFromToolInput when structuredPatch is empty array', () => {
|
|
const toolCall = makeToolCall('Edit', {
|
|
file_path: 'src/x.ts',
|
|
old_string: 'old',
|
|
new_string: 'new',
|
|
});
|
|
|
|
const result = extractDiffData({ structuredPatch: [] }, toolCall);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result!.filePath).toBe('src/x.ts');
|
|
expect(result!.diffLines).toHaveLength(2);
|
|
});
|
|
|
|
it('falls back to diffFromToolInput when toolUseResult is a string', () => {
|
|
const toolCall = makeToolCall('Edit', {
|
|
file_path: 'src/y.ts',
|
|
old_string: 'foo',
|
|
new_string: 'bar',
|
|
});
|
|
|
|
const result = extractDiffData('some string result', toolCall);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result!.filePath).toBe('src/y.ts');
|
|
});
|
|
|
|
it('returns undefined for unknown tool with no structuredPatch', () => {
|
|
const toolCall = makeToolCall('Bash', { command: 'echo hi' });
|
|
|
|
const result = extractDiffData(undefined, toolCall);
|
|
|
|
expect(result).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('diffFromToolInput', () => {
|
|
it('returns delete + insert lines for Edit with valid old_string/new_string', () => {
|
|
const toolCall = makeToolCall('Edit', {
|
|
file_path: 'src/a.ts',
|
|
old_string: 'line1\nline2',
|
|
new_string: 'newline1\nnewline2\nnewline3',
|
|
});
|
|
|
|
const result = diffFromToolInput(toolCall, 'src/a.ts');
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result!.filePath).toBe('src/a.ts');
|
|
// 2 delete lines + 3 insert lines
|
|
expect(result!.diffLines).toHaveLength(5);
|
|
expect(result!.diffLines.filter(l => l.type === 'delete')).toHaveLength(2);
|
|
expect(result!.diffLines.filter(l => l.type === 'insert')).toHaveLength(3);
|
|
expect(result!.stats).toEqual({ added: 3, removed: 2 });
|
|
});
|
|
|
|
it('returns delete + insert lines for Pi Edit edits array', () => {
|
|
const toolCall = makeToolCall('Edit', {
|
|
file_path: 'src/pi.ts',
|
|
edits: [
|
|
{ oldText: 'one\ntwo', newText: 'uno\ndos' },
|
|
{ oldText: 'three', newText: 'tres' },
|
|
],
|
|
});
|
|
|
|
const result = diffFromToolInput(toolCall, 'src/pi.ts');
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result!.filePath).toBe('src/pi.ts');
|
|
expect(result!.diffLines.filter(l => l.type === 'delete').map(l => l.text)).toEqual(['one', 'two', 'three']);
|
|
expect(result!.diffLines.filter(l => l.type === 'insert').map(l => l.text)).toEqual(['uno', 'dos', 'tres']);
|
|
expect(result!.stats).toEqual({ added: 3, removed: 3 });
|
|
});
|
|
|
|
it('uses path as the fallback file path for Pi Write input', () => {
|
|
const toolCall = makeToolCall('Write', {
|
|
path: 'src/pi-new.ts',
|
|
content: 'created',
|
|
});
|
|
|
|
const result = extractDiffData(undefined, toolCall);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result!.filePath).toBe('src/pi-new.ts');
|
|
});
|
|
|
|
it('ignores non-string file_path values when a Pi path fallback is available', () => {
|
|
const toolCall = makeToolCall('Write', {
|
|
file_path: 123,
|
|
path: 'src/pi-new.ts',
|
|
content: 'created',
|
|
});
|
|
|
|
const result = extractDiffData(undefined, toolCall);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result!.filePath).toBe('src/pi-new.ts');
|
|
});
|
|
|
|
it('returns all insert lines for Write with valid content', () => {
|
|
const toolCall = makeToolCall('Write', {
|
|
file_path: 'src/b.ts',
|
|
content: 'a\nb\nc',
|
|
});
|
|
|
|
const result = diffFromToolInput(toolCall, 'src/b.ts');
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result!.diffLines).toHaveLength(3);
|
|
expect(result!.diffLines.every(l => l.type === 'insert')).toBe(true);
|
|
expect(result!.stats).toEqual({ added: 3, removed: 0 });
|
|
});
|
|
|
|
it('returns undefined for Edit with non-string inputs', () => {
|
|
const toolCall = makeToolCall('Edit', {
|
|
file_path: 'src/c.ts',
|
|
old_string: 123,
|
|
new_string: null,
|
|
});
|
|
|
|
const result = diffFromToolInput(toolCall, 'src/c.ts');
|
|
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it('returns undefined for Write with non-string content', () => {
|
|
const toolCall = makeToolCall('Write', {
|
|
file_path: 'src/d.ts',
|
|
content: { data: 'not a string' },
|
|
});
|
|
|
|
const result = diffFromToolInput(toolCall, 'src/d.ts');
|
|
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it('returns undefined for unknown tool name', () => {
|
|
const toolCall = makeToolCall('Bash', { command: 'ls' });
|
|
|
|
const result = diffFromToolInput(toolCall, 'some/path');
|
|
|
|
expect(result).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('parseApplyPatchDiffs', () => {
|
|
it('parses update hunks into diff lines', () => {
|
|
const patch = [
|
|
'*** Begin Patch',
|
|
'*** Update File: src/main.ts',
|
|
'@@',
|
|
"-import { Plugin } from 'obsidian';",
|
|
"+import { Plugin, Notice } from 'obsidian';",
|
|
'*** End Patch',
|
|
].join('\n');
|
|
|
|
const result = parseApplyPatchDiffs(patch);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]).toMatchObject({
|
|
filePath: 'src/main.ts',
|
|
operation: 'update',
|
|
stats: { added: 1, removed: 1 },
|
|
});
|
|
expect(result[0].diffLines.map(line => line.text)).toEqual([
|
|
"import { Plugin } from 'obsidian';",
|
|
"import { Plugin, Notice } from 'obsidian';",
|
|
]);
|
|
});
|
|
|
|
it('parses add and delete file operations', () => {
|
|
const patch = [
|
|
'*** Begin Patch',
|
|
'*** Add File: src/new.ts',
|
|
'+export const value = 1;',
|
|
'*** Delete File: src/old.ts',
|
|
'*** End Patch',
|
|
].join('\n');
|
|
|
|
const result = parseApplyPatchDiffs(patch);
|
|
|
|
expect(result).toHaveLength(2);
|
|
expect(result[0]).toMatchObject({
|
|
filePath: 'src/new.ts',
|
|
operation: 'add',
|
|
stats: { added: 1, removed: 0 },
|
|
});
|
|
expect(result[1]).toMatchObject({
|
|
filePath: 'src/old.ts',
|
|
operation: 'delete',
|
|
diffLines: [],
|
|
});
|
|
});
|
|
});
|