fix(#70): harden the tracked-span revert per branch review

- shiftTracked (pure, anchorer.ts + unit tests): interior/outside edits shift,
  boundary-straddling edits distrust; an insertion exactly at the span start
  lands BEFORE the span (never absorbed into the revert target)
- clear tracked spans on doc close (a closed buffer can change on disk with no
  change events — a kept span could revert over unrelated text, INV-11)
- renderAll only REBUILDS an absent span from an exact resolve; a present entry
  is continuously-tracked ground truth (duplicate-text resolve can't clobber it)
- revertInPlace/finalizeInPlace gain the guard.isReadOnly check (INV-16)
- the hard-fail warning offers 'Discard proposal (leave text)' so a reloaded
  session can still dismiss an unlocatable proposal record
- ✓/✗ CodeLens pair anchors at the tracked span when a tweak orphans the exact
  anchor (the fixed reject path stays reachable from the primary gesture)
- QuickPick batch menu routes through the reporting accept-all/reject-all
  commands (a discarded skip tally re-created the silent failure)
- one clearProposal helper replaces the thrice-copied clear sequence;
  reject() now cleans applied/appliedSpans too

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
BenStullsBets
2026-07-02 18:17:56 -07:00
parent f47dfbc16a
commit e389406637
5 changed files with 173 additions and 33 deletions
+29
View File
@@ -320,6 +320,35 @@ suite("F12 inline diff — #70 reject after interior edit (INV-5)", () => {
assert.strictEqual(doc.getText(), "# T\n\nOne aaa.\n\nTwo bbb.\n", "document fully restored (INV-5)");
assert.strictEqual(p.listProposals(doc).length, 0, "all cleared");
});
// The decision gestures must stay REACHABLE for a tweaked proposal: the ✓/✗
// CodeLens pair anchors at the tracked span when the interior tweak orphans
// the exact anchor — otherwise the fixed reject path exists only at the API.
test("the ✓ Keep / ✗ Reject CodeLens pair survives an interior tweak (tracked-span anchor)", async () => {
const ORIGINAL = "Lens target sentence here.";
const { doc } = await freshDoc("docs/s70-lens-tweak.md", `# T\n\n${ORIGINAL}\n`);
const api = await getApi();
const p = api.proposalController;
const fp = { text: ORIGINAL, before: "", after: "", lineHint: 2 };
const id = await p.propose(doc, fp, "Lens TARGET sentence here.",
{ kind: "agent", id: "claude", agent: { sdk: "x", model: "m", sessionId: "s" } }, { granularity: "block" });
await p.optimisticApply(doc, id!);
await settle();
const at = doc.getText().indexOf("TARGET");
const we = new vscode.WorkspaceEdit();
we.replace(doc.uri, new vscode.Range(doc.positionAt(at), doc.positionAt(at + 6)), "TARGET-tweaked");
assert.ok(await vscode.workspace.applyEdit(we), "interior tweak applied");
await settle();
assert.strictEqual(p.listProposals(doc).find((v) => v.id === id)!.anchorStart, null, "anchor orphaned");
const lenses = api.editorProposalController.provideCodeLenses(doc);
const titles = lenses.map((l) => l.command?.title);
assert.deepStrictEqual(titles, ["✓ Keep", "✗ Reject"], "the pair survives the tweak");
assert.deepStrictEqual(lenses[1].command!.arguments, [id], "reject lens still targets the tweaked proposal");
// The reject it routes at still restores the original (the Task-1 contract).
assert.ok(await p.revertInPlace(p.keyFor(doc), id!), "reject via the surviving gesture succeeds");
await settle();
assert.strictEqual(doc.getText(), `# T\n\n${ORIGINAL}\n`, "original restored exactly (INV-5)");
});
});
// Reload-safety: a proposal that was optimistically applied in a PRIOR session