Files
oh-my-pi/packages/coding-agent/test/core/atom.test.ts
T
can1357 863560ffb6 fix(coding-agent/edit): added atom range-repair and multi-section preflight checks
- Allowed bare `LidA..LidB` to recover a missing-range-delete typo and accepted `|` as a legacy range replacement separator while validating ranges and replacement text.
- Enabled indented hashline statements to parse as replacement edits and added a preflight pass that validates all atom sections before any file write occurs.
- Updated hash-mismatch messaging, prompt wording, and tests to reflect hash-only rebase candidates and the new range/section behaviors.
2026-04-30 00:43:27 +02:00

1149 lines
47 KiB
TypeScript

import { beforeAll, describe, expect, it } from "bun:test";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { _resetSettingsForTest, Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
import {
applyAtomEdits,
atomEditParamsSchema,
computeLineHash,
type ExecuteAtomSingleOptions,
executeAtomSingle,
HashlineMismatchError,
parseAtom,
parseAtomWithWarnings,
splitAtomInput,
splitAtomInputs,
} from "@oh-my-pi/pi-coding-agent/edit";
import type { ToolSession } from "@oh-my-pi/pi-coding-agent/tools";
import { Value } from "@sinclair/typebox/value";
beforeAll(async () => {
_resetSettingsForTest();
await Settings.init({ inMemory: true, cwd: process.cwd() });
});
function tag(line: number, content: string): string {
return `${line}${computeLineHash(line, content)}`;
}
function differentHash(hash: string): string {
return hash === "zz" ? "yy" : "zz";
}
function mistag(line: number, content: string): string {
return `${line}${differentHash(computeLineHash(line, content))}`;
}
// Convenience: parse a diff against a content snapshot and return the resulting text.
function applyDiff(content: string, diff: string): string {
const edits = parseAtom(diff);
return applyAtomEdits(content, edits).lines;
}
async function withTempDir(fn: (tempDir: string) => Promise<void>): Promise<void> {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "atom-edit-"));
try {
await fn(tempDir);
} finally {
await fs.rm(tempDir, { recursive: true, force: true });
}
}
function atomExecuteOptions(tempDir: string, input: string): ExecuteAtomSingleOptions {
return {
session: { cwd: tempDir } as ToolSession,
input,
writethrough: async () => {
throw new Error("unexpected write");
},
beginDeferredDiagnosticsForPath: () => {
throw new Error("unexpected diagnostics");
},
};
}
// ───────────────────────────────────────────────────────────────────────────
// Form coverage
// ───────────────────────────────────────────────────────────────────────────
describe("atom parser — basic forms", () => {
const content = "aaa\nbbb\nccc";
it("canonical set replaces a single line", () => {
const diff = `${tag(2, "bbb")}=BBB`;
expect(applyDiff(content, diff)).toBe("aaa\nBBB\nccc");
});
it("prefix delete `-Lid` removes a single line", () => {
const diff = `-${tag(2, "bbb")}`;
expect(applyDiff(content, diff)).toBe("aaa\nccc");
});
it("`@Lid` moves the cursor after the anchored line", () => {
const diff = `@${tag(2, "bbb")}\n+INSERTED`;
expect(applyDiff(content, diff)).toBe("aaa\nbbb\nINSERTED\nccc");
});
it("^ + + lines prepend to the file", () => {
const diff = `^\n+ZZZ\n+YYY`;
expect(applyDiff(content, diff)).toBe("ZZZ\nYYY\naaa\nbbb\nccc");
});
it("$ + + lines append to the file", () => {
const diff = `$\n+DDD\n+EEE`;
expect(applyDiff(content, diff)).toBe("aaa\nbbb\nccc\nDDD\nEEE");
});
it("+ lines with no cursor move append to the file", () => {
const diff = `+DDD\n+EEE`;
expect(applyDiff(content, diff)).toBe("aaa\nbbb\nccc\nDDD\nEEE");
});
it("`-LidA..LidB` deletes the inclusive line range", () => {
const longer = "aaa\nbbb\nccc\nddd\neee";
const diff = `-${tag(2, "bbb")}..${tag(4, "ddd")}`;
expect(applyDiff(longer, diff)).toBe("aaa\neee");
});
it("`-LidA..LidB` followed by `+TEXT` replaces the block", () => {
const longer = "aaa\nbbb\nccc\nddd\neee";
const diff = `-${tag(2, "bbb")}..${tag(4, "ddd")}\n+REPLACED`;
expect(applyDiff(longer, diff)).toBe("aaa\nREPLACED\neee");
});
it("bare `LidA..LidB` recovers a missing `-` range delete typo", () => {
const longer = "aaa\nbbb\nccc\nddd\neee";
const diff = `${tag(2, "bbb")}..${tag(4, "ddd")}`;
expect(applyDiff(longer, diff)).toBe("aaa\neee");
});
it("`-LidA..LidB` rejects a reversed range", () => {
const longer = "aaa\nbbb\nccc\nddd";
const diff = `-${tag(3, "ccc")}..${tag(2, "bbb")}`;
expect(() => applyDiff(longer, diff)).toThrow(/ends before it starts/);
});
it("`LidA..LidB=TEXT` replaces the inclusive range with one line", () => {
const longer = "aaa\nbbb\nccc\nddd\neee";
const diff = `${tag(2, "bbb")}..${tag(4, "ddd")}=REPLACED`;
expect(applyDiff(longer, diff)).toBe("aaa\nREPLACED\neee");
});
it("`LidA..LidB=TEXT` followed by `+TEXT` lines replaces with multiple lines", () => {
const longer = "aaa\nbbb\nccc\nddd\neee";
const diff = `${tag(2, "bbb")}..${tag(4, "ddd")}=ONE\n+TWO\n+THREE`;
expect(applyDiff(longer, diff)).toBe("aaa\nONE\nTWO\nTHREE\neee");
});
it("`LidA..LidB=` (empty TEXT) replaces the range with one blank line", () => {
const longer = "aaa\nbbb\nccc\nddd\neee";
const diff = `${tag(2, "bbb")}..${tag(4, "ddd")}=`;
expect(applyDiff(longer, diff)).toBe("aaa\n\neee");
});
it("`LidA..LidB=TEXT` rejects a reversed range", () => {
const longer = "aaa\nbbb\nccc\nddd";
const diff = `${tag(3, "ccc")}..${tag(2, "bbb")}=NOPE`;
expect(() => applyDiff(longer, diff)).toThrow(/ends before it starts/);
});
it("`LidA..LidB=TEXT` validates start and end hashes", () => {
const longer = "aaa\nbbb\nccc\nddd";
const diff = `${tag(2, "bbb")}..3xx=NOPE`;
expect(() => applyDiff(longer, diff)).toThrow();
});
it("`LidA..LidA=TEXT` rejects mismatched endpoint hashes", () => {
const longer = "aaa\nbbb\nccc";
const diff = `${tag(2, "bbb")}..${mistag(2, "bbb")}=NOPE`;
expect(() => applyDiff(longer, diff)).toThrow(/two different hashes for the same line/);
});
it("`-LidA..LidA` rejects mismatched endpoint hashes", () => {
const longer = "aaa\nbbb\nccc";
const diff = `-${tag(2, "bbb")}..${mistag(2, "bbb")}`;
expect(() => applyDiff(longer, diff)).toThrow(/two different hashes for the same line/);
});
it("`-LidA..LidB|TEXT` errors and suggests `LidA..LidB=TEXT`", () => {
const longer = "aaa\nbbb\nccc\nddd";
const diff = `-${tag(2, "bbb")}..${tag(3, "ccc")}|REPLACED`;
expect(() => applyDiff(longer, diff)).toThrow(/use `2.{2}\.\.3.{2}=REPLACED`/);
});
it("`-LidA..LidB=TEXT` errors and suggests `LidA..LidB=TEXT`", () => {
const longer = "aaa\nbbb\nccc\nddd";
const diff = `-${tag(2, "bbb")}..${tag(3, "ccc")}=REPLACED`;
expect(() => applyDiff(longer, diff)).toThrow(/use `2.{2}\.\.3.{2}=REPLACED`/);
});
it("`LidA..LidB=FIRST` accepts backslash continuation lines", () => {
const longer = "aaa\nbbb\nccc\nddd\neee";
const diff = [`${tag(2, "bbb")}..${tag(4, "ddd")}=export function label() {`, "\\ return 1;", "\\}"].join(
"\n",
);
expect(applyDiff(longer, diff)).toBe("aaa\nexport function label() {\n return 1;\n}\neee");
});
it("`LidA..LidB|FIRST` accepts legacy pipe as range replacement separator", () => {
const longer = "aaa\nbbb\nccc\nddd\neee";
const diff = [`${tag(2, "bbb")}..${tag(4, "ddd")}|ONE`, "\\TWO"].join("\n");
expect(applyDiff(longer, diff)).toBe("aaa\nONE\nTWO\neee");
});
it("bare backslash continuation inserts a blank replacement line", () => {
const longer = "aaa\nbbb\nccc\nddd\neee";
const diff = [`${tag(2, "bbb")}..${tag(4, "ddd")}=ONE`, "\\", "\\THREE"].join("\n");
expect(applyDiff(longer, diff)).toBe("aaa\nONE\n\nTHREE\neee");
});
it("backslash continuation preserves atom-shaped literal content", () => {
const longer = "aaa\nbbb\nccc\nddd";
const literalLines = [
"#include <x>",
"# Heading",
"+literal",
"-literal",
"@decorator",
"$literal",
"^literal",
"!literal",
"12ab=literal",
"\\literal",
];
const diff = [`${tag(2, "bbb")}..${tag(3, "ccc")}=first`, ...literalLines.map(line => `\\${line}`)].join("\n");
expect(applyDiff(longer, diff)).toBe(["aaa", "first", ...literalLines, "ddd"].join("\n"));
});
it("backslash continuation is rejected outside a replacement op", () => {
expect(() => parseAtom("\\orphan")).toThrow(/\\TEXT continuation is only valid/);
});
it("`Lid=FIRST` accepts backslash continuation lines (single-line set extends to multi-line)", () => {
const content = "aaa\nbbb\nccc";
const diff = [`${tag(2, "bbb")}=export function label() {`, "\\ return 1;", "\\}"].join("\n");
expect(applyDiff(content, diff)).toBe("aaa\nexport function label() {\n return 1;\n}\nccc");
});
it("`@Lid=FIRST` accepts backslash continuation lines (legacy `@`-prefixed form)", () => {
const content = "aaa\nbbb\nccc";
const diff = [`@${tag(2, "bbb")}=ONE`, "\\TWO"].join("\n");
expect(applyDiff(content, diff)).toBe("aaa\nONE\nTWO\nccc");
});
it("`Lid|FIRST` (legacy set syntax) accepts backslash continuation lines", () => {
const content = "aaa\nbbb\nccc";
const diff = [`${tag(2, "bbb")}|ONE`, "\\TWO"].join("\n");
expect(applyDiff(content, diff)).toBe("aaa\nONE\nTWO\nccc");
});
it("backslash continuation after `Lid=FIRST` rejects unprefixed recovery (range-only fallback)", () => {
const content = "aaa\nbbb\nccc";
// Only range replacements get the legacy "raw unprefixed continuation" recovery;
// after a single-line set, an unprefixed line below should parse as its own op
// (or error), not silently fold into the replacement.
const diff = [`${tag(2, "bbb")}=ONE`, "rawline"].join("\n");
expect(() => applyDiff(content, diff)).toThrow();
});
it("backslash continuation stops before the next atom op line", () => {
const longer = "aaa\nbbb\nccc\nddd\neee";
const diff = [`${tag(2, "bbb")}..${tag(4, "ddd")}=ONE`, "\\TWO", `@${tag(1, "aaa")}`, "+BELOW"].join("\n");
expect(applyDiff(longer, diff)).toBe("aaa\nBELOW\nONE\nTWO\neee");
});
it("range replacement accepts optional whitespace before `=` with continuation", () => {
const longer = "aaa\nbbb\nccc\nddd\neee";
const diff = [`${tag(2, "bbb")}..${tag(4, "ddd")} =ONE`, "\\TWO"].join("\n");
expect(applyDiff(longer, diff)).toBe("aaa\nONE\nTWO\neee");
});
it("range replacement preserves whitespace after `=` as literal text", () => {
const longer = "aaa\nbbb\nccc\nddd\neee";
const diff = `${tag(2, "bbb")}..${tag(4, "ddd")} = TEXT`;
expect(applyDiff(longer, diff)).toBe("aaa\n TEXT\neee");
});
it("raw unprefixed range continuation remains accepted as recovery", () => {
const longer = "aaa\nbbb\nccc\nddd\neee";
const diff = `${tag(2, "bbb")}..${tag(4, "ddd")}=ONE\nTWO\n# Heading`;
expect(applyDiff(longer, diff)).toBe("aaa\nONE\nTWO\n# Heading\neee");
});
it("`^Lid` moves the cursor BEFORE the anchored line", () => {
const diff = `^${tag(2, "bbb")}\n+INSERTED`;
expect(applyDiff(content, diff)).toBe("aaa\nINSERTED\nbbb\nccc");
});
it("`^Lid` followed by multiple `+TEXT` inserts in order before the line", () => {
const diff = `^${tag(3, "ccc")}\n+X\n+Y`;
expect(applyDiff(content, diff)).toBe("aaa\nbbb\nX\nY\nccc");
});
it("`^Lid` on the first line inserts above it", () => {
const diff = `^${tag(1, "aaa")}\n+TOP`;
expect(applyDiff(content, diff)).toBe("TOP\naaa\nbbb\nccc");
});
it("`# comment` lines are silently ignored", () => {
const diff = `# This is a section header\n${tag(2, "bbb")}=BBB\n# trailing note`;
expect(applyDiff(content, diff)).toBe("aaa\nBBB\nccc");
});
it("`^+TEXT` shorthand: BOF cursor + insert TEXT on one line", () => {
const diff = `^+TOP`;
expect(applyDiff(content, diff)).toBe("TOP\naaa\nbbb\nccc");
});
it("`^+` shorthand followed by additional inserts stacks correctly", () => {
const diff = `^+ONE\n+TWO`;
expect(applyDiff(content, diff)).toBe("ONE\nTWO\naaa\nbbb\nccc");
});
it("`$+TEXT` shorthand: EOF cursor + insert TEXT on one line", () => {
const diff = `$+TAIL`;
expect(applyDiff(content, diff)).toBe("aaa\nbbb\nccc\nTAIL");
});
it("`^Lid+TEXT` shorthand: cursor-before-Lid + insert TEXT", () => {
const diff = `^${tag(2, "bbb")}+ABOVE`;
expect(applyDiff(content, diff)).toBe("aaa\nABOVE\nbbb\nccc");
});
it("`Lid+TEXT` shorthand: cursor-after-Lid + insert TEXT", () => {
const diff = `${tag(2, "bbb")}+BELOW`;
expect(applyDiff(content, diff)).toBe("aaa\nbbb\nBELOW\nccc");
});
it("`@Lid+TEXT` shorthand: cursor-after-Lid + insert TEXT", () => {
const diff = `@${tag(2, "bbb")}+BELOW`;
expect(applyDiff(content, diff)).toBe("aaa\nbbb\nBELOW\nccc");
});
it("`$=TEXT` rejects with a clear diagnostic about the cursor-only `$`", () => {
expect(() => applyDiff(content, `$=TAIL`)).toThrow(/`\$` only moves the cursor/);
});
it("`^=TEXT` rejects with a clear diagnostic about the cursor-only `^`", () => {
expect(() => applyDiff(content, `^=TOP`)).toThrow(/`\^` only moves the cursor/);
});
it("`^Lid=TEXT` rejects as ambiguous (cursor-before vs replace)", () => {
const diff = `^${tag(2, "bbb")}=BBB`;
expect(() => applyDiff(content, diff)).toThrow(/mixes `\^Lid` \(cursor before line\)/);
});
});
// ───────────────────────────────────────────────────────────────────────────
// Cursor binding rule
// ───────────────────────────────────────────────────────────────────────────
describe("atom parser — cursor binding", () => {
const content = "aaa\nbbb\nccc";
it("set moves the cursor after the set line", () => {
const diff = `${tag(1, "aaa")}=AAA\n+INSERTED\n@${tag(2, "bbb")}`;
expect(applyDiff(content, diff)).toBe("AAA\nINSERTED\nbbb\nccc");
});
it("set before another set keeps inserts at the previous cursor", () => {
const diff = `${tag(1, "aaa")}=AAA\n+INSERTED\n${tag(2, "bbb")}=BBB`;
expect(applyDiff(content, diff)).toBe("AAA\nINSERTED\nBBB\nccc");
});
it("bare Lid moves the cursor before a following set", () => {
const diff = `@${tag(1, "aaa")}\n+INSERTED\n${tag(2, "bbb")}=BBB`;
expect(applyDiff(content, diff)).toBe("aaa\nINSERTED\nBBB\nccc");
});
it("preserves contiguous + lines at the same cursor", () => {
const diff = `${tag(1, "aaa")}=AAA\n+I1\n+I2\n+I3\n@${tag(2, "bbb")}`;
expect(applyDiff(content, diff)).toBe("AAA\nI1\nI2\nI3\nbbb\nccc");
});
it("+ with only a previous anchor inserts after that anchor", () => {
const diff = `@${tag(2, "bbb")}\n+INSERTED`;
expect(applyDiff(content, diff)).toBe("aaa\nbbb\nINSERTED\nccc");
});
it("+ before any cursor move uses the initial EOF cursor", () => {
const diff = `+INSERTED\n@${tag(2, "bbb")}`;
expect(applyDiff(content, diff)).toBe("aaa\nbbb\nccc\nINSERTED");
});
});
// ───────────────────────────────────────────────────────────────────────────
// Edge cases
// ───────────────────────────────────────────────────────────────────────────
describe("atom parser — edge cases", () => {
const content = "aaa\nbbb\nccc";
it("empty set blanks the line", () => {
const diff = `${tag(2, "bbb")}=`;
expect(applyDiff(content, diff)).toBe("aaa\n\nccc");
});
it("set value starting with `!` keeps the leading `!`", () => {
const diff = `${tag(2, "bbb")}=!hello`;
expect(applyDiff(content, diff)).toBe("aaa\n!hello\nccc");
});
it("set value starting with `|` keeps the leading `|`", () => {
const diff = `${tag(2, "bbb")}=|hello`;
expect(applyDiff(content, diff)).toBe("aaa\n|hello\nccc");
});
it("set preserves leading/trailing whitespace exactly", () => {
const diff = `${tag(2, "bbb")}= spaced `;
expect(applyDiff(content, diff)).toBe("aaa\n spaced \nccc");
});
it("empty `+` line inserts a blank line", () => {
const diff = `${tag(1, "aaa")}=AAA\n+\n@${tag(2, "bbb")}`;
expect(applyDiff(content, diff)).toBe("AAA\n\nbbb\nccc");
});
it("`+` block with no cursor emits EOF cursor inserts", () => {
expect(parseAtom(`+lonely`)).toMatchObject([{ kind: "insert", cursor: { kind: "eof" }, text: "lonely" }]);
});
it("out-of-order anchors are accepted (sorted internally)", () => {
const diff = `${tag(3, "ccc")}=CCC\n${tag(1, "aaa")}=AAA`;
expect(applyDiff(content, diff)).toBe("AAA\nbbb\nCCC");
});
it("delete + post: insertions take the deleted line's slot", () => {
const diff = `-${tag(2, "bbb")}\n+INSERTED`;
expect(applyDiff(content, diff)).toBe("aaa\nINSERTED\nccc");
});
it("CRLF input is normalized line-by-line", () => {
const diff = `${tag(2, "bbb")}=BBB\r\n${tag(1, "aaa")}=AAA\r`;
expect(applyDiff(content, diff)).toBe("AAA\nBBB\nccc");
});
it("trailing newline at file end is preserved", () => {
const c = "aaa\nbbb\nccc\n";
const diff = `${tag(2, "bbb")}=BBB`;
expect(applyDiff(c, diff)).toBe("aaa\nBBB\nccc\n");
});
it("empty diff is a no-op", () => {
expect(parseAtom("")).toEqual([]);
expect(parseAtom("\n\n\n")).toEqual([]);
});
it("`Lid=TEXT` is the canonical set form", () => {
const content = "aaa\nbbb\nccc";
expect(applyDiff(content, `${tag(2, "bbb")}=BBB`)).toBe("aaa\nBBB\nccc");
});
it("legacy set and locator forms remain accepted", () => {
const content = "aaa\nbbb\nccc";
const t = tag(2, "bbb");
expect(applyDiff(content, `${t}|BBB`)).toBe("aaa\nBBB\nccc");
expect(applyDiff(content, `@@ ${t}\n+INSERTED`)).toBe("aaa\nbbb\nINSERTED\nccc");
});
it("recovers common replacement slips with @ and @@ prefixes", () => {
const content = "aaa\nbbb\nccc";
const t = tag(2, "bbb");
expect(applyDiff(content, `@${t}=BBB`)).toBe("aaa\nBBB\nccc");
expect(applyDiff(content, `@${t}|BBB`)).toBe("aaa\nBBB\nccc");
expect(applyDiff(content, `@@ ${t}=BBB`)).toBe("aaa\nBBB\nccc");
expect(applyDiff(content, `@@ ${t}|BBB`)).toBe("aaa\nBBB\nccc");
});
it("recovers common delete slips with whitespace and @@ prefixes", () => {
const content = "aaa\nbbb\nccc";
const t = tag(2, "bbb");
expect(applyDiff(content, `- ${t}`)).toBe("aaa\nccc");
expect(applyDiff(content, `@@ -${t}`)).toBe("aaa\nccc");
expect(applyDiff(content, `@@ - ${t}`)).toBe("aaa\nccc");
});
it("ignores whitespace before replacement separator and preserves whitespace after it", () => {
const content = "aaa\nbbb\nccc";
const t = tag(2, "bbb");
expect(applyDiff(content, `${t} = BBB `)).toBe("aaa\n BBB \nccc");
expect(applyDiff(content, `@${t} | BBB `)).toBe("aaa\n BBB \nccc");
expect(applyDiff(content, `@@ ${t} | BBB `)).toBe("aaa\n BBB \nccc");
});
it("rejects partial and missing Lids with repairable diagnostics", () => {
expect(() => parseAtom("@@ 98")).toThrow(/`@@ 98` is missing the two-letter Lid suffix/);
expect(() => parseAtom("yh=TEXT")).toThrow(/`yh` is not a full Lid/);
expect(() => parseAtom("123=TEXT")).toThrow(/`123` is missing the two-letter Lid suffix/);
expect(() => parseAtom("123|TEXT")).toThrow(/`123` is missing the two-letter Lid suffix/);
});
it("canonical equals replacement does not apply legacy OLD|NEW repair", () => {
const content = "aaa\nbbb\nccc";
const t = tag(2, "bbb");
expect(applyDiff(content, `${t}=bbb|BBB`)).toBe("aaa\nbbb|BBB\nccc");
});
it("silently ignores identical replacements when another operation changes the file", () => {
const content = "aaa\nbbb\nccc";
const t = tag(2, "bbb");
const result = applyAtomEdits(content, parseAtom(`${t}=bbb\n+DDD`));
expect(result.lines).toBe("aaa\nbbb\nDDD\nccc");
expect(result.noopEdits).toBeUndefined();
expect(result.warnings).toBeUndefined();
});
it("locator-only patches report the cursor diagnostic", async () => {
await expect(
executeAtomSingle({
session: { cwd: process.cwd() } as ToolSession,
input: "---a.ts\n@123ab",
writethrough: async () => {
throw new Error("unexpected write");
},
beginDeferredDiagnosticsForPath: () => {
throw new Error("unexpected diagnostics");
},
} as ExecuteAtomSingleOptions),
).rejects.toThrow(
"Cursor moved but no mutation found. Add +TEXT to insert, -Lid to delete, or Lid=TEXT to replace.",
);
});
it("@Lid move syntax is accepted as canonical", () => {
expect(parseAtom(`@${tag(2, "bbb")}`)).toEqual([]);
});
it("anchor with non-pipe trailing characters is rejected", () => {
// `1aa/foo/bar/` doesn't match any recognized op; the parser must
// reject it rather than silently treating it as an insert.
expect(() => parseAtom(`${tag(1, "aaa")}/foo/bar/`)).toThrow(/unrecognized op/);
});
it("duplicate sets on the same anchor: last set wins", () => {
const t = tag(2, "bbb");
const diff = `${t}=OLD\n${t}=NEW`;
expect(applyDiff(content, diff)).toBe("aaa\nNEW\nccc");
});
it("same-line OLD|NEW repairs to the new line when OLD is current content", () => {
const t = tag(2, "bbb");
const diff = `${t}|bbb|BBB`;
expect(applyDiff(content, diff)).toBe("aaa\nBBB\nccc");
});
it("repairs indented read-output lines as hashline replacements", () => {
const content = '{\n "mode": "demo",\n "strict": true';
const t1 = tag(1, "{");
const t2 = tag(2, ' "mode": "demo",');
const t3 = tag(3, ' "strict": true');
const diff = `${t1}|{\n ${t2}| "mode": "demo2",\n ${t3}| "strict": true`;
expect(applyDiff(content, diff)).toBe('{\n "mode": "demo2",\n "strict": true');
});
it("same-line OLD|NEW repair works through `@` prefix slip", () => {
const t = tag(2, "bbb");
const diff = `@${t}|bbb|BBB`;
expect(applyDiff(content, diff)).toBe("aaa\nBBB\nccc");
});
it("same-line OLD|NEW repair works through `@@ ` prefix slip", () => {
const t = tag(2, "bbb");
const diff = `@@ ${t}|bbb|BBB`;
expect(applyDiff(content, diff)).toBe("aaa\nBBB\nccc");
});
it("`-Lid` followed by `+Lid|TEXT` fuses into a single replacement", () => {
const t = tag(2, "bbb");
const diff = `-${t}\n+${t}|REPLACED`;
expect(applyDiff(content, diff)).toBe("aaa\nREPLACED\nccc");
});
it("`-Lid` followed by `+Lid=TEXT` fuses into a single replacement", () => {
const t = tag(2, "bbb");
const diff = `-${t}\n+${t}=REPLACED`;
expect(applyDiff(content, diff)).toBe("aaa\nREPLACED\nccc");
});
it("standalone `+Lid|TEXT` rejects as malformed unified-diff syntax", () => {
const t = tag(2, "bbb");
expect(() => parseAtomWithWarnings(`+${t}|REPLACED`)).toThrow(/unified-diff syntax/);
});
it("standalone `+Lid=TEXT` rejects as malformed unified-diff syntax", () => {
const t = tag(2, "bbb");
expect(() => parseAtomWithWarnings(`+${t}=REPLACED`)).toThrow(/unified-diff syntax/);
});
it("`-Lid` followed by `+OtherLid|TEXT` rejects instead of salvaging a mismatched add", () => {
const t1 = tag(1, "aaa");
const t2 = tag(2, "bbb");
expect(() => parseAtomWithWarnings(`-${t1}\n+${t2}|REPLACED`)).toThrow(/not in the preceding delete run/);
});
it("unified-diff-shaped blocks with fabricated Lids on inserts reject clearly", () => {
const ctx = tag(1, "head");
const fake1 = "2aa";
const fake2 = "3bb";
const realDel = tag(2, "body");
const diff = `${ctx}|head\n+${fake1}|new1\n+${fake2}|new2\n-${realDel}\n+replaced`;
expect(() => parseAtomWithWarnings(diff)).toThrow(/unified-diff syntax/);
});
it("plain `+TEXT` insertion is unaffected by diff-ish detection", () => {
// `+5xa hello` — no `=` or `|` separator after the Lid-shaped prefix —
// remains a literal insert, including the `5xa hello` text.
const diff = `+5xa hello`;
expect(applyDiff(content, diff)).toBe("aaa\nbbb\nccc\n5xa hello");
});
it("multi-line hunk: deletes followed by inserts becomes a block replacement at the FIRST deleted slot", () => {
const c = "aaa\nbbb\nccc\nddd\neee";
const t2 = tag(2, "bbb");
const t3 = tag(3, "ccc");
const t4 = tag(4, "ddd");
const diff = `-${t2}\n-${t3}\n-${t4}\n+X1\n+X2\n+X3`;
expect(applyDiff(c, diff)).toBe("aaa\nX1\nX2\nX3\neee");
});
it("multi-line hunk: more inserts than deletes is allowed", () => {
const c = "aaa\nbbb\nccc\nddd";
const t2 = tag(2, "bbb");
const diff = `-${t2}\n+X1\n+X2\n+X3`;
expect(applyDiff(c, diff)).toBe("aaa\nX1\nX2\nX3\nccc\nddd");
});
it("multi-line hunk: fewer inserts than deletes is allowed", () => {
const c = "aaa\nbbb\nccc\nddd\neee";
const t2 = tag(2, "bbb");
const t3 = tag(3, "ccc");
const t4 = tag(4, "ddd");
const diff = `-${t2}\n-${t3}\n-${t4}\n+X`;
expect(applyDiff(c, diff)).toBe("aaa\nX\neee");
});
it("multi-line hunk: `+Lid|TEXT` add lines must reference a deleted Lid", () => {
const c = "aaa\nbbb\nccc\nddd";
const t2 = tag(2, "bbb");
const t3 = tag(3, "ccc");
const t4 = tag(4, "ddd");
// `+Lid|TEXT` for t2 and t3 (in delete run) is OK; t4 (also deleted) is OK.
const diff = `-${t2}\n-${t3}\n-${t4}\n+${t2}|X1\n+${t3}|X2\n+${t4}|X3`;
expect(applyDiff(c, diff)).toBe("aaa\nX1\nX2\nX3");
});
it("multi-line hunk: `+Lid|TEXT` referencing a Lid not in the delete run rejects", () => {
const t1 = tag(1, "aaa");
const t2 = tag(2, "bbb");
const t3 = tag(3, "ccc");
expect(() => parseAtomWithWarnings(`-${t1}\n-${t2}\n+${t3}|X`)).toThrow(/not in the preceding delete run/);
});
it("`-Lid|OLD` deletes when OLD matches the current line", () => {
const t = tag(2, "bbb");
const diff = `-${t}|bbb`;
expect(applyDiff(content, diff)).toBe("aaa\nccc");
});
it("`-Lid=OLD` deletes when OLD matches the current line", () => {
const t = tag(2, "bbb");
const diff = `-${t}=bbb`;
expect(applyDiff(content, diff)).toBe("aaa\nccc");
});
it("`-Lid|OLD` ignores OLD payload (hash already validates)", () => {
const t = tag(2, "bbb");
// Mismatched OLD is tolerated — the Lid hash is the real anchor check.
const diff = `-${t}|XXX`;
expect(applyDiff(content, diff)).toBe("aaa\nccc");
});
it("hunk with `-Lid|OLD` ignores OLD payload on each delete", () => {
const c = "aaa\nbbb\nccc\nddd";
const t2 = tag(2, "bbb");
const t3 = tag(3, "ccc");
// Matching OLD: accepted.
const ok = `-${t2}|bbb\n-${t3}|ccc\n+X1\n+X2`;
expect(applyDiff(c, ok)).toBe("aaa\nX1\nX2\nddd");
// Mismatched OLD: still accepted — hash is the source of truth.
const bad = `-${t2}|bbb\n-${t3}|wrong\n+X1\n+X2`;
expect(applyDiff(c, bad)).toBe("aaa\nX1\nX2\nddd");
});
it("set with current content reports identical replacement as a no-op", () => {
const t = tag(2, "bbb");
const result = applyAtomEdits(content, parseAtom(`${t}=bbb`));
expect(result.lines).toBe(content);
expect(result.noopEdits).toEqual([
{
editIndex: 0,
loc: t,
reason:
"replacement is identical to the current line content; use `Lid=NEW_TEXT` and do not copy an unchanged read line",
current: "bbb",
},
]);
});
});
// ───────────────────────────────────────────────────────────────────────────
// Combined ops on a single anchor
// ───────────────────────────────────────────────────────────────────────────
describe("atom — combining set + post on one anchor", () => {
it("set then `+` lines insert after the set line", () => {
const content = "aaa\nbbb\nccc";
const t = tag(2, "bbb");
const diff = `${t}=NEW\n+POST`;
expect(applyDiff(content, diff)).toBe("aaa\nNEW\nPOST\nccc");
});
it("a leading `+` starts at EOF, and a trailing `+` uses the current anchor cursor", () => {
const content = "aaa\nbbb\nccc\nddd";
const t2 = tag(2, "bbb");
const t3 = tag(3, "ccc");
const diff = `+BEFORE\n${t2}=BBB\n+AFTER\n@${t3}`;
expect(applyDiff(content, diff)).toBe("aaa\nBBB\nAFTER\nccc\nddd\nBEFORE");
});
});
// ───────────────────────────────────────────────────────────────────────────
// Hash mismatch flow
// ───────────────────────────────────────────────────────────────────────────
describe("atom — hash mismatch", () => {
it("propagates HashlineMismatchError on stale hash", () => {
const content = "aaa\nbbb\nccc";
const diff = `2zz=BBB`;
const edits = parseAtom(diff);
expect(() => applyAtomEdits(content, edits)).toThrow(HashlineMismatchError);
});
it("does not use replacement text as a rebase content hint", () => {
const content = "aaa\nchanged\nNEW";
const stale = tag(2, "bbb");
expect(() => applyAtomEdits(content, parseAtom(`${stale}=NEW`))).toThrow(HashlineMismatchError);
});
it("single rebase is permitted (one stale anchor recovers silently)", () => {
// Insert an unrelated line at the top, shifting line 2 to line 3.
// A `Lid=` op that targeted the original line 2 must auto-rebase to line 3.
const content = "aaa\nINSERTED\nbbb\nccc";
const stale = tag(2, "bbb"); // hash for "bbb" computed at line 2
const result = applyAtomEdits(content, parseAtom(`${stale}=BBB`));
expect(result.lines).toBe("aaa\nINSERTED\nBBB\nccc");
});
it("multiple mutating anchors can rebase together", () => {
const content = "aaa\nINSERTED\nbbb\nccc\nddd";
const t2 = tag(2, "bbb"); // bbb is actually at line 3
const t3 = tag(3, "ccc"); // ccc is actually at line 4
const t4 = tag(4, "ddd"); // ddd is actually at line 5
const diff = `${t2}=BBB\n${t3}=CCC\n${t4}=DDD`;
const result = applyAtomEdits(content, parseAtom(diff));
expect(result.lines).toBe("aaa\nINSERTED\nBBB\nCCC\nDDD");
expect(result.warnings).toEqual([
`Auto-rebased anchor ${t2} → 3${computeLineHash(3, "bbb")} (line shifted within ±5; hash matched).`,
`Auto-rebased anchor ${t3} → 4${computeLineHash(4, "ccc")} (line shifted within ±5; hash matched).`,
`Auto-rebased anchor ${t4} → 5${computeLineHash(5, "ddd")} (line shifted within ±5; hash matched).`,
]);
});
it("multiple delete anchors can rebase together", () => {
const content = "aaa\nINSERTED\nbbb\nccc\nddd";
const t2 = tag(2, "bbb"); // bbb is actually at line 3
const t3 = tag(3, "ccc"); // ccc is actually at line 4
const t4 = tag(4, "ddd"); // ddd is actually at line 5
const diff = `-${t2}\n-${t3}\n-${t4}`;
const result = applyAtomEdits(content, parseAtom(diff));
expect(result.lines).toBe("aaa\nINSERTED");
expect(result.warnings).toEqual([
`Auto-rebased anchor ${t2} → 3${computeLineHash(3, "bbb")} (line shifted within ±5; hash matched).`,
`Auto-rebased anchor ${t3} → 4${computeLineHash(4, "ccc")} (line shifted within ±5; hash matched).`,
`Auto-rebased anchor ${t4} → 5${computeLineHash(5, "ddd")} (line shifted within ±5; hash matched).`,
]);
});
});
// ───────────────────────────────────────────────────────────────────────────
// Internal AtomEdit shapes
// ───────────────────────────────────────────────────────────────────────────
describe("parseAtom — emits internal AtomEdit shapes", () => {
it("emits delete op", () => {
const t = tag(2, "bbb");
const edits = parseAtom(`-${t}`);
expect(edits).toHaveLength(1);
expect(edits[0]).toMatchObject({ kind: "delete", anchor: { line: 2 } });
});
it("emits BOF cursor insert for ^ + +", () => {
const edits = parseAtom(`^\n+x`);
expect(edits).toMatchObject([{ kind: "insert", cursor: { kind: "bof" }, text: "x" }]);
});
it("emits EOF cursor insert for bare + +", () => {
const edits = parseAtom(`+x`);
expect(edits).toMatchObject([{ kind: "insert", cursor: { kind: "eof" }, text: "x" }]);
});
it("emits EOF cursor insert for $ + +", () => {
const edits = parseAtom(`$\n+x`);
expect(edits).toMatchObject([{ kind: "insert", cursor: { kind: "eof" }, text: "x" }]);
});
it("delete + set on same anchor is rejected by validateNoConflictingAnchorOps", () => {
const content = "aaa\nbbb\nccc";
const t = tag(2, "bbb");
const diff = `${t}=NEW\n-${t}`;
const edits = parseAtom(diff);
expect(() => applyAtomEdits(content, edits)).toThrow(/Conflicting ops/);
});
});
// ───────────────────────────────────────────────────────────────────────────
// Wire format header
// ───────────────────────────────────────────────────────────────────────────
describe("splitAtomInput — wire-format header", () => {
it("extracts path and diff body from `--- path` header", () => {
const input = `---src/foo.ts\n${tag(2, "bbb")}=BBB`;
const { path, diff } = splitAtomInput(input);
expect(path).toBe("src/foo.ts");
expect(diff).toBe(`${tag(2, "bbb")}=BBB`);
});
it("extracts path and diff body from legacy `--- path` header", () => {
const input = `--- src/foo.ts\n${tag(2, "bbb")}=BBB`;
const { path, diff } = splitAtomInput(input);
expect(path).toBe("src/foo.ts");
expect(diff).toBe(`${tag(2, "bbb")}=BBB`);
});
it("strips leading blank lines before the first header", () => {
const input = `\n\n---a.ts\n+export const A = 1;`;
expect(splitAtomInput(input)).toEqual({ path: "a.ts", diff: "+export const A = 1;" });
});
it("unquotes matching path quotes", () => {
expect(splitAtomInput(`---"foo bar.ts"\n+x`).path).toBe("foo bar.ts");
expect(splitAtomInput(`---'foo bar.ts'\n+x`).path).toBe("foo bar.ts");
});
it("normalizes cwd-prefixed absolute paths to cwd-relative paths", () => {
const cwd = path.join(process.cwd(), "packages", "coding-agent");
const absolute = path.join(cwd, "src", "foo.ts");
expect(splitAtomInput(`---${absolute}\n+x`, { cwd }).path).toBe("src/foo.ts");
});
it("preserves absolute paths outside cwd", () => {
const cwd = path.join(process.cwd(), "packages", "coding-agent");
const outside = path.resolve(process.cwd(), "..", "outside.ts");
expect(splitAtomInput(`---${outside}\n+x`, { cwd }).path).toBe(outside);
});
it("uses explicit fallback path only when input has operations and no header", () => {
expect(splitAtomInput(`${tag(1, "aaa")}=AAA`, { path: "a.ts" })).toEqual({
path: "a.ts",
diff: `${tag(1, "aaa")}=AAA`,
});
expect(() => splitAtomInput("plain text", { path: "a.ts" })).toThrow(/must begin with/);
expect(() => splitAtomInput("---\n+x", { path: "a.ts" })).toThrow(/empty/);
});
it("fallback path mode recognizes atom shorthand and range forms", () => {
const forms = [
`^${tag(1, "aaa")}`,
`^${tag(1, "aaa")}+TEXT`,
"$+TEXT",
`${tag(1, "aaa")}+TEXT`,
`@${tag(1, "aaa")}+TEXT`,
`-${tag(1, "aaa")}..${tag(2, "bbb")}`,
`${tag(1, "aaa")}..${tag(2, "bbb")}=TEXT`,
`${tag(1, "aaa")}..${tag(2, "bbb")} =TEXT`,
];
for (const form of forms) {
expect(splitAtomInput(form, { path: "a.ts" })).toEqual({ path: "a.ts", diff: form });
}
});
it("throws if header is missing", () => {
expect(() => splitAtomInput("124aa=NEW")).toThrow(/must begin with/);
});
it("throws if header path is empty", () => {
expect(() => splitAtomInput("--- \n124aa=NEW")).toThrow(/empty/);
});
it("tolerates CRLF after header", () => {
const input = `--- a.ts\r\n${tag(1, "alpha")}=ALPHA`;
const { path, diff } = splitAtomInput(input);
expect(path).toBe("a.ts");
expect(diff).toBe(`${tag(1, "alpha")}=ALPHA`);
});
it("strips BOM from input", () => {
const input = `\uFEFF---a.ts\n${tag(1, "alpha")}=ALPHA`;
const { path } = splitAtomInput(input);
expect(path).toBe("a.ts");
});
it("splits multiple --- path sections", () => {
const input = `---a.ts\n+export const A = 1;\n--- b.ts\n+export const B = 2;`;
expect(splitAtomInputs(input)).toEqual([
{ path: "a.ts", diff: "+export const A = 1;" },
{ path: "b.ts", diff: "+export const B = 2;" },
]);
});
it("rejects the old colon header after the syntax cutover", () => {
expect(() => splitAtomInput(":a.ts\n+export const A = 1;")).toThrow(/must begin with/);
});
});
// ───────────────────────────────────────────────────────────────────────────
// Whole-file operations
// ───────────────────────────────────────────────────────────────────────────
describe("atom executor — whole-file operations", () => {
it("deletes the section file with !rm", async () => {
await withTempDir(async tempDir => {
const filePath = path.join(tempDir, "file.ts");
await Bun.write(filePath, "export const x = 1;\n");
const result = await executeAtomSingle(atomExecuteOptions(tempDir, "---file.ts\n!rm\n"));
expect(result.content[0]?.type === "text" ? result.content[0].text : "").toContain("Deleted file.ts");
expect(result.details?.op).toBe("delete");
expect(await Bun.file(filePath).exists()).toBe(false);
});
});
it("renames the section file with !mv", async () => {
await withTempDir(async tempDir => {
const sourcePath = path.join(tempDir, "file.ts");
const destinationPath = path.join(tempDir, "file2.ts");
await Bun.write(sourcePath, "export const x = 1;\n");
const result = await executeAtomSingle(atomExecuteOptions(tempDir, "---file.ts\n!mv file2.ts\n"));
expect(result.content[0]?.type === "text" ? result.content[0].text : "").toContain(
"Moved file.ts to file2.ts",
);
expect(result.details?.op).toBe("update");
expect(result.details?.move).toBe("file2.ts");
expect(await Bun.file(sourcePath).exists()).toBe(false);
expect(await Bun.file(destinationPath).text()).toBe("export const x = 1;\n");
});
});
it("rejects sections that mix whole-file operations with line edits", async () => {
await withTempDir(async tempDir => {
await Bun.write(path.join(tempDir, "file.ts"), "export const x = 1;\n");
await expect(
executeAtomSingle(atomExecuteOptions(tempDir, "---file.ts\n!rm\n+export const y = 2;\n")),
).rejects.toThrow(/mixes !rm with line edits/);
await expect(
executeAtomSingle(atomExecuteOptions(tempDir, "---file.ts\n!mv file2.ts\n-1ab\n")),
).rejects.toThrow(/mixes !mv with line edits/);
});
});
it("rejects !mv without a destination", async () => {
await withTempDir(async tempDir => {
await Bun.write(path.join(tempDir, "file.ts"), "export const x = 1;\n");
await expect(executeAtomSingle(atomExecuteOptions(tempDir, "---file.ts\n!mv\n"))).rejects.toThrow(
/!mv requires exactly one non-empty destination path/,
);
});
});
it("rejects `^Lid` anchored inserts on missing files", async () => {
await withTempDir(async tempDir => {
await expect(executeAtomSingle(atomExecuteOptions(tempDir, "---missing.ts\n^1aa\n+top\n"))).rejects.toThrow(
/File not found: missing\.ts/,
);
});
});
it("returns success (not throw) when every edit is a no-op echo of current content", async () => {
await withTempDir(async tempDir => {
const content = "aaa\nbbb\nccc\n";
await Bun.write(path.join(tempDir, "file.ts"), content);
const t1 = tag(1, "aaa");
const t2 = tag(2, "bbb");
const result = await executeAtomSingle(atomExecuteOptions(tempDir, `---file.ts\n${t1}=aaa\n${t2}=bbb\n`));
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
expect(text).toContain("no changes being made");
expect(text).toContain("replacement is identical");
// File untouched
expect(await Bun.file(path.join(tempDir, "file.ts")).text()).toBe(content);
});
});
it("preflights all sections before writing a multi-file edit", async () => {
await withTempDir(async tempDir => {
const aPath = path.join(tempDir, "a.ts");
const bPath = path.join(tempDir, "b.ts");
await Bun.write(aPath, "aaa\n");
await Bun.write(bPath, "bbb\n");
const input = [`---a.ts`, `${tag(1, "aaa")}=AAA`, `---b.ts`, `${mistag(1, "bbb")}=BBB`].join("\n");
await expect(executeAtomSingle(atomExecuteOptions(tempDir, input))).rejects.toThrow(
/changed since the last read/,
);
expect(await Bun.file(aPath).text()).toBe("aaa\n");
expect(await Bun.file(bPath).text()).toBe("bbb\n");
});
});
});
// ───────────────────────────────────────────────────────────────────────────
// Heuristic: post-edit duplicate-line detection
// ───────────────────────────────────────────────────────────────────────────
describe("applyAtomEdits — adjacent duplicate detection", () => {
it("auto-fixes a duplicate line when removing it restores bracket balance", () => {
// Original: function on lines 3-5, single `}` at line 5.
const content = "const X = 1;\n\nexport function f() {\n\treturn X;\n}";
// Botched block rewrite: delete only the declaration + body, not the closing
// brace. Insert a replacement that includes its own `}`. Without auto-fix
// the result would have `}\n}` and brace balance off by one.
const t3 = tag(3, "export function f() {");
const t4 = tag(4, "\treturn X;");
const diff = `-${t3}\n-${t4}\n+export function f(): number {\n+\treturn X * 2;\n+}`;
const result = applyAtomEdits(content, parseAtom(diff));
expect(result.lines).toBe("const X = 1;\n\nexport function f(): number {\n\treturn X * 2;\n}");
expect(result.warnings ?? []).toEqual(
expect.arrayContaining([expect.stringMatching(/Auto-fixed: removed duplicate line/)]),
);
});
it("auto-fixes a duplicated function signature (missed leading delete)", () => {
// Original: function on lines 2-4. Block rewrite forgets to delete the
// original signature on line 2 and inserts a new one, producing two
// adjacent identical `export function f() {` lines and an extra `{`.
const content = "alpha\nexport function f() {\n\treturn 1;\n}\nbeta";
const t3 = tag(3, "\treturn 1;");
const t4 = tag(4, "}");
const diff = `-${t3}\n-${t4}\n+export function f() {\n+\treturn 2;\n+}`;
const result = applyAtomEdits(content, parseAtom(diff));
expect(result.lines).toBe("alpha\nexport function f() {\n\treturn 2;\n}\nbeta");
expect(result.warnings ?? []).toEqual(
expect.arrayContaining([expect.stringMatching(/Auto-fixed: removed duplicate line/)]),
);
});
it("warns but does not auto-fix when bracket balance is unchanged", () => {
// Insert a duplicate non-bracket line — balance is unaffected so we cannot
// safely decide which copy to remove. Should warn only.
const content = "alpha\nbeta\ngamma";
const t1 = tag(1, "alpha");
const result = applyAtomEdits(content, parseAtom(`@${t1}\n+alpha`));
expect(result.lines).toBe("alpha\nalpha\nbeta\ngamma");
expect(result.warnings ?? []).toEqual(expect.arrayContaining([expect.stringMatching(/Suspicious duplicate/)]));
});
it("does not warn when the original already had adjacent duplicates", () => {
// Original has `\t}\n\t}` (nested closing braces). Edit doesn't add new pairs.
const content = "fn outer() {\n\tfn inner() {\n\t\treturn 1;\n\t}\n}";
const t3 = tag(3, "\t\treturn 1;");
const result = applyAtomEdits(content, parseAtom(`${t3}=\t\treturn 2;`));
expect(result.warnings ?? []).not.toEqual(
expect.arrayContaining([expect.stringMatching(/Suspicious duplicate/)]),
);
});
it("does not warn on adjacent blank lines", () => {
const content = "a\nb\nc";
// Insert two blank lines after line 1 — adjacent blanks should be ignored.
const t1 = tag(1, "a");
const result = applyAtomEdits(content, parseAtom(`@${t1}\n+\n+`));
expect(result.warnings ?? []).not.toEqual(
expect.arrayContaining([expect.stringMatching(/Suspicious duplicate/)]),
);
});
it("auto-splits `+@Lid` (op prefixed with `+`) into cursor move + blank insert", () => {
// Original failure: agent wrote `+@13du` meaning to move cursor and chain
// inserts; parser would otherwise insert the literal text `@13du`.
const content = "alpha\nbeta\ngamma";
const t1 = tag(1, "alpha");
const t2 = tag(2, "beta");
const diff = `@${t1}\n+inserted\n+@${t2}\n+after`;
const result = applyAtomEdits(content, parseAtom(diff));
// `+@${t2}` auto-splits to: cursor move to after beta, then blank insert.
expect(result.lines).toBe("alpha\ninserted\nbeta\n\nafter\ngamma");
});
it("auto-splits `+-Lid` (delete prefixed with `+`) into delete + blank insert", () => {
const content = "alpha\nbeta\ngamma";
const t2 = tag(2, "beta");
const result = applyAtomEdits(content, parseAtom(`+-${t2}`));
// `+-${t2}` auto-splits to: delete beta, then blank insert at the deletion slot.
expect(result.lines).toBe("alpha\n\ngamma");
});
it("does not auto-split `+@plain text` that isn't a valid op", () => {
// `@plain text` doesn't match any op shape; body is inserted as literal text.
const content = "x";
const result = applyAtomEdits(content, parseAtom(`+@plain text`));
expect(result.lines).toBe("x\n@plain text");
});
it("non-contiguous deletes followed by an insert: insert lands at the last contiguous sub-run, not the first delete", () => {
// Regression: agent emitted `-186 -197 -198 -199 +TEXT` intending to drop
// a debug line at 186 AND replace lines 197-199 with TEXT. normalizeHunks
// used to fuse all four deletes into one block and place the insert at
// line 186, breaking the function. Now sub-runs are split and the insert
// attaches to [197,198,199].
const content = "A\nB\nC\nD\nE\nF\nG\nH\nI\nJ";
const t1 = tag(1, "A");
const t5 = tag(5, "E");
const t6 = tag(6, "F");
const t7 = tag(7, "G");
const diff = `-${t1}\n-${t5}\n-${t6}\n-${t7}\n+REPLACED`;
const result = applyAtomEdits(content, parseAtom(diff));
expect(result.lines).toBe("B\nC\nD\nREPLACED\nH\nI\nJ");
});
it("multiple far-apart non-contiguous deletes still attach inserts to only the last sub-run", () => {
const content = "A\nB\nC\nD\nE\nF\nG";
const t1 = tag(1, "A");
const t3 = tag(3, "C");
const t6 = tag(6, "F");
const t7 = tag(7, "G");
const diff = `-${t1}\n-${t3}\n-${t6}\n-${t7}\n+REPLACED`;
const result = applyAtomEdits(content, parseAtom(diff));
// Sub-runs: [1], [3], [6,7]. Insert attaches to [6,7] → at line 6 slot.
expect(result.lines).toBe("B\nD\nE\nREPLACED");
});
});
// ───────────────────────────────────────────────────────────────────────────
// Schema is permissive for small models that pass extra fields
// ───────────────────────────────────────────────────────────────────────────
describe("atomEditParamsSchema — extra-field tolerance", () => {
it("accepts extra `path` field alongside `input`", () => {
const args = { path: "x.ts", input: "---x.ts\n1aa=NEW" };
expect(Value.Check(atomEditParamsSchema, args)).toBe(true);
});
it("accepts extra free-form fields like `_`", () => {
const args = { _: "fixing inverted boolean", input: "---x.ts\n1aa=NEW" };
expect(Value.Check(atomEditParamsSchema, args)).toBe(true);
});
it("still requires `input`", () => {
const args = { path: "x.ts" };
expect(Value.Check(atomEditParamsSchema, args)).toBe(false);
});
});