fix: final-review wave — preview host INV-10 gate, thread status+offer contextValue tokens, establish-on-open baseline, polish

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
BenStullsBets
2026-07-02 16:04:40 -07:00
parent 17366a4fb9
commit 0d69a29228
10 changed files with 274 additions and 19 deletions
+35
View File
@@ -6,6 +6,8 @@ import * as path from "node:path";
import * as vscode from "vscode";
import { activateApi, settle, settleUntil } from "./helpers";
const WS = process.env.E2E_WORKSPACE!;
suite("baseline router (PUC-1, INV-7/D13/D14)", () => {
test("snapshot mode: enter captures; markReviewed re-pins clean", async () => {
const api = await activateApi();
@@ -49,4 +51,37 @@ suite("baseline router (PUC-1, INV-7/D13/D14)", () => {
);
assert.strictEqual(api.diffViewController.getBaseline(doc.uri.toString())?.reason, "head");
});
// Finding 3 (final whole-branch review, session native-surfaces-exec):
// DiffViewController only establishes a baseline on the registry's ENTER
// transition (a doc it can find already open) and, at startup, for docs
// already open at that moment. A registry member entered while its document
// was NOT yet open (the real-world case: a persisted coediting set restored
// after a window reload, whose member is only lazily opened later) used to
// never get a baseline at all — reproduced here directly via
// `coeditingRegistry.enter(uri)` on a URI with no open document yet (the
// registry API doesn't require one), then a genuine later open.
test("a registry member entered before its doc was open gets its baseline established once the doc IS opened", async () => {
const api = await activateApi();
const rel = "docs/finding3-establish-on-open.md";
const abs = path.join(WS, rel);
const content = "# late open\n\nestablish-on-open should catch this.\n";
fs.writeFileSync(abs, content, "utf8");
const uri = vscode.Uri.file(abs);
// Membership without an open document — DiffViewController's enter-
// transition handler no-ops (`textDocuments.find` misses), reproducing
// the gap: a coediting member with no mode/baseline yet.
api.coeditingRegistry.enter(uri);
assert.strictEqual(api.coeditingRegistry.isCoediting(uri), true, "sanity: registry membership recorded");
assert.strictEqual(api.diffViewController.modeOf(uri.toString()), undefined, "sanity: the gap is reproduced");
// A genuine later open — the fix's `renderIfOpen`/`onDidOpenTextDocument`
// path must establish the baseline it missed.
const doc = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(doc);
await settleUntil(() => api.diffViewController.modeOf(uri.toString()) !== undefined, 10000);
assert.strictEqual(api.diffViewController.modeOf(uri.toString()), "snapshot");
assert.strictEqual(api.diffViewController.getBaseline(uri.toString())?.text, content);
});
});
+77
View File
@@ -1,7 +1,9 @@
import * as assert from "node:assert";
import * as fs from "node:fs";
import * as path from "node:path";
import MarkdownIt from "markdown-it";
import * as vscode from "vscode";
import { cowritingMarkdownItPlugin } from "../../../src/previewAnnotations";
import { activateApi, settle, settleUntil } from "./helpers";
const WS = process.env.E2E_WORKSPACE!;
@@ -143,4 +145,79 @@ suite("coediting registry (PUC-7)", () => {
assert.strictEqual(api.proposalController.getRendered(key).length, 1, "proposal restored on re-enter");
assert.ok(api.attributionController.getSpans(key).length >= 1, "attribution restored on re-enter");
});
// Finding 1 (final whole-branch review, session native-surfaces-exec): the
// preview-annotation host's INV-10 gate must never leak one document's
// baseline/spans into another document's preview. The built-in preview's
// rendered DOM isn't queryable from a host E2E test (§6.8, mirrored from
// Task 7's own previewAnnotations.test.ts), so this drives the ACTUAL
// production `previewAnnotationHost` through a real markdown-it instance
// and asserts on the rendered HTML.
test("INV-10: a non-coedited doc's preview never inherits another doc's annotations", async () => {
const api = await activateApi();
const docA = await vscode.workspace.openTextDocument({ language: "markdown", content: "alpha original\n" });
await vscode.window.showTextDocument(docA);
await vscode.commands.executeCommand("cowriting.coeditDocument");
await settle();
const start = docA.getText().indexOf("original");
const okA = await vscode.commands.executeCommand<boolean>("cowriting.applyAgentEdit", {
uri: docA.uri.toString(),
start,
end: start + "original".length,
newText: "REPLACED",
model: "sonnet",
sessionId: "e2e-inv10-leak",
turnId: "turn-e2e-inv10-leak",
});
assert.strictEqual(okA, true, "seam edit applies to docA");
await settle();
// docB is opened (becomes the active editor, so it would win any
// `lastCoeditedUri` fallback if the gate were wrong) but NEVER entered
// into coediting.
const docB = await vscode.workspace.openTextDocument({ language: "markdown", content: "bravo untouched\n" });
await vscode.window.showTextDocument(docB);
await settle();
const md = new MarkdownIt();
cowritingMarkdownItPlugin(md, api.previewAnnotationHost);
const html = md.render(docB.getText(), { currentDocument: docB.uri });
assert.ok(!html.includes("cw-ins-claude"), `docB must render with no annotations, got:\n${html}`);
assert.ok(!html.includes("original"), `docA's baseline text must never leak into docB's preview:\n${html}`);
assert.ok(html.includes("bravo untouched"), "docB renders its own content unannotated");
});
// Finding 1, failure mode (b): `lastCoeditedUri` is never re-validated
// against the registry, so a doc's OWN preview stayed annotated after
// `stopCoediting` — an INV-10 violation for the doc whose gate was just
// closed, not just for a different doc.
test("INV-10: stopCoediting immediately clears a doc's own preview annotations", async () => {
const api = await activateApi();
const doc = await vscode.workspace.openTextDocument({ language: "markdown", content: "gamma original\n" });
await vscode.window.showTextDocument(doc);
await vscode.commands.executeCommand("cowriting.coeditDocument");
await settle();
const start = doc.getText().indexOf("original");
const ok = await vscode.commands.executeCommand<boolean>("cowriting.applyAgentEdit", {
uri: doc.uri.toString(),
start,
end: start + "original".length,
newText: "REPLACED",
model: "sonnet",
sessionId: "e2e-inv10-stop",
turnId: "turn-e2e-inv10-stop",
});
assert.strictEqual(ok, true, "seam edit applies");
await settle();
const md = new MarkdownIt();
cowritingMarkdownItPlugin(md, api.previewAnnotationHost);
const before = md.render(doc.getText(), { currentDocument: doc.uri });
assert.ok(before.includes("cw-ins-claude"), `sanity: annotated while coediting, got:\n${before}`);
await vscode.commands.executeCommand("cowriting.stopCoediting");
await settle();
const after = md.render(doc.getText(), { currentDocument: doc.uri });
assert.ok(!after.includes("cw-ins-claude"), `expected a clean render after stopCoediting, got:\n${after}`);
});
});
+27
View File
@@ -32,9 +32,36 @@ suite("comment → reply → offer → proposal (PUC-8, D10/D19)", () => {
const t = api.sidecarRouter.load(api.proposalController.keyFor(doc))?.threads.find((x) => x.id === threadId);
return (t?.messages.length ?? 0) >= 2 && t!.messages[1].author.kind === "agent";
}, 10000);
// Finding 2 (final whole-branch review): the machine reply flips the
// thread into an "offer" — this used to CLOBBER the "open" status token
// instead of folding in alongside it, so Resolve/Reopen disappeared from
// the menu the moment any thread got a reply. Assert both tokens survive
// together, directly via the rendered vsThread's contextValue (the exact
// string package.json's `comments/commentThread/title` `when`-clauses key
// on) — no DOM/UI query needed.
const docKey = api.proposalController.keyFor(doc);
const afterReply = api.threadController.getRendered(docKey).find((t) => t.id === threadId);
assert.ok(afterReply, "thread is rendered after the reply lands");
assert.match(
afterReply!.contextValue,
/\bopen\b/,
`expected the "open" status token to survive the offer transition, got "${afterReply!.contextValue}"`,
);
assert.match(
afterReply!.contextValue,
/\boffer\b/,
`expected the "offer" token once the machine reply lands, got "${afterReply!.contextValue}"`,
);
const ids = await api.threadController.makeThreadEdit(threadId, api.proposalController.keyFor(doc));
assert.ok(ids.length >= 1);
assert.ok(api.proposalController.listProposals(doc).length >= 1); // pending, INV-5
// Spending the offer flips it to "offerdone" — status token still intact,
// and the plain "offer" token must not still match (it's a DIFFERENT word,
// not just a substring: /\boffer\b/ must not match "offerdone").
const afterMakeEdit = api.threadController.getRendered(docKey).find((t) => t.id === threadId);
assert.match(afterMakeEdit!.contextValue, /\bopen\b/, "status token still intact after spending the offer");
assert.match(afterMakeEdit!.contextValue, /\bofferdone\b/, "offer token flips to offerdone once spent");
assert.doesNotMatch(afterMakeEdit!.contextValue, /\boffer\b(?!done)/, "the bare offer token is gone once spent");
});
test("a comment on a NON-coedited doc summons nothing (INV-10)", async () => {