mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
c2950e709f
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.
72 lines
2.5 KiB
TypeScript
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);
|
|
});
|
|
});
|