Files
ThothII/backend/scripts/p11-acceptance.mjs
T

901 lines
49 KiB
JavaScript

#!/usr/bin/env node
import { createHash, randomBytes } from "node:crypto";
import { closeSync, constants as fsConstants, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync } from "node:fs";
import { access, lstat, mkdir, open, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
import {
buildSafeEnvironment,
collectRepositoryProvenance,
deriveOverall,
scanSecrets,
} from "./p1-acceptance.mjs";
const execFileAsync = promisify(execFile);
const modulePath = fileURLToPath(import.meta.url);
const defaultRepositoryRoot = realpathSync(resolve(dirname(modulePath), "../.."));
const RUN_ID = /^p11-[0-9a-f]{32}$/;
const HEX40 = /^[0-9a-f]{40}$/;
const HEX64 = /^[0-9a-f]{64}$/;
const ISO_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
const ZIP_FILES = ["manifest.json", "workspace.yaml", "contract.env.example", "README.md"];
function resolveSystemExecutable(name) {
for (const candidate of [`/usr/bin/${name}`, `/bin/${name}`, `/opt/homebrew/bin/${name}`, `/usr/local/bin/${name}`]) {
try {
const resolved = realpathSync(candidate);
if (lstatSync(resolved).isFile()) return resolved;
} catch {}
}
throw new Error(`required executable not found: ${name}`);
}
function resolveExecutables(repositoryRoot) {
const repo = canonicalRoot(repositoryRoot);
const thtPath = join(repo, "harness", ".venv", "bin", "tht");
if (!existsSync(thtPath)) throw new Error("required executable not found: tht");
return { gitPath: resolveSystemExecutable("git"), pythonPath: resolveSystemExecutable("python3"), thtPath: realpathSync(thtPath) };
}
const TOPOLOGY = [
"remote.git", "author", "installation/registry", "installation/data", "installation/runtime",
"fixture-secrets", "fixtures/descriptors", "fixtures/requests", "requests", "responses",
"exports/raw", "exports/extracted", "rendered", "logs",
];
export const CHECK_IDS = Object.freeze([
"preflight",
"clean_state",
"ownership",
"catalog_bootstrap",
"catalog_only_listing",
"bootstrap_create_once",
"api_curator_boundary",
"curator_descriptor_update",
"content_only_revision",
"docs_only_reconciliation",
"same_revision_git_objects",
"snapshot_and_export",
"runtime_render_determinism",
"tht_config_check",
"negative_catalog_layout_cases",
"negative_schema_context_cases",
"no_p2_scope_artifacts",
"secret_scan",
"cleanup_confinement",
]);
function nowIso() { return new Date().toISOString(); }
function sha256(value) { return createHash("sha256").update(value).digest("hex"); }
function assert(condition, message) { if (!condition) throw new Error(message); }
function scalarSecretBytes(value) {
if (typeof value !== "string" || value.length === 0 || /\s|\0/.test(value)) throw new Error("scalar fixture secret is invalid");
return Buffer.from(value);
}
function canonicalRoot(repositoryRoot) { return realpathSync(repositoryRoot); }
export function canonicalIntegrationBase(repositoryRoot = defaultRepositoryRoot) {
return join(canonicalRoot(repositoryRoot), ".artifacts", "p11-integration");
}
export function validateRunRoot(repositoryRoot, runRoot, runId) {
if (!RUN_ID.test(runId)) throw new Error("invalid owned run id");
const base = canonicalIntegrationBase(repositoryRoot);
const lexical = resolve(runRoot);
if (dirname(lexical) !== base || basename(lexical) !== runId) throw new Error("run root is not a direct integration child");
return lexical;
}
function validateNoSymlinkAncestors(repositoryRoot, target) {
const repo = canonicalRoot(repositoryRoot);
const rel = relative(repo, target);
if (rel.startsWith("..") || isAbsolute(rel)) throw new Error("path leaves repository");
let cursor = repo;
for (const part of rel.split(sep).filter(Boolean)) {
cursor = join(cursor, part);
if (!existsSync(cursor)) break;
const entry = lstatSync(cursor);
if (entry.isSymbolicLink()) throw new Error("owned path ancestor is a symlink");
}
}
async function atomicWrite(path, bytes, mode = 0o600) {
await mkdir(dirname(path), { recursive: true });
const staging = join(dirname(path), `.${basename(path)}.${randomBytes(12).toString("hex")}.tmp`);
let handle;
try {
handle = await open(staging, "wx", mode);
await handle.writeFile(bytes);
await handle.sync();
await handle.close();
handle = undefined;
await rename(staging, path);
const directory = openSync(dirname(path), fsConstants.O_RDONLY);
try { fsyncSync(directory); } finally { closeSync(directory); }
} catch (error) {
if (handle) await handle.close().catch(() => {});
await rm(staging, { force: true }).catch(() => {});
throw error;
}
}
function exactOwnedResources(run) {
return [
run.root,
join(run.root, "remote.git"),
join(run.root, "author"),
join(run.root, "installation", "registry"),
join(run.root, "installation", "data"),
join(run.root, "installation", "runtime"),
];
}
function initialListeners(pid) {
return [{ name: "primary", kind: "fastify", host: "127.0.0.1", requestedPort: 0, pid, state: "not_started" }];
}
function ownershipValue(run, listeners = run.listeners) {
return {
schemaVersion: 1,
kind: "p11-acceptance",
runId: run.runId,
runNonce: run.nonce,
root: run.root,
repositoryRoot: run.repositoryRoot,
startedAt: run.startedAt,
pid: run.pid,
listeners,
resources: exactOwnedResources(run),
};
}
async function writeOwnership(run, listenerUpdate) {
const listeners = listenerUpdate
? run.listeners.map((listener) => listener.name === listenerUpdate.name ? listenerUpdate : listener)
: run.listeners;
await atomicWrite(join(run.root, "ownership.json"), `${JSON.stringify(ownershipValue(run, listeners), null, 2)}\n`);
run.listeners = listeners;
}
export async function createOwnedRun({ repositoryRoot = defaultRepositoryRoot, runId, nonce, now, pid } = {}) {
const repo = canonicalRoot(repositoryRoot);
const base = canonicalIntegrationBase(repo);
validateNoSymlinkAncestors(repo, base);
await mkdir(join(repo, ".artifacts"), { mode: 0o700 }).catch((error) => { if (error.code !== "EEXIST") throw error; });
await mkdir(base, { mode: 0o700 }).catch((error) => { if (error.code !== "EEXIST") throw error; });
const id = runId ?? `p11-${randomBytes(16).toString("hex")}`;
const root = validateRunRoot(repo, join(base, id), id);
const run = {
repositoryRoot: repo,
root,
runId: id,
nonce: nonce ?? randomBytes(32).toString("hex"),
startedAt: now ?? nowIso(),
pid: pid ?? process.pid,
listeners: initialListeners(pid ?? process.pid),
};
if (!HEX64.test(run.nonce) || !ISO_UTC.test(run.startedAt)) throw new Error("invalid ownership identity");
await mkdir(root, { mode: 0o700 });
await writeOwnership(run);
return run;
}
function strictOwnership(value, run, expectedNonce) {
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("ownership is malformed");
const listener = value.listeners?.[0];
const validListener = Array.isArray(value.listeners) && value.listeners.length === 1
&& listener?.name === "primary" && listener.kind === "fastify" && listener.host === "127.0.0.1"
&& listener.requestedPort === 0 && listener.pid === process.pid
&& ["not_started", "listening", "closed", "close_failed"].includes(listener.state)
&& (listener.state === "not_started" ? !("actualPort" in listener)
: Number.isInteger(listener.actualPort) && listener.actualPort >= 1 && listener.actualPort <= 65535);
if (value.schemaVersion !== 1 || value.kind !== "p11-acceptance" || value.runId !== run.runId || value.runNonce !== expectedNonce
|| value.root !== run.root || value.repositoryRoot !== run.repositoryRoot || value.pid !== process.pid
|| !ISO_UTC.test(value.startedAt ?? "") || !validListener
|| JSON.stringify(value.resources) !== JSON.stringify(exactOwnedResources(run))) throw new Error("ownership identity mismatch");
return value;
}
export async function readAndValidateOwnership({ repositoryRoot = defaultRepositoryRoot, runRoot, expectedNonce }) {
const repo = canonicalRoot(repositoryRoot);
const id = basename(resolve(runRoot));
const lexical = validateRunRoot(repo, runRoot, id);
const rootEntry = await lstat(lexical);
if (!rootEntry.isDirectory() || rootEntry.isSymbolicLink()) throw new Error("owned run root is not a directory");
const ownershipPath = join(lexical, "ownership.json");
const ownershipEntry = await lstat(ownershipPath);
if (!ownershipEntry.isFile() || ownershipEntry.isSymbolicLink()) throw new Error("ownership file is unsafe");
let value;
try { value = JSON.parse(await readFile(ownershipPath, "utf8")); } catch { throw new Error("ownership is malformed"); }
return strictOwnership(value, {
repositoryRoot: repo,
root: lexical,
runId: id,
nonce: expectedNonce,
startedAt: value.startedAt,
pid: process.pid,
}, expectedNonce);
}
export async function cleanupOwnedRun({ repositoryRoot = defaultRepositoryRoot, runRoot, expectedNonce }) {
const value = await readAndValidateOwnership({ repositoryRoot, runRoot, expectedNonce });
const base = canonicalIntegrationBase(repositoryRoot);
const tombstone = join(base, `.deleting-${value.runId}-${expectedNonce.slice(0, 16)}`);
await rename(runRoot, tombstone);
await rm(tombstone, { recursive: true, force: false });
}
async function finalizeOwnedRun({ run, success, keep }) {
if (!success || keep) return false;
await cleanupOwnedRun({ repositoryRoot: run.repositoryRoot, runRoot: run.root, expectedNonce: run.nonce });
return true;
}
function sanitizeForEvidence(value, forbiddenValues = []) {
const forbidden = forbiddenValues.filter((item) => typeof item === "string" && item.length > 0);
const redactString = (input) => forbidden.reduce((text, secret) => text.split(secret).join("[REDACTED]"), input);
if (typeof value === "string") return redactString(value);
if (Array.isArray(value)) return value.map((item) => sanitizeForEvidence(item, forbiddenValues));
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, sanitizeForEvidence(item, forbiddenValues)]));
return value;
}
async function fileArtifact(root, relativePath) {
const bytes = await readFile(join(root, relativePath));
return { path: relativePath.split(sep).join("/"), sha256: sha256(bytes) };
}
async function evidence(run, relativePath, value, forbiddenValues = []) {
await atomicWrite(join(run.root, relativePath), `${JSON.stringify(sanitizeForEvidence(value, forbiddenValues), null, 2)}\n`);
return await fileArtifact(run.root, relativePath);
}
async function writeJson(path, value) {
await atomicWrite(path, `${JSON.stringify(value, null, 2)}\n`);
}
async function walkFiles(root) {
const files = [];
async function visit(dir) {
for (const entry of await readdir(dir, { withFileTypes: true })) {
const path = join(dir, entry.name);
if (entry.isDirectory()) await visit(path);
else if (entry.isFile()) files.push({ path, rel: relative(root, path).split(sep).join("/") });
}
}
if (existsSync(root)) await visit(root);
return files.sort((a, b) => a.rel.localeCompare(b.rel));
}
async function snapshotDigest(root) {
const result = {};
for (const file of await walkFiles(root)) result[file.rel] = sha256(await readFile(file.path));
return result;
}
function assertByteIdentical(left, right, label) {
if (JSON.stringify(left) !== JSON.stringify(right)) throw new Error(`${label} changed unexpectedly`);
}
async function writeReportFiles({ run, report }) {
validateReport(report);
await writeJson(join(run.root, "report.json"), report);
const lines = [
`# P1.1 acceptance report`,
"",
`Run ID: ${report.runId}`,
`Overall: ${report.overall}`,
"",
...report.checks.map((check) => `- ${check.id}: ${check.status}`),
"",
`report.json sha256: ${sha256(await readFile(join(run.root, "report.json")))}`,
`P1.1 automated integration: ${report.overall}`,
"P1.1 manual acceptance: PENDING",
];
await atomicWrite(join(run.root, "report.md"), `${lines.join("\n")}\n`);
}
export function validateReport(report) {
if (!report || typeof report !== "object" || Array.isArray(report)) throw new Error("report is malformed");
if (report.schemaVersion !== 1 || !RUN_ID.test(report.runId ?? "") || !ISO_UTC.test(report.startedAt ?? "")
|| !ISO_UTC.test(report.finishedAt ?? "") || report.command !== "p11-acceptance integration --keep") throw new Error("report identity is invalid");
if (report.overall !== deriveOverall(report.checks ?? [])) throw new Error("report overall is not derived");
if (!Array.isArray(report.checks) || report.checks.length !== CHECK_IDS.length) throw new Error("report checks are incomplete");
const ids = report.checks.map((check) => check.id);
if (JSON.stringify(ids) !== JSON.stringify(CHECK_IDS)) throw new Error("report checks are not exact");
const artifactPaths = new Set();
for (const check of report.checks) {
if (!["PASS", "FAIL"].includes(check.status) || !ISO_UTC.test(check.startedAt ?? "") || !ISO_UTC.test(check.finishedAt ?? "")) {
throw new Error("report check metadata is invalid");
}
if (!Array.isArray(check.commands) || check.commands.some((command) => typeof command !== "string" || !/^[A-Za-z0-9._+-]+$/.test(command))) {
throw new Error("report command is invalid");
}
if (!Array.isArray(check.artifacts)) throw new Error("report artifacts are invalid");
for (const artifact of check.artifacts) {
if (typeof artifact.path !== "string" || artifact.path.startsWith("/") || artifact.path.includes("..") || !/^[A-Za-z0-9._/-]+$/.test(artifact.path)) {
throw new Error("report artifact path is invalid");
}
if (!HEX64.test(artifact.sha256 ?? "")) throw new Error("report artifact hash is invalid");
if (artifactPaths.has(artifact.path)) throw new Error("report artifact path is duplicated");
artifactPaths.add(artifact.path);
}
}
}
async function execCommand(executable, argv, { cwd, env, timeoutMs = 30_000, stdin } = {}) {
if (!Array.isArray(argv) || argv.some((value) => typeof value !== "string")) throw new Error("command argv must be a string array");
const result = await execFileAsync(executable, argv, {
cwd,
env,
timeout: timeoutMs,
maxBuffer: 16 * 1024 * 1024,
encoding: "utf8",
...(stdin === undefined ? {} : { input: stdin }),
});
return { code: 0, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
}
async function git(ctx, argv, options = {}) {
return await execCommand(ctx.executables.gitPath, argv, { ...options, env: ctx.env });
}
async function tht(ctx, argv, options = {}) {
try {
return await execCommand(ctx.executables.thtPath, argv, { ...options, env: ctx.env });
} catch (error) {
if (typeof error?.code === "number") return { code: error.code, stdout: error.stdout ?? "", stderr: error.stderr ?? "" };
throw error;
}
}
function namespace(id) { return id.toUpperCase().replaceAll("-", "_"); }
function baseWorkspace(id, evidenceSource) {
return {
workspace: { schema_version: 4, id, name: `P1.1 ${id}`, description: `Catalog entry for ${id}`, language: "en" },
dwh: { engine: "postgres", database: "postgres", schema: "public", supported_transports: ["postgres_direct"] },
evidence: { source: evidenceSource, policy: { max_chunk_chars: 4000, retain_published_generations: 3 } },
};
}
function descriptors() {
return [
baseWorkspace("p11-filesystem", { type: "filesystem", uri: "p11-filesystem/evidence", patterns: ["**/*.md"], max_bytes: 10485760 }),
baseWorkspace("p11-http", { type: "http", uris: ["https://evidence.example.test/guide.md"], authentication: "signed_urls_file", connect_timeout_ms: 1250, read_timeout_ms: 30001, max_bytes: 12345, max_redirects: 2, allow_private_hosts: false, max_cache_bytes: 67890 }),
baseWorkspace("p11-s3", { type: "s3", uri: "s3://p11-evidence/published/", endpoint_url: "https://s3.example.test/", region: "eu-west-1", credentials: "static_files", trusted_endpoint: true, allow_private_endpoint: false, allow_insecure_endpoint: false, max_bytes: 12345, max_objects: 33, max_pages: 4, page_size: 5 }),
];
}
async function createTopology(run) {
for (const path of TOPOLOGY) await mkdir(join(run.root, path), { recursive: true, mode: path === "fixture-secrets" ? 0o700 : 0o755 });
}
async function setupSecrets(ctx) {
const secretDir = join(ctx.run.root, "fixture-secrets");
const values = {
dwh: `DWH-${randomBytes(12).toString("hex")}`,
signed: `SIGNED-${randomBytes(12).toString("hex")}`,
access: `ACCESS-${randomBytes(12).toString("hex")}`,
secret: `SECRET-${randomBytes(12).toString("hex")}`,
session: `SESSION-${randomBytes(12).toString("hex")}`,
rejected: `REJECTED-${randomBytes(12).toString("hex")}`,
};
ctx.forbiddenValues = Object.values(values);
ctx.secretValues = values;
const paths = {
dwh: join(secretDir, "dwh-password"),
signed: join(secretDir, "evidence-signed-urls.json"),
access: join(secretDir, "evidence-access"),
secret: join(secretDir, "evidence-secret"),
session: join(secretDir, "evidence-session"),
};
await atomicWrite(paths.dwh, scalarSecretBytes(values.dwh));
await atomicWrite(paths.signed, JSON.stringify([`https://evidence.example.test/guide.md?token=${values.signed}`]));
await atomicWrite(paths.access, scalarSecretBytes(values.access));
await atomicWrite(paths.secret, scalarSecretBytes(values.secret));
await atomicWrite(paths.session, scalarSecretBytes(values.session));
const env = {};
for (const workspace of ctx.descriptors) {
const prefix = `THT_WS_${namespace(workspace.workspace.id)}`;
Object.assign(env, {
[`${prefix}_DWH_TRANSPORT`]: "postgres_direct",
[`${prefix}_DWH_HOST`]: "dwh.invalid",
[`${prefix}_DWH_PORT`]: "5432",
[`${prefix}_DWH_USER`]: "reader",
[`${prefix}_DWH_PASSWORD_FILE`]: paths.dwh,
});
}
Object.assign(env, {
THT_WS_P11_HTTP_EVIDENCE_SIGNED_URLS_FILE: paths.signed,
THT_WS_P11_S3_EVIDENCE_ACCESS_KEY_FILE: paths.access,
THT_WS_P11_S3_EVIDENCE_SECRET_KEY_FILE: paths.secret,
THT_WS_P11_S3_EVIDENCE_SESSION_TOKEN_FILE: paths.session,
});
Object.assign(ctx.env, env);
await atomicWrite(join(ctx.run.root, "installation", "bindings.env"), `${Object.entries(env).map(([key, value]) => `${key}=${value}`).join("\n")}\n`);
await atomicWrite(join(ctx.run.root, "installation", "runtime", "base.yaml"), "{}\n");
}
function catalog(entries = ctxDescriptors) {
return { schema_version: 1, workspaces: entries.map(({ workspace }) => ({ id: workspace.id, name: workspace.name, description: workspace.description })) };
}
const ctxDescriptors = descriptors();
async function initializeGit(ctx) {
const author = join(ctx.run.root, "author");
await git(ctx, ["init", "--bare", "--initial-branch=main", join(ctx.run.root, "remote.git")], { cwd: ctx.run.root });
await git(ctx, ["clone", join(ctx.run.root, "remote.git"), author], { cwd: ctx.run.root });
await git(ctx, ["config", "user.name", "P1 Fixture Curator"], { cwd: author });
await git(ctx, ["config", "user.email", "p1-curator@example.invalid"], { cwd: author });
const catalogBytes = `${JSON.stringify({
schema_version: 1,
workspaces: [
...catalog(ctx.descriptors).workspaces,
{ id: "p11-pending", name: "P1.1 pending", description: "Catalog-only slot awaiting bootstrap" },
],
}, null, 2)}\n`;
await atomicWrite(join(author, "thoth-workspaces.yaml"), catalogBytes, 0o644);
const evidenceRoot = join(author, "p11-filesystem", "evidence");
await mkdir(join(evidenceRoot, "domain"), { recursive: true });
await atomicWrite(join(evidenceRoot, "guide.md"), "# P1.1 curated Evidence\n", 0o644);
await atomicWrite(join(evidenceRoot, "domain", "table.md"), "# Curated table\n", 0o644);
await git(ctx, ["add", "thoth-workspaces.yaml"], { cwd: author });
await git(ctx, ["add", "p11-filesystem/evidence/guide.md"], { cwd: author });
await git(ctx, ["add", "-A", "p11-filesystem/evidence"], { cwd: author });
await git(ctx, ["commit", "-m", "Bootstrap curated P1 content"], { cwd: author });
await git(ctx, ["push", "origin", "main"], { cwd: author });
ctx.bootstrapCommit = (await git(ctx, ["rev-parse", "HEAD"], { cwd: author })).stdout.trim();
ctx.catalogBlobBefore = (await git(ctx, ["rev-parse", `HEAD:thoth-workspaces.yaml`], { cwd: author })).stdout.trim();
ctx.evidenceTreeBefore = (await git(ctx, ["rev-parse", `HEAD:p11-filesystem/evidence`], { cwd: author })).stdout.trim();
}
async function loadProductionBackend() {
const [{ loadConfig }, { buildApp }, { WorkspaceRegistry }, { ThtRunner }] = await Promise.all([
import("../dist/config.js"),
import("../dist/app.js"),
import("../dist/workspaces/registry.js"),
import("../dist/tht/tht-runner.js"),
]);
return { loadConfig, buildApp, WorkspaceRegistry, ThtRunner };
}
async function startBackend(ctx) {
const { loadConfig, buildApp, WorkspaceRegistry, ThtRunner } = await loadProductionBackend();
const config = loadConfig(ctx.env);
const registry = new WorkspaceRegistry(config.workspaceRegistry);
const thtRunner = new ThtRunner({
thtBin: config.thtBin,
harnessDir: config.harnessDir,
configPath: join(ctx.run.root, "installation", "runtime", "base.yaml"),
dataRoot: config.dataRoot,
runtimeSnapshotRoot: join(config.workspaceRegistry.root, "snapshots", "runtime"),
secretRoots: config.workspaceRegistry.secretRoots,
secretsFile: config.secretsFile,
secretFiles: config.secretFiles,
semanticRuntime: {
internalQdrantUrl: config.internalQdrantUrl,
internalEmbeddingUrl: config.internalEmbeddingUrl,
internalEmbeddingModel: config.internalEmbeddingModel,
internalEmbeddingDimensions: config.internalEmbeddingDimensions,
},
});
const app = buildApp(config, { thtRunner, workspaceRegistry: registry });
const address = await app.listen({ host: "127.0.0.1", port: 0 });
const baseUrl = `http://127.0.0.1:${new URL(address).port}`;
ctx.registry = registry;
ctx.thtRunner = thtRunner;
ctx.app = app;
ctx.baseUrl = baseUrl;
await writeOwnership(ctx.run, {
name: "primary", kind: "fastify", host: "127.0.0.1", requestedPort: 0,
actualPort: Number(new URL(address).port), pid: process.pid, state: "listening",
});
}
async function stopBackend(ctx) {
if (ctx.app) {
await ctx.app.close().catch(() => {});
await writeOwnership(ctx.run, {
name: "primary", kind: "fastify", host: "127.0.0.1", requestedPort: 0,
actualPort: Number(new URL(ctx.baseUrl).port), pid: process.pid, state: "closed",
}).catch(() => {});
}
}
async function request(ctx, id, method, path, body, binary = false, safeInput) {
const requestSummary = safeInput === undefined
? { method, path, ...(body === undefined ? {} : { body: sanitizeForEvidence(body, ctx.forbiddenValues) }) }
: { method, path, input: safeInput };
await evidence(ctx.run, `requests/${id}.json`, requestSummary, ctx.forbiddenValues);
const response = await fetch(`${ctx.baseUrl}${path}`, {
method,
headers: body === undefined ? {} : { "content-type": "application/json" },
...(body === undefined ? {} : { body: JSON.stringify(body) }),
signal: AbortSignal.timeout(15_000),
});
if (binary) {
const bytes = Buffer.from(await response.arrayBuffer());
await atomicWrite(join(ctx.run.root, `exports/raw/${id}.zip`), bytes);
await evidence(ctx.run, `responses/${id}.json`, { status: response.status, bytes: bytes.length, contentType: response.headers.get("content-type") });
return { status: response.status, bytes };
}
const text = await response.text();
let parsed;
try { parsed = text ? JSON.parse(text) : null; } catch { parsed = { invalidJson: true, raw: text }; }
await evidence(ctx.run, `responses/${id}.json`, { status: response.status, body: sanitizeForEvidence(parsed, ctx.forbiddenValues) }, ctx.forbiddenValues);
return { status: response.status, body: parsed };
}
async function extractZip(ctx, id, bytes) {
const yauzl = (await import("yauzl")).default;
const output = join(ctx.run.root, "exports", "extracted", id);
await mkdir(output, { recursive: true });
const files = await new Promise((resolvePromise, reject) => {
yauzl.fromBuffer(bytes, { lazyEntries: true, strictFileNames: true, validateEntrySizes: true }, (error, zip) => {
if (error || !zip) return reject(error ?? new Error("zip open failed"));
const collected = new Map();
zip.on("error", reject);
zip.on("entry", (entry) => {
if (!ZIP_FILES.includes(entry.fileName) || entry.fileName.includes("..") || entry.fileName.startsWith("/") || entry.fileName.endsWith("/")) return reject(new Error("unsafe export entry"));
zip.openReadStream(entry, (streamError, stream) => {
if (streamError || !stream) return reject(streamError ?? new Error("zip stream failed"));
const chunks = [];
stream.on("data", (chunk) => chunks.push(chunk));
stream.on("error", reject);
stream.on("end", async () => {
const buffer = Buffer.concat(chunks);
collected.set(entry.fileName, buffer);
await atomicWrite(join(output, entry.fileName), buffer);
zip.readEntry();
});
});
});
zip.on("end", () => resolvePromise(collected));
zip.readEntry();
});
});
assert(files.size === ZIP_FILES.length, "export bundle entry mismatch");
return JSON.parse(files.get("manifest.json").toString("utf8"));
}
function checkResult(id, startedAt, status, artifacts = [], commands = [], error) {
return { id, status, startedAt, finishedAt: nowIso(), artifacts, commands, ...(error ? { error } : {}) };
}
async function executeChecks({ checks }) {
const results = [];
let stopped = false;
for (const scenario of checks) {
const startedAt = nowIso();
if (stopped) {
results.push(checkResult(scenario.id, startedAt, "FAIL", [], [], "Not executed after earlier failure."));
continue;
}
try {
const output = await scenario.run();
results.push(checkResult(scenario.id, startedAt, "PASS", output.artifacts ?? [], output.commands ?? []));
} catch (error) {
const partial = error?.acceptancePartial ?? {};
results.push(checkResult(scenario.id, startedAt, "FAIL", partial.artifacts ?? [], partial.commands ?? [], "Acceptance scenario failed safely."));
stopped = true;
}
}
return results;
}
async function registryState(ctx) {
const statePath = join(ctx.run.root, "installation", "registry", "state", "active.json");
const active = JSON.parse(await readFile(statePath, "utf8"));
return {
head: active.head,
revisions: active.revisions.map((revision) => ({ id: revision.id, commit: revision.commit, blob: revision.blob })),
catalog: active.catalog ?? null,
};
}
function safeErrorEnvelope(response, code, status) {
assert(response.status === status, `expected ${status}`);
assert(response.body?.code === code, `expected error code ${code}`);
assert(Object.keys(response.body).sort().join(",") === "code,message", "error envelope is not exact");
}
async function productionChecks(ctx) {
const check = async (id, value, commands = []) => ({ commands, artifacts: [await evidence(ctx.run, `logs/${id}.json`, value, ctx.forbiddenValues)] });
return [
{ id: "preflight", run: async () => check("preflight", { node: process.version, repositoryHead: ctx.provenance.head, repositoryTree: ctx.provenance.tree, clean: ctx.provenance.clean, thtExecutable: true }) },
{ id: "clean_state", run: async () => check("clean_state", { runId: ctx.run.runId, reused: false }) },
{ id: "ownership", run: async () => { await readAndValidateOwnership({ repositoryRoot: ctx.repositoryRoot, runRoot: ctx.run.root, expectedNonce: ctx.run.nonce }); return await check("ownership", { valid: true }); } },
{ id: "catalog_bootstrap", run: async () => {
await initializeGit(ctx);
for (const workspace of ctx.descriptors) await atomicWrite(join(ctx.run.root, "fixtures", "descriptors", `${workspace.workspace.id}.json`), `${JSON.stringify(workspace, null, 2)}\n`);
return {
commands: ["git"],
artifacts: [
await evidence(ctx.run, "logs/catalog-bootstrap.json", { bootstrapCommit: ctx.bootstrapCommit, catalogOnly: true }),
await fileArtifact(ctx.run.root, "author/thoth-workspaces.yaml"),
await fileArtifact(ctx.run.root, "author/p11-filesystem/evidence/guide.md"),
],
};
} },
{ id: "catalog_only_listing", run: async () => {
await startBackend(ctx);
const status = await request(ctx, "registry-status", "GET", "/workspace-registry/status");
assert(status.status === 200 && status.body.head === ctx.bootstrapCommit, "status head mismatch");
const listed = await request(ctx, "workspace-list-initial", "GET", "/workspaces");
assert(listed.status === 200 && listed.body.length === 4, "catalog listing failed");
assert(listed.body.every((entry) => entry.configurationState === "configuration_required"), "catalog entries were not configuration_required");
ctx.baseCommit = status.body.head;
return await check("catalog_only_listing", { head: status.body.head, ids: listed.body.map((entry) => entry.id), allConfigurationRequired: true });
} },
{ id: "bootstrap_create_once", run: async () => {
let base = ctx.baseCommit;
ctx.bootstrapResponses = {};
for (const workspace of ctx.descriptors) {
const validated = await request(ctx, `validate-${workspace.workspace.id}`, "POST", "/workspaces/validate", { workspace });
assert(validated.status === 200, `validate failed ${workspace.workspace.id}`);
const published = await request(ctx, `publish-${workspace.workspace.id}`, "POST", "/workspaces/publish", { action: "create", workspace, baseCommit: base });
assert(published.status === 200 && HEX40.test(published.body.revision.commit), `publish failed ${workspace.workspace.id}`);
ctx.bootstrapResponses[workspace.workspace.id] = published.body;
base = published.body.revision.commit;
}
ctx.publishHead = base;
const listed = await request(ctx, "workspace-list-ready", "GET", "/workspaces");
assert(listed.body.filter((entry) => entry.configurationState === "ready").length === 3, "bootstrap did not activate all published entries");
assert(listed.body.find((entry) => entry.id === "p11-pending")?.configurationState === "configuration_required", "pending slot was not left unconfigured");
return await check("bootstrap_create_once", { head: base, readyIds: listed.body.filter((entry) => entry.configurationState === "ready").map((entry) => entry.id) });
} },
{ id: "api_curator_boundary", run: async () => {
const author = join(ctx.run.root, "author");
const catalogAfter = (await git(ctx, ["rev-parse", `HEAD:thoth-workspaces.yaml`], { cwd: author })).stdout.trim();
const evidenceAfter = (await git(ctx, ["rev-parse", `HEAD:p11-filesystem/evidence`], { cwd: author })).stdout.trim();
assert(catalogAfter === ctx.catalogBlobBefore, "catalog blob changed during bootstrap");
assert(evidenceAfter === ctx.evidenceTreeBefore, "evidence tree changed during bootstrap");
ctx.apiBoundaryState = await registryState(ctx);
return await check("api_curator_boundary", { catalogUnchanged: true, evidenceUnchanged: true, state: ctx.apiBoundaryState }, ["git"]);
} },
{ id: "curator_descriptor_update", run: async () => {
const author = join(ctx.run.root, "author");
await git(ctx, ["fetch", "origin", "main"], { cwd: author });
await git(ctx, ["reset", "--hard", "origin/main"], { cwd: author });
const workspace = structuredClone(ctx.descriptors[0]);
workspace.workspace.name = "P1.1 Curated Filesystem";
workspace.workspace.description = "Curator updated descriptor and catalog metadata";
ctx.curatedWorkspace = workspace;
const updatedCatalog = catalog([workspace, ctx.descriptors[1], ctx.descriptors[2]]);
await atomicWrite(join(author, "thoth-workspaces.yaml"), `${JSON.stringify(updatedCatalog, null, 2)}\n`, 0o644);
await atomicWrite(join(author, "p11-filesystem", "workspace.yaml"), `${(await import("yaml")).stringify(workspace)}`, 0o644);
await git(ctx, ["add", "thoth-workspaces.yaml"], { cwd: author });
await git(ctx, ["add", "--", "p11-filesystem/workspace.yaml"], { cwd: author });
await git(ctx, ["commit", "-m", "Publish workspace p1-filesystem"], { cwd: author });
await git(ctx, ["push", "origin", "main"], { cwd: author });
ctx.curatorCommit = (await git(ctx, ["rev-parse", "HEAD"], { cwd: author })).stdout.trim();
ctx.curatorDescriptorBlob = (await git(ctx, ["rev-parse", `HEAD:p11-filesystem/workspace.yaml`], { cwd: author })).stdout.trim();
const pulled = await request(ctx, "pull-after-curator-update", "POST", "/workspace-registry/pull");
assert(pulled.status === 200 && HEX40.test(pulled.body.head), "pull after curator update failed");
ctx.docsFollowupHead = pulled.body.head;
const read = await request(ctx, "read-after-curator-update", "GET", "/workspaces/p11-filesystem");
assert(read.status === 200 && read.body.workspace.workspace.name === workspace.workspace.name, "curator update did not activate");
assert(read.body.revision.blob === ctx.curatorDescriptorBlob, "api rewrote curator descriptor bytes");
return await check("curator_descriptor_update", { curatorCommit: ctx.curatorCommit, activeHead: ctx.docsFollowupHead, descriptorBlob: ctx.curatorDescriptorBlob }, ["git"]);
} },
{ id: "content_only_revision", run: async () => {
const author = join(ctx.run.root, "author");
await git(ctx, ["fetch", "origin", "main"], { cwd: author });
await git(ctx, ["reset", "--hard", "origin/main"], { cwd: author });
await atomicWrite(join(author, "p11-filesystem", "evidence", "guide.md"), "# P1.1 curated Evidence v2\n", 0o644);
await git(ctx, ["add", "p11-filesystem/evidence/guide.md"], { cwd: author });
await git(ctx, ["commit", "-m", "Update curated Evidence only"], { cwd: author });
await git(ctx, ["push", "origin", "main"], { cwd: author });
ctx.contentCommit = (await git(ctx, ["rev-parse", "HEAD"], { cwd: author })).stdout.trim();
const pulled = await request(ctx, "pull-after-content-update", "POST", "/workspace-registry/pull");
assert(pulled.status === 200 && pulled.body.head === ctx.contentCommit, "content pull head mismatch");
const read = await request(ctx, "read-after-content-update", "GET", "/workspaces/p11-filesystem");
assert(read.body.revision.commit === ctx.contentCommit, "content commit did not activate");
assert(read.body.revision.blob === ctx.curatorDescriptorBlob, "descriptor blob changed on content-only update");
ctx.currentRead = read.body;
return await check("content_only_revision", { commit: ctx.contentCommit, descriptorBlobUnchanged: true }, ["git"]);
} },
{ id: "docs_only_reconciliation", run: async () => {
const repo = join(ctx.run.root, "installation", "registry", "repo");
const diff = (await git(ctx, ["show", "--name-only", "--format=", ctx.docsFollowupHead], { cwd: repo })).stdout.trim().split(/\n+/).filter(Boolean);
assert(diff.length > 0 && diff.every((path) => path.startsWith("workspace-docs/")), "docs follow-up touched non-doc paths");
const finalDescriptor = (await git(ctx, ["rev-parse", `${ctx.docsFollowupHead}:p11-filesystem/workspace.yaml`], { cwd: repo })).stdout.trim();
assert(finalDescriptor === ctx.curatorDescriptorBlob, "docs follow-up rewrote descriptor");
return await check("docs_only_reconciliation", { head: ctx.docsFollowupHead, files: diff, descriptorBlobPreserved: true }, ["git"]);
} },
{ id: "same_revision_git_objects", run: async () => {
const repo = join(ctx.run.root, "installation", "registry", "repo");
const revision = ctx.currentRead.revision;
const manifestPath = join(dirname(revision.snapshotPath), "snapshot.json");
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
const catalogBlob = (await git(ctx, ["rev-parse", `${revision.commit}:thoth-workspaces.yaml`], { cwd: repo })).stdout.trim();
const descriptorBlob = (await git(ctx, ["rev-parse", `${revision.commit}:p11-filesystem/workspace.yaml`], { cwd: repo })).stdout.trim();
const evidenceTree = (await git(ctx, ["rev-parse", `${revision.commit}:p11-filesystem/evidence`], { cwd: repo })).stdout.trim();
assert(manifest.head === revision.commit, "snapshot manifest head mismatch");
assert(descriptorBlob === revision.blob, "descriptor blob mismatch");
ctx.snapshotManifest = manifest;
return {
commands: ["git"],
artifacts: [
await evidence(ctx.run, "logs/same-revision-git-objects.json", { commit: revision.commit, catalogBlob, descriptorBlob, evidenceTree, snapshotHead: manifest.head }),
await fileArtifact(ctx.run.root, relative(ctx.run.root, revision.snapshotPath)),
await fileArtifact(ctx.run.root, relative(ctx.run.root, manifestPath)),
],
};
} },
{ id: "snapshot_and_export", run: async () => {
ctx.exportManifests = {};
const artifacts = [];
for (const workspace of ctx.descriptors) {
const id = workspace.workspace.id;
const exported = await request(ctx, `export-${id}`, "GET", `/workspaces/${id}/export`, undefined, true);
assert(exported.status === 200, `export failed ${id}`);
ctx.exportManifests[id] = await extractZip(ctx, id, exported.bytes);
artifacts.push(await fileArtifact(ctx.run.root, `exports/raw/export-${id}.zip`));
for (const name of ZIP_FILES) artifacts.push(await fileArtifact(ctx.run.root, `exports/extracted/${id}/${name}`));
}
return { commands: [], artifacts: [await evidence(ctx.run, "logs/snapshot-and-export.json", { exported: Object.keys(ctx.exportManifests), files: ZIP_FILES }), ...artifacts] };
} },
{ id: "runtime_render_determinism", run: async () => {
const YAML = await import("yaml");
ctx.configChecks = [];
const artifacts = [];
for (const workspace of ctx.descriptors) {
const revision = (await request(ctx, `read-render-${workspace.workspace.id}`, "GET", `/workspaces/${workspace.workspace.id}`)).body.revision;
const renders = [];
for (let n = 1; n <= 2; n += 1) {
const lease = ctx.thtRunner.acquireWorkspaceRuntime(revision.snapshotPath);
try {
const bytes = await readFile(lease.path);
renders.push(bytes);
await atomicWrite(join(ctx.run.root, "rendered", `${workspace.workspace.id}-${n}.yaml`), bytes);
const checked = await tht(ctx, ["config", "check", "-c", lease.path], { cwd: ctx.env.THT_HARNESS_DIR, timeoutMs: 30_000 });
ctx.configChecks.push({ id: workspace.workspace.id, observation: n, code: checked.code });
} finally {
lease.release();
}
artifacts.push(await fileArtifact(ctx.run.root, `rendered/${workspace.workspace.id}-${n}.yaml`));
}
assert(renders[0].equals(renders[1]), `render was nondeterministic ${workspace.workspace.id}`);
const rendered = YAML.parse(renders[0].toString("utf8"));
assert(rendered.runtime_identity.workspace_revision === revision.commit, `runtime identity mismatch ${workspace.workspace.id}`);
}
return { commands: ["tht"], artifacts: [await evidence(ctx.run, "logs/runtime-render-determinism.json", { deterministic: true, checks: ctx.configChecks }), ...artifacts] };
} },
{ id: "tht_config_check", run: async () => {
assert(ctx.configChecks.length === ctx.descriptors.length * 2 && ctx.configChecks.every((item) => item.code === 0), "tht config checks failed");
return await check("tht-config-check", ctx.configChecks, ["tht"]);
} },
{ id: "negative_catalog_layout_cases", run: async () => {
const baseline = await registryState(ctx);
const author = join(ctx.run.root, "author");
const current = (await request(ctx, "current-list-before-negatives", "GET", "/workspaces")).body;
const secondCreate = await request(ctx, "second-create", "POST", "/workspaces/publish", { action: "create", workspace: ctx.descriptors[0], baseCommit: baseline.head });
safeErrorEnvelope(secondCreate, "workspace_curator_owned", 409);
const update = await request(ctx, "legacy-update", "POST", "/workspaces/publish", { action: "update", workspace: ctx.descriptors[0], baseCommit: baseline.head, baseBlob: ctx.curatorDescriptorBlob });
safeErrorEnvelope(update, "workspace_curator_owned", 409);
const deletion = await request(ctx, "legacy-delete", "POST", "/workspaces/publish", { action: "delete", id: "p11-filesystem", baseCommit: baseline.head, baseBlob: ctx.curatorDescriptorBlob });
safeErrorEnvelope(deletion, "workspace_curator_owned", 409);
const unknown = structuredClone(ctx.descriptors[0]);
unknown.workspace.id = "p11-unknown";
const unknownPublish = await request(ctx, "unknown-catalog-id", "POST", "/workspaces/publish", { action: "create", workspace: unknown, baseCommit: baseline.head });
safeErrorEnvelope(unknownPublish, "workspace_invalid", 400);
const mismatch = structuredClone(ctx.descriptors[0]);
mismatch.workspace.id = "p11-pending";
mismatch.workspace.name = "Mismatched pending name";
mismatch.semantic_index.vector_store.collection = "p11-pending";
const mismatchPublish = await request(ctx, "catalog-metadata-mismatch", "POST", "/workspaces/publish", { action: "create", workspace: mismatch, baseCommit: baseline.head });
safeErrorEnvelope(mismatchPublish, "workspace_invalid", 400);
const after = await registryState(ctx);
assertByteIdentical(after, baseline, "registry state after curator-owned refusals");
assert(JSON.stringify((await request(ctx, "current-list-after-negatives", "GET", "/workspaces")).body) === JSON.stringify(current), "workspace listing mutated after negative cases");
await git(ctx, ["fetch", "origin", "main"], { cwd: author });
await git(ctx, ["reset", "--hard", "origin/main"], { cwd: author });
await mkdir(join(author, "workspaces"), { recursive: true });
await atomicWrite(join(author, "workspaces", "legacy.yaml"), "workspace: bad\n", 0o644);
await git(ctx, ["add", "--", "workspaces/legacy.yaml"], { cwd: author });
await git(ctx, ["commit", "-m", "Invalid contextual Evidence state"], { cwd: author });
await git(ctx, ["push", "origin", "HEAD:main"], { cwd: author });
const rejectedPull = await request(ctx, "invalid-layout-pull", "POST", "/workspace-registry/pull");
safeErrorEnvelope(rejectedPull, "workspace_invalid", 400);
const afterInvalidPull = await registryState(ctx);
assertByteIdentical(afterInvalidPull, baseline, "registry state after invalid pull");
return await check("negative_catalog_layout_cases", { secondCreate: true, update: true, delete: true, unknownCatalogId: true, metadataMismatch: true, oldLayoutRejected: true }, ["git"]);
} },
{ id: "negative_schema_context_cases", run: async () => {
const base = structuredClone(ctx.descriptors[0]);
const cases = [
["invalid-uri", (workspace) => { workspace.evidence.source.uri = "/etc/passwd"; }, "evidence.source.uri"],
["invalid-secret-field", (workspace) => { workspace.evidence.source.password = ctx.secretValues.rejected; }, "evidence.source.password"],
["missing-evidence-tree", (workspace) => { workspace.workspace.id = "p11-pending"; workspace.workspace.name = "P1.1 pending"; workspace.workspace.description = "Catalog-only slot awaiting bootstrap"; workspace.semantic_index.vector_store.collection = "p11-pending"; workspace.evidence.source.uri = "p11-pending/evidence"; }, "evidence.source.uri"],
];
const outcomes = [];
for (const [id, mutate, field] of cases) {
const workspace = structuredClone(base);
mutate(workspace);
const endpoint = id === "missing-evidence-tree" ? "/workspaces/publish" : "/workspaces/validate";
const payload = id === "missing-evidence-tree" ? { action: "create", workspace, baseCommit: ctx.publishHead } : { workspace };
const response = await request(ctx, `negative-schema-${id}`, "POST", endpoint, payload, false, { case: id, expectedInputField: field });
safeErrorEnvelope(response, "workspace_invalid", 400);
outcomes.push({ case: id, status: response.status, field });
}
return await check("negative_schema_context_cases", outcomes);
} },
{ id: "no_p2_scope_artifacts", run: async () => {
const forbidden = ["artifacts/evidence", "materialized", "qdrant", "embedding", "ACTIVE", "retention"];
const present = forbidden.filter((path) => existsSync(join(ctx.run.root, path)));
assert(present.length === 0, "p2 scope artifacts present");
return await check("no_p2_scope_artifacts", { absent: forbidden });
} },
{ id: "secret_scan", run: async () => {
const findings = await scanSecrets({ runRoot: ctx.run.root, forbiddenValues: ctx.forbiddenValues, expectedGitRepositories: ["remote.git", "author"] });
assert(findings.length === 0, "secret scan found leaked secret material");
return await check("secret_scan", { findings: 0 });
} },
{ id: "cleanup_confinement", run: async () => {
const parent = canonicalIntegrationBase(ctx.repositoryRoot);
const siblings = (await readdir(parent)).filter((name) => name !== ctx.run.runId);
return await check("cleanup_confinement", { listenerState: ctx.run.listeners[0].state, siblingCount: siblings.length });
} },
];
}
async function setupContext({ repositoryRoot = defaultRepositoryRoot, env = process.env } = {}) {
const run = await createOwnedRun({ repositoryRoot });
const provenance = await collectRepositoryProvenance({ repositoryRoot });
const executables = resolveExecutables(repositoryRoot);
const harnessDir = realpathSync(join(repositoryRoot, "harness"));
const ownedHome = join(run.root, "installation", "runtime", "acceptance-home");
const ownedTmp = join(run.root, "installation", "runtime", "tmp");
await mkdir(ownedHome, { recursive: true, mode: 0o700 });
await mkdir(ownedTmp, { recursive: true, mode: 0o700 });
const executablePath = [...new Set([dirname(executables.gitPath), dirname(executables.pythonPath), dirname(executables.thtPath)])].join(":");
const fixtureEnv = {
PATH: executablePath,
HOME: ownedHome,
TMPDIR: ownedTmp,
HOST: "127.0.0.1",
PORT: "0",
AUTH_MODE: "none",
THT_BIN: executables.thtPath,
THT_HARNESS_DIR: harnessDir,
THT_DATA_ROOT: join(run.root, "installation", "data"),
SETTINGS_FILE: join(run.root, "installation", "data", "settings.json"),
MAINTENANCE_STATE_FILE: join(run.root, "installation", "data", "maintenance.json"),
THT_WORKSPACE_REGISTRY_ROOT: join(run.root, "installation", "registry"),
THT_WORKSPACE_GIT_REMOTE: join(run.root, "remote.git"),
THT_WORKSPACE_GIT_BRANCH: "main",
THT_WORKSPACE_GIT_AUTHOR_NAME: "P1 API Publisher",
THT_WORKSPACE_GIT_AUTHOR_EMAIL: "p1-api@example.invalid",
THT_WORKSPACE_INSTALLATION_ID: "p11-acceptance",
THT_WORKSPACE_SECRET_ROOTS: join(run.root, "fixture-secrets"),
THT_HOME: join(run.root, "installation", "runtime", "tht-home"),
PYTHONDONTWRITEBYTECODE: "1",
PYTHONNOUSERSITE: "1",
};
const ctx = {
run,
repositoryRoot: canonicalRoot(repositoryRoot),
provenance,
executables,
descriptors: descriptors(),
env: buildSafeEnvironment({ ambient: env, fixture: fixtureEnv }),
forbiddenValues: [],
};
await createTopology(run);
await setupSecrets(ctx);
return ctx;
}
export async function runIntegration({ repositoryRoot = defaultRepositoryRoot, keep = false, env = process.env, announce } = {}) {
const ctx = await setupContext({ repositoryRoot, env });
const priorEnv = {};
for (const [key, value] of Object.entries(ctx.env)) {
priorEnv[key] = process.env[key];
process.env[key] = value;
}
let success = false;
try {
const checks = await productionChecks(ctx);
const results = await executeChecks({ checks });
const report = {
schemaVersion: 1,
runId: ctx.run.runId,
startedAt: ctx.run.startedAt,
finishedAt: nowIso(),
command: "p11-acceptance integration --keep",
overall: deriveOverall(results),
checks: results,
};
await writeReportFiles({ run: ctx.run, report });
success = report.overall === "PASS";
if (announce) await announce({ report, runRoot: ctx.run.root });
return { exitCode: success ? 0 : 1, runRoot: ctx.run.root, retained: !(await finalizeOwnedRun({ run: ctx.run, success, keep })) };
} finally {
await stopBackend(ctx).catch(() => {});
for (const [key, value] of Object.entries(ctx.env)) {
if (priorEnv[key] === undefined) delete process.env[key];
else process.env[key] = priorEnv[key];
}
}
}
export async function main(argv = process.argv.slice(2), env = process.env) {
if (argv.length < 1 || argv[0] !== "integration" || argv.length > 2 || (argv[1] && argv[1] !== "--keep")) {
throw new Error("usage: p11-acceptance.mjs integration [--keep]");
}
const result = await runIntegration({ keep: argv.includes("--keep"), env });
return result.exitCode;
}
if (process.argv[1] && realpathSync(process.argv[1]) === modulePath) {
try {
const code = await main();
process.exitCode = code;
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
}
}