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:
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user