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"; import type { Artifact } from "../../../src/model"; const WS = process.env.E2E_WORKSPACE!; const DOC_REL = "docs/attrib.md"; function sidecarPath(): string { return path.join(WS, ".threads", "docs", "attrib.md.json"); } function readSidecar(): Artifact { return JSON.parse(fs.readFileSync(sidecarPath(), "utf8")) as Artifact; } async function openDoc(): Promise { const uri = vscode.Uri.file(path.join(WS, DOC_REL)); const doc = await vscode.workspace.openTextDocument(uri); await vscode.window.showTextDocument(doc); return doc; } async function getApi(): Promise { const ext = vscode.extensions.getExtension("benstull.vscode-cowriting-plugin")!; const api = (await ext.activate()) as CowritingApi; assert.ok(api?.attributionController, "extension exports attributionController"); return api; } async function externalWriteAndReload(uri: vscode.Uri, content: string): Promise { fs.writeFileSync(uri.fsPath, content, "utf8"); const doc = await vscode.workspace.openTextDocument(uri); await vscode.window.showTextDocument(doc); await vscode.commands.executeCommand("workbench.action.files.revert"); return doc; } const settle = () => new Promise((r) => setTimeout(r, 300)); suite("F3 live attribution (host E2E — seam-driven, no LLM)", () => { const TYPED = "A human-typed sentence. "; test("typing produces a live human span", async () => { const doc = await openDoc(); const api = await getApi(); const editor = vscode.window.activeTextEditor!; const insertAt = doc.getText().length; await editor.edit((eb) => eb.insert(doc.positionAt(insertAt), TYPED)); await settle(); const spans = api.attributionController.getSpans(DOC_REL); assert.strictEqual(spans.length, 1); assert.strictEqual(spans[0].authorKind, "human"); assert.strictEqual(doc.getText().slice(spans[0].range.start, spans[0].range.end), TYPED); }); test("a seam edit produces an agent span; mixed edits stay char-honest (INV-7/INV-9)", async () => { const doc = await openDoc(); const api = await getApi(); const target = "target sentence"; const start = doc.getText().indexOf(target); const ok = await api.attributionController.applyAgentEdit( doc, new vscode.Range(doc.positionAt(start), doc.positionAt(start + target.length)), "REWRITTEN-BY-CLAUDE sentence", { kind: "agent", id: "claude", agent: { sdk: "@cline/sdk", model: "sonnet", sessionId: "e2e-run" } }, { turnId: "turn-e2e-1" }, ); assert.strictEqual(ok, true, "seam edit applies"); await settle(); const spans = api.attributionController.getSpans(DOC_REL); const agent = spans.find((s) => s.authorKind === "agent"); assert.ok(agent, "agent span exists"); assert.strictEqual(agent!.turnId, "turn-e2e-1"); assert.strictEqual( doc.getText().slice(agent!.range.start, agent!.range.end), "REWRITTEN-BY-CLAUDE sentence", ); assert.ok(spans.some((s) => s.authorKind === "human"), "human span still tracked"); }); test("stale expectedVersion refuses to apply (spec §6.9)", async () => { const doc = await openDoc(); const api = await getApi(); const ok = await api.attributionController.applyAgentEdit( doc, new vscode.Range(doc.positionAt(0), doc.positionAt(1)), "X", { kind: "agent", id: "claude", agent: { sdk: "@cline/sdk", model: "sonnet", sessionId: "e2e" } }, { expectedVersion: doc.version - 1 }, ); assert.strictEqual(ok, false); }); test("save persists attributions[]; reload restores spans at re-resolved anchors (PUC-4)", async () => { const doc = await openDoc(); const api = await getApi(); await doc.save(); await settle(); const art = readSidecar(); assert.ok(art.attributions.length >= 2, "human + agent records persisted"); const agentRec = art.attributions.find((a) => a.author.kind === "agent")!; assert.strictEqual(art.anchors[agentRec.anchorId].fingerprint.text, "REWRITTEN-BY-CLAUDE sentence"); assert.strictEqual(agentRec.turnId, "turn-e2e-1"); api.attributionController.loadAll(doc); const spans = api.attributionController.getSpans(DOC_REL); assert.ok(spans.some((s) => s.authorKind === "agent"), "agent span restored from sidecar"); assert.strictEqual(api.attributionController.getOrphanCount(DOC_REL), 0); }); test("external move re-anchors; mangling the text orphans (INV-1/INV-6)", async () => { const uri = vscode.Uri.file(path.join(WS, DOC_REL)); const original = fs.readFileSync(uri.fsPath, "utf8"); const api = await getApi(); let doc = await externalWriteAndReload(uri, "PREPENDED LINE\n\n" + original); await settle(); api.attributionController.loadAll(doc); let spans = api.attributionController.getSpans(DOC_REL); const agent = spans.find((s) => s.authorKind === "agent")!; const moved = doc.getText().indexOf("REWRITTEN-BY-CLAUDE sentence"); assert.strictEqual(agent.range.start, moved, "agent span re-anchored after move"); assert.strictEqual(api.attributionController.getOrphanCount(DOC_REL), 0); const mangled = doc.getText().replace("REWRITTEN-BY-CLAUDE sentence", "now something else entirely"); doc = await externalWriteAndReload(uri, mangled); await settle(); api.attributionController.loadAll(doc); spans = api.attributionController.getSpans(DOC_REL); assert.ok(!spans.some((s) => s.authorKind === "agent"), "mangled agent span not rendered"); assert.ok(api.attributionController.getOrphanCount(DOC_REL) >= 1, "…it is orphaned instead (INV-1)"); }); });