From a323b827a8c8a71fd46bca375871f515d9a8d59d Mon Sep 17 00:00:00 2001 From: BenStullsBets Date: Thu, 2 Jul 2026 07:41:13 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20gate=20every=20surface=20on=20Coediting?= =?UTF-8?q?Registry=20(INV-10)=20=E2=80=94=20commenting=20ranges=20re-assi?= =?UTF-8?q?gned=20on=20gate=20change=20(spec=20=C2=A76.4=20v0.2.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- package.json | 12 +++---- src/attributionController.ts | 4 +++ src/editorProposalController.ts | 15 +++++++++ src/extension.ts | 32 ++++++++++++++++--- src/proposalController.ts | 11 ++++++- src/threadController.ts | 46 +++++++++++++++++++++++---- src/trackChangesPreview.ts | 12 +++++++ test/e2e/suite/attribution.test.ts | 3 ++ test/e2e/suite/authorship.test.ts | 3 ++ test/e2e/suite/coediting.test.ts | 23 ++++++++++++++ test/e2e/suite/crossrung.test.ts | 2 ++ test/e2e/suite/f12Accept.test.ts | 3 ++ test/e2e/suite/f12Reach.test.ts | 6 ++++ test/e2e/suite/f12Review.test.ts | 3 ++ test/e2e/suite/liveProgress.test.ts | 3 ++ test/e2e/suite/outOfWorkspace.test.ts | 4 +++ test/e2e/suite/proposals.test.ts | 4 +++ test/e2e/suite/threads.test.ts | 3 ++ 18 files changed, 171 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index a889b3d..bd15bcf 100644 --- a/package.json +++ b/package.json @@ -173,11 +173,11 @@ }, { "command": "cowriting.acceptAllProposals", - "when": "editorLangId == markdown" + "when": "editorLangId == markdown && cowriting.isCoediting" }, { "command": "cowriting.rejectAllProposals", - "when": "editorLangId == markdown" + "when": "editorLangId == markdown && cowriting.isCoediting" }, { "command": "cowriting.proposalAcceptMenu", @@ -208,7 +208,7 @@ "editor/title/context": [ { "command": "cowriting.edit", - "when": "resourceLangId == markdown", + "when": "resourceLangId == markdown && cowriting.isCoediting", "group": "1_cowriting@1" }, { @@ -227,12 +227,12 @@ "editor/context": [ { "command": "cowriting.edit", - "when": "editorLangId == markdown && (resourceScheme == file || resourceScheme == untitled)", + "when": "editorLangId == markdown && (resourceScheme == file || resourceScheme == untitled) && cowriting.isCoediting", "group": "1_cowriting@1" }, { "command": "cowriting.createThread", - "when": "editorHasSelection && (resourceScheme == file || resourceScheme == untitled)", + "when": "editorHasSelection && (resourceScheme == file || resourceScheme == untitled) && cowriting.isCoediting", "group": "1_cowriting@2" }, { @@ -277,7 +277,7 @@ "command": "cowriting.edit", "key": "ctrl+alt+e", "mac": "cmd+alt+e", - "when": "editorTextFocus && editorLangId == markdown" + "when": "editorTextFocus && editorLangId == markdown && cowriting.isCoediting" } ] }, diff --git a/src/attributionController.ts b/src/attributionController.ts index 94f759f..528a67e 100644 --- a/src/attributionController.ts +++ b/src/attributionController.ts @@ -21,6 +21,7 @@ import { minimizeReplace, PendingEditRegistry } from "./pendingEdits"; import type { VersionGuard } from "./versionGuard"; import { isAuthorable } from "./workspacePath"; import type { AuthorSpan } from "./trackChangesModel"; +import type { CoeditingRegistry } from "./coeditingRegistry"; /** Test-facing snapshot of live attribution state for a document. */ export interface RenderedSpan { @@ -69,6 +70,7 @@ export class AttributionController implements vscode.Disposable { private readonly store: SidecarRouter, private readonly rootDir: string | undefined, private readonly guard: VersionGuard, + private readonly registry: CoeditingRegistry, ) { this.disposables.push(this.statusItem, this.output, this.applyEmitter); this.disposables.push( @@ -104,6 +106,7 @@ export class AttributionController implements vscode.Disposable { /** Load the sidecar and re-resolve every attribution (live span | orphan). */ loadAll(document: vscode.TextDocument): void { if (!this.isTracked(document)) return; + if (!this.registry.isCoediting(document.uri)) return; const docPath = this.keyOf(document); const s = this.state(docPath); const artifact = this.store.load(docPath); @@ -145,6 +148,7 @@ export class AttributionController implements vscode.Disposable { private onDidChange(e: vscode.TextDocumentChangeEvent): void { if (!this.isTracked(e.document) || e.contentChanges.length === 0) return; + if (!this.registry.isCoediting(e.document.uri)) return; const docPath = this.keyOf(e.document); if (!e.document.isDirty && this.matchesDisk(e.document)) { // Disk sync (revert / external reload): buffer now equals the file on diff --git a/src/editorProposalController.ts b/src/editorProposalController.ts index 370b2c5..b9489a3 100644 --- a/src/editorProposalController.ts +++ b/src/editorProposalController.ts @@ -17,6 +17,7 @@ import type { DiffViewController } from "./diffViewController"; import type { AttributionController } from "./attributionController"; import { decorationPlan, wordEditHunks, authorAt } from "./trackChangesModel"; import { isAuthorable } from "./workspacePath"; +import type { CoeditingRegistry } from "./coeditingRegistry"; export class EditorProposalController implements vscode.Disposable, vscode.CodeLensProvider { private readonly disposables: vscode.Disposable[] = []; @@ -51,6 +52,7 @@ export class EditorProposalController implements vscode.Disposable, vscode.CodeL private readonly proposals: ProposalController, private readonly diffView: DiffViewController, private readonly attribution: AttributionController, + private readonly registry: CoeditingRegistry, ) { this.disposables.push( this.insDeco.human, this.insDeco.claude, this.delDeco.human, this.delDeco.claude, this.delDeco.none, this.lensEmitter, @@ -67,6 +69,13 @@ export class EditorProposalController implements vscode.Disposable, vscode.CodeL const ed = vscode.window.activeTextEditor; if (ed && e.document === ed.document) this.scheduleRender(ed); }), + // INV-10: re-render (and refresh CodeLens) on every gate change — exit clears + // decorations/CodeLens via renderEditor's own gate check, re-entry redraws. + this.registry.onDidChange(({ uri }) => { + const ed = vscode.window.visibleTextEditors.find((e) => e.document.uri.toString() === uri); + if (ed) this.renderEditor(ed); + this.lensEmitter.fire(); + }), // the four QuickPick-backed menu commands vscode.commands.registerCommand("cowriting.proposalAcceptMenu", (id?: string) => this.menu("accept", id)), vscode.commands.registerCommand("cowriting.proposalRejectMenu", (id?: string) => this.menu("reject", id)), @@ -106,6 +115,9 @@ export class EditorProposalController implements vscode.Disposable, vscode.CodeL private async onProposalsChanged(uri: string): Promise { const doc = vscode.workspace.textDocuments.find((d) => d.uri.toString() === uri); if (!doc || doc.languageId !== "markdown" || !isAuthorable(doc.uri.scheme)) return; + // INV-10: a doc that exited coediting between the schedule and this tick gets + // no optimistic-apply/decorations/CodeLens. + if (!this.registry.isCoediting(doc.uri)) return; const key = this.proposals.keyFor(doc); for (const v of this.proposals.listProposals(doc)) { if (v.anchorStart !== null && !this.proposals.isApplied(key, v.id)) { @@ -125,6 +137,8 @@ export class EditorProposalController implements vscode.Disposable, vscode.CodeL for (const a of ["human", "claude", "none"] as const) editor.setDecorations(this.delDeco[a], []); }; if (doc.languageId !== "markdown") return clearAll(); + // INV-10: a non-entered doc gets no decorations at all. + if (!this.registry.isCoediting(doc.uri)) return clearAll(); const key = this.proposals.keyFor(doc); const ins: Record<"human" | "claude", vscode.Range[]> = { human: [], claude: [] }; const del: Record<"human" | "claude" | "none", vscode.DecorationOptions[]> = { human: [], claude: [], none: [] }; @@ -205,6 +219,7 @@ export class EditorProposalController implements vscode.Disposable, vscode.CodeL /** CodeLensProvider: a `Accept ▾` / `Reject ▾` pair above each applied block. */ provideCodeLenses(document: vscode.TextDocument): vscode.CodeLens[] { if (document.languageId !== "markdown") return []; + if (!this.registry.isCoediting(document.uri)) return []; const key = this.proposals.keyFor(document); const lenses: vscode.CodeLens[] = []; for (const v of this.proposals.listProposals(document)) { diff --git a/src/extension.ts b/src/extension.ts index eb73135..cf978a6 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -19,6 +19,8 @@ import { CoeditingRegistry } from "./coeditingRegistry"; import { ScmSurfaceController } from "./scmSurface"; const CHANNEL_NAME = "Cowriting (Cline SDK)"; +/** The exact warning copy for every gated edit gesture (INV-10). */ +const NOT_COEDITING_WARNING = "Run ✦ Coedit this Document with Claude first."; export interface CowritingApi { threadController: ThreadController; @@ -144,16 +146,22 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef // F5 (INV-16): one shared guard — newer-major sidecars are read-only, one // warning per doc across the three co-owning controllers. const versionGuard = new VersionGuard(sidecarRouter); - const threadController = new ThreadController(sidecarRouter, root, versionGuard); + const threadController = new ThreadController(sidecarRouter, root, versionGuard, coeditingRegistry); context.subscriptions.push(threadController); // --- F3: live attribution (Feature #6) --- - const attributionController = new AttributionController(sidecarRouter, root, versionGuard); + const attributionController = new AttributionController(sidecarRouter, root, versionGuard, coeditingRegistry); context.subscriptions.push(attributionController); // --- F4: propose/accept (Feature #12) — constructed before the preview so F10 // can route ✓/✗ through it --- - const proposalController = new ProposalController(sidecarRouter, attributionController, root, versionGuard); + const proposalController = new ProposalController( + sidecarRouter, + attributionController, + root, + versionGuard, + coeditingRegistry, + ); context.subscriptions.push(proposalController); // --- F7/F10: the review preview is the single interactive review surface --- @@ -166,12 +174,18 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef attributionController, proposalController, liveProgressUi, + coeditingRegistry, ); context.subscriptions.push(trackChangesPreviewController); // --- F12 (#64): the editor surface — optimistic-apply proposals into the buffer, // decorate the diff, and provide Accept ▾/Reject ▾ CodeLens (INV-48/49/52/53). --- - const editorProposalController = new EditorProposalController(proposalController, diffViewController, attributionController); + const editorProposalController = new EditorProposalController( + proposalController, + diffViewController, + attributionController, + coeditingRegistry, + ); context.subscriptions.push(editorProposalController); // #46 (INV-42): accept every pending proposal on the active doc in one gesture @@ -289,6 +303,11 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef } if (!editor) return; // unreachable once reason is null, but narrows the type const document = editor.document; + // INV-10: a non-entered doc gets the gate warning, not a turn. + if (!coeditingRegistry.isCoediting(document.uri)) { + void vscode.window.showWarningMessage(NOT_COEDITING_WARNING); + return; + } const selection = editor.selection; // non-empty (selectionRejection guaranteed it) // Capture the selection text + anchor BEFORE prompting — the inline prompt // moves the cursor to the document top (where its box anchors), which would @@ -394,8 +413,11 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef ); // Render threads + attributions for already-open editors, and on future opens. + // INV-10: gated on the registry too — belt-and-suspenders with the per-controller + // gates (renderAll/loadAll already early-return), but keeps the intent explicit + // at the call site and avoids the no-op calls for a non-entered doc. const renderIfOpen = (doc: vscode.TextDocument) => { - if (isAuthorable(doc.uri.scheme)) { + if (isAuthorable(doc.uri.scheme) && coeditingRegistry.isCoediting(doc.uri)) { threadController.renderAll(doc); attributionController.loadAll(doc); proposalController.renderAll(doc); diff --git a/src/proposalController.ts b/src/proposalController.ts index 792b082..36373a5 100644 --- a/src/proposalController.ts +++ b/src/proposalController.ts @@ -19,6 +19,7 @@ import type { AttributionController } from "./attributionController"; import type { VersionGuard } from "./versionGuard"; import { isAuthorable } from "./workspacePath"; import { wordEditHunks, type ProposalView } from "./trackChangesModel"; +import type { CoeditingRegistry } from "./coeditingRegistry"; /** Test-facing snapshot of what is currently rendered for a document. */ export interface RenderedProposal { @@ -56,6 +57,7 @@ export class ProposalController implements vscode.Disposable { private readonly attribution: AttributionController, private readonly rootDir: string | undefined, private readonly guard: VersionGuard, + private readonly registry: CoeditingRegistry, ) { this.disposables.push(this.statusItem); this.disposables.push(this.onDidChangeProposalsEmitter); @@ -459,7 +461,13 @@ export class ProposalController implements vscode.Disposable { // ---- PUC-4: load / external change / resolve-or-flag -------------------------------- - /** Load + (re)render every pending proposal at its resolved anchor (or flagged). */ + /** Load + (re)render every pending proposal at its resolved anchor (or flagged). + * INV-10: the RENDER portion (live/unresolved recompute, the status bar, and + * the onDidChangeProposals fire that drives F12's optimistic-apply/CodeLens/ + * decorations) is gated on coediting — `propose()` itself stays ungated (it + * always persists), and so does the state LOAD below: `acceptById`/`rejectById` + * key off `this.docs` (populated here) regardless of coediting, since deciding + * an already-persisted proposal is not a "rendering" surface. */ renderAll(document: vscode.TextDocument): void { if (!this.isTracked(document)) return; const docPath = this.keyOf(document); @@ -467,6 +475,7 @@ export class ProposalController implements vscode.Disposable { state.artifact = this.store.load(docPath) ?? emptyArtifact(docPath); state.live.clear(); state.unresolved.clear(); + if (!this.registry.isCoediting(document.uri)) return; const text = document.getText(); for (const proposal of state.artifact.proposals) { const fp = state.artifact.anchors[proposal.anchorId]?.fingerprint; diff --git a/src/threadController.ts b/src/threadController.ts index d5b7445..54d3135 100644 --- a/src/threadController.ts +++ b/src/threadController.ts @@ -15,6 +15,7 @@ import { gitUserEmail } from "./identity"; import { addThread, appendMessage, setStatus } from "./threadModel"; import type { VersionGuard } from "./versionGuard"; import { isAuthorable } from "./workspacePath"; +import type { CoeditingRegistry } from "./coeditingRegistry"; /** Test-facing snapshot of what is currently rendered for a document. */ export interface RenderedThread { @@ -41,18 +42,24 @@ export class ThreadController implements vscode.Disposable { private readonly disposables: vscode.Disposable[] = []; private readonly docs = new Map(); // keyed by docPath + /** Kept as ONE object; re-assigned on every registry change so VS Code re-queries + * (spec §6.4 v0.2.1 — reassignment is the API's only "ranges changed" signal). */ + private readonly rangeProvider: vscode.CommentingRangeProvider = { + provideCommentingRanges: (document) => { + if (!isAuthorable(document.uri.scheme)) return []; + if (!this.registry.isCoediting(document.uri)) return []; + return [new vscode.Range(0, 0, Math.max(0, document.lineCount - 1), 0)]; + }, + }; + constructor( private readonly store: SidecarRouter, private readonly rootDir: string | undefined, private readonly guard: VersionGuard, + private readonly registry: CoeditingRegistry, ) { this.controller = vscode.comments.createCommentController("cowriting.threads", "Coauthoring Threads"); - this.controller.commentingRangeProvider = { - provideCommentingRanges: (document) => { - if (!isAuthorable(document.uri.scheme)) return []; - return [new vscode.Range(0, 0, Math.max(0, document.lineCount - 1), 0)]; - }, - }; + this.controller.commentingRangeProvider = this.rangeProvider; this.disposables.push(this.controller); this.disposables.push( @@ -66,6 +73,20 @@ export class ThreadController implements vscode.Disposable { ), vscode.workspace.onDidChangeTextDocument((e) => this.onDidChange(e)), vscode.workspace.onDidSaveTextDocument((d) => this.onDidSave(d)), + // INV-10 (PUC-7): force VS Code to re-query commenting ranges on every gate + // change, and hide/restore rendered threads — exit hides only (dispose the + // rendered vsThreads), re-enter restores (renderAll reloads from the + // sidecar, which is the durable source of truth regardless of gate state). + this.registry.onDidChange(({ uri, coediting }) => { + this.controller.commentingRangeProvider = this.rangeProvider; + const doc = vscode.workspace.textDocuments.find((d) => d.uri.toString() === uri); + if (!doc) return; + if (coediting) { + this.renderAll(doc); + } else { + this.disposeRendered(this.keyOf(doc)); + } + }), ); } @@ -111,6 +132,7 @@ export class ThreadController implements vscode.Disposable { async createThreadOnSelection(firstBody = "New thread"): Promise { const editor = vscode.window.activeTextEditor; if (!editor || editor.selection.isEmpty || !isAuthorable(editor.document.uri.scheme)) return undefined; + if (!this.registry.isCoediting(editor.document.uri)) return undefined; if (this.guard.isReadOnly(this.keyOf(editor.document))) return undefined; const document = editor.document; const state = this.ensureState(document); @@ -152,6 +174,7 @@ export class ThreadController implements vscode.Disposable { /** Load + (re)render every thread for a document at its resolved anchor (or orphaned). */ renderAll(document: vscode.TextDocument): void { + if (!this.registry.isCoediting(document.uri)) return; const docPath = this.keyOf(document); const state = this.ensureState(document); // fresh artifact from disk (reload / external change) @@ -269,6 +292,17 @@ export class ThreadController implements vscode.Disposable { return undefined; } + /** PUC-7 (INV-10): exit hides only — dispose the rendered vsThreads for a doc + * without touching its sidecar-backed artifact (re-enter restores via renderAll). */ + private disposeRendered(docPath: string): void { + const state = this.docs.get(docPath); + if (!state) return; + for (const vsThread of state.vsThreads.values()) vsThread.dispose(); + state.vsThreads.clear(); + state.live.clear(); + state.orphaned.clear(); + } + // ---- test-facing surface -------------------------------------------------------- getRendered(docPath: string): RenderedThread[] { diff --git a/src/trackChangesPreview.ts b/src/trackChangesPreview.ts index a6c0c8e..8c2b951 100644 --- a/src/trackChangesPreview.ts +++ b/src/trackChangesPreview.ts @@ -19,6 +19,10 @@ import { isAuthorable } from "./workspacePath"; import type { EditTurnResult, RunEditTurnOptions } from "./liveTurn"; import type { LiveProgressUi } from "./liveProgressUi"; import { promptEditInstruction } from "./editInstructionInput"; +import type { CoeditingRegistry } from "./coeditingRegistry"; + +/** The exact warning copy for every gated edit gesture (INV-10). */ +const NOT_COEDITING_WARNING = "Run ✦ Coedit this Document with Claude first."; /** * F11: a host edit turn (selection/document text + instruction → rewrite). @@ -83,6 +87,7 @@ export class TrackChangesPreviewController implements vscode.Disposable { private readonly attribution: AttributionController, private readonly proposals: ProposalController, private readonly liveProgressUi: LiveProgressUi, + private readonly registry: CoeditingRegistry, ) { this.disposables.push( // F11 (SLICE-5): the editor/title gateway passes the tab's resource Uri; @@ -247,6 +252,13 @@ export class TrackChangesPreviewController implements vscode.Disposable { * the result as F4 proposal(s). UI wrapper around `runEditAndPropose`. */ private async askClaude(document: vscode.TextDocument, target: EditTarget): Promise { + // INV-10: the choke point for BOTH the editDocument command and the webview's + // askClaude toolbar intent (either scope) — a non-entered doc gets the warning, + // not a turn. + if (!this.registry.isCoediting(document.uri)) { + void vscode.window.showWarningMessage(NOT_COEDITING_WARNING); + return; + } // Both scopes use the same multi-line split-below webview box; only the header // (and the downstream proposal logic) differs. For a selection the document // above keeps the selection highlighted while the box is open. diff --git a/test/e2e/suite/attribution.test.ts b/test/e2e/suite/attribution.test.ts index a1b1010..4dc334a 100644 --- a/test/e2e/suite/attribution.test.ts +++ b/test/e2e/suite/attribution.test.ts @@ -18,6 +18,9 @@ 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); + // INV-10: attribution tracking is gated on coediting (Task 4) — idempotent + // across this order-dependent suite's repeated openDoc() calls. + await vscode.commands.executeCommand("cowriting.coeditDocument"); return doc; } async function getApi(): Promise { diff --git a/test/e2e/suite/authorship.test.ts b/test/e2e/suite/authorship.test.ts index 64d2ec9..300220e 100644 --- a/test/e2e/suite/authorship.test.ts +++ b/test/e2e/suite/authorship.test.ts @@ -34,6 +34,9 @@ suite("F10 review preview — pending Claude proposal renders cw-ins-claude in t const uri = vscode.Uri.file(abs); const doc = await vscode.workspace.openTextDocument(uri); await vscode.window.showTextDocument(doc); + // INV-10: entering coediting is required for attribution tracking + F12 + // optimistic-apply to fire — this suite never entered it (pre-migration). + await vscode.commands.executeCommand("cowriting.coeditDocument"); await settle(); const api = await getApi(); const key = uri.toString(); diff --git a/test/e2e/suite/coediting.test.ts b/test/e2e/suite/coediting.test.ts index ff99efe..3c2fe0d 100644 --- a/test/e2e/suite/coediting.test.ts +++ b/test/e2e/suite/coediting.test.ts @@ -32,4 +32,27 @@ suite("coediting registry (PUC-7)", () => { assert.strictEqual(api.coeditingRegistry.isCoediting(doc.uri), false); assert.strictEqual(api.scmSurfaceController.changeCount(doc.uri.toString()), 0); }); + + // Task 4 (INV-10, PUC-7): the no-hijack scenario — a non-entered doc gets no + // thread-creation surface at all; entering unlocks it; exiting hides the + // rendered thread (not the sidecar-backed data); re-entering restores it. + test("non-entered doc gets no surfaces; exit detaches; re-enter restores threads", async () => { + const api = await activateApi(); + const doc = await vscode.workspace.openTextDocument({ language: "markdown", content: "# a\n\npara\n" }); + const ed = await vscode.window.showTextDocument(doc); + // not entered → no thread creation + ed.selection = new vscode.Selection(2, 0, 2, 4); + const before = await api.threadController.createThreadOnSelection("hi"); + assert.strictEqual(before, undefined); + // entered → works + await vscode.commands.executeCommand("cowriting.coeditDocument"); + const id = await api.threadController.createThreadOnSelection("hi"); + assert.ok(id); + // exit → rendered thread set empty; re-enter → restored from sidecar + await vscode.commands.executeCommand("cowriting.stopCoediting"); + assert.strictEqual(api.threadController.getRendered(api.proposalController.keyFor(doc)).length, 0); + await vscode.commands.executeCommand("cowriting.coeditDocument"); + await settle(); + assert.strictEqual(api.threadController.getRendered(api.proposalController.keyFor(doc)).length, 1); + }); }); diff --git a/test/e2e/suite/crossrung.test.ts b/test/e2e/suite/crossrung.test.ts index aa188e9..9ac7f1c 100644 --- a/test/e2e/suite/crossrung.test.ts +++ b/test/e2e/suite/crossrung.test.ts @@ -17,6 +17,8 @@ async function open(docRel: string): Promise { const uri = vscode.Uri.file(path.join(WS, docRel)); const doc = await vscode.workspace.openTextDocument(uri); await vscode.window.showTextDocument(doc); + // INV-10: thread creation/rendering is gated on coediting (Task 4). + await vscode.commands.executeCommand("cowriting.coeditDocument"); return doc; } async function getApi(): Promise { diff --git a/test/e2e/suite/f12Accept.test.ts b/test/e2e/suite/f12Accept.test.ts index 16a38f0..ae7d769 100644 --- a/test/e2e/suite/f12Accept.test.ts +++ b/test/e2e/suite/f12Accept.test.ts @@ -21,6 +21,9 @@ async function freshDoc(rel: string, body: string): Promise<{ doc: vscode.TextDo const uri = vscode.Uri.file(abs); const doc = await vscode.workspace.openTextDocument(uri); await vscode.window.showTextDocument(doc); + // INV-10: proposal rendering (F12 optimistic-apply, driven by + // onDidChangeProposals) is gated on coediting (Task 4). + await vscode.commands.executeCommand("cowriting.coeditDocument"); await settle(); return { doc, key: uri.toString() }; } diff --git a/test/e2e/suite/f12Reach.test.ts b/test/e2e/suite/f12Reach.test.ts index e559073..6aaf940 100644 --- a/test/e2e/suite/f12Reach.test.ts +++ b/test/e2e/suite/f12Reach.test.ts @@ -20,6 +20,12 @@ async function freshDoc(rel: string, body: string): Promise<{ doc: vscode.TextDo fs.writeFileSync(abs, body, "utf8"); const uri = vscode.Uri.file(abs); const doc = await vscode.workspace.openTextDocument(uri); + // INV-10: cowriting.editDocument now warns instead of editing a non-entered + // doc (Task 4) — enter it here (briefly making it active) so the tests below + // exercise the routing/targeting behavior, not the gate. The caller + // re-establishes whichever doc it wants active afterward. + await vscode.window.showTextDocument(doc); + await vscode.commands.executeCommand("cowriting.coeditDocument"); return { doc, key: uri.toString() }; } diff --git a/test/e2e/suite/f12Review.test.ts b/test/e2e/suite/f12Review.test.ts index 867cd9f..7a7be35 100644 --- a/test/e2e/suite/f12Review.test.ts +++ b/test/e2e/suite/f12Review.test.ts @@ -20,6 +20,9 @@ async function freshDoc(rel: string, body: string): Promise fs.writeFileSync(abs, body, "utf8"); const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(abs)); await vscode.window.showTextDocument(doc); + // INV-10: attribution tracking (accept's per-word attribution, INV-40) is + // gated on coediting (Task 4). + await vscode.commands.executeCommand("cowriting.coeditDocument"); await settle(); return doc; } diff --git a/test/e2e/suite/liveProgress.test.ts b/test/e2e/suite/liveProgress.test.ts index 1ca5c7c..39dabaf 100644 --- a/test/e2e/suite/liveProgress.test.ts +++ b/test/e2e/suite/liveProgress.test.ts @@ -20,6 +20,9 @@ async function freshDoc(rel: string, body: string): Promise fs.writeFileSync(abs, body, "utf8"); const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(abs)); await vscode.window.showTextDocument(doc); + // INV-10: proposal rendering (F12 optimistic-apply, driven by + // onDidChangeProposals) is gated on coediting (Task 4). + await vscode.commands.executeCommand("cowriting.coeditDocument"); await settle(); return doc; } diff --git a/test/e2e/suite/outOfWorkspace.test.ts b/test/e2e/suite/outOfWorkspace.test.ts index f75de0c..adabf77 100644 --- a/test/e2e/suite/outOfWorkspace.test.ts +++ b/test/e2e/suite/outOfWorkspace.test.ts @@ -37,6 +37,8 @@ suite("F8 out-of-workspace authoring (host E2E — programmatic seam, no LLM)", const uri = vscode.Uri.file(outsidePath); const doc = await vscode.workspace.openTextDocument(uri); await vscode.window.showTextDocument(doc); + // INV-10: attribution tracking + thread creation are gated on coediting (Task 4). + await vscode.commands.executeCommand("cowriting.coeditDocument"); await settle(); const api = await getApi(); const key = uri.toString(); @@ -80,6 +82,8 @@ suite("F8 out-of-workspace authoring (host E2E — programmatic seam, no LLM)", const TARGET = "The untitled draft sentence."; const untitled = await vscode.workspace.openTextDocument({ content: `${TARGET}\n`, language: "markdown" }); await vscode.window.showTextDocument(untitled); + // INV-10: attribution tracking is gated on coediting (Task 4). + await vscode.commands.executeCommand("cowriting.coeditDocument"); await settle(); const api = await getApi(); const key = untitled.uri.toString(); diff --git a/test/e2e/suite/proposals.test.ts b/test/e2e/suite/proposals.test.ts index fc6721c..4684b6a 100644 --- a/test/e2e/suite/proposals.test.ts +++ b/test/e2e/suite/proposals.test.ts @@ -18,6 +18,10 @@ 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); + // INV-10: proposal rendering (and the F12 optimistic-apply it drives via + // onDidChangeProposals) is gated on coediting (Task 4) — idempotent across + // this order-dependent suite's repeated openDoc() calls. + await vscode.commands.executeCommand("cowriting.coeditDocument"); return doc; } async function getApi(): Promise { diff --git a/test/e2e/suite/threads.test.ts b/test/e2e/suite/threads.test.ts index 5b43dfe..347c1f8 100644 --- a/test/e2e/suite/threads.test.ts +++ b/test/e2e/suite/threads.test.ts @@ -18,6 +18,9 @@ async function openSample(): Promise { const uri = vscode.Uri.file(path.join(WS, DOC_REL)); const doc = await vscode.workspace.openTextDocument(uri); await vscode.window.showTextDocument(doc); + // INV-10: thread creation/rendering is gated on coediting (Task 4) — idempotent + // across this order-dependent suite's repeated openSample() calls. + await vscode.commands.executeCommand("cowriting.coeditDocument"); return doc; }