feat(editor): EditorProposalController — optimistic apply + decorations + CodeLens (#64)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
BenStullsBets
2026-06-26 07:39:14 -07:00
parent 8c6e7822b4
commit e5992840d2
9 changed files with 279 additions and 29 deletions
+39 -16
View File
@@ -63,13 +63,14 @@ suite("F4 propose/accept (host E2E — programmatic ingress, no LLM)", () => {
const TARGET = "The propose target sentence lives here.";
const REPLACEMENT = "PROPOSED-BY-CLAUDE replacement sentence.";
test("propose records + renders a pending proposal and does NOT touch the document (INV-10)", async () => {
test("propose records a pending proposal; F12 optimistically applies it (INV-48, reverses INV-10)", async () => {
const doc = await openDoc();
const api = await getApi();
const before = doc.getText();
await proposeViaCommand(doc, TARGET, REPLACEMENT, "turn-p1");
await settle();
assert.strictEqual(doc.getText(), before, "document text unchanged (INV-10)");
// F12 (INV-48): EditorProposalController auto-applies the proposed text into the
// buffer, so after settle the buffer has the replacement text (INV-10 is reversed).
assert.ok(doc.getText().includes(REPLACEMENT), "F12 optimistic-apply: replacement in buffer after propose");
const rendered = api.proposalController.getRendered(DOC_REL);
assert.strictEqual(rendered.length, 1);
assert.strictEqual(rendered[0].pending, true);
@@ -77,7 +78,8 @@ suite("F4 propose/accept (host E2E — programmatic ingress, no LLM)", () => {
assert.strictEqual(rendered[0].canReply, false, "proposals are decide-only — no dead reply input (INV-12)");
const art = readSidecar();
assert.strictEqual(art.proposals.length, 1, "proposal persisted at propose time");
assert.strictEqual(art.anchors[art.proposals[0].anchorId].fingerprint.text, TARGET);
// After optimistic apply, the fingerprint re-anchors to the replacement text.
assert.strictEqual(art.anchors[art.proposals[0].anchorId].fingerprint.text, REPLACEMENT);
});
test("accept applies via the seam: text replaced, Claude-attributed, proposal removed (INV-9/11/13)", async () => {
@@ -97,15 +99,18 @@ suite("F4 propose/accept (host E2E — programmatic ingress, no LLM)", () => {
assert.strictEqual(readSidecar().proposals.length, 0, "removed from the sidecar");
});
test("reject leaves the document untouched and removes the proposal (PUC-3)", async () => {
test("reject reverts the optimistically-applied text and removes the proposal (PUC-3, F12-INV-48)", async () => {
const doc = await openDoc();
const api = await getApi();
const before = doc.getText();
const id = await proposeViaCommand(doc, "A second target for coexistence checks.", "WOULD-BE replacement.", "turn-p2");
await settle();
assert.strictEqual(api.proposalController.rejectById(DOC_REL, id), true);
// F12: optimistic apply has put "WOULD-BE replacement." in the buffer.
assert.ok(doc.getText().includes("WOULD-BE replacement."), "F12: optimistically applied");
// rejectByIdInPlace reverts the buffer to the original (PUC-3, INV-51).
assert.ok(await api.proposalController.rejectByIdInPlace(DOC_REL, id), "rejectByIdInPlace removes + reverts");
await settle();
assert.strictEqual(doc.getText(), before, "document untouched");
assert.strictEqual(doc.getText(), before, "document restored to pre-propose state");
assert.strictEqual(api.proposalController.getRendered(DOC_REL).length, 0);
assert.strictEqual(readSidecar().proposals.length, 0);
});
@@ -114,25 +119,35 @@ suite("F4 propose/accept (host E2E — programmatic ingress, no LLM)", () => {
let doc = await openDoc();
const api = await getApi();
const anchor = "A stable closing paragraph.";
await proposeViaCommand(doc, anchor, "A PROPOSED closing paragraph.", "turn-p3");
const appliedText = "A PROPOSED closing paragraph.";
await proposeViaCommand(doc, anchor, appliedText, "turn-p3");
await settle();
// F12: optimistic apply has put the proposed text in the buffer (anchor → appliedText).
assert.ok(doc.getText().includes(appliedText), "F12: optimistically applied before reload");
const uri = vscode.Uri.file(path.join(WS, DOC_REL));
// doc.getText() now has appliedText; the reload prepends a line.
doc = await externalWriteAndReload(uri, "PREPENDED LINE\n\n" + doc.getText());
await settle();
api.proposalController.renderAll(doc);
const rendered = api.proposalController.getRendered(DOC_REL);
assert.strictEqual(rendered.length, 1, "proposal survived reload");
assert.strictEqual(rendered[0].pending, true, "still decidable");
const moved = doc.getText().indexOf(anchor);
assert.strictEqual(rendered[0].range.start, moved, "re-anchored after the move");
// After F12 re-anchor, the fingerprint tracks the appliedText, not the original anchor.
const moved = doc.getText().indexOf(appliedText);
assert.ok(moved >= 0, "applied text present in the reloaded document");
assert.strictEqual(rendered[0].range.start, moved, "re-anchored to appliedText after the move");
});
test("editing the target text makes the proposal stale: flagged, accept refused, doc untouched (INV-11)", async () => {
let doc = await openDoc();
const api = await getApi();
const id = api.proposalController.getRendered(DOC_REL)[0].id;
const mangled = doc.getText().replace("A stable closing paragraph.", "A reworded closing paragraph.");
doc = await externalWriteAndReload(vscode.Uri.file(path.join(WS, DOC_REL)), mangled);
// After F12 optimistic apply the fingerprint tracks the APPLIED text ("A PROPOSED
// closing paragraph."), not the original. Mangle that text to make the proposal stale.
const preMangled = doc.getText();
const mangled = preMangled.replace("A PROPOSED closing paragraph.", "A reworded closing paragraph.");
const uri = vscode.Uri.file(path.join(WS, DOC_REL));
doc = await externalWriteAndReload(uri, mangled);
await settle();
api.proposalController.renderAll(doc);
assert.strictEqual(api.proposalController.getStaleCount(DOC_REL), 1, "flagged stale");
@@ -143,22 +158,30 @@ suite("F4 propose/accept (host E2E — programmatic ingress, no LLM)", () => {
assert.strictEqual(readSidecar().proposals.length, 1, "proposal still pending (recoverable)");
// discard the husk so later tests see a clean sidecar
assert.strictEqual(api.proposalController.rejectById(DOC_REL, id), true);
// Restore the file so subsequent tests can find their fixture targets:
// replace the optimistically-applied text back to the original fixture text.
const restored = preMangled.replace("A PROPOSED closing paragraph.", "A stable closing paragraph.");
doc = await externalWriteAndReload(uri, restored);
await settle();
});
test("multiple pending proposals coexist and are decidable out of order (PUC-5)", async () => {
const doc = await openDoc();
const api = await getApi();
const id1 = await proposeViaCommand(doc, "A stable opening paragraph.", "An ACCEPTED opening paragraph.", "turn-p4");
const id2 = await proposeViaCommand(doc, "PROPOSED-BY-CLAUDE replacement sentence.", "A REPLACED-AGAIN sentence.", "turn-p5");
// Clean up any proposals left over from prior tests (F12: proposals persist in state).
await api.proposalController.rejectAll(doc);
await settle();
assert.strictEqual(api.proposalController.getRendered(DOC_REL).length, 2);
const id1 = await proposeViaCommand(doc, "A stable opening paragraph.", "An ACCEPTED opening paragraph.", "turn-p4");
const id2 = await proposeViaCommand(doc, "A stable closing paragraph.", "A REPLACED closing paragraph.", "turn-p5");
await settle();
assert.strictEqual(api.proposalController.getRendered(DOC_REL).length, 2, "two new proposals");
// decide the SECOND first, then the first — order independence
assert.strictEqual(await api.proposalController.acceptById(DOC_REL, id2), true);
await settle();
assert.strictEqual(await api.proposalController.acceptById(DOC_REL, id1), true);
await settle();
assert.ok(doc.getText().includes("An ACCEPTED opening paragraph."));
assert.ok(doc.getText().includes("A REPLACED-AGAIN sentence."));
assert.ok(doc.getText().includes("A REPLACED closing paragraph."));
assert.strictEqual(api.proposalController.getRendered(DOC_REL).length, 0);
});
});