Describe the bug
The edit tool from @cloudflare/computer/tools saves a rewritten copy of the whole file whenever oldText doesn't match exactly.
When the exact match misses, matching falls back to a loosened copy of the file: trailing whitespace stripped from every line, smart quotes and dashes flattened to ASCII, Unicode normalization applied. The fallback itself is fine. The problem is that applyEditsToNormalizedContent then uses that same loosened copy as the base for the write, so the whole file gets saved in loosened form.
The diff the tool returns is computed between the loosened copy and the loosened copy plus the edit, so none of the extra changes show up in the result. The caller sees a clean one line change.
One stray trailing space in oldText is enough to trigger it.
Expected behavior
Only the text the edit targeted changes. The rest of the file stays byte for byte identical, and the returned diff and patch describe what actually landed.
Steps to reproduce
- Save this as
packages/computer/src/tools/fs/repro.test.ts. The quotes around must are U+201C/U+201D, the dash is U+2014, and the two trailing spaces after optional. are a Markdown line break, so all three matter.
import { SQLiteTestStorage } from "@cloudflare/dofs/testing";
import { expect, it } from "vitest";
import { Workspace } from "../../workspace.js";
import { createAITools } from "../index.js";
it("only changes the text the edit targeted", async () => {
const workspace = new Workspace({ storage: new SQLiteTestStorage() });
const tools = createAITools({ workspace });
const original = 'The spec says “must” — not optional. \nlet target = 1;\n';
await workspace.fs.mkdir("/workspace", { recursive: true });
await workspace.fs.writeFile("/workspace/notes.md", original);
// One stray trailing space, so the exact match misses.
const result = await tools.edit.execute(
{
path: "/workspace/notes.md",
edits: [{ oldText: "let target = 1; ", newText: "let target = 2;" }],
},
{ toolCallId: "1", messages: [] },
);
console.log("reported diff:\n%s", result.diff);
await expect(workspace.fs.readFile("/workspace/notes.md", "utf8")).resolves.toBe(
original.replace("let target = 1;", "let target = 2;"),
);
});
-
Run npm test --workspace @cloudflare/computer -- src/tools/fs/repro.test.ts
-
The test fails. On disk the first line comes back as The spec says "must" - not optional., with ASCII quotes, a plain hyphen and no trailing spaces, so the Markdown line break is gone too. The logged diff only mentions the let target line.
Environment
@cloudflare/computer 0.1.0-alpha.1 at 76d9e75, Node 22.22.2 on Linux, run through the package's own vitest config. No Workers or Containers setup needed, since the tool runs against a Workspace backed by SQLiteTestStorage.
Happy to send a fix if you want one. What I have working locally keeps the loosened copy for matching, but records where each of its characters came from in the original, so the replacement can be spliced into the original file instead. Two cases have no honest source range and error out rather than guess: Unicode normalization merging a letter and an accent into a single character, and a match covering half of a character that expanded, like "af" against the ligature in "afib". edit-diff.ts has no tests today so it comes with them, plus one at the workspace level that reads the file back after an edit. Full suite passes.
Opening this as an issue rather than a pull request, since I see external ones get closed automatically without the allow-pr label.
Describe the bug
The
edittool from@cloudflare/computer/toolssaves a rewritten copy of the whole file wheneveroldTextdoesn't match exactly.When the exact match misses, matching falls back to a loosened copy of the file: trailing whitespace stripped from every line, smart quotes and dashes flattened to ASCII, Unicode normalization applied. The fallback itself is fine. The problem is that
applyEditsToNormalizedContentthen uses that same loosened copy as the base for the write, so the whole file gets saved in loosened form.The
diffthe tool returns is computed between the loosened copy and the loosened copy plus the edit, so none of the extra changes show up in the result. The caller sees a clean one line change.One stray trailing space in
oldTextis enough to trigger it.Expected behavior
Only the text the edit targeted changes. The rest of the file stays byte for byte identical, and the returned
diffandpatchdescribe what actually landed.Steps to reproduce
packages/computer/src/tools/fs/repro.test.ts. The quotes aroundmustareU+201C/U+201D, the dash isU+2014, and the two trailing spaces afteroptional.are a Markdown line break, so all three matter.Run
npm test --workspace @cloudflare/computer -- src/tools/fs/repro.test.tsThe test fails. On disk the first line comes back as
The spec says "must" - not optional., with ASCII quotes, a plain hyphen and no trailing spaces, so the Markdown line break is gone too. The logged diff only mentions thelet targetline.Environment
@cloudflare/computer0.1.0-alpha.1 at76d9e75, Node 22.22.2 on Linux, run through the package's own vitest config. No Workers or Containers setup needed, since the tool runs against aWorkspacebacked bySQLiteTestStorage.Happy to send a fix if you want one. What I have working locally keeps the loosened copy for matching, but records where each of its characters came from in the original, so the replacement can be spliced into the original file instead. Two cases have no honest source range and error out rather than guess: Unicode normalization merging a letter and an accent into a single character, and a match covering half of a character that expanded, like
"af"against the ligature in"afib".edit-diff.tshas no tests today so it comes with them, plus one at the workspace level that reads the file back after an edit. Full suite passes.Opening this as an issue rather than a pull request, since I see external ones get closed automatically without the
allow-prlabel.