0d1a5635cb
A whole-document Ask-Claude rewrite is diffed into hunks and surfaced as N independent F4 proposals (one per changed hunk) — reusing the F4 single-range model N times, no new model. Per spec §6.4/§7.2 SLICE-3. - trackChangesModel: pure `diffToHunks(currentText, rewrittenText)` → EditHunk[] (vscode-free, deterministic; diffWordsWithSpace, coalescing adjacent add/remove runs; offsets index currentText). - trackChangesPreview: `runEditAndPropose(document, target, instruction)` — the shared host routine (selection → one single-range propose; document → diff → one propose per hunk; never mutates the doc, INV-10); `askClaude` UI wrapper (host showInputBox keeps LLM/secrets out of the sealed webview, INV-8/35); injectable `editTurn` + `setEditTurnForTest` seam (no LLM in CI); the `askClaude` inbound message branch; `cowriting.editDocument` command for #42 reuse. - package.json: register cowriting.editDocument, palette-guarded on markdown. - webview: ✦ Ask Claude to Edit Document button → { askClaude, scope:"document" }. - unit: diffToHunks fixtures (zero/one/multi-hunk, wholesale, determinism). - host E2E: stubbed multi-hunk rewrite → N matching proposals, doc untouched; editDocument command registered + markdown-guarded. 205 unit + 49 host E2E green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
116 lines
6.0 KiB
TypeScript
116 lines
6.0 KiB
TypeScript
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<CowritingApi> {
|
|
const ext = vscode.extensions.getExtension("benstull.vscode-cowriting-plugin")!;
|
|
const api = (await ext.activate()) as CowritingApi;
|
|
assert.ok(api?.trackChangesPreviewController && api?.diffViewController, "exports preview + diffView");
|
|
return api;
|
|
}
|
|
|
|
/** Create + open a fresh markdown doc under WS, returning the doc + its uri key. */
|
|
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);
|
|
await vscode.window.showTextDocument(doc);
|
|
await settle();
|
|
return { doc, key: uri.toString() };
|
|
}
|
|
|
|
// F11 host E2E (no LLM): the preview toolbar is the primary interaction surface.
|
|
// The webview posts intent messages; the host routes them through the existing
|
|
// F4/F6/F3 seams (INV-35). The webview DOM (real button clicks) is sealed and
|
|
// manual-smoke only; here we simulate the inbound messages via `receiveMessage`.
|
|
suite("F11 preview toolbar (host E2E — message → seam wiring, no LLM)", () => {
|
|
// SLICE-1: the Pin baseline button.
|
|
test("pinBaseline message pins the PREVIEWED doc → marks clear, baseline reason is pinned (PUC-5, INV-35)", async () => {
|
|
const { doc, key } = await freshDoc("docs/f11pin.md", "# F11 pin\n\nA baseline paragraph that will diverge.\n");
|
|
const api = await getApi();
|
|
await vscode.commands.executeCommand("cowriting.showTrackChangesPreview");
|
|
await settle();
|
|
assert.strictEqual(api.trackChangesPreviewController.isOpen(key), true, "panel open");
|
|
|
|
// Diverge from the opened baseline so the preview carries a real change-mark.
|
|
const edit = new vscode.WorkspaceEdit();
|
|
edit.insert(doc.uri, doc.positionAt(doc.getText().length), "\n\nA freshly typed paragraph that diverges.\n");
|
|
assert.ok(await vscode.workspace.applyEdit(edit), "operator edit applied");
|
|
await settle();
|
|
const marked = (api.trackChangesPreviewController.getLastModel(key) ?? []).some((o) => o.kind !== "unchanged");
|
|
assert.ok(marked, "the typed paragraph shows as a change before pinning");
|
|
|
|
// Simulate the webview's Pin baseline button posting its intent.
|
|
api.trackChangesPreviewController.receiveMessage(key, { type: "pinBaseline" });
|
|
await settle();
|
|
|
|
const model = api.trackChangesPreviewController.getLastModel(key) ?? [];
|
|
assert.ok(model.length > 0 && model.every((o) => o.kind === "unchanged"), "after pin, every block is unchanged");
|
|
assert.strictEqual(api.diffViewController.getBaseline(key)?.reason, "pinned", "baseline reason advanced to pinned");
|
|
});
|
|
|
|
// SLICE-1 reachability: the orphaned pin command gets a real palette `when`.
|
|
test("pinDiffBaseline is reachable from the command palette (when: editorLangId == markdown)", async () => {
|
|
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "../../../../package.json"), "utf8"));
|
|
const entry = (pkg.contributes.menus.commandPalette as Array<{ command: string; when?: string }>).find(
|
|
(m) => m.command === "cowriting.pinDiffBaseline",
|
|
);
|
|
assert.ok(entry, "pinDiffBaseline has a commandPalette entry");
|
|
assert.notStrictEqual(entry!.when, "false", "it is no longer hidden (when:false)");
|
|
assert.match(entry!.when ?? "", /editorLangId == markdown/, "guarded on markdown");
|
|
});
|
|
|
|
// SLICE-3: Edit Document → a whole-document rewrite diffed into N F4 proposals.
|
|
test("runEditAndPropose(document) with a stubbed multi-hunk rewrite → N proposals matching the hunks (PUC-4, INV-37)", async () => {
|
|
const { doc, key } = await freshDoc(
|
|
"docs/f11doc.md",
|
|
"# F11 doc\n\nThe quick brown fox jumps over the lazy dog.\n",
|
|
);
|
|
const api = await getApi();
|
|
const ctl = api.trackChangesPreviewController;
|
|
await vscode.commands.executeCommand("cowriting.showTrackChangesPreview");
|
|
await settle();
|
|
|
|
// Stub the host edit turn (no LLM in CI): rewrite two distinct words.
|
|
ctl.setEditTurnForTest(async () => ({
|
|
replacement: "# F11 doc\n\nThe quick RED fox jumps over the lazy CAT.\n",
|
|
model: "sonnet",
|
|
sessionId: "e2e-f11-doc",
|
|
}));
|
|
|
|
const ids = await ctl.runEditAndPropose(doc, { kind: "document" }, "swap brown→RED and dog→CAT");
|
|
await settle();
|
|
assert.strictEqual(ids.length, 2, "two changed words → two independent proposals");
|
|
|
|
const views = api.proposalController.listProposals(doc);
|
|
assert.ok(
|
|
ids.every((id) => views.some((v) => v.id === id)),
|
|
"every returned proposal id is a live pending proposal",
|
|
);
|
|
const replacements = views.map((v) => v.replacement);
|
|
assert.ok(replacements.includes("RED") && replacements.includes("CAT"), "proposals carry the per-hunk replacements");
|
|
// INV-10: proposing never mutates the document.
|
|
assert.ok(doc.getText().includes("brown fox") && doc.getText().includes("lazy dog"), "document unchanged by propose");
|
|
void key;
|
|
});
|
|
|
|
// SLICE-3: the document-scoped command exists for #42 reuse, guarded on markdown.
|
|
test("cowriting.editDocument is a registered command, palette-guarded on markdown", async () => {
|
|
const all = await vscode.commands.getCommands(true);
|
|
assert.ok(all.includes("cowriting.editDocument"), "editDocument command registered");
|
|
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "../../../../package.json"), "utf8"));
|
|
const entry = (pkg.contributes.menus.commandPalette as Array<{ command: string; when?: string }>).find(
|
|
(m) => m.command === "cowriting.editDocument",
|
|
);
|
|
assert.ok(entry, "editDocument has a commandPalette entry");
|
|
assert.match(entry!.when ?? "", /editorLangId == markdown/, "guarded on markdown");
|
|
});
|
|
});
|