Files
oh-my-pi/packages/hashline/test/format-v2.test.ts
T
can1357 9856f904d7 feat(typescript-edit-benchmark): introduced empirical edit mutation planning
- Add new structural, multi-edit, and block-level mutation classes with updated category mappings.
- Introduce hunk extraction, placement, rendering, and solver utilities along with unit tests.
- Implement size-based mutation planning, prompt validation logic, and new prompt markdown templates.
- Update benchmark generation scripts and package configurations to support empirical edit shape statistics.
2026-07-30 07:45:01 +02:00

131 lines
5.0 KiB
TypeScript

import { describe, expect, it } from "bun:test";
import { applyEdits, parseLid, parsePatch, parsePatchStreaming, Tokenizer } from "@oh-my-pi/hashline";
function applyPatch(text: string, diff: string): string {
return applyEdits(text, parsePatch(diff).edits).text;
}
describe("hashline format v4", () => {
it("replaces a concrete range with literal body rows in textual order", () => {
const text = "a\nb\nc";
const diff = ["SWAP 2.=2:", "+before", "+after"].join("\n");
expect(applyPatch(text, diff)).toBe("a\nbefore\nafter\nc");
});
it("deletes a single source line", () => {
const text = "a\nb\nc";
expect(applyPatch(text, "CUT 2")).toBe("a\nc");
});
it("deletes a concrete range", () => {
const text = "a\nb\nc\nd";
expect(applyPatch(text, "CUT 2.=3")).toBe("a\nd");
});
it("accepts a single dot between integer range endpoints", () => {
const text = "a\nb\nc\nd";
expect(applyPatch(text, "CUT 2.3")).toBe("a\nd");
expect(applyPatch(text, "SWAP 2.3:\n+middle")).toBe("a\nmiddle\nd");
});
it("inserts before and after concrete anchors", () => {
const text = "a\nb\nc";
const diff = ["INS.PRE 2:", "+before", "INS.POST 2:", "+after"].join("\n");
expect(applyPatch(text, diff)).toBe("a\nbefore\nb\nafter\nc");
});
it("inserts at head and tail", () => {
const text = "a\nb";
expect(applyPatch(text, "INS.HEAD:\n+HEAD")).toBe("HEAD\na\nb");
expect(applyPatch(text, "INS.TAIL:\n+TAIL")).toBe("a\nb\nTAIL");
});
it("treats an empty replace hunk as a delete and still rejects empty inserts", () => {
const text = "a\nb\nc";
expect(applyPatch(text, "SWAP 2.=2:")).toBe("a\nc");
expect(() => parsePatch("INS.HEAD:")).toThrow(/needs at least one/);
});
it("rejects body rows under cut", () => {
expect(() => parsePatch("CUT 2\n+replacement")).toThrow(/takes no body rows/);
});
it("does not recognize removed DEL or COPY headers", () => {
for (const header of ["DEL 2", "DEL.BLK 2", "COPY 2", "COPY.BLK 2"]) {
expect(() => parsePatch(header)).toThrow(/payload line has no preceding hunk header/);
}
});
it("auto-pipes bare body rows as literal text", () => {
const text = "a\nb\nc";
expect(applyPatch(text, "SWAP 2.=2:\nraw")).toBe("a\nraw\nc");
const { warnings } = parsePatch("SWAP 2.=2:\nraw");
expect(warnings.some(w => /Auto-prefixed bare body row/.test(w))).toBe(true);
});
it("strips read-output line number prefix from auto-piped bare body rows", () => {
const text = "a\nb\nc";
// Without this fix, "3:text" becomes literal "3:text" in the file.
// With the fix, the "3:" prefix is stripped, yielding just "text".
const { edits, warnings } = parsePatch("SWAP 2.=2:\n3:replaced");
expect(applyEdits(text, edits).text).toBe("a\nreplaced\nc");
expect(warnings.some(w => /Auto-prefixed bare body row/.test(w))).toBe(true);
});
it("validates insert anchors against file bounds", () => {
const edits = parsePatch("INS.PRE 4:\n+x").edits;
expect(() => applyEdits("a\nb", edits)).toThrow(/Line 4 does not exist/);
});
it("rejects unsafe line numbers before expanding ranges", () => {
const unsafe = String(Number.MAX_SAFE_INTEGER + 1);
expect(parseLid(String(Number.MAX_SAFE_INTEGER), 1)).toEqual({ line: Number.MAX_SAFE_INTEGER });
expect(() => parseLid(unsafe, 1)).toThrow();
expect(new Tokenizer().isOp(`SWAP ${unsafe}.=${unsafe}:`)).toBe(false);
});
it("rejects safe-integer ranges above the expansion limit", () => {
expect(() => parsePatch("SWAP 1.=100001:\n+x")).toThrow(
/replace range spans 100001 lines; the maximum is 100000/,
);
});
it("ignores cutting the trailing blank sentinel of a newline-terminated file", () => {
// "a\nb\n" splits into ["a", "b", ""]; line 3 is the phantom sentinel.
const edits = parsePatch("CUT 3").edits;
expect(applyEdits("a\nb\n", edits).text).toBe("a\nb\n");
});
it("treats a cut range ending at the trailing sentinel as ending at the last real line", () => {
const edits = parsePatch("CUT 2.=3").edits;
expect(applyEdits("a\nb\n", edits).text).toBe("a\n");
});
it("treats a replace range ending at the trailing sentinel as ending at the last real line", () => {
const edits = parsePatch("SWAP 2.=3:\n+B").edits;
expect(applyEdits("a\nb\n", edits).text).toBe("a\nB\n");
});
it("still allows inserts anchored on the trailing blank sentinel", () => {
const edits = parsePatch("INS.POST 3:\n+tail").edits;
expect(applyEdits("a\nb\n", edits).text).toBe("a\nb\n\ntail");
});
it("still cuts a genuine last line of a non-newline-terminated file", () => {
// "a\nb" has no sentinel; line 2 is real content.
const edits = parsePatch("CUT 2").edits;
expect(applyEdits("a\nb", edits).text).toBe("a");
});
it("does not flush a trailing streaming pending empty replace hunk", () => {
const result = parsePatchStreaming("SWAP 5.=5:\n");
expect(result.edits).toEqual([]);
});
it("flushes a streaming empty replace hunk when another hunk starts", () => {
const result = parsePatchStreaming("SWAP 2.=2:\nINS.TAIL:\n");
expect(result.edits).toEqual([{ kind: "delete", anchor: { line: 2 }, lineNum: 1, index: 0 }]);
});
});