a5293b2fa6
- Added multi-cell Python execution with sequential processing in persistent kernel. - Changed Python tool API to use cells array instead of single code parameter. - Renamed workdir parameter to cwd across Bash and Python tools for consistency. - Fixed indentation adjustment logic for mixed indentation levels in patch tool.
210 lines
7.5 KiB
TypeScript
210 lines
7.5 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { adjustIndentation, DEFAULT_FUZZY_THRESHOLD, findEditMatch } from "../src/core/tools/patch";
|
|
|
|
describe("findEditMatch", () => {
|
|
describe("exact matching", () => {
|
|
test("finds exact match", () => {
|
|
const content = "line1\nline2\nline3";
|
|
const target = "line2";
|
|
const result = findEditMatch(content, target, { allowFuzzy: false });
|
|
expect(result.match).toBeDefined();
|
|
expect(result.match!.confidence).toBe(1);
|
|
expect(result.match!.startLine).toBe(2);
|
|
});
|
|
|
|
test("reports multiple occurrences", () => {
|
|
const content = "foo\nbar\nfoo";
|
|
const target = "foo";
|
|
const result = findEditMatch(content, target, { allowFuzzy: false });
|
|
expect(result.match).toBeUndefined();
|
|
expect(result.occurrences).toBe(2);
|
|
});
|
|
|
|
test("returns empty for no match", () => {
|
|
const content = "line1\nline2";
|
|
const target = "notfound";
|
|
const result = findEditMatch(content, target, { allowFuzzy: false });
|
|
expect(result.match).toBeUndefined();
|
|
expect(result.occurrences).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("tab/space normalization", () => {
|
|
test("matches tabs in file with spaces in target", () => {
|
|
const content = "\tfoo\n\t\tbar\n\tbaz";
|
|
const target = " foo\n bar\n baz";
|
|
const result = findEditMatch(content, target, { allowFuzzy: true });
|
|
expect(result.match).toBeDefined();
|
|
expect(result.match!.confidence).toBeGreaterThanOrEqual(DEFAULT_FUZZY_THRESHOLD);
|
|
});
|
|
|
|
test("matches spaces in file with tabs in target", () => {
|
|
const content = " foo\n bar\n baz";
|
|
const target = "\tfoo\n\t\tbar\n\tbaz";
|
|
const result = findEditMatch(content, target, { allowFuzzy: true });
|
|
expect(result.match).toBeDefined();
|
|
expect(result.match!.confidence).toBeGreaterThanOrEqual(DEFAULT_FUZZY_THRESHOLD);
|
|
});
|
|
|
|
test("matches different space counts with same relative structure", () => {
|
|
const content = " foo\n bar\n baz";
|
|
const target = " foo\n bar\n baz";
|
|
const result = findEditMatch(content, target, { allowFuzzy: true });
|
|
expect(result.match).toBeDefined();
|
|
expect(result.match!.confidence).toBeGreaterThanOrEqual(DEFAULT_FUZZY_THRESHOLD);
|
|
});
|
|
|
|
test("matches single line with different indentation", () => {
|
|
const content = 'prefix\n\t\t\t"value",\nsuffix';
|
|
const target = ' "value",';
|
|
const result = findEditMatch(content, target, { allowFuzzy: true });
|
|
expect(result.match).toBeDefined();
|
|
expect(result.match!.confidence).toBeGreaterThanOrEqual(DEFAULT_FUZZY_THRESHOLD);
|
|
});
|
|
});
|
|
|
|
describe("fallback for inconsistent indentation", () => {
|
|
test("matches despite one line with wrong indentation in file", () => {
|
|
const content = "\t\t\tline1\n\t\t\tline2\n\t\tline3\n\t\t\tline4";
|
|
const target = " line1\n line2\n line3\n line4";
|
|
const result = findEditMatch(content, target, { allowFuzzy: true });
|
|
expect(result.match).toBeDefined();
|
|
expect(result.match!.confidence).toBeGreaterThanOrEqual(DEFAULT_FUZZY_THRESHOLD);
|
|
});
|
|
|
|
test("matches when target has consistent indent but file varies", () => {
|
|
const content = " a\n b\n c\n d";
|
|
const target = " a\n b\n c\n d";
|
|
const result = findEditMatch(content, target, { allowFuzzy: true });
|
|
expect(result.match).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe("content matching", () => {
|
|
test("collapses internal whitespace", () => {
|
|
const content = "foo bar baz";
|
|
const target = "foo bar baz";
|
|
const result = findEditMatch(content, target, { allowFuzzy: true });
|
|
expect(result.match).toBeDefined();
|
|
expect(result.match!.confidence).toBeGreaterThanOrEqual(DEFAULT_FUZZY_THRESHOLD);
|
|
});
|
|
|
|
test("matches with trailing whitespace differences", () => {
|
|
const content = "line1 \nline2\t";
|
|
const target = "line1\nline2";
|
|
const result = findEditMatch(content, target, { allowFuzzy: true });
|
|
expect(result.match).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe("threshold behavior", () => {
|
|
test("respects custom similarity threshold", () => {
|
|
const content = "function foo() {}";
|
|
const target = "function bar() {}";
|
|
const strictResult = findEditMatch(content, target, {
|
|
allowFuzzy: true,
|
|
threshold: 0.99,
|
|
});
|
|
expect(strictResult.match).toBeUndefined();
|
|
|
|
const lenientResult = findEditMatch(content, target, {
|
|
allowFuzzy: true,
|
|
threshold: 0.7,
|
|
});
|
|
expect(lenientResult.match).toBeDefined();
|
|
});
|
|
|
|
test("reports fuzzyMatches count when multiple above threshold", () => {
|
|
const content = " item1\n item2\n item3";
|
|
const target = " itemX";
|
|
const result = findEditMatch(content, target, {
|
|
allowFuzzy: true,
|
|
threshold: 0.7,
|
|
});
|
|
expect(result.fuzzyMatches).toBeGreaterThan(1);
|
|
});
|
|
});
|
|
|
|
describe("edge cases", () => {
|
|
test("handles empty target", () => {
|
|
const content = "some content";
|
|
const result = findEditMatch(content, "", { allowFuzzy: true });
|
|
expect(result).toEqual({});
|
|
});
|
|
|
|
test("handles empty lines in content", () => {
|
|
const content = "line1\n\nline3";
|
|
const target = "line1\n\nline3";
|
|
const result = findEditMatch(content, target, { allowFuzzy: false });
|
|
expect(result.match).toBeDefined();
|
|
expect(result.match!.confidence).toBe(1);
|
|
});
|
|
|
|
test("handles target longer than content", () => {
|
|
const content = "short";
|
|
const target = "this is much longer than the content";
|
|
const result = findEditMatch(content, target, { allowFuzzy: true });
|
|
expect(result.match).toBeUndefined();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("adjustIndentation", () => {
|
|
test("adds indentation when actualText is more indented than oldText", () => {
|
|
const oldText = "foo\nbar";
|
|
const actualText = " foo\n bar";
|
|
const newText = "foo\nbaz\nbar";
|
|
const result = adjustIndentation(oldText, actualText, newText);
|
|
expect(result).toBe(" foo\n baz\n bar");
|
|
});
|
|
|
|
test("removes indentation when actualText is less indented", () => {
|
|
const oldText = " foo\n bar";
|
|
const actualText = " foo\n bar";
|
|
const newText = " foo\n baz";
|
|
const result = adjustIndentation(oldText, actualText, newText);
|
|
expect(result).toBe(" foo\n baz");
|
|
});
|
|
|
|
test("preserves empty lines", () => {
|
|
const oldText = "foo\n\nbar";
|
|
const actualText = " foo\n\n bar";
|
|
const newText = "foo\n\nbaz";
|
|
const result = adjustIndentation(oldText, actualText, newText);
|
|
expect(result).toBe(" foo\n\n baz");
|
|
});
|
|
|
|
test("returns unchanged when indentation matches", () => {
|
|
const oldText = " foo";
|
|
const actualText = " foo";
|
|
const newText = " bar";
|
|
const result = adjustIndentation(oldText, actualText, newText);
|
|
expect(result).toBe(" bar");
|
|
});
|
|
|
|
test("uses tab from actualText when adding indentation", () => {
|
|
const oldText = "foo";
|
|
const actualText = "\t\tfoo";
|
|
const newText = "bar";
|
|
const result = adjustIndentation(oldText, actualText, newText);
|
|
expect(result).toBe("\t\tbar");
|
|
});
|
|
|
|
test("handles mixed content with different indent levels", () => {
|
|
const oldText = "if (x) {\n return y;\n}";
|
|
const actualText = " if (x) {\n return y;\n }";
|
|
const newText = "if (x) {\n return z;\n}";
|
|
const result = adjustIndentation(oldText, actualText, newText);
|
|
expect(result).toBe(" if (x) {\n return z;\n }");
|
|
});
|
|
|
|
test("does not go negative on removal", () => {
|
|
const oldText = " foo";
|
|
const actualText = "foo";
|
|
const newText = " bar";
|
|
const result = adjustIndentation(oldText, actualText, newText);
|
|
// Should remove up to 4 chars, but line only has 2, so remove 2
|
|
expect(result).toBe("bar");
|
|
});
|
|
});
|