import * as assert from "assert"; import * as fs from "fs"; import * as os from "os"; 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, 300)); async function getApi(): Promise { const ext = vscode.extensions.getExtension("benstull.vscode-cowriting-plugin")!; const api = (await ext.activate()) as CowritingApi; assert.ok(api?.proposalController && api?.sidecarRouter, "extension exports the authoring API + router"); return api; } async function proposeViaCommand(uri: string, text: string, target: string, newText: string, turnId: string): Promise { const start = text.indexOf(target); assert.ok(start >= 0, `text contains "${target}"`); const id = await vscode.commands.executeCommand("cowriting.proposeAgentEdit", { uri, start, end: start + target.length, newText, model: "sonnet", sessionId: "e2e-oow", turnId, }); assert.ok(id, "propose returns the proposal id"); return id!; } // F8 host E2E (no LLM): authoring on a file OUTSIDE the workspace folder and on // an untitled buffer, plus an in-workspace byte-for-byte regression. Runs in the // WITH-workspace EDH pass (a folder is open; the out-of-folder file lives in a // temp dir outside it — exactly like diffView's out-of-folder test). suite("F8 out-of-workspace authoring (host E2E — programmatic seam, no LLM)", () => { test("out-of-folder file: propose→accept lands Claude-attributed; persisted in GLOBAL storage, no .threads (PUC-1, INV-9/24/25)", async () => { const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), "cowriting-oow-")); const outsidePath = path.join(outsideDir, "sibling.md"); const TARGET = "The sibling-repo sentence Claude edits."; fs.writeFileSync(outsidePath, `# Sibling\n\n${TARGET}\n`, "utf8"); const uri = vscode.Uri.file(outsidePath); const doc = await vscode.workspace.openTextDocument(uri); await vscode.window.showTextDocument(doc); await settle(); const api = await getApi(); const key = uri.toString(); const id = await proposeViaCommand(key, doc.getText(), TARGET, "The sibling sentence CLAUDE REWROTE.", "turn-oow1"); await settle(); // routed to global storage, never the repo const sidecar = api.sidecarRouter.sidecarPath(key)!; assert.ok(sidecar && fs.existsSync(sidecar), `global sidecar exists at ${sidecar}`); assert.ok(sidecar.includes(`${path.sep}sidecars${path.sep}`), "lives under /sidecars/"); assert.ok(!sidecar.includes(`${path.sep}.threads${path.sep}`), "NOT in a .threads/ tree (INV-25)"); assert.ok(!fs.existsSync(path.join(outsideDir, ".threads")), "no .threads/ written beside the out-of-folder file"); assert.ok(await api.proposalController.acceptById(key, id), "accept applies via the seam"); await settle(); assert.ok(doc.getText().includes("The sibling sentence CLAUDE REWROTE."), "replacement landed"); const agent = api.attributionController.getSpans(key).find((s) => s.turnId === "turn-oow1"); assert.ok(agent && agent.authorKind === "agent", "accepted text is Claude-attributed"); assert.strictEqual(api.proposalController.getRendered(key).length, 0, "proposal gone (INV-13)"); // a thread can be opened on the out-of-folder doc await vscode.window.showTextDocument(doc); const editor = vscode.window.activeTextEditor!; editor.selection = new vscode.Selection(doc.positionAt(0), doc.positionAt(8)); const threadId = await api.threadController.createThreadOnSelection("thread on a sibling file"); assert.ok(threadId, "a thread opens on the out-of-folder doc"); // reload-restore from the GLOBAL sidecar (re-anchor content-based) fs.writeFileSync(outsidePath, "PREPENDED\n\n" + doc.getText(), "utf8"); const reloaded = await vscode.workspace.openTextDocument(uri); await vscode.window.showTextDocument(reloaded); await vscode.commands.executeCommand("workbench.action.files.revert"); await settle(); api.threadController.renderAll(reloaded); assert.strictEqual(api.threadController.getRendered(key).length, 1, "thread restored from the global sidecar after reload"); fs.rmSync(outsideDir, { recursive: true, force: true }); }); test("untitled buffer: propose→accept works in-session, state in-memory only (PUC-2)", async () => { const TARGET = "The untitled draft sentence."; const untitled = await vscode.workspace.openTextDocument({ content: `${TARGET}\n`, language: "markdown" }); await vscode.window.showTextDocument(untitled); await settle(); const api = await getApi(); const key = untitled.uri.toString(); assert.strictEqual(untitled.uri.scheme, "untitled", "really untitled"); const id = await proposeViaCommand(key, untitled.getText(), TARGET, "The untitled draft, CLAUDE-EDITED.", "turn-oow2"); await settle(); assert.strictEqual(api.sidecarRouter.sidecarPath(key), undefined, "untitled artifact is in-memory only (no disk)"); assert.ok(await api.proposalController.acceptById(key, id), "accept applies"); await settle(); assert.ok(untitled.getText().includes("The untitled draft, CLAUDE-EDITED."), "replacement landed in the untitled buffer"); const agent = api.attributionController.getSpans(key).find((s) => s.turnId === "turn-oow2"); assert.ok(agent && agent.authorKind === "agent", "untitled accepted text is Claude-attributed"); }); test("in-workspace regression: artifact still lands at /.threads/.json with the repo-relative document.path (INV-2)", async () => { const DOC_REL = "docs/f8regression.md"; const TARGET = "An in-folder sentence for the F8 regression."; const abs = path.join(WS, DOC_REL); fs.mkdirSync(path.dirname(abs), { recursive: true }); fs.writeFileSync(abs, `# F8 regression\n\n${TARGET}\n`, "utf8"); const uri = vscode.Uri.file(abs); const doc = await vscode.workspace.openTextDocument(uri); await vscode.window.showTextDocument(doc); await settle(); const api = await getApi(); // the in-workspace key is the repo-relative path, not the URI string assert.strictEqual( api.sidecarRouter.keyOf({ uri: uri.toString(), fsPath: abs, scheme: "file" }), DOC_REL, "in-folder key is the repo-relative path", ); const id = await proposeViaCommand(uri.toString(), doc.getText(), TARGET, "An in-folder sentence, EDITED.", "turn-oow3"); await settle(); assert.ok(await api.proposalController.acceptById(DOC_REL, id), "accept by the repo-relative key"); await settle(); const sidecar = api.sidecarRouter.sidecarPath(DOC_REL)!; assert.strictEqual(sidecar, path.join(WS, ".threads", "docs", "f8regression.md.json"), "committable .threads/ path unchanged (INV-2)"); assert.ok(fs.existsSync(sidecar), "sidecar written in the repo"); const onDisk = JSON.parse(fs.readFileSync(sidecar, "utf8")); assert.strictEqual(onDisk.document.path, DOC_REL, "document.path is the repo-relative path (byte-for-byte)"); }); });