303 lines
9.0 KiB
TypeScript
303 lines
9.0 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { mkdtemp, mkdir, rm, symlink, writeFile } from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import test from "node:test";
|
|
import {
|
|
formatQuixosLock,
|
|
formatQuixosLockDocument,
|
|
loadQuixosLock,
|
|
nixGitInput,
|
|
parseQuixosLock,
|
|
parseQuixosLockDocument,
|
|
resolveQuixosLock,
|
|
retentionTagForCommit,
|
|
} from "../src/resource-lock/index.js";
|
|
|
|
const quixosCommit = "1".repeat(40);
|
|
const namedCommit = "2".repeat(40);
|
|
const packageCommit = "3".repeat(64);
|
|
const fixture = `quixos-lock version 1 {
|
|
quixos source {
|
|
repository "https://gitea.example/quixos/quixos.git";
|
|
policy track-development;
|
|
ref "dev/alice/main";
|
|
commit "${quixosCommit}";
|
|
}
|
|
|
|
interface Named source {
|
|
repository "https://repos.example/alice/interface-named.git";
|
|
commit "${namedCommit}";
|
|
}
|
|
|
|
package TodoRuntime source {
|
|
repository "ssh://git@repos.example/alice/package-todo-runtime.git";
|
|
commit "${packageCommit}";
|
|
}
|
|
}
|
|
`;
|
|
|
|
test("parses and canonically formats a Git-only repository lock", () => {
|
|
const parsed = parseQuixosLock(fixture, "quixos.lock");
|
|
assert.equal(parsed.ok, true);
|
|
if (!parsed.ok) return;
|
|
assert.equal(parsed.lock.formatVersion, 1);
|
|
assert.equal(parsed.lock.quixos.commit, quixosCommit);
|
|
assert.equal(parsed.lock.quixos.policy, "track-development");
|
|
assert.equal(parsed.lock.quixos.ref, "dev/alice/main");
|
|
assert.deepEqual(
|
|
parsed.lock.resources.map(({ kind, binding }) => ({ kind, binding })),
|
|
[
|
|
{ kind: "interface", binding: "Named" },
|
|
{ kind: "package", binding: "TodoRuntime" },
|
|
],
|
|
);
|
|
assert.deepEqual(parseQuixosLock(formatQuixosLock(parsed.lock)), {
|
|
ok: true,
|
|
lock: parsed.lock,
|
|
diagnostics: [],
|
|
});
|
|
});
|
|
|
|
test("validates pinned Quixos selections without imposing policy on resource locks", () => {
|
|
const mismatch = parseQuixosLockDocument(`quixos-lock version 1 {
|
|
quixos source {
|
|
repository "https://gitea.example/quixos/quixos.git";
|
|
policy pinned;
|
|
ref "${namedCommit}";
|
|
commit "${quixosCommit}";
|
|
}
|
|
}`);
|
|
assert.equal(mismatch.ok, false);
|
|
if (!mismatch.ok) {
|
|
assert.equal(mismatch.diagnostics[0]?.code, "pinned-quixos-commit-mismatch");
|
|
}
|
|
|
|
const resourceBaseline = parseQuixosLockDocument(`quixos-lock version 1 {
|
|
quixos source {
|
|
repository "https://gitea.example/quixos/quixos.git";
|
|
commit "${quixosCommit}";
|
|
}
|
|
}`);
|
|
assert.equal(resourceBaseline.ok, true);
|
|
});
|
|
|
|
test("derives immutable retention and Nix inputs without storing extra identity", () => {
|
|
const source = {
|
|
resolver: "git" as const,
|
|
repository: "https://repos.example/alice/interface-named.git",
|
|
commit: namedCommit,
|
|
};
|
|
const ref = `refs/tags/quixos-reachability/${namedCommit}`;
|
|
assert.equal(retentionTagForCommit(namedCommit.toUpperCase()), ref);
|
|
assert.deepEqual(nixGitInput(source), {
|
|
type: "git",
|
|
url: source.repository,
|
|
ref,
|
|
rev: namedCommit,
|
|
});
|
|
});
|
|
|
|
test("rejects mutable revisions, embedded credentials, and duplicate bindings", () => {
|
|
const parsed = parseQuixosLock(`quixos-lock version 1 {
|
|
quixos source {
|
|
repository "https://user:secret@example/quixos.git";
|
|
commit "main";
|
|
}
|
|
interface Named source {
|
|
repository "file:///tmp/named";
|
|
commit "${namedCommit}";
|
|
}
|
|
interface Named source {
|
|
repository "https://repos.example/named.git?ref=main";
|
|
commit "${namedCommit}";
|
|
}
|
|
}`);
|
|
assert.equal(parsed.ok, false);
|
|
if (parsed.ok) return;
|
|
assert.deepEqual(
|
|
new Set(parsed.diagnostics.map((entry) => entry.code)),
|
|
new Set([
|
|
"invalid-git-commit",
|
|
"embedded-git-credential",
|
|
"unsupported-git-transport",
|
|
"decorated-git-repository",
|
|
"duplicate-resource-binding",
|
|
]),
|
|
);
|
|
});
|
|
|
|
test("resolves root-relative lock fragments into one deterministic resource closure", async () => {
|
|
const root = `quixos-lock version 1 {
|
|
quixos source {
|
|
repository "https://gitea.example/quixos/quixos.git";
|
|
commit "${quixosCommit}";
|
|
}
|
|
import "locks/web-studio.lock";
|
|
import "locks/domain.lock";
|
|
package RootRuntime source {
|
|
repository "https://repos.example/alice/package-root.git";
|
|
commit "${packageCommit}";
|
|
}
|
|
}`;
|
|
const sources = new Map([
|
|
[
|
|
"locks/web-studio.lock",
|
|
`quixos-lock fragment version 1 {
|
|
import "locks/shared.lock";
|
|
interface Placeable source {
|
|
repository "https://repos.example/alice/interface-placeable.git";
|
|
commit "${namedCommit}";
|
|
}
|
|
}`,
|
|
],
|
|
[
|
|
"locks/shared.lock",
|
|
`quixos-lock fragment version 1 {
|
|
interface Named source {
|
|
repository "https://repos.example/alice/interface-named.git";
|
|
commit "${namedCommit}";
|
|
}
|
|
}`,
|
|
],
|
|
[
|
|
"locks/domain.lock",
|
|
`quixos-lock fragment version 1 {
|
|
package TodoRuntime source {
|
|
repository "https://repos.example/alice/package-todo.git";
|
|
commit "${packageCommit}";
|
|
}
|
|
}`,
|
|
],
|
|
]);
|
|
const result = await resolveQuixosLock(root, async (relativePath) => {
|
|
const source = sources.get(relativePath);
|
|
if (!source) throw new Error("missing fixture");
|
|
return source;
|
|
});
|
|
assert.equal(result.ok, true);
|
|
if (!result.ok) return;
|
|
assert.deepEqual(result.lock.sourceFiles, [
|
|
"quixos.lock",
|
|
"locks/web-studio.lock",
|
|
"locks/shared.lock",
|
|
"locks/domain.lock",
|
|
]);
|
|
assert.deepEqual(
|
|
result.lock.resources.map(({ kind, binding }) => ({ kind, binding })),
|
|
[
|
|
{ kind: "package", binding: "RootRuntime" },
|
|
{ kind: "interface", binding: "Placeable" },
|
|
{ kind: "interface", binding: "Named" },
|
|
{ kind: "package", binding: "TodoRuntime" },
|
|
],
|
|
);
|
|
});
|
|
|
|
test("lock fragments format canonically and cannot redeclare the Quixos source", () => {
|
|
const document = {
|
|
kind: "fragment" as const,
|
|
formatVersion: 1 as const,
|
|
imports: ["locks/shared.lock"],
|
|
resources: [
|
|
{
|
|
kind: "interface" as const,
|
|
binding: "Named",
|
|
source: {
|
|
resolver: "git" as const,
|
|
repository: "https://repos.example/alice/interface-named.git",
|
|
commit: namedCommit,
|
|
},
|
|
},
|
|
],
|
|
};
|
|
assert.deepEqual(parseQuixosLockDocument(formatQuixosLockDocument(document)), {
|
|
ok: true,
|
|
document,
|
|
diagnostics: [],
|
|
});
|
|
const invalid = parseQuixosLockDocument(
|
|
`quixos-lock fragment version 1 {
|
|
quixos source {
|
|
repository "https://gitea.example/quixos/quixos.git";
|
|
commit "${quixosCommit}";
|
|
}
|
|
}`,
|
|
"bad.lock",
|
|
);
|
|
assert.equal(invalid.ok, false);
|
|
if (!invalid.ok) assert.equal(invalid.diagnostics[0]?.code, "fragment-has-quixos-source");
|
|
});
|
|
|
|
test("lock imports reject traversal, cycles, root documents, and cross-file binding collisions", async () => {
|
|
const invalidPath = parseQuixosLockDocument(
|
|
`quixos-lock fragment version 1 {
|
|
import "../outside.lock";
|
|
}`,
|
|
"bad-path.lock",
|
|
);
|
|
assert.equal(invalidPath.ok, false);
|
|
if (!invalidPath.ok) assert.equal(invalidPath.diagnostics[0]?.code, "invalid-import-path");
|
|
|
|
const root = `quixos-lock version 1 {
|
|
quixos source {
|
|
repository "https://gitea.example/quixos/quixos.git";
|
|
commit "${quixosCommit}";
|
|
}
|
|
import "a.lock";
|
|
import "root-again.lock";
|
|
interface Named source {
|
|
repository "https://repos.example/alice/interface-named.git";
|
|
commit "${namedCommit}";
|
|
}
|
|
}`;
|
|
const sources = new Map([
|
|
[
|
|
"a.lock",
|
|
`quixos-lock fragment version 1 {
|
|
import "b.lock";
|
|
interface Named source {
|
|
repository "https://repos.example/alice/interface-named-copy.git";
|
|
commit "${namedCommit}";
|
|
}
|
|
}`,
|
|
],
|
|
["b.lock", `quixos-lock fragment version 1 { import "a.lock"; }`],
|
|
["root-again.lock", root],
|
|
]);
|
|
const result = await resolveQuixosLock(root, async (relativePath) => sources.get(relativePath) ?? "");
|
|
assert.equal(result.ok, false);
|
|
if (!result.ok) {
|
|
assert.deepEqual(
|
|
new Set(result.diagnostics.map(({ code }) => code)),
|
|
new Set(["duplicate-resource-binding", "import-cycle", "imported-root-lock"]),
|
|
);
|
|
}
|
|
});
|
|
|
|
test("file loading rejects a symlink in any import path component", async (context) => {
|
|
const directory = await mkdtemp(path.join(os.tmpdir(), "quixos-lock-test-"));
|
|
context.after(() => rm(directory, { recursive: true, force: true }));
|
|
const outside = path.join(directory, "outside");
|
|
await mkdir(outside);
|
|
await writeFile(path.join(outside, "fragment.lock"), "quixos-lock fragment version 1 {}\n");
|
|
await symlink(outside, path.join(directory, "linked"), "dir");
|
|
await writeFile(
|
|
path.join(directory, "quixos.lock"),
|
|
`quixos-lock version 1 {
|
|
quixos source {
|
|
repository "https://gitea.example/quixos/quixos.git";
|
|
commit "${quixosCommit}";
|
|
}
|
|
import "linked/fragment.lock";
|
|
}`,
|
|
);
|
|
|
|
const result = await loadQuixosLock(path.join(directory, "quixos.lock"));
|
|
assert.equal(result.ok, false);
|
|
if (!result.ok) {
|
|
assert.equal(result.diagnostics[0]?.code, "import-read-failed");
|
|
assert.match(result.diagnostics[0]?.message ?? "", /symbolic links/);
|
|
}
|
|
});
|