feat: crossrung-reply stand-in writer — validate, append, stable-serialize (F5 SLICE-4, #14)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { emptyArtifact, serializeArtifact, type Artifact } from "../src/model";
|
||||
import { validateSidecar } from "./helpers/validateSidecar";
|
||||
|
||||
const SCRIPT = path.resolve(__dirname, "../scripts/crossrung-reply.mjs");
|
||||
let dir: string;
|
||||
let sidecar: string;
|
||||
|
||||
beforeEach(() => {
|
||||
dir = fs.mkdtempSync(path.join(os.tmpdir(), "cowriting-crossrung-"));
|
||||
sidecar = path.join(dir, "sample.md.json");
|
||||
const a = emptyArtifact("docs/sample.md");
|
||||
a.anchors["a_1"] = { fingerprint: { text: "alpha", before: "", after: "", lineHint: 0 } };
|
||||
a.threads.push({
|
||||
id: "t_1",
|
||||
anchorId: "a_1",
|
||||
status: "open",
|
||||
messages: [{ id: "m_1", author: { kind: "human", id: "ben" }, body: "hi", createdAt: "2026-06-10T00:00:00.000Z" }],
|
||||
});
|
||||
// INV-15 must hold through a FOREIGN writer too
|
||||
(a as unknown as Record<string, unknown>).futureSection = [{ id: "f_1" }];
|
||||
fs.writeFileSync(sidecar, serializeArtifact(a), "utf8");
|
||||
});
|
||||
afterEach(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
|
||||
function run(...args: string[]): string {
|
||||
return execFileSync(process.execPath, [SCRIPT, ...args], { encoding: "utf8" });
|
||||
}
|
||||
|
||||
describe("crossrung-reply stand-in (F5 SLICE-4, PUC-2)", () => {
|
||||
it("appends a conforming reply, preserves unknowns, byte-identical to the reference serializer", () => {
|
||||
run(sidecar, "t_1", "Reply from the forge", "--author-id", "gitea", "--author-email", "forge@example.org");
|
||||
const raw = fs.readFileSync(sidecar, "utf8");
|
||||
const parsed = JSON.parse(raw) as Artifact;
|
||||
expect(parsed.threads[0].messages).toHaveLength(2);
|
||||
const reply = parsed.threads[0].messages[1];
|
||||
expect(reply.body).toBe("Reply from the forge");
|
||||
expect(reply.author).toEqual({ kind: "human", id: "gitea", email: "forge@example.org" });
|
||||
expect(reply.id).toMatch(/^m_/);
|
||||
expect((parsed as unknown as Record<string, unknown>).futureSection).toEqual([{ id: "f_1" }]);
|
||||
expect(validateSidecar(parsed).valid).toBe(true);
|
||||
// the stand-in's independent serialization is byte-identical to the
|
||||
// editor's (INV-2 across writers) — the contract-drift tripwire.
|
||||
expect(serializeArtifact(parsed)).toBe(raw);
|
||||
});
|
||||
|
||||
it("refuses an unknown thread id and leaves the file untouched", () => {
|
||||
const before = fs.readFileSync(sidecar, "utf8");
|
||||
expect(() => run(sidecar, "t_nope", "x")).toThrow();
|
||||
expect(fs.readFileSync(sidecar, "utf8")).toBe(before);
|
||||
});
|
||||
|
||||
it("refuses an invalid sidecar (validates BEFORE writing)", () => {
|
||||
fs.writeFileSync(sidecar, '{"schemaVersion":1}\n', "utf8");
|
||||
expect(() => run(sidecar, "t_1", "x")).toThrow();
|
||||
});
|
||||
|
||||
it("refuses a newer-major sidecar (INV-16 holds for foreign writers too)", () => {
|
||||
const v2 = JSON.parse(fs.readFileSync(sidecar, "utf8"));
|
||||
v2.schemaVersion = 2;
|
||||
fs.writeFileSync(sidecar, JSON.stringify(v2, null, 2) + "\n", "utf8");
|
||||
const before = fs.readFileSync(sidecar, "utf8");
|
||||
expect(() => run(sidecar, "t_1", "x")).toThrow();
|
||||
expect(fs.readFileSync(sidecar, "utf8")).toBe(before);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user