Files
vscode-cowriting-plugin/test/e2e/suite/diffView.test.ts
T
BenStullsBets 447a1170ce feat: baseline router — git HEAD + snapshot per INV-7/D13/D14; retire machine-landing advance (spec §6.4)
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>
2026-07-02 07:09:55 -07:00

174 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as assert from "assert";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import * as vscode from "vscode";
import type { CowritingApi } from "../../../src/extension";
const WS = process.env.E2E_WORKSPACE!;
const DOC_REL = "docs/diffview.md";
const docUri = () => vscode.Uri.file(path.join(WS, DOC_REL)).toString();
async function openDoc(): Promise<vscode.TextDocument> {
const uri = vscode.Uri.file(path.join(WS, DOC_REL));
const doc = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(doc);
return doc;
}
/** Open the doc AND enter coediting (INV-10) — baselines are established only
* on entry (Task 2). Idempotent across tests: re-entering an already-coedited
* doc is a no-op, so the once-captured baseline survives (order-dependent
* suite, same as before). */
async function openAndCoedit(): Promise<vscode.TextDocument> {
const doc = await openDoc();
await vscode.commands.executeCommand("cowriting.coeditDocument");
await settle();
return doc;
}
async function getApi(): Promise<CowritingApi> {
const ext = vscode.extensions.getExtension("benstull.vscode-cowriting-plugin")!;
const api = (await ext.activate()) as CowritingApi;
assert.ok(api?.diffViewController, "extension exports diffViewController");
return api;
}
const settle = () => new Promise((r) => setTimeout(r, 300));
// The F6 two-pane vscode.diff VIEW was removed in #34 (F10's rendered preview is
// the single review surface); only the baseline DATA layer survives, consumed by
// F7/F10. This suite covers that data layer. Order-dependent (F2F4 pattern):
// later tests consume earlier state. Owns docs/diffview.md exclusively. The
// baseline works on ANY file (#19), so the last two tests use an out-of-workspace
// file and an untitled buffer.
//
// Native-surfaces migration (Task 2, spec §6.4/INV-7): a baseline is now
// established only once a document ENTERS coediting (not merely opened), and
// the fixture workspace is NOT a git repo, so every doc here resolves to
// SNAPSHOT mode. The shipped machine-landing baseline advance (#48/INV-18) is
// retired — an accepted proposal now stays a visible change-since-baseline
// until "Mark Changes as Reviewed" (D21).
suite("F6 baseline data layer (host E2E — any file, programmatic seam ingress, no LLM)", () => {
const TARGET = "A target sentence Claude will rewrite via the seam.";
const REPLACEMENT = "A SENTENCE CLAUDE REWROTE via the seam.";
test("entering coediting on a snapshot-mode doc captures an `entered` baseline equal to the buffer (INV-7)", async () => {
const doc = await openAndCoedit();
const api = await getApi();
assert.strictEqual(api.diffViewController.modeOf(docUri()), "snapshot", "no git repo → snapshot mode");
const baseline = api.diffViewController.getBaseline(docUri());
assert.ok(baseline, "baseline captured on coediting entry");
assert.strictEqual(baseline!.reason, "entered");
assert.strictEqual(baseline!.text, doc.getText(), "baseline = entry-time buffer");
});
test("typing leaves the baseline unchanged while the buffer diverges", async () => {
const doc = await openAndCoedit();
const api = await getApi();
const before = api.diffViewController.getBaseline(docUri())!.text;
const edit = new vscode.WorkspaceEdit();
edit.insert(doc.uri, new vscode.Position(0, 0), "OPERATOR ADDED LINE\n");
assert.ok(await vscode.workspace.applyEdit(edit), "operator edit applied");
await settle();
assert.strictEqual(api.diffViewController.getBaseline(docUri())!.text, before, "baseline unchanged by typing");
assert.notStrictEqual(doc.getText(), before, "buffer diverged");
});
test("accepting a proposal does NOT advance the baseline — the landed text stays a change (INV-7/D21, #48 retired)", async () => {
const doc = await openAndCoedit();
const api = await getApi();
const before = api.diffViewController.getBaseline(docUri())!;
const start = doc.getText().indexOf(TARGET);
assert.ok(start >= 0, "fixture contains the target");
const id = await vscode.commands.executeCommand<string>("cowriting.proposeAgentEdit", {
uri: doc.uri.toString(),
start,
end: start + TARGET.length,
newText: REPLACEMENT,
model: "sonnet",
sessionId: "e2e-diff",
turnId: "turn-d1",
});
assert.ok(id, "propose returns an id");
assert.ok(await api.proposalController.acceptById(DOC_REL, id!), "accept applies via the seam");
await settle();
const baseline = api.diffViewController.getBaseline(docUri())!;
assert.strictEqual(baseline.reason, before.reason, "baseline reason untouched by the landing");
assert.strictEqual(baseline.text, before.text, "baseline text untouched by the landing");
assert.ok(!baseline.text.includes(REPLACEMENT), "landed text is NOT folded into the baseline");
assert.notStrictEqual(baseline.text, doc.getText(), "baseline != buffer — the landing reads as a change");
});
test("an operator edit after the landing keeps baseline ≠ buffer (the operator delta)", async () => {
const doc = await openAndCoedit();
const api = await getApi();
const edit = new vscode.WorkspaceEdit();
edit.insert(doc.uri, new vscode.Position(0, 0), "POST-LANDING OPERATOR LINE\n");
assert.ok(await vscode.workspace.applyEdit(edit));
await settle();
assert.notStrictEqual(
api.diffViewController.getBaseline(docUri())!.text,
doc.getText(),
"operator changes show against the untouched baseline",
);
});
test("markReviewed resets the baseline to now: baseline == buffer, reason pinned", async () => {
const doc = await openAndCoedit();
const api = await getApi();
await vscode.commands.executeCommand("cowriting.markReviewed");
await settle();
const baseline = api.diffViewController.getBaseline(docUri())!;
assert.strictEqual(baseline.reason, "pinned");
assert.strictEqual(baseline.text, doc.getText(), "pinned baseline == current buffer (change-marks empty)");
});
test("the baseline is persisted in GLOBAL storage, never the repo (INV-19)", async () => {
await openAndCoedit();
const api = await getApi();
const p = api.diffViewController.baselineFilePath(docUri());
assert.ok(p, "storage-backed baseline path is available for a file: doc");
assert.ok(fs.existsSync(p!), `baseline file exists at ${p}`);
const onDisk = JSON.parse(fs.readFileSync(p!, "utf8"));
assert.strictEqual(onDisk.uri, docUri(), "baseline records the document URI");
assert.strictEqual(onDisk.reason, "pinned", "last epoch (markReviewed) persisted");
assert.strictEqual(onDisk.text, api.diffViewController.getBaseline(docUri())!.text, "on-disk == in-memory");
// INV-19: baseline lives under the extension's storage dir, not the repo.
assert.ok(!p!.includes(`${path.sep}.threads${path.sep}`), "baseline is NOT in the sidecar tree");
assert.ok(!p!.startsWith(WS + path.sep), "baseline is NOT under the workspace folder");
assert.ok(p!.includes(`${path.sep}baselines${path.sep}`), "baseline lives under <globalStorage>/baselines/");
});
test("a baseline is captured + persisted for a file OUTSIDE the workspace folder (#19)", async () => {
const api = await getApi();
const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), "cowriting-outside-"));
const outsidePath = path.join(outsideDir, "outside.md");
fs.writeFileSync(outsidePath, "# Outside the workspace\n\nThe operator edits this too.\n", "utf8");
const outsideUri = vscode.Uri.file(outsidePath);
const doc = await vscode.workspace.openTextDocument(outsideUri);
await vscode.window.showTextDocument(doc);
await vscode.commands.executeCommand("cowriting.coeditDocument");
await settle();
// Captured on coediting entry even though it is NOT under the workspace folder.
const baseline = api.diffViewController.getBaseline(outsideUri.toString());
assert.ok(baseline, "baseline captured for an out-of-folder file");
assert.strictEqual(baseline!.reason, "entered");
const fp = api.diffViewController.baselineFilePath(outsideUri.toString());
assert.ok(fp && fs.existsSync(fp), "outside-file baseline persisted in global storage");
assert.ok(!fp!.startsWith(WS + path.sep), "not under the workspace folder");
fs.rmSync(outsideDir, { recursive: true, force: true });
void doc;
});
test("an UNTITLED buffer gets an in-memory baseline, never persisted to disk (#19)", async () => {
const api = await getApi();
const untitled = await vscode.workspace.openTextDocument({ content: "scratch line\n", language: "markdown" });
await vscode.window.showTextDocument(untitled);
await vscode.commands.executeCommand("cowriting.coeditDocument");
await settle();
const key = untitled.uri.toString();
assert.strictEqual(untitled.uri.scheme, "untitled", "it really is an untitled buffer");
const baseline = api.diffViewController.getBaseline(key);
assert.ok(baseline, "untitled buffer got an in-memory baseline");
assert.strictEqual(api.diffViewController.baselineFilePath(key), undefined, "untitled is NOT persisted to disk");
});
});