fix: synchronized snapshot-tag checks and fixed ANSI width emoji handling

- Introduced a shared missingSnapshotTagMessage helper and reused it from both hashline patcher and preview validation.
- Required snapshot tags in preview hashline sections for all edits, including head/tail inserts, so preview failures now match apply-time rejections.
- Updated TUI width calculation to strip ANSI escapes before grapheme splitting, preserving styled ZWJ emoji width behavior.
This commit is contained in:
can1357
2026-05-30 15:59:58 +02:00
parent 5cd88b7abc
commit 55e146fa3c
7 changed files with 98 additions and 23 deletions
+26 -4
View File
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { formatHashlineHeader, InMemorySnapshotStore } from "@oh-my-pi/hashline";
import { formatHashlineHeader, InMemorySnapshotStore, missingSnapshotTagMessage } from "@oh-my-pi/hashline";
import {
adjustIndentation,
computeEditDiff,
@@ -251,18 +251,40 @@ describe("computeHashlineDiff", () => {
test("accepts hashline input edits", async () => {
const sourcePath = path.join(tempDir, "source.txt");
await Bun.write(sourcePath, "first\n");
const text = "first\n";
await Bun.write(sourcePath, text);
const snapshotStore = new InMemorySnapshotStore();
const tag = snapshotStore.record(sourcePath, text);
const result = await computeHashlineDiff(
{ input: `${sourcePath}\ninsert tail:\n+second` },
{ input: `${formatHashlineHeader(sourcePath, tag)}\ninsert tail:\n+second` },
tempDir,
new InMemorySnapshotStore(),
snapshotStore,
);
expect("diff" in result).toBe(true);
if ("diff" in result) {
expect(result.diff).toContain("second");
}
});
test("rejects a tagless head/tail insert in the preview path, matching apply", async () => {
const relativePath = "source.txt";
await Bun.write(path.join(tempDir, relativePath), "first\n");
// A tagless `insert tail:` carries no anchored edit, yet the apply path
// (Patcher.prepare) rejects it for the missing mandatory tag. The
// preview/diff path MUST emit the SAME rejection so a successful preview
// never precedes a failing apply.
const result = await computeHashlineDiff(
{ input: `${relativePath}\ninsert tail:\n+second` },
tempDir,
new InMemorySnapshotStore(),
);
expect("error" in result).toBe(true);
if ("error" in result) {
expect(result.error).toBe(missingSnapshotTagMessage(relativePath));
}
});
test("returns a handled error when the source path is a local URL", async () => {
const result = await computeHashlineDiff(
{ input: "¶local://PLAN.md\ninsert tail:\n+x" },