447a1170ce
Task 2 of the native-surfaces migration plan. GitBaselineAdapter resolves a
document's HEAD blob via the built-in vscode.git extension (hardened with a
.git/logs/HEAD watch that nudges repo.status(), since the git extension's own
watcher doesn't reliably notice out-of-band commits on non-workspace-folder
repos in the E2E host). DiffViewController is reworked into a baseline router:
head mode (git-tracked, never persisted, re-read on commit) vs snapshot mode
(captured on CoeditingRegistry entry, re-pinned by "Mark Changes as Reviewed"
— renamed from cowriting.pinDiffBaseline, D14). The shipped machine-landing
baseline advance (#48/INV-18) is retired: a landed Claude edit now stays a
visible change-since-baseline until commit or review (INV-7/D21). Legacy
on-disk reasons ("opened"/"machine-landing") migrate to "entered" via
BaselineStore.normalizeReason on load.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
250 lines
14 KiB
TypeScript
250 lines
14 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.
|
|
* Also enters coediting (INV-10/Task 2): the baseline is established only on
|
|
* entry, not merely on open. */
|
|
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 vscode.commands.executeCommand("cowriting.coeditDocument");
|
|
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`.
|
|
// Renamed cowriting.pinDiffBaseline → cowriting.markReviewed (Task 2, D14).
|
|
test("markReviewed 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.markReviewed",
|
|
);
|
|
assert.ok(entry, "markReviewed 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");
|
|
});
|
|
|
|
// #47 (was INV-37): Edit Document now cuts at BLOCK granularity — two word
|
|
// changes in ONE paragraph are ONE block proposal (INV-39 supersedes INV-37's
|
|
// per-word cut). Full block coverage lives in f12Review.test.ts.
|
|
test("runEditAndPropose(document) — two word edits in one paragraph → ONE block proposal (PUC-4, INV-39)", 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, 1, "two changed words in one block → ONE block proposal (INV-39)");
|
|
|
|
const views = api.proposalController.listProposals(doc);
|
|
const view = views.find((v) => v.id === ids[0]);
|
|
assert.ok(view, "the returned proposal id is a live pending proposal");
|
|
assert.strictEqual(
|
|
view!.replacement,
|
|
"The quick RED fox jumps over the lazy CAT.",
|
|
"the block proposal carries the whole rewritten paragraph",
|
|
);
|
|
// F12 (INV-48): EditorProposalController optimistically applies the proposed text,
|
|
// so after settle the buffer has the replacement. The proposal is still pending.
|
|
assert.ok(doc.getText().includes("RED fox") && doc.getText().includes("lazy CAT"), "F12 optimistic-apply: proposed text in buffer");
|
|
void key;
|
|
});
|
|
|
|
// SLICE-4: Edit Selection → one single-range proposal over the resolved block-union.
|
|
test("runEditAndPropose(range) with a stubbed turn → exactly one proposal over the resolved range (PUC-3, INV-37)", async () => {
|
|
const body = "# F11 sel\n\nThe target paragraph Claude will rewrite.\n\nAnother untouched paragraph.\n";
|
|
const { doc, key } = await freshDoc("docs/f11sel.md", body);
|
|
const api = await getApi();
|
|
const ctl = api.trackChangesPreviewController;
|
|
await vscode.commands.executeCommand("cowriting.showTrackChangesPreview");
|
|
await settle();
|
|
|
|
const target = "The target paragraph Claude will rewrite.";
|
|
const start = doc.getText().indexOf(target);
|
|
const end = start + target.length;
|
|
ctl.setEditTurnForTest(async (_instruction, text) => {
|
|
assert.strictEqual(text, target, "the turn receives exactly the selected source range");
|
|
return { replacement: "The REWRITTEN paragraph from Claude.", model: "sonnet", sessionId: "e2e-f11-sel" };
|
|
});
|
|
|
|
const ids = await ctl.runEditAndPropose(doc, { kind: "range", start, end }, "rewrite this paragraph");
|
|
await settle();
|
|
assert.strictEqual(ids.length, 1, "a selection yields exactly one proposal");
|
|
const views = api.proposalController.listProposals(doc);
|
|
const view = views.find((v) => v.id === ids[0]);
|
|
assert.ok(view, "the proposal is live");
|
|
assert.strictEqual(view!.replacement, "The REWRITTEN paragraph from Claude.", "carries the turn replacement");
|
|
assert.strictEqual(view!.replaced, target, "replaces exactly the selected range");
|
|
// F12 (INV-48): the EditorProposalController optimistically applies, so the buffer
|
|
// now has the replacement. `view.replaced` still records the original target text.
|
|
assert.ok(doc.getText().includes("The REWRITTEN paragraph from Claude."), "F12 optimistic-apply: proposed text in buffer");
|
|
void key;
|
|
});
|
|
|
|
// SLICE-4: a no-op turn (Claude returns the input unchanged) produces no proposal.
|
|
test("runEditAndPropose(range) where Claude returns the selection unchanged → no proposal", async () => {
|
|
const { doc } = await freshDoc("docs/f11noop.md", "# noop\n\nLeave me exactly as I am.\n");
|
|
const api = await getApi();
|
|
const ctl = api.trackChangesPreviewController;
|
|
await vscode.commands.executeCommand("cowriting.showTrackChangesPreview");
|
|
await settle();
|
|
const target = "Leave me exactly as I am.";
|
|
const start = doc.getText().indexOf(target);
|
|
ctl.setEditTurnForTest(async (_i, text) => ({ replacement: text, model: "sonnet", sessionId: "e2e-noop" }));
|
|
const ids = await ctl.runEditAndPropose(doc, { kind: "range", start, end: start + target.length }, "no change");
|
|
assert.strictEqual(ids.length, 0, "an unchanged replacement proposes nothing");
|
|
});
|
|
|
|
// SLICE-3 (review follow-up): a document rewrite that INSERTS text → an
|
|
// acceptable proposal (not a born-orphaned zero-width hunk), and accepting all
|
|
// hunks of a multi-hunk rewrite lands the intended document.
|
|
test("document rewrite with an insertion → acceptable proposals; accept-all reaches the rewrite", async () => {
|
|
const original = "# F11 accept\n\nThe brown fox sleeps.\n";
|
|
const rewrite = "# F11 accept\n\nThe brown fox QUIETLY sleeps today.\n";
|
|
const { doc } = await freshDoc("docs/f11accept.md", original);
|
|
const api = await getApi();
|
|
const ctl = api.trackChangesPreviewController;
|
|
await vscode.commands.executeCommand("cowriting.showTrackChangesPreview");
|
|
await settle();
|
|
|
|
ctl.setEditTurnForTest(async () => ({ replacement: rewrite, model: "sonnet", sessionId: "e2e-accept" }));
|
|
const ids = await ctl.runEditAndPropose(doc, { kind: "document" }, "expand the sentence");
|
|
await settle();
|
|
assert.ok(ids.length >= 1, "the rewrite produced at least one proposal");
|
|
|
|
// Every proposal must be acceptable (the insertion-anchoring fix): accept each.
|
|
for (const id of ids) {
|
|
const ok = await api.proposalController.acceptById("docs/f11accept.md", id);
|
|
assert.ok(ok, `proposal ${id} is acceptable (not born-orphaned)`);
|
|
await settle();
|
|
}
|
|
assert.strictEqual(doc.getText(), rewrite, "accepting all hunks reconstructs the intended rewrite");
|
|
assert.strictEqual(api.proposalController.listProposals(doc).length, 0, "no proposals left pending");
|
|
});
|
|
|
|
// SLICE-3: the document-scoped command stays registered for #42 reuse (now behind
|
|
// the unified `cowriting.edit`). editDocument is hidden from the palette; the
|
|
// markdown-guarded user-facing palette command is `cowriting.edit`.
|
|
test("cowriting.editDocument stays registered; cowriting.edit is the markdown-guarded palette command", async () => {
|
|
const all = await vscode.commands.getCommands(true);
|
|
assert.ok(all.includes("cowriting.editDocument"), "editDocument command registered");
|
|
assert.ok(all.includes("cowriting.edit"), "unified edit command registered");
|
|
const palette = JSON.parse(fs.readFileSync(path.join(__dirname, "../../../../package.json"), "utf8")).contributes
|
|
.menus.commandPalette as Array<{ command: string; when?: string }>;
|
|
// editDocument is hidden (routed via cowriting.edit).
|
|
const docEntry = palette.find((m) => m.command === "cowriting.editDocument");
|
|
assert.strictEqual(docEntry?.when, "false", "editDocument hidden from the palette");
|
|
// cowriting.edit is the user-facing, markdown-guarded entry.
|
|
const editEntry = palette.find((m) => m.command === "cowriting.edit");
|
|
assert.ok(editEntry, "cowriting.edit has a commandPalette entry");
|
|
assert.match(editEntry!.when ?? "", /editorLangId == markdown/, "guarded on markdown");
|
|
});
|
|
|
|
// SLICE-5: the minimal right-click gateway lives in editor/title (markdown only).
|
|
test("the editor/title gateway opens the preview, and the menu entry is markdown-guarded (PUC-6)", async () => {
|
|
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "../../../../package.json"), "utf8"));
|
|
const entry = (pkg.contributes.menus["editor/title"] as Array<{ command: string; when?: string }>).find(
|
|
(m) => m.command === "cowriting.showTrackChangesPreview",
|
|
);
|
|
assert.ok(entry, "showTrackChangesPreview is in editor/title");
|
|
assert.match(entry!.when ?? "", /editorLangId == markdown/, "gateway guarded on markdown");
|
|
// the command it invokes opens the panel.
|
|
const { key } = await freshDoc("docs/f11gw.md", "# F11 gateway\n\nReachable end to end.\n");
|
|
const api = await getApi();
|
|
await vscode.commands.executeCommand("cowriting.showTrackChangesPreview");
|
|
await settle();
|
|
assert.strictEqual(api.trackChangesPreviewController.isOpen(key), true, "gateway command opens the preview");
|
|
});
|
|
|
|
// SLICE-5: edit controls are inert on a non-authorable (read-only scheme) doc.
|
|
test("toolbar edit controls are disabled for a non-authorable document (PUC-1/7)", async () => {
|
|
const SCHEME = "cwf11ro";
|
|
const provider = new (class implements vscode.TextDocumentContentProvider {
|
|
onDidChange = undefined;
|
|
provideTextDocumentContent(): string {
|
|
return "# Read only\n\nThis markdown doc is not authorable.\n";
|
|
}
|
|
})();
|
|
const reg = vscode.workspace.registerTextDocumentContentProvider(SCHEME, provider);
|
|
try {
|
|
const uri = vscode.Uri.parse(`${SCHEME}:/readonly.md`);
|
|
const doc = await vscode.workspace.openTextDocument(uri);
|
|
await vscode.languages.setTextDocumentLanguage(doc, "markdown");
|
|
await vscode.window.showTextDocument(doc);
|
|
await settle();
|
|
const api = await getApi();
|
|
const key = uri.toString();
|
|
assert.strictEqual(doc.languageId, "markdown", "fixture is markdown");
|
|
await vscode.commands.executeCommand("cowriting.showTrackChangesPreview");
|
|
await settle();
|
|
assert.strictEqual(api.trackChangesPreviewController.isOpen(key), true, "preview opens (reading is always allowed)");
|
|
assert.strictEqual(
|
|
api.trackChangesPreviewController.editControlsEnabled(key),
|
|
false,
|
|
"Pin + Ask-Claude controls are disabled on a non-authorable doc",
|
|
);
|
|
} finally {
|
|
reg.dispose();
|
|
}
|
|
});
|
|
});
|