mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(verifiers): share package command and workspace ownership
Join canonical owned command cleanup before releasing package verifier workspaces. Replace raw package command wrappers, the updater supervisor, WASM cleanup composite, and boolean-only retention with explicit capability ownership. Preserve selected environments, npm arguments, no-deadline calls, clean exit status, and distinct aggregate/per-stream output budgets. Exercise actual timeout descendants, cancellation, retained cleanup retry, and workspace admission.
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
/** Preserve the caller's npm entry and arguments without invoking a shell. */
|
||||
export function npmInvocation(
|
||||
arguments_: readonly string[],
|
||||
environment: Readonly<NodeJS.ProcessEnv>,
|
||||
): { readonly command: string; readonly arguments: readonly string[] };
|
||||
@@ -0,0 +1,19 @@
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
/** Preserve the caller's npm entry and arguments without invoking a shell. */
|
||||
export const npmInvocation = (arguments_, environment) => {
|
||||
const selectedEntry = environment.npm_execpath;
|
||||
if (
|
||||
selectedEntry !== undefined &&
|
||||
(typeof selectedEntry !== "string" || selectedEntry.length === 0)
|
||||
)
|
||||
throw new Error("npm_execpath must name the selected npm CLI entry point");
|
||||
const entry =
|
||||
selectedEntry ??
|
||||
(process.platform === "win32"
|
||||
? join(dirname(process.execPath), "node_modules/npm/bin/npm-cli.js")
|
||||
: undefined);
|
||||
return entry === undefined
|
||||
? { command: "npm", arguments: arguments_ }
|
||||
: { command: process.execPath, arguments: [entry, ...arguments_] };
|
||||
};
|
||||
@@ -0,0 +1,332 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
import {
|
||||
ArtifactResourceScope,
|
||||
type ArtifactResourceOwner,
|
||||
} from "../../src/artifacts/ArtifactResourceScope.js";
|
||||
import { AnalysisCleanupLease } from "../../src/artifacts/AnalysisCleanupLease.js";
|
||||
import type { RetainedResourceScope } from "../../src/application/AnalysisProvider.js";
|
||||
import {
|
||||
prepareOwnedCommand,
|
||||
runOwnedCommand,
|
||||
} from "../../src/process/OwnedCommand.js";
|
||||
import {
|
||||
spawnOwnedProviderProcess,
|
||||
type OwnedProviderProcessSpawnOptions,
|
||||
type ProviderProcessSnapshot,
|
||||
type SpawnedOwnedProviderProcess,
|
||||
} from "../../src/process/ProviderProcess.js";
|
||||
import { retainOwnedCommandCleanup } from "../../src/providers/OwnedCommandCleanup.js";
|
||||
import { WindowsOwnedProcess } from "../../src/windows/WindowsOwnedProcess.js";
|
||||
import { npmInvocation } from "./npm-invocation.mjs";
|
||||
import { createWindowsOwnedCommand } from "./windows-owned-command.mjs";
|
||||
|
||||
export interface PackageVerifierWorkspace {
|
||||
readonly path: string;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
interface PackageCommandContext {
|
||||
readonly signal?: AbortSignal | undefined;
|
||||
readonly cwd?: string | undefined;
|
||||
readonly env?: NodeJS.ProcessEnv | undefined;
|
||||
readonly timeout?: number | undefined;
|
||||
readonly windowsHide?: boolean | undefined;
|
||||
}
|
||||
|
||||
export type PackageCommandOptions = PackageCommandContext &
|
||||
(
|
||||
| {
|
||||
/** execFile-compatible capacity for each output stream. */
|
||||
readonly maxBuffer?: number | undefined;
|
||||
readonly diagnosticBytes?: never;
|
||||
}
|
||||
| {
|
||||
/** Canonical command capacity across both streams. */
|
||||
readonly diagnosticBytes: number;
|
||||
readonly maxBuffer?: never;
|
||||
}
|
||||
);
|
||||
|
||||
export interface PackageCommandOutput {
|
||||
readonly stdout: string;
|
||||
readonly stderr: string;
|
||||
}
|
||||
|
||||
export interface PackageCleanupOwner {
|
||||
close(): Promise<void>;
|
||||
readonly cleanupResources?: readonly string[] | undefined;
|
||||
}
|
||||
|
||||
export interface PackageVerifierCommands {
|
||||
exec(
|
||||
command: string,
|
||||
arguments_: readonly string[],
|
||||
options?: PackageCommandOptions,
|
||||
): Promise<PackageCommandOutput>;
|
||||
execNpm(
|
||||
arguments_: readonly string[],
|
||||
options?: PackageCommandOptions,
|
||||
): Promise<PackageCommandOutput>;
|
||||
run(
|
||||
command: string,
|
||||
arguments_: readonly string[],
|
||||
environment?: NodeJS.ProcessEnv,
|
||||
): Promise<string>;
|
||||
runWithStatus(
|
||||
command: string,
|
||||
arguments_: readonly string[],
|
||||
environment?: NodeJS.ProcessEnv,
|
||||
): Promise<{ readonly stdout: string; readonly status: number }>;
|
||||
adopt(owner: PackageCleanupOwner, resource?: string): void;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
/** An ordinary nonzero exit whose process and output cleanup both completed. */
|
||||
export class PackageCommandExitError extends Error {
|
||||
readonly stdout: string;
|
||||
readonly stderr: string;
|
||||
constructor(
|
||||
readonly command: string,
|
||||
readonly arguments_: readonly string[],
|
||||
readonly code: number,
|
||||
readonly snapshot: ProviderProcessSnapshot,
|
||||
) {
|
||||
super(
|
||||
`Command failed: ${[command, ...arguments_].join(" ")}\n${snapshot.stderr.text}`,
|
||||
);
|
||||
this.name = "PackageCommandExitError";
|
||||
this.stdout = snapshot.stdout.text;
|
||||
this.stderr = snapshot.stderr.text;
|
||||
}
|
||||
}
|
||||
|
||||
/** The same command/workspace capability remains available after failed close. */
|
||||
export class PackageVerifierCleanupFailure extends Error {
|
||||
readonly path: string;
|
||||
constructor(
|
||||
cause: unknown,
|
||||
readonly cleanupOwner: PackageVerifierCommands,
|
||||
readonly workspace: PackageVerifierWorkspace,
|
||||
) {
|
||||
super(
|
||||
`Package verification cleanup incomplete; retained ${workspace.path}: ${cause instanceof Error ? cause.message : String(cause)}`,
|
||||
{ cause },
|
||||
);
|
||||
this.name = "PackageVerifierCleanupFailure";
|
||||
this.path = workspace.path;
|
||||
}
|
||||
}
|
||||
|
||||
const launchPackageCommand = async (
|
||||
input: OwnedProviderProcessSpawnOptions,
|
||||
): Promise<SpawnedOwnedProviderProcess> => {
|
||||
if (process.platform !== "win32") return spawnOwnedProviderProcess(input);
|
||||
input.signal?.throwIfAborted();
|
||||
const child = createWindowsOwnedCommand({
|
||||
WindowsOwnedProcess,
|
||||
command: input.command,
|
||||
arguments: input.arguments,
|
||||
cwd: input.cwd,
|
||||
environment: {
|
||||
...input.hostEnvironment,
|
||||
...input.env,
|
||||
REA_PROCESS_RUN_ID: input.runId,
|
||||
},
|
||||
});
|
||||
// These labels describe the launch; cleanup authority is the exact native job.
|
||||
return {
|
||||
process: child,
|
||||
ownership: {
|
||||
runId: input.runId,
|
||||
leaderPid: child.pid,
|
||||
processGroupId: child.pid,
|
||||
expectedParentPid: process.pid,
|
||||
},
|
||||
cleanup: () => child.cleanup(),
|
||||
};
|
||||
};
|
||||
|
||||
const commandLimits = (options: PackageCommandOptions) => {
|
||||
const timeout = options.timeout ?? 0;
|
||||
if (!Number.isSafeInteger(timeout) || timeout < 0)
|
||||
throw new RangeError(
|
||||
"Package command timeout must be a nonnegative integer",
|
||||
);
|
||||
if (options.maxBuffer !== undefined && options.diagnosticBytes !== undefined)
|
||||
throw new TypeError("Select a per-stream or aggregate command capacity");
|
||||
const capacity = options.diagnosticBytes ?? options.maxBuffer ?? 1024 * 1024;
|
||||
if (typeof capacity !== "number" || Number.isNaN(capacity) || capacity < 0)
|
||||
throw new RangeError("Package command output capacity must be nonnegative");
|
||||
if (process.platform === "win32" && options.windowsHide === false)
|
||||
throw new Error(
|
||||
"Owned package commands require hidden Windows process launch",
|
||||
);
|
||||
return {
|
||||
timeoutMs: timeout === 0 ? null : timeout,
|
||||
diagnosticBytes:
|
||||
options.diagnosticBytes === undefined
|
||||
? { stdout: capacity, stderr: capacity }
|
||||
: capacity,
|
||||
acceptNonZeroExit: true,
|
||||
};
|
||||
};
|
||||
|
||||
class PackageCommands implements PackageVerifierCommands {
|
||||
readonly #resources = new ArtifactResourceScope();
|
||||
readonly #controller = new AbortController();
|
||||
readonly #adopted = new WeakMap<PackageCleanupOwner, ArtifactResourceOwner>();
|
||||
readonly #resourceOwner: RetainedResourceScope;
|
||||
#closed = false;
|
||||
#workspaceReleaseStarted = false;
|
||||
#closing: Promise<void> | undefined;
|
||||
|
||||
constructor(
|
||||
readonly workspace: PackageVerifierWorkspace,
|
||||
readonly launcher: (
|
||||
input: OwnedProviderProcessSpawnOptions,
|
||||
) => Promise<SpawnedOwnedProviderProcess>,
|
||||
) {
|
||||
this.#resourceOwner = {
|
||||
retain: (owner, resource) => this.adopt(owner, resource),
|
||||
run: (work) => this.#resources.run(work),
|
||||
};
|
||||
}
|
||||
|
||||
exec(
|
||||
command: string,
|
||||
arguments_: readonly string[],
|
||||
options: PackageCommandOptions = {},
|
||||
): Promise<PackageCommandOutput> {
|
||||
if (this.#closed)
|
||||
return Promise.reject(new Error("Package command owner is closed"));
|
||||
return this.#resources.run(async () => {
|
||||
try {
|
||||
const limits = commandLimits(options);
|
||||
const signal =
|
||||
options.signal === undefined
|
||||
? this.#controller.signal
|
||||
: AbortSignal.any([this.#controller.signal, options.signal]);
|
||||
// Match execFile's command deadline while keeping native ownership
|
||||
// preparation independently bounded and inside this resource lifetime.
|
||||
await prepareOwnedCommand(signal);
|
||||
const snapshot = await runOwnedCommand(
|
||||
{
|
||||
command,
|
||||
arguments: arguments_,
|
||||
cwd: options.cwd ?? process.cwd(),
|
||||
hostEnvironment: options.env ?? process.env,
|
||||
expectedCommand: null,
|
||||
runId: `rea-package-command-${randomUUID()}`,
|
||||
sweepTokenOwnedProcesses: true,
|
||||
},
|
||||
limits,
|
||||
{
|
||||
signal,
|
||||
launcher: this.launcher,
|
||||
},
|
||||
);
|
||||
const output = {
|
||||
stdout: snapshot.stdout.text,
|
||||
stderr: snapshot.stderr.text,
|
||||
};
|
||||
if (typeof snapshot.exitCode === "number" && snapshot.exitCode !== 0)
|
||||
throw new PackageCommandExitError(
|
||||
command,
|
||||
arguments_,
|
||||
snapshot.exitCode,
|
||||
snapshot,
|
||||
);
|
||||
return output;
|
||||
} catch (cause: unknown) {
|
||||
retainOwnedCommandCleanup(
|
||||
cause,
|
||||
this.#resourceOwner,
|
||||
"package-verifier",
|
||||
command,
|
||||
);
|
||||
throw cause;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
execNpm(
|
||||
arguments_: readonly string[],
|
||||
options: PackageCommandOptions = {},
|
||||
): Promise<PackageCommandOutput> {
|
||||
const invocation = npmInvocation(arguments_, options.env ?? process.env);
|
||||
return this.exec(invocation.command, invocation.arguments, options);
|
||||
}
|
||||
|
||||
async run(
|
||||
command: string,
|
||||
arguments_: readonly string[],
|
||||
environment?: NodeJS.ProcessEnv,
|
||||
): Promise<string> {
|
||||
return (await this.exec(command, arguments_, { env: environment })).stdout;
|
||||
}
|
||||
|
||||
async runWithStatus(
|
||||
command: string,
|
||||
arguments_: readonly string[],
|
||||
environment?: NodeJS.ProcessEnv,
|
||||
): Promise<{ readonly stdout: string; readonly status: number }> {
|
||||
try {
|
||||
return {
|
||||
stdout: await this.run(command, arguments_, environment),
|
||||
status: 0,
|
||||
};
|
||||
} catch (cause: unknown) {
|
||||
if (cause instanceof PackageCommandExitError && cause.code === 1)
|
||||
return { stdout: cause.stdout, status: 1 };
|
||||
throw cause;
|
||||
}
|
||||
}
|
||||
|
||||
adopt(owner: PackageCleanupOwner, resource = this.workspace.path): void {
|
||||
if (this.#workspaceReleaseStarted)
|
||||
throw new Error("Package workspace release has started");
|
||||
let retained = this.#adopted.get(owner);
|
||||
if (retained === undefined) {
|
||||
retained = {
|
||||
kind: "lease",
|
||||
resource,
|
||||
lease: AnalysisCleanupLease.forCloseOwner(owner, resource),
|
||||
};
|
||||
this.#adopted.set(owner, retained);
|
||||
}
|
||||
this.#resources.retain(retained);
|
||||
}
|
||||
|
||||
close(): Promise<void> {
|
||||
this.#closed = true;
|
||||
this.#controller.abort(new Error("Package command owner closed"));
|
||||
if (this.#closing !== undefined) return this.#closing;
|
||||
let attempt: Promise<void>;
|
||||
attempt = this.#release().catch((cause: unknown) => {
|
||||
if (this.#closing === attempt) this.#closing = undefined;
|
||||
throw new PackageVerifierCleanupFailure(cause, this, this.workspace);
|
||||
});
|
||||
this.#closing = attempt;
|
||||
return attempt;
|
||||
}
|
||||
|
||||
async #release(): Promise<void> {
|
||||
await this.#resources.close();
|
||||
this.#workspaceReleaseStarted = true;
|
||||
await this.workspace.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** Allocate one command lifetime whose workspace depends on completed cleanup. */
|
||||
export async function createPackageVerifierCommands({
|
||||
workspace,
|
||||
launcher = launchPackageCommand,
|
||||
}: {
|
||||
readonly workspace: PackageVerifierWorkspace;
|
||||
readonly launcher?: (
|
||||
input: OwnedProviderProcessSpawnOptions,
|
||||
) => Promise<SpawnedOwnedProviderProcess>;
|
||||
}): Promise<PackageVerifierCommands> {
|
||||
return new PackageCommands(workspace, launcher);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export { createPackageVerifierCommands } from "./package-command-owner.mjs";
|
||||
export type {
|
||||
PackageVerifierCommands,
|
||||
PackageVerifierWorkspace,
|
||||
PackageCommandOptions,
|
||||
PackageCommandOutput,
|
||||
PackageCleanupOwner,
|
||||
} from "./package-command-owner.mjs";
|
||||
@@ -0,0 +1,13 @@
|
||||
let implementation;
|
||||
|
||||
/** Load the development owner as one source graph, only when commands are needed. */
|
||||
export async function createPackageVerifierCommands(options) {
|
||||
implementation ??= import("tsx/esm/api").then(({ tsImport }) =>
|
||||
tsImport(
|
||||
new URL("./package-command-owner.mts", import.meta.url).href,
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
const module = await implementation;
|
||||
return module.createPackageVerifierCommands(options);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { lstat } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
|
||||
@@ -8,55 +7,16 @@ import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
* `mcp-verifier-results.mjs` reaches into `dist/` at module load, so eager
|
||||
* importing it propagates that build dependency to every consumer in this
|
||||
* file. `verifyCompleteToolCatalog` is the only consumer, so we lazy-load
|
||||
* the module there and keep the other helpers (`exec`, `run`, `json`,
|
||||
* the module there and keep the remaining helpers (`exec`, `json`,
|
||||
* `pathExists`) usable from `test:fast` callers that only need `dist/`-free
|
||||
* utilities — see docs/testing.md § test scripts, which promises `test:fast`
|
||||
* runs "without build".
|
||||
*/
|
||||
const loadMcpVerifierResults = () => import("./mcp-verifier-results.mjs");
|
||||
|
||||
/** Promisified child_process execution helper. */
|
||||
/** Native-client commands expose the live child handle for caller-owned stdin. */
|
||||
export const exec = promisify(execFile);
|
||||
|
||||
/** Run npm through its JavaScript entry point so Windows command scripts are not required. */
|
||||
export const execNpm = (arguments_, options = {}) => {
|
||||
const environment = options.env ?? process.env;
|
||||
const selectedEntry = environment.npm_execpath;
|
||||
if (
|
||||
selectedEntry !== undefined &&
|
||||
(typeof selectedEntry !== "string" || selectedEntry.length === 0)
|
||||
)
|
||||
throw new Error("npm_execpath must name the selected npm CLI entry point");
|
||||
const entry =
|
||||
selectedEntry ??
|
||||
(process.platform === "win32"
|
||||
? join(dirname(process.execPath), "node_modules/npm/bin/npm-cli.js")
|
||||
: undefined);
|
||||
return entry === undefined
|
||||
? exec("npm", arguments_, options)
|
||||
: exec(process.execPath, [entry, ...arguments_], options);
|
||||
};
|
||||
|
||||
/** Run a command and return its stdout. */
|
||||
export const run = async (command, args, env) =>
|
||||
(await exec(command, args, { env })).stdout;
|
||||
|
||||
/** Run a command and treat exit code 1 as a non-throwing status result. */
|
||||
export const runWithStatus = async (command, args, env) => {
|
||||
try {
|
||||
return { stdout: await run(command, args, env), status: 0 };
|
||||
} catch (cause) {
|
||||
if (
|
||||
typeof cause === "object" &&
|
||||
cause !== null &&
|
||||
cause.code === 1 &&
|
||||
typeof cause.stdout === "string"
|
||||
)
|
||||
return { stdout: cause.stdout, status: 1 };
|
||||
throw cause;
|
||||
}
|
||||
};
|
||||
|
||||
/** Parse JSON from a command output string. */
|
||||
export const json = (text) => JSON.parse(text);
|
||||
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
#!/usr/bin/env node
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { access, readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, isAbsolute, join, resolve } from "node:path";
|
||||
import { setTimeout as pause } from "node:timers/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { PrivateRuntimeRoot } from "../dist/process/PrivateRuntimeRoot.js";
|
||||
import {
|
||||
OwnedCommandFailure,
|
||||
runOwnedCommand,
|
||||
} from "../dist/process/OwnedCommand.js";
|
||||
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
|
||||
import { execNpm } from "./lib/verify-package-core.mjs";
|
||||
import { createPackageVerifierCommands } from "./lib/verify-package-commands.mjs";
|
||||
|
||||
const AGENTENV_VERSION = "0.9.1334";
|
||||
const AGENTENV_PIN = `agentenv-framework ${AGENTENV_VERSION}`;
|
||||
@@ -34,7 +27,6 @@ const TEMPLATE_RULES = [
|
||||
"opaque-token",
|
||||
"sibling-variance",
|
||||
];
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
// Both tools are caller-supplied absolute paths, as every real-* lane names its upstream.
|
||||
for (const key of ["REA_AGENT_ENV_COMMAND", "REA_DOCKER_COMMAND"])
|
||||
@@ -62,8 +54,10 @@ const failures = [];
|
||||
let cases = 0;
|
||||
const receipt = {};
|
||||
let knownContainers;
|
||||
let commands;
|
||||
try {
|
||||
const version = await execFileAsync(agentEnv, ["--version"], {
|
||||
commands = await createPackageVerifierCommands({ workspace: runtime });
|
||||
const version = await commands.exec(agentEnv, ["--version"], {
|
||||
env: environment,
|
||||
timeout: 10_000,
|
||||
maxBuffer: 64 * 1024,
|
||||
@@ -124,7 +118,7 @@ try {
|
||||
await writeFile(capturePath, JSON.stringify(capture));
|
||||
await writeFile(manifestPath, JSON.stringify(manifest));
|
||||
// The lane proves this checkout, not the registry package: pack it and install the tarball in the image.
|
||||
const { stdout: packed } = await execNpm(
|
||||
const { stdout: packed } = await commands.execNpm(
|
||||
["pack", "--ignore-scripts", "--json", "--pack-destination", runtime.path],
|
||||
{ cwd: checkout, env: environment, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
@@ -179,20 +173,25 @@ try {
|
||||
attempts += 1;
|
||||
let outcome;
|
||||
try {
|
||||
outcome = await runOwnedCommand(
|
||||
const output = await commands.exec(
|
||||
agentEnv,
|
||||
["run", bundle, "--sandbox", "local"],
|
||||
{
|
||||
command: agentEnv,
|
||||
arguments: ["run", bundle, "--sandbox", "local"],
|
||||
cwd: runtime.path,
|
||||
runId: `rea-agentenv-run-${randomUUID()}`,
|
||||
hostEnvironment: environment,
|
||||
env: environment,
|
||||
timeout: timeoutMs,
|
||||
diagnosticBytes: 4 * 1024 * 1024,
|
||||
},
|
||||
{ timeoutMs, diagnosticBytes: 4 * 1024 * 1024 },
|
||||
);
|
||||
outcome = {
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
stdout: { text: output.stdout },
|
||||
stderr: { text: output.stderr },
|
||||
};
|
||||
return outcome;
|
||||
} catch (cause) {
|
||||
outcome =
|
||||
cause instanceof OwnedCommandFailure ? cause.snapshot : undefined;
|
||||
outcome = cause?.snapshot;
|
||||
throw cause;
|
||||
} finally {
|
||||
// Recorded before the outcome is judged, so a failed receipt still says how the last
|
||||
@@ -257,7 +256,8 @@ try {
|
||||
failures.push(cause);
|
||||
}
|
||||
try {
|
||||
await runtime.close();
|
||||
if (commands === undefined) await runtime.close();
|
||||
else await commands.close();
|
||||
} catch (cause) {
|
||||
failures.push(cause);
|
||||
}
|
||||
@@ -299,7 +299,7 @@ console.log(
|
||||
);
|
||||
|
||||
async function cli(...args) {
|
||||
const { stdout } = await execFileAsync(
|
||||
const { stdout } = await commands.exec(
|
||||
process.execPath,
|
||||
[entrypoint, ...args, "--json"],
|
||||
{ env: environment, cwd: runtime.path, maxBuffer: 64 * 1024 * 1024 },
|
||||
@@ -339,7 +339,7 @@ function packedFilename(report) {
|
||||
|
||||
// Containers are not verifier descendants, so leaks are checked by id against the pre-run set.
|
||||
async function containers() {
|
||||
const { stdout } = await execFileAsync(
|
||||
const { stdout } = await commands.exec(
|
||||
docker,
|
||||
["ps", "-a", "--format", "{{.ID}}\t{{.Names}}"],
|
||||
{ env: environment },
|
||||
@@ -355,7 +355,7 @@ async function containers() {
|
||||
}
|
||||
|
||||
async function imageIds() {
|
||||
const { stdout } = await execFileAsync(docker, ["images", "-q"], {
|
||||
const { stdout } = await commands.exec(docker, ["images", "-q"], {
|
||||
env: environment,
|
||||
});
|
||||
return [...new Set(stdout.split("\n").filter((line) => line.length > 0))];
|
||||
|
||||
@@ -3,20 +3,23 @@ import { isDeepStrictEqual } from "node:util";
|
||||
import { join } from "node:path";
|
||||
import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js";
|
||||
|
||||
import { json, run } from "./lib/verify-package-core.mjs";
|
||||
import { json } from "./lib/verify-package-core.mjs";
|
||||
|
||||
/** Run artifact and JavaScript application analysis against packaged fixtures. */
|
||||
export async function verifyPackageArtifactAndElectron({
|
||||
commands,
|
||||
cli,
|
||||
workspace,
|
||||
environment,
|
||||
}) {
|
||||
const artifactArchive = await verifyPackagedArtifact({
|
||||
commands,
|
||||
cli,
|
||||
workspace,
|
||||
environment,
|
||||
});
|
||||
await verifyPackagedElectronApplication({
|
||||
commands,
|
||||
cli,
|
||||
workspace,
|
||||
environment,
|
||||
@@ -24,20 +27,25 @@ export async function verifyPackageArtifactAndElectron({
|
||||
return { artifactArchive };
|
||||
}
|
||||
|
||||
const verifyPackagedArtifact = async ({ cli, workspace, environment }) => {
|
||||
const verifyPackagedArtifact = async ({
|
||||
commands,
|
||||
cli,
|
||||
workspace,
|
||||
environment,
|
||||
}) => {
|
||||
const artifactArchive = join(workspace, "artifact.zip");
|
||||
const artifactWriter = new ZipWriter(new Uint8ArrayWriter());
|
||||
await artifactWriter.add("app/main.js", new TextReader("main();"));
|
||||
await writeFile(artifactArchive, await artifactWriter.close());
|
||||
const artifactInspection = json(
|
||||
await run(
|
||||
await commands.run(
|
||||
cli,
|
||||
["inspect-artifact", artifactArchive, "--json"],
|
||||
environment,
|
||||
),
|
||||
);
|
||||
const repeatedArtifactInspection = json(
|
||||
await run(
|
||||
await commands.run(
|
||||
cli,
|
||||
["inspect-artifact", artifactArchive, "--json"],
|
||||
environment,
|
||||
@@ -57,6 +65,7 @@ const verifyPackagedArtifact = async ({ cli, workspace, environment }) => {
|
||||
)
|
||||
throw new Error("packaged artifact inventory CLI failed");
|
||||
await verifyPackagedArtifactExtraction({
|
||||
commands,
|
||||
artifactArchive,
|
||||
artifactInventory,
|
||||
cli,
|
||||
@@ -67,6 +76,7 @@ const verifyPackagedArtifact = async ({ cli, workspace, environment }) => {
|
||||
};
|
||||
|
||||
const verifyPackagedElectronApplication = async ({
|
||||
commands,
|
||||
cli,
|
||||
workspace,
|
||||
environment,
|
||||
@@ -86,7 +96,7 @@ const verifyPackagedElectronApplication = async ({
|
||||
'const { contextBridge, ipcRenderer } = require("electron");\ncontextBridge.exposeInMainWorld("rea", { ping: () => ipcRenderer.invoke("rea:ping") });\n',
|
||||
);
|
||||
const applicationAnalysis = json(
|
||||
await run(
|
||||
await commands.run(
|
||||
cli,
|
||||
["analyze-javascript-application", applicationRoot, "--json"],
|
||||
environment,
|
||||
@@ -101,7 +111,11 @@ const verifyPackagedElectronApplication = async ({
|
||||
)
|
||||
throw new Error("packaged JavaScript application analysis CLI failed");
|
||||
const routedApplicationAnalysis = json(
|
||||
await run(cli, ["analyze", applicationRoot, "--json"], environment),
|
||||
await commands.run(
|
||||
cli,
|
||||
["analyze", applicationRoot, "--json"],
|
||||
environment,
|
||||
),
|
||||
);
|
||||
assertRoutedApplicationAnalysis(routedApplicationAnalysis);
|
||||
};
|
||||
@@ -113,6 +127,7 @@ const sameArtifactIdentity = (left, right) =>
|
||||
isDeepStrictEqual(left?.edges, right?.edges);
|
||||
|
||||
const verifyPackagedArtifactExtraction = async ({
|
||||
commands,
|
||||
artifactArchive,
|
||||
artifactInventory,
|
||||
cli,
|
||||
@@ -124,7 +139,7 @@ const verifyPackagedArtifactExtraction = async ({
|
||||
if (occurrence?.logical_path === undefined)
|
||||
throw new Error("packaged artifact inventory omitted the selected member");
|
||||
const extraction = json(
|
||||
await run(
|
||||
await commands.run(
|
||||
cli,
|
||||
["extract-artifact", artifactArchive, "--json"],
|
||||
environment,
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import { json, run } from "./lib/verify-package-core.mjs";
|
||||
import { json } from "./lib/verify-package-core.mjs";
|
||||
import {
|
||||
expandCapabilitiesOutput,
|
||||
expandProviderOutput,
|
||||
} from "./lib/mcp-verifier-results.mjs";
|
||||
|
||||
/** Verify packaged capabilities, providers, and non-Linux search results. */
|
||||
export async function verifyPackageCapabilitiesAndSearch({ cli, environment }) {
|
||||
export async function verifyPackageCapabilitiesAndSearch({
|
||||
commands,
|
||||
cli,
|
||||
environment,
|
||||
}) {
|
||||
const capabilities = expandCapabilitiesOutput(
|
||||
json(await run(cli, ["capabilities", "--json"], environment)),
|
||||
json(await commands.run(cli, ["capabilities", "--json"], environment)),
|
||||
);
|
||||
if (!Array.isArray(capabilities.capabilities))
|
||||
throw new Error("packaged capabilities CLI failed");
|
||||
const providers = expandProviderOutput(
|
||||
json(await run(cli, ["providers", "--json"], environment)),
|
||||
json(await commands.run(cli, ["providers", "--json"], environment)),
|
||||
);
|
||||
const candidates = providers.analysis_provider_candidates;
|
||||
if (
|
||||
@@ -29,7 +33,7 @@ export async function verifyPackageCapabilitiesAndSearch({ cli, environment }) {
|
||||
throw new Error("packaged providers CLI failed");
|
||||
if (process.platform !== "linux") {
|
||||
const searchResult = json(
|
||||
await run(
|
||||
await commands.run(
|
||||
cli,
|
||||
[
|
||||
"search",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { PackageVerifierCommands } from "./lib/package-command-owner.mjs";
|
||||
|
||||
/**
|
||||
* Host facts the packaged setup support is derived from.
|
||||
*
|
||||
@@ -31,6 +33,7 @@ export function packageSetupSupport(
|
||||
|
||||
/** Validate --help, --llms, and doctor output and determine host setup support. */
|
||||
export function verifyPackageDiscovery(input: {
|
||||
readonly commands: Pick<PackageVerifierCommands, "run" | "runWithStatus">;
|
||||
readonly cli: string;
|
||||
readonly environment: Readonly<Record<string, string | undefined>>;
|
||||
}): Promise<{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { json, run, runWithStatus } from "./lib/verify-package-core.mjs";
|
||||
import { json } from "./lib/verify-package-core.mjs";
|
||||
|
||||
const REQUIRED_HELP_COMMANDS = [
|
||||
"setup",
|
||||
@@ -79,10 +79,10 @@ async function readHostLinuxDistribution() {
|
||||
}
|
||||
|
||||
/** Validate --help, --llms, and doctor output and determine host setup support. */
|
||||
export async function verifyPackageDiscovery({ cli, environment }) {
|
||||
const help = await run(cli, ["--help"], environment);
|
||||
const llms = await run(cli, ["--llms"], environment);
|
||||
const doctorExecution = await runWithStatus(
|
||||
export async function verifyPackageDiscovery({ commands, cli, environment }) {
|
||||
const help = await commands.run(cli, ["--help"], environment);
|
||||
const llms = await commands.run(cli, ["--llms"], environment);
|
||||
const doctorExecution = await commands.runWithStatus(
|
||||
cli,
|
||||
["doctor", "--json"],
|
||||
environment,
|
||||
|
||||
@@ -2,17 +2,18 @@ import { readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { createEvidence } from "../dist/domain/evidence.js";
|
||||
import { json, run } from "./lib/verify-package-core.mjs";
|
||||
import { json } from "./lib/verify-package-core.mjs";
|
||||
|
||||
/** Round-trip historical reference import and evidence bundles. */
|
||||
export async function verifyPackageEvidence({
|
||||
commands,
|
||||
cli,
|
||||
evidenceRoot,
|
||||
referenceRoot,
|
||||
environment,
|
||||
}) {
|
||||
const referenceImport = json(
|
||||
await run(
|
||||
await commands.run(
|
||||
cli,
|
||||
["import-reference-source", referenceRoot, "--json"],
|
||||
environment,
|
||||
@@ -39,21 +40,21 @@ export async function verifyPackageEvidence({
|
||||
const canonicalBundlePath = join(evidenceRoot, "canonical.json");
|
||||
await writeFile(sourceBundlePath, serializeEvidenceBundle(sourceBundle));
|
||||
const importedBundle = json(
|
||||
await run(
|
||||
await commands.run(
|
||||
cli,
|
||||
["evidence-import", sourceBundlePath, "--json"],
|
||||
environment,
|
||||
),
|
||||
);
|
||||
const exportedBundle = json(
|
||||
await run(
|
||||
await commands.run(
|
||||
cli,
|
||||
["evidence-export", sourceBundlePath, canonicalBundlePath, "--json"],
|
||||
environment,
|
||||
),
|
||||
);
|
||||
const comparedBundle = json(
|
||||
await run(
|
||||
await commands.run(
|
||||
cli,
|
||||
["compare", sourceBundlePath, canonicalBundlePath, "--json"],
|
||||
environment,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
import { exec, execNpm, json, run } from "./lib/verify-package-core.mjs";
|
||||
import { json } from "./lib/verify-package-core.mjs";
|
||||
|
||||
/** Install the packed CLI globally with and without optional dependencies and verify PTY degradation. */
|
||||
export async function verifyPackageInstall({
|
||||
@@ -10,8 +10,9 @@ export async function verifyPackageInstall({
|
||||
prefix,
|
||||
workspace,
|
||||
environment,
|
||||
commands,
|
||||
}) {
|
||||
await execNpm(
|
||||
await commands.execNpm(
|
||||
[
|
||||
"install",
|
||||
"--global",
|
||||
@@ -26,7 +27,11 @@ export async function verifyPackageInstall({
|
||||
);
|
||||
const cli = join(prefix, "bin", "rea");
|
||||
const packageRunnerCli = join(prefix, "bin", "rea-agents");
|
||||
if (!(await run(packageRunnerCli, ["--help"], environment)).includes("setup"))
|
||||
if (
|
||||
!(await commands.run(packageRunnerCli, ["--help"], environment)).includes(
|
||||
"setup",
|
||||
)
|
||||
)
|
||||
throw new Error("packaged rea-agents alias did not expose setup");
|
||||
const processCaptureCapabilityUrl = pathToFileURL(
|
||||
join(
|
||||
@@ -36,7 +41,7 @@ export async function verifyPackageInstall({
|
||||
).href;
|
||||
const processCaptureCapability = json(
|
||||
(
|
||||
await exec(
|
||||
await commands.exec(
|
||||
process.execPath,
|
||||
[
|
||||
"--input-type=module",
|
||||
@@ -52,7 +57,7 @@ export async function verifyPackageInstall({
|
||||
`packaged PTY backend failed without lifecycle scripts: ${JSON.stringify(processCaptureCapability)}`,
|
||||
);
|
||||
const noOptionalPrefix = join(workspace, "prefix-no-optional");
|
||||
await execNpm(
|
||||
await commands.execNpm(
|
||||
[
|
||||
"install",
|
||||
"--global",
|
||||
@@ -81,7 +86,7 @@ export async function verifyPackageInstall({
|
||||
).href;
|
||||
const noOptionalCapability = json(
|
||||
(
|
||||
await exec(
|
||||
await commands.exec(
|
||||
process.execPath,
|
||||
[
|
||||
"--input-type=module",
|
||||
@@ -93,20 +98,26 @@ export async function verifyPackageInstall({
|
||||
).stdout,
|
||||
);
|
||||
if (
|
||||
!(await run(noOptionalCli, ["--help"], environment)).includes("setup") ||
|
||||
!(await commands.run(noOptionalCli, ["--help"], environment)).includes(
|
||||
"setup",
|
||||
) ||
|
||||
noOptionalCapability.available !== false
|
||||
)
|
||||
throw new Error(
|
||||
`packaged CLI did not degrade without the optional PTY binary: ${JSON.stringify(noOptionalCapability)}`,
|
||||
);
|
||||
await verifyInstalledPackageCanRepack(prefix, environment);
|
||||
await verifyInstalledPackageCanRepack(commands, prefix, environment);
|
||||
return { cli, packageRunnerCli };
|
||||
}
|
||||
|
||||
const verifyInstalledPackageCanRepack = async (prefix, environment) => {
|
||||
const verifyInstalledPackageCanRepack = async (
|
||||
commands,
|
||||
prefix,
|
||||
environment,
|
||||
) => {
|
||||
const installedPackageRoot = join(prefix, "lib/node_modules/rea-agents");
|
||||
const repackedTarball = (
|
||||
await execNpm(["pack", "--silent"], {
|
||||
await commands.execNpm(["pack", "--silent"], {
|
||||
cwd: installedPackageRoot,
|
||||
env: environment,
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ import { join } from "node:path";
|
||||
import { createEvidence } from "../dist/domain/evidence.js";
|
||||
import { buildManagedPeFixture } from "./fixtures/managed/pe.mjs";
|
||||
import { functionDossier } from "./fixtures/managed/function-dossier.mjs";
|
||||
import { json, run } from "./lib/verify-package-core.mjs";
|
||||
import { json } from "./lib/verify-package-core.mjs";
|
||||
|
||||
const buildManagedFixtures = async (workspace) => {
|
||||
const managedPath = join(workspace, "managed-fixture.exe");
|
||||
@@ -38,9 +38,14 @@ const buildManagedFixtures = async (workspace) => {
|
||||
return { managedPath, managedRightPath };
|
||||
};
|
||||
|
||||
const verifyManagedArtifact = async ({ cli, managedPath, environment }) => {
|
||||
const verifyManagedArtifact = async ({
|
||||
commands,
|
||||
cli,
|
||||
managedPath,
|
||||
environment,
|
||||
}) => {
|
||||
const managedArtifact = json(
|
||||
await run(
|
||||
await commands.run(
|
||||
cli,
|
||||
["inspect-managed-artifact", managedPath, "--json"],
|
||||
environment,
|
||||
@@ -55,9 +60,14 @@ const verifyManagedArtifact = async ({ cli, managedPath, environment }) => {
|
||||
return managedArtifact;
|
||||
};
|
||||
|
||||
const verifyManagedMembers = async ({ cli, managedPath, environment }) => {
|
||||
const verifyManagedMembers = async ({
|
||||
commands,
|
||||
cli,
|
||||
managedPath,
|
||||
environment,
|
||||
}) => {
|
||||
const managedMembers = json(
|
||||
await run(
|
||||
await commands.run(
|
||||
cli,
|
||||
["inspect-managed-members", managedPath, "--json"],
|
||||
environment,
|
||||
@@ -72,6 +82,7 @@ const verifyManagedMembers = async ({ cli, managedPath, environment }) => {
|
||||
};
|
||||
|
||||
const verifyManagedReconstruction = async ({
|
||||
commands,
|
||||
cli,
|
||||
managedMembers,
|
||||
managedMethod,
|
||||
@@ -101,7 +112,7 @@ const verifyManagedReconstruction = async ({
|
||||
notes: ["packaged synthetic reconstruction import"],
|
||||
};
|
||||
const managedReconstruction = json(
|
||||
await run(
|
||||
await commands.run(
|
||||
cli,
|
||||
[
|
||||
"import-managed-reconstruction",
|
||||
@@ -121,9 +132,14 @@ const verifyManagedReconstruction = async ({
|
||||
throw new Error("packaged managed reconstruction import CLI failed");
|
||||
};
|
||||
|
||||
const verifyManagedBoundaries = async ({ cli, managedPath, environment }) => {
|
||||
const verifyManagedBoundaries = async ({
|
||||
commands,
|
||||
cli,
|
||||
managedPath,
|
||||
environment,
|
||||
}) => {
|
||||
const managedBoundaries = json(
|
||||
await run(
|
||||
await commands.run(
|
||||
cli,
|
||||
["inspect-managed-native-boundaries", managedPath, "--json"],
|
||||
environment,
|
||||
@@ -140,6 +156,7 @@ const verifyManagedBoundaries = async ({ cli, managedPath, environment }) => {
|
||||
};
|
||||
|
||||
const verifyManagedApplicationGraph = async ({
|
||||
commands,
|
||||
cli,
|
||||
managedArtifact,
|
||||
managedMembers,
|
||||
@@ -158,7 +175,7 @@ const verifyManagedApplicationGraph = async ({
|
||||
),
|
||||
};
|
||||
const managedApplicationGraph = json(
|
||||
await run(
|
||||
await commands.run(
|
||||
cli,
|
||||
[
|
||||
"project-managed-application-graph",
|
||||
@@ -209,6 +226,7 @@ const managedEvidence = (output, operation) => {
|
||||
};
|
||||
|
||||
const verifyManagedNativeVerification = async ({
|
||||
commands,
|
||||
cli,
|
||||
managedBoundaries,
|
||||
environment,
|
||||
@@ -243,7 +261,7 @@ const verifyManagedNativeVerification = async ({
|
||||
],
|
||||
};
|
||||
const managedNativeVerification = json(
|
||||
await run(
|
||||
await commands.run(
|
||||
cli,
|
||||
[
|
||||
"verify-managed-native-boundaries",
|
||||
@@ -265,13 +283,14 @@ const verifyManagedNativeVerification = async ({
|
||||
};
|
||||
|
||||
const verifyManagedComparison = async ({
|
||||
commands,
|
||||
cli,
|
||||
managedPath,
|
||||
managedRightPath,
|
||||
environment,
|
||||
}) => {
|
||||
const managedComparison = json(
|
||||
await run(
|
||||
await commands.run(
|
||||
cli,
|
||||
["compare-managed-members", managedPath, managedRightPath, "--json"],
|
||||
environment,
|
||||
@@ -287,32 +306,37 @@ const verifyManagedComparison = async ({
|
||||
};
|
||||
|
||||
/** Exercise all packaged managed static and workflow CLIs. */
|
||||
export async function verifyManaged({ cli, workspace, environment }) {
|
||||
export async function verifyManaged({ commands, cli, workspace, environment }) {
|
||||
const { managedPath, managedRightPath } =
|
||||
await buildManagedFixtures(workspace);
|
||||
const managedArtifact = await verifyManagedArtifact({
|
||||
commands,
|
||||
cli,
|
||||
managedPath,
|
||||
environment,
|
||||
});
|
||||
const managedMembers = await verifyManagedMembers({
|
||||
commands,
|
||||
cli,
|
||||
managedPath,
|
||||
environment,
|
||||
});
|
||||
const managedMethod = managedMembers.normalized_result.methods[0];
|
||||
await verifyManagedReconstruction({
|
||||
commands,
|
||||
cli,
|
||||
managedMembers,
|
||||
managedMethod,
|
||||
environment,
|
||||
});
|
||||
const managedBoundaries = await verifyManagedBoundaries({
|
||||
commands,
|
||||
cli,
|
||||
managedPath,
|
||||
environment,
|
||||
});
|
||||
await verifyManagedApplicationGraph({
|
||||
commands,
|
||||
cli,
|
||||
managedArtifact,
|
||||
managedMembers,
|
||||
@@ -320,11 +344,13 @@ export async function verifyManaged({ cli, workspace, environment }) {
|
||||
environment,
|
||||
});
|
||||
await verifyManagedNativeVerification({
|
||||
commands,
|
||||
cli,
|
||||
managedBoundaries,
|
||||
environment,
|
||||
});
|
||||
await verifyManagedComparison({
|
||||
commands,
|
||||
cli,
|
||||
managedPath,
|
||||
managedRightPath,
|
||||
|
||||
@@ -4,8 +4,6 @@ import {
|
||||
} from "./lib/mcp-verifier-results.mjs";
|
||||
import assert from "node:assert/strict";
|
||||
import { join } from "node:path";
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { readFile, rm } from "node:fs/promises";
|
||||
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
@@ -14,8 +12,7 @@ import { resolveMcpStartupBudgets } from "../dist/config/mcpStartupBudgets.js";
|
||||
import { parseEvidence } from "../dist/domain/evidence.js";
|
||||
import * as prompts from "./verify-package-prompts.mjs";
|
||||
import { json, verifyCompleteToolCatalog } from "./lib/verify-package-core.mjs";
|
||||
|
||||
const execute = promisify(execFile);
|
||||
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
|
||||
|
||||
const verifyMcpToolsAndPrompts = async (client, mcpOptions) => {
|
||||
await verifyCompleteToolCatalog(client, mcpOptions);
|
||||
@@ -248,6 +245,7 @@ const verifyMcpInlineArtifactEvidence = async (
|
||||
|
||||
/** Connect to the packaged MCP server and verify catalog and Evidence composition. */
|
||||
export async function verifyPackageMcp({
|
||||
commands,
|
||||
cli,
|
||||
environment,
|
||||
evidenceRoot,
|
||||
@@ -255,7 +253,7 @@ export async function verifyPackageMcp({
|
||||
}) {
|
||||
const diagnosed = json(
|
||||
(
|
||||
await execute(cli, ["mcp", "doctor", "--json"], {
|
||||
await commands.exec(cli, ["mcp", "doctor", "--json"], {
|
||||
env: environment,
|
||||
timeout: resolveMcpStartupBudgets(environment).doctor.milliseconds,
|
||||
})
|
||||
@@ -280,19 +278,39 @@ export async function verifyPackageMcp({
|
||||
mcpStderr += chunk.toString();
|
||||
});
|
||||
const client = new Client({ name: "package-smoke", version: "1.0.0" });
|
||||
try {
|
||||
await client.connect(transport);
|
||||
const mcpOptions = { timeout: 15_000 };
|
||||
await verifyMcpToolsAndPrompts(client, mcpOptions);
|
||||
await verifyMcpTargetFree(client, mcpOptions);
|
||||
await verifyMcpUnknownProvider(client, mcpOptions);
|
||||
await verifyMcpBinaryLifecycle(client, mcpOptions);
|
||||
await verifyMcpInlineArtifactEvidence(client, mcpOptions, artifactArchive);
|
||||
await verifyMcpEvidenceBundle(client, mcpOptions, evidenceRoot);
|
||||
} catch (cause) {
|
||||
throw new Error(`packaged MCP smoke failed: ${mcpStderr}`, { cause });
|
||||
} finally {
|
||||
await client.close();
|
||||
await transport.close();
|
||||
}
|
||||
await withVerifierCleanup(async () => {
|
||||
try {
|
||||
await client.connect(transport);
|
||||
const mcpOptions = { timeout: 15_000 };
|
||||
await verifyMcpToolsAndPrompts(client, mcpOptions);
|
||||
await verifyMcpTargetFree(client, mcpOptions);
|
||||
await verifyMcpUnknownProvider(client, mcpOptions);
|
||||
await verifyMcpBinaryLifecycle(client, mcpOptions);
|
||||
await verifyMcpInlineArtifactEvidence(
|
||||
client,
|
||||
mcpOptions,
|
||||
artifactArchive,
|
||||
);
|
||||
await verifyMcpEvidenceBundle(client, mcpOptions, evidenceRoot);
|
||||
} catch (cause) {
|
||||
throw new Error(`packaged MCP smoke failed: ${mcpStderr}`, { cause });
|
||||
}
|
||||
}, [
|
||||
async () => {
|
||||
try {
|
||||
await client.close();
|
||||
} catch (cause) {
|
||||
commands.adopt(client, "packaged MCP client");
|
||||
throw cause;
|
||||
}
|
||||
},
|
||||
async () => {
|
||||
try {
|
||||
await transport.close();
|
||||
} catch (cause) {
|
||||
commands.adopt(transport, "packaged MCP transport");
|
||||
throw cause;
|
||||
}
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -2,27 +2,30 @@ import { mkdir } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { FULL_CATALOG_IDENTITY } from "../dist/catalogIdentity.js";
|
||||
|
||||
import { exec, execNpm } from "./lib/verify-package-core.mjs";
|
||||
import { parseTarListing } from "./lib/verify-package-listing.mjs";
|
||||
import { verifyPackedBridge } from "./verify-packed-bridge.mjs";
|
||||
|
||||
/** Create a tarball and assert production packaging constraints. */
|
||||
export async function verifyPackagePack({ root, workspace }) {
|
||||
export async function verifyPackagePack({ root, workspace, commands }) {
|
||||
const packDirectory = join(workspace, "pack");
|
||||
await mkdir(packDirectory);
|
||||
const tarball = join(
|
||||
packDirectory,
|
||||
(
|
||||
await execNpm(["pack", "--silent", "--pack-destination", packDirectory], {
|
||||
cwd: root,
|
||||
})
|
||||
await commands.execNpm(
|
||||
["pack", "--silent", "--pack-destination", packDirectory],
|
||||
{
|
||||
cwd: root,
|
||||
},
|
||||
)
|
||||
).stdout.trim(),
|
||||
);
|
||||
const packedFiles = parseTarListing(
|
||||
(await exec("tar", ["-tf", tarball])).stdout,
|
||||
(await commands.exec("tar", ["-tf", tarball])).stdout,
|
||||
);
|
||||
const packedManifest = JSON.parse(
|
||||
(await exec("tar", ["-xOf", tarball, "package/package.json"])).stdout,
|
||||
(await commands.exec("tar", ["-xOf", tarball, "package/package.json"]))
|
||||
.stdout,
|
||||
);
|
||||
if (
|
||||
packedManifest.scripts?.postinstall !== undefined ||
|
||||
@@ -46,7 +49,7 @@ export async function verifyPackagePack({ root, workspace }) {
|
||||
if (packedFiles.some((path) => path.startsWith("package/.cache/")))
|
||||
throw new Error("package included development cache or test metadata");
|
||||
const skill = (
|
||||
await exec("tar", [
|
||||
await commands.exec("tar", [
|
||||
"-xOf",
|
||||
tarball,
|
||||
"package/skills/reverse-engineer-anything/SKILL.md",
|
||||
@@ -68,6 +71,6 @@ export async function verifyPackagePack({ root, workspace }) {
|
||||
) {
|
||||
throw new Error("package contained generated Python bytecode");
|
||||
}
|
||||
await verifyPackedBridge({ tarball, packedFiles });
|
||||
await verifyPackedBridge({ tarball, packedFiles, commands });
|
||||
return { tarball };
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { PackageVerifierCommands } from "./lib/package-command-owner.mjs";
|
||||
|
||||
/** Validate the expected fail-closed Linux fixture outcome. */
|
||||
export function assertLinuxPackageProviderFailure(execution: {
|
||||
readonly status: number | null;
|
||||
@@ -6,12 +8,8 @@ export function assertLinuxPackageProviderFailure(execution: {
|
||||
|
||||
/** Run platform-specific packaged analysis checks. */
|
||||
export function verifyPackagePlatform(input: {
|
||||
readonly commands: Pick<PackageVerifierCommands, "run" | "runWithStatus">;
|
||||
readonly cli: string;
|
||||
readonly environment: Readonly<Record<string, string | undefined>>;
|
||||
readonly platform?: string;
|
||||
readonly runCommand?: (
|
||||
command: string,
|
||||
arguments_: readonly string[],
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
) => Promise<string>;
|
||||
}): Promise<void>;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { json, run, runWithStatus } from "./lib/verify-package-core.mjs";
|
||||
import { json } from "./lib/verify-package-core.mjs";
|
||||
|
||||
/** Run platform-specific deep-analysis smoke tests. */
|
||||
export async function verifyPackagePlatform({
|
||||
commands,
|
||||
cli,
|
||||
environment,
|
||||
platform = process.platform,
|
||||
runCommand = run,
|
||||
}) {
|
||||
if (platform === "linux") {
|
||||
const unsupportedExecution = await runWithStatus(
|
||||
const unsupportedExecution = await commands.runWithStatus(
|
||||
cli,
|
||||
["analyze", process.execPath, "--provider", "hopper", "--json"],
|
||||
environment,
|
||||
@@ -16,7 +16,7 @@ export async function verifyPackagePlatform({
|
||||
assertLinuxPackageProviderFailure(unsupportedExecution);
|
||||
} else {
|
||||
const overview = json(
|
||||
await runCommand(
|
||||
await commands.run(
|
||||
cli,
|
||||
["analyze", process.execPath, "--provider", "hopper", "--json"],
|
||||
environment,
|
||||
@@ -27,7 +27,7 @@ export async function verifyPackagePlatform({
|
||||
`packaged Hopper-backed analyze CLI failed: ${JSON.stringify(overview)}`,
|
||||
);
|
||||
const inspected = json(
|
||||
await runCommand(
|
||||
await commands.run(
|
||||
cli,
|
||||
["inspect", process.execPath, "--provider", "hopper", "--json"],
|
||||
environment,
|
||||
@@ -36,7 +36,7 @@ export async function verifyPackagePlatform({
|
||||
if (!hasPositiveProcedureCount(inspected))
|
||||
throw new Error("packaged inspect CLI failed");
|
||||
const functionResult = json(
|
||||
await runCommand(
|
||||
await commands.run(
|
||||
cli,
|
||||
[
|
||||
"function",
|
||||
@@ -58,7 +58,7 @@ export async function verifyPackagePlatform({
|
||||
`packaged function CLI failed: ${JSON.stringify(functionResult)}`,
|
||||
);
|
||||
const xrefs = json(
|
||||
await runCommand(
|
||||
await commands.run(
|
||||
cli,
|
||||
["xrefs", process.execPath, "0x1000", "--provider", "hopper", "--json"],
|
||||
environment,
|
||||
@@ -70,7 +70,7 @@ export async function verifyPackagePlatform({
|
||||
)
|
||||
throw new Error(`packaged xrefs CLI failed: ${JSON.stringify(xrefs)}`);
|
||||
const trace = json(
|
||||
await runCommand(
|
||||
await commands.run(
|
||||
cli,
|
||||
[
|
||||
"trace",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { PackageVerifierCommands } from "./lib/package-command-owner.mjs";
|
||||
|
||||
/** Validate a live registration and its byte-identical pre-setup backup. */
|
||||
export function assertClientConfig(
|
||||
cli: string,
|
||||
@@ -7,3 +9,21 @@ export function assertClientConfig(
|
||||
|
||||
/** Return true only when stty output positively reports canonical input and echo. */
|
||||
export function terminalModesRestored(termios: unknown): boolean;
|
||||
|
||||
/** Verify setup using the parent's explicit command owner. */
|
||||
export function verifyPackageSetup(input: {
|
||||
readonly commands: Pick<PackageVerifierCommands, "run" | "runWithStatus">;
|
||||
readonly cli: string;
|
||||
readonly packageRunnerCli: string;
|
||||
readonly environment: NodeJS.ProcessEnv;
|
||||
readonly home: string;
|
||||
readonly npxLog: string;
|
||||
readonly claudeConfig: string;
|
||||
readonly codexConfig: string;
|
||||
readonly cursorConfig: string;
|
||||
readonly codexTarget: string;
|
||||
readonly cursorTarget: string;
|
||||
readonly supportedSetupHost: boolean;
|
||||
readonly hopperSetupSupported: boolean | undefined;
|
||||
readonly root: string;
|
||||
}): Promise<void>;
|
||||
|
||||
@@ -13,12 +13,7 @@ import { spawn } from "@lydell/node-pty";
|
||||
|
||||
import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
|
||||
import { PRODUCT_IDENTITY } from "../dist/identity.js";
|
||||
import {
|
||||
json,
|
||||
pathExists,
|
||||
run,
|
||||
runWithStatus,
|
||||
} from "./lib/verify-package-core.mjs";
|
||||
import { json, pathExists } from "./lib/verify-package-core.mjs";
|
||||
import { parse as parseJsonc } from "jsonc-parser";
|
||||
import { skillReferenceIssues } from "./lib/docs-facts.mjs";
|
||||
import { setupPlanFailureMessage } from "./verify-package-setup-plan.mjs";
|
||||
@@ -46,8 +41,8 @@ const CODEX_ORIGINAL = [
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
const verifyMcpAdd = async ({ cli, environment, npxLog }) => {
|
||||
await run(cli, ["mcp", "add"], environment);
|
||||
const verifyMcpAdd = async ({ commands, cli, environment, npxLog }) => {
|
||||
await commands.run(cli, ["mcp", "add"], environment);
|
||||
const mcpRegistration = await readFile(npxLog, "utf8");
|
||||
const expected = `add-mcp npx -y ${PRODUCT_IDENTITY.registrationPackageSpecifier} mcp --name rea`;
|
||||
if (!mcpRegistration.includes(expected))
|
||||
@@ -235,6 +230,7 @@ const verifyRedirectedStdoutSetup = async ({
|
||||
});
|
||||
|
||||
const verifySetupPlan = async ({
|
||||
commands,
|
||||
cli,
|
||||
environment,
|
||||
claudeConfig,
|
||||
@@ -244,7 +240,7 @@ const verifySetupPlan = async ({
|
||||
opencodeOriginal,
|
||||
supportedSetupHost,
|
||||
}) => {
|
||||
const plannedExecution = await runWithStatus(
|
||||
const plannedExecution = await commands.runWithStatus(
|
||||
cli,
|
||||
["setup", "--all-detected", "--dry-run", "--json"],
|
||||
environment,
|
||||
@@ -287,12 +283,17 @@ const verifySetupPlan = async ({
|
||||
return { planned };
|
||||
};
|
||||
|
||||
const verifyAbsentOpenCodeSetup = async ({ cli, environment, home }) => {
|
||||
const verifyAbsentOpenCodeSetup = async ({
|
||||
commands,
|
||||
cli,
|
||||
environment,
|
||||
home,
|
||||
}) => {
|
||||
const xdgConfigHome = join(home, ".xdg-opencode-absent");
|
||||
const configPath = join(xdgConfigHome, "opencode", "opencode.json");
|
||||
if (await pathExists(configPath))
|
||||
throw new Error("packaged OpenCode first-run fixture was not absent");
|
||||
const execution = await runWithStatus(
|
||||
const execution = await commands.runWithStatus(
|
||||
cli,
|
||||
["setup", "--yes", "--client", "opencode", "--skill=false", "--json"],
|
||||
{ ...environment, XDG_CONFIG_HOME: xdgConfigHome },
|
||||
@@ -313,7 +314,12 @@ const verifyAbsentOpenCodeSetup = async ({ cli, environment, home }) => {
|
||||
await rm(xdgConfigHome, { recursive: true, force: true });
|
||||
};
|
||||
|
||||
const verifyAbsentClaudeCodeSetup = async ({ cli, environment, home }) => {
|
||||
const verifyAbsentClaudeCodeSetup = async ({
|
||||
commands,
|
||||
cli,
|
||||
environment,
|
||||
home,
|
||||
}) => {
|
||||
const freshHome = join(home, ".claude-code-absent-home");
|
||||
const configPath = join(freshHome, ".claude.json");
|
||||
const markerPath = join(freshHome, ".claude");
|
||||
@@ -336,7 +342,11 @@ const verifyAbsentClaudeCodeSetup = async ({ cli, environment, home }) => {
|
||||
"--skill=false",
|
||||
"--json",
|
||||
];
|
||||
const firstExecution = await runWithStatus(cli, args, childEnvironment);
|
||||
const firstExecution = await commands.runWithStatus(
|
||||
cli,
|
||||
args,
|
||||
childEnvironment,
|
||||
);
|
||||
const first = json(firstExecution.stdout);
|
||||
const config = json(await readFile(configPath, "utf8"));
|
||||
const registration = first.doctor.identity?.registrations?.find(
|
||||
@@ -355,7 +365,11 @@ const verifyAbsentClaudeCodeSetup = async ({ cli, environment, home }) => {
|
||||
throw new Error(
|
||||
`packaged setup did not configure an explicitly selected Claude Code with no marker directory: ${JSON.stringify(first)}`,
|
||||
);
|
||||
const repeatExecution = await runWithStatus(cli, args, childEnvironment);
|
||||
const repeatExecution = await commands.runWithStatus(
|
||||
cli,
|
||||
args,
|
||||
childEnvironment,
|
||||
);
|
||||
const repeat = json(repeatExecution.stdout);
|
||||
if (
|
||||
repeatExecution.status !== 0 ||
|
||||
@@ -370,6 +384,7 @@ const verifyAbsentClaudeCodeSetup = async ({ cli, environment, home }) => {
|
||||
};
|
||||
|
||||
const verifySetupApply = async ({
|
||||
commands,
|
||||
cli,
|
||||
environment,
|
||||
supportedSetupHost,
|
||||
@@ -378,19 +393,19 @@ const verifySetupApply = async ({
|
||||
const status = "ready";
|
||||
const hopperStatus =
|
||||
hopperSetupSupported === true ? "needs_confirmation" : "needs_human";
|
||||
const firstExecution = await runWithStatus(
|
||||
const firstExecution = await commands.runWithStatus(
|
||||
cli,
|
||||
["setup", "--yes", "--all-detected", "--json"],
|
||||
environment,
|
||||
);
|
||||
const first = json(firstExecution.stdout);
|
||||
const alignedExecution = await runWithStatus(
|
||||
const alignedExecution = await commands.runWithStatus(
|
||||
cli,
|
||||
["setup", "--all-detected", "--json"],
|
||||
environment,
|
||||
);
|
||||
const aligned = json(alignedExecution.stdout);
|
||||
const secondExecution = await runWithStatus(
|
||||
const secondExecution = await commands.runWithStatus(
|
||||
cli,
|
||||
[
|
||||
"setup",
|
||||
@@ -520,8 +535,8 @@ const assertSkill = async ({ skillPath, siblingSkillPath, root }) => {
|
||||
throw new Error("packaged skill installation modified a sibling skill");
|
||||
};
|
||||
|
||||
const assertAlignedDoctor = async (cli, environment) => {
|
||||
const alignedDoctorExecution = await runWithStatus(
|
||||
const assertAlignedDoctor = async (commands, cli, environment) => {
|
||||
const alignedDoctorExecution = await commands.runWithStatus(
|
||||
cli,
|
||||
["doctor", "--json"],
|
||||
environment,
|
||||
@@ -540,6 +555,7 @@ const assertAlignedDoctor = async (cli, environment) => {
|
||||
};
|
||||
|
||||
const verifyConfigReadback = async ({
|
||||
commands,
|
||||
cli,
|
||||
environment,
|
||||
claudeConfig,
|
||||
@@ -575,10 +591,11 @@ const verifyConfigReadback = async ({
|
||||
throw new Error(
|
||||
"packaged OpenCode backup did not preserve the original JSONC",
|
||||
);
|
||||
await assertAlignedDoctor(cli, environment);
|
||||
await assertAlignedDoctor(commands, cli, environment);
|
||||
};
|
||||
|
||||
const verifySetupFailureRecovery = async ({
|
||||
commands,
|
||||
cli,
|
||||
environment,
|
||||
claudeConfig,
|
||||
@@ -587,7 +604,7 @@ const verifySetupFailureRecovery = async ({
|
||||
}) => {
|
||||
await writeFile(claudeConfig, "malformed");
|
||||
await writeFile(cursorConfig, '{"existing":true}\n');
|
||||
const failedExecution = await runWithStatus(
|
||||
const failedExecution = await commands.runWithStatus(
|
||||
cli,
|
||||
["setup", "--yes", "--all-detected", "--json"],
|
||||
environment,
|
||||
@@ -607,7 +624,7 @@ const verifySetupFailureRecovery = async ({
|
||||
`packaged setup mutated clients after a preflight failure: ${JSON.stringify(failed)}`,
|
||||
);
|
||||
await writeFile(claudeConfig, "{}\n");
|
||||
const recoveredExecution = await runWithStatus(
|
||||
const recoveredExecution = await commands.runWithStatus(
|
||||
cli,
|
||||
["setup", "--yes", "--all-detected", "--json"],
|
||||
environment,
|
||||
@@ -621,12 +638,12 @@ const verifySetupFailureRecovery = async ({
|
||||
throw new Error("packaged setup did not recover");
|
||||
};
|
||||
|
||||
const verifyDanglingSymlink = async ({ cli, environment, home }) => {
|
||||
const verifyDanglingSymlink = async ({ commands, cli, environment, home }) => {
|
||||
const geminiDir = join(home, ".gemini");
|
||||
const geminiConfig = join(geminiDir, "settings.json");
|
||||
await mkdir(geminiDir, { recursive: true });
|
||||
await symlink(join(home, "missing-gemini.json"), geminiConfig);
|
||||
const danglingExecution = await runWithStatus(
|
||||
const danglingExecution = await commands.runWithStatus(
|
||||
cli,
|
||||
["setup", "--yes", "--client", "gemini_cli", "--json"],
|
||||
environment,
|
||||
@@ -649,6 +666,7 @@ const verifyDanglingSymlink = async ({ cli, environment, home }) => {
|
||||
};
|
||||
|
||||
const verifyUninstall = async ({
|
||||
commands,
|
||||
cli,
|
||||
environment,
|
||||
cursorConfig,
|
||||
@@ -657,7 +675,7 @@ const verifyUninstall = async ({
|
||||
codexTarget,
|
||||
opencodeConfig,
|
||||
}) => {
|
||||
const uninstallExecution = await runWithStatus(
|
||||
const uninstallExecution = await commands.runWithStatus(
|
||||
cli,
|
||||
["uninstall", "--json"],
|
||||
environment,
|
||||
@@ -707,6 +725,7 @@ const verifyUninstall = async ({
|
||||
|
||||
/** Verify setup transactions, skill installation, and uninstall for supported hosts. */
|
||||
export async function verifyPackageSetup({
|
||||
commands,
|
||||
cli,
|
||||
packageRunnerCli,
|
||||
environment,
|
||||
@@ -721,7 +740,7 @@ export async function verifyPackageSetup({
|
||||
hopperSetupSupported,
|
||||
root,
|
||||
}) {
|
||||
await verifyMcpAdd({ cli, environment, npxLog });
|
||||
await verifyMcpAdd({ commands, cli, environment, npxLog });
|
||||
const skillPath = join(
|
||||
home,
|
||||
".agents/skills/reverse-engineer-anything/SKILL.md",
|
||||
@@ -764,6 +783,7 @@ export async function verifyPackageSetup({
|
||||
unchangedPaths,
|
||||
});
|
||||
await verifySetupPlan({
|
||||
commands,
|
||||
cli,
|
||||
environment,
|
||||
claudeConfig,
|
||||
@@ -774,15 +794,17 @@ export async function verifyPackageSetup({
|
||||
supportedSetupHost,
|
||||
});
|
||||
const apply = await verifySetupApply({
|
||||
commands,
|
||||
cli,
|
||||
environment,
|
||||
supportedSetupHost,
|
||||
hopperSetupSupported,
|
||||
});
|
||||
if (supportedSetupHost) {
|
||||
await verifyAbsentOpenCodeSetup({ cli, environment, home });
|
||||
await verifyAbsentClaudeCodeSetup({ cli, environment, home });
|
||||
await verifyAbsentOpenCodeSetup({ commands, cli, environment, home });
|
||||
await verifyAbsentClaudeCodeSetup({ commands, cli, environment, home });
|
||||
await verifyConfigReadback({
|
||||
commands,
|
||||
cli,
|
||||
environment,
|
||||
claudeConfig,
|
||||
@@ -798,14 +820,16 @@ export async function verifyPackageSetup({
|
||||
cursorOriginal,
|
||||
});
|
||||
await verifySetupFailureRecovery({
|
||||
commands,
|
||||
cli,
|
||||
environment,
|
||||
claudeConfig,
|
||||
cursorConfig,
|
||||
status: apply.status,
|
||||
});
|
||||
await verifyDanglingSymlink({ cli, environment, home });
|
||||
await verifyDanglingSymlink({ commands, cli, environment, home });
|
||||
await verifyUninstall({
|
||||
commands,
|
||||
cli,
|
||||
environment,
|
||||
cursorConfig,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { json, runWithStatus } from "./lib/verify-package-core.mjs";
|
||||
import { json } from "./lib/verify-package-core.mjs";
|
||||
|
||||
/** Verify that an unknown --provider is rejected by the packaged CLI. */
|
||||
export async function verifyUnknownProvider({ cli, environment }) {
|
||||
const unknownProviderExecution = await runWithStatus(
|
||||
export async function verifyUnknownProvider({ commands, cli, environment }) {
|
||||
const unknownProviderExecution = await commands.runWithStatus(
|
||||
cli,
|
||||
["analyze", process.execPath, "--provider", "missing-provider", "--json"],
|
||||
environment,
|
||||
|
||||
@@ -1,108 +1,12 @@
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { createHash } from "node:crypto";
|
||||
import { createServer } from "node:http";
|
||||
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
import { exec, json } from "./lib/verify-package-core.mjs";
|
||||
import { json } from "./lib/verify-package-core.mjs";
|
||||
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
|
||||
|
||||
/** Keep the live updater owner and its workspace when process cleanup is unproven. */
|
||||
export class PackageUpdateCleanupFailure extends AggregateError {
|
||||
constructor(primary, cleanupOwner, result, workspace) {
|
||||
const cleanup = new Error(result.reason);
|
||||
super(
|
||||
primary === undefined ? [cleanup] : [primary, cleanup],
|
||||
`Package updater cleanup incomplete; retained ${workspace}`,
|
||||
{ cause: primary },
|
||||
);
|
||||
this.cleanupOwner = cleanupOwner;
|
||||
this.workspace = workspace;
|
||||
}
|
||||
}
|
||||
|
||||
/** Own the helper and installer descendants until output and process cleanup finish. */
|
||||
async function runUpdater({
|
||||
harness,
|
||||
environment,
|
||||
signal,
|
||||
workspace,
|
||||
retainWorkspace,
|
||||
}) {
|
||||
// Only updater execution needs built process ownership; importing this fixture
|
||||
// does not make unrelated source-only verifier helpers depend on dist.
|
||||
const { ProviderProcessSupervisor, spawnOwnedProviderProcess } =
|
||||
await import("../dist/process/ProviderProcess.js");
|
||||
const { cleanupOwnedProcessGroup } =
|
||||
await import("../dist/process/ProcessOwnership.js");
|
||||
const launch = await spawnOwnedProviderProcess({
|
||||
command: process.execPath,
|
||||
arguments: [harness],
|
||||
expectedCommand: null,
|
||||
hostEnvironment: environment,
|
||||
cwd: workspace,
|
||||
runId: randomUUID(),
|
||||
sweepTokenOwnedProcesses: true,
|
||||
signal,
|
||||
});
|
||||
let collectionFailure;
|
||||
const supervisor = new ProviderProcessSupervisor(
|
||||
{
|
||||
...launch,
|
||||
ownsProcessLifetime: true,
|
||||
cleanup:
|
||||
launch.cleanup ?? (() => cleanupOwnedProcessGroup(launch.ownership)),
|
||||
},
|
||||
{
|
||||
maxDiagnosticBytes: 16 * 1024 * 1024,
|
||||
onDiagnostic(event) {
|
||||
if (event.type === "output" && event.truncated)
|
||||
collectionFailure ??= new Error(
|
||||
"Package updater exceeded its diagnostic capacity",
|
||||
);
|
||||
if (event.type === "error")
|
||||
collectionFailure ??= new Error(event.message, {
|
||||
cause: event.cause,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
let failure;
|
||||
let stdout;
|
||||
try {
|
||||
while (!(await supervisor.waitForOutputClose(50))) {
|
||||
signal.throwIfAborted();
|
||||
if (collectionFailure !== undefined) throw collectionFailure;
|
||||
}
|
||||
signal.throwIfAborted();
|
||||
if (collectionFailure !== undefined) throw collectionFailure;
|
||||
const snapshot = supervisor.snapshot();
|
||||
if (
|
||||
snapshot.diagnosticTruncated ||
|
||||
snapshot.exitCode !== 0 ||
|
||||
snapshot.signal !== null
|
||||
)
|
||||
throw new Error(
|
||||
`Package updater failed: ${snapshot.stdout.text}\n${snapshot.stderr.text}`,
|
||||
);
|
||||
stdout = snapshot.stdout.text;
|
||||
} catch (cause) {
|
||||
failure = { cause };
|
||||
}
|
||||
const stopped = await supervisor.stop();
|
||||
if (stopped.status === "incomplete") {
|
||||
retainWorkspace?.();
|
||||
throw new PackageUpdateCleanupFailure(
|
||||
failure?.cause,
|
||||
supervisor,
|
||||
stopped,
|
||||
workspace,
|
||||
);
|
||||
}
|
||||
if (failure !== undefined) throw failure.cause;
|
||||
return stdout;
|
||||
}
|
||||
|
||||
/** Exercise the packaged updater with real npm and a local release registry fixture. */
|
||||
export async function verifyPackageUpdate({
|
||||
prefix,
|
||||
@@ -110,7 +14,7 @@ export async function verifyPackageUpdate({
|
||||
tarball,
|
||||
workspace,
|
||||
environment,
|
||||
retainWorkspace,
|
||||
commands,
|
||||
}) {
|
||||
const metadataPath = join(packageRoot, "package.json");
|
||||
const metadata = json(await readFile(metadataPath, "utf8"));
|
||||
@@ -126,8 +30,8 @@ export async function verifyPackageUpdate({
|
||||
const fixture = new AbortController();
|
||||
const server = createServer((request, response) => {
|
||||
void serveRegistry(request, response).catch((error) => {
|
||||
fixtureFailure ??= error;
|
||||
fixture.abort(fixtureFailure);
|
||||
fixtureFailure ??= { cause: error };
|
||||
fixture.abort(fixtureFailure.cause);
|
||||
if (response.headersSent) {
|
||||
response.destroy();
|
||||
return;
|
||||
@@ -222,7 +126,7 @@ export async function verifyPackageUpdate({
|
||||
npm_config_registry: registry,
|
||||
};
|
||||
const previous = (
|
||||
await exec(process.execPath, [entry, "--version"], {
|
||||
await commands.exec(process.execPath, [entry, "--version"], {
|
||||
env: runtimeEnvironment,
|
||||
})
|
||||
).stdout.trim();
|
||||
@@ -241,13 +145,14 @@ if (result.status === "failed") process.exitCode = 1;
|
||||
`,
|
||||
);
|
||||
const result = json(
|
||||
await runUpdater({
|
||||
harness,
|
||||
environment: runtimeEnvironment,
|
||||
signal: fixture.signal,
|
||||
workspace,
|
||||
retainWorkspace,
|
||||
}),
|
||||
(
|
||||
await commands.exec(process.execPath, [harness], {
|
||||
env: runtimeEnvironment,
|
||||
cwd: workspace,
|
||||
signal: fixture.signal,
|
||||
diagnosticBytes: 16 * 1024 * 1024,
|
||||
})
|
||||
).stdout,
|
||||
);
|
||||
if (
|
||||
result.status !== "updated" ||
|
||||
@@ -265,14 +170,14 @@ if (result.status === "failed") process.exitCode = 1;
|
||||
"Update applied integration maintenance without approval",
|
||||
);
|
||||
const current = (
|
||||
await exec(process.execPath, [entry, "--version"], {
|
||||
await commands.exec(process.execPath, [entry, "--version"], {
|
||||
env: runtimeEnvironment,
|
||||
})
|
||||
).stdout.trim();
|
||||
if (current !== metadata.version)
|
||||
throw new Error(`Updated executable reported ${current}`);
|
||||
const help = (
|
||||
await exec(process.execPath, [entry, "--help"], {
|
||||
await commands.exec(process.execPath, [entry, "--help"], {
|
||||
env: runtimeEnvironment,
|
||||
})
|
||||
).stdout;
|
||||
@@ -280,18 +185,15 @@ if (result.status === "failed") process.exitCode = 1;
|
||||
throw new Error(
|
||||
"Updated CLI did not expose update as its sole maintenance command",
|
||||
);
|
||||
if (fixtureFailure !== undefined) throw fixtureFailure;
|
||||
if (fixtureFailure !== undefined) throw fixtureFailure.cause;
|
||||
return {
|
||||
installed_version: result.installedVersion,
|
||||
exact_release: true,
|
||||
maintenance_read_only: true,
|
||||
};
|
||||
} catch (error) {
|
||||
// A retained process owner takes precedence over its triggering fixture
|
||||
// failure; otherwise report the first registry/proxy failure exactly.
|
||||
throw error instanceof PackageUpdateCleanupFailure
|
||||
? error
|
||||
: (fixtureFailure ?? error);
|
||||
// The registry owns its failure; command cleanup remains with commands.
|
||||
throw fixtureFailure === undefined ? error : fixtureFailure.cause;
|
||||
}
|
||||
}, [
|
||||
async () => {
|
||||
|
||||
+24
-22
@@ -1,8 +1,6 @@
|
||||
import { mkdir, mkdtemp, rm, unlink } from "node:fs/promises";
|
||||
import { execFile } from "node:child_process";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
|
||||
import { PRODUCT_IDENTITY } from "../dist/identity.js";
|
||||
@@ -22,6 +20,7 @@ import { verifyPackageSetup } from "./verify-package-setup.mjs";
|
||||
import { verifyRepositorySkill } from "./verify-repository-skill.mjs";
|
||||
import { verifyManaged } from "./verify-package-managed.mjs";
|
||||
import { verifyUnknownProvider } from "./verify-package-unknown-provider.mjs";
|
||||
import { createPackageVerifierCommands } from "./lib/verify-package-commands.mjs";
|
||||
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
|
||||
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
||||
import {
|
||||
@@ -35,7 +34,6 @@ import {
|
||||
} from "./lib/mcp-startup-probe.mjs";
|
||||
|
||||
const root = process.cwd();
|
||||
const execFileAsync = promisify(execFile);
|
||||
const retentionDirectory = process.env.REA_VERIFIED_PACKAGE_DIRECTORY;
|
||||
const verifierRun = createVerifierRun();
|
||||
const workspace = await mkdtemp(join(tmpdir(), "rea-package-"));
|
||||
@@ -49,7 +47,11 @@ let repositorySkill;
|
||||
let report;
|
||||
let retainedPackage;
|
||||
let retentionMetadata;
|
||||
let updateWorkspaceRetained = false;
|
||||
let commands;
|
||||
const releaseWorkspace = () =>
|
||||
runStage("workspace cleanup", () =>
|
||||
rm(workspace, { recursive: true, force: true }),
|
||||
);
|
||||
|
||||
const runStage = async (name, action) => {
|
||||
const started = performance.now();
|
||||
@@ -62,9 +64,12 @@ const runStage = async (name, action) => {
|
||||
};
|
||||
|
||||
await withVerifierCleanup(async () => {
|
||||
commands = await createPackageVerifierCommands({
|
||||
workspace: { path: workspace, close: releaseWorkspace },
|
||||
});
|
||||
if (retentionDirectory !== undefined && retentionDirectory !== "") {
|
||||
const sourceSha = (
|
||||
await execFileAsync("git", ["rev-parse", "HEAD"], { cwd: root })
|
||||
await commands.exec("git", ["rev-parse", "HEAD"], { cwd: root })
|
||||
).stdout.trim();
|
||||
retentionMetadata = createVerifiedPackageMetadata({
|
||||
sourceSha,
|
||||
@@ -84,10 +89,10 @@ await withVerifierCleanup(async () => {
|
||||
});
|
||||
}
|
||||
repositorySkill = await runStage("repository skill", () =>
|
||||
verifyRepositorySkill({ root, workspace }),
|
||||
verifyRepositorySkill({ commands, root, workspace }),
|
||||
);
|
||||
({ tarball } = await runStage("package archive", () =>
|
||||
verifyPackagePack({ root, workspace }),
|
||||
verifyPackagePack({ commands, root, workspace }),
|
||||
));
|
||||
const environmentData = await runStage("package environment", () =>
|
||||
verifyPackageEnvironment({
|
||||
@@ -99,6 +104,7 @@ await withVerifierCleanup(async () => {
|
||||
);
|
||||
const { cli, packageRunnerCli } = await runStage("package install", () =>
|
||||
verifyPackageInstall({
|
||||
commands,
|
||||
tarball,
|
||||
prefix: environmentData.prefix,
|
||||
workspace,
|
||||
@@ -107,6 +113,7 @@ await withVerifierCleanup(async () => {
|
||||
);
|
||||
update = await runStage("package update", () =>
|
||||
verifyPackageUpdate({
|
||||
commands,
|
||||
prefix: environmentData.prefix,
|
||||
packageRoot: join(
|
||||
environmentData.prefix,
|
||||
@@ -117,9 +124,6 @@ await withVerifierCleanup(async () => {
|
||||
tarball,
|
||||
workspace,
|
||||
environment: environmentData.environment,
|
||||
retainWorkspace() {
|
||||
updateWorkspaceRetained = true;
|
||||
},
|
||||
}),
|
||||
);
|
||||
mcpStartup = await runStage("MCP startup", () =>
|
||||
@@ -147,12 +151,14 @@ await withVerifierCleanup(async () => {
|
||||
"package discovery",
|
||||
() =>
|
||||
verifyPackageDiscovery({
|
||||
commands,
|
||||
cli,
|
||||
environment: environmentData.environment,
|
||||
}),
|
||||
);
|
||||
const { artifactArchive } = await runStage("artifact and Electron CLI", () =>
|
||||
verifyPackageArtifactAndElectron({
|
||||
commands,
|
||||
cli,
|
||||
workspace,
|
||||
environment: environmentData.environment,
|
||||
@@ -160,6 +166,7 @@ await withVerifierCleanup(async () => {
|
||||
);
|
||||
await runStage("managed analysis CLI", () =>
|
||||
verifyManaged({
|
||||
commands,
|
||||
cli,
|
||||
workspace,
|
||||
environment: environmentData.environment,
|
||||
@@ -167,24 +174,28 @@ await withVerifierCleanup(async () => {
|
||||
);
|
||||
await runStage("unknown provider CLI", () =>
|
||||
verifyUnknownProvider({
|
||||
commands,
|
||||
cli,
|
||||
environment: environmentData.environment,
|
||||
}),
|
||||
);
|
||||
await runStage("platform CLI", () =>
|
||||
verifyPackagePlatform({
|
||||
commands,
|
||||
cli,
|
||||
environment: environmentData.environment,
|
||||
}),
|
||||
);
|
||||
await runStage("capabilities and search CLI", () =>
|
||||
verifyPackageCapabilitiesAndSearch({
|
||||
commands,
|
||||
cli,
|
||||
environment: environmentData.environment,
|
||||
}),
|
||||
);
|
||||
await runStage("evidence CLI", () =>
|
||||
verifyPackageEvidence({
|
||||
commands,
|
||||
cli,
|
||||
evidenceRoot,
|
||||
referenceRoot,
|
||||
@@ -193,6 +204,7 @@ await withVerifierCleanup(async () => {
|
||||
);
|
||||
await runStage("setup and uninstall CLI", () =>
|
||||
verifyPackageSetup({
|
||||
commands,
|
||||
cli,
|
||||
packageRunnerCli,
|
||||
environment: environmentData.environment,
|
||||
@@ -210,6 +222,7 @@ await withVerifierCleanup(async () => {
|
||||
);
|
||||
await runStage("MCP tools and evidence", () =>
|
||||
verifyPackageMcp({
|
||||
commands,
|
||||
cli,
|
||||
environment: environmentData.environment,
|
||||
evidenceRoot,
|
||||
@@ -259,18 +272,7 @@ await withVerifierCleanup(async () => {
|
||||
metadata: retentionMetadata,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
async () => {
|
||||
if (updateWorkspaceRetained)
|
||||
process.stderr.write(
|
||||
`Package updater cleanup incomplete; retained ${workspace}\n`,
|
||||
);
|
||||
else
|
||||
await runStage("workspace cleanup", () =>
|
||||
rm(workspace, { recursive: true, force: true }),
|
||||
);
|
||||
},
|
||||
]);
|
||||
}, [() => (commands === undefined ? releaseWorkspace() : commands.close())]);
|
||||
const verifierLineage = await runStage("verifier lineage", () =>
|
||||
completeVerifierRun(verifierRun),
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,60 +1,23 @@
|
||||
#!/usr/bin/env node
|
||||
import { join } from "node:path";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { PrivateRuntimeRoot } from "../dist/process/PrivateRuntimeRoot.js";
|
||||
import {
|
||||
OwnedCommandFailure,
|
||||
runOwnedCommand,
|
||||
} from "../dist/process/OwnedCommand.js";
|
||||
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
|
||||
import { execNpm } from "./lib/verify-package-core.mjs";
|
||||
import { createPackageVerifierCommands } from "./lib/verify-package-commands.mjs";
|
||||
import { WABT_WORKFLOW_TIMEOUT_MS } from "./lib/wabt-verifier-timeouts.mjs";
|
||||
import { verifyPackagePack } from "./verify-package-pack.mjs";
|
||||
|
||||
/** Preserve the actual dependent process/root owner across failed cleanup. */
|
||||
class PackagedWasmCleanupFailure extends Error {
|
||||
constructor(cause, cleanupOwner, runtimeRoot) {
|
||||
super(
|
||||
`Packaged WASM verifier cleanup incomplete; retained ${runtimeRoot.path}: ${cause instanceof Error ? cause.message : String(cause)}`,
|
||||
{ cause },
|
||||
);
|
||||
this.name = "PackagedWasmCleanupFailure";
|
||||
this.cleanupOwner = cleanupOwner;
|
||||
this.runtimeRoot = runtimeRoot;
|
||||
}
|
||||
}
|
||||
|
||||
const root = await PrivateRuntimeRoot.create({
|
||||
prefix: "rea-wabt-package-verifier-",
|
||||
});
|
||||
let commandCleanupOwner;
|
||||
let closing;
|
||||
const cleanupOwner = {
|
||||
close() {
|
||||
if (closing !== undefined) return closing;
|
||||
let attempt;
|
||||
attempt = (async () => {
|
||||
if (commandCleanupOwner !== undefined) {
|
||||
const stopped = await commandCleanupOwner.stop();
|
||||
if (stopped.status === "incomplete")
|
||||
throw new Error(stopped.reason, { cause: stopped });
|
||||
commandCleanupOwner = undefined;
|
||||
}
|
||||
await root.close();
|
||||
})().catch((cause) => {
|
||||
if (closing === attempt) closing = undefined;
|
||||
throw new PackagedWasmCleanupFailure(cause, cleanupOwner, root);
|
||||
});
|
||||
closing = attempt;
|
||||
return attempt;
|
||||
},
|
||||
};
|
||||
let commands;
|
||||
const verifiedOutput = await withVerifierCleanup(async () => {
|
||||
commands = await createPackageVerifierCommands({ workspace: root });
|
||||
const { tarball } = await verifyPackagePack({
|
||||
root: process.cwd(),
|
||||
workspace: root.path,
|
||||
commands,
|
||||
});
|
||||
const prefix = join(root.path, "prefix");
|
||||
const home = join(root.path, "home");
|
||||
@@ -66,7 +29,7 @@ const verifiedOutput = await withVerifierCleanup(async () => {
|
||||
XDG_CONFIG_HOME: join(home, ".config"),
|
||||
XDG_CACHE_HOME: join(home, ".cache"),
|
||||
};
|
||||
await execNpm(
|
||||
await commands.execNpm(
|
||||
[
|
||||
"install",
|
||||
"--global",
|
||||
@@ -88,32 +51,20 @@ const verifiedOutput = await withVerifierCleanup(async () => {
|
||||
"scripts",
|
||||
"rea.mjs",
|
||||
);
|
||||
let verified;
|
||||
try {
|
||||
verified = await runOwnedCommand(
|
||||
{
|
||||
command: process.execPath,
|
||||
arguments: [
|
||||
fileURLToPath(new URL("./verify-wasm-artifact.mjs", import.meta.url)),
|
||||
entrypoint,
|
||||
"--require-tools",
|
||||
],
|
||||
cwd: process.cwd(),
|
||||
hostEnvironment: environment,
|
||||
expectedCommand: process.execPath,
|
||||
runId: `rea-packaged-wabt-${randomUUID()}`,
|
||||
sweepTokenOwnedProcesses: true,
|
||||
},
|
||||
{
|
||||
timeoutMs: WABT_WORKFLOW_TIMEOUT_MS,
|
||||
diagnosticBytes: 4 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
} catch (cause) {
|
||||
if (cause instanceof OwnedCommandFailure)
|
||||
commandCleanupOwner = cause.cleanupOwner;
|
||||
throw cause;
|
||||
}
|
||||
return verified.stdout.text;
|
||||
}, [() => cleanupOwner.close()]);
|
||||
const verified = await commands.exec(
|
||||
process.execPath,
|
||||
[
|
||||
fileURLToPath(new URL("./verify-wasm-artifact.mjs", import.meta.url)),
|
||||
entrypoint,
|
||||
"--require-tools",
|
||||
],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
env: environment,
|
||||
timeout: WABT_WORKFLOW_TIMEOUT_MS,
|
||||
diagnosticBytes: 4 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
return verified.stdout;
|
||||
}, [() => (commands === undefined ? root.close() : commands.close())]);
|
||||
process.stdout.write(verifiedOutput);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { PackageVerifierCommands } from "./lib/verify-package-commands.mjs";
|
||||
|
||||
/** Validate required shipped bridges and parse packed Python without extraction. */
|
||||
export function verifyPackedBridge(input: {
|
||||
readonly tarball: string;
|
||||
readonly packedFiles: readonly string[];
|
||||
readonly commands: PackageVerifierCommands;
|
||||
}): Promise<void>;
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { GHIDRA_BRIDGE_SOURCES } from "../src/ghidra/GhidraBridgeSources.ts";
|
||||
|
||||
const exec = promisify(execFile);
|
||||
const packedHopperBridge = "package/bridge/hopper_bridge.py";
|
||||
const packedCutterBridge = "package/bridge/cutter/rea_bridge.py";
|
||||
|
||||
/** Check shipped bridge files; real-provider lanes verify runtime behavior. */
|
||||
export async function verifyPackedBridge({ tarball, packedFiles }) {
|
||||
export async function verifyPackedBridge({ tarball, packedFiles, commands }) {
|
||||
for (const path of [
|
||||
packedHopperBridge,
|
||||
packedCutterBridge,
|
||||
@@ -20,7 +17,7 @@ export async function verifyPackedBridge({ tarball, packedFiles }) {
|
||||
// Read the packed source without extracting into the caller's leased root.
|
||||
// Only the interpreter's standard library is needed; caller-local modules
|
||||
// and site startup hooks must not replace the parser or execute here.
|
||||
await exec("python3", [
|
||||
await commands.exec("python3", [
|
||||
"-I",
|
||||
"-S",
|
||||
"-c",
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { cp, mkdir, readFile, readdir } from "node:fs/promises";
|
||||
import { dirname, join, relative } from "node:path";
|
||||
|
||||
import { exec, execNpm, pathExists } from "./lib/verify-package-core.mjs";
|
||||
import { pathExists } from "./lib/verify-package-core.mjs";
|
||||
|
||||
/** Install the separately locked Skills CLI so verification resolves no registry metadata. */
|
||||
const lockedSkillsCli = async (root, workspace) => {
|
||||
const lockedSkillsCli = async (commands, root, workspace) => {
|
||||
const tool = join(workspace, "skills-cli");
|
||||
await mkdir(tool, { recursive: true });
|
||||
for (const file of ["package.json", "package-lock.json"])
|
||||
await cp(join(root, "scripts/verify-tools/skills", file), join(tool, file));
|
||||
await execNpm(
|
||||
await commands.execNpm(
|
||||
["ci", "--prefer-offline", "--ignore-scripts", "--no-audit", "--no-fund"],
|
||||
{ cwd: tool, timeout: 120_000 },
|
||||
);
|
||||
@@ -24,12 +24,12 @@ const lockedSkillsCli = async (root, workspace) => {
|
||||
};
|
||||
|
||||
/** Exercise Skills' default root discovery without ignored build outputs. */
|
||||
export async function verifyRepositorySkill({ root, workspace }) {
|
||||
export async function verifyRepositorySkill({ root, workspace, commands }) {
|
||||
const snapshot = join(workspace, "repository");
|
||||
const consumer = join(workspace, "skill-consumer");
|
||||
const home = join(workspace, "skill-consumer-home");
|
||||
const tracked = (
|
||||
await exec(
|
||||
await commands.exec(
|
||||
"git",
|
||||
["ls-files", "--cached", "--format=%(objectmode) %(path)", "-z"],
|
||||
{ cwd: root },
|
||||
@@ -54,10 +54,11 @@ export async function verifyRepositorySkill({ root, workspace }) {
|
||||
await mkdir(consumer, { recursive: true });
|
||||
await mkdir(home, { recursive: true });
|
||||
const { version: skillsVersion, entry } = await lockedSkillsCli(
|
||||
commands,
|
||||
root,
|
||||
workspace,
|
||||
);
|
||||
await exec(
|
||||
await commands.exec(
|
||||
process.execPath,
|
||||
[
|
||||
entry,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execFile } from "node:child_process";
|
||||
import {
|
||||
access,
|
||||
mkdir,
|
||||
@@ -13,30 +12,35 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { parse as parseJsonc } from "jsonc-parser";
|
||||
import { supportedClients } from "../dist/application/SupportedClients.js";
|
||||
import { promisify } from "node:util";
|
||||
import { GHIDRA_BRIDGE_SOURCES } from "../src/ghidra/GhidraBridgeSources.ts";
|
||||
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
|
||||
import {
|
||||
execNpm,
|
||||
verifyCompleteToolCatalog,
|
||||
} from "./lib/verify-package-core.mjs";
|
||||
import { verifyCompleteToolCatalog } from "./lib/verify-package-core.mjs";
|
||||
import { createPackageVerifierCommands } from "./lib/verify-package-commands.mjs";
|
||||
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
|
||||
import { verifyPackageUpdate } from "./verify-package-update.mjs";
|
||||
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
||||
|
||||
const exec = promisify(execFile);
|
||||
const verifierRun = createVerifierRun();
|
||||
const root = process.cwd();
|
||||
const workspace = await mkdtemp(join(tmpdir(), "rea-windows-package-"));
|
||||
const workspaceOwner = {
|
||||
path: workspace,
|
||||
close: () => rm(workspace, { recursive: true, force: true }),
|
||||
};
|
||||
const prefix = join(workspace, "prefix");
|
||||
let commands;
|
||||
const closeWorkspace = () =>
|
||||
commands === undefined ? workspaceOwner.close() : commands.close();
|
||||
let report;
|
||||
|
||||
try {
|
||||
commands = await createPackageVerifierCommands({ workspace: workspaceOwner });
|
||||
const packed = JSON.parse(
|
||||
(
|
||||
await execNpm(
|
||||
await commands.execNpm(
|
||||
["pack", "--json", "--silent", "--pack-destination", workspace],
|
||||
{ cwd: root, windowsHide: true },
|
||||
)
|
||||
@@ -69,7 +73,7 @@ try {
|
||||
|
||||
const tarball = join(workspace, packageResult.filename);
|
||||
await access(tarball);
|
||||
await execNpm(
|
||||
await commands.execNpm(
|
||||
["install", "--no-package-lock", "--no-save", "--prefix", prefix, tarball],
|
||||
{ cwd: workspace, windowsHide: true },
|
||||
);
|
||||
@@ -84,7 +88,7 @@ try {
|
||||
...process.env,
|
||||
REA_ANALYSIS_PROVIDER: "auto",
|
||||
};
|
||||
const help = await exec(process.execPath, [entry, "--help"], {
|
||||
const help = await commands.exec(process.execPath, [entry, "--help"], {
|
||||
env: environment,
|
||||
windowsHide: true,
|
||||
});
|
||||
@@ -103,12 +107,27 @@ try {
|
||||
});
|
||||
const client = new Client({ name: "rea-windows-package", version: "1.0.0" });
|
||||
let toolCount = 0;
|
||||
try {
|
||||
await withVerifierCleanup(async () => {
|
||||
await client.connect(server);
|
||||
toolCount = (await verifyCompleteToolCatalog(client)).length;
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
}, [
|
||||
async () => {
|
||||
try {
|
||||
await client.close();
|
||||
} catch (cause) {
|
||||
commands.adopt(client, entry);
|
||||
throw cause;
|
||||
}
|
||||
},
|
||||
async () => {
|
||||
try {
|
||||
await server.close();
|
||||
} catch (cause) {
|
||||
commands.adopt(server, entry);
|
||||
throw cause;
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
||||
const home = join(workspace, "home");
|
||||
const setupEnvironment = {
|
||||
@@ -148,7 +167,7 @@ try {
|
||||
];
|
||||
const setup = JSON.parse(
|
||||
(
|
||||
await exec(process.execPath, [entry, ...setupArgs], {
|
||||
await commands.exec(process.execPath, [entry, ...setupArgs], {
|
||||
env: setupEnvironment,
|
||||
windowsHide: true,
|
||||
})
|
||||
@@ -188,7 +207,7 @@ try {
|
||||
}
|
||||
const repeated = JSON.parse(
|
||||
(
|
||||
await exec(process.execPath, [entry, ...setupArgs], {
|
||||
await commands.exec(process.execPath, [entry, ...setupArgs], {
|
||||
env: setupEnvironment,
|
||||
windowsHide: true,
|
||||
})
|
||||
@@ -202,7 +221,7 @@ try {
|
||||
throw new Error("Packaged agent setup is not idempotent");
|
||||
const uninstalled = JSON.parse(
|
||||
(
|
||||
await exec(process.execPath, [entry, "uninstall", "--json"], {
|
||||
await commands.exec(process.execPath, [entry, "uninstall", "--json"], {
|
||||
env: setupEnvironment,
|
||||
windowsHide: true,
|
||||
})
|
||||
@@ -215,11 +234,12 @@ try {
|
||||
throw new Error(`Packaged uninstall did not remove ${id}`);
|
||||
|
||||
if (process.platform !== "win32")
|
||||
await execNpm(
|
||||
await commands.execNpm(
|
||||
["install", "--global", "--ignore-scripts", "--prefix", prefix, tarball],
|
||||
{ cwd: workspace, windowsHide: true },
|
||||
);
|
||||
const update = await verifyPackageUpdate({
|
||||
commands,
|
||||
prefix,
|
||||
packageRoot:
|
||||
process.platform === "win32"
|
||||
@@ -241,9 +261,10 @@ try {
|
||||
uninstall: true,
|
||||
update,
|
||||
};
|
||||
} finally {
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
} catch (cause) {
|
||||
await withVerifierCleanup(() => Promise.reject(cause), [closeWorkspace]);
|
||||
}
|
||||
await closeWorkspace();
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), ...report })}\n`,
|
||||
);
|
||||
|
||||
@@ -13,7 +13,10 @@ import {
|
||||
OwnedCommandFailure,
|
||||
runOwnedCommand,
|
||||
} from "../process/OwnedCommand.js";
|
||||
import type { ProviderProcessSnapshot } from "../process/ProviderProcess.js";
|
||||
import type {
|
||||
ProcessDiagnosticBudget,
|
||||
ProviderProcessSnapshot,
|
||||
} from "../process/ProviderProcess.js";
|
||||
import { ApktoolProvider } from "./ApktoolProvider.js";
|
||||
|
||||
vi.mock("../process/OwnedCommand.js", async (importOriginal) => ({
|
||||
@@ -37,8 +40,8 @@ const MANIFEST = [
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
const decodeTimeouts: number[] = [];
|
||||
const outputBudgets: number[] = [];
|
||||
const decodeTimeouts: (number | null)[] = [];
|
||||
const outputBudgets: ProcessDiagnosticBudget[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
decodeTimeouts.length = 0;
|
||||
|
||||
@@ -72,7 +72,7 @@ describe("native_command_timeout_ms", () => {
|
||||
const calls: {
|
||||
command: string;
|
||||
arguments: readonly string[];
|
||||
timeoutMs: number;
|
||||
timeoutMs: number | null;
|
||||
}[] = [];
|
||||
vi.mocked(runOwnedCommand).mockImplementation(async (spawn, limits) => {
|
||||
calls.push({
|
||||
@@ -99,7 +99,7 @@ describe("native_command_timeout_ms", () => {
|
||||
});
|
||||
|
||||
it("uses 60000 ms for xcrun resolution when the caller omits the deadline", async () => {
|
||||
const timeouts: number[] = [];
|
||||
const timeouts: (number | null)[] = [];
|
||||
vi.mocked(runOwnedCommand).mockImplementation(async (_spawn, limits) => {
|
||||
timeouts.push(limits.timeoutMs);
|
||||
throw new OwnedCommandFailure("timeout", "Command deadline elapsed.");
|
||||
|
||||
@@ -9,6 +9,7 @@ import { pipeline } from "node:stream/promises";
|
||||
import type {
|
||||
OwnedProviderProcessSpawnOptions,
|
||||
ProviderProcessSnapshot,
|
||||
ProcessDiagnosticBudget,
|
||||
ProviderProcessStopResult,
|
||||
SpawnedOwnedProviderProcess,
|
||||
} from "./ProviderProcess.js";
|
||||
@@ -82,8 +83,8 @@ export const ownedCommandResources = (
|
||||
|
||||
/** Output handling for one owned command. */
|
||||
interface OwnedCommandLimits {
|
||||
/** Retained stderr bytes, plus stdout bytes unless a sink consumes stdout. */
|
||||
readonly diagnosticBytes: number;
|
||||
/** Retained output budget; stdout is excluded when a sink consumes it. */
|
||||
readonly diagnosticBytes: ProcessDiagnosticBudget;
|
||||
/** Accept an ordinary nonzero exit after output closes without other process errors. */
|
||||
readonly acceptNonZeroExit?: boolean;
|
||||
/** Interpret complete physical output synchronously before awaited final cleanup. */
|
||||
@@ -158,7 +159,10 @@ function throwCommandPreparationFailure(
|
||||
/** Execute a single owned process with independent cleanup and bounded diagnostic retention. */
|
||||
export const runOwnedCommand = async (
|
||||
spawn: OwnedProviderProcessSpawnOptions,
|
||||
limits: OwnedCommandLimits & { readonly timeoutMs: number },
|
||||
limits: OwnedCommandLimits & {
|
||||
/** A positive absolute deadline, or null to keep only caller cancellation. */
|
||||
readonly timeoutMs: number | null;
|
||||
},
|
||||
options: {
|
||||
readonly signal?: AbortSignal | undefined;
|
||||
/** Synchronously accept unresolved cleanup ownership; execution still returns its original outcome. Must not throw. */
|
||||
@@ -180,18 +184,27 @@ export const runOwnedCommand = async (
|
||||
null,
|
||||
{ cancellation: { reason: callerSignal.reason } },
|
||||
);
|
||||
const deadline = new ProviderStartupDeadline(limits.timeoutMs, callerSignal);
|
||||
const deadline =
|
||||
limits.timeoutMs === null
|
||||
? undefined
|
||||
: new ProviderStartupDeadline(limits.timeoutMs, callerSignal);
|
||||
const control = deadline ?? {
|
||||
signal: callerSignal ?? new AbortController().signal,
|
||||
get interruption(): "cancelled" | undefined {
|
||||
return callerSignal?.aborted === true ? "cancelled" : undefined;
|
||||
},
|
||||
};
|
||||
try {
|
||||
return await collectOwnedCommand(
|
||||
spawn,
|
||||
limits,
|
||||
options.launcher ?? spawnOwnedProviderProcess,
|
||||
deadline,
|
||||
control,
|
||||
callerSignal,
|
||||
options.retainCleanup,
|
||||
);
|
||||
} finally {
|
||||
deadline.dispose();
|
||||
deadline?.dispose();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -201,7 +214,7 @@ const collectOwnedCommand = async (
|
||||
launcher: (
|
||||
input: OwnedProviderProcessSpawnOptions,
|
||||
) => Promise<SpawnedOwnedProviderProcess>,
|
||||
deadline: ProviderStartupDeadline,
|
||||
deadline: Pick<ProviderStartupDeadline, "signal" | "interruption">,
|
||||
callerSignal?: AbortSignal,
|
||||
retainCleanup?: (
|
||||
owner: ProviderProcessSupervisor,
|
||||
|
||||
@@ -184,3 +184,79 @@ it("bounds simulated Linux preparation by the command deadline before launch", a
|
||||
await expected;
|
||||
expect(enteredLaunch).toBe(false);
|
||||
});
|
||||
|
||||
it.each([false, true])(
|
||||
"keeps no-deadline preparation pending without a timer (caller signal: %s)",
|
||||
async (withCallerSignal) => {
|
||||
vi.useFakeTimers();
|
||||
const controller = new AbortController();
|
||||
const prepared = createDeferred<void>();
|
||||
const failure = new Error("authored preparation failure");
|
||||
let preparationSignal: AbortSignal | undefined;
|
||||
const pending = runOwnedCommand(
|
||||
{
|
||||
command: "never-launched",
|
||||
arguments: [],
|
||||
runId: "authored-no-deadline-prepare",
|
||||
},
|
||||
{ timeoutMs: null, diagnosticBytes: 1024 },
|
||||
{
|
||||
...(withCallerSignal ? { signal: controller.signal } : {}),
|
||||
launcher: async (input) => {
|
||||
preparationSignal = input.signal;
|
||||
await prepared.promise;
|
||||
throw failure;
|
||||
},
|
||||
},
|
||||
);
|
||||
const expected = expect(pending).rejects.toBe(failure);
|
||||
await vi.advanceTimersByTimeAsync(10_000);
|
||||
expect(preparationSignal?.aborted).toBe(false);
|
||||
if (withCallerSignal) expect(preparationSignal).toBe(controller.signal);
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
prepared.resolve();
|
||||
await expected;
|
||||
},
|
||||
);
|
||||
|
||||
it("cancels no-deadline preparation with the caller's exact opaque reason", async () => {
|
||||
const controller = new AbortController();
|
||||
const reason = new DarwinProcessOwnershipInspectionError(
|
||||
"opaque caller reason",
|
||||
);
|
||||
const started = createDeferred<void>();
|
||||
const pending = runOwnedCommand(
|
||||
{
|
||||
command: "never-launched",
|
||||
arguments: [],
|
||||
runId: "authored-no-deadline-cancel",
|
||||
},
|
||||
{ timeoutMs: null, diagnosticBytes: 1024 },
|
||||
{
|
||||
signal: controller.signal,
|
||||
launcher: async (input) => {
|
||||
expect(input.signal).toBe(controller.signal);
|
||||
started.resolve();
|
||||
await new Promise<void>((_resolve, reject) =>
|
||||
input.signal?.addEventListener(
|
||||
"abort",
|
||||
() => reject(input.signal?.reason),
|
||||
{
|
||||
once: true,
|
||||
},
|
||||
),
|
||||
);
|
||||
throw new Error("Cancelled preparation must not launch");
|
||||
},
|
||||
},
|
||||
);
|
||||
const expected = expect(pending).rejects.toMatchObject({
|
||||
reason: "cancelled",
|
||||
cancellation: { reason },
|
||||
preparationFailure: undefined,
|
||||
snapshot: null,
|
||||
});
|
||||
await started.promise;
|
||||
controller.abort(reason);
|
||||
await expected;
|
||||
});
|
||||
|
||||
@@ -193,12 +193,17 @@ export type ProviderProcessDiagnostic =
|
||||
readonly cause?: Error;
|
||||
};
|
||||
|
||||
/** Aggregate byte budget, or independent retained stdout and stderr byte budgets. */
|
||||
export type ProcessDiagnosticBudget =
|
||||
| number
|
||||
| { readonly stdout: number; readonly stderr: number };
|
||||
|
||||
/** Options for process lifecycle diagnostics. */
|
||||
export interface ProviderProcessSupervisorOptions {
|
||||
/** Retain stdout for diagnostics; disable when it carries a parsed protocol. Defaults to true. */
|
||||
readonly captureStdout?: boolean;
|
||||
/** Aggregate retained stdout/stderr byte budget; overflow is drained and reported, not retained. */
|
||||
readonly maxDiagnosticBytes?: number;
|
||||
/** Retained output budget; overflow is drained and reported, not retained. */
|
||||
readonly maxDiagnosticBytes?: ProcessDiagnosticBudget;
|
||||
/**
|
||||
* Single-line token ending at a non-ASCII-word byte or EOF, located before retention
|
||||
* applies, so its latest occurrence survives an exhausted budget.
|
||||
@@ -395,7 +400,10 @@ export class ProviderProcessSupervisor {
|
||||
#stopPromise: Promise<ProviderProcessStopResult> | undefined;
|
||||
#stopRequested = false;
|
||||
#disposed = false;
|
||||
#maxDiagnosticBytes: number | undefined;
|
||||
#maxAggregateDiagnosticBytes: number | undefined;
|
||||
readonly #maxStreamDiagnosticBytes:
|
||||
| Exclude<ProcessDiagnosticBudget, number>
|
||||
| undefined;
|
||||
#outputEpoch = 0;
|
||||
|
||||
constructor(
|
||||
@@ -403,7 +411,13 @@ export class ProviderProcessSupervisor {
|
||||
options: ProviderProcessSupervisorOptions = {},
|
||||
) {
|
||||
this.#options = options;
|
||||
this.#maxDiagnosticBytes = options.maxDiagnosticBytes;
|
||||
const budget = options.maxDiagnosticBytes;
|
||||
if (typeof budget === "number") this.#maxAggregateDiagnosticBytes = budget;
|
||||
else if (budget !== undefined)
|
||||
this.#maxStreamDiagnosticBytes = {
|
||||
stdout: budget.stdout,
|
||||
stderr: budget.stderr,
|
||||
};
|
||||
this.#resetMarkers();
|
||||
if (options.captureStdout !== false)
|
||||
this.#attach(launch.process.stdout, "stdout");
|
||||
@@ -593,10 +607,10 @@ export class ProviderProcessSupervisor {
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
retained + maxBytes,
|
||||
);
|
||||
this.#maxDiagnosticBytes =
|
||||
this.#maxDiagnosticBytes === undefined
|
||||
this.#maxAggregateDiagnosticBytes =
|
||||
this.#maxAggregateDiagnosticBytes === undefined
|
||||
? requestedLimit
|
||||
: Math.min(this.#maxDiagnosticBytes, requestedLimit);
|
||||
: Math.min(this.#maxAggregateDiagnosticBytes, requestedLimit);
|
||||
}
|
||||
|
||||
/** Unreference inherited output pipes without closing streams shared with descendants. */
|
||||
@@ -797,14 +811,25 @@ export class ProviderProcessSupervisor {
|
||||
this.#stderrMarker?.scan(bytes, this.#stderrObservedBytes);
|
||||
this.#stderrObservedBytes += bytes.byteLength;
|
||||
}
|
||||
const remaining =
|
||||
this.#maxDiagnosticBytes === undefined
|
||||
const aggregateRemaining =
|
||||
this.#maxAggregateDiagnosticBytes === undefined
|
||||
? bytes.byteLength
|
||||
: Math.max(
|
||||
0,
|
||||
this.#maxDiagnosticBytes - this.#stdout.bytes - this.#stderr.bytes,
|
||||
this.#maxAggregateDiagnosticBytes -
|
||||
this.#stdout.bytes -
|
||||
this.#stderr.bytes,
|
||||
);
|
||||
const retained = Math.min(bytes.byteLength, remaining);
|
||||
const streamLimit = this.#maxStreamDiagnosticBytes?.[stream];
|
||||
const streamRemaining =
|
||||
streamLimit === undefined
|
||||
? bytes.byteLength
|
||||
: Math.max(0, streamLimit - capture.bytes);
|
||||
const retained = Math.min(
|
||||
bytes.byteLength,
|
||||
aggregateRemaining,
|
||||
streamRemaining,
|
||||
);
|
||||
if (retained < bytes.byteLength) this.#diagnosticTruncated = true;
|
||||
if (retained > 0) capture.append(bytes.subarray(0, retained));
|
||||
this.#options.onDiagnostic?.({
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { PassThrough } from "node:stream";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { ProviderProcessSupervisor } from "./ProviderProcess.js";
|
||||
import {
|
||||
ProviderProcessSupervisor,
|
||||
type ProcessDiagnosticBudget,
|
||||
} from "./ProviderProcess.js";
|
||||
import { redactCapturedTransportCredential } from "./ProviderDiagnosticRedaction.js";
|
||||
|
||||
it("retains final stderr between verified exit and output close during stop", async () => {
|
||||
@@ -332,6 +335,169 @@ it("scopes output checkpoints to raw bytes on one supervisor and epoch", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it.each<{ budget: ProcessDiagnosticBudget; stderr: string }>([
|
||||
{ budget: 4, stderr: "" },
|
||||
{ budget: { stdout: 4, stderr: 4 }, stderr: "wxyz" },
|
||||
])(
|
||||
"distinguishes aggregate and per-stream output budgets: $budget",
|
||||
({ budget, stderr }) => {
|
||||
const producer = Object.assign(new EventEmitter(), {
|
||||
stdout: new PassThrough(),
|
||||
stderr: new PassThrough(),
|
||||
exitCode: null,
|
||||
signalCode: null,
|
||||
kill: () => false,
|
||||
});
|
||||
const supervisor = new ProviderProcessSupervisor(
|
||||
{ process: producer, ownsProcessLifetime: false },
|
||||
{ maxDiagnosticBytes: budget },
|
||||
);
|
||||
try {
|
||||
const checkpoint = supervisor.checkpointOutput();
|
||||
producer.stdout.write("abcd");
|
||||
producer.stderr.write("wxyz");
|
||||
expect(supervisor.snapshot().stdout).toEqual({
|
||||
text: "abcd",
|
||||
bytes: 4,
|
||||
observedBytes: 4,
|
||||
});
|
||||
expect(supervisor.snapshot().stderr).toEqual({
|
||||
text: stderr,
|
||||
bytes: stderr.length,
|
||||
observedBytes: 4,
|
||||
});
|
||||
expect(supervisor.snapshot().diagnosticTruncated === true).toBe(
|
||||
stderr === "",
|
||||
);
|
||||
expect(supervisor.snapshotOutputSince(checkpoint).complete).toBe(
|
||||
stderr !== "",
|
||||
);
|
||||
} finally {
|
||||
supervisor.dispose();
|
||||
producer.stdout.destroy();
|
||||
producer.stderr.destroy();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["stdout", "stderr"] as const)(
|
||||
"drains and reports overflowing %s without consuming the other stream's budget",
|
||||
(stream) => {
|
||||
const producer = Object.assign(new EventEmitter(), {
|
||||
stdout: new PassThrough(),
|
||||
stderr: new PassThrough(),
|
||||
exitCode: null,
|
||||
signalCode: null,
|
||||
kill: () => false,
|
||||
});
|
||||
const events: unknown[] = [];
|
||||
const supervisor = new ProviderProcessSupervisor(
|
||||
{ process: producer, ownsProcessLifetime: false },
|
||||
{
|
||||
maxDiagnosticBytes: { stdout: 4, stderr: 4 },
|
||||
onDiagnostic: (event) => {
|
||||
if (event.type === "output") events.push(event);
|
||||
},
|
||||
},
|
||||
);
|
||||
const other = stream === "stdout" ? "stderr" : "stdout";
|
||||
try {
|
||||
producer[stream].write("abcde");
|
||||
producer[stream].write("fg");
|
||||
producer[other].write("wxyz");
|
||||
expect(supervisor.snapshot()).toMatchObject({
|
||||
diagnosticTruncated: true,
|
||||
[stream]: { text: "abcd", bytes: 4, observedBytes: 7 },
|
||||
[other]: { text: "wxyz", bytes: 4, observedBytes: 4 },
|
||||
});
|
||||
expect(events).toEqual([
|
||||
{ type: "output", stream, bytes: 5, totalBytes: 5, truncated: true },
|
||||
{ type: "output", stream, bytes: 2, totalBytes: 7, truncated: true },
|
||||
{ type: "output", stream: other, bytes: 4, totalBytes: 4 },
|
||||
]);
|
||||
} finally {
|
||||
supervisor.dispose();
|
||||
producer.stdout.destroy();
|
||||
producer.stderr.destroy();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("tightens aggregate retention without replacing or relaxing the original stream caps", () => {
|
||||
const producer = Object.assign(new EventEmitter(), {
|
||||
stdout: new PassThrough(),
|
||||
stderr: new PassThrough(),
|
||||
exitCode: null,
|
||||
signalCode: null,
|
||||
kill: () => false,
|
||||
});
|
||||
const supervisor = new ProviderProcessSupervisor(
|
||||
{ process: producer, ownsProcessLifetime: false },
|
||||
{ maxDiagnosticBytes: { stdout: 4, stderr: 10 } },
|
||||
);
|
||||
try {
|
||||
producer.stdout.write("abc");
|
||||
producer.stderr.write("de");
|
||||
supervisor.limitAdditionalOutputBytes(4);
|
||||
producer.stdout.write("fgh");
|
||||
producer.stderr.write("ijklmn");
|
||||
expect(supervisor.snapshot()).toMatchObject({
|
||||
diagnosticTruncated: true,
|
||||
stdout: { text: "abcf", bytes: 4, observedBytes: 6 },
|
||||
stderr: { text: "deijk", bytes: 5, observedBytes: 8 },
|
||||
});
|
||||
supervisor.limitAdditionalOutputBytes(100);
|
||||
producer.stderr.write("op");
|
||||
expect(supervisor.snapshot().stderr).toEqual({
|
||||
text: "deijk",
|
||||
bytes: 5,
|
||||
observedBytes: 10,
|
||||
});
|
||||
} finally {
|
||||
supervisor.dispose();
|
||||
producer.stdout.destroy();
|
||||
producer.stderr.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
it.each(["stdout", "stderr"] as const)(
|
||||
"retains raw UTF-8 and checkpoint facts while scanning %s markers past its stream cap",
|
||||
(stream) => {
|
||||
const producer = Object.assign(new EventEmitter(), {
|
||||
stdout: new PassThrough(),
|
||||
stderr: new PassThrough(),
|
||||
exitCode: null,
|
||||
signalCode: null,
|
||||
kill: () => false,
|
||||
});
|
||||
const supervisor = new ProviderProcessSupervisor(
|
||||
{ process: producer, ownsProcessLifetime: false },
|
||||
{ maxDiagnosticBytes: { stdout: 4, stderr: 4 }, outputToken: "DONE" },
|
||||
);
|
||||
try {
|
||||
const checkpoint = supervisor.checkpointOutput();
|
||||
const bytes = Buffer.from("é🧭 DONE\n");
|
||||
producer[stream].write(bytes.subarray(0, 3));
|
||||
producer[stream].write(bytes.subarray(3));
|
||||
const scoped = supervisor.snapshotOutputSince(checkpoint);
|
||||
expect(scoped.complete).toBe(false);
|
||||
expect(scoped.snapshot[stream]).toEqual({
|
||||
text: "é\ufffd",
|
||||
bytes: 4,
|
||||
observedBytes: 12,
|
||||
invalidUtf8: true,
|
||||
});
|
||||
expect(scoped.markers).toEqual([
|
||||
{ stream, offset: 7, line: "é🧭 DONE", truncated: false },
|
||||
]);
|
||||
} finally {
|
||||
supervisor.dispose();
|
||||
producer.stdout.destroy();
|
||||
producer.stderr.destroy();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["stdout", "stderr"] as const)(
|
||||
"redacts a budget-cut credential in scoped %s after prior output",
|
||||
(stream) => {
|
||||
|
||||
@@ -210,7 +210,15 @@ it("rejects an omitted auxiliary source at the actual package verification bound
|
||||
"--input-type=module",
|
||||
"-e",
|
||||
`import { verifyPackedBridge } from "./scripts/verify-packed-bridge.mjs";
|
||||
await verifyPackedBridge({ tarball: "unused-because-admission-must-fail", packedFiles: JSON.parse(process.argv[1]) });`,
|
||||
import { createPackageVerifierCommands } from "./scripts/lib/verify-package-commands.mjs";
|
||||
import { withVerifierCleanup } from "./scripts/lib/verifier-cleanup.mjs";
|
||||
import { PrivateRuntimeRoot } from "./dist/process/PrivateRuntimeRoot.js";
|
||||
const root = await PrivateRuntimeRoot.create({ prefix: "rea-bridge-admission-" });
|
||||
let commands;
|
||||
await withVerifierCleanup(async () => {
|
||||
commands = await createPackageVerifierCommands({ workspace: root });
|
||||
await verifyPackedBridge({ tarball: "unused-because-admission-must-fail", packedFiles: JSON.parse(process.argv[1]), commands });
|
||||
}, [() => commands === undefined ? root.close() : commands.close()]);`,
|
||||
JSON.stringify(packedFiles),
|
||||
]),
|
||||
).rejects.toMatchObject({
|
||||
|
||||
@@ -95,17 +95,20 @@ it("bounds retained diagnostics across both streams during a real output burst",
|
||||
}
|
||||
});
|
||||
|
||||
it("collects a real owned command and verifies its exit before returning diagnostics", async () => {
|
||||
const result = await runOwnedCommand(
|
||||
command('process.stdout.write("observed-output")'),
|
||||
{ timeoutMs: 2_000, diagnosticBytes: 1024 },
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
stdout: { text: "observed-output", bytes: 15 },
|
||||
});
|
||||
});
|
||||
it.each([2_000, null])(
|
||||
"collects a real owned command and verifies its exit before returning diagnostics (timeout %s)",
|
||||
async (timeoutMs) => {
|
||||
const result = await runOwnedCommand(
|
||||
command('process.stdout.write("observed-output")'),
|
||||
{ timeoutMs, diagnosticBytes: 1024 },
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
stdout: { text: "observed-output", bytes: 15 },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([0, 2])(
|
||||
"observes complete accepted output once before final cleanup (exit %s)",
|
||||
@@ -155,50 +158,56 @@ it("does not interpret output rejected by its complete-output budget", async ()
|
||||
expect(observeCompleted).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("retains late cancellation separately from a physical failure with refused cleanup", async () => {
|
||||
const controller = new AbortController();
|
||||
const callerReason = { request: "cancel during refused finalization" };
|
||||
let denied = true;
|
||||
let failure: OwnedCommandFailure | undefined;
|
||||
try {
|
||||
await runOwnedCommand(
|
||||
command("process.exit(2)"),
|
||||
{ timeoutMs: 2000, diagnosticBytes: 1024 },
|
||||
{
|
||||
signal: controller.signal,
|
||||
launcher: async (input) => {
|
||||
const acquired = await spawnOwnedProviderProcess(input);
|
||||
return {
|
||||
...acquired,
|
||||
cleanup: async () => {
|
||||
if (denied) {
|
||||
controller.abort(callerReason);
|
||||
return { cleaned: false, reason: "controlled cleanup refusal" };
|
||||
}
|
||||
return cleanupOwnedProcessGroup(acquired.ownership);
|
||||
},
|
||||
};
|
||||
it.each([2000, null])(
|
||||
"retains late cancellation separately from a physical failure with refused cleanup (timeout %s)",
|
||||
async (timeoutMs) => {
|
||||
const controller = new AbortController();
|
||||
const callerReason = { request: "cancel during refused finalization" };
|
||||
let denied = true;
|
||||
let failure: OwnedCommandFailure | undefined;
|
||||
try {
|
||||
await runOwnedCommand(
|
||||
command("process.exit(2)"),
|
||||
{ timeoutMs, diagnosticBytes: 1024 },
|
||||
{
|
||||
signal: controller.signal,
|
||||
launcher: async (input) => {
|
||||
const acquired = await spawnOwnedProviderProcess(input);
|
||||
return {
|
||||
...acquired,
|
||||
cleanup: async () => {
|
||||
if (denied) {
|
||||
controller.abort(callerReason);
|
||||
return {
|
||||
cleaned: false,
|
||||
reason: "controlled cleanup refusal",
|
||||
};
|
||||
}
|
||||
return cleanupOwnedProcessGroup(acquired.ownership);
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
).catch((cause: unknown) => {
|
||||
if (!(cause instanceof OwnedCommandFailure)) throw cause;
|
||||
failure = cause;
|
||||
});
|
||||
expect(failure).toMatchObject({
|
||||
reason: "process",
|
||||
snapshot: { exitCode: 2 },
|
||||
cleanupFailure: "controlled cleanup refusal",
|
||||
});
|
||||
expect(failure?.cancellation?.reason).toBe(callerReason);
|
||||
denied = false;
|
||||
expect(await failure?.cleanupOwner?.stop()).toEqual({
|
||||
status: "verified-cleanup",
|
||||
});
|
||||
} finally {
|
||||
denied = false;
|
||||
await failure?.cleanupOwner?.stop();
|
||||
}
|
||||
});
|
||||
).catch((cause: unknown) => {
|
||||
if (!(cause instanceof OwnedCommandFailure)) throw cause;
|
||||
failure = cause;
|
||||
});
|
||||
expect(failure).toMatchObject({
|
||||
reason: "process",
|
||||
snapshot: { exitCode: 2 },
|
||||
cleanupFailure: "controlled cleanup refusal",
|
||||
});
|
||||
expect(failure?.cancellation?.reason).toBe(callerReason);
|
||||
denied = false;
|
||||
expect(await failure?.cleanupOwner?.stop()).toEqual({
|
||||
status: "verified-cleanup",
|
||||
});
|
||||
} finally {
|
||||
denied = false;
|
||||
await failure?.cleanupOwner?.stop();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps an exit failure observed before cancellation primary across delayed classification", async () => {
|
||||
const controller = new AbortController();
|
||||
@@ -576,38 +585,41 @@ it.skipIf(process.platform === "win32")(
|
||||
},
|
||||
);
|
||||
|
||||
it("cancels a real acquired process and independently releases it", async () => {
|
||||
const controller = new AbortController();
|
||||
const callerReason = {
|
||||
request: "active cancellation",
|
||||
cleanup: "caller metadata",
|
||||
};
|
||||
let pid: number | undefined;
|
||||
const result = runOwnedCommand(
|
||||
command(
|
||||
'process.stdout.write("ready\\n"); setInterval(() => undefined, 1000)',
|
||||
),
|
||||
{ timeoutMs: 5_000, diagnosticBytes: 1024 },
|
||||
{
|
||||
signal: controller.signal,
|
||||
launcher: async (input) => {
|
||||
const process = await spawnOwnedProviderProcess(input);
|
||||
pid = process.process.pid;
|
||||
await waitForProviderProcessReady(process.process);
|
||||
setImmediate(() => controller.abort(callerReason));
|
||||
return process;
|
||||
it.each([5_000, null])(
|
||||
"cancels a real acquired process and independently releases it (timeout %s)",
|
||||
async (timeoutMs) => {
|
||||
const controller = new AbortController();
|
||||
const callerReason = {
|
||||
request: "active cancellation",
|
||||
cleanup: "caller metadata",
|
||||
};
|
||||
let pid: number | undefined;
|
||||
const result = runOwnedCommand(
|
||||
command(
|
||||
'process.stdout.write("ready\\n"); setInterval(() => undefined, 1000)',
|
||||
),
|
||||
{ timeoutMs, diagnosticBytes: 1024 },
|
||||
{
|
||||
signal: controller.signal,
|
||||
launcher: async (input) => {
|
||||
const process = await spawnOwnedProviderProcess(input);
|
||||
pid = process.process.pid;
|
||||
await waitForProviderProcessReady(process.process);
|
||||
setImmediate(() => controller.abort(callerReason));
|
||||
return process;
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
await expect(result).rejects.toMatchObject({
|
||||
reason: "cancelled",
|
||||
cleanupFailure: null,
|
||||
cancellation: { reason: callerReason },
|
||||
});
|
||||
if (pid === undefined) throw new Error("Process was not acquired");
|
||||
const acquiredPid = pid;
|
||||
expect(() => process.kill(acquiredPid, 0)).toThrow();
|
||||
});
|
||||
);
|
||||
await expect(result).rejects.toMatchObject({
|
||||
reason: "cancelled",
|
||||
cleanupFailure: null,
|
||||
cancellation: { reason: callerReason },
|
||||
});
|
||||
if (pid === undefined) throw new Error("Process was not acquired");
|
||||
const acquiredPid = pid;
|
||||
expect(() => process.kill(acquiredPid, 0)).toThrow();
|
||||
},
|
||||
);
|
||||
|
||||
it("returns a timeout for a running process after independent cleanup", async () => {
|
||||
await expect(
|
||||
|
||||
@@ -1,49 +1,61 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { copyFile, mkdir, writeFile } from "node:fs/promises";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { expect, it } from "vitest";
|
||||
import { expect } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { processTest } from "../../support/process/processFixture.js";
|
||||
|
||||
const execute = promisify(execFile);
|
||||
|
||||
it("runs the caller-selected npm entry through Node without rewriting its inputs", async () => {
|
||||
const root = await createTestTempDirectory("rea-npm-invocation-");
|
||||
const npmEntry = join(root, "selected npm entry.mjs");
|
||||
await writeFile(
|
||||
npmEntry,
|
||||
`process.stdout.write(JSON.stringify({ arguments: process.argv.slice(2), cwd: process.cwd(), marker: process.env.REA_NPM_TEST_MARKER }));\n`,
|
||||
);
|
||||
const helper = pathToFileURL(
|
||||
resolve("scripts/lib/verify-package-core.mjs"),
|
||||
).href;
|
||||
const program = `
|
||||
const { execNpm } = await import(${JSON.stringify(helper)});
|
||||
const result = await execNpm(["pack", "value with spaces"], {
|
||||
cwd: ${JSON.stringify(root)},
|
||||
env: { ...process.env, npm_execpath: ${JSON.stringify(npmEntry)}, REA_NPM_TEST_MARKER: "caller-selected" },
|
||||
processTest(
|
||||
"runs the caller-selected npm entry through Node without rewriting its inputs",
|
||||
async ({ processes }) => {
|
||||
const root = await createTestTempDirectory("rea-npm-invocation-");
|
||||
const npmEntry = join(root, "selected npm entry.mjs");
|
||||
await writeFile(
|
||||
npmEntry,
|
||||
`process.stdout.write(JSON.stringify({ arguments: process.argv.slice(2), cwd: process.cwd(), marker: process.env.REA_NPM_TEST_MARKER }));\n`,
|
||||
);
|
||||
const helper = pathToFileURL(
|
||||
resolve("scripts/lib/verify-package-commands.mjs"),
|
||||
).href;
|
||||
const cleanup = pathToFileURL(
|
||||
resolve("scripts/lib/verifier-cleanup.mjs"),
|
||||
).href;
|
||||
const program = `
|
||||
const { createPackageVerifierCommands } = await import(${JSON.stringify(helper)});
|
||||
const { withVerifierCleanup } = await import(${JSON.stringify(cleanup)});
|
||||
const commands = await createPackageVerifierCommands({
|
||||
workspace: { path: ${JSON.stringify(root)}, close: async () => {} },
|
||||
});
|
||||
const result = await withVerifierCleanup(
|
||||
() => commands.execNpm(["pack", "value with spaces"], {
|
||||
cwd: ${JSON.stringify(root)},
|
||||
env: { ...process.env, npm_execpath: ${JSON.stringify(npmEntry)}, REA_NPM_TEST_MARKER: "caller-selected" },
|
||||
}),
|
||||
[() => commands.close()],
|
||||
);
|
||||
process.stdout.write(result.stdout);
|
||||
`;
|
||||
const result = await execute(
|
||||
process.execPath,
|
||||
["--input-type=module", "--eval", program],
|
||||
{ windowsHide: true },
|
||||
);
|
||||
expect(JSON.parse(result.stdout)).toEqual({
|
||||
arguments: ["pack", "value with spaces"],
|
||||
cwd: root,
|
||||
marker: "caller-selected",
|
||||
});
|
||||
});
|
||||
const result = await processes.run(
|
||||
process.execPath,
|
||||
["--input-type=module", "--eval", program],
|
||||
{ windowsHide: true, timeoutMs: 20_000 },
|
||||
);
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
expect(result.signal, result.stderr).toBeNull();
|
||||
expect(JSON.parse(result.stdout)).toEqual({
|
||||
arguments: ["pack", "value with spaces"],
|
||||
cwd: root,
|
||||
marker: "caller-selected",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const windowsIt = process.platform === "win32" ? it : it.skip;
|
||||
const windowsIt = processTest.skipIf(process.platform !== "win32");
|
||||
|
||||
windowsIt(
|
||||
"uses the npm entry adjacent to Node when none is selected",
|
||||
async () => {
|
||||
async ({ processes }) => {
|
||||
const root = await createTestTempDirectory("rea-adjacent-npm-");
|
||||
const node = join(root, "node.exe");
|
||||
const npmEntry = join(root, "node_modules/npm/bin/npm-cli.js");
|
||||
@@ -56,25 +68,37 @@ windowsIt(
|
||||
),
|
||||
]);
|
||||
const helper = pathToFileURL(
|
||||
resolve("scripts/lib/verify-package-core.mjs"),
|
||||
resolve("scripts/lib/verify-package-commands.mjs"),
|
||||
).href;
|
||||
const cleanup = pathToFileURL(
|
||||
resolve("scripts/lib/verifier-cleanup.mjs"),
|
||||
).href;
|
||||
const program = `
|
||||
const { execNpm } = await import(${JSON.stringify(helper)});
|
||||
const { createPackageVerifierCommands } = await import(${JSON.stringify(helper)});
|
||||
const { withVerifierCleanup } = await import(${JSON.stringify(cleanup)});
|
||||
const commands = await createPackageVerifierCommands({
|
||||
workspace: { path: ${JSON.stringify(root)}, close: async () => {} },
|
||||
});
|
||||
const environment = { ...process.env };
|
||||
delete environment.npm_execpath;
|
||||
const result = await execNpm(["pack", "value with spaces"], {
|
||||
cwd: ${JSON.stringify(root)},
|
||||
env: environment,
|
||||
});
|
||||
const result = await withVerifierCleanup(
|
||||
() => commands.execNpm(["pack", "value with spaces"], {
|
||||
cwd: ${JSON.stringify(root)},
|
||||
env: environment,
|
||||
}),
|
||||
[() => commands.close()],
|
||||
);
|
||||
process.stdout.write(result.stdout);
|
||||
`;
|
||||
const environment = { ...process.env };
|
||||
delete environment.npm_execpath;
|
||||
const result = await execute(
|
||||
const result = await processes.run(
|
||||
node,
|
||||
["--input-type=module", "--eval", program],
|
||||
{ env: environment, windowsHide: true },
|
||||
{ env: environment, windowsHide: true, timeoutMs: 20_000 },
|
||||
);
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
expect(result.signal, result.stderr).toBeNull();
|
||||
expect(JSON.parse(result.stdout)).toEqual({
|
||||
arguments: ["pack", "value with spaces"],
|
||||
cwd: root,
|
||||
|
||||
@@ -38,19 +38,30 @@ it("keeps npm pack output in the owned workspace when package validation fails",
|
||||
const packerUrl = pathToFileURL(
|
||||
resolve("scripts/verify-package-pack.mjs"),
|
||||
).href;
|
||||
const commandsUrl = pathToFileURL(
|
||||
resolve("scripts/lib/verify-package-commands.mjs"),
|
||||
).href;
|
||||
const cleanupUrl = pathToFileURL(
|
||||
resolve("scripts/lib/verifier-cleanup.mjs"),
|
||||
).href;
|
||||
const harness = `
|
||||
import { rm } from "node:fs/promises";
|
||||
import { createPackageVerifierCommands } from ${JSON.stringify(commandsUrl)};
|
||||
import { withVerifierCleanup } from ${JSON.stringify(cleanupUrl)};
|
||||
const workspace = ${JSON.stringify(workspace)};
|
||||
const scratch = { path: workspace, close: () => rm(workspace, { recursive: true, force: true }) };
|
||||
let commands;
|
||||
let failure;
|
||||
await withVerifierCleanup(async () => {
|
||||
commands = await createPackageVerifierCommands({ workspace: scratch });
|
||||
try {
|
||||
const { verifyPackagePack } = await import(${JSON.stringify(packerUrl)});
|
||||
await verifyPackagePack({ root: ${JSON.stringify(packageRoot)}, workspace });
|
||||
await verifyPackagePack({ root: ${JSON.stringify(packageRoot)}, workspace, commands });
|
||||
} catch (cause) {
|
||||
if (!(cause instanceof Error) || cause.message !== ${JSON.stringify(expectedFailure)}) throw cause;
|
||||
failure = cause.message;
|
||||
} finally {
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
}
|
||||
}, [() => commands === undefined ? scratch.close() : commands.close()]);
|
||||
if (failure !== ${JSON.stringify(expectedFailure)}) throw new Error("Expected package metadata validation to fail after packing");
|
||||
process.stdout.write(JSON.stringify({ failure }));
|
||||
`;
|
||||
|
||||
@@ -33,13 +33,9 @@ async function verifyListing(listing: string) {
|
||||
const { stdout } = await execFileAsync(process.execPath, [
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
`import childProcess from "node:child_process";
|
||||
import { syncBuiltinESMExports } from "node:module";
|
||||
import { promisify } from "node:util";
|
||||
import { FULL_CATALOG_IDENTITY } from ${JSON.stringify(catalogUrl)};
|
||||
`import { FULL_CATALOG_IDENTITY } from ${JSON.stringify(catalogUrl)};
|
||||
let bridgeParsed = false;
|
||||
childProcess.execFile = () => { throw new Error("Expected promisified execution"); };
|
||||
childProcess.execFile[promisify.custom] = async (command, args) => {
|
||||
const exec = async (command, args) => {
|
||||
let stdout;
|
||||
if ((command === "npm" && args[0] === "pack") || (command === process.execPath && args[1] === "pack")) stdout = "fixture.tgz\\n";
|
||||
else if (command === "tar" && args[0] === "-tf") stdout = ${JSON.stringify(listing)};
|
||||
@@ -52,10 +48,10 @@ childProcess.execFile[promisify.custom] = async (command, args) => {
|
||||
else throw new Error("Unexpected command: " + command + " " + JSON.stringify(args));
|
||||
return { stdout, stderr: "" };
|
||||
};
|
||||
syncBuiltinESMExports();
|
||||
const commands = { exec, execNpm: (args) => exec("npm", args) };
|
||||
const { verifyPackagePack } = await import(${JSON.stringify(packerUrl)});
|
||||
try {
|
||||
const result = await verifyPackagePack({ root: process.cwd(), workspace: ${JSON.stringify(workspace)} });
|
||||
const result = await verifyPackagePack({ root: process.cwd(), workspace: ${JSON.stringify(workspace)}, commands });
|
||||
process.stdout.write(JSON.stringify({ ...result, bridgeParsed }));
|
||||
} catch (cause) {
|
||||
process.stdout.write(JSON.stringify({ error: cause.message, bridgeParsed }));
|
||||
|
||||
@@ -72,7 +72,16 @@ it("retains upstream body failure and joins the active updater and installer chi
|
||||
const verifierUrl = pathToFileURL(
|
||||
resolve("scripts/verify-package-update.mjs"),
|
||||
).href;
|
||||
const commandsUrl = pathToFileURL(
|
||||
resolve("scripts/lib/verify-package-commands.mjs"),
|
||||
).href;
|
||||
const cleanupUrl = pathToFileURL(
|
||||
resolve("scripts/lib/verifier-cleanup.mjs"),
|
||||
).href;
|
||||
const harness = `
|
||||
import { readFile, rm } from "node:fs/promises";
|
||||
import { createPackageVerifierCommands } from ${JSON.stringify(commandsUrl)};
|
||||
import { withVerifierCleanup } from ${JSON.stringify(cleanupUrl)};
|
||||
globalThis.fetch = async () => ({
|
||||
status: 200,
|
||||
headers: new Headers({ "content-type": "application/json" }),
|
||||
@@ -81,7 +90,12 @@ globalThis.fetch = async () => ({
|
||||
},
|
||||
});
|
||||
const { verifyPackageUpdate } = await import(${JSON.stringify(verifierUrl)});
|
||||
const scratch = { path: ${JSON.stringify(workspace)}, close: () => rm(${JSON.stringify(workspace)}, { recursive: true, force: true }) };
|
||||
let commands;
|
||||
let failure;
|
||||
let pids;
|
||||
await withVerifierCleanup(async () => {
|
||||
commands = await createPackageVerifierCommands({ workspace: scratch });
|
||||
try {
|
||||
await verifyPackageUpdate({
|
||||
prefix: ${JSON.stringify(prefix)},
|
||||
@@ -89,12 +103,13 @@ try {
|
||||
tarball: ${JSON.stringify(tarball)},
|
||||
workspace: ${JSON.stringify(workspace)},
|
||||
environment: { PATH: process.env.PATH },
|
||||
commands,
|
||||
});
|
||||
} catch (cause) {
|
||||
failure = cause instanceof Error ? cause.message : String(cause);
|
||||
}
|
||||
const { readFile } = await import("node:fs/promises");
|
||||
const pids = JSON.parse(await readFile(${JSON.stringify(descendants)}, "utf8"));
|
||||
pids = JSON.parse(await readFile(${JSON.stringify(descendants)}, "utf8"));
|
||||
}, [() => commands === undefined ? scratch.close() : commands.close()]);
|
||||
let livePids;
|
||||
if (process.platform === "win32") {
|
||||
livePids = pids.filter((pid) => {
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { join, resolve } from "node:path";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { expect } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { liveProcesses } from "../../../src/process/ProcessOwnershipProcessTree.js";
|
||||
import { systemProcessOwnershipHost } from "../../../src/process/ProcessOwnershipObservation.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { processTest } from "../../support/process/processFixture.js";
|
||||
|
||||
const descendantSource = `
|
||||
import { appendFileSync, existsSync, writeFileSync } from "node:fs";
|
||||
const record = (entry) => appendFileSync(process.env.OWNER_EVENTS, JSON.stringify(entry) + "\\n");
|
||||
const finish = (reason) => {
|
||||
record({ kind: "descendant-exit", reason, rootExists: existsSync(process.env.OWNER_ROOT) });
|
||||
process.exit(0);
|
||||
};
|
||||
process.on("SIGTERM", () => finish("SIGTERM"));
|
||||
writeFileSync(process.env.OWNER_DESCENDANT, String(process.pid));
|
||||
process.send({ pid: process.pid });
|
||||
setInterval(() => {
|
||||
if (existsSync(process.env.OWNER_RELEASE)) finish("fixture-release");
|
||||
}, 25);
|
||||
setTimeout(() => finish("fixture-deadline"), 20000);
|
||||
`;
|
||||
|
||||
const npmSource = `
|
||||
import { spawn } from "node:child_process";
|
||||
import { appendFileSync, existsSync, writeFileSync } from "node:fs";
|
||||
process.once("SIGTERM", () => {
|
||||
appendFileSync(process.env.OWNER_EVENTS, JSON.stringify({ kind: "leader-exit", rootExists: existsSync(process.env.OWNER_ROOT) }) + "\\n");
|
||||
process.kill(process.pid, "SIGTERM");
|
||||
});
|
||||
const child = spawn(process.execPath, [process.env.OWNER_DESCENDANT_ENTRY], {
|
||||
stdio: ["ignore", "ignore", "ignore", "ipc"],
|
||||
});
|
||||
const ready = await new Promise((resolve, reject) => {
|
||||
child.once("message", resolve);
|
||||
child.once("error", reject);
|
||||
});
|
||||
writeFileSync(process.env.OWNER_READY, JSON.stringify({ pids: [process.pid, ready.pid] }));
|
||||
process.stdout.write("npm descendant ready\\n");
|
||||
process.stderr.write("selected npm diagnostics\\n");
|
||||
setInterval(() => {}, 1000);
|
||||
`;
|
||||
|
||||
const reportSchema = z.object({
|
||||
pids: z.array(z.number().int().positive()).length(2),
|
||||
liveAtRemoval: z.array(z.number().int().positive()),
|
||||
rootRemoved: z.boolean(),
|
||||
failure: z.object({
|
||||
reason: z.literal("timeout"),
|
||||
stdout: z.string(),
|
||||
stderr: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
const stopReceiptSchema = z.object({
|
||||
kind: z.enum(["leader-exit", "descendant-exit"]),
|
||||
rootExists: z.boolean(),
|
||||
reason: z.string().optional(),
|
||||
});
|
||||
|
||||
const releaseDescendant = async (release: string, receipt: string) => {
|
||||
await writeFile(release, "release this authored descendant");
|
||||
const pid = await readFile(receipt, "utf8").then(Number, (cause: unknown) => {
|
||||
if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
|
||||
return undefined;
|
||||
throw cause;
|
||||
});
|
||||
if (pid === undefined) return;
|
||||
const deadline = Date.now() + 7_500;
|
||||
while (
|
||||
liveProcesses(await systemProcessOwnershipHost.listProcesses()).some(
|
||||
(entry) => entry.pid === pid,
|
||||
)
|
||||
) {
|
||||
if (Date.now() >= deadline)
|
||||
throw new Error(
|
||||
"Authored npm descendant did not consume its release marker",
|
||||
);
|
||||
await delay(25);
|
||||
}
|
||||
};
|
||||
|
||||
// Actual npm-entry/descendant processes and actual scratch removal; no package
|
||||
// install, native engine, or WABT behavior is simulated or claimed.
|
||||
processTest.skipIf(process.platform === "win32")(
|
||||
"joins a timed-out npm entry and its acknowledged descendant before removing the verifier root",
|
||||
{ timeout: 30_000 },
|
||||
async ({ processes }) => {
|
||||
const root = await createTestTempDirectory("rea-package-command-owner-");
|
||||
const scratch = join(root, "scratch");
|
||||
const entry = join(root, "selected npm entry.mjs");
|
||||
const descendant = join(root, "descendant.mjs");
|
||||
const release = join(root, "release-descendant");
|
||||
const receipt = join(root, "descendant.pid");
|
||||
const ready = join(root, "ready.json");
|
||||
const events = join(root, "events.jsonl");
|
||||
const driver = join(root, "driver.mjs");
|
||||
await writeFile(entry, npmSource);
|
||||
await writeFile(descendant, descendantSource);
|
||||
const commandsModule = pathToFileURL(
|
||||
resolve("scripts/lib/verify-package-commands.mjs"),
|
||||
).href;
|
||||
const cleanup = pathToFileURL(
|
||||
resolve("scripts/lib/verifier-cleanup.mjs"),
|
||||
).href;
|
||||
const observations = pathToFileURL(
|
||||
resolve("dist/process/ProcessOwnershipObservation.js"),
|
||||
).href;
|
||||
const tree = pathToFileURL(
|
||||
resolve("dist/process/ProcessOwnershipProcessTree.js"),
|
||||
).href;
|
||||
await writeFile(
|
||||
driver,
|
||||
`
|
||||
import { existsSync } from "node:fs";
|
||||
import { mkdir, readFile, rm } from "node:fs/promises";
|
||||
import { createPackageVerifierCommands } from ${JSON.stringify(commandsModule)};
|
||||
import { withVerifierCleanup } from ${JSON.stringify(cleanup)};
|
||||
import { systemProcessOwnershipHost } from ${JSON.stringify(observations)};
|
||||
import { liveProcesses } from ${JSON.stringify(tree)};
|
||||
const scratch = process.env.OWNER_ROOT;
|
||||
await mkdir(scratch);
|
||||
let commands, recoveryOwner, pids, liveAtRemoval, failure, diagnostics;
|
||||
const workspace = {
|
||||
path: scratch,
|
||||
async close() {
|
||||
const ready = await readFile(process.env.OWNER_READY, "utf8").then(JSON.parse, cause => {
|
||||
if (cause.code === "ENOENT") return { pids: [] };
|
||||
throw cause;
|
||||
});
|
||||
pids = ready.pids;
|
||||
liveAtRemoval = liveProcesses(await systemProcessOwnershipHost.listProcesses()).filter(({ pid }) => pids.includes(pid)).map(({ pid }) => pid);
|
||||
await rm(scratch, { recursive: true, force: true });
|
||||
},
|
||||
};
|
||||
try {
|
||||
await withVerifierCleanup(
|
||||
async () => {
|
||||
commands = await createPackageVerifierCommands({ workspace });
|
||||
await commands.execNpm(["pack", "caller value with spaces"], { cwd: scratch, env: process.env, timeout: 1500 });
|
||||
},
|
||||
[async () => {
|
||||
if (commands === undefined) await rm(scratch, { recursive: true, force: true });
|
||||
else await commands.close();
|
||||
}],
|
||||
);
|
||||
} catch (cause) {
|
||||
const failures = cause instanceof AggregateError ? cause.errors : [cause];
|
||||
// Capture exact cleanup authority before projecting facts or making assertions.
|
||||
recoveryOwner = failures.find(error => typeof error?.cleanupOwner?.close === "function")?.cleanupOwner ?? commands;
|
||||
const primary = failures.find(error => error?.snapshot !== undefined && typeof error?.reason === "string");
|
||||
failure = { reason: primary?.reason, stdout: primary?.snapshot?.stdout.text, stderr: primary?.snapshot?.stderr.text };
|
||||
diagnostics = failures.map(error => ({
|
||||
name: error?.name ?? typeof error,
|
||||
message: error?.message ?? String(error),
|
||||
reason: error?.reason,
|
||||
snapshotAvailable: error?.snapshot !== undefined && error?.snapshot !== null,
|
||||
stack: error?.stack,
|
||||
cleanupFailure: error?.cleanupFailure,
|
||||
processError: error?.processError?.message,
|
||||
preparationFailure: error?.preparationFailure?.message,
|
||||
cause: {
|
||||
name: error?.cause?.name,
|
||||
message: error?.cause?.message ?? String(error?.cause),
|
||||
stack: error?.cause?.stack,
|
||||
cause: error?.cause?.cause?.message ?? String(error?.cause?.cause),
|
||||
},
|
||||
}));
|
||||
} finally {
|
||||
if (recoveryOwner !== undefined) await recoveryOwner.close();
|
||||
else if (commands !== undefined) await commands.close();
|
||||
else await rm(scratch, { recursive: true, force: true });
|
||||
}
|
||||
process.stdout.write(JSON.stringify({ pids, liveAtRemoval, rootRemoved: !existsSync(scratch), failure, diagnostics }));
|
||||
`,
|
||||
);
|
||||
try {
|
||||
const result = await processes.run(process.execPath, [driver], {
|
||||
timeoutMs: 20_000,
|
||||
env: {
|
||||
...process.env,
|
||||
npm_execpath: entry,
|
||||
OWNER_ROOT: scratch,
|
||||
OWNER_DESCENDANT_ENTRY: descendant,
|
||||
OWNER_DESCENDANT: receipt,
|
||||
OWNER_RELEASE: release,
|
||||
OWNER_READY: ready,
|
||||
OWNER_EVENTS: events,
|
||||
},
|
||||
});
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
expect(result.signal, result.stderr).toBeNull();
|
||||
const parsed = reportSchema.safeParse(JSON.parse(result.stdout));
|
||||
expect(parsed.success, result.stdout).toBe(true);
|
||||
if (!parsed.success) throw parsed.error;
|
||||
const report = parsed.data;
|
||||
expect(report.failure).toEqual({
|
||||
reason: "timeout",
|
||||
stdout: "npm descendant ready\n",
|
||||
stderr: "selected npm diagnostics\n",
|
||||
});
|
||||
expect(report.liveAtRemoval).toEqual([]);
|
||||
expect(report.rootRemoved).toBe(true);
|
||||
const receipts = await readFile(events, "utf8").catch(
|
||||
(cause: unknown) => {
|
||||
if (
|
||||
cause instanceof Error &&
|
||||
"code" in cause &&
|
||||
cause.code === "ENOENT"
|
||||
)
|
||||
return "";
|
||||
throw cause;
|
||||
},
|
||||
);
|
||||
const stopped = receipts
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter((line) => line !== "")
|
||||
.map((line) => stopReceiptSchema.parse(JSON.parse(line)));
|
||||
for (const receipt of stopped) expect(receipt.rootExists).toBe(true);
|
||||
} finally {
|
||||
await releaseDescendant(release, receipt);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,459 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
|
||||
import { expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createPackageVerifierCommands } from "../../../scripts/lib/verify-package-commands.mjs";
|
||||
import { systemProcessOwnershipHost } from "../../../src/process/ProcessOwnershipObservation.js";
|
||||
import { liveProcesses } from "../../../src/process/ProcessOwnershipProcessTree.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
const observed = <Value>(promise: Promise<Value>) =>
|
||||
promise.then(
|
||||
(value) => ({ ok: true as const, value }),
|
||||
(cause: unknown) => ({ ok: false as const, cause }),
|
||||
);
|
||||
|
||||
const commandFixture = async (
|
||||
beforeWorkspaceClose?: (path: string) => void | Promise<void>,
|
||||
) => {
|
||||
const parent = await createTestTempDirectory(
|
||||
"rea-package-command-semantics-",
|
||||
);
|
||||
const path = join(parent, "workspace");
|
||||
await mkdir(path);
|
||||
let closeCalls = 0;
|
||||
const workspace = {
|
||||
path,
|
||||
async close() {
|
||||
closeCalls += 1;
|
||||
await beforeWorkspaceClose?.(path);
|
||||
await rm(path, { recursive: true, force: true });
|
||||
},
|
||||
};
|
||||
const commands = await createPackageVerifierCommands({ workspace });
|
||||
return { parent, workspace, commands, closeCalls: () => closeCalls };
|
||||
};
|
||||
|
||||
it("admits complete stdout and stderr independently of their combined size", async () => {
|
||||
const fixture = await commandFixture();
|
||||
let output;
|
||||
try {
|
||||
output = await fixture.commands.exec(
|
||||
process.execPath,
|
||||
[
|
||||
"-e",
|
||||
'process.stdout.write("o".repeat(48)); process.stderr.write("e".repeat(48));',
|
||||
],
|
||||
{ maxBuffer: 64 },
|
||||
);
|
||||
} finally {
|
||||
await fixture.commands.close();
|
||||
}
|
||||
|
||||
expect(output).toEqual({ stdout: "o".repeat(48), stderr: "e".repeat(48) });
|
||||
expect(existsSync(fixture.workspace.path)).toBe(false);
|
||||
expect(fixture.closeCalls()).toBe(1);
|
||||
});
|
||||
|
||||
it("enforces an explicit aggregate diagnostic budget across both streams", async () => {
|
||||
const fixture = await commandFixture();
|
||||
let outcome;
|
||||
try {
|
||||
outcome = await observed(
|
||||
fixture.commands.exec(
|
||||
process.execPath,
|
||||
[
|
||||
"-e",
|
||||
'process.stdout.write("o".repeat(48)); process.stderr.write("e".repeat(48));',
|
||||
],
|
||||
{ diagnosticBytes: 64 },
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
await fixture.commands.close();
|
||||
}
|
||||
|
||||
expect(outcome).toEqual({
|
||||
ok: false,
|
||||
cause: expect.objectContaining({ reason: "output-limit" }),
|
||||
});
|
||||
expect(existsSync(fixture.workspace.path)).toBe(false);
|
||||
expect(fixture.closeCalls()).toBe(1);
|
||||
});
|
||||
|
||||
it("rejects either overflowing stream even when the command exits with status one", async () => {
|
||||
const fixture = await commandFixture();
|
||||
const outcomes = [];
|
||||
try {
|
||||
for (const stream of ["stdout", "stderr"] as const)
|
||||
outcomes.push(
|
||||
await observed(
|
||||
fixture.commands.runWithStatus(process.execPath, [
|
||||
"-e",
|
||||
`process.${stream}.write("x".repeat(1024 * 1024 + 1)); process.exitCode = 1;`,
|
||||
]),
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
await fixture.commands.close();
|
||||
}
|
||||
|
||||
expect(outcomes).toEqual([
|
||||
{ ok: false, cause: expect.objectContaining({ reason: "output-limit" }) },
|
||||
{ ok: false, cause: expect.objectContaining({ reason: "output-limit" }) },
|
||||
]);
|
||||
expect(existsSync(fixture.workspace.path)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns exact stdout for clean exit one and rejects exit two", async () => {
|
||||
const fixture = await commandFixture();
|
||||
let first;
|
||||
let second;
|
||||
try {
|
||||
first = await fixture.commands.runWithStatus(process.execPath, [
|
||||
"-e",
|
||||
'process.stdout.write("selected stdout\\n "); process.stderr.write("exit one diagnostics"); process.exitCode = 1;',
|
||||
]);
|
||||
second = await observed(
|
||||
fixture.commands.runWithStatus(process.execPath, [
|
||||
"-e",
|
||||
'process.stdout.write("exit two stdout"); process.stderr.write("exit two diagnostics"); process.exitCode = 2;',
|
||||
]),
|
||||
);
|
||||
} finally {
|
||||
await fixture.commands.close();
|
||||
}
|
||||
|
||||
expect(first).toEqual({ stdout: "selected stdout\n ", status: 1 });
|
||||
expect(second).toEqual({
|
||||
ok: false,
|
||||
cause: expect.objectContaining({
|
||||
name: "PackageCommandExitError",
|
||||
code: 2,
|
||||
stdout: "exit two stdout",
|
||||
stderr: "exit two diagnostics",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"rejects a signal exit instead of reporting status one",
|
||||
async () => {
|
||||
const fixture = await commandFixture();
|
||||
let outcome;
|
||||
try {
|
||||
outcome = await observed(
|
||||
fixture.commands.runWithStatus(process.execPath, [
|
||||
"-e",
|
||||
'process.kill(process.pid, "SIGTERM");',
|
||||
]),
|
||||
);
|
||||
} finally {
|
||||
await fixture.commands.close();
|
||||
}
|
||||
|
||||
expect(outcome).toEqual({
|
||||
ok: false,
|
||||
cause: expect.objectContaining({
|
||||
reason: "process",
|
||||
snapshot: expect.objectContaining({ signal: "SIGTERM" }),
|
||||
}),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("uses the exact selected environment and npm entry without rewriting arguments", async () => {
|
||||
const fixture = await commandFixture();
|
||||
const entry = join(fixture.parent, "selected npm entry.mjs");
|
||||
const sentinel = "REA_PACKAGE_COMMAND_AMBIENT_SENTINEL";
|
||||
const previous = process.env[sentinel];
|
||||
const source = `process.stdout.write(JSON.stringify({ args: process.argv.slice(2), cwd: process.cwd(), selected: process.env.REA_PACKAGE_COMMAND_SELECTED, ambient: process.env.${sentinel} ?? null }));`;
|
||||
await writeFile(entry, source);
|
||||
const arguments_ = [
|
||||
"pack",
|
||||
"value with spaces",
|
||||
"--literal=$HOME",
|
||||
"quote'and\"double",
|
||||
];
|
||||
const environment = {
|
||||
...(process.platform === "win32"
|
||||
? { SystemRoot: process.env.SystemRoot }
|
||||
: {}),
|
||||
REA_PACKAGE_COMMAND_SELECTED: "caller-selected",
|
||||
npm_execpath: entry,
|
||||
};
|
||||
let direct;
|
||||
let npm;
|
||||
process.env[sentinel] = "ambient-only";
|
||||
try {
|
||||
direct = await fixture.commands.exec(process.execPath, ["-e", source], {
|
||||
cwd: fixture.workspace.path,
|
||||
env: environment,
|
||||
});
|
||||
npm = await fixture.commands.execNpm(arguments_, {
|
||||
cwd: fixture.workspace.path,
|
||||
env: environment,
|
||||
});
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env[sentinel];
|
||||
else process.env[sentinel] = previous;
|
||||
await fixture.commands.close();
|
||||
}
|
||||
|
||||
expect(JSON.parse(direct!.stdout)).toEqual({
|
||||
args: [],
|
||||
cwd: fixture.workspace.path,
|
||||
selected: "caller-selected",
|
||||
ambient: null,
|
||||
});
|
||||
expect(JSON.parse(npm!.stdout)).toEqual({
|
||||
args: arguments_,
|
||||
cwd: fixture.workspace.path,
|
||||
selected: "caller-selected",
|
||||
ambient: null,
|
||||
});
|
||||
expect(direct?.stderr).toBe("");
|
||||
expect(npm?.stderr).toBe("");
|
||||
});
|
||||
|
||||
it("allows delayed commands with omitted and explicit zero timeouts", async () => {
|
||||
const fixture = await commandFixture();
|
||||
const outputs = [];
|
||||
try {
|
||||
for (const options of [{}, { timeout: 0 }])
|
||||
outputs.push(
|
||||
await fixture.commands.exec(
|
||||
process.execPath,
|
||||
["-e", 'setTimeout(() => process.stdout.write("delayed\\n"), 75);'],
|
||||
options,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
await fixture.commands.close();
|
||||
}
|
||||
|
||||
expect(outputs).toEqual([
|
||||
{ stdout: "delayed\n", stderr: "" },
|
||||
{ stdout: "delayed\n", stderr: "" },
|
||||
]);
|
||||
});
|
||||
|
||||
const waitForMarker = async (path: string): Promise<void> => {
|
||||
const deadline = Date.now() + 5_000;
|
||||
while (!existsSync(path)) {
|
||||
if (Date.now() >= deadline)
|
||||
throw new Error("Authored command did not become ready");
|
||||
await delay(10);
|
||||
}
|
||||
};
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"shares concurrent close and joins the active command before removing its workspace",
|
||||
async () => {
|
||||
let acknowledgedPid: number | undefined;
|
||||
let liveAtWorkspaceClose: readonly number[] | undefined;
|
||||
let rootExistsAtCensus = false;
|
||||
const fixture = await commandFixture(async (path) => {
|
||||
rootExistsAtCensus = existsSync(path);
|
||||
liveAtWorkspaceClose = liveProcesses(
|
||||
await systemProcessOwnershipHost.listProcesses(),
|
||||
)
|
||||
.filter(({ pid }) => pid === acknowledgedPid)
|
||||
.map(({ pid }) => pid);
|
||||
});
|
||||
const ready = join(fixture.parent, "ready");
|
||||
const source = `
|
||||
require("node:fs").writeFileSync(${JSON.stringify(ready)}, JSON.stringify({ pid: process.pid }));
|
||||
setInterval(() => {}, 1000);
|
||||
setTimeout(() => process.exit(0), 10000);
|
||||
`;
|
||||
const running = observed(
|
||||
fixture.commands.exec(process.execPath, ["-e", source]),
|
||||
);
|
||||
let sameClose = false;
|
||||
let outcome;
|
||||
try {
|
||||
await waitForMarker(ready);
|
||||
({ pid: acknowledgedPid } = z
|
||||
.object({ pid: z.number().int().positive() })
|
||||
.parse(JSON.parse(await readFile(ready, "utf8"))));
|
||||
const first = fixture.commands.close();
|
||||
const second = fixture.commands.close();
|
||||
sameClose = first === second;
|
||||
await Promise.all([first, second]);
|
||||
outcome = await running;
|
||||
} finally {
|
||||
await fixture.commands.close();
|
||||
await running;
|
||||
}
|
||||
expect(sameClose).toBe(true);
|
||||
expect(outcome).toEqual({
|
||||
ok: false,
|
||||
cause: expect.objectContaining({ reason: "cancelled" }),
|
||||
});
|
||||
expect(acknowledgedPid).toBeGreaterThan(0);
|
||||
expect(liveAtWorkspaceClose).toEqual([]);
|
||||
expect(rootExistsAtCensus).toBe(true);
|
||||
expect(existsSync(fixture.workspace.path)).toBe(false);
|
||||
expect(fixture.closeCalls()).toBe(1);
|
||||
},
|
||||
);
|
||||
|
||||
it("retains the same adopted capability and workspace after failed close, then releases each once", async () => {
|
||||
const events: string[] = [];
|
||||
const fixture = await commandFixture(() => {
|
||||
events.push("workspace");
|
||||
});
|
||||
const child = join(fixture.workspace.path, "adopted");
|
||||
const forbidden = join(fixture.parent, "forbidden-command");
|
||||
await mkdir(child);
|
||||
const owners: object[] = [];
|
||||
let attempts = 0;
|
||||
const capability = {
|
||||
async close() {
|
||||
owners.push(this);
|
||||
attempts += 1;
|
||||
if (attempts === 1) {
|
||||
events.push("adopted-failed");
|
||||
throw new Error("retry this exact adopted directory capability");
|
||||
}
|
||||
await rm(child, { recursive: true, force: true });
|
||||
events.push("adopted-released");
|
||||
},
|
||||
};
|
||||
fixture.commands.adopt(capability, child);
|
||||
fixture.commands.adopt(capability, child);
|
||||
let failure: unknown;
|
||||
let retained;
|
||||
let blockedBefore;
|
||||
let blockedAfter;
|
||||
try {
|
||||
const closed = await observed(fixture.commands.close());
|
||||
if (!closed.ok) failure = closed.cause;
|
||||
retained = {
|
||||
root: existsSync(fixture.workspace.path),
|
||||
child: existsSync(child),
|
||||
rootCloseCalls: fixture.closeCalls(),
|
||||
};
|
||||
const arguments_ = [
|
||||
"-e",
|
||||
`require("node:fs").writeFileSync(${JSON.stringify(forbidden)}, "unexpected launch");`,
|
||||
];
|
||||
blockedBefore = await observed(
|
||||
fixture.commands.exec(process.execPath, arguments_, {
|
||||
cwd: fixture.parent,
|
||||
}),
|
||||
);
|
||||
await fixture.commands.close();
|
||||
blockedAfter = await observed(
|
||||
fixture.commands.exec(process.execPath, arguments_, {
|
||||
cwd: fixture.parent,
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
await fixture.commands.close();
|
||||
}
|
||||
|
||||
expect(failure).toMatchObject({
|
||||
name: "PackageVerifierCleanupFailure",
|
||||
path: fixture.workspace.path,
|
||||
});
|
||||
if (
|
||||
!(failure instanceof Error) ||
|
||||
!("cleanupOwner" in failure) ||
|
||||
!("workspace" in failure)
|
||||
)
|
||||
throw new Error("Expected retained package cleanup authority");
|
||||
expect(failure.cleanupOwner).toBe(fixture.commands);
|
||||
expect(failure.workspace).toBe(fixture.workspace);
|
||||
expect(retained).toEqual({ root: true, child: true, rootCloseCalls: 0 });
|
||||
expect(owners).toHaveLength(2);
|
||||
expect(owners[0]).toBe(capability);
|
||||
expect(owners[1]).toBe(capability);
|
||||
expect(events).toEqual(["adopted-failed", "adopted-released", "workspace"]);
|
||||
expect(fixture.closeCalls()).toBe(1);
|
||||
expect(existsSync(fixture.workspace.path)).toBe(false);
|
||||
expect(existsSync(forbidden)).toBe(false);
|
||||
for (const outcome of [blockedBefore, blockedAfter])
|
||||
expect(outcome).toEqual({
|
||||
ok: false,
|
||||
cause: expect.objectContaining({
|
||||
message: "Package command owner is closed",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects a new cleanup owner once workspace release has started", async () => {
|
||||
let markReleaseStarted!: () => void;
|
||||
const releaseStarted = new Promise<void>((resolve) => {
|
||||
markReleaseStarted = resolve;
|
||||
});
|
||||
let unblockWorkspaceClose!: () => void;
|
||||
const allowRelease = new Promise<void>((resolve) => {
|
||||
unblockWorkspaceClose = resolve;
|
||||
});
|
||||
const events: string[] = [];
|
||||
const fixture = await commandFixture(async () => {
|
||||
events.push("workspace-release-started");
|
||||
markReleaseStarted();
|
||||
await allowRelease;
|
||||
events.push("workspace-close-unblocked");
|
||||
});
|
||||
// This resource belongs to the caller until adoption succeeds. Keep it
|
||||
// outside the verifier workspace so workspace removal cannot conceal a leak.
|
||||
const path = join(fixture.parent, "caller-owned-directory");
|
||||
await mkdir(path);
|
||||
const releases: object[] = [];
|
||||
const capability = {
|
||||
async close() {
|
||||
releases.push(this);
|
||||
await rm(path, { recursive: true, force: true });
|
||||
events.push("caller-owner-released");
|
||||
},
|
||||
};
|
||||
const closing = fixture.commands.close();
|
||||
let rejection: unknown;
|
||||
let retained;
|
||||
try {
|
||||
await releaseStarted;
|
||||
try {
|
||||
fixture.commands.adopt(capability, path);
|
||||
} catch (cause: unknown) {
|
||||
rejection = cause;
|
||||
}
|
||||
retained = {
|
||||
workspace: existsSync(fixture.workspace.path),
|
||||
callerResource: existsSync(path),
|
||||
callerCloseCalls: releases.length,
|
||||
};
|
||||
} finally {
|
||||
try {
|
||||
await capability.close();
|
||||
} finally {
|
||||
unblockWorkspaceClose();
|
||||
await closing;
|
||||
await fixture.commands.close();
|
||||
}
|
||||
}
|
||||
|
||||
expect(rejection).toBeInstanceOf(Error);
|
||||
expect(retained).toEqual({
|
||||
workspace: true,
|
||||
callerResource: true,
|
||||
callerCloseCalls: 0,
|
||||
});
|
||||
expect(releases).toHaveLength(1);
|
||||
expect(releases[0]).toBe(capability);
|
||||
expect(events).toEqual([
|
||||
"workspace-release-started",
|
||||
"caller-owner-released",
|
||||
"workspace-close-unblocked",
|
||||
]);
|
||||
expect(existsSync(path)).toBe(false);
|
||||
expect(existsSync(fixture.workspace.path)).toBe(false);
|
||||
expect(fixture.closeCalls()).toBe(1);
|
||||
});
|
||||
@@ -20,9 +20,12 @@ const builtModule = (path: string): string =>
|
||||
pathToFileURL(resolve("dist", path)).href;
|
||||
|
||||
const PACKAGE_PACK_FIXTURE = `
|
||||
import assert from "node:assert/strict";
|
||||
import { appendFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
export async function verifyPackagePack({ root, workspace }) {
|
||||
import { packageCommands } from "./lib/verify-package-commands.mjs";
|
||||
export async function verifyPackagePack({ root, workspace, commands }) {
|
||||
assert.equal(commands, packageCommands);
|
||||
appendFileSync(process.env.OWNERSHIP_EVENTS, JSON.stringify({ kind: "pack", root, workspace }) + "\\n");
|
||||
const tarball = join(workspace, "authored.tgz");
|
||||
writeFileSync(tarball, "authored archive boundary");
|
||||
@@ -30,15 +33,13 @@ export async function verifyPackagePack({ root, workspace }) {
|
||||
}
|
||||
`;
|
||||
|
||||
const PACKAGE_INSTALL_FIXTURE = `
|
||||
const NPM_INSTALL_FIXTURE = `
|
||||
import assert from "node:assert/strict";
|
||||
import { appendFileSync } from "node:fs";
|
||||
export async function execNpm(args, { env }) {
|
||||
assert.equal(args[0], "install");
|
||||
assert.ok(args.includes("--ignore-scripts"));
|
||||
appendFileSync(process.env.OWNERSHIP_EVENTS, JSON.stringify({ kind: "install", prefix: args[args.indexOf("--prefix") + 1], home: env.HOME }) + "\\n");
|
||||
return { stdout: "", stderr: "" };
|
||||
}
|
||||
const args = process.argv.slice(2);
|
||||
assert.equal(args[0], "install");
|
||||
assert.ok(args.includes("--ignore-scripts"));
|
||||
appendFileSync(process.env.OWNERSHIP_EVENTS, JSON.stringify({ kind: "install", prefix: args[args.indexOf("--prefix") + 1], home: process.env.HOME }) + "\\n");
|
||||
`;
|
||||
|
||||
const PRIVATE_ROOT_OBSERVER = `
|
||||
@@ -65,15 +66,20 @@ export const PrivateRuntimeRoot = {
|
||||
};
|
||||
`;
|
||||
|
||||
const OWNED_COMMAND_FIXTURE = `
|
||||
const PACKAGE_COMMANDS_FIXTURE = `
|
||||
import { appendFileSync, existsSync } from "node:fs";
|
||||
import { runOwnedCommand as actualRun, OwnedCommandFailure } from ${JSON.stringify(builtModule("process/OwnedCommand.js"))};
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createPackageVerifierCommands as createActualCommands } from ${JSON.stringify(pathToFileURL(resolve("scripts/lib/verify-package-commands.mjs")).href)};
|
||||
import { spawnOwnedProviderProcess } from ${JSON.stringify(builtModule("process/ProviderProcess.js"))};
|
||||
import { cleanupOwnedProcessGroup } from ${JSON.stringify(builtModule("process/ProcessOwnership.js"))};
|
||||
export { OwnedCommandFailure };
|
||||
export const runOwnedCommand = (spawn, limits) => actualRun(spawn, limits, {
|
||||
const nestedVerifier = fileURLToPath(new URL("../verify-wasm-artifact.mjs", import.meta.url));
|
||||
export let packageCommands;
|
||||
export async function createPackageVerifierCommands(options) {
|
||||
packageCommands = await createActualCommands({
|
||||
...options,
|
||||
launcher: async (input) => {
|
||||
const acquired = await spawnOwnedProviderProcess(input);
|
||||
if (!input.arguments.includes(nestedVerifier)) return acquired;
|
||||
return {
|
||||
...acquired,
|
||||
cleanup: async () => {
|
||||
@@ -85,6 +91,8 @@ export const runOwnedCommand = (spawn, limits) => actualRun(spawn, limits, {
|
||||
};
|
||||
},
|
||||
});
|
||||
return packageCommands;
|
||||
}
|
||||
`;
|
||||
|
||||
const NESTED_VERIFIER_FIXTURE = `
|
||||
@@ -121,29 +129,32 @@ const HARNESS_FIXTURE = `
|
||||
import assert from "node:assert/strict";
|
||||
import { existsSync } from "node:fs";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { OwnedCommandFailure, prepareOwnedCommand } from ${JSON.stringify(builtModule("process/OwnedCommand.js"))};
|
||||
import { prepareOwnedCommand } from ${JSON.stringify(builtModule("process/OwnedCommand.js"))};
|
||||
import { systemProcessOwnershipHost } from ${JSON.stringify(builtModule("process/ProcessOwnershipObservation.js"))};
|
||||
import { liveProcesses } from ${JSON.stringify(builtModule("process/ProcessOwnershipProcessTree.js"))};
|
||||
import { withVerifierCleanup } from "./scripts/lib/verifier-cleanup.mjs";
|
||||
import { packageCommands } from "./scripts/lib/verify-package-commands.mjs";
|
||||
await prepareOwnedCommand();
|
||||
let failure;
|
||||
try { await import("./scripts/verify-packaged-wasm-artifact.mjs"); }
|
||||
catch (cause) { failure = cause; }
|
||||
// Capture actual capabilities before any fixture parsing or assertion can fail.
|
||||
const failures = failure instanceof AggregateError ? failure.errors : [failure];
|
||||
const primary = failures.find((error) => error instanceof OwnedCommandFailure);
|
||||
const primary = failures.find((error) => error !== null && typeof error === "object" &&
|
||||
error.reason === "timeout" && error.snapshot !== null && typeof error.snapshot === "object");
|
||||
const cleanup = failures.find((error) => error !== null && typeof error === "object" &&
|
||||
(error.runtimeRoot !== undefined || typeof error.cleanupOwner?.close === "function"));
|
||||
typeof error.cleanupOwner?.close === "function");
|
||||
const compositeOwner = typeof cleanup?.cleanupOwner?.close === "function" ? cleanup.cleanupOwner : undefined;
|
||||
const retainedProcess = primary?.cleanupOwner;
|
||||
const retainedRoot = cleanup?.runtimeRoot;
|
||||
const retainedRoot = cleanup?.workspace;
|
||||
await withVerifierCleanup(async () => {
|
||||
if (process.env.OWNERSHIP_HARNESS_ASSERT === "1") throw new Error("authored harness assertion failure");
|
||||
const events = (await readFile(process.env.OWNERSHIP_EVENTS, "utf8")).trim().split("\\n").map(JSON.parse);
|
||||
const ready = events.find((entry) => entry.kind === "ready");
|
||||
assert.ok(ready, "The actual descendant must acknowledge startup before the command deadline");
|
||||
assert.ok(primary instanceof OwnedCommandFailure);
|
||||
assert.ok(primary, "The owned nested command must fail with its timeout snapshot");
|
||||
assert.equal(primary.reason, "timeout");
|
||||
assert.ok(primary.snapshot.stdout.text.includes("nested ready"));
|
||||
const pids = [ready.leader, ready.descendant];
|
||||
const remaining = async () => liveProcesses(await systemProcessOwnershipHost.listProcesses()).filter(({ pid }) => pids.includes(pid)).map(({ pid }) => pid);
|
||||
const result = { reason: primary.reason, runtime: ready.runtime, rootExists: existsSync(ready.runtime), livePids: await remaining(), pids, runId: ready.runId, descendantRunId: ready.descendantRunId };
|
||||
@@ -151,9 +162,10 @@ if (process.env.OWNERSHIP_DENY === "1") {
|
||||
assert.ok(failure instanceof AggregateError);
|
||||
assert.equal(failure.cause, primary);
|
||||
assert.equal(failure.errors[1], cleanup);
|
||||
assert.equal(cleanup.name, "PackagedWasmCleanupFailure");
|
||||
assert.equal(cleanup.runtimeRoot.path, ready.runtime);
|
||||
assert.equal(cleanup.name, "PackageVerifierCleanupFailure");
|
||||
assert.equal(cleanup.workspace.path, ready.runtime);
|
||||
assert.equal(typeof cleanup.cleanupOwner.close, "function");
|
||||
assert.equal(cleanup.cleanupOwner, packageCommands);
|
||||
assert.ok(primary.cleanupOwner);
|
||||
result.compositeOwner = true;
|
||||
}
|
||||
@@ -207,7 +219,7 @@ const expectHarnessFailureCleanup = async (
|
||||
.map((line) => JSON.parse(line));
|
||||
const ready = events.find(({ kind }) => kind === "ready");
|
||||
expect(ready).toBeDefined();
|
||||
expect(ready.runId).toMatch(/^rea-packaged-wabt-/);
|
||||
expect(ready.runId).toMatch(/^rea-package-command-/);
|
||||
expect(ready.descendantRunId).toBe(ready.runId);
|
||||
expectRootCleanupAfterProcessExit(events, ready.runtime, [
|
||||
ready.leader,
|
||||
@@ -251,10 +263,10 @@ it.skipIf(process.platform === "win32").each([
|
||||
),
|
||||
workspace.write("scripts/verify-package-pack.mjs", PACKAGE_PACK_FIXTURE),
|
||||
workspace.write(
|
||||
"scripts/lib/verify-package-core.mjs",
|
||||
PACKAGE_INSTALL_FIXTURE,
|
||||
"scripts/lib/verify-package-commands.mjs",
|
||||
PACKAGE_COMMANDS_FIXTURE,
|
||||
),
|
||||
workspace.write("dist/process/OwnedCommand.js", OWNED_COMMAND_FIXTURE),
|
||||
workspace.write("scripts/npm-fixture.mjs", NPM_INSTALL_FIXTURE),
|
||||
workspace.write(
|
||||
"scripts/verify-wasm-artifact.mjs",
|
||||
NESTED_VERIFIER_FIXTURE,
|
||||
@@ -279,6 +291,7 @@ it.skipIf(process.platform === "win32").each([
|
||||
TMPDIR: runtimeParent,
|
||||
TMP: runtimeParent,
|
||||
TEMP: runtimeParent,
|
||||
npm_execpath: workspace.path("scripts/npm-fixture.mjs"),
|
||||
OWNERSHIP_EVENTS: events,
|
||||
OWNERSHIP_REPORT: report,
|
||||
OWNERSHIP_ALLOW: allow,
|
||||
@@ -303,7 +316,7 @@ it.skipIf(process.platform === "win32").each([
|
||||
const observed = JSON.parse(await readFile(report, "utf8"));
|
||||
expect(observed.reason).toBe("timeout");
|
||||
expect(observed.pids).toHaveLength(2);
|
||||
expect(observed.runId).toMatch(/^rea-packaged-wabt-/);
|
||||
expect(observed.runId).toMatch(/^rea-package-command-/);
|
||||
expect(observed.descendantRunId).toBe(observed.runId);
|
||||
expect(observed.rootExists).toBe(denied);
|
||||
expect(observed.livePids.sort()).toEqual(
|
||||
|
||||
@@ -9,16 +9,24 @@ import {
|
||||
} from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { expect, it } from "vitest";
|
||||
import { expect, it, onTestFinished } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
createPackageVerifierCommands,
|
||||
type PackageVerifierCommands,
|
||||
} from "../../../scripts/lib/verify-package-commands.mjs";
|
||||
import { withVerifierCleanup } from "../../../scripts/lib/verifier-cleanup.mjs";
|
||||
import { verifyPackedBridge } from "../../../scripts/verify-packed-bridge.mjs";
|
||||
import {
|
||||
PrivateRuntimeRoot,
|
||||
privateRuntimeRootCapability,
|
||||
} from "../../../src/process/PrivateRuntimeRoot.js";
|
||||
import { GHIDRA_BRIDGE_SOURCES } from "../../../src/ghidra/GhidraBridgeSources.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import {
|
||||
createTestWorkspace,
|
||||
removeTestWorkspace,
|
||||
} from "../../support/workspace/workspaceFixture.js";
|
||||
|
||||
const exec = promisify(execFile);
|
||||
const packedFiles = [
|
||||
@@ -33,7 +41,15 @@ const validSource =
|
||||
'# Unicode source: 雨\nraise RuntimeError("Packed source must never execute")\n';
|
||||
|
||||
async function fixture(source: string = validSource, omitHopper = false) {
|
||||
const directory = await createTestTempDirectory("rea-packed-bridge-");
|
||||
const scratch = await createTestWorkspace("rea-packed-bridge-");
|
||||
const directory = scratch.root;
|
||||
let commands: PackageVerifierCommands | undefined;
|
||||
onTestFinished(() =>
|
||||
commands === undefined ? removeTestWorkspace(directory) : commands.close(),
|
||||
);
|
||||
commands = await createPackageVerifierCommands({
|
||||
workspace: { path: directory, close: () => removeTestWorkspace(directory) },
|
||||
});
|
||||
const staging = join(directory, "source");
|
||||
const workspace = join(directory, "workspace");
|
||||
await mkdir(workspace);
|
||||
@@ -45,7 +61,7 @@ async function fixture(source: string = validSource, omitHopper = false) {
|
||||
}
|
||||
const tarball = join(directory, "packed bridges.tgz");
|
||||
await exec("tar", ["-czf", tarball, "-C", staging, "package"]);
|
||||
return { workspace, tarball, packedFiles };
|
||||
return { workspace, tarball, packedFiles, commands };
|
||||
}
|
||||
|
||||
it("parses packed Python without executing it or extracting archive members", async () => {
|
||||
@@ -88,12 +104,20 @@ it.each(["cwd", "pythonpath", "sitecustomize"] as const)(
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
`import { verifyPackedBridge } from ${JSON.stringify(verifier)};
|
||||
import { createPackageVerifierCommands } from ${JSON.stringify(new URL("../../../scripts/lib/verify-package-commands.mjs", import.meta.url).href)};
|
||||
import { withVerifierCleanup } from ${JSON.stringify(new URL("../../../scripts/lib/verifier-cleanup.mjs", import.meta.url).href)};
|
||||
import { PrivateRuntimeRoot } from ${JSON.stringify(new URL("../../../dist/process/PrivateRuntimeRoot.js", import.meta.url).href)};
|
||||
const root = await PrivateRuntimeRoot.create({ prefix: "rea-packed-python-command-" });
|
||||
let commands;
|
||||
await withVerifierCleanup(async () => {
|
||||
commands = await createPackageVerifierCommands({ workspace: root });
|
||||
try {
|
||||
await verifyPackedBridge(JSON.parse(process.env.REA_PACKED_VERIFY_INPUT));
|
||||
await verifyPackedBridge({ ...JSON.parse(process.env.REA_PACKED_VERIFY_INPUT), commands });
|
||||
console.log(JSON.stringify({ accepted: true }));
|
||||
} catch (error) {
|
||||
console.log(JSON.stringify({ accepted: false, stderr: String(error.stderr) }));
|
||||
}`,
|
||||
}
|
||||
}, [() => commands === undefined ? root.close() : commands.close()]);`,
|
||||
],
|
||||
{
|
||||
cwd: kind === "cwd" ? modules : input.workspace,
|
||||
@@ -105,7 +129,10 @@ try {
|
||||
),
|
||||
PYTHONPATH: kind === "cwd" ? "" : modules,
|
||||
REA_PACKED_IMPORT_MARKER: marker,
|
||||
REA_PACKED_VERIFY_INPUT: JSON.stringify(input),
|
||||
REA_PACKED_VERIFY_INPUT: JSON.stringify({
|
||||
tarball: input.tarball,
|
||||
packedFiles: input.packedFiles,
|
||||
}),
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -160,12 +187,14 @@ it.each(["valid", "invalid"] as const)(
|
||||
const root = await PrivateRuntimeRoot.create({
|
||||
prefix: "rea-packed-bridge-lease-",
|
||||
});
|
||||
try {
|
||||
let commands: PackageVerifierCommands | undefined;
|
||||
await withVerifierCleanup(async () => {
|
||||
commands = await createPackageVerifierCommands({ workspace: root });
|
||||
const pack = join(root.path, "pack");
|
||||
await mkdir(pack);
|
||||
const tarball = join(pack, "packed bridges.tgz");
|
||||
await copyFile(input.tarball, tarball);
|
||||
const parsed = verifyPackedBridge({ tarball, packedFiles });
|
||||
const parsed = verifyPackedBridge({ tarball, packedFiles, commands });
|
||||
if (kind === "valid") await parsed;
|
||||
else
|
||||
await expect(parsed).rejects.toMatchObject({
|
||||
@@ -173,9 +202,7 @@ it.each(["valid", "invalid"] as const)(
|
||||
});
|
||||
expect(await readdir(root.path)).toEqual(["pack"]);
|
||||
expect(await readFile(tarball)).toEqual(await readFile(input.tarball));
|
||||
} finally {
|
||||
await root.close();
|
||||
}
|
||||
}, [() => (commands === undefined ? root.close() : commands.close())]);
|
||||
await expect(access(root.path)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
},
|
||||
);
|
||||
|
||||
@@ -578,7 +578,7 @@ it("forwards selected and omitted deadlines through the production command runne
|
||||
const directory = await mkdtemp(join(tmpdir(), "rea-owned-timeout-"));
|
||||
const path = join(directory, "fixture.bin");
|
||||
await writeFile(path, "before");
|
||||
const timeouts: number[] = [];
|
||||
const timeouts: (number | null)[] = [];
|
||||
const service = new ReverseEngineeringService({
|
||||
runOwned: async (_spawn, limits) => {
|
||||
timeouts.push(limits.timeoutMs);
|
||||
|
||||
@@ -54,8 +54,13 @@ const verifyMacOS = async (
|
||||
cli: "fixture-cli",
|
||||
environment: {},
|
||||
platform: "darwin",
|
||||
runCommand: async (_cli, arguments_) =>
|
||||
JSON.stringify(outputs[arguments_[0]!]) ?? "",
|
||||
commands: {
|
||||
run: async (_cli, arguments_) =>
|
||||
JSON.stringify(outputs[arguments_[0]!]) ?? "",
|
||||
runWithStatus: async () => {
|
||||
throw new Error("Unexpected status command in macOS fixture");
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user