Files
backnotprop__plannotator/apps/pi-extension/server/external-annotations.test.ts
Michael Ramos c2950e709f fix: pre-release QA findings for 0.27.9 (#1405)
Fixes from the 0.27.9 pre-release review. Servers: an unreadable rendered-HTML root falls back to the startup snapshot on both runtimes with a once-per-process warning instead of hanging (Pi) or answering 500 (Bun); the version diff is recomputed against current bytes on reload and carried through the in-app Refresh instead of being dropped, with no history write on a GET. Client: a Refresh action on the compact touch shell; HtmlSurfaceControls renders Refresh independently of the eye; the dead HtmlSurfaceActions removed. Threading: one linear, cycle-safe reply resolution shared by the annotations panel, its sort, and the export (5,000-chain tests), PATCH ingest on both runtimes rejects self-references and cycles, nothing is ever dropped from feedback. WebMCP and viewer hygiene: bounded tombstone and request memories, per-instance minted ids, nudge id caps, waiter cleanup on unmount, a shared retry epoch for diagram blocks. Docs: HTML Refresh documented, the WebMCP design pointer fixed, marketing pages updated.

AI-assisted (Claude) under maintainer direction.
2026-08-27 15:23:28 -07:00

72 lines
2.5 KiB
TypeScript

/**
* External annotations (Pi/Node): PATCH ingest of `inReplyTo`.
*
* Node mirror of the PATCH describe in packages/server/external-annotations.test.ts:
* PATCH merges arbitrary fields, so it was the one way to create an inReplyTo
* self-reference or cycle; the invalid state is refused at ingest on both
* runtimes.
*/
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { createServer, type Server } from "node:http";
import { createExternalAnnotationHandler } from "./external-annotations.ts";
import { requestUrl } from "./helpers.ts";
describe("pi external annotations: PATCH inReplyTo", () => {
const handler = createExternalAnnotationHandler("plan");
let server: Server;
let base = "";
beforeAll(async () => {
server = createServer(async (req, res) => {
const handled = await handler.handle(req, res, requestUrl(req));
if (!handled) {
res.writeHead(404);
res.end();
}
});
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address();
if (!address || typeof address === "string") throw new Error("no port");
base = `http://127.0.0.1:${address.port}`;
});
afterAll(() => {
server.close();
});
const patch = async (id: string, body: unknown) => {
const res = await fetch(`${base}/api/external-annotations?id=${encodeURIComponent(id)}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
return { status: res.status, body: (await res.json()) as { error?: string; annotation?: { inReplyTo?: string } } };
};
test("refuses an inReplyTo that is self, missing, or would close a cycle; accepts a valid reply", async () => {
const added = handler.addAnnotations({
annotations: [
{ source: "tool", text: "first" },
{ source: "tool", text: "second" },
],
});
if ("error" in added) throw new Error(added.error);
const [first, second] = added.ids;
expect((await patch(first, { inReplyTo: first })).status).toBe(400);
expect((await patch(first, { inReplyTo: "nope" })).status).toBe(400);
expect((await patch(first, { inReplyTo: 7 })).status).toBe(400);
const ok = await patch(second, { inReplyTo: first });
expect(ok.status).toBe(200);
expect(ok.body.annotation?.inReplyTo).toBe(first);
const cycle = await patch(first, { inReplyTo: second });
expect(cycle.status).toBe(400);
expect(cycle.body.error).toContain("cycle");
expect((await patch(second, { inReplyTo: null })).status).toBe(200);
expect((await patch(second, { text: "still fine" })).status).toBe(200);
});
});