Candidate quackerjack/fed-r5-r5a74cd9f87onc3d88840c, manifest of 6 paths (CANDIDATE.sha256). Duckworth APPROVE round 5, rev 3026 (#1700 c30184). C1 gate on Sage's slot by Quackerjack on the merge ofa74cd9f87ontoa849d89b7(3b86180b9), finished 01:56:24Z: cli suite 771/771, skip 0 (run 2; run 1 red on a too-long gate TMPDIR, harness only), every scripts/test-*.sh rc 0 with Docker up, mutants M1-M13 killed by their named tests. Landed onto9c9732983with the candidate's blobs and modes for the 6 paths; c3d88840c..9c9732983 touches none of them.
315 lines
15 KiB
JavaScript
315 lines
15 KiB
JavaScript
// `mosaic access` (FED R5, #414). Every write goes to a scratch config dir and a
|
|
// scratch data root. The success paths run on a stand-in human CLI (fakeCli).
|
|
// The one test that runs the real human-cli.mjs does it on a scratch bus from
|
|
// agent ancestry, so the bus must refuse, and nothing is written.
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { spawnSync } from "node:child_process";
|
|
import * as nodeFs from "node:fs";
|
|
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { startBroker } from "../../bus/src/runtime.mjs";
|
|
import { accessCommand, accessContextFor, withLock } from "../src/access.mjs";
|
|
import { CliError } from "../src/errors.mjs";
|
|
import { socketPath } from "../src/config.mjs";
|
|
import { HUMAN_CLI, humanTransport } from "../src/transport.mjs";
|
|
import { fakeCli, humanEnv, io, tmp, fixture } from "./helpers.mjs";
|
|
import { systemFor, writeJson } from "../../business/tests/helpers.mjs";
|
|
|
|
const PROJECT = "3f1c2a64-0d8e-4b7a-9c55-1a2b3c4d5e6f";
|
|
const ROWS = [{ id: PROJECT, name: "hub", repo: null }];
|
|
const CONTEXT = {
|
|
providers: ["authentik"],
|
|
peers: [],
|
|
businesses: { acme: { human: "jason", instances: ["coder"] } },
|
|
ssoSubjects: [{ provider: "authentik", sub: "jason-sub" }],
|
|
};
|
|
const DOC = {
|
|
accessVersion: 1,
|
|
revision: 1,
|
|
users: [{ id: "alice", home: "local", subjects: [{ provider: "authentik", sub: "alice-sub" }] }],
|
|
grants: [],
|
|
};
|
|
|
|
// The stand-in human CLI. It answers project.list with ROWS, or refuses like a bus
|
|
// that sees no human shell when FAKE_MODE is "refuse".
|
|
const BODY = `
|
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
const input = JSON.parse(readFileSync(0, "utf8"));
|
|
writeFileSync(process.env.FAKE_LOG, JSON.stringify(input));
|
|
if (process.env.FAKE_MODE === "refuse") { process.stderr.write("human-required\\n"); process.exit(3); }
|
|
process.stdout.write(JSON.stringify(${JSON.stringify(ROWS)}));
|
|
`;
|
|
|
|
function setup(t, { mode = "ok", doc = DOC } = {}) {
|
|
const root = tmp(t, "mosaic-access-");
|
|
const config = join(root, "config");
|
|
mkdirSync(config, { recursive: true });
|
|
const dataRoot = join(root, "data");
|
|
const log = join(root, "fake.log");
|
|
const cli = fakeCli(root, BODY);
|
|
const env = humanEnv({ FAKE_LOG: log, FAKE_MODE: mode });
|
|
const file = join(config, "access.json");
|
|
if (doc) writeFileSync(file, JSON.stringify(doc, null, 2), { mode: 0o600 });
|
|
const deps = {
|
|
system: { dataRoot },
|
|
configDir: config,
|
|
contextFor: () => CONTEXT,
|
|
now: () => new Date("2026-10-20T20:00:00Z"),
|
|
transport: (business) => humanTransport({ socket: join(root, "bus.sock"), business, env, cli, timeoutMs: 5000 }),
|
|
};
|
|
const receipts = () => (existsSync(join(dataRoot, "state", "access-log.jsonl")) ? readFileSync(join(dataRoot, "state", "access-log.jsonl"), "utf8") : "");
|
|
return { root, config, file, deps, env, log, receipts, read: () => JSON.parse(readFileSync(file, "utf8")) };
|
|
}
|
|
|
|
const GRANT_BASE = ["grant", "--business", "acme", "--user", "alice"];
|
|
const GRANT = [...GRANT_BASE, "--projects", PROJECT, "--instances", "coder", "--level", "collaborator"];
|
|
|
|
async function run(args, deps, env = humanEnv()) {
|
|
const i = io(env);
|
|
await accessCommand(args, i, deps);
|
|
return i.out.text;
|
|
}
|
|
|
|
async function refusal(promise, pattern) {
|
|
await assert.rejects(promise, (e) => e instanceof CliError && pattern.test(e.message));
|
|
return promise.catch((e) => e);
|
|
}
|
|
|
|
test("list prints the revision and the users", async (t) => {
|
|
const s = setup(t);
|
|
const out = await run(["list", "--business", "acme"], s.deps);
|
|
assert.match(out, /access file revision 1/);
|
|
assert.match(out, /user alice home local/);
|
|
});
|
|
|
|
test("grant writes one revision up, a receipt line, and the id in the file", async (t) => {
|
|
const s = setup(t);
|
|
const out = await run([...GRANT, "--revision", "1"], s.deps);
|
|
assert.match(out, /access grant: g-[0-9a-f]{8} at acme, revision 2/);
|
|
const doc = s.read();
|
|
assert.equal(doc.revision, 2);
|
|
assert.equal(doc.grants.length, 1);
|
|
assert.deepEqual(doc.grants[0].projects, [PROJECT]);
|
|
const [line] = s.receipts().trim().split("\n").map((l) => JSON.parse(l));
|
|
assert.equal(line.verb, "grant");
|
|
assert.equal(line.revisionBefore, 1);
|
|
assert.equal(line.revisionAfter, 2);
|
|
assert.equal(line.actor, "human");
|
|
});
|
|
|
|
test("a viewer grant writes the viewer level, one revision up", async (t) => {
|
|
const s = setup(t);
|
|
await run([...GRANT_BASE, "--projects", PROJECT, "--instances", "coder", "--level", "viewer", "--revision", "1"], s.deps);
|
|
const doc = s.read();
|
|
assert.equal(doc.revision, 2);
|
|
assert.equal(doc.grants[0].level, "viewer");
|
|
});
|
|
|
|
test("a second write appends a second receipt and leaves the first intact", async (t) => {
|
|
const s = setup(t);
|
|
await run([...GRANT, "--revision", "1"], s.deps);
|
|
await run(["revoke", "--business", "acme", "--user", "alice", "--revision", "2"], s.deps);
|
|
const lines = s.receipts().trim().split("\n").map((l) => JSON.parse(l));
|
|
assert.deepEqual(lines.map((l) => l.verb), ["grant", "revoke"]);
|
|
assert.equal(lines[0].revisionAfter, 2);
|
|
});
|
|
|
|
test("a stale revision refuses with revision-conflict and writes nothing", async (t) => {
|
|
const s = setup(t);
|
|
await run([...GRANT, "--revision", "1"], s.deps);
|
|
const before = readFileSync(s.file, "utf8");
|
|
await refusal(run([...GRANT, "--revision", "1"], s.deps), /revision-conflict/);
|
|
assert.equal(readFileSync(s.file, "utf8"), before);
|
|
assert.equal(s.receipts().trim().split("\n").length, 1);
|
|
});
|
|
|
|
test("a live holder's lock refuses and leaves the file and the lock alone", async (t) => {
|
|
const s = setup(t);
|
|
writeFileSync(`${s.file}.lock`, `${process.pid}\n`, { mode: 0o600 });
|
|
const before = readFileSync(s.file, "utf8");
|
|
await refusal(run([...GRANT, "--revision", "1"], s.deps), /being written by another command/);
|
|
assert.equal(readFileSync(s.file, "utf8"), before);
|
|
assert.equal(readFileSync(`${s.file}.lock`, "utf8"), `${process.pid}\n`);
|
|
assert.equal(readdirSync(s.config).filter((f) => f.startsWith(".access-")).length, 0);
|
|
});
|
|
|
|
test("an empty or unreadable lock refuses, since its holder may still be writing", async (t) => {
|
|
const s = setup(t);
|
|
writeFileSync(`${s.file}.lock`, "");
|
|
await refusal(run([...GRANT, "--revision", "1"], s.deps), /is unreadable or not a pid/);
|
|
writeFileSync(`${s.file}.lock`, "not-a-pid\n");
|
|
await refusal(run([...GRANT, "--revision", "1"], s.deps), /is unreadable or not a pid/);
|
|
assert.equal(existsSync(`${s.file}.lock`), true);
|
|
});
|
|
|
|
test("a lock left by a dead pid refuses, names the pid, and is not reclaimed", async (t) => {
|
|
const s = setup(t);
|
|
const dead = spawnSync(process.execPath, ["-e", ""]);
|
|
writeFileSync(`${s.file}.lock`, `${dead.pid}\n`, { mode: 0o600 });
|
|
const before = readFileSync(s.file, "utf8");
|
|
await refusal(run([...GRANT, "--revision", "1"], s.deps), new RegExp(`${s.file}\\.lock is left by pid ${dead.pid}, which is not running`));
|
|
assert.equal(readFileSync(s.file, "utf8"), before);
|
|
assert.equal(readFileSync(`${s.file}.lock`, "utf8"), `${dead.pid}\n`);
|
|
assert.equal(s.receipts(), "");
|
|
});
|
|
|
|
test("a failed rename leaves the old bytes, removes the temp file, and writes no receipt", async (t) => {
|
|
const s = setup(t);
|
|
const before = readFileSync(s.file, "utf8");
|
|
const fs = { ...nodeFs, renameSync: () => { throw Object.assign(new Error("injected rename failure"), { code: "EXDEV" }); } };
|
|
await assert.rejects(run([...GRANT, "--revision", "1"], { ...s.deps, fs }), /injected rename failure/);
|
|
assert.equal(readFileSync(s.file, "utf8"), before);
|
|
assert.deepEqual(readdirSync(s.config).filter((f) => f.startsWith(".access-")), []);
|
|
assert.equal(existsSync(`${s.file}.lock`), false);
|
|
assert.equal(s.receipts(), "");
|
|
});
|
|
|
|
test("a group-writable access file refuses with exit 3, names the file, and changes nothing", async (t) => {
|
|
const s = setup(t);
|
|
chmodSync(s.file, 0o664);
|
|
const before = readFileSync(s.file, "utf8");
|
|
const err = await refusal(run([...GRANT, "--revision", "1"], s.deps), /must not be writable by group or other/);
|
|
assert.equal(err.exitCode, 3);
|
|
assert.match(err.message, new RegExp(s.file.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
|
assert.equal(readFileSync(s.file, "utf8"), before);
|
|
assert.equal(s.receipts(), "");
|
|
assert.equal(existsSync(`${s.file}.lock`), false);
|
|
});
|
|
|
|
test("an agent or tester caller is refused before the context or the access file is read", async (t) => {
|
|
const s = setup(t);
|
|
chmodSync(s.file, 0o664);
|
|
const deps = { ...s.deps, contextFor: () => { throw new Error("context was read"); } };
|
|
const env = { ...humanEnv(), MOSAIC_RUN_ID: "run-1" };
|
|
await refusal(run([...GRANT, "--revision", "1"], deps, env), /human shell/);
|
|
await refusal(run(["list", "--business", "acme", "--tester"], deps), /not --tester/);
|
|
});
|
|
|
|
test("the lock is mode 0600 while held and removed after a refused write", async (t) => {
|
|
const s = setup(t);
|
|
let mode;
|
|
withLock(s.file, () => {
|
|
mode = statSync(`${s.file}.lock`).mode & 0o777;
|
|
});
|
|
assert.equal(mode, 0o600);
|
|
assert.equal(existsSync(`${s.file}.lock`), false);
|
|
await refusal(run([...GRANT, "--revision", "9"], s.deps), /revision-conflict/);
|
|
assert.equal(existsSync(`${s.file}.lock`), false);
|
|
});
|
|
|
|
test("a grant naming an unknown instance is refused before any write", async (t) => {
|
|
const s = setup(t);
|
|
const before = readFileSync(s.file, "utf8");
|
|
const bad = [...GRANT_BASE, "--projects", PROJECT, "--instances", "nobody", "--level", "collaborator", "--revision", "1"];
|
|
await refusal(run(bad, s.deps), /refused: /);
|
|
assert.equal(readFileSync(s.file, "utf8"), before);
|
|
assert.equal(s.receipts(), "");
|
|
});
|
|
|
|
test("a project name resolves to its id at grant time", async (t) => {
|
|
const s = setup(t);
|
|
const args = [...GRANT_BASE, "--projects", "hub", "--instances", "coder", "--level", "collaborator", "--revision", "1"];
|
|
await run(args, s.deps);
|
|
assert.deepEqual(s.read().grants[0].projects, [PROJECT]);
|
|
});
|
|
|
|
test("an unknown project name refuses before any write", async (t) => {
|
|
const s = setup(t);
|
|
const before = readFileSync(s.file, "utf8");
|
|
const args = [...GRANT_BASE, "--projects", "nope", "--instances", "coder", "--level", "collaborator", "--revision", "1"];
|
|
await refusal(run(args, s.deps), /project-not-found/);
|
|
assert.equal(readFileSync(s.file, "utf8"), before);
|
|
});
|
|
|
|
test("revoke removes the user's grants and leaves a revision behind", async (t) => {
|
|
const s = setup(t);
|
|
await run([...GRANT, "--revision", "1"], s.deps);
|
|
await run(["revoke", "--business", "acme", "--user", "alice", "--revision", "2"], s.deps);
|
|
const doc = s.read();
|
|
assert.equal(doc.revision, 3);
|
|
assert.deepEqual(doc.grants, []);
|
|
});
|
|
|
|
test("revoke of a user with no grant refuses and writes nothing", async (t) => {
|
|
const s = setup(t);
|
|
const before = readFileSync(s.file, "utf8");
|
|
await refusal(run(["revoke", "--business", "acme", "--user", "alice", "--revision", "1"], s.deps), /no grant/);
|
|
assert.equal(readFileSync(s.file, "utf8"), before);
|
|
});
|
|
|
|
test("an agent caller is refused before the bus is asked", async (t) => {
|
|
const s = setup(t);
|
|
const before = readFileSync(s.file, "utf8");
|
|
const env = { ...humanEnv(), MOSAIC_RUN_ID: "run-1" };
|
|
const err = await refusal(run([...GRANT, "--revision", "1"], s.deps, env), /human shell/);
|
|
assert.equal(err.exitCode, 3);
|
|
assert.equal(existsSync(s.log), false);
|
|
assert.equal(readFileSync(s.file, "utf8"), before);
|
|
});
|
|
|
|
test("--tester is refused on every verb before the bus is asked", async (t) => {
|
|
const s = setup(t);
|
|
for (const verb of [["list"], [...GRANT, "--revision", "1"], ["revoke", "--user", "alice", "--revision", "1"]]) {
|
|
const args = [...verb, "--business", "acme", "--tester"];
|
|
const err = await refusal(run(args, s.deps), /not --tester/);
|
|
assert.equal(err.exitCode, 3);
|
|
}
|
|
assert.equal(existsSync(s.log), false);
|
|
});
|
|
|
|
test("a bus that refuses the human proof blocks the write", async (t) => {
|
|
const s = setup(t, { mode: "refuse" });
|
|
const before = readFileSync(s.file, "utf8");
|
|
const err = await refusal(run([...GRANT, "--revision", "1"], s.deps), /human-required/);
|
|
assert.equal(err.exitCode, 3);
|
|
assert.equal(readFileSync(s.file, "utf8"), before);
|
|
assert.equal(s.receipts(), "");
|
|
});
|
|
|
|
test("the context reads console.sso subjects and the business files, and peers stay empty", async (t) => {
|
|
const root = tmp(t, "mosaic-access-ctx-");
|
|
const f = fixture(root, "acme");
|
|
const system = { console: { sso: { providers: [{ id: "authentik", subjects: [{ sub: "jason-sub" }] }] } } };
|
|
const ctx = accessContextFor(system, { MOSAIC_CONFIG: join(root, "config", "config.json") });
|
|
assert.deepEqual(ctx.providers, ["authentik"]);
|
|
assert.deepEqual(ctx.ssoSubjects, [{ provider: "authentik", sub: "jason-sub" }]);
|
|
assert.deepEqual(ctx.peers, []);
|
|
assert.equal(ctx.businesses.acme.human, "jason");
|
|
assert.ok(Array.isArray(ctx.businesses.acme.instances) && ctx.businesses.acme.instances.length > 0);
|
|
const bare = accessContextFor({}, { MOSAIC_CONFIG: join(root, "config", "config.json") });
|
|
assert.deepEqual(bare.ssoSubjects, []);
|
|
assert.deepEqual(bare.providers, []);
|
|
});
|
|
|
|
// Real human-cli.mjs on a scratch bus, run from agent ancestry. The proof must fail.
|
|
test("the real human-cli, run from agent ancestry, refuses and writes nothing", async (t) => {
|
|
const root = tmp(t, "mosaic-access-real-");
|
|
const f = fixture(root, "acme");
|
|
const config = join(root, "config");
|
|
const bus = await startBroker({
|
|
dataRoot: f.dataRoot,
|
|
businesses: { acme: { id: "acme", human: "jason", arbiters: { technical: "coder", delivery: "coder" }, roles: { coder: { authority: { withinRole: [], crossRole: [] } } } } },
|
|
organization: { dir: config, rolesDir: f.roles, system: systemFor(root) },
|
|
});
|
|
t.after(() => bus.close());
|
|
writeJson(join(config, "access.json"), DOC);
|
|
const file = join(config, "access.json");
|
|
const before = readFileSync(file, "utf8");
|
|
const agentEnv = { ...humanEnv(), CLAUDECODE: "1" };
|
|
const deps = {
|
|
system: { dataRoot: f.dataRoot },
|
|
configDir: config,
|
|
contextFor: () => CONTEXT,
|
|
transport: (business) => humanTransport({ socket: socketPath(f.dataRoot), business, env: agentEnv, cli: HUMAN_CLI, timeoutMs: 10000 }),
|
|
};
|
|
// The bus closes the socket on a failed human proof, so the client reports
|
|
// outcome-unknown (client.mjs:32-33), not human-required. That is a bus behavior
|
|
// outside R5 (BUILD.md). The test pins it, and nothing is written.
|
|
const err = await refusal(run([...GRANT, "--revision", "1"], deps), /^outcome-unknown/);
|
|
assert.equal(err.code, "outcome-unknown");
|
|
assert.equal(err.exitCode, 1);
|
|
assert.equal(readFileSync(file, "utf8"), before);
|
|
assert.equal(existsSync(join(f.dataRoot, "state", "access-log.jsonl")), false);
|
|
});
|