Files
quixos-protocol/test/query-compiler.test.ts
T
Timothy J. Aveni bd42306059 Fix Studio query contracts for authored workspaces
Keep query definition identity scoped to interfaces present in the generated
query schema. Exclude retention provenance from both interface revisions and
generic applications while preserving nominal identities, closed arguments,
operation contracts, documents, budgets and fragment dependencies. Advance the
query semantics version so prior compiler outputs cannot share the new identity.

Add a generic compiler regression covering unrelated imports, retained generic
sources and invalidation when an observed getter changes. This fixes Studio's
CLIENT_QUERY_MISMATCH when template contracts become editable local repositories.

Validation: protocol tests (296 total, 293 passed, 3 existing skips); nix fmt,
monorepo lint, final formatting check and diff check; camino-dev/qx-workspace
builds. All 17 Studio query descriptors match a rebuilt copy of the user's
workspace. Headless fresh-workspace canvas/library/Reader/reload check passes.

Repair the existing local qx-reader workbench through its ordinary check and
activation path, updating only its pinned protocol/checker CLI. Retain the
original toolchain GC root and record checker provenance. Source commit, runtime
and template pins, and database are preserved. Verify the selected candidate is
ready at epoch 2 and its canvas loads through the corrected Studio bundle without
creating application objects. Stop the independent smoke-test services.
2026-10-01 09:49:40 -07:00

541 lines
24 KiB
TypeScript

import assert from "node:assert/strict";
import { test } from "node:test";
import { compileCapabilityResourceSource } from "../src/capability-language/index.js";
import { compileQuery } from "../src/query/compile.js";
import { QueryCompileError } from "../src/query/types.js";
import { checkQueryTemplate } from "../src/query/templates.js";
import { querySelectionToWire } from "../src/query/proto.js";
import type { InterfaceRevision } from "../src/capability-model/types.js";
const source = { repository: "https://example.test/queries.git", commit: "a".repeat(40) };
const iface = (text: string, dependencies: InterfaceRevision[] = []) => {
const compiled = compileCapabilityResourceSource(text, {
source,
environment: {
interfaces: new Map(dependencies.map((entry) => [entry.displayName, entry])),
interfaceClosure: dependencies,
},
});
assert.ok(compiled.ok, JSON.stringify(compiled.diagnostics));
assert.equal(compiled.resource.kind, "interface");
if (compiled.resource.kind !== "interface") throw new Error("interface required");
return compiled.resource.revision;
};
const facts = iface(`interface TaskFacts id "facts" revision "facts@1" {
queryable value title id "title" : string { get id "title:get"; }
queryable value done id "done" : bool { get id "done:get"; }
queryable value due id "due" : optional<int64> { get id "due:get"; }
queryable rpc value score id "score" : int32 { get id "score:get"; }
value hidden id "hidden" : string { get id "hidden:get"; }
}`);
const collection = iface(
`import interface TaskFacts;
interface Tasks id "tasks" revision "tasks@1" {
queryable relation items id "items" : many interface TaskFacts ordered { resolve id "items:resolve"; }
}`,
[facts],
);
test("query identity ignores unrelated imports and generic retention provenance", async () => {
const generic = iface(`interface Box<object Item> id "box" revision "box@1" {
queryable relation item id "box:item" : optional-one object Item {resolve id "box:item:resolve";}
}`);
const compiled = compileCapabilityResourceSource(
`import interface Box; import interface TaskFacts;
package Queries id "queries" revision "queries@1" {
query Row id "row" root Box<interface TaskFacts> document "row.graphql" operation "Row" {replica;max rows 10;}
}`,
{
source,
environment: {
interfaces: new Map([
["Box", generic],
["TaskFacts", facts],
]),
interfaceClosure: [generic, facts],
},
},
);
assert.ok(compiled.ok, JSON.stringify(compiled.diagnostics));
if (compiled.resource.kind !== "package") throw new Error("package required");
const declaration = compiled.resource.revision.queries![0]!;
const closed = compiled.resource.specializations![0]!;
const read = async () => "query Row {root {item {title}}}";
const before = await compileQuery(declaration, [facts, closed], read);
const retained = structuredClone(closed);
retained.source = { repository: "https://local.test/box.git", commit: "b".repeat(40) };
retained.application!.source = retained.source;
const unrelated = iface(`interface Unrelated id "unrelated" revision "unrelated@1" {
queryable value label id "label" : string {get id "label:get";}
}`);
assert.equal(
(await compileQuery(declaration, [unrelated, generic, retained, facts], read)).definitionDigest,
before.definitionDigest,
);
const changed = structuredClone(facts);
changed.members[0]!.operations[0]!.id = "changed:title:get" as (typeof changed.members)[0]["operations"][0]["id"];
assert.notEqual(
(await compileQuery(declaration, [changed, retained], read)).definitionDigest,
before.definitionDigest,
);
});
test("generic query templates check declared bounds before closed specialization", async () => {
const generic = iface(
`import interface TaskFacts;
interface Collection<object Item implements TaskFacts> id "generic-collection" revision "generic-collection@1" {
queryable relation items id "items" : many object Item { resolve id "items:resolve"; }
}`,
[facts],
);
const build = (bound: string, workspace = false) =>
compileCapabilityResourceSource(
`import interface Collection; import interface TaskFacts;
package GenericQueries id "generic-queries" revision "generic-queries@1" {
query Rows<object Item ${bound}> id "rows-template" root ${workspace ? "workspace " : ""}Collection<Item>
document "rows.graphql" operation "Rows" { view object Item as TaskFacts; max rows 30; }
query TaskRows id "task-rows" specialize Rows<interface TaskFacts>;
}`,
{
source,
environment: {
interfaces: new Map([
["Collection", generic],
["TaskFacts", facts],
]),
interfaceClosure: [generic, facts],
},
},
);
const result = build("implements TaskFacts");
assert.ok(result.ok, JSON.stringify(result.diagnostics));
if (result.resource.kind !== "package") throw new Error("package expected");
const pkg = result.resource.revision;
assert.equal(pkg.queries?.length, 1);
assert.equal(pkg.queryTemplates?.length, 1);
assert.equal(pkg.queries![0]!.application!.templateId, "rows-template");
const read = async () => `query Rows { root { items(first: 3) { entries { node { title } } } } }`;
const universal = await checkQueryTemplate(pkg.queryTemplates![0]!, [generic, facts], read);
assert.equal(
universal.effects.some((effect) => effect.memberId === "title"),
true,
);
const specialized = await compileQuery(
pkg.queries![0]!,
[generic, facts, ...(result.resource.specializations ?? [])],
read,
);
assert.equal(
specialized.effects.some((effect) => effect.memberId === "title"),
true,
);
const invalid = build("");
assert.ok(invalid.ok, JSON.stringify(invalid.diagnostics));
if (invalid.resource.kind !== "package") throw new Error("package expected");
await assert.rejects(
checkQueryTemplate(invalid.resource.revision.queryTemplates![0]!, [generic, facts], read),
/bound|guarantee|implement/i,
);
const extent = build("implements TaskFacts", true);
assert.ok(extent.ok, JSON.stringify(extent.diagnostics));
if (extent.resource.kind !== "package") throw new Error("package expected");
const extentPackage = extent.resource.revision;
const readExtent = async () =>
`query Rows {root {objects(first: 2) {entries {node {items(first: 3) {entries {node {title}}}}}}}}`;
await checkQueryTemplate(extentPackage.queryTemplates![0]!, [generic, facts], readExtent);
const closed = await compileQuery(
extentPackage.queries![0]!,
[generic, facts, ...(extent.resource.specializations ?? [])],
readExtent,
);
assert.equal(closed.declaration.root.kind, "workspace-interface");
assert.equal(closed.declaration.root.interface, specialized.declaration.root.interface);
const invalidExtent = build("", true);
assert.ok(invalidExtent.ok, JSON.stringify(invalidExtent.diagnostics));
if (invalidExtent.resource.kind !== "package") throw new Error("package expected");
await assert.rejects(
checkQueryTemplate(invalidExtent.resource.revision.queryTemplates![0]!, [generic, facts], readExtent),
/bound|guarantee|implement/i,
);
});
function fixture(clauses = "", root = "Tasks") {
const result = compileCapabilityResourceSource(
`import interface Tasks; import interface TaskFacts;
package Queries id "queries" revision "queries@1" {
query Upcoming id "upcoming" root ${root} document "queries/upcoming.graphql" operation "Upcoming" {
fragments "queries/row.graphql"; max rows 30; ${clauses}
}
}`,
{
source,
environment: {
interfaces: new Map([
["Tasks", collection],
["TaskFacts", facts],
]),
interfaceClosure: [collection, facts],
},
},
);
assert.ok(result.ok, JSON.stringify(result.diagnostics));
assert.equal(result.resource.kind, "package");
if (result.resource.kind !== "package") throw new Error("package required");
assert.equal(result.resource.revision.exports.length, 0);
return result.resource.revision.queries![0]!;
}
test("workspace roots expose a bounded interface extent, not a root object or writable relationship", async () => {
const declaration = { ...fixture("watch;", "workspace TaskFacts"), fragments: [] };
assert.deepEqual(declaration.root, { kind: "workspace-interface", interface: "facts@1" });
const checked = await compileQuery(
declaration,
[facts, collection],
async () => `query Upcoming {
root {
objects(first: 3, where: {done: {eq: false}}, orderBy: {title: ASC}) {
entries {key node {_camino {ref} title}} pageInfo {hasNextPage endCursor}
}
_camino {relations {objects {aggregate {count sum {due}}}}}
}
}`,
);
assert.deepEqual(new Set(checked.effects.map((effect) => effect.memberId)), new Set(["done", "title", "due"]));
assert.ok(!checked.effects.some((effect) => effect.memberId.includes("workspace-objects")));
assert.ok(JSON.stringify(checked.selection).includes("qx-internal-workspace-objects"));
await assert.rejects(
compileQuery(declaration, [facts], async () => `query Upcoming {root {_camino {ref}}}`),
/QUERY_VALIDATION/,
);
await assert.rejects(
compileQuery(declaration, [facts], async () => `query Upcoming {root {objects {entries {key}}}}`),
/QUERY_ROW_LIMIT/,
);
});
const document = `query Upcoming($first: Int!, $before: Int64!) {
root { items(first: $first, where: {done: {eq: false}, due: {lt: $before}}, orderBy: [{due: ASC}]) {
entries { key node { _camino { ref } ...Row } } pageInfo { hasNextPage endCursor }
} }
}`;
const compile = (query = document, row = "fragment Row on TaskFacts { title due }", clauses = "") =>
compileQuery(fixture(clauses), [collection, facts], async (name) => (name.endsWith("row.graphql") ? row : query));
test("ordinary selections preserve aliases, fragment conditions and wire identity", async () => {
const checked = await compile(
`query Upcoming($show: Boolean!) {root {items(first: 3) {entries {node {plain: title ...Row @include(if: $show)}}}}}`,
`fragment Row on TaskFacts {label: title due}`,
);
const node = checked.selection[0]!.selection[0]!.selection[0]!.selection[0]!;
assert.deepEqual(
node.selection.map((s) => s.key),
["plain", "label", "due", "_camino"],
);
assert.equal(node.selection[1]!.conditions.length, 1);
assert.ok(JSON.stringify(checked.selection.map(querySelectionToWire)).includes("label"));
});
test("object references are unconditional, canonical and idempotent through fragments", async () => {
const checked = await compile(
`query Upcoming($show: Boolean!) {root {items(first: 3) {entries {node {...Row @include(if: $show)}}}}}`,
`fragment Row on TaskFacts {label: title identity: _camino {alias: ref} _camino @include(if: $show) {ref}}`,
);
const root = checked.selection[0]!;
const node = root.selection.find((s) => s.name === "items")!.selection[0]!.selection[0]!;
for (const object of [root, node]) {
const metadata = object.selection.filter((s) => s.key === "_camino");
assert.equal(metadata.length, 1);
assert.deepEqual(metadata[0]!.conditions, []);
assert.deepEqual(
metadata[0]!.selection.filter((s) => s.key === "ref").map((s) => s.conditions),
[[]],
);
}
assert.equal(node.selection.find((s) => s.key === "identity")!.conditions.length, 1);
const recompiled = await compileQuery(
{ ...checked.declaration, fragments: [] },
[collection, facts],
async () => checked.document,
);
assert.deepEqual(recompiled.selection, checked.selection);
assert.deepEqual(recompiled.output, checked.output);
});
test("retired live syntax fails with migration guidance", async () => {
for (const selected of ["score @live", "_camino @live {ref}", "title @live title", "title @live(unchecked: true)"])
await assert.rejects(
compile(
`query Upcoming {root {items(first: 3) {entries {node {...Row}}}}}`,
`fragment Row on TaskFacts {${selected}}`,
'allow TaskFacts.score select "bounded";',
),
(error: unknown) => error instanceof QueryCompileError && error.code === "QUERY_RETIRED_DIRECTIVE",
);
});
test("implicit references reject aliases which occupy the canonical metadata path", async () => {
for (const invalid of ["_camino: title", "_camino {ref: __typename}"])
await assert.rejects(
compile(
`query Upcoming {root {items(first: 3) {entries {node {...Row}}}}}`,
`fragment Row on TaskFacts {${invalid}}`,
),
/QUERY_VALIDATION|Introspection/,
);
});
test("implicit reference merges with conditional aggregate metadata", async () => {
const checked = await compileQuery(
{ ...fixture(""), fragments: [] },
[collection, facts],
async () =>
`query Upcoming($show: Boolean!) {root {_camino @include(if: $show) {relations {items {aggregate {count}}}}}}`,
);
const metadata = checked.selection[0]!.selection.filter((field) => field.key === "_camino");
assert.equal(metadata.length, 1);
assert.deepEqual(metadata[0]!.conditions, []);
assert.equal(metadata[0]!.selection.find((field) => field.name === "relations")!.conditions.length, 1);
assert.deepEqual(metadata[0]!.selection.find((field) => field.name === "ref")!.conditions, []);
assert.equal(
metadata[0]!.selection
.find((field) => field.name === "relations")!
.selection[0]!.selection[0]!.selection.some((field) => field.name === "_camino"),
false,
);
});
test("qualified interface predicates and guaranteed inline projections retain nominal reads", async () => {
const owner = iface(
`interface Owner id "owner" revision "owner@1" { queryable value name id "name" : string {get id "name:get";} }`,
);
const owned = iface(
`import interface Owner;
interface Owned id "owned" revision "owned@1" {
queryable relation owner id "owner" : optional-one interface Owner {resolve id "owner:resolve";}
queryable value label id "owned:label" : string {get id "owned:label:get";}
}`,
[owner],
);
const item = iface(
`import interface Owned;
interface Item id "item" revision "item@1" requires Owned {
queryable value label id "item:label" : string {get id "item:label:get";}
}`,
[owned, owner],
);
const declaration = {
...fixture(""),
root: { kind: "workspace-interface" as const, interface: item.revisionId },
fragments: [],
};
const document = `query Upcoming($owner: QxRef_Owner!) {root {objects(all: 10, where: {_camino: {as: {Owned: {owner: {eq: $owner}}}}}) {entries {node {label ... on Owned {owner {name} ownerLabel: label}}}}}}`;
const checked = await compileQuery(declaration, [item, owned, owner], async () => document);
assert.ok(
checked.effects.some(
(effect) =>
effect.interfaceRevisionId === owned.revisionId &&
effect.memberId === "owner" &&
effect.uses.includes("predicate"),
),
);
const objects = checked.selection[0]!.selection[0]!;
assert.equal(objects.predicate?.kind, "and");
assert.ok(JSON.stringify(objects.predicate).includes('"kind":"as"'));
const node = objects.selection[0]!.selection[0]!;
assert.equal(node.selection.find((s) => s.key === "ownerLabel")!.interfaceRevisionId, owned.revisionId);
assert.equal(node.selection.filter((s) => s.key === "_camino").length, 1);
await assert.rejects(
compileQuery(declaration, [item, owned, owner], async () =>
document.replace("ownerLabel: label", "ownerLabel: label _camino {ref}"),
),
/QUERY_VALIDATION/,
);
const explicitProjectionRef = await compileQuery(declaration, [item, owned, owner], async () =>
document.replace("ownerLabel: label", "ownerLabel: label ownedIdentity: _camino {ref}"),
);
const projectedNode = explicitProjectionRef.selection[0]!.selection[0]!.selection[0]!.selection[0]!;
assert.equal(
projectedNode.selection.find((field) => field.key === "_camino")!.selection.find((field) => field.key === "ref")!
.targetInterfaceRevisionId,
item.revisionId,
);
await assert.rejects(
compileQuery(declaration, [item, owned, owner], async () => document.replace("ownerLabel: label", "label")),
/QUERY_PROJECTION_CONFLICT|QUERY_VALIDATION/,
);
});
test("query dependency ports resolve exact exports without declaration ordering constraints", () => {
const result = compileCapabilityResourceSource(
`import interface Tasks;
package Queries id "queries" revision "queries@1" {
operation load id "read" : unit -> unit mode call receiver interfaces [Tasks]
requires { query upcoming id "upcoming-port" : Queries.Upcoming; };
query Upcoming id "upcoming" root Tasks document "upcoming.graphql" operation "Upcoming" { max rows 30; }
}`,
{ source, environment: { interfaces: new Map([["Tasks", collection]]), interfaceClosure: [collection, facts] } },
);
assert.ok(result.ok, JSON.stringify(result.diagnostics));
assert.equal(result.resource.kind, "package");
if (result.resource.kind !== "package") throw new Error("package expected");
assert.deepEqual(result.resource.revision.exports[0]!.dependencyPorts[0]!.requirement, {
kind: "query",
packageRevisionId: "queries@1",
queryId: "upcoming",
});
});
test("result types merge repeated selections and preserve conditional fragment fields", async () => {
const checked = await compile(
`query Upcoming($show: Boolean!) {
root { items(first: 3) { entries { node { title } } } }
root { items(first: 3) { entries { node { ...Row @include(if: $show) } } } }
}`,
"fragment Row on TaskFacts { due done }",
);
const output = JSON.stringify(checked.output);
assert.match(output, /\"title\":\{\"kind\":\"scalar\",\"name\":\"string\"\}/);
assert.match(output, /\"done\":\{\"kind\":\"optional\",\"value\":\{\"kind\":\"scalar\",\"name\":\"bool\"\}\}/);
assert.match(output, /\"due\"/);
});
test("fixed GraphQL yields exact effects, typed references and distinct query artifacts", async () => {
const checked = await compile();
assert.deepEqual(checked.variables, {
kind: "record",
fields: {
first: { kind: "scalar", name: "int32" },
before: { kind: "scalar", name: "int64" },
},
});
assert.match(JSON.stringify(checked.output), /"kind":"object-ref"/);
assert.ok(
checked.effects.some(
(effect) => effect.memberId === "due" && effect.uses.includes("order") && effect.uses.includes("predicate"),
),
);
assert.equal(checked.definitionDigest, (await compile()).definitionDigest);
assert.notEqual(
checked.definitionDigest,
(await compile(document, "fragment Row on TaskFacts { title }")).definitionDigest,
);
});
test("query allowlist and two-sided RPC effects fail during compilation", async () => {
const rejects = (promise: Promise<unknown>, code: string) =>
assert.rejects(promise, (error: unknown) => error instanceof QueryCompileError && error.code === code);
const substring = (field: string) =>
`query Upcoming {root {items(first: 3, where: {${field}: {containsInsensitive: "TASK"}}) {entries {node {...Row}}}}}`;
await compile(substring("title"));
await rejects(compile(substring("due")), "QUERY_VALIDATION");
await rejects(compile(document, "fragment Row on TaskFacts { hidden }"), "QUERY_VALIDATION");
await rejects(compile(document, "fragment Row on TaskFacts { score }"), "QUERY_RPC_CONSUMER_REASON");
await compile(
document,
"fragment Row on TaskFacts { score }",
'allow TaskFacts.score select "Only the visible page is enriched";',
);
await rejects(
compile(document, "fragment Row on TaskFacts { score }", 'watch; allow TaskFacts.score select "Visible rows";'),
"QUERY_WATCH_UNSUPPORTED",
);
await compile(
document,
"fragment Row on TaskFacts { score }",
'watch; poll 5000 "External score has no push API"; allow TaskFacts.score select "Visible rows";',
);
await rejects(compile(document.replace("first: $first", "first: 31")), "QUERY_VALIDATION");
await rejects(compile(document.replace("...Row", "__typename ...Row")), "QUERY_UNSUPPORTED_FEATURE");
await rejects(compile(document, "fragment Row on TaskFacts { ...Row }"), "QUERY_VALIDATION");
});
test("unselected atom relationships do not require views on reference-only targets", async () => {
const target = iface(`external atom Detail id "detail";
interface Referenced id "referenced" revision "referenced@1" {
queryable relation details id "details" : many atom Detail {resolve id "details:resolve";}
}`);
const subject = iface(
`external atom Detail id "detail"; import interface Referenced;
interface Subject id "subject" revision "subject@1" {
queryable relation selected id "selected" : optional-one interface Referenced {resolve id "selected:resolve";}
}`,
[target],
);
const declaration = {
...fixture(""),
root: { kind: "object" as const, interface: subject.revisionId },
fragments: [],
};
const document = "query Upcoming {root {selected {_camino {ref}}}}";
const checked = await compileQuery(declaration, [subject, target], async () => document);
assert.match(JSON.stringify(checked.output), /referenced@1/);
await assert.rejects(
compileQuery(
declaration,
[subject, target],
async () => "query Upcoming {root {selected {details(all: 3) {entries {node {_camino {ref}}}}}}}",
),
/QUERY_VIEW_REQUIRED/,
);
});
test("React tooling generates checked owner queries from imported closed navigation fields", async () => {
const { presentationQueries } = await import("../src/bindings/presentation-queries.js");
const mount = iface('interface PresentationMount id "mount" revision "mount@1" {}');
const subject = iface('interface Subject id "subject" revision "subject@1" {}');
const state = iface(
`import interface PresentationMount; import interface Subject;
interface PresentationState id "state" revision "state@1" {
queryable value occurrence id "occurrence" : string {get id "occurrence:get" execution snapshot;}
queryable relation presentationMount id "mount" : optional-one interface PresentationMount {resolve id "mount:resolve" execution snapshot;}
queryable relation parent id "parent" : optional-one interface PresentationState {resolve id "parent:resolve" execution snapshot;}
queryable relation source id "source" : exactly-one interface Subject {resolve id "source:resolve" execution snapshot;}
}`,
[mount, subject],
);
const navigation = iface(
`import interface PresentationState;
interface Navigation<value T> id "navigation" revision "navigation@1" requires PresentationState {
queryable value custom id "custom" : T {get id "custom:get" execution snapshot;}
}`,
[mount, subject, state],
);
const react = iface(
`import interface PresentationState;
interface ReactComponent<object Subject, object Navigation implements PresentationState> id "react" revision "react@1" {
module render id "render" contract "org.quixos.react.component/2" subject object Subject;
}`,
[mount, subject, state],
);
const dependencies = [mount, subject, state, navigation, react];
const parsed = compileCapabilityResourceSource(
`import interface ReactComponent; import interface Navigation; import interface Subject;
package Example id "example" revision "example@1" {
module Screen id "screen" implements ReactComponent<interface Subject,interface Navigation<record {label:string;count:int32;}>>.render;
}`,
{
source,
environment: {
interfaces: new Map(dependencies.map((entry) => [entry.displayName, entry])),
interfaceClosure: dependencies,
},
},
);
assert.ok(parsed.ok, JSON.stringify(parsed.diagnostics));
if (parsed.resource.kind !== "package") throw Error("package required");
const contracts = [...dependencies, ...(parsed.resource.specializations ?? [])];
const documents = presentationQueries(parsed.resource.revision, contracts);
assert.equal(documents.size, 2);
const checked = await Promise.all(
parsed.resource.revision.queries!.map((query) =>
compileQuery(query, contracts, async (name) => documents.get(name)!),
),
);
assert.deepEqual(
checked.map((query) => query.declaration.placement?.kind),
["replica", "local"],
);
assert.match(JSON.stringify(checked[0]!.output), /"custom":\{"kind":"record","fields":\{"label"/);
assert.match(JSON.stringify(checked[0]!.selection), /"kind":"as"/);
assert(
checked.every((query) =>
query.effects.some((effect) => effect.memberId === "mount" && effect.uses.includes("predicate")),
),
);
});