update fetch urls & web search show view

This commit is contained in:
duanfuxiang
2025-04-01 18:56:11 +08:00
parent 7642cc1410
commit c8d03bf799
25 changed files with 156 additions and 4967 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,738 +0,0 @@
import { NewUnifiedDiffStrategy } from "../new-unified"
describe("main", () => {
let strategy: NewUnifiedDiffStrategy
beforeEach(() => {
strategy = new NewUnifiedDiffStrategy(0.97)
})
describe("constructor", () => {
it("should use default confidence threshold when not provided", () => {
const defaultStrategy = new NewUnifiedDiffStrategy()
expect(defaultStrategy["confidenceThreshold"]).toBe(1)
})
it("should use provided confidence threshold", () => {
const customStrategy = new NewUnifiedDiffStrategy(0.85)
expect(customStrategy["confidenceThreshold"]).toBe(0.85)
})
it("should enforce minimum confidence threshold", () => {
const lowStrategy = new NewUnifiedDiffStrategy(0.7) // Below minimum of 0.8
expect(lowStrategy["confidenceThreshold"]).toBe(0.8)
})
})
describe("getToolDescription", () => {
it("should return tool description with correct cwd", () => {
const cwd = "/test/path"
const description = strategy.getToolDescription({ cwd })
expect(description).toContain("apply_diff Tool - Generate Precise Code Changes")
expect(description).toContain(cwd)
expect(description).toContain("Step-by-Step Instructions")
expect(description).toContain("Requirements")
expect(description).toContain("Examples")
expect(description).toContain("Parameters:")
})
})
it("should apply simple diff correctly", async () => {
const original = `line1
line2
line3`
const diff = `--- a/file.txt
+++ b/file.txt
@@ ... @@
line1
+new line
line2
-line3
+modified line3`
const result = await strategy.applyDiff(original, diff)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(`line1
new line
line2
modified line3`)
}
})
it("should handle multiple hunks", async () => {
const original = `line1
line2
line3
line4
line5`
const diff = `--- a/file.txt
+++ b/file.txt
@@ ... @@
line1
+new line
line2
-line3
+modified line3
@@ ... @@
line4
-line5
+modified line5
+new line at end`
const result = await strategy.applyDiff(original, diff)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(`line1
new line
line2
modified line3
line4
modified line5
new line at end`)
}
})
it("should handle complex large", async () => {
const original = `line1
line2
line3
line4
line5
line6
line7
line8
line9
line10`
const diff = `--- a/file.txt
+++ b/file.txt
@@ ... @@
line1
+header line
+another header
line2
-line3
-line4
+modified line3
+modified line4
+extra line
@@ ... @@
line6
+middle section
line7
-line8
+changed line8
+bonus line
@@ ... @@
line9
-line10
+final line
+very last line`
const result = await strategy.applyDiff(original, diff)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(`line1
header line
another header
line2
modified line3
modified line4
extra line
line5
line6
middle section
line7
changed line8
bonus line
line9
final line
very last line`)
}
})
it("should handle indentation changes", async () => {
const original = `first line
indented line
double indented line
back to single indent
no indent
indented again
double indent again
triple indent
back to single
last line`
const diff = `--- original
+++ modified
@@ ... @@
first line
indented line
+ tab indented line
+ new indented line
double indented line
back to single indent
no indent
indented again
double indent again
- triple indent
+ hi there mate
back to single
last line`
const expected = `first line
indented line
tab indented line
new indented line
double indented line
back to single indent
no indent
indented again
double indent again
hi there mate
back to single
last line`
const result = await strategy.applyDiff(original, diff)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(expected)
}
})
it("should handle high level edits", async () => {
const original = `def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n-1)`
const diff = `@@ ... @@
-def factorial(n):
- if n == 0:
- return 1
- else:
- return n * factorial(n-1)
+def factorial(number):
+ if number == 0:
+ return 1
+ else:
+ return number * factorial(number-1)`
const expected = `def factorial(number):
if number == 0:
return 1
else:
return number * factorial(number-1)`
const result = await strategy.applyDiff(original, diff)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(expected)
}
})
it("it should handle very complex edits", async () => {
const original = `//Initialize the array that will hold the primes
var primeArray = [];
/*Write a function that checks for primeness and
pushes those values to t*he array*/
function PrimeCheck(candidate){
isPrime = true;
for(var i = 2; i < candidate && isPrime; i++){
if(candidate%i === 0){
isPrime = false;
} else {
isPrime = true;
}
}
if(isPrime){
primeArray.push(candidate);
}
return primeArray;
}
/*Write the code that runs the above until the
l ength of the array equa*ls the number of primes
desired*/
var numPrimes = prompt("How many primes?");
//Display the finished array of primes
//for loop starting at 2 as that is the lowest prime number keep going until the array is as long as we requested
for (var i = 2; primeArray.length < numPrimes; i++) {
PrimeCheck(i); //
}
console.log(primeArray);
`
const diff = `--- test_diff.js
+++ test_diff.js
@@ ... @@
-//Initialize the array that will hold the primes
var primeArray = [];
-/*Write a function that checks for primeness and
- pushes those values to t*he array*/
function PrimeCheck(candidate){
isPrime = true;
for(var i = 2; i < candidate && isPrime; i++){
@@ ... @@
return primeArray;
}
-/*Write the code that runs the above until the
- l ength of the array equa*ls the number of primes
- desired*/
var numPrimes = prompt("How many primes?");
-//Display the finished array of primes
-
-//for loop starting at 2 as that is the lowest prime number keep going until the array is as long as we requested
for (var i = 2; primeArray.length < numPrimes; i++) {
- PrimeCheck(i); //
+ PrimeCheck(i);
}
console.log(primeArray);`
const expected = `var primeArray = [];
function PrimeCheck(candidate){
isPrime = true;
for(var i = 2; i < candidate && isPrime; i++){
if(candidate%i === 0){
isPrime = false;
} else {
isPrime = true;
}
}
if(isPrime){
primeArray.push(candidate);
}
return primeArray;
}
var numPrimes = prompt("How many primes?");
for (var i = 2; primeArray.length < numPrimes; i++) {
PrimeCheck(i);
}
console.log(primeArray);
`
const result = await strategy.applyDiff(original, diff)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(expected)
}
})
describe("error handling and edge cases", () => {
it("should reject completely invalid diff format", async () => {
const original = "line1\nline2\nline3"
const invalidDiff = "this is not a diff at all"
const result = await strategy.applyDiff(original, invalidDiff)
expect(result.success).toBe(false)
})
it("should reject diff with invalid hunk format", async () => {
const original = "line1\nline2\nline3"
const invalidHunkDiff = `--- a/file.txt
+++ b/file.txt
invalid hunk header
line1
-line2
+new line`
const result = await strategy.applyDiff(original, invalidHunkDiff)
expect(result.success).toBe(false)
})
it("should fail when diff tries to modify non-existent content", async () => {
const original = "line1\nline2\nline3"
const nonMatchingDiff = `--- a/file.txt
+++ b/file.txt
@@ ... @@
line1
-nonexistent line
+new line
line3`
const result = await strategy.applyDiff(original, nonMatchingDiff)
expect(result.success).toBe(false)
})
it("should handle overlapping hunks", async () => {
const original = `line1
line2
line3
line4
line5`
const overlappingDiff = `--- a/file.txt
+++ b/file.txt
@@ ... @@
line1
line2
-line3
+modified3
line4
@@ ... @@
line2
-line3
-line4
+modified3and4
line5`
const result = await strategy.applyDiff(original, overlappingDiff)
expect(result.success).toBe(false)
})
it("should handle empty lines modifications", async () => {
const original = `line1
line3
line5`
const emptyLinesDiff = `--- a/file.txt
+++ b/file.txt
@@ ... @@
line1
-line3
+line3modified
line5`
const result = await strategy.applyDiff(original, emptyLinesDiff)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(`line1
line3modified
line5`)
}
})
it("should handle mixed line endings in diff", async () => {
const original = "line1\r\nline2\nline3\r\n"
const mixedEndingsDiff = `--- a/file.txt
+++ b/file.txt
@@ ... @@
line1\r
-line2
+modified2\r
line3`
const result = await strategy.applyDiff(original, mixedEndingsDiff)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe("line1\r\nmodified2\r\nline3\r\n")
}
})
it("should handle partial line modifications", async () => {
const original = "const value = oldValue + 123;"
const partialDiff = `--- a/file.txt
+++ b/file.txt
@@ ... @@
-const value = oldValue + 123;
+const value = newValue + 123;`
const result = await strategy.applyDiff(original, partialDiff)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe("const value = newValue + 123;")
}
})
it("should handle slightly malformed but recoverable diff", async () => {
const original = "line1\nline2\nline3"
// Missing space after --- and +++
const slightlyBadDiff = `---a/file.txt
+++b/file.txt
@@ ... @@
line1
-line2
+new line
line3`
const result = await strategy.applyDiff(original, slightlyBadDiff)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe("line1\nnew line\nline3")
}
})
})
describe("similar code sections", () => {
it("should correctly modify the right section when similar code exists", async () => {
const original = `function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
function multiply(a, b) {
return a + b; // Bug here
}`
const diff = `--- a/math.js
+++ b/math.js
@@ ... @@
function multiply(a, b) {
- return a + b; // Bug here
+ return a * b;
}`
const result = await strategy.applyDiff(original, diff)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(`function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
function multiply(a, b) {
return a * b;
}`)
}
})
it("should handle multiple similar sections with correct context", async () => {
const original = `if (condition) {
doSomething();
doSomething();
doSomething();
}
if (otherCondition) {
doSomething();
doSomething();
doSomething();
}`
const diff = `--- a/file.js
+++ b/file.js
@@ ... @@
if (otherCondition) {
doSomething();
- doSomething();
+ doSomethingElse();
doSomething();
}`
const result = await strategy.applyDiff(original, diff)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(`if (condition) {
doSomething();
doSomething();
doSomething();
}
if (otherCondition) {
doSomething();
doSomethingElse();
doSomething();
}`)
}
})
})
describe("hunk splitting", () => {
it("should handle large diffs with multiple non-contiguous changes", async () => {
const original = `import { readFile } from 'fs';
import { join } from 'path';
import { Logger } from './logger';
const logger = new Logger();
async function processFile(filePath: string) {
try {
const data = await readFile(filePath, 'utf8');
logger.info('File read successfully');
return data;
} catch (error) {
logger.error('Failed to read file:', error);
throw error;
}
}
function validateInput(input: string): boolean {
if (!input) {
logger.warn('Empty input received');
return false;
}
return input.length > 0;
}
async function writeOutput(data: string) {
logger.info('Processing output');
// TODO: Implement output writing
return Promise.resolve();
}
function parseConfig(configPath: string) {
logger.debug('Reading config from:', configPath);
// Basic config parsing
return {
enabled: true,
maxRetries: 3
};
}
export {
processFile,
validateInput,
writeOutput,
parseConfig
};`
const diff = `--- a/file.ts
+++ b/file.ts
@@ ... @@
-import { readFile } from 'fs';
+import { readFile, writeFile } from 'fs';
import { join } from 'path';
-import { Logger } from './logger';
+import { Logger } from './utils/logger';
+import { Config } from './types';
-const logger = new Logger();
+const logger = new Logger('FileProcessor');
async function processFile(filePath: string) {
try {
const data = await readFile(filePath, 'utf8');
- logger.info('File read successfully');
+ logger.info(\`File \${filePath} read successfully\`);
return data;
} catch (error) {
- logger.error('Failed to read file:', error);
+ logger.error(\`Failed to read file \${filePath}:\`, error);
throw error;
}
}
function validateInput(input: string): boolean {
if (!input) {
- logger.warn('Empty input received');
+ logger.warn('Validation failed: Empty input received');
return false;
}
- return input.length > 0;
+ return input.trim().length > 0;
}
-async function writeOutput(data: string) {
- logger.info('Processing output');
- // TODO: Implement output writing
- return Promise.resolve();
+async function writeOutput(data: string, outputPath: string) {
+ try {
+ await writeFile(outputPath, data, 'utf8');
+ logger.info(\`Output written to \${outputPath}\`);
+ } catch (error) {
+ logger.error(\`Failed to write output to \${outputPath}:\`, error);
+ throw error;
+ }
}
-function parseConfig(configPath: string) {
- logger.debug('Reading config from:', configPath);
- // Basic config parsing
- return {
- enabled: true,
- maxRetries: 3
- };
+async function parseConfig(configPath: string): Promise<Config> {
+ try {
+ const configData = await readFile(configPath, 'utf8');
+ logger.debug(\`Reading config from \${configPath}\`);
+ return JSON.parse(configData);
+ } catch (error) {
+ logger.error(\`Failed to parse config from \${configPath}:\`, error);
+ throw error;
+ }
}
export {
processFile,
validateInput,
writeOutput,
- parseConfig
+ parseConfig,
+ type Config
};`
const expected = `import { readFile, writeFile } from 'fs';
import { join } from 'path';
import { Logger } from './utils/logger';
import { Config } from './types';
const logger = new Logger('FileProcessor');
async function processFile(filePath: string) {
try {
const data = await readFile(filePath, 'utf8');
logger.info(\`File \${filePath} read successfully\`);
return data;
} catch (error) {
logger.error(\`Failed to read file \${filePath}:\`, error);
throw error;
}
}
function validateInput(input: string): boolean {
if (!input) {
logger.warn('Validation failed: Empty input received');
return false;
}
return input.trim().length > 0;
}
async function writeOutput(data: string, outputPath: string) {
try {
await writeFile(outputPath, data, 'utf8');
logger.info(\`Output written to \${outputPath}\`);
} catch (error) {
logger.error(\`Failed to write output to \${outputPath}:\`, error);
throw error;
}
}
async function parseConfig(configPath: string): Promise<Config> {
try {
const configData = await readFile(configPath, 'utf8');
logger.debug(\`Reading config from \${configPath}\`);
return JSON.parse(configData);
} catch (error) {
logger.error(\`Failed to parse config from \${configPath}:\`, error);
throw error;
}
}
export {
processFile,
validateInput,
writeOutput,
parseConfig,
type Config
};`
const result = await strategy.applyDiff(original, diff)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(expected)
}
})
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -1,228 +0,0 @@
import { UnifiedDiffStrategy } from "../unified"
describe("UnifiedDiffStrategy", () => {
let strategy: UnifiedDiffStrategy
beforeEach(() => {
strategy = new UnifiedDiffStrategy()
})
describe("getToolDescription", () => {
it("should return tool description with correct cwd", () => {
const cwd = "/test/path"
const description = strategy.getToolDescription({ cwd })
expect(description).toContain("apply_diff")
expect(description).toContain(cwd)
expect(description).toContain("Parameters:")
expect(description).toContain("Format Requirements:")
})
})
describe("applyDiff", () => {
it("should successfully apply a function modification diff", async () => {
const originalContent = `import { Logger } from '../logger';
function calculateTotal(items: number[]): number {
return items.reduce((sum, item) => {
return sum + item;
}, 0);
}
export { calculateTotal };`
const diffContent = `--- src/utils/helper.ts
+++ src/utils/helper.ts
@@ -1,9 +1,10 @@
import { Logger } from '../logger';
function calculateTotal(items: number[]): number {
- return items.reduce((sum, item) => {
- return sum + item;
+ const total = items.reduce((sum, item) => {
+ return sum + item * 1.1; // Add 10% markup
}, 0);
+ return Math.round(total * 100) / 100; // Round to 2 decimal places
}
export { calculateTotal };`
const expected = `import { Logger } from '../logger';
function calculateTotal(items: number[]): number {
const total = items.reduce((sum, item) => {
return sum + item * 1.1; // Add 10% markup
}, 0);
return Math.round(total * 100) / 100; // Round to 2 decimal places
}
export { calculateTotal };`
const result = await strategy.applyDiff(originalContent, diffContent)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(expected)
}
})
it("should successfully apply a diff adding a new method", async () => {
const originalContent = `class Calculator {
add(a: number, b: number): number {
return a + b;
}
}`
const diffContent = `--- src/Calculator.ts
+++ src/Calculator.ts
@@ -1,5 +1,9 @@
class Calculator {
add(a: number, b: number): number {
return a + b;
}
+
+ multiply(a: number, b: number): number {
+ return a * b;
+ }
}`
const expected = `class Calculator {
add(a: number, b: number): number {
return a + b;
}
multiply(a: number, b: number): number {
return a * b;
}
}`
const result = await strategy.applyDiff(originalContent, diffContent)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(expected)
}
})
it("should successfully apply a diff modifying imports", async () => {
const originalContent = `import { useState } from 'react';
import { Button } from './components';
function App() {
const [count, setCount] = useState(0);
return <Button onClick={() => setCount(count + 1)}>{count}</Button>;
}`
const diffContent = `--- src/App.tsx
+++ src/App.tsx
@@ -1,7 +1,8 @@
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
import { Button } from './components';
function App() {
const [count, setCount] = useState(0);
+ useEffect(() => { document.title = \`Count: \${count}\` }, [count]);
return <Button onClick={() => setCount(count + 1)}>{count}</Button>;
}`
const expected = `import { useState, useEffect } from 'react';
import { Button } from './components';
function App() {
const [count, setCount] = useState(0);
useEffect(() => { document.title = \`Count: \${count}\` }, [count]);
return <Button onClick={() => setCount(count + 1)}>{count}</Button>;
}`
const result = await strategy.applyDiff(originalContent, diffContent)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(expected)
}
})
it("should successfully apply a diff with multiple hunks", async () => {
const originalContent = `import { readFile, writeFile } from 'fs';
function processFile(path: string) {
readFile(path, 'utf8', (err, data) => {
if (err) throw err;
const processed = data.toUpperCase();
writeFile(path, processed, (err) => {
if (err) throw err;
});
});
}
export { processFile };`
const diffContent = `--- src/file-processor.ts
+++ src/file-processor.ts
@@ -1,12 +1,14 @@
-import { readFile, writeFile } from 'fs';
+import { promises as fs } from 'fs';
+import { join } from 'path';
-function processFile(path: string) {
- readFile(path, 'utf8', (err, data) => {
- if (err) throw err;
+async function processFile(path: string) {
+ try {
+ const data = await fs.readFile(join(__dirname, path), 'utf8');
const processed = data.toUpperCase();
- writeFile(path, processed, (err) => {
- if (err) throw err;
- });
- });
+ await fs.writeFile(join(__dirname, path), processed);
+ } catch (error) {
+ console.error('Failed to process file:', error);
+ throw error;
+ }
}
export { processFile };`
const expected = `import { promises as fs } from 'fs';
import { join } from 'path';
async function processFile(path: string) {
try {
const data = await fs.readFile(join(__dirname, path), 'utf8');
const processed = data.toUpperCase();
await fs.writeFile(join(__dirname, path), processed);
} catch (error) {
console.error('Failed to process file:', error);
throw error;
}
}
export { processFile };`
const result = await strategy.applyDiff(originalContent, diffContent)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(expected)
}
})
it("should handle empty original content", async () => {
const originalContent = ""
const diffContent = `--- empty.ts
+++ empty.ts
@@ -0,0 +1,3 @@
+export function greet(name: string): string {
+ return \`Hello, \${name}!\`;
+}`
const expected = `export function greet(name: string): string {
return \`Hello, \${name}!\`;
}\n`
const result = await strategy.applyDiff(originalContent, diffContent)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(expected)
}
})
})
})

View File

@@ -1,295 +0,0 @@
import { applyContextMatching, applyDMP, applyGitFallback } from "../edit-strategies"
import { Hunk } from "../types"
const testCases = [
{
name: "should return original content if no match is found",
hunk: {
changes: [
{ type: "context", content: "line1" },
{ type: "add", content: "line2" },
],
} as Hunk,
content: ["line1", "line3"],
matchPosition: -1,
expected: {
confidence: 0,
result: ["line1", "line3"],
},
expectedResult: "line1\nline3",
strategies: ["context", "dmp"],
},
{
name: "should apply a simple add change",
hunk: {
changes: [
{ type: "context", content: "line1" },
{ type: "add", content: "line2" },
],
} as Hunk,
content: ["line1", "line3"],
matchPosition: 0,
expected: {
confidence: 1,
result: ["line1", "line2", "line3"],
},
expectedResult: "line1\nline2\nline3",
strategies: ["context", "dmp"],
},
{
name: "should apply a simple remove change",
hunk: {
changes: [
{ type: "context", content: "line1" },
{ type: "remove", content: "line2" },
],
} as Hunk,
content: ["line1", "line2", "line3"],
matchPosition: 0,
expected: {
confidence: 1,
result: ["line1", "line3"],
},
expectedResult: "line1\nline3",
strategies: ["context", "dmp"],
},
{
name: "should apply a simple context change",
hunk: {
changes: [{ type: "context", content: "line1" }],
} as Hunk,
content: ["line1", "line2", "line3"],
matchPosition: 0,
expected: {
confidence: 1,
result: ["line1", "line2", "line3"],
},
expectedResult: "line1\nline2\nline3",
strategies: ["context", "dmp"],
},
{
name: "should apply a multi-line add change",
hunk: {
changes: [
{ type: "context", content: "line1" },
{ type: "add", content: "line2\nline3" },
],
} as Hunk,
content: ["line1", "line4"],
matchPosition: 0,
expected: {
confidence: 1,
result: ["line1", "line2\nline3", "line4"],
},
expectedResult: "line1\nline2\nline3\nline4",
strategies: ["context", "dmp"],
},
{
name: "should apply a multi-line remove change",
hunk: {
changes: [
{ type: "context", content: "line1" },
{ type: "remove", content: "line2\nline3" },
],
} as Hunk,
content: ["line1", "line2", "line3", "line4"],
matchPosition: 0,
expected: {
confidence: 1,
result: ["line1", "line4"],
},
expectedResult: "line1\nline4",
strategies: ["context", "dmp"],
},
{
name: "should apply a multi-line context change",
hunk: {
changes: [
{ type: "context", content: "line1" },
{ type: "context", content: "line2\nline3" },
],
} as Hunk,
content: ["line1", "line2", "line3", "line4"],
matchPosition: 0,
expected: {
confidence: 1,
result: ["line1", "line2\nline3", "line4"],
},
expectedResult: "line1\nline2\nline3\nline4",
strategies: ["context", "dmp"],
},
{
name: "should apply a change with indentation",
hunk: {
changes: [
{ type: "context", content: " line1" },
{ type: "add", content: " line2" },
],
} as Hunk,
content: [" line1", " line3"],
matchPosition: 0,
expected: {
confidence: 1,
result: [" line1", " line2", " line3"],
},
expectedResult: " line1\n line2\n line3",
strategies: ["context", "dmp"],
},
{
name: "should apply a change with mixed indentation",
hunk: {
changes: [
{ type: "context", content: "\tline1" },
{ type: "add", content: " line2" },
],
} as Hunk,
content: ["\tline1", " line3"],
matchPosition: 0,
expected: {
confidence: 1,
result: ["\tline1", " line2", " line3"],
},
expectedResult: "\tline1\n line2\n line3",
strategies: ["context", "dmp"],
},
{
name: "should apply a change with mixed indentation and multi-line",
hunk: {
changes: [
{ type: "context", content: " line1" },
{ type: "add", content: "\tline2\n line3" },
],
} as Hunk,
content: [" line1", " line4"],
matchPosition: 0,
expected: {
confidence: 1,
result: [" line1", "\tline2\n line3", " line4"],
},
expectedResult: " line1\n\tline2\n line3\n line4",
strategies: ["context", "dmp"],
},
{
name: "should apply a complex change with mixed indentation and multi-line",
hunk: {
changes: [
{ type: "context", content: " line1" },
{ type: "remove", content: " line2" },
{ type: "add", content: "\tline3\n line4" },
{ type: "context", content: " line5" },
],
} as Hunk,
content: [" line1", " line2", " line5", " line6"],
matchPosition: 0,
expected: {
confidence: 1,
result: [" line1", "\tline3\n line4", " line5", " line6"],
},
expectedResult: " line1\n\tline3\n line4\n line5\n line6",
strategies: ["context", "dmp"],
},
{
name: "should apply a complex change with mixed indentation and multi-line and context",
hunk: {
changes: [
{ type: "context", content: " line1" },
{ type: "remove", content: " line2" },
{ type: "add", content: "\tline3\n line4" },
{ type: "context", content: " line5" },
{ type: "context", content: " line6" },
],
} as Hunk,
content: [" line1", " line2", " line5", " line6", " line7"],
matchPosition: 0,
expected: {
confidence: 1,
result: [" line1", "\tline3\n line4", " line5", " line6", " line7"],
},
expectedResult: " line1\n\tline3\n line4\n line5\n line6\n line7",
strategies: ["context", "dmp"],
},
{
name: "should apply a complex change with mixed indentation and multi-line and context and a different match position",
hunk: {
changes: [
{ type: "context", content: " line1" },
{ type: "remove", content: " line2" },
{ type: "add", content: "\tline3\n line4" },
{ type: "context", content: " line5" },
{ type: "context", content: " line6" },
],
} as Hunk,
content: [" line0", " line1", " line2", " line5", " line6", " line7"],
matchPosition: 1,
expected: {
confidence: 1,
result: [" line0", " line1", "\tline3\n line4", " line5", " line6", " line7"],
},
expectedResult: " line0\n line1\n\tline3\n line4\n line5\n line6\n line7",
strategies: ["context", "dmp"],
},
]
describe("applyContextMatching", () => {
testCases.forEach(({ name, hunk, content, matchPosition, expected, strategies, expectedResult }) => {
if (!strategies?.includes("context")) {
return
}
it(name, () => {
const result = applyContextMatching(hunk, content, matchPosition)
expect(result.result.join("\n")).toEqual(expectedResult)
expect(result.confidence).toBeGreaterThanOrEqual(expected.confidence)
expect(result.strategy).toBe("context")
})
})
})
describe("applyDMP", () => {
testCases.forEach(({ name, hunk, content, matchPosition, expected, strategies, expectedResult }) => {
if (!strategies?.includes("dmp")) {
return
}
it(name, () => {
const result = applyDMP(hunk, content, matchPosition)
expect(result.result.join("\n")).toEqual(expectedResult)
expect(result.confidence).toBeGreaterThanOrEqual(expected.confidence)
expect(result.strategy).toBe("dmp")
})
})
})
describe("applyGitFallback", () => {
it("should successfully apply changes using git operations", async () => {
const hunk = {
changes: [
{ type: "context", content: "line1", indent: "" },
{ type: "remove", content: "line2", indent: "" },
{ type: "add", content: "new line2", indent: "" },
{ type: "context", content: "line3", indent: "" },
],
} as Hunk
const content = ["line1", "line2", "line3"]
const result = await applyGitFallback(hunk, content)
expect(result.result.join("\n")).toEqual("line1\nnew line2\nline3")
expect(result.confidence).toBe(1)
expect(result.strategy).toBe("git-fallback")
})
it("should return original content with 0 confidence when changes cannot be applied", async () => {
const hunk = {
changes: [
{ type: "context", content: "nonexistent", indent: "" },
{ type: "add", content: "new line", indent: "" },
],
} as Hunk
const content = ["line1", "line2", "line3"]
const result = await applyGitFallback(hunk, content)
expect(result.result).toEqual(content)
expect(result.confidence).toBe(0)
expect(result.strategy).toBe("git-fallback")
})
})

View File

@@ -1,262 +0,0 @@
import { findAnchorMatch, findExactMatch, findSimilarityMatch, findLevenshteinMatch } from "../search-strategies"
type SearchStrategy = (
searchStr: string,
content: string[],
startIndex?: number,
) => {
index: number
confidence: number
strategy: string
}
const testCases = [
{
name: "should return no match if the search string is not found",
searchStr: "not found",
content: ["line1", "line2", "line3"],
expected: { index: -1, confidence: 0 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match if the search string is found",
searchStr: "line2",
content: ["line1", "line2", "line3"],
expected: { index: 1, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match with correct index when startIndex is provided",
searchStr: "line3",
content: ["line1", "line2", "line3", "line4", "line3"],
startIndex: 3,
expected: { index: 4, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match even if there are more lines in content",
searchStr: "line2",
content: ["line1", "line2", "line3", "line4", "line5"],
expected: { index: 1, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match even if the search string is at the beginning of the content",
searchStr: "line1",
content: ["line1", "line2", "line3"],
expected: { index: 0, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match even if the search string is at the end of the content",
searchStr: "line3",
content: ["line1", "line2", "line3"],
expected: { index: 2, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match for a multi-line search string",
searchStr: "line2\nline3",
content: ["line1", "line2", "line3", "line4"],
expected: { index: 1, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return no match if a multi-line search string is not found",
searchStr: "line2\nline4",
content: ["line1", "line2", "line3", "line4"],
expected: { index: -1, confidence: 0 },
strategies: ["exact", "similarity"],
},
{
name: "should return a match with indentation",
searchStr: " line2",
content: ["line1", " line2", "line3"],
expected: { index: 1, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match with more complex indentation",
searchStr: " line3",
content: [" line1", " line2", " line3", " line4"],
expected: { index: 2, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match with mixed indentation",
searchStr: "\tline2",
content: [" line1", "\tline2", " line3"],
expected: { index: 1, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match with mixed indentation and multi-line",
searchStr: " line2\n\tline3",
content: ["line1", " line2", "\tline3", " line4"],
expected: { index: 1, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return no match if mixed indentation and multi-line is not found",
searchStr: " line2\n line4",
content: ["line1", " line2", "\tline3", " line4"],
expected: { index: -1, confidence: 0 },
strategies: ["exact", "similarity"],
},
{
name: "should return a match with leading and trailing spaces",
searchStr: " line2 ",
content: ["line1", " line2 ", "line3"],
expected: { index: 1, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match with leading and trailing tabs",
searchStr: "\tline2\t",
content: ["line1", "\tline2\t", "line3"],
expected: { index: 1, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match with mixed leading and trailing spaces and tabs",
searchStr: " \tline2\t ",
content: ["line1", " \tline2\t ", "line3"],
expected: { index: 1, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return a match with mixed leading and trailing spaces and tabs and multi-line",
searchStr: " \tline2\t \n line3 ",
content: ["line1", " \tline2\t ", " line3 ", "line4"],
expected: { index: 1, confidence: 1 },
strategies: ["exact", "similarity", "levenshtein"],
},
{
name: "should return no match if mixed leading and trailing spaces and tabs and multi-line is not found",
searchStr: " \tline2\t \n line4 ",
content: ["line1", " \tline2\t ", " line3 ", "line4"],
expected: { index: -1, confidence: 0 },
strategies: ["exact", "similarity"],
},
]
describe("findExactMatch", () => {
testCases.forEach(({ name, searchStr, content, startIndex, expected, strategies }) => {
if (!strategies?.includes("exact")) {
return
}
it(name, () => {
const result = findExactMatch(searchStr, content, startIndex)
expect(result.index).toBe(expected.index)
expect(result.confidence).toBeGreaterThanOrEqual(expected.confidence)
expect(result.strategy).toMatch(/exact(-overlapping)?/)
})
})
})
describe("findAnchorMatch", () => {
const anchorTestCases = [
{
name: "should return no match if no anchors are found",
searchStr: " \n \n ",
content: ["line1", "line2", "line3"],
expected: { index: -1, confidence: 0 },
},
{
name: "should return no match if anchor positions cannot be validated",
searchStr: "unique line\ncontext line 1\ncontext line 2",
content: [
"different line 1",
"different line 2",
"different line 3",
"another unique line",
"context line 1",
"context line 2",
],
expected: { index: -1, confidence: 0 },
},
{
name: "should return a match if anchor positions can be validated",
searchStr: "unique line\ncontext line 1\ncontext line 2",
content: ["line1", "line2", "unique line", "context line 1", "context line 2", "line 6"],
expected: { index: 2, confidence: 1 },
},
{
name: "should return a match with correct index when startIndex is provided",
searchStr: "unique line\ncontext line 1\ncontext line 2",
content: ["line1", "line2", "line3", "unique line", "context line 1", "context line 2", "line 7"],
startIndex: 3,
expected: { index: 3, confidence: 1 },
},
{
name: "should return a match even if there are more lines in content",
searchStr: "unique line\ncontext line 1\ncontext line 2",
content: [
"line1",
"line2",
"unique line",
"context line 1",
"context line 2",
"line 6",
"extra line 1",
"extra line 2",
],
expected: { index: 2, confidence: 1 },
},
{
name: "should return a match even if the anchor is at the beginning of the content",
searchStr: "unique line\ncontext line 1\ncontext line 2",
content: ["unique line", "context line 1", "context line 2", "line 6"],
expected: { index: 0, confidence: 1 },
},
{
name: "should return a match even if the anchor is at the end of the content",
searchStr: "unique line\ncontext line 1\ncontext line 2",
content: ["line1", "line2", "unique line", "context line 1", "context line 2"],
expected: { index: 2, confidence: 1 },
},
{
name: "should return no match if no valid anchor is found",
searchStr: "non-unique line\ncontext line 1\ncontext line 2",
content: ["line1", "line2", "non-unique line", "context line 1", "context line 2", "non-unique line"],
expected: { index: -1, confidence: 0 },
},
]
anchorTestCases.forEach(({ name, searchStr, content, startIndex, expected }) => {
it(name, () => {
const result = findAnchorMatch(searchStr, content, startIndex)
expect(result.index).toBe(expected.index)
expect(result.confidence).toBeGreaterThanOrEqual(expected.confidence)
expect(result.strategy).toBe("anchor")
})
})
})
describe("findSimilarityMatch", () => {
testCases.forEach(({ name, searchStr, content, startIndex, expected, strategies }) => {
if (!strategies?.includes("similarity")) {
return
}
it(name, () => {
const result = findSimilarityMatch(searchStr, content, startIndex)
expect(result.index).toBe(expected.index)
expect(result.confidence).toBeGreaterThanOrEqual(expected.confidence)
expect(result.strategy).toBe("similarity")
})
})
})
describe("findLevenshteinMatch", () => {
testCases.forEach(({ name, searchStr, content, startIndex, expected, strategies }) => {
if (!strategies?.includes("levenshtein")) {
return
}
it(name, () => {
const result = findLevenshteinMatch(searchStr, content, startIndex)
expect(result.index).toBe(expected.index)
expect(result.confidence).toBeGreaterThanOrEqual(expected.confidence)
expect(result.strategy).toBe("levenshtein")
})
})
})

View File

@@ -157,7 +157,7 @@ export async function applyGitFallback(app: App, hunk: Hunk, content: string[]):
const vaultBasePath = adapter.getBasePath();
const tmpGitPath = normalizePath(path.join(vaultBasePath, ".tmp_git"));
console.log("tmpGitPath", tmpGitPath)
// console.log("tmpGitPath", tmpGitPath)
try {
const exists = await adapter.exists(tmpGitPath);
@@ -190,22 +190,22 @@ export async function applyGitFallback(app: App, hunk: Hunk, content: string[]):
fs.writeFileSync(filePath, originalText)
await git.add("file.txt")
const originalCommit = await git.commit("original")
console.log("Strategy 1 - Original commit:", originalCommit.commit)
// console.log("Strategy 1 - Original commit:", originalCommit.commit)
fs.writeFileSync(filePath, searchText)
await git.add("file.txt")
const searchCommit1 = await git.commit("search")
console.log("Strategy 1 - Search commit:", searchCommit1.commit)
// const searchCommit1 = await git.commit("search")
// console.log("Strategy 1 - Search commit:", searchCommit1.commit)
fs.writeFileSync(filePath, replaceText)
await git.add("file.txt")
const replaceCommit = await git.commit("replace")
console.log("Strategy 1 - Replace commit:", replaceCommit.commit)
// console.log("Strategy 1 - Replace commit:", replaceCommit.commit)
console.log("Strategy 1 - Attempting checkout of:", originalCommit.commit)
// console.log("Strategy 1 - Attempting checkout of:", originalCommit.commit)
await git.raw(["checkout", originalCommit.commit])
try {
console.log("Strategy 1 - Attempting cherry-pick of:", replaceCommit.commit)
// console.log("Strategy 1 - Attempting cherry-pick of:", replaceCommit.commit)
await git.raw(["cherry-pick", "--minimal", replaceCommit.commit])
const newText = fs.readFileSync(filePath, "utf-8")
@@ -231,23 +231,23 @@ export async function applyGitFallback(app: App, hunk: Hunk, content: string[]):
await git.add("file.txt")
const searchCommit = await git.commit("search")
const searchHash = searchCommit.commit.replace(/^HEAD /, "")
console.log("Strategy 2 - Search commit:", searchHash)
// console.log("Strategy 2 - Search commit:", searchHash)
fs.writeFileSync(filePath, replaceText)
await git.add("file.txt")
const replaceCommit = await git.commit("replace")
const replaceHash = replaceCommit.commit.replace(/^HEAD /, "")
console.log("Strategy 2 - Replace commit:", replaceHash)
// console.log("Strategy 2 - Replace commit:", replaceHash)
console.log("Strategy 2 - Attempting checkout of:", searchHash)
// console.log("Strategy 2 - Attempting checkout of:", searchHash)
await git.raw(["checkout", searchHash])
fs.writeFileSync(filePath, originalText)
await git.add("file.txt")
const originalCommit2 = await git.commit("original")
console.log("Strategy 2 - Original commit:", originalCommit2.commit)
// const originalCommit2 = await git.commit("original")
// console.log("Strategy 2 - Original commit:", originalCommit2.commit)
try {
console.log("Strategy 2 - Attempting cherry-pick of:", replaceHash)
// console.log("Strategy 2 - Attempting cherry-pick of:", replaceHash)
await git.raw(["cherry-pick", "--minimal", replaceHash])
const newText = fs.readFileSync(filePath, "utf-8")
@@ -287,7 +287,7 @@ export async function applyEdit(
): Promise<EditResult> {
// Don't attempt regular edits if confidence is too low
if (confidence < confidenceThreshold) {
console.log(
console.warn(
`Search confidence (${confidence}) below minimum threshold (${confidenceThreshold}), trying git fallback...`,
)
return applyGitFallback(app, hunk, content)

View File

@@ -27,7 +27,6 @@ export class NewUnifiedDiffStrategy implements DiffStrategy {
private parseUnifiedDiff(diff: string): Diff {
const MAX_CONTEXT_LINES = 6 // Number of context lines to keep before/after changes
const lines = diff.split("\n")
// console.log("lines: ", lines)
const hunks: Hunk[] = []
let currentHunk: Hunk | null = null
@@ -269,7 +268,7 @@ Your diff here
strategy,
} = findBestMatch(contextStr, result, 0, this.confidenceThreshold)
if (confidence < this.confidenceThreshold) {
console.log("Full hunk application failed, trying sub-hunks strategy")
console.warn("Full hunk application failed, trying sub-hunks strategy")
// Try splitting the hunk into smaller hunks
const subHunks = this.splitHunk(hunk)
let subHunkSuccess = true

View File

@@ -199,16 +199,12 @@ export function findExactMatch(
startIndex: number = 0,
confidenceThreshold: number = 0.97,
): SearchResult {
// console.log("searchStr: ", searchStr)
// console.log("content: ", content)
const searchLines = searchStr.split("\n")
const windows = createOverlappingWindows(content.slice(startIndex), searchLines.length)
const matches: (SearchResult & { windowIndex: number })[] = []
windows.forEach((windowData, windowIndex) => {
const windowStr = windowData.window.join("\n")
// console.log("searchStr: ", searchStr)
// console.log("windowStr:", windowStr)
const exactMatch = windowStr.indexOf(searchStr)
if (exactMatch !== -1) {
@@ -404,18 +400,10 @@ export function findBestMatch(
for (const strategy of strategies) {
const result = strategy(searchStr, content, startIndex, confidenceThreshold)
if (searchStr === "由于年久失修,街区路面坑洼不平,污水横流,垃圾遍地,甚至可见弹痕血迹。") {
console.log("findBestMatch result: ", strategy.name, result)
}
if (result.confidence > bestResult.confidence) {
bestResult = result
}
}
// if (bestResult.confidence < 0.97) {
// console.log("searchStr: ", searchStr)
// console.log("content: ", content)
// console.log("findBestMatch result: ", bestResult)
// }
return bestResult
}