import assert from "node:assert/strict"; import { test } from "node:test"; import { compileCapabilityResourceSource } from "../src/capability-language/index.js"; import type { InterfaceRevision } from "../src/capability-model/types.js"; import { valueType } from "../src/capability-model/types.js"; import { compileQuery } from "../src/query/compile.js"; import { compileQueryComposition, type QueryComposition } from "../src/query/composition.js"; import { defaultQueryBudgets } from "../src/query/types.js"; const iface = (text: string, dependencies: InterfaceRevision[] = []) => { const result = compileCapabilityResourceSource(text, { source: { repository: "https://example.test/devices", commit: "a".repeat(40) }, environment: { interfaces: new Map(dependencies.map((entry) => [entry.displayName, entry])), interfaceClosure: dependencies, }, }); assert.ok(result.ok, JSON.stringify(result.diagnostics)); if (result.resource.kind !== "interface") throw Error("Expected interface"); return result.resource.revision; }; test("dependent query roots and arguments are checked without a presentation library", async () => { const sensor = iface(`interface Sensor id "sensor" revision "sensor@1" { queryable value reading id "reading" : int64 { get id "reading:get"; } }`); const control = iface( `import interface Sensor; interface Control id "control" revision "control@1" { queryable value enabled id "enabled" : bool { get id "enabled:get"; } queryable relation selected id "selected" : optional-one interface Sensor { resolve id "selected:get"; } }`, [sensor], ); const query = (id: string, root: InterfaceRevision["revisionId"], text: string) => compileQuery( { id, displayName: id, document: `${id}.graphql`, operation: id, root: { kind: "object", interface: root }, fragments: [], views: [], allowances: [], budgets: { ...defaultQueryBudgets }, watch: false, placement: { kind: "replica" }, }, [control, sensor], async () => text, ); const selection = await query( "Selection", control.revisionId, "query Selection { root { enabled selected { _camino { ref atom } } } }", ); const reading = await query( "Reading", sensor.revisionId, "query Reading($enabled: Boolean!) { root { reading @include(if: $enabled) } }", ); const queries = new Map([ ["Selection", selection], ["Reading", reading], ]); const source: QueryComposition = { id: "device-display", input: { kind: "record", fields: { controller: valueType.interfaceRef(control.revisionId) } }, maxQueries: 2, steps: [ { name: "selection", query: "Selection", root: { from: "input", path: ["controller"] }, arguments: { record: {} }, }, { name: "reading", query: "Reading", with: { from: "result", name: "selection", path: ["root", "selected"] }, root: { from: "item", path: ["_camino", "ref"] }, arguments: { record: { enabled: { from: "result", name: "selection", path: ["root", "enabled"] } } }, }, ], }; const checked = compileQueryComposition(source, queries); assert.equal(checked.output.kind, "record"); assert.equal(checked.dependencies.length, 2); assert.ok(checked.definitionDigest); const changed = new Map(queries); changed.set("Reading", { ...reading, definitionDigest: "different-source" }); assert.notEqual(compileQueryComposition(source, changed).definitionDigest, checked.definitionDigest); assert.throws(() => compileQueryComposition({ ...source, maxQueries: 1 }, queries), /exceed maxQueries/); assert.throws( () => compileQueryComposition({ ...source, steps: [...source.steps].reverse() }, queries), /earlier step/, ); const wrongArguments = structuredClone(source); wrongArguments.steps[1]!.arguments = { record: { enabled: { from: "input", path: ["controller"] } } }; assert.throws(() => compileQueryComposition(wrongArguments, queries), /arguments do not match/); const wrongRoot = structuredClone(source); wrongRoot.steps[1]!.root = { from: "input", path: ["controller"] }; assert.throws(() => compileQueryComposition(wrongRoot, queries), /reference to sensor@1/); const nullableTraversal = structuredClone(source); nullableTraversal.steps[1]!.root = { from: "result", name: "selection", path: ["root", "selected", "_camino", "ref"], }; assert.throws(() => compileQueryComposition(nullableTraversal, queries), /explicit boundary/); const branch = structuredClone(source); branch.steps[1]!.when = { equal: [{ from: "item", path: ["_camino", "atom"] }, { literal: "thermometer" }] }; assert.ok(compileQueryComposition(branch, queries).definitionDigest); branch.steps[1]!.when = { literal: "not a boolean" }; assert.throws(() => compileQueryComposition(branch, queries), /when requires a boolean/); });