import assert from "node:assert/strict"; import test from "node:test"; import { referenceFromWire } from "../dist/references.js"; import { bindQxHandler, decodeQxValue, encodeQxValue, jsToProtoValue, liveValue, protoValueToJs, } from "../dist/index.js"; const scalar = (name) => ({ kind: "scalar", name }); const unit = { kind: "builtin", name: "unit" }; test("free function handlers never acquire a receiver or object session", async () => { const handler = bindQxHandler( { receiver: "none", inputType: unit, outputType: unit, ports: {} }, async (context) => { assert.equal("objectId" in context, false); assert.equal("openSession" in context, false); }, {}, ); await handler({ inputProto: {}, get objectId() { throw new Error("no receiver exists"); }, openSession() { assert.fail("no object session"); }, }); }); test("React props no longer have an opaque-message reference escape hatch", () => { const descriptorId = "org.quixos.web-studio.ReactProps"; const reference = referenceFromWire("obj:task"); const type = { kind: "message", descriptorId }; const messages = { [descriptorId]: { encode: jsToProtoValue, decode: protoValueToJs } }; assert.throws(() => encodeQxValue(type, { subject: reference }, messages), /Managed references/); assert.throws(() => decodeQxValue(type, jsToProtoValue({ subject: reference }), messages), /Managed references/); }); test("declared record inputs preserve opaque references without opening message codecs", async () => { const target = referenceFromWire("obj:board"); const type = { kind: "record", fields: { target: { kind: "object-ref", expectation: { kind: "atom", atomId: "board" } }, x: scalar("double"), note: { kind: "optional", value: scalar("string") }, }, }; const encoded = encodeQxValue(type, { target, x: 12 }, {}); const decoded = decodeQxValue(type, encoded, {}); assert.ok(decoded.target.equals(target)); assert.equal(decoded.x, 12); assert.equal(decoded.note, null); assert.throws(() => JSON.stringify(decoded), /cannot be serialized/); assert.throws(() => encodeQxValue(type, { target, x: 12, hidden: target }, {}), /Unexpected/); assert.throws(() => encodeQxValue(type, { x: 12 }, {}), /Missing/); assert.throws(() => encodeQxValue(type, { target: "obj:board", x: 12 }, {}), /opaque object reference/); const handler = bindQxHandler( { inputType: type, outputType: unit, ports: {} }, async (context) => { assert.ok(context.input.target.equals(target)); assert.equal(context.input.x, 12); }, {}, ); await handler({ objectId: target, inputProto: encoded.kind.value.fields }); }); test("typed sessions rebind ports and cancellation to each acquired invocation", async () => { const first = new AbortController(), second = new AbortController(); let closed = false; const context = (value, signal) => ({ objectId: referenceFromWire("obj:owner"), inputProto: {}, signal, state: () => ({ live: async () => liveValue(jsToProtoValue(value)) }), }); const raw = { ...context(1n, first.signal), openSession: async () => ({ id: "session:test", run: (work) => work(context(2n, second.signal)), close: async () => { closed = true; }, }), }; const handler = bindQxHandler( { inputType: unit, outputType: unit, ports: { counter: { kind: "state", id: "counter", valueType: scalar("int64"), primitives: ["read"] } }, }, async (bound) => { assert.equal(bound.signal, first.signal); const session = await bound.openSession(); await session.run(async (next) => { assert.equal(next.signal, second.signal); assert.equal(await next.ports.counter.get(), 2n); assert.equal(next.openSession, undefined); }); await session.close(); }, {}, ); await handler(raw); assert.equal(closed, true); }); test("binding codecs round trip nested bytes, 64-bit integers, nulls, and references", () => { const values = [ [scalar("int64"), -(2n ** 63n)], [scalar("uint64"), 2n ** 64n - 1n], [scalar("bytes"), new Uint8Array([0, 255])], [{ kind: "list", value: { kind: "optional", value: scalar("int64") } }, [null, 2n ** 60n]], [{ kind: "object-ref", expectation: { kind: "atom", atomId: "thing" } }, referenceFromWire("obj:thing")], ]; for (const [type, value] of values) assert.deepEqual(decodeQxValue(type, encodeQxValue(type, value, {}), {}), value); }); test("opaque references pass declared RPC boundaries but cannot enter ordinary data", () => { const reference = referenceFromWire("obj:thing"); const type = { kind: "object-ref", expectation: { kind: "atom", atomId: "thing" } }; const roundtrip = decodeQxValue(type, encodeQxValue(type, reference, {}), {}); assert.equal(reference.equals(roundtrip), true); assert.equal(reference.equals(referenceFromWire("obj:other")), false); assert.throws(() => JSON.stringify({ nested: [reference] }), /cannot be serialized/); assert.throws(() => String(reference), /cannot be coerced/); assert.throws(() => encodeQxValue(type, "obj:thing", {}), /opaque/); assert.throws(() => encodeQxValue(scalar("string"), reference, {}), /Managed references/); const message = { kind: "message", descriptorId: "Payload" }; assert.throws( () => encodeQxValue(message, { nested: reference }, { Payload: { encode: jsToProtoValue } }), /Managed references/, ); assert.throws( () => encodeQxValue(message, {}, { Payload: { encode: () => jsToProtoValue(reference) } }), /Managed references/, ); assert.throws( () => decodeQxValue(message, jsToProtoValue(reference), { Payload: { decode: () => ({}) } }), /Managed references/, ); }); test("typed state and interface ports preserve declared values and exact operation IDs", async () => { const spec = { inputType: scalar("int64"), outputType: scalar("bytes"), ports: { data: { kind: "state", id: "state-id", valueType: scalar("int64"), primitives: ["read", "write"] }, reader: { kind: "interface", id: "interface-id", operations: { "payload.get": { id: "get-id", inputType: unit, outputType: scalar("bytes") } }, }, }, }; let written; const handler = bindQxHandler( spec, async ({ input, ports }) => { assert.equal(input, 2n ** 60n); assert.equal(await ports.data.get(), 9n); assert.deepEqual((await ports.data.live()).$quixosValue, jsToProtoValue(9n)); assert.ok(ports.reader.objectId.equals(referenceFromWire("obj:reader"))); assert.deepEqual((await ports.reader.live["payload.get"]()).$quixosValue, jsToProtoValue(new Uint8Array([7]))); await ports.data.set(input); return ports.reader["payload.get"](); }, {}, ); const result = await handler({ inputProto: { value: jsToProtoValue(2n ** 60n) }, objectId: "obj", state: (id) => { assert.equal(id, "state-id"); return { live: async () => liveValue(jsToProtoValue(9n)), set: async (value) => { written = value; }, }; }, interface: (id) => { assert.equal(id, "interface-id"); return { objectId: referenceFromWire("obj:reader"), live: async (operation, input) => { assert.equal(operation, "get-id"); assert.deepEqual(input, {}); return liveValue(jsToProtoValue(new Uint8Array([7]))); }, }; }, }); assert.equal(written.$quixosValue.kind.value, String(2n ** 60n)); assert.deepEqual(result.$quixosValue.kind.value, new Uint8Array([7])); }); test("external message bindings and derived event types are used at the boundary", async () => { const message = { kind: "message", descriptorId: "Payload" }; const messages = { Payload: { encode: jsToProtoValue, decode: protoValueToJs } }; const handler = bindQxHandler( { inputType: message, outputType: { kind: "builtin", name: "watch-handle" }, eventType: message, ports: {} }, { kind: "derived", get: ({ input }) => ({ value: input.title }) }, messages, ); assert.equal(handler.kind, "derived"); const result = await handler.get({ objectId: "obj", inputProto: { title: jsToProtoValue("hello") } }); assert.deepEqual(protoValueToJs(result.$quixosValue), { value: "hello" }); assert.throws(() => encodeQxValue(message, {}, {}), /Missing message binding/); });