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`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user