import * as assert from "assert"; import * as fs from "fs"; import * as path from "path"; import * as vscode from "vscode"; import type { CowritingApi } from "../../../src/extension"; const WS = process.env.E2E_WORKSPACE!; const settle = () => new Promise((r) => setTimeout(r, 400)); async function getApi(): Promise { const ext = vscode.extensions.getExtension("benstull.vscode-cowriting-plugin")!; const api = (await ext.activate()) as CowritingApi; assert.ok(api?.trackChangesPreviewController && api?.proposalController, "exports controllers"); return api; } async function freshDoc(rel: string, body: string): Promise { const abs = path.join(WS, rel); fs.mkdirSync(path.dirname(abs), { recursive: true }); fs.writeFileSync(abs, body, "utf8"); const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(abs)); await vscode.window.showTextDocument(doc); // INV-10: attribution tracking (accept's per-word attribution, INV-40) is // gated on coediting (Task 4). await vscode.commands.executeCommand("cowriting.coeditDocument"); await settle(); return doc; } // #47 SLICE-2 (review, P1): a document rewrite proposes ONE F4 proposal per // CHANGED BLOCK (INV-39 supersedes INV-37's per-word cut), but accepting a block // reconciles attribution at WORD granularity (INV-40 — block = decision unit, // word = attribution unit). Host E2E, no LLM (the edit turn is stubbed). suite("F12 SLICE-2 — block-granularity document proposals (#47, INV-39/40/41)", () => { // PUC-4: M changed blocks → M proposals; unchanged blocks → none. test("edits across two paragraphs → two proposals; the untouched paragraph yields none (INV-39)", async () => { const doc = await freshDoc( "docs/f12-multi.md", "# Doc\n\nFirst paragraph alpha.\n\nSecond paragraph beta.\n\nThird paragraph gamma.\n", ); const api = await getApi(); const editFlow = api.editFlow; editFlow.setEditTurnForTest(async () => ({ replacement: "# Doc\n\nFirst paragraph ALPHA.\n\nSecond paragraph beta.\n\nThird paragraph GAMMA.\n", model: "sonnet", sessionId: "e2e-f12-multi", })); const ids = await editFlow.runEditAndPropose(doc, { kind: "document" }, "uppercase the first/last nouns"); await settle(); assert.strictEqual(ids.length, 2, "two changed blocks → two proposals; the unchanged middle block → none"); // each proposal's anchor spans a whole block, and the replacement is that block's rewrite const views = api.proposalController.listProposals(doc); const replacements = views.map((v) => v.replacement).sort(); assert.deepStrictEqual(replacements, ["First paragraph ALPHA.", "Third paragraph GAMMA."]); }); // PUC-4 + INV-23: a changed code fence is ONE atomic whole-fence proposal. test("a changed code fence → one atomic proposal over the whole fence (INV-23)", async () => { const doc = await freshDoc( "docs/f12-fence.md", "# Code\n\n```js\nconst a = 1;\nconst b = 2;\n```\n", ); const api = await getApi(); const editFlow = api.editFlow; editFlow.setEditTurnForTest(async () => ({ replacement: "# Code\n\n```js\nconst a = 10;\nconst b = 2;\n```\n", model: "sonnet", sessionId: "e2e-f12-fence", })); const ids = await editFlow.runEditAndPropose(doc, { kind: "document" }, "bump a to 10"); await settle(); assert.strictEqual(ids.length, 1, "a changed fence is one atomic proposal"); const view = api.proposalController.listProposals(doc).find((v) => v.id === ids[0])!; assert.strictEqual(view.replacement, "```js\nconst a = 10;\nconst b = 2;\n```", "whole-fence replacement"); }); // PUC-5 / INV-40: accepting a block attributes ONLY the words Claude changed — // unchanged words in the block are NOT swept into Claude's authorship. test("accepting a block proposal attributes only the changed words to Claude (INV-40)", async () => { const doc = await freshDoc("docs/f12-attr.md", "# T\n\nThe quick brown fox jumps lazily.\n"); const api = await getApi(); const editFlow = api.editFlow; const key = api.proposalController.keyFor(doc); editFlow.setEditTurnForTest(async () => ({ replacement: "# T\n\nThe quick RED fox jumps SLOWLY.\n", model: "sonnet", sessionId: "e2e-f12-attr", })); const ids = await editFlow.runEditAndPropose(doc, { kind: "document" }, "change two words"); await settle(); assert.strictEqual(ids.length, 1, "one changed paragraph → one block proposal"); const ok = await api.proposalController.acceptById(key, ids[0]); assert.ok(ok, "the block proposal accepts"); await settle(); assert.strictEqual(doc.getText(), "# T\n\nThe quick RED fox jumps SLOWLY.\n", "the whole block landed"); const agentTexts = api.attributionController .getSpans(key) .filter((s) => s.authorKind === "agent") .map((s) => doc.getText().slice(s.range.start, s.range.end).trim()) .filter((t) => t.length > 0); // Only the two words Claude actually changed are Claude-attributed. const joined = agentTexts.join(" "); assert.ok(joined.includes("RED"), "the changed word RED is Claude-attributed"); assert.ok(joined.includes("SLOWLY"), "the changed word SLOWLY is Claude-attributed"); assert.ok(!/\bquick\b/.test(joined), "the unchanged word 'quick' is NOT swept into Claude's authorship"); assert.ok(!/\bfox\b/.test(joined), "the unchanged word 'fox' is NOT swept into Claude's authorship"); }); // PUC-4 / INV-41: a block-insertion rewrite produces acceptable proposals and // accepting them all reconstructs the intended document (no born-orphaned hunk). test("a rewrite that inserts a paragraph → acceptable proposals; accept-all reaches the rewrite (INV-41)", async () => { const original = "# Ins\n\nAlpha block.\n\nBeta block.\n"; const rewrite = "# Ins\n\nAlpha block.\n\nBrand new middle block.\n\nBeta block.\n"; const doc = await freshDoc("docs/f12-insert.md", original); const api = await getApi(); const editFlow = api.editFlow; const key = api.proposalController.keyFor(doc); editFlow.setEditTurnForTest(async () => ({ replacement: rewrite, model: "sonnet", sessionId: "e2e-f12-insert" })); const ids = await editFlow.runEditAndPropose(doc, { kind: "document" }, "insert a paragraph"); await settle(); assert.ok(ids.length >= 1, "the insertion produced at least one proposal"); for (const id of ids) { const ok = await api.proposalController.acceptById(key, id); assert.ok(ok, `proposal ${id} is acceptable (not born-orphaned, INV-41)`); await settle(); } assert.strictEqual(doc.getText(), rewrite, "accepting all proposals reconstructs the intended rewrite"); assert.strictEqual(api.proposalController.listProposals(doc).length, 0, "no proposals left pending"); }); });