feat(editor): EditorProposalController — optimistic apply + decorations + CodeLens (#64)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user