ad5d34b85b
Moves runEditAndPropose, the askClaude gate/prompt/progress wrapper, the
EditTarget type, the editTurn/askEditInstruction/turnSeq state, and the
cowriting.editDocument command registration out of TrackChangesPreviewController
into a new EditFlow (src/editFlow.ts), reachable from the review webview
(delegates) and, from Task 6 on, the thread controller. extension.ts now
constructs EditFlow before the preview controller and routes
acceptAllProposals/rejectAllProposals directly to ProposalController, dropping
the preview-controller indirection. E2E suites that drove the old
trackChangesPreviewController.{setEditTurnForTest,runEditAndPropose,askEditInstruction}
seams now drive the same seams on editFlow.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
90 lines
4.3 KiB
TypeScript
90 lines
4.3 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, "exports preview 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;
|
|
await vscode.commands.executeCommand("cowriting.showTrackChangesPreview");
|
|
await settle();
|
|
|
|
// 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;
|
|
await vscode.commands.executeCommand("cowriting.showTrackChangesPreview");
|
|
await settle();
|
|
|
|
// 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");
|
|
});
|
|
});
|