import * as fs from "node:fs"; import * as path from "node:path"; import * as vscode from "vscode"; import type { CowritingApi } from "../../../src/extension"; import { settleUntil } from "../suite/helpers"; /** * Task 9 (spec §7.1 rung 3 — the real @cline/sdk, manual gate). Drives the * SAME comments-first ask -> reply -> offer -> proposal loop as * test/e2e/suite/commentLoop.test.ts / fullLoop.test.ts, but with BOTH turns * left UNSTUBBED: ThreadController's reply loop and EditFlow's edit turn hit * the real local `claude` login (INV-8 -- the extension itself holds no * credentials). Not part of `npm run test:e2e` / CI -- driven only by * `scripts/smoke-native-loop.mjs` (`npm run smoke:native`), mirroring * `scripts/smoke-live-turn.mjs`'s "manual gate, real SDK" role for F3. * * There is no VS Code API to drive the native comment-widget's OWN submit * gesture from outside the UI (see commentLoop.test.ts's "Finding 1" note) -- * this calls the exact seam a real comment submission reaches * (ThreadController.createThreadOnSelection / .makeThreadEdit), which is the * closest a headless script gets to "post a real comment" on a live doc. */ export async function run(): Promise { const ext = vscode.extensions.getExtension("benstull.vscode-cowriting-plugin"); if (!ext) throw new Error("extension not found -- is extensionDevelopmentPath wired correctly?"); const api = (await ext.activate()) as CowritingApi; if (!api?.threadController || !api?.editFlow || !api?.proposalController || !api?.sidecarRouter) { throw new Error("extension did not export the expected controllers"); } const folder = vscode.workspace.workspaceFolders?.[0]; if (!folder) throw new Error("no workspace folder -- launch with the sandbox/ folder open"); const target = path.join(folder.uri.fsPath, `.smoke-native-loop-${process.pid}.md`); const ORIGINAL = "The quick brown fox jumps over the lazy dog paragraph."; fs.writeFileSync(target, `# Native-loop smoke\n\n${ORIGINAL}\n`, "utf8"); try { const uri = vscode.Uri.file(target); const doc = await vscode.workspace.openTextDocument(uri); const ed = await vscode.window.showTextDocument(doc); await vscode.commands.executeCommand("cowriting.coeditDocument"); const docKey = api.proposalController.keyFor(doc); const start = doc.getText().indexOf(ORIGINAL); ed.selection = new vscode.Selection(doc.positionAt(start), doc.positionAt(start + ORIGINAL.length)); console.log("[smoke-native-loop] posting a real comment (createThreadOnSelection)..."); const threadId = await api.threadController.createThreadOnSelection( "Rewrite this sentence to be more concise.", ); if (!threadId) throw new Error("createThreadOnSelection refused (not coediting, or no selection)"); console.log(`[smoke-native-loop] thread created: ${threadId} -- waiting for the real @cline/sdk reply (this calls the live SDK; may take a while)...`); await settleUntil(() => { const t = api.sidecarRouter.load(docKey)?.threads.find((x) => x.id === threadId); return (t?.messages.length ?? 0) >= 2 && t!.messages[1].author.kind === "agent"; }, 180000); console.log("[smoke-native-loop] reply landed -- thread flipped to \"offer\". Making the edit (real SDK)..."); const ids = await api.threadController.makeThreadEdit(threadId, docKey); if (ids.length === 0) throw new Error("makeThreadEdit produced no proposals (the turn returned an empty/no-op replacement)"); console.log(`[smoke-native-loop] proposal id(s) created: ${ids.join(", ")}`); await settleUntil(() => ids.every((id) => api.proposalController.isApplied(docKey, id)), 10000); for (const id of ids) { const ok = await api.proposalController.finalizeInPlace(docKey, id); console.log(`[smoke-native-loop] accepted proposal ${id}: ${ok}`); } console.log(`[smoke-native-loop] PASS -- ${ids.length} proposal(s) created and accepted: ${ids.join(", ")}`); } finally { try { fs.rmSync(target, { force: true }); } catch { // best-effort cleanup -- never let cleanup mask the real result } } }