7e42c281f8
Rejected headers such as ¶src/file.ts#1A2B:42 as malformed headers instead of treating the whole tail as a hashless path and failing later in body parsing. Added strict and apply_patch-recovery regression coverage for line-suffixed snapshot tags.
137 lines
5.6 KiB
TypeScript
137 lines
5.6 KiB
TypeScript
import { describe, expect, it } from "bun:test";
|
|
import { applyEdits, Patch, parsePatch } from "@oh-my-pi/hashline";
|
|
|
|
function applyPatch(text: string, diff: string): string {
|
|
return applyEdits(text, parsePatch(diff).edits).text;
|
|
}
|
|
|
|
const FILE = "a\nb\nc\nd\ne";
|
|
|
|
describe("hashline section headers", () => {
|
|
it("accepts paths with spaces in anchored section headers", () => {
|
|
const section = Patch.parseSingle("¶dir with spaces/file.ts#1a2b\nreplace 1..1:\n+after");
|
|
|
|
expect(section.path).toBe("dir with spaces/file.ts");
|
|
expect(section.fileHash).toBe("1A2B");
|
|
expect(section.applyTo("before").text).toBe("after");
|
|
});
|
|
|
|
it("recovers apply_patch-contaminated headers whose paths contain spaces", () => {
|
|
const section = Patch.parseSingle("¶*** Update File: dir with spaces/file.ts#1A2B\nreplace 1..1:\n+after");
|
|
|
|
expect(section.path).toBe("dir with spaces/file.ts");
|
|
expect(section.fileHash).toBe("1A2B");
|
|
expect(section.applyTo("before").text).toBe("after");
|
|
});
|
|
|
|
it("rejects trailing junk after a snapshot tag", () => {
|
|
expect(() => Patch.parse("¶src/a.ts#1A2B copied from read\nreplace 1..1:\n+after")).toThrow(
|
|
/Input header must be/,
|
|
);
|
|
expect(() => Patch.parse("¶src/a.ts#1A2B:812\nreplace 1..1:\n+after")).toThrow(/Input header must be/);
|
|
});
|
|
|
|
it("rejects trailing junk after a snapshot tag even with apply_patch noise", () => {
|
|
expect(() => Patch.parse("¶Update File: src/a.ts#1A2B copied from read\nreplace 1..1:\n+after")).toThrow(
|
|
/Input header must be/,
|
|
);
|
|
expect(() => Patch.parse("¶Update File: src/a.ts#1A2B:812\nreplace 1..1:\n+after")).toThrow(
|
|
/Input header must be/,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("hashline core — verb header forms", () => {
|
|
it("rejects a bare single-number hunk header with verb guidance", () => {
|
|
expect(() => parsePatch("2\n+B")).toThrow(/hunk headers need a verb/);
|
|
});
|
|
|
|
it("rejects a bare numeric range with verb guidance", () => {
|
|
expect(() => parsePatch("2 3\n+X")).toThrow(/Hunk headers need a verb/);
|
|
});
|
|
|
|
it("accepts canonical replace/delete/insert forms", () => {
|
|
expect(applyPatch(FILE, "replace 2..3:\n+X")).toBe("a\nX\nd\ne");
|
|
expect(applyPatch(FILE, "delete 2..3")).toBe("a\nd\ne");
|
|
expect(applyPatch(FILE, "insert before 2:\n+X")).toBe("a\nX\nb\nc\nd\ne");
|
|
expect(applyPatch(FILE, "insert after 2:\n+X")).toBe("a\nb\nX\nc\nd\ne");
|
|
expect(applyPatch(FILE, "insert head:\n+X")).toBe("X\na\nb\nc\nd\ne");
|
|
expect(applyPatch(FILE, "insert tail:\n+X")).toBe("a\nb\nc\nd\ne\nX");
|
|
});
|
|
|
|
it("accepts single-number replace and delete shorthand", () => {
|
|
expect(applyPatch(FILE, "replace 2:\n+X")).toBe("a\nX\nc\nd\ne");
|
|
expect(applyPatch(FILE, "delete 2")).toBe("a\nc\nd\ne");
|
|
});
|
|
|
|
it("accepts alternate replace range separators and missing colon", () => {
|
|
expect(applyPatch(FILE, "replace 2-3:\n+X")).toBe("a\nX\nd\ne");
|
|
expect(applyPatch(FILE, "replace 2\u20263:\n+X")).toBe("a\nX\nd\ne");
|
|
expect(applyPatch(FILE, "replace 2 3:\n+X")).toBe("a\nX\nd\ne");
|
|
expect(applyPatch(FILE, "replace 2..3\n+X")).toBe("a\nX\nd\ne");
|
|
});
|
|
|
|
it("accepts missing colon on insert headers", () => {
|
|
expect(applyPatch(FILE, "insert before 2\n+X")).toBe("a\nX\nb\nc\nd\ne");
|
|
expect(applyPatch(FILE, "insert head\n+X")).toBe("X\na\nb\nc\nd\ne");
|
|
});
|
|
});
|
|
|
|
describe("hashline body contracts", () => {
|
|
it("auto-pipes a bare body row while warning", () => {
|
|
const result = parsePatch("replace 2..2:\n hello");
|
|
expect(applyEdits(FILE, result.edits).text).toBe("a\n hello\nc\nd\ne");
|
|
expect(result.warnings.some(w => /Auto-prefixed bare body row/.test(w))).toBe(true);
|
|
});
|
|
|
|
it("rejects `-` body rows with a teaching error", () => {
|
|
expect(() => parsePatch("replace 2..2:\n-old\n+new")).toThrow(/`-` rows are not valid/);
|
|
});
|
|
|
|
it("allows literal text that begins with `-` or `+` when prefixed with `+`", () => {
|
|
expect(applyPatch(FILE, "replace 2..2:\n+-literal\n++plus")).toBe("a\n-literal\n+plus\nc\nd\ne");
|
|
});
|
|
|
|
it("treats empty replace as delete and still rejects empty insert", () => {
|
|
expect(applyPatch(FILE, "replace 2..2:")).toBe("a\nc\nd\ne");
|
|
expect(() => parsePatch("insert tail:")).toThrow(/`insert` needs/);
|
|
});
|
|
|
|
it("rejects delete with a body", () => {
|
|
expect(() => parsePatch("delete 2\n+X")).toThrow(/does not take body rows/);
|
|
});
|
|
|
|
it("rejects delete with a colon", () => {
|
|
expect(() => parsePatch("delete 2:\n+X")).toThrow(/has no colon/);
|
|
});
|
|
});
|
|
|
|
describe("hashline — apply_patch / unified-diff contamination", () => {
|
|
it("rejects apply_patch sentinels as contamination", () => {
|
|
expect(() => parsePatch("*** Update File: a.ts\nreplace 2..2:\n+X")).toThrow(/apply_patch sentinel/);
|
|
expect(() => parsePatch("*** Add File: a.ts\nreplace 2..2:\n+X")).toThrow(/apply_patch sentinel/);
|
|
});
|
|
|
|
it("rejects unified-diff hunk headers as contamination", () => {
|
|
expect(() => parsePatch("@@ -1,3 +1,3 @@\nreplace 2..2:\n+X")).toThrow(/unified-diff hunk header/);
|
|
});
|
|
|
|
it("treats top-level `+TEXT` as an orphan literal payload", () => {
|
|
expect(() => parsePatch("+const X = 1;\nreplace 2..2:")).toThrow(/payload line has no preceding hunk header/);
|
|
});
|
|
});
|
|
|
|
describe("hashline apply — duplicate boundary payloads", () => {
|
|
it("keeps replacement boundary echoes literal unless balance repair applies", () => {
|
|
const text = ["// one", "// two", "old();"].join("\n");
|
|
const diff = "replace 3..3:\n+// one\n+// two\n+new();";
|
|
expect(applyPatch(text, diff)).toBe(["// one", "// two", "// one", "// two", "new();"].join("\n"));
|
|
});
|
|
|
|
it("keeps pure-insert context echoes literal", () => {
|
|
const text = ["aaa", "bbb", "ccc"].join("\n");
|
|
const diff = "insert tail:\n+bbb\n+ccc\n+NEW";
|
|
expect(applyPatch(text, diff)).toBe("aaa\nbbb\nccc\nbbb\nccc\nNEW");
|
|
});
|
|
});
|