Files

78 lines
4.1 KiB
TypeScript

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<void> {
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
}
}
}