2026-09-05 12:33:52 -07:00
|
|
|
import assert from "node:assert/strict";
|
|
|
|
|
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
|
|
|
|
import os from "node:os";
|
|
|
|
|
import path from "node:path";
|
|
|
|
|
import test from "node:test";
|
2026-09-15 15:23:24 -07:00
|
|
|
import { compileCapabilityResourceRepository, compileWorkspaceRepository } from "../src/capability-language/index.js";
|
2026-09-05 12:33:52 -07:00
|
|
|
|
|
|
|
|
const quixosCommit = "1".repeat(40);
|
|
|
|
|
const namedCommit = "2".repeat(40);
|
|
|
|
|
const packageCommit = "3".repeat(40);
|
|
|
|
|
|
|
|
|
|
const lock = (resources = "") => `quixos-lock version 1 {
|
|
|
|
|
quixos source {
|
|
|
|
|
repository "https://example.test/quixos.git";
|
|
|
|
|
commit "${quixosCommit}";
|
|
|
|
|
}
|
|
|
|
|
${resources}
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
test("workspace assembly resolves resource-owned dependencies recursively", async (context) => {
|
|
|
|
|
const directory = await mkdtemp(path.join(os.tmpdir(), "quixos-assembly-"));
|
|
|
|
|
context.after(() => rm(directory, { recursive: true, force: true }));
|
|
|
|
|
const root = path.join(directory, "root");
|
|
|
|
|
const named = path.join(directory, "named");
|
|
|
|
|
const runtime = path.join(directory, "runtime");
|
|
|
|
|
await Promise.all([mkdir(root), mkdir(named), mkdir(runtime)]);
|
2026-09-15 15:23:24 -07:00
|
|
|
await writeFile(
|
|
|
|
|
path.join(root, "quixos.lock"),
|
|
|
|
|
lock(`package Runtime source {
|
2026-09-05 12:33:52 -07:00
|
|
|
repository "https://example.test/package-runtime.git";
|
|
|
|
|
commit "${packageCommit}";
|
2026-09-15 15:23:24 -07:00
|
|
|
}`),
|
|
|
|
|
);
|
|
|
|
|
await writeFile(
|
|
|
|
|
path.join(root, "workspace.qx"),
|
|
|
|
|
`workspace Test id "workspace:test" revision "workspace:test@1" commit "${"4".repeat(40)}" {
|
2026-09-05 12:33:52 -07:00
|
|
|
atom Subject id "atom:subject";
|
|
|
|
|
import package Runtime;
|
|
|
|
|
}
|
2026-09-15 15:23:24 -07:00
|
|
|
`,
|
|
|
|
|
);
|
2026-09-05 12:33:52 -07:00
|
|
|
await writeFile(path.join(named, "quixos.lock"), lock());
|
2026-09-15 15:23:24 -07:00
|
|
|
await writeFile(
|
|
|
|
|
path.join(named, "interface.qx"),
|
|
|
|
|
`interface Named id "interface:named" revision "interface:named@1" {
|
2026-09-05 12:33:52 -07:00
|
|
|
value name id "member:named:name" : string {
|
|
|
|
|
get id "operation:named:name:get";
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-15 15:23:24 -07:00
|
|
|
`,
|
|
|
|
|
);
|
|
|
|
|
await writeFile(
|
|
|
|
|
path.join(runtime, "quixos.lock"),
|
|
|
|
|
lock(`interface Named source {
|
2026-09-05 12:33:52 -07:00
|
|
|
repository "https://example.test/interface-named.git";
|
|
|
|
|
commit "${namedCommit}";
|
2026-09-15 15:23:24 -07:00
|
|
|
}`),
|
|
|
|
|
);
|
|
|
|
|
await writeFile(
|
|
|
|
|
path.join(runtime, "package.qx"),
|
|
|
|
|
`import interface Named;
|
2026-09-05 12:33:52 -07:00
|
|
|
package Runtime id "package:runtime" revision "package:runtime@1" {
|
|
|
|
|
function describe id "export:runtime:describe" : interface-ref<Named> -> string;
|
|
|
|
|
}
|
2026-09-15 15:23:24 -07:00
|
|
|
`,
|
|
|
|
|
);
|
2026-09-05 12:33:52 -07:00
|
|
|
|
|
|
|
|
const directories = new Map([
|
|
|
|
|
[`interface\0https://example.test/interface-named.git\0${namedCommit}`, named],
|
|
|
|
|
[`package\0https://example.test/package-runtime.git\0${packageCommit}`, runtime],
|
|
|
|
|
]);
|
|
|
|
|
const result = await compileWorkspaceRepository({
|
|
|
|
|
rootDirectory: root,
|
|
|
|
|
resolveResource: async (source, kind) => {
|
|
|
|
|
const resolved = directories.get(`${kind}\0${source.repository}\0${source.commit}`);
|
|
|
|
|
if (!resolved) throw new Error(`Unexpected source ${source.repository}`);
|
|
|
|
|
return { directory: resolved };
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
assert.equal(result.resources.length, 2);
|
|
|
|
|
assert.deepEqual(
|
|
|
|
|
result.workspace.interfaceImports.map((entry) => entry.revisionId),
|
|
|
|
|
["interface:named@1"],
|
|
|
|
|
);
|
|
|
|
|
assert.deepEqual(
|
|
|
|
|
result.workspace.packageImports.map((entry) => entry.revisionId),
|
|
|
|
|
["package:runtime@1"],
|
|
|
|
|
);
|
|
|
|
|
assert.equal(result.directResources.size, 1);
|
|
|
|
|
|
2026-10-10 17:09:03 -07:00
|
|
|
const resolveResource = async (source: { repository: string; commit: string }, kind: string) => ({
|
|
|
|
|
directory: directories.get(`${kind}\0${source.repository}\0${source.commit}`)!,
|
|
|
|
|
});
|
|
|
|
|
const bound = await Promise.all(
|
|
|
|
|
["workspace:first", "workspace:second"].map((workspaceId) =>
|
|
|
|
|
compileWorkspaceRepository({
|
|
|
|
|
rootDirectory: root,
|
|
|
|
|
resolveResource,
|
|
|
|
|
workspaceId,
|
|
|
|
|
sourceRootCommit: "5".repeat(40),
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
assert.notEqual(bound[0]!.workspace.workspaceId, bound[1]!.workspace.workspaceId);
|
|
|
|
|
assert.notEqual(bound[0]!.workspace.id, bound[1]!.workspace.id);
|
|
|
|
|
for (const instance of bound) {
|
|
|
|
|
assert.equal(instance.workspace.sourceRootCommit, "5".repeat(40));
|
|
|
|
|
assert.deepEqual(instance.workspace.packageImports, result.workspace.packageImports);
|
|
|
|
|
assert.deepEqual(instance.workspace.interfaceImports, result.workspace.interfaceImports);
|
|
|
|
|
assert.deepEqual(instance.resources, result.resources, "binding does not fork resource identities");
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-05 12:33:52 -07:00
|
|
|
const resource = await compileCapabilityResourceRepository({
|
|
|
|
|
rootDirectory: runtime,
|
|
|
|
|
kind: "package",
|
|
|
|
|
source: {
|
|
|
|
|
resolver: "git",
|
|
|
|
|
repository: "https://example.test/package-runtime.git",
|
|
|
|
|
commit: packageCommit,
|
|
|
|
|
},
|
|
|
|
|
resolveResource: async (source, kind) => {
|
|
|
|
|
const resolved = directories.get(`${kind}\0${source.repository}\0${source.commit}`);
|
|
|
|
|
if (!resolved) throw new Error(`Unexpected source ${source.repository}`);
|
|
|
|
|
return { directory: resolved };
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
assert.equal(resource.resource.kind, "package");
|
|
|
|
|
assert.equal(resource.resources.length, 2);
|
|
|
|
|
assert.equal(resource.directResources.size, 1);
|
2026-10-01 01:37:46 -07:00
|
|
|
|
|
|
|
|
const checked = new Map(result.resources.map((node) => [node.key, node.resource]));
|
|
|
|
|
let generated = 0;
|
|
|
|
|
const reused = await compileWorkspaceRepository({
|
|
|
|
|
rootDirectory: root,
|
|
|
|
|
resolveResource: async (source, kind) => ({
|
|
|
|
|
directory: directories.get(`${kind}\0${source.repository}\0${source.commit}`)!,
|
|
|
|
|
}),
|
|
|
|
|
checkedResource: async (kind, source) => {
|
|
|
|
|
const value = checked.get(`${kind}\0${source.repository}\0${source.commit}`);
|
|
|
|
|
return value?.kind === "build" ? undefined : value;
|
|
|
|
|
},
|
|
|
|
|
generateQueries: async () => {
|
|
|
|
|
generated++;
|
|
|
|
|
return { queries: [], documents: new Map() };
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
assert.deepEqual(reused, result, "Nix-composed resource checks preserve full assembly results");
|
|
|
|
|
assert.equal(generated, 0, "unchanged checked packages do not run their query frontend again");
|
|
|
|
|
await assert.rejects(
|
|
|
|
|
compileWorkspaceRepository({
|
|
|
|
|
rootDirectory: root,
|
|
|
|
|
resolveResource: async (source, kind) => ({
|
|
|
|
|
directory: directories.get(`${kind}\0${source.repository}\0${source.commit}`)!,
|
|
|
|
|
}),
|
|
|
|
|
checkedResource: async () => (resource.resource.kind === "build" ? undefined : resource.resource),
|
|
|
|
|
}),
|
|
|
|
|
/Checked resource identity mismatch/,
|
|
|
|
|
);
|
2026-09-05 12:33:52 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("resource repositories cannot own workspace Quixos selection policy", async (context) => {
|
|
|
|
|
const directory = await mkdtemp(path.join(os.tmpdir(), "quixos-resource-policy-"));
|
|
|
|
|
context.after(() => rm(directory, { recursive: true, force: true }));
|
2026-09-15 15:23:24 -07:00
|
|
|
await writeFile(
|
|
|
|
|
path.join(directory, "quixos.lock"),
|
|
|
|
|
`quixos-lock version 1 {
|
2026-09-05 12:33:52 -07:00
|
|
|
quixos source {
|
|
|
|
|
repository "https://example.test/quixos.git";
|
|
|
|
|
policy track-development;
|
|
|
|
|
ref "dev/alice/main";
|
|
|
|
|
commit "${quixosCommit}";
|
|
|
|
|
}
|
2026-09-15 15:23:24 -07:00
|
|
|
}`,
|
|
|
|
|
);
|
|
|
|
|
await writeFile(
|
|
|
|
|
path.join(directory, "package.qx"),
|
|
|
|
|
`package Runtime id "package:runtime" revision "package:runtime@1" { }\n`,
|
|
|
|
|
);
|
2026-09-05 12:33:52 -07:00
|
|
|
|
|
|
|
|
await assert.rejects(
|
|
|
|
|
compileCapabilityResourceRepository({
|
|
|
|
|
rootDirectory: directory,
|
|
|
|
|
kind: "package",
|
|
|
|
|
source: {
|
|
|
|
|
resolver: "git",
|
|
|
|
|
repository: "https://example.test/package-runtime.git",
|
|
|
|
|
commit: packageCommit,
|
|
|
|
|
},
|
|
|
|
|
resolveResource: async () => ({ directory }),
|
|
|
|
|
}),
|
|
|
|
|
/resource locks may only declare their exact authored-against commit/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("resource manifests cannot hide lock dependencies", async (context) => {
|
|
|
|
|
const directory = await mkdtemp(path.join(os.tmpdir(), "quixos-assembly-mismatch-"));
|
|
|
|
|
context.after(() => rm(directory, { recursive: true, force: true }));
|
|
|
|
|
const root = path.join(directory, "root");
|
|
|
|
|
const runtime = path.join(directory, "runtime");
|
|
|
|
|
await Promise.all([mkdir(root), mkdir(runtime)]);
|
2026-09-15 15:23:24 -07:00
|
|
|
await writeFile(
|
|
|
|
|
path.join(root, "quixos.lock"),
|
|
|
|
|
lock(`package Runtime source {
|
2026-09-05 12:33:52 -07:00
|
|
|
repository "https://example.test/package-runtime.git";
|
|
|
|
|
commit "${packageCommit}";
|
2026-09-15 15:23:24 -07:00
|
|
|
}`),
|
|
|
|
|
);
|
|
|
|
|
await writeFile(
|
|
|
|
|
path.join(root, "workspace.qx"),
|
|
|
|
|
`workspace Test id "workspace:test" revision "workspace:test@1" commit "${"4".repeat(40)}" {
|
2026-09-05 12:33:52 -07:00
|
|
|
atom Subject id "atom:subject";
|
|
|
|
|
import package Runtime;
|
|
|
|
|
}
|
2026-09-15 15:23:24 -07:00
|
|
|
`,
|
|
|
|
|
);
|
2026-09-05 12:33:52 -07:00
|
|
|
await writeFile(path.join(runtime, "quixos.lock"), lock());
|
2026-09-15 15:23:24 -07:00
|
|
|
await writeFile(
|
|
|
|
|
path.join(runtime, "package.qx"),
|
|
|
|
|
`import interface Hidden;
|
2026-09-05 12:33:52 -07:00
|
|
|
package Runtime id "package:runtime" revision "package:runtime@1" { }
|
2026-09-15 15:23:24 -07:00
|
|
|
`,
|
|
|
|
|
);
|
2026-09-05 12:33:52 -07:00
|
|
|
|
|
|
|
|
await assert.rejects(
|
|
|
|
|
compileWorkspaceRepository({
|
|
|
|
|
rootDirectory: root,
|
|
|
|
|
resolveResource: async () => ({ directory: runtime }),
|
|
|
|
|
}),
|
|
|
|
|
/No resolved interface is available for lock binding Hidden/,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("workspace assembly must satisfy nominal external interfaces", async (context) => {
|
|
|
|
|
const directory = await mkdtemp(path.join(os.tmpdir(), "quixos-assembly-external-"));
|
|
|
|
|
context.after(() => rm(directory, { recursive: true, force: true }));
|
|
|
|
|
const root = path.join(directory, "root");
|
|
|
|
|
const runtime = path.join(directory, "runtime");
|
|
|
|
|
await Promise.all([mkdir(root), mkdir(runtime)]);
|
2026-09-15 15:23:24 -07:00
|
|
|
await writeFile(
|
|
|
|
|
path.join(root, "quixos.lock"),
|
|
|
|
|
lock(`package Runtime source {
|
2026-09-05 12:33:52 -07:00
|
|
|
repository "https://example.test/package-runtime.git";
|
|
|
|
|
commit "${packageCommit}";
|
2026-09-15 15:23:24 -07:00
|
|
|
}`),
|
|
|
|
|
);
|
|
|
|
|
await writeFile(
|
|
|
|
|
path.join(root, "workspace.qx"),
|
|
|
|
|
`workspace Test id "workspace:test" revision "workspace:test@1" commit "${"4".repeat(40)}" {
|
2026-09-05 12:33:52 -07:00
|
|
|
atom Subject id "atom:subject";
|
|
|
|
|
import package Runtime;
|
|
|
|
|
}
|
2026-09-15 15:23:24 -07:00
|
|
|
`,
|
|
|
|
|
);
|
2026-09-05 12:33:52 -07:00
|
|
|
await writeFile(path.join(runtime, "quixos.lock"), lock());
|
2026-09-15 15:23:24 -07:00
|
|
|
await writeFile(
|
|
|
|
|
path.join(runtime, "package.qx"),
|
|
|
|
|
`external interface Named revision "interface:named@1";
|
2026-09-05 12:33:52 -07:00
|
|
|
package Runtime id "package:runtime" revision "package:runtime@1" { }
|
2026-09-15 15:23:24 -07:00
|
|
|
`,
|
|
|
|
|
);
|
2026-09-05 12:33:52 -07:00
|
|
|
|
|
|
|
|
await assert.rejects(
|
|
|
|
|
compileWorkspaceRepository({
|
|
|
|
|
rootDirectory: root,
|
|
|
|
|
resolveResource: async () => ({ directory: runtime }),
|
|
|
|
|
}),
|
|
|
|
|
/requires external interface Named \(interface:named@1\)/,
|
|
|
|
|
);
|
|
|
|
|
});
|
2026-09-22 15:41:27 -07:00
|
|
|
|
|
|
|
|
test("resource-owned fragment imports compile through the locked package closure", async (context) => {
|
|
|
|
|
const directory = await mkdtemp(path.join(os.tmpdir(), "quixos-fragment-assembly-"));
|
|
|
|
|
context.after(() => rm(directory, { recursive: true, force: true }));
|
|
|
|
|
const dirs = Object.fromEntries(["item", "rows", "screen"].map((name) => [name, path.join(directory, name)]));
|
|
|
|
|
await Promise.all(Object.values(dirs).map((dir) => mkdir(dir)));
|
|
|
|
|
const dependency = (kind: string, name: string) =>
|
|
|
|
|
`${kind} ${name} source {repository "https://example.test/${name.toLowerCase()}.git"; commit "${namedCommit}";}`;
|
|
|
|
|
await writeFile(path.join(dirs.item!, "quixos.lock"), lock());
|
|
|
|
|
await writeFile(
|
|
|
|
|
path.join(dirs.item!, "interface.qx"),
|
2026-09-28 20:29:21 -07:00
|
|
|
`interface Item id "item" revision "item@1" {queryable value title id "title" : string {get id "title:get" execution snapshot;}}`,
|
2026-09-22 15:41:27 -07:00
|
|
|
);
|
|
|
|
|
await writeFile(path.join(dirs.rows!, "quixos.lock"), lock(dependency("interface", "Item")));
|
|
|
|
|
await writeFile(
|
|
|
|
|
path.join(dirs.rows!, "package.qx"),
|
|
|
|
|
`import interface Item; package Rows id "rows" revision "rows@1" {query fragment Row id "row" root Item document "row.graphql" fragment "Row";}`,
|
|
|
|
|
);
|
|
|
|
|
await writeFile(path.join(dirs.rows!, "row.graphql"), "fragment Row on Item {_camino {ref} title}");
|
|
|
|
|
await writeFile(
|
|
|
|
|
path.join(dirs.screen!, "quixos.lock"),
|
|
|
|
|
lock(dependency("interface", "Item") + dependency("package", "Rows")),
|
|
|
|
|
);
|
|
|
|
|
await writeFile(
|
|
|
|
|
path.join(dirs.screen!, "package.qx"),
|
|
|
|
|
`import interface Item; import package Rows; package Screen id "screen" revision "screen@1" {
|
|
|
|
|
query Inputs id "inputs" root Item document "screen.graphql" operation "Screen" {replica; fragments Rows.Row;}
|
|
|
|
|
query View id "view" root Item document "screen.graphql" operation "Screen" {local from Inputs; fragments Rows.Row;}
|
|
|
|
|
}`,
|
|
|
|
|
);
|
|
|
|
|
await writeFile(path.join(dirs.screen!, "screen.graphql"), "query Screen {root {...Row}}");
|
2026-10-01 01:37:46 -07:00
|
|
|
const options = {
|
2026-09-22 15:41:27 -07:00
|
|
|
rootDirectory: dirs.screen!,
|
2026-10-01 01:37:46 -07:00
|
|
|
kind: "package" as const,
|
|
|
|
|
source: { resolver: "git" as const, repository: "https://example.test/screen.git", commit: packageCommit },
|
|
|
|
|
resolveResource: async (source: { repository: string }) => {
|
2026-09-22 15:41:27 -07:00
|
|
|
const name = new URL(source.repository).pathname.slice(1, -4);
|
|
|
|
|
assert.ok(dirs[name], name);
|
|
|
|
|
return { directory: dirs[name]! };
|
|
|
|
|
},
|
2026-10-01 01:37:46 -07:00
|
|
|
};
|
|
|
|
|
const result = await compileCapabilityResourceRepository(options);
|
2026-09-22 15:41:27 -07:00
|
|
|
assert.equal(result.resource.kind, "package");
|
|
|
|
|
if (result.resource.kind !== "package") throw Error("expected package");
|
|
|
|
|
const view = result.resource.revision.checkedQueries!.find((q) => q.declaration.id === "view")!;
|
|
|
|
|
assert.equal(view.selection[0]!.selection[0]!.fragmentSpread?.package, "rows@1");
|
|
|
|
|
assert.equal(result.resources.length, 3);
|
2026-10-01 01:37:46 -07:00
|
|
|
const checked = new Map(result.resources.map((node) => [node.key, node.resource]));
|
|
|
|
|
let generated = 0;
|
|
|
|
|
const reused = await compileCapabilityResourceRepository({
|
|
|
|
|
...options,
|
|
|
|
|
checkedResource: async (kind, source) => {
|
|
|
|
|
if (source.repository === options.source.repository) return undefined;
|
|
|
|
|
const resource = checked.get(`${kind}\0${source.repository}\0${source.commit}`);
|
|
|
|
|
return resource?.kind === "build" ? undefined : resource;
|
|
|
|
|
},
|
|
|
|
|
generateQueries: async () => {
|
|
|
|
|
generated++;
|
|
|
|
|
return { queries: [], documents: new Map() };
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
assert.deepEqual(reused, result, "cached imported fragments and full checking produce identical contracts");
|
|
|
|
|
assert.equal(generated, 1, "only the uncached package checks its queries");
|
|
|
|
|
await writeFile(path.join(dirs.screen!, "screen.graphql"), "query Screen {root {unknownField}}");
|
|
|
|
|
await assert.rejects(
|
|
|
|
|
compileCapabilityResourceRepository({
|
|
|
|
|
...options,
|
|
|
|
|
source: { ...options.source, commit: "5".repeat(40) },
|
|
|
|
|
checkedResource: async (kind, source) => {
|
|
|
|
|
const resource = checked.get(`${kind}\0${source.repository}\0${source.commit}`);
|
|
|
|
|
return resource?.kind === "build" ? undefined : resource;
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
/unknownField/,
|
|
|
|
|
);
|
2026-09-22 15:41:27 -07:00
|
|
|
});
|