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, "exports preview controller"); return api; } async function freshDoc(rel: string, body: string): Promise<{ doc: vscode.TextDocument; key: string }> { const abs = path.join(WS, rel); fs.mkdirSync(path.dirname(abs), { recursive: true }); fs.writeFileSync(abs, body, "utf8"); const uri = vscode.Uri.file(abs); const doc = await vscode.workspace.openTextDocument(uri); // INV-10: cowriting.editDocument now warns instead of editing a non-entered // doc (Task 4) — enter it here (briefly making it active) so the tests below // exercise the routing/targeting behavior, not the gate. The caller // re-establishes whichever doc it wants active afterward. await vscode.window.showTextDocument(doc); await vscode.commands.executeCommand("cowriting.coeditDocument"); return { doc, key: uri.toString() }; } function pkg(): any { return JSON.parse(fs.readFileSync(path.join(__dirname, "../../../../package.json"), "utf8")); } function menu(id: string): Array<{ command: string; when?: string; group?: string }> { return pkg().contributes.menus[id] ?? []; } // SLICE-1 / #42 (reach): "Ask Claude to Edit" reachable from the editor BODY and // the editor TAB. The two split commands (editSelection / editDocument) were // unified behind ONE user-facing command `cowriting.edit` that auto-routes by // selection at runtime (routeEdit: selection → editSelection, none → editDocument) // — so the menus carry a SINGLE selection-agnostic entry, both markdown/authorable // -gated. The routing itself is unit-tested (routeEdit) and exercised through the // command below; here we assert the declarative menu `when` clauses. suite("F12 SLICE-1 — Ask-Claude reach (#42, INV-38)", () => { // PUC-1/2: editor BODY (editor/context) offers the single unified entry, // markdown + authorable gated, NOT selection-gated (it routes both cases). test("editor/context offers a single 'Ask Claude to Edit' (cowriting.edit), markdown+authorable", () => { const m = menu("editor/context"); const edit = m.find((e) => e.command === "cowriting.edit"); assert.ok(edit, "cowriting.edit is in editor/context"); assert.match(edit!.when ?? "", /editorLangId == markdown/, "gated on markdown"); assert.match(edit!.when ?? "", /resourceScheme == file|resourceScheme == untitled/, "gated authorable"); assert.ok(!/editorHasSelection/.test(edit!.when ?? ""), "not selection-gated — one entry routes both cases"); // The old split pair is gone from the menu (the commands stay registered/hidden). assert.ok(!m.some((e) => e.command === "cowriting.editSelection"), "old editSelection menu entry removed"); assert.ok(!m.some((e) => e.command === "cowriting.editDocument"), "old editDocument menu entry removed"); }); // PUC-3: editor TAB (editor/title/context) carries the same single entry. test("editor/title/context offers a single 'Ask Claude to Edit' (cowriting.edit), markdown-gated", () => { const m = menu("editor/title/context"); const edit = m.find((e) => e.command === "cowriting.edit"); assert.ok(edit, "cowriting.edit is in editor/title/context"); assert.match(edit!.when ?? "", /resourceLangId == markdown/, "tab entry gated on markdown"); assert.ok( !m.some((e) => e.command === "cowriting.editSelection" || e.command === "cowriting.editDocument"), "old split pair removed from the tab menu", ); }); // PUC-3 behavior: editDocument invoked with a tab URI targets THAT document, // not whatever editor happens to be active (mirrors #41's clicked-doc resolution). test("editDocument(uri) targets the clicked tab's document, not the active editor", async () => { const api = await getApi(); const editFlow = api.editFlow; // Doc A is the active editor; Doc B is the "clicked tab" we pass by URI. const a = await freshDoc("docs/f12-active.md", "# Active\n\nThe active editor paragraph.\n"); const b = await freshDoc("docs/f12-tab.md", "# Tab\n\nThe tab target paragraph to rewrite.\n"); await vscode.window.showTextDocument(a.doc); await settle(); // Stub the document instruction prompt (the webview can't run in CI) + the LLM turn. const origPrompt = editFlow.askEditInstruction; editFlow.askEditInstruction = async () => "rewrite it"; editFlow.setEditTurnForTest(async () => ({ replacement: "# Tab\n\nThe REWRITTEN tab paragraph.\n", model: "sonnet", sessionId: "e2e-f12-tab", })); try { await vscode.commands.executeCommand("cowriting.editDocument", b.doc.uri); await settle(); } finally { editFlow.askEditInstruction = origPrompt; } // The proposal(s) landed on the TAB doc (B), and the ACTIVE doc (A) has none. assert.ok(api.proposalController.listProposals(b.doc).length >= 1, "tab doc B received the document-edit proposal(s)"); assert.strictEqual( api.proposalController.listProposals(a.doc).length, 0, "active doc A was NOT edited — editDocument honored the tab URI", ); // F12 (INV-48): EditorProposalController optimistically applies proposals into the // buffer, so the proposed text is now in the tab doc B. Confirm one of the proposal // texts is present (the tab doc was edited, not A). assert.ok(b.doc.getText().includes("REWRITTEN"), "F12 optimistic-apply: proposed text in tab doc B"); }); // No URI arg (palette / keybinding) → fall back to the active editor. test("editDocument() with no arg targets the active editor", async () => { const api = await getApi(); const editFlow = api.editFlow; const a = await freshDoc("docs/f12-noarg.md", "# No arg\n\nThe active doc paragraph here.\n"); await vscode.window.showTextDocument(a.doc); await settle(); const origPrompt = editFlow.askEditInstruction; editFlow.askEditInstruction = async () => "rewrite it"; editFlow.setEditTurnForTest(async () => ({ replacement: "# No arg\n\nThe REWRITTEN active doc paragraph.\n", model: "sonnet", sessionId: "e2e-f12-noarg", })); try { await vscode.commands.executeCommand("cowriting.editDocument"); await settle(); } finally { editFlow.askEditInstruction = origPrompt; } assert.ok(api.proposalController.listProposals(a.doc).length >= 1, "active doc received the proposal(s) on no-arg"); }); });