test: host E2E — crossrung round-trip + newer-major fail-safe (F5 SLICE-4, PUC-2/PUC-4, #14)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import * as assert from "assert";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { execFileSync } from "child_process";
|
||||
import * as vscode from "vscode";
|
||||
import type { CowritingApi } from "../../../src/extension";
|
||||
import type { Artifact } from "../../../src/model";
|
||||
|
||||
const WS = process.env.E2E_WORKSPACE!;
|
||||
const PROJECT_ROOT = path.resolve(__dirname, "../../../..");
|
||||
const STANDIN = path.join(PROJECT_ROOT, "scripts", "crossrung-reply.mjs");
|
||||
|
||||
function sidecarPath(docRel: string): string {
|
||||
return path.join(WS, ".threads", `${docRel}.json`);
|
||||
}
|
||||
async function open(docRel: string): Promise<vscode.TextDocument> {
|
||||
const uri = vscode.Uri.file(path.join(WS, docRel));
|
||||
const doc = await vscode.workspace.openTextDocument(uri);
|
||||
await vscode.window.showTextDocument(doc);
|
||||
return doc;
|
||||
}
|
||||
async function getApi(): Promise<CowritingApi> {
|
||||
const ext = vscode.extensions.getExtension("benstull.vscode-cowriting-plugin")!;
|
||||
const api = (await ext.activate()) as CowritingApi;
|
||||
assert.ok(api?.versionGuard, "extension exports versionGuard (F5)");
|
||||
return api;
|
||||
}
|
||||
// The extension host is Electron: run the stand-in with the same binary in
|
||||
// plain-node mode — a forge's write is just a local process to git (fork e).
|
||||
function runStandin(...args: string[]): void {
|
||||
execFileSync(process.execPath, [STANDIN, ...args], {
|
||||
encoding: "utf8",
|
||||
env: { ...process.env, ELECTRON_RUN_AS_NODE: "1" },
|
||||
});
|
||||
}
|
||||
function commentsOf(api: CowritingApi, docRel: string, threadId: string): number {
|
||||
const docs = (
|
||||
api.threadController as unknown as {
|
||||
docs: Map<string, { vsThreads: Map<string, vscode.CommentThread> }>;
|
||||
}
|
||||
).docs;
|
||||
const vsThread = docs.get(docRel)?.vsThreads.get(threadId);
|
||||
return vsThread ? vsThread.comments.length : -1;
|
||||
}
|
||||
async function until(cond: () => boolean, ms = 5000): Promise<void> {
|
||||
const t0 = Date.now();
|
||||
while (!cond()) {
|
||||
if (Date.now() - t0 > ms) throw new Error("timed out waiting for condition");
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
}
|
||||
}
|
||||
|
||||
suite("F5 cross-rung round-trip (host E2E)", () => {
|
||||
test("stand-in reply lands in the editor's thread via external change (PUC-2)", async () => {
|
||||
const DOC = "docs/crossrung.md";
|
||||
const doc = await open(DOC);
|
||||
const api = await getApi();
|
||||
const target = "portable record";
|
||||
const start = doc.getText().indexOf(target);
|
||||
const editor = vscode.window.activeTextEditor!;
|
||||
editor.selection = new vscode.Selection(doc.positionAt(start), doc.positionAt(start + target.length));
|
||||
const threadId = await api.threadController.createThreadOnSelection("Editor note");
|
||||
assert.ok(threadId, "thread created in the editor");
|
||||
|
||||
// the Gitea-rung stand-in appends a reply OUT OF EDITOR (spec fork e)
|
||||
runStandin(sidecarPath(DOC), threadId!, "Reply from the forge", "--author-id", "gitea", "--author-email", "forge@example.org");
|
||||
|
||||
// watcher → external change → re-render with the new message (the F2 path)
|
||||
await until(() => commentsOf(api, DOC, threadId!) === 2);
|
||||
const rendered = api.threadController.getRendered(DOC);
|
||||
const mine = rendered.find((r) => r.id === threadId)!;
|
||||
assert.strictEqual(mine.orphaned, false, "anchor still resolves after the foreign write");
|
||||
const anchorLine = doc.positionAt(doc.getText().indexOf(target)).line;
|
||||
assert.strictEqual(mine.range.startLine, anchorLine, "rendered at the re-resolved anchor");
|
||||
|
||||
const art = JSON.parse(fs.readFileSync(sidecarPath(DOC), "utf8")) as Artifact;
|
||||
const msgs = art.threads.find((t) => t.id === threadId)!.messages;
|
||||
assert.strictEqual(msgs.length, 2);
|
||||
assert.deepStrictEqual(msgs[1].author, { kind: "human", id: "gitea", email: "forge@example.org" });
|
||||
});
|
||||
|
||||
test("newer-major sidecar: warning + read-only, file bytes never change (PUC-4)", async () => {
|
||||
const DOC = "docs/v2.md";
|
||||
const scPath = sidecarPath(DOC);
|
||||
fs.mkdirSync(path.dirname(scPath), { recursive: true });
|
||||
// a v2 sidecar: valid v1 sections plus future content this build can't know
|
||||
const v2 = {
|
||||
schemaVersion: 2,
|
||||
document: { path: DOC },
|
||||
anchors: {
|
||||
a_1: { fingerprint: { text: "Anchored text", before: "", after: " from the future.", lineHint: 2 } },
|
||||
},
|
||||
threads: [
|
||||
{
|
||||
id: "t_1",
|
||||
anchorId: "a_1",
|
||||
status: "open",
|
||||
messages: [{ id: "m_1", author: { kind: "human", id: "future" }, body: "from v2", createdAt: "2026-06-10T00:00:00.000Z" }],
|
||||
},
|
||||
],
|
||||
attributions: [],
|
||||
proposals: [],
|
||||
v2OnlySection: { shiny: true },
|
||||
};
|
||||
fs.writeFileSync(scPath, JSON.stringify(v2, null, 2) + "\n", "utf8");
|
||||
const before = fs.readFileSync(scPath, "utf8");
|
||||
|
||||
const doc = await open(DOC);
|
||||
const api = await getApi();
|
||||
api.threadController.renderAll(doc); // render best-effort: the v1 subset shows
|
||||
const rendered = api.threadController.getRendered(DOC);
|
||||
assert.strictEqual(rendered.length, 1, "renders what it understands");
|
||||
|
||||
// write gestures are refused + warned (PUC-4)
|
||||
const target = "Anchored text";
|
||||
const start = doc.getText().indexOf(target);
|
||||
const editor = vscode.window.activeTextEditor!;
|
||||
editor.selection = new vscode.Selection(doc.positionAt(start), doc.positionAt(start + target.length));
|
||||
const created = await api.threadController.createThreadOnSelection("should not land");
|
||||
assert.strictEqual(created, undefined, "createThread refused on newer major");
|
||||
assert.strictEqual(api.versionGuard.wasWarned(DOC), true, "user warned once (PUC-4)");
|
||||
|
||||
// an edit + save must NOT rewrite the sidecar (INV-16)
|
||||
await editor.edit((b) => b.insert(doc.positionAt(0), "edited "));
|
||||
await doc.save();
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
assert.strictEqual(fs.readFileSync(scPath, "utf8"), before, "sidecar bytes untouched (INV-16)");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user