Repository navigation
fix(apply-diff): report all failures and diagnose malformed markers #2053
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
simurg79
wants to merge
14
commits into
Zoo-Code-Org:main
Choose a base branch
from
simurg79:port/apply-diff-failure-reporting-pr15
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+831
−65
Open
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
8a2c1eb
fix(apply-diff): port bounded failure reporting from upstream PR15
541a0e2
fix(apply-diff): name target file in multi-block failure output
1cc062b
test(apply-diff): kill surviving mutants in failure reporting and mar…
738d68b
test(apply-diff): pass toolCallId required by ToolCallbacks on main
92d8de1
fix(apply-diff): count line-level markers and retain main guidance
560a68d
fix(apply-diff): avoid claiming failed blocks are absent; assert full…
64ddf47
fix(apply-diff): report partial-apply totals from the parsed block count
f2cf1c6
test(apply-diff): assert repaired first-block content
cf041bf
fix(apply_diff): limit file excerpts in multi-failure and partial-app…
b5b8cf8
fix(apply_diff): report only non-sensitive diagnostics on diff failures
20dcb7c
fix(apply_diff): stop echoing malformed marker text; align e2e error …
4da8b64
fix(apply_diff): guard marker-hint regex length, label failures by or…
e75fd92
style(apply_diff): split marker-hint guard to satisfy prettier
09b67e5
test(apply-diff): make overlong marker fixture exercise the length guard
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
177 changes: 177 additions & 0 deletions
177
src/core/diff/strategies/__tests__/multi-search-replace-marker-hint.spec.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,177 @@ | ||
| import { MultiSearchReplaceDiffStrategy } from "../multi-search-replace" | ||
|
|
||
| // Guards the POST-FAILURE "LIKELY CAUSE" hint for mistyped start-line markers. | ||
| // The heuristic must never act as an upfront rejection gate. | ||
| describe("MultiSearchReplaceDiffStrategy malformed start-line marker hint", () => { | ||
| let strategy: MultiSearchReplaceDiffStrategy | ||
|
|
||
| beforeEach(() => { | ||
| strategy = new MultiSearchReplaceDiffStrategy() | ||
| }) | ||
|
|
||
| const nonMatchingFile = ["alpha", "beta", "gamma", "delta", "epsilon"].join("\n") | ||
|
|
||
| function buildDiff(searchBody: string, replaceBody: string): string { | ||
| return `<<<<<<< SEARCH\n${searchBody}\n=======\n${replaceBody}\n>>>>>>> REPLACE` | ||
| } | ||
|
|
||
| function firstFailureError(result: Awaited<ReturnType<MultiSearchReplaceDiffStrategy["applyDiff"]>>): string { | ||
| expect(result.success).toBe(false) | ||
| const failures = result.failParts ?? [] | ||
| expect(failures.length).toBeGreaterThan(0) | ||
| const failure = failures[0] | ||
| if (failure.success || !failure.error) { | ||
| throw new Error("Expected a failed block with error details") | ||
| } | ||
| return failure.error | ||
| } | ||
|
|
||
| it("adds LIKELY CAUSE when the first search line is a bare ':3' marker and the block fails", async () => { | ||
| const diffContent = buildDiff(":3\nnot-in-the-file", "replacement") | ||
| const result = await strategy.applyDiff(nonMatchingFile, diffContent) | ||
|
|
||
| const errorText = firstFailureError(result) | ||
| expect(errorText).toContain("LIKELY CAUSE") | ||
| expect(errorText).toContain(":start_line:") | ||
| }) | ||
|
|
||
| const markerVariants = ["start_line:3", ":start-line 3", ": start_line : 3", ":START_LINE:3", "Start_Line: 3"] | ||
|
|
||
| it.each(markerVariants)("adds LIKELY CAUSE for the malformed marker variant %j", async (markerVariant) => { | ||
| const diffContent = buildDiff(`${markerVariant}\nnot-in-the-file`, "replacement") | ||
| const result = await strategy.applyDiff(nonMatchingFile, diffContent) | ||
|
|
||
| expect(firstFailureError(result)).toContain("LIKELY CAUSE") | ||
| }) | ||
|
|
||
| it.each([": 3", ":42", "startline:3", "start_line:42"])( | ||
| "adds LIKELY CAUSE for the spacing, multi-digit or separator-less marker %j", | ||
| async (markerVariant) => { | ||
| const diffContent = buildDiff(`${markerVariant}\nnot-in-the-file`, "replacement") | ||
| const result = await strategy.applyDiff(nonMatchingFile, diffContent) | ||
|
|
||
| const errorText = firstFailureError(result) | ||
| expect(errorText).toContain("first line of your search content looks like a malformed start-line marker") | ||
| expect(errorText).not.toContain(markerVariant) | ||
| }, | ||
| ) | ||
|
|
||
| it("skips the marker hint for an overlong whitespace-padded first line", async () => { | ||
| const paddedLine = `startline${" ".repeat(40_000)}3` | ||
| const diffContent = buildDiff(`${paddedLine}\nnot-in-the-file`, "replacement") | ||
|
|
||
| const startedAt = performance.now() | ||
| const errorText = firstFailureError(await strategy.applyDiff(nonMatchingFile, diffContent)) | ||
|
|
||
| expect(errorText).not.toContain("LIKELY CAUSE") | ||
| expect(performance.now() - startedAt).toBeLessThan(2_000) | ||
| }) | ||
|
|
||
| it("still hints for a padded marker within the length guard", async () => { | ||
| const diffContent = buildDiff(`start_line:${" ".repeat(40)}3\nnot-in-the-file`, "replacement") | ||
|
|
||
| expect(firstFailureError(await strategy.applyDiff(nonMatchingFile, diffContent))).toContain("LIKELY CAUSE") | ||
| }) | ||
|
|
||
| it.each(["value:3", ":3 trailing", "x start_line:3"])( | ||
| "omits LIKELY CAUSE when %j only contains a marker-like fragment", | ||
| async (firstLine) => { | ||
| const diffContent = buildDiff(`${firstLine}\nnot-in-the-file`, "replacement") | ||
| const result = await strategy.applyDiff(nonMatchingFile, diffContent) | ||
|
|
||
| expect(firstFailureError(result)).not.toContain("LIKELY CAUSE") | ||
| }, | ||
| ) | ||
|
|
||
| it("does not claim a separator was swallowed when none follows the marker", async () => { | ||
| const diffContent = buildDiff(":3\nnot-in-the-file", "replacement") | ||
| const result = await strategy.applyDiff(nonMatchingFile, diffContent) | ||
|
|
||
| const errorText = firstFailureError(result) | ||
| expect(errorText).toContain( | ||
| "looks like a malformed start-line marker and was therefore searched for as literal file text. The only recognized form", | ||
| ) | ||
| expect(errorText).not.toContain("together with") | ||
| }) | ||
|
|
||
| it("detects a swallowed separator even when it is indented", async () => { | ||
| const diffContent = buildDiff(" :start_line:3\n -------\nnot-in-the-file", "replacement") | ||
| const result = await strategy.applyDiff(nonMatchingFile, diffContent) | ||
|
|
||
| expect(firstFailureError(result)).toContain('together with the "-------" line after it') | ||
| }) | ||
|
|
||
| it("reports the real similarity, threshold and distance for a near miss", async () => { | ||
| const diffContent = buildDiff("alpha\nbetx", "replacement") | ||
| const result = await strategy.applyDiff(nonMatchingFile, diffContent) | ||
|
|
||
| const errorText = firstFailureError(result) | ||
| const similarity = /\((\d+)% similar, needs 100%\)\n\nDebug Info:\n- Similarity Score: (\d+)%\n/.exec(errorText) | ||
| expect(similarity).not.toBeNull() | ||
| expect(Number(similarity?.[1])).toBeGreaterThan(50) | ||
| expect(similarity?.[2]).toBe(similarity?.[1]) | ||
| expect(errorText).toMatch(/- Levenshtein Distance: [1-9]\d* characters\n/) | ||
| }) | ||
|
|
||
| it("still applies cleanly when the file genuinely contains a ':3' line (hint is not a gate)", async () => { | ||
| const fileWithMarkerLikeLine = ["alpha", ":3", "beta", "gamma"].join("\n") | ||
| const diffContent = buildDiff(":3\nbeta", ":3\nBETA") | ||
|
|
||
| const result = await strategy.applyDiff(fileWithMarkerLikeLine, diffContent) | ||
|
|
||
| expect(result.success).toBe(true) | ||
| if (!result.success) { | ||
| throw new Error("Expected marker-like file content to match") | ||
| } | ||
| expect(result.content).toBe(["alpha", ":3", "BETA", "gamma"].join("\n")) | ||
| expect(result.content).not.toContain("LIKELY CAUSE") | ||
| expect(result.failParts ?? []).toHaveLength(0) | ||
| }) | ||
|
|
||
| it("reports the parsed block count when repair splits an inline SEARCH marker", async () => { | ||
| const original = ["alpha", "beta"].join("\n") | ||
| const diffContent = "<<<<<<< SEARCH\nalpha\n=======\nALPHA x <<<<<<< SEARCH\nmissing\n>>>>>>> REPLACE" | ||
|
|
||
| const result = await strategy.applyDiff(original, diffContent) | ||
|
|
||
| expect(result.success).toBe(true) | ||
| if (!result.success) { | ||
| throw new Error("Expected the first repaired block to apply") | ||
| } | ||
| expect(result.content).toBe(["ALPHA x", "beta"].join("\n")) | ||
| expect(result.blockCount).toBe(2) | ||
| expect(result.failParts?.filter((part) => !part.success)).toHaveLength(1) | ||
| }) | ||
|
|
||
| it("omits the hint for ordinary non-matching content but keeps the file-changed tip", async () => { | ||
| const diffContent = buildDiff("completely unrelated content\nsecond line", "replacement") | ||
| const result = await strategy.applyDiff(nonMatchingFile, diffContent) | ||
|
|
||
| const errorText = firstFailureError(result) | ||
| expect(errorText).not.toContain("LIKELY CAUSE") | ||
| expect(errorText).toContain( | ||
| "- Tried both standard and aggressive line number stripping\n" + | ||
| "- Tip: Use the read_file tool to get the latest content of the file before attempting " + | ||
| "to use the apply_diff tool again, as the file content may have changed\n" + | ||
| "- Tip: The SEARCH block must contain complete, whole lines — partial-line (substring) matching is not supported. " + | ||
| "If the target line is very long, copy the entire line into the SEARCH block", | ||
| ) | ||
| }) | ||
|
|
||
| it("does not flag the correct ':start_line:3' marker form", async () => { | ||
| const diffContent = `<<<<<<< SEARCH\n:start_line:3\n-------\nnot-in-the-file\n=======\nreplacement\n>>>>>>> REPLACE` | ||
| const result = await strategy.applyDiff(nonMatchingFile, diffContent) | ||
|
|
||
| expect(firstFailureError(result)).not.toContain("LIKELY CAUSE") | ||
| }) | ||
|
|
||
| it("adds LIKELY CAUSE when a well-formed ':start_line:N' line goes unparsed and swallows its separator", async () => { | ||
| // Indenting the marker makes the parser miss it, so both it and the "-------" become search text. | ||
| const diffContent = buildDiff(" :start_line:3\n-------\nnot-in-the-file", "replacement") | ||
| const result = await strategy.applyDiff(nonMatchingFile, diffContent) | ||
|
|
||
| const errorText = firstFailureError(result) | ||
| expect(errorText).toContain("LIKELY CAUSE") | ||
| expect(errorText).toContain('together with the "-------" line after it') | ||
| }) | ||
| }) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.