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); 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, selection-aware (selection → editSelection; no selection → // editDocument), both markdown/authorable-gated, both routing through the single // runEditAndPropose path (INV-38). The menu `when` clauses are declarative, so we // assert them directly; the tab-targeting behavior is exercised through the command. suite("F12 SLICE-1 — Ask-Claude reach (#42, INV-38)", () => { // PUC-1/2: editor BODY (editor/context) is selection-aware + markdown-gated. test("editor/context offers editSelection (with selection) and editDocument (without), markdown+authorable", () => { const m = menu("editor/context"); const sel = m.find((e) => e.command === "cowriting.editSelection"); const doc = m.find((e) => e.command === "cowriting.editDocument"); assert.ok(sel, "editSelection is in editor/context"); assert.ok(doc, "editDocument is in editor/context"); assert.match(sel!.when ?? "", /editorHasSelection/, "editSelection shows only with a selection"); assert.ok(!/!\s*editorHasSelection/.test(sel!.when ?? ""), "editSelection is not gated on NO selection"); assert.match(sel!.when ?? "", /editorLangId == markdown/, "editSelection gated on markdown"); assert.match(sel!.when ?? "", /resourceScheme == file|resourceScheme == untitled/, "editSelection gated authorable"); assert.match(doc!.when ?? "", /!\s*editorHasSelection/, "editDocument shows only without a selection"); assert.match(doc!.when ?? "", /editorLangId == markdown/, "editDocument gated on markdown"); assert.match(doc!.when ?? "", /resourceScheme == file|resourceScheme == untitled/, "editDocument gated authorable"); }); // PUC-3: editor TAB (editor/title/context) carries the same selection-aware pair. test("editor/title/context offers editSelection (with selection) and editDocument (without), markdown-gated", () => { const m = menu("editor/title/context"); const sel = m.find((e) => e.command === "cowriting.editSelection"); const doc = m.find((e) => e.command === "cowriting.editDocument"); assert.ok(sel, "editSelection is in editor/title/context"); assert.ok(doc, "editDocument is in editor/title/context"); assert.match(sel!.when ?? "", /editorHasSelection/, "tab editSelection shows only with a selection"); assert.ok(!/!\s*editorHasSelection/.test(sel!.when ?? ""), "tab editSelection is not gated on NO selection"); assert.match(sel!.when ?? "", /resourceLangId == markdown/, "tab editSelection gated on markdown"); assert.match(doc!.when ?? "", /!\s*editorHasSelection/, "tab editDocument shows only without a selection"); assert.match(doc!.when ?? "", /resourceLangId == markdown/, "tab editDocument gated on markdown"); }); // 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 ctl = api.trackChangesPreviewController; // 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 instruction prompt (sealed input box can't run in CI) + the LLM turn. const origInput = vscode.window.showInputBox; (vscode.window as any).showInputBox = async () => "rewrite it"; ctl.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 { (vscode.window as any).showInputBox = origInput; } // 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", ); // INV-10: proposing never mutates the document. assert.ok(b.doc.getText().includes("tab target paragraph"), "tab doc unchanged by propose"); }); // 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 ctl = api.trackChangesPreviewController; 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 origInput = vscode.window.showInputBox; (vscode.window as any).showInputBox = async () => "rewrite it"; ctl.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 { (vscode.window as any).showInputBox = origInput; } assert.ok(api.proposalController.listProposals(a.doc).length >= 1, "active doc received the proposal(s) on no-arg"); }); });