fix(coding-agent): resolved hashline snapshot tag collisions using exact matches

- Guard against 16-bit snapshot tag collisions by requiring live text to be byte-identical to the retained snapshot.
- Transition base text resolution to query exact matches via `snapshots.byHashExact`.
- Prevent applying incorrect preview edits when live file contents drift to a colliding state.
This commit is contained in:
can1357
2026-07-02 01:51:10 +02:00
parent e12a8f161a
commit ac9e85564f
2 changed files with 70 additions and 2 deletions
@@ -298,6 +298,63 @@ describe("computeHashlineDiff", () => {
expect(result.error).toContain('internal scheme "local://"');
}
});
// A 16-bit snapshot tag can collide across two different file states. The
// preview must mirror Patcher's apply-time guard: hash equality alone never
// proves the live text IS the snapshot the anchors were minted against.
test("rejects the no-drift path when the live text is a colliding ambiguous tag", async () => {
// Both texts hash to `1D84` (pinned in hashline's collision tests).
const SNAPSHOT_TEXT = "line one 263\nline two 4471\n";
const LIVE_TEXT = "line one 410\nline two 6970\n";
const sourcePath = path.join(tempDir, "source.txt");
await Bun.write(sourcePath, LIVE_TEXT);
const snapshotStore = new InMemorySnapshotStore();
// Anchors were minted against SNAPSHOT_TEXT; the live file is the
// colliding LIVE_TEXT, also retained (e.g. read after an external
// write). The tag is ambiguous — previewing the SWAP against live
// would show the model's payload landing on unrelated content.
const tag = snapshotStore.record(sourcePath, SNAPSHOT_TEXT);
snapshotStore.record(sourcePath, LIVE_TEXT);
const result = await computeHashlineDiff(
{ input: `${formatHashlineHeader(sourcePath, tag)}\nSWAP 2.=2:\n+edited from snapshot` },
tempDir,
snapshotStore,
);
expect("error" in result).toBe(true);
if ("error" in result) {
expect(result.error).toContain("file changed between read and edit");
}
});
test("rejects the no-drift path when the live text collides with the single retained snapshot", async () => {
const SNAPSHOT_TEXT = "line one 263\nline two 4471\n";
const LIVE_TEXT = "line one 410\nline two 6970\n";
const sourcePath = path.join(tempDir, "source.txt");
await Bun.write(sourcePath, LIVE_TEXT);
const snapshotStore = new InMemorySnapshotStore();
// Only SNAPSHOT_TEXT is retained; live drifted to a colliding text the
// store never saw. computeFileHash(live) === tag, but the retained
// text differs — recovery (3-way merge from SNAPSHOT_TEXT) must run
// instead of anchoring directly onto the collider. Here the merge
// cannot apply (every line differs), so the preview surfaces the
// drift error rather than a bogus diff.
const tag = snapshotStore.record(sourcePath, SNAPSHOT_TEXT);
const result = await computeHashlineDiff(
{ input: `${formatHashlineHeader(sourcePath, tag)}\nSWAP 2.=2:\n+edited from snapshot` },
tempDir,
snapshotStore,
);
expect("error" in result).toBe(true);
if ("error" in result) {
expect(result.error).toContain("file changed between read and edit");
}
});
});
describe("computeEditDiff", () => {