216 lines
9.9 KiB
TypeScript
216 lines
9.9 KiB
TypeScript
import assert from "node:assert/strict";
|
|||
|
|
import { execFile as callback } from "node:child_process";
|
||
|
|
import { promisify } from "node:util";
|
||
|
|
import { mkdtemp, mkdir, writeFile, readFile, rm, symlink } from "node:fs/promises";
|
||
|
|
import path from "node:path";
|
||
|
|
import os from "node:os";
|
||
|
|
import test from "node:test";
|
||
|
|
import { pathToFileURL } from "node:url";
|
||
|
|
import { materializeWorkspaceSources, workspaceSourcePin } from "../src/capability-language/source-closure.js";
|
||
|
|
import { prepareDevelopmentSources } from "../src/capability-language/development-sources.js";
|
||
|
|
import { backupWorkspaceSources, restoreWorkspaceSources } from "../src/capability-language/source-backup.js";
|
||
|
|
|
||
|
|
const exec = promisify(callback);
|
||
|
|
const lock = (resources: string) => `quixos-lock version 1 {
|
||
|
|
quixos source { repository "https://unavailable.test/toolchain.git"; commit "${"a".repeat(40)}"; }
|
||
|
|
${resources}
|
||
|
|
}`;
|
||
|
|
test("exact source closure recovers through explicitly selected bundles, preserves history and rejects edits", async (context) => {
|
||
|
|
const temporary = await mkdtemp(path.join(os.tmpdir(), "qx-closure-"));
|
||
|
|
context.after(() => rm(temporary, { recursive: true, force: true }));
|
||
|
|
const locations: Record<string, string> = {};
|
||
|
|
const repository = async (name: string, resources: string) => {
|
||
|
|
const directory = path.join(temporary, name);
|
||
|
|
await mkdir(directory);
|
||
|
|
const git = async (...args: string[]) => (await exec("git", args, { cwd: directory })).stdout.trim();
|
||
|
|
await git("init");
|
||
|
|
await writeFile(path.join(directory, "quixos.lock"), lock(""));
|
||
|
|
await git("add", ".");
|
||
|
|
const commit = async () => {
|
||
|
|
await git("-c", "user.name=Test", "-c", "user.email=test@example.test", "commit", "-am", "Source");
|
||
|
|
return await git("rev-parse", "HEAD");
|
||
|
|
};
|
||
|
|
const parent = await commit();
|
||
|
|
await writeFile(path.join(directory, "quixos.lock"), lock(resources));
|
||
|
|
await writeFile(path.join(directory, "marker"), name);
|
||
|
|
await git("add", ".");
|
||
|
|
const rev = await commit();
|
||
|
|
await git("tag", `quixos-reachability/${rev}`);
|
||
|
|
const bundle = path.join(temporary, `${name}.bundle`);
|
||
|
|
await git("bundle", "create", bundle, `refs/tags/quixos-reachability/${rev}`);
|
||
|
|
const source = { repository: `https://unavailable.test/${name}.git`, commit: rev };
|
||
|
|
locations[source.repository] = pathToFileURL(bundle).href;
|
||
|
|
return { ...source, parent };
|
||
|
|
};
|
||
|
|
const common = await repository("common", "");
|
||
|
|
const dependency = `interface Common source { repository "${common.repository}"; commit "${common.commit}"; }`;
|
||
|
|
const left = await repository("left", dependency),
|
||
|
|
right = await repository("right", dependency);
|
||
|
|
const root = await repository(
|
||
|
|
"root",
|
||
|
|
`
|
||
|
|
package Left source { repository "${left.repository}"; commit "${left.commit}"; }
|
||
|
|
package Right source { repository "${right.repository}"; commit "${right.commit}"; }`,
|
||
|
|
);
|
||
|
|
const directory = path.join(temporary, "checkout");
|
||
|
|
const closure = await materializeWorkspaceSources({ source: root, directory, locations, concurrency: 2 });
|
||
|
|
assert.deepEqual(
|
||
|
|
await materializeWorkspaceSources({ source: root, directory, repositoryPrefix: "https://unavailable.test/" }),
|
||
|
|
closure,
|
||
|
|
);
|
||
|
|
// Allow this root but not its dependencies: reject them before Git acquisition.
|
||
|
|
const scopedRoot = { ...root, repository: "https://unavailable.test/selected/root.git" };
|
||
|
|
await assert.rejects(
|
||
|
|
materializeWorkspaceSources({
|
||
|
|
source: scopedRoot,
|
||
|
|
directory: path.join(temporary, "scoped"),
|
||
|
|
locations: { ...locations, [scopedRoot.repository]: locations[root.repository] },
|
||
|
|
repositoryPrefix: "https://unavailable.test/selected/",
|
||
|
|
}),
|
||
|
|
/outside the acquisition boundary/,
|
||
|
|
);
|
||
|
|
await assert.rejects(
|
||
|
|
materializeWorkspaceSources({ source: root, directory, repositoryPrefix: "https://unavailable.test/root.git/" }),
|
||
|
|
/outside the acquisition boundary/,
|
||
|
|
);
|
||
|
|
assert.equal(closure.resources.length, 4, "diamond dependency is materialized once");
|
||
|
|
assert.deepEqual(JSON.parse(await readFile(path.join(directory, "sources.json"), "utf8")), closure);
|
||
|
|
const rootEntry = closure.resources.find((entry) => entry.kind === "workspace")!;
|
||
|
|
const rootDirectory = path.join(directory, rootEntry.directory);
|
||
|
|
const rootLock = path.join(rootDirectory, "quixos.lock");
|
||
|
|
const rootBody = await readFile(rootLock, "utf8");
|
||
|
|
const outsideLock = path.join(temporary, "outside.lock");
|
||
|
|
await writeFile(outsideLock, rootBody);
|
||
|
|
// Exercise the shared loader directly: committed root symlinks must not let
|
||
|
|
// privileged source inspection read outside the acquired repository.
|
||
|
|
const { loadQuixosLock } = await import("../src/resource-lock/index.js");
|
||
|
|
const linkedLock = path.join(temporary, "linked.lock");
|
||
|
|
await symlink(outsideLock, linkedLock);
|
||
|
|
await assert.rejects(loadQuixosLock(linkedLock), /root locks must be ordinary/);
|
||
|
|
const backup = path.join(temporary, "retained");
|
||
|
|
const manifest = await backupWorkspaceSources({ source: root, directory, output: backup });
|
||
|
|
assert.equal(manifest.repositories.length, 4);
|
||
|
|
const restored = await restoreWorkspaceSources({ backup, directory: path.join(temporary, "restored") });
|
||
|
|
assert.deepEqual(restored, closure, "Source-only recovery retains canonical identity without an available forge");
|
||
|
|
await assert.rejects(backupWorkspaceSources({ source: root, directory, output: backup }), /must not exist/);
|
||
|
|
const missing = {
|
||
|
|
...manifest,
|
||
|
|
repositories: manifest.repositories.filter((entry) => entry.repository !== common.repository),
|
||
|
|
};
|
||
|
|
await writeFile(path.join(backup, "backup.json"), JSON.stringify(missing));
|
||
|
|
await assert.rejects(
|
||
|
|
restoreWorkspaceSources({ backup, directory: path.join(temporary, "incomplete") }),
|
||
|
|
/omits required repository/,
|
||
|
|
);
|
||
|
|
await writeFile(path.join(backup, "backup.json"), JSON.stringify(manifest));
|
||
|
|
await writeFile(path.join(backup, manifest.repositories[0].bundle), "damaged backup");
|
||
|
|
await assert.rejects(
|
||
|
|
restoreWorkspaceSources({ backup, directory: path.join(temporary, "damaged") }),
|
||
|
|
/integrity mismatch/,
|
||
|
|
);
|
||
|
|
assert.equal((await exec("git", ["rev-parse", "HEAD^"], { cwd: rootDirectory })).stdout.trim(), root.parent);
|
||
|
|
assert.equal(
|
||
|
|
(await exec("git", ["remote", "get-url", "origin"], { cwd: rootDirectory })).stdout.trim(),
|
||
|
|
root.repository,
|
||
|
|
);
|
||
|
|
// Cached recovery does not need the primary or the backup.
|
||
|
|
assert.deepEqual(await materializeWorkspaceSources({ source: root, directory }), closure);
|
||
|
|
const pinFile = path.join(temporary, "source.json");
|
||
|
|
await writeFile(pinFile, JSON.stringify(root));
|
||
|
|
const cli = path.resolve(import.meta.dirname, "../src/capability-language/tool-cli.js");
|
||
|
|
const command = await exec(process.execPath, [cli, "source-checkout", pinFile, "--dir", directory]);
|
||
|
|
assert.deepEqual(JSON.parse(command.stdout), closure, "Protocol CLI preserves exact cached source identity");
|
||
|
|
const scoped = await exec(process.execPath, [
|
||
|
|
cli,
|
||
|
|
"source-checkout",
|
||
|
|
pinFile,
|
||
|
|
"--dir",
|
||
|
|
directory,
|
||
|
|
"--repository-prefix",
|
||
|
|
"https://unavailable.test/",
|
||
|
|
]);
|
||
|
|
assert.deepEqual(JSON.parse(scoped.stdout), closure);
|
||
|
|
const cliBackup = path.join(temporary, "cli-backup");
|
||
|
|
const backupCommand = await exec(process.execPath, [
|
||
|
|
cli,
|
||
|
|
"source-backup",
|
||
|
|
pinFile,
|
||
|
|
"--dir",
|
||
|
|
directory,
|
||
|
|
"--output",
|
||
|
|
cliBackup,
|
||
|
|
]);
|
||
|
|
assert.equal(JSON.parse(backupCommand.stdout).repositories.length, 4);
|
||
|
|
const restoreCommand = await exec(process.execPath, [
|
||
|
|
cli,
|
||
|
|
"source-restore",
|
||
|
|
cliBackup,
|
||
|
|
"--dir",
|
||
|
|
path.join(temporary, "cli-restore"),
|
||
|
|
]);
|
||
|
|
assert.deepEqual(JSON.parse(restoreCommand.stdout), closure);
|
||
|
|
const developmentCommand = await exec(process.execPath, [
|
||
|
|
cli,
|
||
|
|
"source-develop",
|
||
|
|
pinFile,
|
||
|
|
"--dir",
|
||
|
|
path.join(temporary, "cli-developer"),
|
||
|
|
"--cache",
|
||
|
|
directory,
|
||
|
|
]);
|
||
|
|
const developmentResult = JSON.parse(developmentCommand.stdout);
|
||
|
|
assert.equal(developmentResult.repositories, 4);
|
||
|
|
assert.deepEqual(developmentResult.source, closure.root);
|
||
|
|
const a = await prepareDevelopmentSources({
|
||
|
|
source: root,
|
||
|
|
cacheDirectory: directory,
|
||
|
|
directory: path.join(temporary, "a"),
|
||
|
|
});
|
||
|
|
const b = await prepareDevelopmentSources({
|
||
|
|
source: root,
|
||
|
|
cacheDirectory: directory,
|
||
|
|
directory: path.join(temporary, "b"),
|
||
|
|
});
|
||
|
|
assert.notEqual(a.workspaceId, b.workspaceId);
|
||
|
|
const aRoot = path.join(a.directory, "root"),
|
||
|
|
bRoot = path.join(b.directory, "root");
|
||
|
|
assert.equal((await exec("git", ["remote", "get-url", "origin"], { cwd: aRoot })).stdout.trim(), root.repository);
|
||
|
|
assert.equal(
|
||
|
|
await readFile(path.join(aRoot, "quixos.lock"), "utf8"),
|
||
|
|
await readFile(path.join(rootDirectory, "quixos.lock"), "utf8"),
|
||
|
|
);
|
||
|
|
await exec("git", ["cat-file", "-e", root.parent], { cwd: aRoot });
|
||
|
|
await writeFile(path.join(aRoot, "marker"), "Independent developer change");
|
||
|
|
assert.equal(await readFile(path.join(bRoot, "marker"), "utf8"), "root");
|
||
|
|
assert.equal(await readFile(path.join(rootDirectory, "marker"), "utf8"), "root");
|
||
|
|
await assert.rejects(
|
||
|
|
prepareDevelopmentSources({ source: root, cacheDirectory: directory, directory: a.directory }),
|
||
|
|
/never overwritten/,
|
||
|
|
);
|
||
|
|
await writeFile(path.join(rootDirectory, "marker"), "edited");
|
||
|
|
await assert.rejects(materializeWorkspaceSources({ source: root, directory, locations }), /modified/);
|
||
|
|
await assert.rejects(
|
||
|
|
materializeWorkspaceSources({
|
||
|
|
source: root,
|
||
|
|
directory: path.join(temporary, "bounded"),
|
||
|
|
locations,
|
||
|
|
maxRepositories: 2,
|
||
|
|
}),
|
||
|
|
/repository limit/,
|
||
|
|
);
|
||
|
|
await assert.rejects(
|
||
|
|
materializeWorkspaceSources({
|
||
|
|
source: root,
|
||
|
|
directory,
|
||
|
|
locations: { [root.repository]: "https://secret:password@example.test/root.git" },
|
||
|
|
}),
|
||
|
|
/transport substitution/,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
test("Workspace pins reject floating revisions and embedded credentials", () => {
|
||
|
|
assert.throws(() => workspaceSourcePin({ repository: "https://example.test/root.git", commit: "main" }));
|
||
|
|
assert.throws(() =>
|
||
|
|
workspaceSourcePin({ repository: "https://user:password@example.test/root.git", commit: "a".repeat(40) }),
|
||
|
|
);
|
||
|
|
});
|