import assert from "node:assert/strict"; import test from "node:test"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; import { compileCapabilityResourceSource, compileCapabilitySource } from "../src/capability-language/parser.js"; import { generateReactBindings } from "../src/bindings/react.js"; import { reactPlatformTypes } from "../src/bindings/react-platform.js"; const source = { repository: "https://example.test/fields.git", commit: "a".repeat(40) }; test("React bindings preserve read-only, writable and nested reference contracts", async (t) => { const iface = compileCapabilityResourceSource( `interface Fields id "fields" revision "fields@1" { value title id "title" : string { get id "title:get"; set id "title:set"; watch start id "watch" stop id "stop"; } value summary id "summary" : string { get id "summary:get"; } }`, { source }, ); assert.ok(iface.ok && iface.resource.kind === "interface"); if (!iface.ok || iface.resource.kind !== "interface") throw new Error("interface failed"); const pkg = compileCapabilityResourceSource( `import interface Fields; package P id "p" revision "p@1" { function props id "props" : unit -> record {fields: interface-ref; caption: string;}; }`, { source, environment: { interfaces: new Map([["Fields", iface.resource.revision]]) } }, ); assert.ok(pkg.ok && pkg.resource.kind === "package"); if (!pkg.ok || pkg.resource.kind !== "package") throw new Error("package failed"); const schema = { format: "quixos-bindings", version: 1, interfaces: [iface.resource.revision], packages: [pkg.resource.revision], } as const; const generated = generateReactBindings( { ...schema, interfaces: [...schema.interfaces], packages: [...schema.packages] }, "p@1", ["props"], ); assert.match(generated, /"title": WritableField/); assert.match(generated, /"summary": ReadableField/); assert.throws( () => generateReactBindings({ ...schema, interfaces: [], packages: [...schema.packages] }, "p@1", ["props"]), /Missing React reference/, ); const root = await fs.mkdtemp(path.join(os.tmpdir(), "qx-react-types-")); t.after(() => fs.rm(root, { recursive: true, force: true })); await fs.writeFile(path.join(root, "react-props.gen.ts"), generated); await fs.writeFile( path.join(root, "platform.d.ts"), reactPlatformTypes + '\ndeclare module "react" {export type ReactNode = unknown; export type CSSProperties = {}; export function createElement(...args: unknown[]): unknown;}\n', ); await fs.writeFile( path.join(root, "consumer.ts"), `import {useLiveField, type ReadableField, type WritableField} from "@quixos/web-studio-react-runtime"; import type {ReactResults} from "./react-props.gen.js"; declare const props: ReactResults["props"]; const [title, setTitle] = useLiveField(props.fields.fields.title); setTitle("new"); // @ts-expect-error wrong setter value setTitle(123); // @ts-expect-error read-only hook has no setter const [summary, setSummary] = useLiveField(props.fields.fields.summary); const [manual, write] = useLiveField(props.fields.fields.summary, {write: async (value: string) => {}}); write("new"); const readonly: ReadableField = props.fields.fields.title; // @ts-expect-error read-only does not satisfy writable const writable: WritableField = props.fields.fields.summary; declare const narrow: WritableField<"only">; // @ts-expect-error writable references are invariant const widened: WritableField = narrow; // @ts-expect-error callbacks must accept the field's type useLiveField(props.fields.fields.summary, {write: async (value: number) => {}}); `, ); const result = spawnSync( process.execPath, [ path.resolve("node_modules/typescript/bin/tsc"), "--strict", "--noEmit", "--skipLibCheck", "--target", "ES2022", path.join(root, "platform.d.ts"), path.join(root, "consumer.ts"), ], { encoding: "utf8", cwd: root }, ); assert.equal(result.status, 0, result.stdout + result.stderr); await fs.writeFile( path.join(root, "react-props.gen.ts"), generateReactBindings( { ...schema, interfaces: [...schema.interfaces], packages: [...schema.packages] }, "p@1", ["props"], [{ module: "./component.js", propsExport: "props" }], ), ); const checkComponent = () => spawnSync( process.execPath, [ path.resolve("node_modules/typescript/bin/tsc"), "--strict", "--noEmit", "--skipLibCheck", "--target", "ES2022", path.join(root, "platform.d.ts"), path.join(root, "react-props.gen.ts"), path.join(root, "component.ts"), ], { encoding: "utf8", cwd: root }, ); await fs.writeFile( path.join(root, "component.ts"), 'import type {ReactResults} from "./react-props.gen.js"; export default function Component(props: {camino: ReactResults["props"]}) {return null;}', ); const matching = checkComponent(); assert.equal(matching.status, 0, matching.stdout + matching.stderr); await fs.writeFile( path.join(root, "component.ts"), 'import type {WritableField} from "@quixos/web-studio-react-runtime"; export default function Component(props: {camino: {fields: {fields: {summary: WritableField}}}}) {return null;}', ); const incompatible = checkComponent(); assert.notEqual(incompatible.status, 0, "component cannot strengthen a read-only prop into a writable field"); assert.match(incompatible.stdout + incompatible.stderr, /writable|WritableField/); }); test("class factories specialize return types and reject instance implementations or mismatched inputs", () => { const factory = compileCapabilityResourceSource( 'interface Factory id "factory" revision "factory@1" {static operation create id "create" : unit -> ref {call id "call";}}', { source }, ); assert.ok(factory.ok && factory.resource.kind === "interface"); if (!factory.ok || factory.resource.kind !== "interface") throw new Error("factory failed"); const factoryRevision = factory.resource.revision; const compile = (declaration: string) => { const pkg = compileCapabilityResourceSource( `external atom Note id "note"; package P id "p" revision "p@1" {${declaration}}`, { source }, ); assert.ok(pkg.ok && pkg.resource.kind === "package", JSON.stringify(pkg.diagnostics)); if (!pkg.ok || pkg.resource.kind !== "package") throw new Error("package failed"); return compileCapabilitySource( `workspace W id "w" revision "w@1" commit "${source.commit}" { atom Note id "note"; import interface Factory; import package P; conform Note as Factory id "factory-conformance" {bind create.call to package P.make;} }`, "workspace.qx", { interfaces: new Map([["Factory", factoryRevision]]), packages: new Map([["P", pkg.resource.revision]]) }, ); }; const good = compile('function make id "make" : unit -> atom-ref;'); assert.ok(good.ok, JSON.stringify(good.diagnostics)); assert.equal(good.workspace.interfaceImports[0].members[0].operations[0].scope, "class"); const wrongInput = compile('function make id "make" : string -> atom-ref;'); assert.equal(wrongInput.ok, false); const wrongReceiver = compile('operation make id "make" : unit -> atom-ref mode call receiver atom Note;'); assert.equal(wrongReceiver.ok, false); assert.match(JSON.stringify(wrongReceiver.diagnostics), /free function/); }); test("state-field shorthand binds exactly the declared accessors, never invents writes", () => { const iface = compileCapabilityResourceSource( 'interface Reader id "reader" revision "reader@1" {value title id "title" : string {get id "get"; watch start id "watch" stop id "stop";}}', { source }, ); assert.ok(iface.ok && iface.resource.kind === "interface"); if (!iface.ok || iface.resource.kind !== "interface") throw new Error("interface failed"); const result = compileCapabilitySource( `workspace W id "w" revision "w@1" commit "${source.commit}" { atom Note id "note"; import interface Reader; conform Note as Reader id "reader-conformance" {private state Title id "title-slot" on Note : string policy crdt(string) default ""; bind title to state Title;} }`, "workspace.qx", { interfaces: new Map([["Reader", iface.resource.revision]]) }, ); assert.ok(result.ok, JSON.stringify(result.diagnostics)); assert.deepEqual( result.workspace.conformances[0].operationBindings.map((binding) => binding.operationId), ["get", "watch", "stop"], ); });