Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions internal-packages/ai/__tests__/searchReplace_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,44 @@ Goodbye, world!
expect(result.value).toBe("Goodbye, world!");
});

test("should skip no-op blocks when other blocks make changes", () => {
const originalText = "first line\nsecond line";
const promptResponse = `
<<<<<<< SEARCH
first line
=======
first line
>>>>>>> REPLACE

<<<<<<< SEARCH
second line
=======
changed line
>>>>>>> REPLACE
`;

const result = processSearchReplaceResponse(originalText, promptResponse);

expect(result.success).toBe(true);
expect(result.value).toBe("first line\nchanged line");
});

test("should fail when all blocks are no-ops", () => {
const originalText = "Hello, world!";
const promptResponse = `
<<<<<<< SEARCH
Hello, world!
=======
Hello, world!
>>>>>>> REPLACE
`;

const result = processSearchReplaceResponse(originalText, promptResponse);

expect(result.success).toBe(false);
expect(result.value).toContain("no-ops");
});

test("should handle empty prompt response", () => {
const originalText = "Hello, world!";
const promptResponse = "";
Expand Down
20 changes: 15 additions & 5 deletions internal-packages/ai/src/squiggle/searchReplace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,16 @@ function applySearchReplaceBlocks(
blocks: Array<{ search: string; replace: string }>
): { success: boolean; value: string } {
let updatedText = originalText;
// LLMs sometimes emit blocks where search === replace (e.g. an unchanged
// context block alongside real edits). Those are harmless no-ops — skip
// them instead of failing the whole edit. We only fail if *no* block
// changes anything, so the model gets feedback and retries.
let appliedBlocks = 0;

try {
for (const block of blocks) {
// Check if search and replace are identical
if (block.search === block.replace) {
return {
success: false,
value: `Error: Search and replace texts are identical: \n\`\`\`\n${block.search}\n\`\`\``,
};
continue;
}

if (block.search === "") {
Expand All @@ -81,6 +82,15 @@ function applySearchReplaceBlocks(
block.replace +
updatedText.slice(match.index + match[0].length);
}
appliedBlocks++;
}

if (appliedBlocks === 0) {
return {
success: false,
value:
"Error: All search/replace blocks were no-ops (SEARCH and REPLACE texts are identical), so the code was not changed. Put the modified text in the REPLACE section.",
};
}

return { success: true, value: updatedText };
Expand Down
Loading