// Private fixture runtime only; no live host, unit, store or organization file. import { spawn } from "node:child_process"; import { once } from "node:events"; import { chmodSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { fixture, BUSINESSES } from "./helpers.mjs"; import { systemFor } from "../../organization/tests/helpers.mjs"; import { reserveOrg, activateOrg } from '../../organization/src/org-registry.mjs'; export async function childResult(file, args, env, input) { const child = spawn(file, args, { env, stdio: ["pipe", "pipe", "pipe"] }); let stdout = "", stderr = ""; child.stdout.on("data", b => stdout += b); child.stderr.on("data", b => stderr += b); child.stdin.on("error", () => {}); child.stdin.end(input); const [status, signal] = await once(child, "close"); return { status, signal, stdout, stderr }; } export async function provedRuntime(t) { const root = mkdtempSync(join(tmpdir(), "channel-recovery-")); let host; t.after(async () => { try { if (host && host.exitCode === null && host.signalCode === null) { const done = once(host, "close"); const timer = setTimeout(() => host.kill("SIGKILL"), 3000); host.kill("SIGTERM"); try { await done; } finally { clearTimeout(timer); } } } finally { rmSync(root, { recursive: true, force: true }); } }); const f = fixture(root, "demo", doc => ({ ...doc, plugins: { channel: [] } })); mkdirSync(join(root, '.config'), { mode: 0o700 }); const configDir = join(root, '.config', 'mosaic-test'); renameSync(join(root, 'config'), configDir); chmodSync(configDir, 0o700); writeFileSync(join(f.dataRoot, 'instance.json'), JSON.stringify({ instanceVersion: 1, name: 'test' })); const ctx = { configDir, dataRoot: f.dataRoot, instance: 'test' }; const org = await reserveOrg(ctx, 'demo'); await activateOrg(ctx, 'demo'); const url = rel => new URL(rel, import.meta.url).href; const humanURL = url("../../bus/src/human.mjs"); const script = join(root, "fixture-runtime.mjs"); // As in stack-copy.test.mjs, isolate the fixture's ancestry from the seat. // The real PID, UID, argv path, start time, nonce and verifier still run. writeFileSync(script, ` import { mock } from 'node:test'; const real = await import(${JSON.stringify(humanURL)}); mock.module(${JSON.stringify(humanURL)}, { namedExports: { readProcess: real.readProcess, verifyHuman: (proof, options) => real.verifyHuman(proof, { ...options, readProcess: pid => ({ ...real.readProcess(pid), ppid: 1 }) }), } }); const { startBroker } = await import(${JSON.stringify(url("../../bus/src/runtime.mjs"))}); const runtime = await startBroker({ dataRoot: ${JSON.stringify(org.root)}, businesses: ${JSON.stringify(BUSINESSES)}, organization: { dir: ${JSON.stringify(configDir)}, rolesDir: ${JSON.stringify(f.roles)}, system: ${JSON.stringify(systemFor(root))} } }); process.on('SIGTERM', async () => { await runtime.close(); process.exit(0); }); console.log('fixture ready'); `); const env = { PATH: process.env.PATH, HOME: root, TMPDIR: process.env.TMPDIR, MOSAIC_CONFIG: join(configDir, 'config.json'), MOSAIC_ROLES_DIR: f.roles }; host = spawn(process.execPath, ["--experimental-test-module-mocks", script], { env, stdio: ["ignore", "pipe", "pipe"] }); let output = "", errors = ""; host.stderr.on("data", b => errors += b); await new Promise((resolve, reject) => { host.stdout.on("data", b => { output += b; if (output.includes("fixture ready")) resolve(); }); host.once("exit", code => reject(new Error(`fixture failed ${code}: ${errors}`))); host.once("error", reject); }); const cli = fileURLToPath(new URL("../src/cli.mjs", import.meta.url)); const shim = fileURLToPath(new URL("../../bus/src/human-cli.mjs", import.meta.url)); return { root, f, env, shim, socket: join(org.root, "bus/broker.sock"), cli: argv => childResult(process.execPath, [cli, ...argv, "--business", "demo"], env), shimCall: (file, request) => childResult(process.execPath, [file, join(org.root, "bus/broker.sock")], env, JSON.stringify(request) + "\n"), file: join(configDir, "businesses/demo.json"), history: () => readFileSync(join(configDir, "businesses/.history/demo/changes.jsonl"), "utf8").trim().split("\n").map(JSON.parse), }; }