F3 SLICE-4: persistence/re-anchor/orphan decision-logic tests (INV-1/INV-6) (#6)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { buildFingerprint, resolve } from "../src/anchorer";
|
||||
import { applyChange, type LiveSpan } from "../src/attributionTracker";
|
||||
import type { Provenance } from "../src/model";
|
||||
|
||||
const HUMAN: Provenance = { kind: "human", id: "ben" };
|
||||
const DOC = "intro\nClaude wrote this sentence here.\noutro\n";
|
||||
|
||||
function liveSpan(): LiveSpan {
|
||||
const start = DOC.indexOf("Claude wrote this sentence");
|
||||
return {
|
||||
id: "at_1", start, end: start + "Claude wrote this sentence".length,
|
||||
author: { kind: "agent", id: "claude", agent: { sdk: "@cline/sdk", model: "sonnet", sessionId: "r1" } },
|
||||
createdAt: "2026-06-10T00:00:00.000Z",
|
||||
};
|
||||
}
|
||||
|
||||
describe("attribution persistence decision logic (SLICE-4, spec §6.5 PUC-4)", () => {
|
||||
it("save → reload: fingerprint built from the live span resolves to the same range", () => {
|
||||
const s = liveSpan();
|
||||
const fp = buildFingerprint(DOC, { start: s.start, end: s.end });
|
||||
expect(resolve(DOC, fp)).toEqual({ start: s.start, end: s.end });
|
||||
});
|
||||
|
||||
it("external edit above: fingerprint re-resolves at the moved range", () => {
|
||||
const s = liveSpan();
|
||||
const fp = buildFingerprint(DOC, { start: s.start, end: s.end });
|
||||
const edited = "a new line\n" + DOC;
|
||||
const moved = edited.indexOf("Claude wrote this sentence");
|
||||
expect(resolve(edited, fp)).toEqual({ start: moved, end: moved + (s.end - s.start) });
|
||||
});
|
||||
|
||||
it("anchored text mangled: orphaned, never guessed (INV-1/INV-6)", () => {
|
||||
const s = liveSpan();
|
||||
const fp = buildFingerprint(DOC, { start: s.start, end: s.end });
|
||||
const mangled = DOC.replace("Claude wrote this sentence", "Someone rewrote everything");
|
||||
expect(resolve(mangled, fp)).toBe("orphaned");
|
||||
});
|
||||
|
||||
it("in-session edits keep the live span fingerprint-able (tracker → anchorer round trip)", () => {
|
||||
let spans = [liveSpan()];
|
||||
let doc = DOC;
|
||||
const insertAt = DOC.indexOf("intro") + "intro".length;
|
||||
const inserted = " (note)";
|
||||
doc = doc.slice(0, insertAt) + inserted + doc.slice(insertAt);
|
||||
spans = applyChange(spans, { start: insertAt, end: insertAt, newLength: inserted.length }, HUMAN, {
|
||||
newId: () => "n1", now: () => "2026-06-10T01:00:00.000Z",
|
||||
});
|
||||
const agent = spans.find((x) => x.author.kind === "agent")!;
|
||||
expect(doc.slice(agent.start, agent.end)).toBe("Claude wrote this sentence");
|
||||
const fp = buildFingerprint(doc, { start: agent.start, end: agent.end });
|
||||
expect(resolve(doc, fp)).toEqual({ start: agent.start, end: agent.end });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user