2170a0d282
Deletes the last bespoke UI surface (TrackChangesPreviewController + its sealed webview client assets); every entry point re-points at native VS Code chrome per spec §5: the #41 right-click entries become cowriting.openReviewPreview (enter coediting if needed -> "Open Preview to the Side"), Ctrl+Alt+R/Cmd+Alt+R moves to cowriting.reviewChanges (native diff), and the F12 CodeLens per-proposal titles read "Keep"/"Reject" with a top-of-file "Keep all (N)"/"Reject all" pair once >=2 proposals are pending. EditFlow drops its own askClaude/askEditInstruction (the webview's only caller) and its now-unused constructor params. Coverage that lived only in the webview's test seams (renderHtmlFor, receiveMessage, isOpen, ...) moves to direct calls against the surviving controllers/pure renderReview (test/e2e/suite/helpers.ts gains a shared renderHtmlFor probe); a genuine gap (pending-proposal + unchanged-block rendering) is backfilled in test/previewAnnotations.test.ts. README's "how it works" is rewritten as the native-surface map; the superseded F6/F7/F9/ F10/F11 sections are kept as a marked historical record rather than deleted outright. 292 unit + 91 E2E green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
86 lines
4.1 KiB
TypeScript
86 lines
4.1 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?.editFlow && api?.proposalController, "exports edit flow + proposal controller");
|
|
return api;
|
|
}
|
|
|
|
async function freshDoc(rel: string, body: string): Promise<vscode.TextDocument> {
|
|
const abs = path.join(WS, rel);
|
|
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
fs.writeFileSync(abs, body, "utf8");
|
|
const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(abs));
|
|
await vscode.window.showTextDocument(doc);
|
|
// INV-10: proposal rendering (F12 optimistic-apply, driven by
|
|
// onDidChangeProposals) is gated on coediting (Task 4).
|
|
await vscode.commands.executeCommand("cowriting.coeditDocument");
|
|
await settle();
|
|
return doc;
|
|
}
|
|
|
|
// #60 host E2E (no LLM): live progress is purely additive observability. The
|
|
// harness can't read notification subtitles, so it asserts the CONTRACT —
|
|
// progress events don't change the proposals (INV-44), and an aborted turn
|
|
// proposes nothing (INV-47). The live notification/OutputChannel UI itself is
|
|
// covered by the turnProgress unit tests + the manual smoke.
|
|
suite("#60 live turn progress (additive + cancel)", () => {
|
|
test("a stub that emits progress still produces the same proposals (INV-44)", async () => {
|
|
const doc = await freshDoc("docs/live60-additive.md", "# Title\n\nOld paragraph.\n");
|
|
const api = await getApi();
|
|
const editFlow = api.editFlow;
|
|
|
|
// The stub honors opts.onProgress (emitting synthetic snapshots) but returns
|
|
// the same rewrite — proposals must be unaffected by progress events.
|
|
editFlow.setEditTurnForTest(async (_i, _text, opts) => {
|
|
opts?.onProgress?.({ phase: "writing", chars: 5 });
|
|
opts?.onProgress?.({ phase: "writing", chars: 13, tokens: 1234, textDelta: "New paragraph." });
|
|
return { replacement: "# Title\n\nNew paragraph.\n", model: "sonnet", sessionId: "e2e-live60" };
|
|
});
|
|
|
|
const ids = await editFlow.runEditAndPropose(doc, { kind: "document" }, "rewrite the paragraph");
|
|
await settle();
|
|
assert.strictEqual(ids.length, 1, "one changed block → one proposal, regardless of progress events");
|
|
const view = api.proposalController.listProposals(doc).find((v) => v.id === ids[0]);
|
|
assert.ok(view, "the proposal is live");
|
|
// F12 (INV-48): the EditorProposalController optimistically applies the proposed
|
|
// text into the buffer, so after settle the active-editor buffer shows the
|
|
// replacement. The proposal is still pending (not finalized) until Accept.
|
|
assert.ok(doc.getText().includes("New paragraph."), "F12 optimistic-apply: proposed text in buffer");
|
|
});
|
|
|
|
test("an aborted turn proposes nothing (INV-47)", async () => {
|
|
const doc = await freshDoc("docs/live60-cancel.md", "# Title\n\nOld paragraph.\n");
|
|
const api = await getApi();
|
|
const editFlow = api.editFlow;
|
|
|
|
// The stub throws ONLY when the aborted signal reached it — so if opts.signal
|
|
// failed to thread through runEditAndPropose, the stub would instead return a
|
|
// rewrite and create a proposal, failing this test. That proves propagation.
|
|
editFlow.setEditTurnForTest(async (_i, _text, opts) => {
|
|
if (opts?.signal?.aborted) throw new Error("claude-code turn aborted");
|
|
return { replacement: "# Title\n\nSHOULD NOT BE PROPOSED.\n", model: "sonnet", sessionId: "e2e-live60-nope" };
|
|
});
|
|
|
|
const ac = new AbortController();
|
|
ac.abort();
|
|
let ids: string[] = [];
|
|
try {
|
|
ids = await editFlow.runEditAndPropose(doc, { kind: "document" }, "rewrite", { signal: ac.signal });
|
|
} catch {
|
|
ids = [];
|
|
}
|
|
await settle();
|
|
assert.strictEqual(ids.length, 0, "aborted turn must create no proposals");
|
|
assert.strictEqual(api.proposalController.listProposals(doc).length, 0, "no pending proposals after abort");
|
|
});
|
|
});
|