test(e2e): assert author-colored track changes render in the preview
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,15 +16,16 @@ async function getApi(): Promise<CowritingApi> {
|
||||
|
||||
// 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<string>("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, /<span class="cw-by-claude">/, "landed Claude prose is author-colored in the on-state render");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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, /<span class="cw-by-human">/, "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<string>("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(/<div class="cw-blk cw-unchanged"[^>]*>([\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 ----
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 = /<span class="cw-by-human">[^<]*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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user