Rocko-authored, Filbert-reviewed inspector (r6 manifest a4a44930...) with full review/build/verdict evidence under docs/plans/reviews. 43/0 selftests, oracle zero-disagreement, foundation checker PASS. Owner A9 acceptance recorded separately.
318 lines
14 KiB
JavaScript
318 lines
14 KiB
JavaScript
// Isolated CLI tests: usage, I/O failures, bounds, output escaping, privacy and
|
|
// non-effect inventory (charter §7, §8, §10.3). Every run uses a disposable synthetic
|
|
// sandbox (home, cwd, fixtures, data-root) with canary files; nothing here touches
|
|
// the operator's real HOME, ~/.mosaic or any credential.
|
|
import { test, before, after } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { spawnSync } from "node:child_process";
|
|
import {
|
|
mkdtempSync, mkdirSync, writeFileSync, symlinkSync, readdirSync, lstatSync, readFileSync, rmSync, chmodSync,
|
|
} from "node:fs";
|
|
import { join, dirname } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
import { createHash } from "node:crypto";
|
|
import { fileURLToPath } from "node:url";
|
|
import { renderText, renderJson, escapeText, run } from "../foundation-inspect.mjs";
|
|
import { exitFor } from "./resolve.mjs";
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
const CLI = join(HERE, "..", "foundation-inspect.mjs");
|
|
const DEMO = join(HERE, "fixtures", "demo");
|
|
const CANARY = "CANARY-DO-NOT-PRINT";
|
|
|
|
let root;
|
|
let env;
|
|
let cwd;
|
|
|
|
function sha256(buf) {
|
|
return createHash("sha256").update(buf).digest("hex");
|
|
}
|
|
|
|
/** path/type/size/mode/uid/gid/ino/mtime/content-hash inventory of a tree, sorted by path (charter §8 ownership included). */
|
|
function inventory(dir, out = []) {
|
|
for (const name of readdirSync(dir).sort()) {
|
|
const p = join(dir, name);
|
|
const st = lstatSync(p);
|
|
const type = st.isDirectory() ? "dir" : st.isSymbolicLink() ? "link" : st.isFIFO() ? "fifo" : st.isFile() ? "file" : "other";
|
|
const content = st.isFile() ? sha256(readFileSync(p)) : null;
|
|
out.push([p.slice(root.length), type, st.size, st.mode, st.uid, st.gid, st.ino, st.mtimeMs, content].join("|"));
|
|
if (st.isDirectory()) inventory(p, out);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function inspect(args, opts = {}) {
|
|
const r = spawnSync(process.execPath, [CLI, ...args], {
|
|
cwd: opts.cwd || cwd, env: opts.env || env, encoding: "buffer", timeout: 30000,
|
|
});
|
|
assert.equal(r.error, undefined, "spawn failed");
|
|
return { status: r.status, stdout: r.stdout.toString("utf8"), stderr: r.stderr.toString("utf8"), raw: r.stdout };
|
|
}
|
|
|
|
function jsonRun(args, opts) {
|
|
const r = inspect(["--json", ...args], opts);
|
|
assert.equal(r.stderr, "", "stderr must be empty");
|
|
const parsed = JSON.parse(r.stdout);
|
|
assert.ok(!("exit" in parsed), "exit is process metadata, never a serialized field");
|
|
assert.equal(r.status, exitFor(parsed), "process exit code is derived from the closed result");
|
|
return { ...r, parsed };
|
|
}
|
|
|
|
before(() => {
|
|
root = mkdtempSync(join(tmpdir(), "foundation-cli-"));
|
|
for (const d of ["home", "home/.mosaic", "home/.config", "home/.config/mosaic", "cwd", "fixtures", "data-root", "data-root/runs"]) {
|
|
mkdirSync(join(root, d), { recursive: true });
|
|
}
|
|
writeFileSync(join(root, "home", ".mosaic", "credentials.json"), `{"token":"${CANARY}-1"}\n`);
|
|
writeFileSync(join(root, "home", ".config", "mosaic", "config.json"), `{"secret":"${CANARY}-2"}\n`);
|
|
writeFileSync(join(root, "cwd", "bundle.json"), `{"canary":"${CANARY}-3"}\n`);
|
|
writeFileSync(join(root, "data-root", "runs", "result.json"), `{"canary":"${CANARY}-4"}\n`);
|
|
writeFileSync(join(root, "cwd", "package.json"), `{"name":"${CANARY}-5"}\n`);
|
|
for (const name of readdirSync(DEMO)) {
|
|
if (name.endsWith(".json") && !name.includes(".expected.")) writeFileSync(join(root, "fixtures", name), readFileSync(join(DEMO, name)));
|
|
}
|
|
cwd = join(root, "cwd");
|
|
env = {
|
|
HOME: join(root, "home"),
|
|
TMPDIR: join(root, "data-root"),
|
|
PATH: "/nonexistent",
|
|
MOSAIC_CONFIG: join(root, "home", ".config", "mosaic", "config.json"),
|
|
XDG_CONFIG_HOME: join(root, "home", ".config"),
|
|
MOSAIC_DATA_ROOT: join(root, "data-root"),
|
|
LANG: "C",
|
|
};
|
|
});
|
|
|
|
after(() => {
|
|
if (root) rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
test("usage errors exit 2 usage-invalid without touching any file", () => {
|
|
const before = inventory(root);
|
|
for (const args of [[], ["--json"], ["a.json", "b.json"], ["--nope", "a.json"], ["--json", "--json", "a.json"], ["-x"]]) {
|
|
const r = jsonRun(args);
|
|
assert.equal(r.status, 2, JSON.stringify(args));
|
|
assert.equal(r.parsed.rule, "usage-invalid");
|
|
assert.equal(r.parsed.reason, "invalid-request");
|
|
assert.equal(r.parsed.result, "invalid");
|
|
assert.equal(r.parsed.diagnostic, null);
|
|
}
|
|
const t = inspect([]);
|
|
assert.equal(t.status, 2);
|
|
assert.match(t.stdout, /^SYNTHETIC PREVIEW/);
|
|
assert.deepEqual(inventory(root), before);
|
|
});
|
|
|
|
test("missing file: exit 4 input-open-failed with the input path echoed in the diagnostic only", () => {
|
|
const r = jsonRun(["does-not-exist.json"]);
|
|
assert.equal(r.status, 4);
|
|
assert.equal(r.parsed.rule, "input-open-failed");
|
|
assert.equal(r.parsed.reason, "io-failure");
|
|
assert.deepEqual(r.parsed.diagnostic, { byteOffset: null, inputPath: "does-not-exist.json" });
|
|
assert.equal(r.parsed.selection, null);
|
|
});
|
|
|
|
test("symlink to a valid bundle is refused (O_NOFOLLOW) with exit 4", () => {
|
|
symlinkSync(join(root, "fixtures", "demo-read-w1.json"), join(root, "cwd", "link.json"));
|
|
const r = jsonRun(["link.json"]);
|
|
assert.equal(r.status, 4);
|
|
assert.equal(r.parsed.rule, "input-open-failed");
|
|
assert.equal(r.parsed.diagnostic.inputPath, "link.json");
|
|
});
|
|
|
|
test("directory and FIFO are not regular files: exit 4 input-not-regular", () => {
|
|
const d = jsonRun([join(root, "fixtures")]);
|
|
assert.equal(d.status, 4);
|
|
assert.equal(d.parsed.rule, "input-not-regular");
|
|
const fifo = join(root, "cwd", "pipe.json");
|
|
const mk = spawnSync("mkfifo", [fifo], { env: { ...env, PATH: "/usr/bin:/bin" } });
|
|
if (mk.status !== 0) {
|
|
assert.fail("mkfifo unavailable; FIFO case cannot be exercised");
|
|
}
|
|
const f = jsonRun(["pipe.json"]);
|
|
assert.equal(f.status, 4, "FIFO must be refused after O_NONBLOCK open, not read");
|
|
assert.equal(f.parsed.rule, "input-not-regular");
|
|
});
|
|
|
|
test("unreadable regular file: exit 4 input-open-failed", (t) => {
|
|
if (typeof process.getuid === "function" && process.getuid() === 0) {
|
|
t.skip("root ignores mode bits");
|
|
return;
|
|
}
|
|
const p = join(root, "cwd", "unreadable.json");
|
|
writeFileSync(p, "{}\n");
|
|
chmodSync(p, 0o000);
|
|
const r = jsonRun(["unreadable.json"]);
|
|
assert.equal(r.status, 4);
|
|
assert.equal(r.parsed.rule, "input-open-failed");
|
|
chmodSync(p, 0o600);
|
|
});
|
|
|
|
test("oversize file: exit 2 input-too-large with a null diagnostic (no path echo)", () => {
|
|
const p = join(root, "cwd", "big.json");
|
|
const buf = Buffer.alloc(1024 * 1024 + 1, 0x20);
|
|
buf[0] = 0x7b; buf[buf.length - 1] = 0x7d;
|
|
writeFileSync(p, buf);
|
|
const r = jsonRun(["big.json"]);
|
|
assert.equal(r.status, 2);
|
|
assert.equal(r.parsed.rule, "input-too-large");
|
|
assert.equal(r.parsed.reason, "invalid-request");
|
|
assert.equal(r.parsed.diagnostic, null);
|
|
rmSync(p);
|
|
// exactly 1 MiB is read and parsed (here: an object, so a shape refusal, not a bound)
|
|
const exact = Buffer.alloc(1024 * 1024, 0x20);
|
|
exact[0] = 0x7b; exact[exact.length - 1] = 0x7d;
|
|
writeFileSync(p, exact);
|
|
const e = jsonRun(["big.json"]);
|
|
assert.equal(e.status, 2);
|
|
assert.equal(e.parsed.rule, "shape-missing-field");
|
|
rmSync(p);
|
|
});
|
|
|
|
test("parse failure carries byteOffset and never the path", () => {
|
|
writeFileSync(join(root, "cwd", "dup.json"), '{"a": 1, "a": 2}');
|
|
const r = jsonRun(["dup.json"]);
|
|
assert.equal(r.status, 2);
|
|
assert.equal(r.parsed.rule, "input-parse-failed");
|
|
assert.deepEqual(r.parsed.diagnostic, { byteOffset: 9, inputPath: null });
|
|
});
|
|
|
|
test("text output is derived from the same result as JSON output", () => {
|
|
for (const name of ["demo-read-w1", "demo-change-w1", "adm-registration-revoked"]) {
|
|
const j = jsonRun([join(root, "fixtures", `${name}.json`)]);
|
|
const t = inspect([join(root, "fixtures", `${name}.json`)]);
|
|
assert.equal(t.stderr, "");
|
|
assert.equal(t.status, j.status);
|
|
assert.equal(t.stdout, renderText(j.parsed), name);
|
|
assert.equal(j.stdout, renderJson(j.parsed), name);
|
|
assert.equal(j.stdout, renderJson(JSON.parse(j.stdout)), `${name}: JSON output is a pure round-trip`);
|
|
}
|
|
});
|
|
|
|
test("owner demo: permitted preview exits 0 and prints the disclaimer first", () => {
|
|
const t = inspect([join(root, "fixtures", "demo-read-w1.json")]);
|
|
assert.equal(t.status, 0);
|
|
const lines = t.stdout.split("\n");
|
|
assert.equal(lines[0], "SYNTHETIC PREVIEW — NO LIVE EFFECTS");
|
|
assert.equal(lines[1], "preview: no live registrations or permission grants");
|
|
assert.ok(lines.includes("result: allowed"));
|
|
assert.ok(!lines.some((l) => l.startsWith("exit")), "no exit line in text output");
|
|
});
|
|
|
|
test("risky code points in echoed fields are escaped in both renderings", () => {
|
|
const bundle = JSON.parse(readFileSync(join(root, "fixtures", "demo-read-w1.json"), "utf8"));
|
|
bundle.operation = { name: "file.read", target: { root: "workspace", path: "src/a\u2028b" } };
|
|
writeFileSync(join(root, "cwd", "sep.json"), `${JSON.stringify(bundle)}\n`);
|
|
const j = jsonRun(["sep.json"]);
|
|
assert.equal(j.status, 0, "U+2028 is Zl, accepted by the pinned checker");
|
|
assert.ok(!j.stdout.includes("\u2028"), "JSON output escapes U+2028");
|
|
assert.ok(j.stdout.includes("\\u2028"));
|
|
const t = inspect(["sep.json"]);
|
|
assert.ok(!t.stdout.includes("\u2028"));
|
|
assert.ok(t.stdout.includes("operation: file.read target workspace:src/a\\u2028b"));
|
|
// ESC in a selection id is a shape refusal; the id must still not reach the terminal raw
|
|
bundle.operation = { name: "work.read", target: null };
|
|
bundle.selection.agentId = "agent-a\u001b[31m";
|
|
writeFileSync(join(root, "cwd", "esc.json"), `${JSON.stringify(bundle)}\n`);
|
|
const e = inspect(["esc.json"]);
|
|
assert.equal(e.status, 2);
|
|
assert.ok(!e.stdout.includes("\u001b"));
|
|
assert.ok(e.stdout.includes("selection: null"));
|
|
assert.equal(escapeText("a\u001b\u2028\\\u{1F600}b"), "a\\u001b\\u2028\\u005c\u{1F600}b");
|
|
});
|
|
|
|
test("canaries never reach stdout: bundle strings, HOME, config, cwd, data-root", () => {
|
|
const bundle = JSON.parse(readFileSync(join(root, "fixtures", "demo-read-w1.json"), "utf8"));
|
|
for (const r of bundle.records) if (r.kind === "project") r.payload.displayName = `${CANARY}-6`;
|
|
writeFileSync(join(root, "cwd", "canary.json"), `${JSON.stringify(bundle)}\n`);
|
|
const outputs = [];
|
|
for (const args of [["canary.json"], ["--json", "canary.json"], ["missing.json"], ["--json", "missing.json"], []]) {
|
|
const r = inspect(args);
|
|
outputs.push(r.stdout, r.stderr);
|
|
}
|
|
const joined = outputs.join("\n");
|
|
assert.ok(!joined.includes(CANARY), "canary content leaked to output");
|
|
assert.ok(!joined.includes(root), "sandbox root path leaked to output");
|
|
});
|
|
|
|
test("HOME/config independence: identical bytes across different synthetic environments", () => {
|
|
const args = ["--json", join(root, "fixtures", "demo-change-w1.json")];
|
|
const a = inspect(args);
|
|
const altHome = join(root, "home2");
|
|
mkdirSync(altHome, { recursive: true });
|
|
const b = inspect(args, { env: { HOME: altHome, PATH: "/nonexistent", MOSAIC_CONFIG: "/nonexistent/config.json", NODE_OPTIONS: "" }, cwd: root });
|
|
const c = inspect(args);
|
|
assert.equal(a.stdout, b.stdout);
|
|
assert.equal(a.stdout, c.stdout);
|
|
assert.equal(a.status, 3);
|
|
assert.equal(JSON.parse(a.stdout).result, "unresolved");
|
|
});
|
|
|
|
test("non-effect: before/after inventory of the sandbox is unchanged by every kind of run", () => {
|
|
rmSync(join(root, "cwd", "pipe.json"), { force: true });
|
|
const before = inventory(root);
|
|
const runs = [
|
|
["demo-read-w1.json"], ["--json", "demo-change-w1.json"], ["adm-registration-revoked.json"],
|
|
["prop-message-is-not-authority.json"], ["demo-file-change-src.json"],
|
|
];
|
|
for (const args of runs) {
|
|
const r = inspect(args.map((a) => (a.endsWith(".json") ? join(root, "fixtures", a) : a)));
|
|
assert.equal(r.stderr, "");
|
|
assert.ok([0, 3].includes(r.status), args.join(" "));
|
|
}
|
|
inspect(["../home/.mosaic/credentials.json"]);
|
|
inspect([join(root, "home", ".config", "mosaic", "config.json")]);
|
|
inspect(["missing.json"]);
|
|
inspect([]);
|
|
const after = inventory(root);
|
|
assert.deepEqual(after, before);
|
|
assert.deepEqual(readdirSync(join(root, "data-root")), ["runs"]);
|
|
assert.deepEqual(readdirSync(join(root, "data-root", "runs")), ["result.json"]);
|
|
});
|
|
|
|
test("in-process run() agrees with the spawned CLI", () => {
|
|
const p = join(root, "fixtures", "demo-read-w1.json");
|
|
const inproc = run(["--json", p]);
|
|
const spawned = jsonRun([p]);
|
|
assert.deepEqual(inproc.result, spawned.parsed);
|
|
assert.equal(inproc.json, true);
|
|
assert.equal(run([]).result.rule, "usage-invalid");
|
|
assert.equal(run(["--json"]).json, true);
|
|
});
|
|
|
|
test("profile negative control: one final LF in a typed field is refused pre-admission through the CLI and never echoed", () => {
|
|
const bundle = JSON.parse(readFileSync(join(root, "fixtures", "demo-read-w1.json"), "utf8"));
|
|
bundle.selection.agentId = `${bundle.selection.agentId}\n`;
|
|
const p = join(root, "cwd", "profile-lf.json");
|
|
writeFileSync(p, `${JSON.stringify(bundle)}\n`);
|
|
const r = jsonRun(["profile-lf.json"]);
|
|
assert.equal(r.status, 2);
|
|
assert.equal(r.parsed.result, "invalid");
|
|
assert.equal(r.parsed.reason, "invalid-request");
|
|
assert.equal(r.parsed.rule, "profile-pattern-mismatch");
|
|
assert.equal(r.parsed.selection, null);
|
|
assert.equal(r.parsed.operation, null);
|
|
assert.equal(r.parsed.proposal, null);
|
|
assert.equal(r.parsed.diagnostic, null);
|
|
assert.ok(!r.stdout.includes("agent-a"), "typed value never echoed");
|
|
const t = inspect(["profile-lf.json"]);
|
|
assert.equal(t.status, 2);
|
|
assert.equal(t.stderr, "");
|
|
assert.ok(t.stdout.includes("\nrule: profile-pattern-mismatch\n"));
|
|
assert.ok(t.stdout.includes("\nselection: null\n"));
|
|
assert.ok(!t.stdout.includes("agent-a"));
|
|
// Two final LFs fail the schema pattern itself (both implementations), not the profile.
|
|
bundle.selection.agentId = `${bundle.selection.agentId}\n`;
|
|
writeFileSync(p, `${JSON.stringify(bundle)}\n`);
|
|
const two = jsonRun(["profile-lf.json"]);
|
|
assert.equal(two.status, 2);
|
|
assert.equal(two.parsed.rule, "shape-pattern-mismatch");
|
|
// Positive control: free-form text with escaped newlines is not blanket-rejected.
|
|
const ok = JSON.parse(readFileSync(join(root, "fixtures", "demo-read-w1.json"), "utf8"));
|
|
ok.records.find((x) => x.kind === "mission" && x.id === "m-w1").payload.objective = "line one\nline two\n";
|
|
writeFileSync(join(root, "cwd", "text-lf.json"), `${JSON.stringify(ok)}\n`);
|
|
const allowed = jsonRun(["text-lf.json"]);
|
|
assert.equal(allowed.status, 0);
|
|
assert.equal(allowed.parsed.result, "allowed");
|
|
});
|