feat: CoeditingRegistry opt-in gate (INV-10, spec §6.4) — enter/stop commands + context keys

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
BenStullsBets
2026-07-02 06:40:15 -07:00
parent ef61b141b6
commit 2597cba229
5 changed files with 189 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
import { describe, expect, it, vi } from "vitest";
// The pure set logic lives in a vscode-free helper the class wraps, so unit-test the class
// with a Memento stub (vitest runs vscode-free; the module imports vscode only for types +
// EventEmitter — mock it).
vi.mock("vscode", () => ({
EventEmitter: class {
private handlers: Array<(e: unknown) => void> = [];
event = (h: (e: unknown) => void) => { this.handlers.push(h); return { dispose() {} }; };
fire(e: unknown) { for (const h of this.handlers) h(e); }
dispose() {}
},
Uri: { parse: (s: string) => ({ toString: () => s }) },
commands: { executeCommand: vi.fn() },
window: { activeTextEditor: undefined },
}));
import { CoeditingRegistry } from "../src/coeditingRegistry";
function memento(): { store: Map<string, unknown> } & { get: any; update: any } {
const store = new Map<string, unknown>();
return {
store,
get: (k: string, dflt?: unknown) => (store.has(k) ? store.get(k) : dflt),
update: (k: string, v: unknown) => { store.set(k, v); return Promise.resolve(); },
} as any;
}
describe("CoeditingRegistry", () => {
it("enter/exit flips membership and fires onDidChange", () => {
const reg = new CoeditingRegistry(memento() as any);
const events: Array<{ uri: string; coediting: boolean }> = [];
reg.onDidChange((e) => events.push(e as any));
const uri = { toString: () => "file:///a.md" } as any;
expect(reg.isCoediting(uri)).toBe(false);
reg.enter(uri);
expect(reg.isCoediting(uri)).toBe(true);
reg.exit(uri);
expect(reg.isCoediting(uri)).toBe(false);
expect(events).toEqual([
{ uri: "file:///a.md", coediting: true },
{ uri: "file:///a.md", coediting: false },
]);
});
it("persists the set across construction (reload survival)", () => {
const m = memento();
const reg1 = new CoeditingRegistry(m as any);
reg1.enter({ toString: () => "file:///a.md" } as any);
const reg2 = new CoeditingRegistry(m as any);
expect(reg2.isCoediting({ toString: () => "file:///a.md" } as any)).toBe(true);
expect(reg2.list()).toEqual(["file:///a.md"]);
});
it("enter is idempotent (no duplicate events)", () => {
const reg = new CoeditingRegistry(memento() as any);
const events: unknown[] = [];
reg.onDidChange((e) => events.push(e));
const uri = { toString: () => "file:///a.md" } as any;
reg.enter(uri);
reg.enter(uri);
expect(events).toHaveLength(1);
});
});
+20
View File
@@ -0,0 +1,20 @@
import * as assert from "node:assert";
import * as vscode from "vscode";
async function api() {
const ext = vscode.extensions.getExtension("benstull.vscode-cowriting-plugin")!;
return (await ext.activate()) as import("../../../src/extension").CowritingApi;
}
suite("coediting registry (PUC-7)", () => {
test("enter → isCoediting true; exit → false; persists in list()", async () => {
const { coeditingRegistry } = await api();
const doc = await vscode.workspace.openTextDocument({ language: "markdown", content: "# t\n\nbody\n" });
await vscode.window.showTextDocument(doc);
await vscode.commands.executeCommand("cowriting.coeditDocument");
assert.strictEqual(coeditingRegistry.isCoediting(doc.uri), true);
assert.ok(coeditingRegistry.list().includes(doc.uri.toString()));
await vscode.commands.executeCommand("cowriting.stopCoediting");
assert.strictEqual(coeditingRegistry.isCoediting(doc.uri), false);
});
});