From c579f67a43ed253e091e7e391b36661f1046c91a Mon Sep 17 00:00:00 2001 From: BenStullsBets Date: Fri, 26 Jun 2026 14:37:29 -0700 Subject: [PATCH] test(e2e): assert author-colored track changes render in the preview Co-Authored-By: Claude Sonnet 4.6 --- test/e2e/suite/authorship.test.ts | 32 +++++++++------ test/e2e/suite/f10Review.test.ts | 62 ++++++++++++++++++++++++++++-- test/e2e/suite/s48PinClean.test.ts | 10 ++--- test/e2e/suite/undoMarks.test.ts | 5 ++- 4 files changed, 87 insertions(+), 22 deletions(-) diff --git a/test/e2e/suite/authorship.test.ts b/test/e2e/suite/authorship.test.ts index 638c161..efaa760 100644 --- a/test/e2e/suite/authorship.test.ts +++ b/test/e2e/suite/authorship.test.ts @@ -16,15 +16,16 @@ async function getApi(): Promise { // F10 host E2E (no LLM): the rewrite of the obsolete F9 authorship-mode test. // F9's "authorship" mode / renderAuthorship is gone — the on-state renderReview -// now author-colors Claude's landed prose. This suite confirms a Claude-landed -// span renders as a cw-by-claude span in the on-state preview HTML. Owns its own -// markdown doc, disjoint from the other suites' fixtures. -suite("F10 review preview — Claude-authored prose is cw-by-claude in the on-state (host E2E, no LLM)", () => { +// now author-colors Claude's PENDING proposal blocks as cw-ins-claude. After the +// proposal is accepted the baseline advances (INV-18/F12) and the text becomes +// "unchanged" → renders plain. The class to check is cw-ins-claude (proposal block), +// NOT cw-by-claude (the old F9 authorship render). Owns its own markdown doc. +suite("F10 review preview — pending Claude proposal renders cw-ins-claude in the on-state (host E2E, no LLM)", () => { const DOC_REL = "docs/f10claude.md"; const TARGET = "The sentence Claude will compose over."; const REPLACEMENT = "The sentence CLAUDE COMPOSED via the seam."; - test("an accepted Claude edit author-colors as cw-by-claude in the on-state render; mode defaults to on", async () => { + test("a pending Claude proposal carries cw-ins-claude; accepted proposal leaves attribution data intact; mode defaults to on", async () => { const abs = path.join(WS, DOC_REL); fs.mkdirSync(path.dirname(abs), { recursive: true }); fs.writeFileSync(abs, `# F10\n\n${TARGET}\n`, "utf8"); @@ -41,7 +42,7 @@ suite("F10 review preview — Claude-authored prose is cw-by-claude in the on-st assert.ok(api.trackChangesPreviewController.isOpen(key), "preview open"); assert.strictEqual(api.trackChangesPreviewController.getMode(key), "on", "annotations default to on"); - // Claude composes via the seam (propose → accept) + // Claude proposes via the seam (F12 optimistic-apply; proposal stays PENDING) const start = doc.getText().indexOf(TARGET); const id = await vscode.commands.executeCommand("cowriting.proposeAgentEdit", { uri: key, @@ -52,17 +53,26 @@ suite("F10 review preview — Claude-authored prose is cw-by-claude in the on-st sessionId: "e2e-f10", turnId: "turn-f10", }); + assert.ok(id, "propose returns an id"); + await settle(); + + // PENDING proposal: the preview renders the proposal block with cw-ins-claude. + // After accepting, the baseline advances (INV-18) → the text becomes "unchanged" + // and renders plain — so we check BEFORE accepting. + const pendingHtml = api.trackChangesPreviewController.renderHtmlFor(key); + assert.ok( + pendingHtml.includes("cw-ins-claude"), + "pending Claude proposal block carries cw-ins-claude in the on-state render", + ); + + // accept via the seam — baseline advances (machine-landing, INV-18/F12) assert.ok(await api.proposalController.acceptById(DOC_REL, id!), "accept applies via the seam"); await settle(); // attribution recorded a Claude (agent) span (data layer intact) const claudeSpan = api.attributionController.getSpans(DOC_REL).find((s) => s.authorKind === "agent"); - assert.ok(claudeSpan, "Claude span recorded by F3"); + assert.ok(claudeSpan, "Claude span recorded by F3 (data layer intact after accept)"); const spans = api.attributionController.spansFor(doc); assert.ok(spans.some((s) => s.author === "claude"), "spansFor reports a Claude span for the preview"); - - // the on-state render author-colors the landed Claude text as cw-by-claude - const html = api.trackChangesPreviewController.renderHtmlFor(key); - assert.match(html, //, "landed Claude prose is author-colored in the on-state render"); }); }); diff --git a/test/e2e/suite/f10Review.test.ts b/test/e2e/suite/f10Review.test.ts index 496d8c5..bbe9928 100644 --- a/test/e2e/suite/f10Review.test.ts +++ b/test/e2e/suite/f10Review.test.ts @@ -74,7 +74,7 @@ suite("F10 interactive review (host E2E — preview is the single review surface void doc; }); - test("typing produces an added/changed block and a cw-by-human span in the on-state render (PUC-2)", async () => { + test("typing produces an added/changed block and a cw-ins-human span in the on-state render (PUC-2)", async () => { const { key } = await reopen(DOC_REL); const api = await getApi(); const doc = byKey(key)!; @@ -85,13 +85,13 @@ suite("F10 interactive review (host E2E — preview is the single review surface const kinds = (api.trackChangesPreviewController.getLastModel(key) ?? []).map((o) => o.kind); assert.ok(kinds.some((k) => k === "added" || k === "changed"), "an added/changed block after typing"); // Attribution recorded the human span (data layer), and the on-state render - // author-colors that prose as cw-by-human. + // author-colors that prose as cw-ins-human (added blocks use cw-ins-{author} since Task 3/44ef0a2). assert.ok( api.attributionController.spansFor(doc).some((s) => s.author === "human"), "a human span was recorded for the typed text", ); const html = api.trackChangesPreviewController.renderHtmlFor(key); - assert.match(html, //, "typed text is author-colored human in the on-state"); + assert.ok(html.includes("cw-ins-human"), "typed text is author-colored human in the on-state (cw-ins-human for added blocks)"); }); test("propose → the preview surfaces it as a cw-proposal block with ✓/✗ actions (PUC-3)", async () => { @@ -175,7 +175,7 @@ suite("F10 interactive review (host E2E — preview is the single review surface // (INV-33). Assert the actual off-state body the controller posts has no cw- // author/proposal marks — meaningful because the on-state above DID carry one. const offBody = renderPlain(doc.getText()); - assert.ok(!/cw-proposal|cw-by-claude|cw-by-human|cw-del/.test(offBody), "off-state render carries no cw- marks"); + assert.ok(!/cw-proposal|cw-by-|cw-ins-|cw-del/.test(offBody), "off-state render carries no cw- marks"); // toggle back on → marks return. api.trackChangesPreviewController.setMode(key, "on"); @@ -232,6 +232,60 @@ suite("F10 interactive review (host E2E — preview is the single review surface assert.ok(all.includes("cowriting.pinDiffBaseline"), "pinDiffBaseline (baseline data layer) is kept"); void key; }); + + test("author-colored track changes: cw-ins-human + cw-ins-claude appear; unchanged blocks are plain (Tasks 1-4)", async () => { + // Fresh doc with three blocks: one that stays unchanged, one for Claude to target, one for human typing. + const { doc, key } = await freshDoc( + "docs/f10authorcolors.md", + "# Author Colors\n\nUnchanged paragraph that stays.\n\nClaude target block.\n", + ); + const api = await getApi(); + await vscode.commands.executeCommand("cowriting.showTrackChangesPreview"); + await settle(); + + // Human types a new paragraph → an "added" block relative to the opened baseline. + const edit = new vscode.WorkspaceEdit(); + edit.insert(doc.uri, doc.positionAt(doc.getText().length), "\n\nHuman typed paragraph.\n"); + assert.ok(await vscode.workspace.applyEdit(edit), "human edit applied"); + await settle(); + + // Propose a Claude change (stays PENDING — F12 optimistic-apply; NOT accepted). + // The proposal block renders with cw-ins-claude. + const target = "Claude target block."; + const targetStart = doc.getText().indexOf(target); + assert.ok(targetStart >= 0, "target text present after human edit"); + await vscode.commands.executeCommand("cowriting.proposeAgentEdit", { + uri: key, + start: targetStart, + end: targetStart + target.length, + newText: "Claude replacement block.", + model: "sonnet", + sessionId: "e2e-f10-colors", + turnId: "turn-colors-1", + }); + await settle(); + + const html = api.trackChangesPreviewController.renderHtmlFor(key); + + // The human-typed added block renders with cw-ins-human. + assert.ok(html.includes("cw-ins-human"), "human added block is colored cw-ins-human"); + + // The pending Claude proposal block renders with cw-ins-claude. + assert.ok(html.includes("cw-ins-claude"), "pending Claude proposal block carries cw-ins-claude"); + + // The unchanged block renders as cw-unchanged with no cw-ins-/cw-by- inside it + // (color = "changed since baseline"; unchanged text is never annotated). + const unchangedBlocks = [...html.matchAll(/
]*>([\s\S]*?)<\/div>/g)]; + assert.ok(unchangedBlocks.length > 0, "at least one unchanged block exists"); + assert.ok( + unchangedBlocks.some((m) => m[1].includes("Unchanged paragraph that stays")), + "the unchanged paragraph is in a cw-unchanged block", + ); + assert.ok( + unchangedBlocks.every((m) => !/cw-ins-|cw-by-/.test(m[1])), + "unchanged blocks carry no cw-ins- or cw-by- classes", + ); + }); }); // ---- helpers reused across the order-dependent main-flow tests ---- diff --git a/test/e2e/suite/s48PinClean.test.ts b/test/e2e/suite/s48PinClean.test.ts index c34a6c3..a626f53 100644 --- a/test/e2e/suite/s48PinClean.test.ts +++ b/test/e2e/suite/s48PinClean.test.ts @@ -41,15 +41,15 @@ suite("S48 — pin → fully clean review panel (host E2E, no LLM)", () => { edit.insert(doc.uri, doc.positionAt(doc.getText().length), "\n\nA freshly typed human paragraph.\n"); assert.ok(await vscode.workspace.applyEdit(edit), "operator edit applied"); await settle(); - assert.match(ctl.renderHtmlFor(key), /cw-by-human/, "typed text is author-colored before the pin"); + assert.match(ctl.renderHtmlFor(key), /cw-ins-human/, "typed text is author-colored cw-ins-human before the pin"); // Pin the baseline → zero diff → the panel must be fully clean. ctl.receiveMessage(key, { type: "pinBaseline" }); await settle(); const pinned = ctl.renderHtmlFor(key); - assert.ok(!pinned.includes("cw-by-human"), "no human authorship coloring after pin"); - assert.ok(!pinned.includes("cw-by-claude"), "no Claude authorship coloring after pin"); - assert.ok(!pinned.includes("cw-add") && !pinned.includes("cw-del"), "no change marks after pin"); + assert.ok(!pinned.includes("cw-ins-human"), "no human insertion marks after pin"); + assert.ok(!pinned.includes("cw-ins-claude"), "no Claude insertion marks after pin"); + assert.ok(!pinned.includes("cw-ins-") && !pinned.includes("cw-del-"), "no insertion or deletion marks after pin"); assert.match(pinned, /data-src-start/, "blocks still carry data-src offsets (INV-36 mapping kept)"); assert.ok(pinned.includes("freshly typed human paragraph"), "the body text is still rendered, just plain"); @@ -58,6 +58,6 @@ suite("S48 — pin → fully clean review panel (host E2E, no LLM)", () => { edit2.insert(doc.uri, doc.positionAt(doc.getText().length), "\n\nA second typed paragraph diverges again.\n"); assert.ok(await vscode.workspace.applyEdit(edit2), "second operator edit applied"); await settle(); - assert.match(ctl.renderHtmlFor(key), /cw-by-human/, "authorship coloring returns once the doc diverges from the pin"); + assert.match(ctl.renderHtmlFor(key), /cw-ins-human/, "cw-ins-human authorship coloring returns once the doc diverges from the pin"); }); }); diff --git a/test/e2e/suite/undoMarks.test.ts b/test/e2e/suite/undoMarks.test.ts index 7f6d6d0..1404e4d 100644 --- a/test/e2e/suite/undoMarks.test.ts +++ b/test/e2e/suite/undoMarks.test.ts @@ -78,8 +78,9 @@ suite("F10 #38 — undo does not mis-attribute restored text (host E2E, no LLM)" ); // And the on-state render must not color 'bravo' as human-authored. + // Added blocks use cw-ins-human (not cw-by-human) since Task 3/44ef0a2. const html = api.trackChangesPreviewController.renderHtmlFor(key); - const bravoColoredHuman = /[^<]*bravo/.test(html); - assert.ok(!bravoColoredHuman, "restored 'bravo' is not colored cw-by-human in the preview"); + const bravoColoredHuman = /cw-ins-human[^<]*bravo|<[^>]+class="[^"]*cw-ins-human[^"]*"[^>]*>[^<]*bravo/.test(html); + assert.ok(!bravoColoredHuman, "restored 'bravo' is not colored cw-ins-human in the preview"); }); });