import { execFile } from
"node:child_process" ;
import { randomUUID } from
"node:crypto" ;
import fs from
"node:fs" ;
import { access, appendFile, mkdir, writeFile } from
"node:fs/promises" ;
import path from
"node:path" ;
import { resolvePreferredOpenClawTmpDir } from
"openclaw/plugin-sdk/temp-path" ;
import type { QaProviderMode } from
"./model-selection.js" ;
import { resolveQaForwardedLiveEnv, resolveQaLiveProviderConfigPath } from
"./providers/env.js" ;
import { DEFAULT_QA_LIVE_PROVIDER_MODE, getQaProvider } from
"./providers/index.js" ;
const MULTIPASS_MOUNTED_REPO_PATH =
"/workspace/openclaw-host" ;
const MULTIPASS_GUEST_REPO_PATH =
"/workspace/openclaw" ;
const MULTIPASS_GUEST_CODEX_HOME_PATH =
"/workspace/openclaw-codex-home" ;
const MULTIPASS_GUEST_PACKAGES = [
"build-essential" ,
"ca-certificates" ,
"curl" ,
"pkg-config" ,
"python3" ,
"rsync" ,
"xz-utils" ,
] as
const ;
const MULTIPASS_REPO_SYNC_EXCLUDES = [
".git" ,
"node_modules" ,
".artifacts" ,
".tmp" ,
".turbo" ,
"coverage" ,
"*.heapsnapshot" ,
] as
const ;
const MULTIPASS_EXEC_MAX_BUFFER =
64 *
1024 *
1024 ;
const MULTIPASS_GUEST_RUN_TIMEOUT_MS =
60 *
60 *
1000 ;
export
const qaMultipassDefaultResources = {
image:
"lts" ,
cpus:
2 ,
memory:
"4G" ,
disk:
"24G" ,
} as
const ;
type ExecResult = {
stdout: string;
stderr: string;
};
type ExecFileError = Error & {
code?: string;
};
type ExecFileOptions = {
timeoutMs?: number;
};
export type QaMultipassPlan = {
repoRoot: string;
outputDir: string;
reportPath: string;
summaryPath: string;
hostLogPath: string;
hostBootstrapLogPath: string;
hostGuestScriptPath: string;
vmName: string;
image: string;
cpus: number;
memory: string;
disk: string;
pnpmVersion: string;
transportId: string;
providerMode: QaProviderMode;
primaryModel?: string;
alternateModel?: string;
fastMode?:
boolean ;
thinkingDefault?: string;
scenarioIds: string[];
forwardedEnv: Record<string, string>;
hostCodexHomePath?: string;
guestCodexHomePath?: string;
hostLiveProviderConfigPath?: string;
guestLiveProviderConfigPath?: string;
guestMountedRepoPath: string;
guestRepoPath: string;
guestOutputDir: string;
guestScriptPath: string;
guestBootstrapLogPath: string;
qaCommand: string[];
};
export type QaMultipassRunResult = {
outputDir: string;
reportPath: string;
summaryPath: string;
hostLogPath: string;
bootstrapLogPath: string;
guestScriptPath: string;
vmName: string;
scenarioIds: string[];
};
type RenderGuestScriptOptions = {
redactSecrets?:
boolean ;
};
function shellQuote(value: string) {
return `
'${value.replaceAll("' ", `'" '"' `)}
'`;
}
function createOutputStamp() {
return new Date().toISOString().replaceAll(
":" ,
"" ).replaceAll(
"." ,
"" ).replace(
"T" ,
"-" );
}
function createVmSuffix() {
return `${Date.now().toString(
36 )}-${randomUUID().slice(
0 ,
8 )}`;
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function execFileAsync(file: string, args: string[], options: ExecFileOptions = {}) {
return new Promise<ExecResult>((resolve, reject) => {
execFile(
file,
args,
{
encoding:
"utf8" ,
maxBuffer: MULTIPASS_EXEC_MAX_BUFFER,
timeout: options.timeoutMs,
},
(error, stdout, stderr) => {
if (error) {
const message = stderr.trim() || stdout.trim() || error.message;
const wrappedError =
new Error(message, { cause: error }) as ExecFileError;
wrappedError.code = (error as NodeJS.ErrnoException).code;
reject(wrappedError);
return ;
}
resolve({ stdout, stderr });
},
);
});
}
function resolveRealPath(value: string) {
return fs.realpathSync.
native ?.(value) ?? fs.realpathSync(value);
}
function resolveExistingPath(value: string) {
let currentPath = value;
while (!fs.existsSync(currentPath)) {
const parentPath = path.dirname(currentPath);
if (parentPath === currentPath) {
throw new Error(`unable to resolve existing path
for ${value}`);
}
currentPath = parentPath;
}
return currentPath;
}
function isPathInside(parentPath: string, childPath: string) {
const relativePath = path.relative(parentPath, childPath);
return !relativePath.startsWith(
".." ) && !path.isAbsolute(relativePath);
}
function validatePnpmVersion(version: string) {
if (!/^[
0 -
9 A-Za-z.+_-]+$/u.test(version)) {
throw new Error(`unsupported pnpm version in packageManager: ${version}`);
}
return version;
}
function resolveMountedOutputPath(repoRoot: string, hostPath: string) {
const relativePath = path.relative(repoRoot, hostPath);
if (relativePath.startsWith(
".." ) || path.isAbsolute(relativePath) || relativePath.le
ngth === 0 ) {
throw new Error(
`qa suite --runner multipass requires --output-dir to stay under the repo root (${repoRoot}), got ${hostPath}.`,
);
}
const realRepoRoot = resolveRealPath(repoRoot);
const existingHostPath = resolveExistingPath(hostPath);
const realExistingHostPath = resolveRealPath(existingHostPath);
if (!isPathInside(realRepoRoot, realExistingHostPath) && realExistingHostPath !== realRepoRoot) {
throw new Error(
`qa suite --runner multipass requires --output-dir to stay under the repo root (${repoRoot}), got ${hostPath}.`,
);
}
return path.posix.join(MULTIPASS_MOUNTED_REPO_PATH, ...relativePath.split(path.sep));
}
function resolvePnpmVersion(repoRoot: string) {
const packageJsonPath = path.join(repoRoot, "package.json" );
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8" )) as {
packageManager?: string;
};
const packageManager = packageJson.packageManager ?? "" ;
const match = /^pnpm@(.+)$/.exec(packageManager);
if (!match?.[1 ]) {
throw new Error(`unable to resolve pnpm version from packageManager in ${packageJsonPath}`);
}
return match[1 ];
}
function resolveMultipassInstallHint() {
if (process.platform === "darwin" ) {
return "brew install --cask multipass" ;
}
if (process.platform === "win32" ) {
return "winget install Canonical.Multipass" ;
}
if (process.platform === "linux" ) {
return "sudo snap install multipass" ;
}
return "https://multipass.run/install ";
}
function createQaMultipassOutputDir(repoRoot: string) {
return path.join(repoRoot, ".artifacts" , "qa-e2e" , `multipass-${createOutputStamp()}`);
}
function resolveGuestMountedPath(repoRoot: string, hostPath: string) {
return resolveMountedOutputPath(repoRoot, hostPath);
}
function appendScenarioArgs(command: string[], scenarioIds: string[]) {
for (const scenarioId of scenarioIds) {
command.push("--scenario" , scenarioId);
}
return command;
}
export function createQaMultipassPlan(params: {
repoRoot: string;
outputDir?: string;
transportId?: string;
providerMode?: QaProviderMode;
primaryModel?: string;
alternateModel?: string;
fastMode?: boolean ;
thinkingDefault?: string;
allowFailures?: boolean ;
scenarioIds?: string[];
concurrency?: number;
image?: string;
cpus?: number;
memory?: string;
disk?: string;
}) {
const outputDir = params.outputDir ?? createQaMultipassOutputDir(params.repoRoot);
const scenarioIds = [...new Set(params.scenarioIds ?? [])];
const transportId = params.transportId?.trim() || "qa-channel" ;
const providerMode = params.providerMode ?? DEFAULT_QA_LIVE_PROVIDER_MODE;
const provider = getQaProvider(providerMode);
const forwardedEnv = provider.appliesLiveEnvAliases ? resolveQaForwardedLiveEnv() : {};
const hostCodexHomePath = forwardedEnv.CODEX_HOME;
const liveProviderConfig = provider.usesModelProviderPlugins
? resolveQaLiveProviderConfigPath()
: undefined;
const hostLiveProviderConfigPath =
liveProviderConfig && fs.existsSync(liveProviderConfig.path)
? liveProviderConfig.path
: undefined;
const vmName = `openclaw-qa-${createVmSuffix()}`;
const guestOutputDir = resolveGuestMountedPath(params.repoRoot, outputDir);
const qaCommand = appendScenarioArgs(
[
"pnpm" ,
"openclaw" ,
"qa" ,
"suite" ,
"--transport" ,
transportId,
"--provider-mode" ,
providerMode,
"--output-dir" ,
guestOutputDir,
...(params.primaryModel ? ["--model" , params.primaryModel] : []),
...(params.alternateModel ? ["--alt-model" , params.alternateModel] : []),
...(params.fastMode ? ["--fast" ] : []),
...(params.thinkingDefault ? ["--thinking" , params.thinkingDefault] : []),
...(params.allowFailures ? ["--allow-failures" ] : []),
...(params.concurrency ? ["--concurrency" , String(params.concurrency)] : []),
],
scenarioIds,
);
return {
repoRoot: params.repoRoot,
outputDir,
reportPath: path.join(outputDir, "qa-suite-report.md" ),
summaryPath: path.join(outputDir, "qa-suite-summary.json" ),
hostLogPath: path.join(outputDir, "multipass-host.log" ),
hostBootstrapLogPath: path.join(outputDir, "multipass-guest-bootstrap.log" ),
hostGuestScriptPath: path.join(outputDir, "multipass-guest-run.sh" ),
vmName,
image: params.image ?? qaMultipassDefaultResources.image,
cpus: params.cpus ?? qaMultipassDefaultResources.cpus,
memory: params.memory ?? qaMultipassDefaultResources.memory,
disk: params.disk ?? qaMultipassDefaultResources.disk,
pnpmVersion: validatePnpmVersion(resolvePnpmVersion(params.repoRoot)),
transportId,
providerMode,
primaryModel: params.primaryModel,
alternateModel: params.alternateModel,
fastMode: params.fastMode,
thinkingDefault: params.thinkingDefault,
scenarioIds,
forwardedEnv,
hostCodexHomePath,
guestCodexHomePath: hostCodexHomePath ? MULTIPASS_GUEST_CODEX_HOME_PATH : undefined,
hostLiveProviderConfigPath,
guestLiveProviderConfigPath: hostLiveProviderConfigPath
? `/tmp/${vmName}-live-provider-config.json`
: undefined,
guestMountedRepoPath: MULTIPASS_MOUNTED_REPO_PATH,
guestRepoPath: MULTIPASS_GUEST_REPO_PATH,
guestOutputDir,
guestScriptPath: `/tmp/${vmName}-qa-suite.sh`,
guestBootstrapLogPath: `/tmp/${vmName}-bootstrap.log`,
qaCommand,
} satisfies QaMultipassPlan;
}
export function renderQaMultipassGuestScript(
plan: QaMultipassPlan,
options: RenderGuestScriptOptions = {},
) {
const redactSecrets = options.redactSecrets ?? false ;
const rsyncCommand = [
"rsync -a --delete" ,
...MULTIPASS_REPO_SYNC_EXCLUDES.flatMap((value) => ["--exclude" , shellQuote(value)]),
shellQuote(`${plan.guestMountedRepoPath}/`),
shellQuote(`${plan.guestRepoPath}/`),
].join(" " );
const qaCommand = [
...Object.entries(plan.forwardedEnv)
.filter(
([key]) =>
key !== "CODEX_HOME" &&
key !== "OPENCLAW_CONFIG_PATH" &&
key !== "OPENCLAW_QA_LIVE_PROVIDER_CONFIG_PATH" ,
)
.map(([key, value]) => `${key}=${shellQuote(redactSecrets ? "<redacted>" : value)}`),
...(plan.guestCodexHomePath ? [`CODEX_HOME=${shellQuote(plan.guestCodexHomePath)}`] : []),
...(plan.guestLiveProviderConfigPath
? [
`OPENCLAW_CONFIG_PATH=${shellQuote(plan.guestLiveProviderConfigPath)}`,
`OPENCLAW_QA_LIVE_PROVIDER_CONFIG_PATH=${shellQuote(plan.guestLiveProviderConfigPath)}`,
]
: []),
plan.qaCommand.map(shellQuote).join(" " ),
].join(" " );
const lines = [
"#!/usr/bin/env bash" ,
"set -euo pipefail" ,
"trap 'status=$?; echo \" guest failure (exit ${status})\" >&2; exit ${status}' ERR" ,
"" ,
"export DEBIAN_FRONTEND=noninteractive" ,
`BOOTSTRAP_LOG=${shellQuote(plan.guestBootstrapLogPath)}`,
': > "$BOOTSTRAP_LOG"' ,
"" ,
"ensure_guest_packages() {" ,
' sudo -E apt-get update >>"$BOOTSTRAP_LOG" 2>&1' ,
" sudo -E apt-get install -y \\" ,
...MULTIPASS_GUEST_PACKAGES.map((value, index) =>
index === MULTIPASS_GUEST_PACKAGES.length - 1
? ` ${value} >>"$BOOTSTRAP_LOG" 2 >&1 `
: ` ${value} \\`,
),
"}" ,
"" ,
"ensure_node() {" ,
" if command -v node >/dev/null; then" ,
" local node_major" ,
' node_major="$(node -p \' process.versions.node.split("." )[0 ]\' 2>/dev/null || echo 0)"' ,
' if [ "${node_major}" -ge 22 ]; then' ,
" return 0" ,
" fi" ,
" fi" ,
" local node_arch" ,
' case "$(uname -m)" in' ,
' x86_64) node_arch="x64" ;;' ,
' aarch64|arm64) node_arch="arm64" ;;' ,
' *) echo "unsupported guest architecture for node bootstrap: $(uname -m)" >&2; return 1 ;;' ,
" esac" ,
" local node_tmp_dir tarball_name extract_dir base_url" ,
' node_tmp_dir="$(mktemp -d)"' ,
" trap 'rm -rf \" ${node_tmp_dir}\"' RETURN" ,
' base_url="https://nodejs.org/dist/latest-v22.x "',
' curl -fsSL "${base_url}/SHASUMS256.txt" -o "${node_tmp_dir}/SHASUMS256.txt" >>"$BOOTSTRAP_LOG" 2>&1' ,
' tarball_name="$(awk \' /linux-\'"${node_arch}"\' \\.tar\\.xz$/ { print $2 ; exit }\' "${node_tmp_dir}/SHASUMS256.txt")"' ,
' [ -n "${tarball_name}" ] || { echo "unable to resolve node tarball for ${node_arch}" >&2; return 1; }' ,
' curl -fsSL "${base_url}/${tarball_name}" -o "${node_tmp_dir}/${tarball_name}" >>"$BOOTSTRAP_LOG" 2>&1' ,
' (cd "${node_tmp_dir}" && grep " ${tarball_name}$" SHASUMS256.txt | sha256sum -c -) >>"$BOOTSTRAP_LOG" 2>&1' ,
' extract_dir="${tarball_name%.tar.xz}"' ,
' sudo mkdir -p /usr/local/lib/nodejs >>"$BOOTSTRAP_LOG" 2>&1' ,
' sudo rm -rf "/usr/local/lib/nodejs/${extract_dir}" >>"$BOOTSTRAP_LOG" 2>&1' ,
' sudo tar -xJf "${node_tmp_dir}/${tarball_name}" -C /usr/local/lib/nodejs >>"$BOOTSTRAP_LOG" 2>&1' ,
' sudo ln -sf "/usr/local/lib/nodejs/${extract_dir}/bin/node" /usr/local/bin/node >>"$BOOTSTRAP_LOG" 2>&1' ,
' sudo ln -sf "/usr/local/lib/nodejs/${extract_dir}/bin/npm" /usr/local/bin/npm >>"$BOOTSTRAP_LOG" 2>&1' ,
' sudo ln -sf "/usr/local/lib/nodejs/${extract_dir}/bin/npx" /usr/local/bin/npx >>"$BOOTSTRAP_LOG" 2>&1' ,
' sudo ln -sf "/usr/local/lib/nodejs/${extract_dir}/bin/corepack" /usr/local/bin/corepack >>"$BOOTSTRAP_LOG" 2>&1' ,
"}" ,
"" ,
"ensure_pnpm() {" ,
' sudo env PATH="/usr/local/bin:/usr/bin:/bin" corepack enable >>"$BOOTSTRAP_LOG" 2>&1' ,
` sudo env PATH="/usr/local/bin:/usr/bin:/bin" corepack prepare ${shellQuote(`pnpm@${plan.pnpmVersion}`)} --activate >>"$BOOTSTRAP_LOG" 2 >&1 `,
"}" ,
"" ,
'command -v sudo >/dev/null || { echo "missing sudo in guest" >&2; exit 1; }' ,
"ensure_guest_packages" ,
"ensure_node" ,
"ensure_pnpm" ,
'command -v node >/dev/null || { echo "missing node after guest bootstrap" >&2; exit 1; }' ,
'command -v pnpm >/dev/null || { echo "missing pnpm after guest bootstrap" >&2; exit 1; }' ,
'command -v rsync >/dev/null || { echo "missing rsync after guest bootstrap" >&2; exit 1; }' ,
"" ,
`mkdir -p ${shellQuote(path.posix.dirname(plan.guestRepoPath))}`,
`rm -rf ${shellQuote(plan.guestRepoPath)}`,
`mkdir -p ${shellQuote(plan.guestRepoPath)}`,
`mkdir -p ${shellQuote(plan.guestOutputDir)}`,
rsyncCommand,
`cd ${shellQuote(plan.guestRepoPath)}`,
'pnpm install --frozen-lockfile >>"$BOOTSTRAP_LOG" 2>&1' ,
'pnpm build >>"$BOOTSTRAP_LOG" 2>&1' ,
qaCommand,
"" ,
];
return lines.join("\n" );
}
async function appendMultipassLog(logPath: string, message: string) {
await appendFile(logPath, message, "utf8" );
}
async function runMultipassCommand(logPath: string, args: string[], options: ExecFileOptions = {}) {
await appendMultipassLog(logPath, `$ ${["multipass" , ...args].join(" " )}\n`);
const result = await execFileAsync("multipass" , args, options);
if (result.stdout.trim()) {
await appendMultipassLog(logPath, `${result.stdout.trim()}\n`);
}
if (result.stderr.trim()) {
await appendMultipassLog(logPath, `${result.stderr.trim()}\n`);
}
await appendMultipassLog(logPath, "\n" );
return result;
}
async function waitForGuestReady(logPath: string, vmName: string) {
let lastError: unknown;
for (let attempt = 1 ; attempt <= 12 ; attempt += 1 ) {
try {
await runMultipassCommand(logPath, ["exec" , vmName, "--" , "bash" , "-lc" , "echo guest-ready" ]);
return ;
} catch (error) {
lastError = error;
await appendMultipassLog(
logPath,
`guest-ready retry ${attempt}/12 : ${error instanceof Error ? error.message : String(error)}\n\n`,
);
if (attempt < 12 ) {
await sleep(2 _000 );
}
}
}
throw lastError instanceof Error ? lastError : new Error(String(lastError));
}
async function mountRepo(logPath: string, repoRoot: string, vmName: string) {
let lastError: unknown;
for (let attempt = 1 ; attempt <= 5 ; attempt += 1 ) {
try {
await runMultipassCommand(logPath, [
"mount" ,
repoRoot,
`${vmName}:${MULTIPASS_MOUNTED_REPO_PATH}`,
]);
return ;
} catch (error) {
lastError = error;
await appendMultipassLog(
logPath,
`mount retry ${attempt}/5 : ${error instanceof Error ? error.message : String(error)}\n\n`,
);
if (attempt < 5 ) {
await sleep(2 _000 );
}
}
}
throw lastError instanceof Error ? lastError : new Error(String(lastError));
}
async function mountCodexHome(logPath: string, hostCodexHomePath: string, vmName: string) {
let lastError: unknown;
for (let attempt = 1 ; attempt <= 5 ; attempt += 1 ) {
try {
await runMultipassCommand(logPath, [
"mount" ,
hostCodexHomePath,
`${vmName}:${MULTIPASS_GUEST_CODEX_HOME_PATH}`,
]);
return ;
} catch (error) {
lastError = error;
await appendMultipassLog(
logPath,
`codex-home mount retry ${attempt}/5 : ${error instanceof Error ? error.message : String(error)}\n\n`,
);
if (attempt < 5 ) {
await sleep(2 _000 );
}
}
}
throw lastError instanceof Error ? lastError : new Error(String(lastError));
}
async function transferLiveProviderConfig(plan: QaMultipassPlan) {
if (!plan.hostLiveProviderConfigPath || !plan.guestLiveProviderConfigPath) {
return ;
}
await runMultipassCommand(plan.hostLogPath, [
"transfer" ,
plan.hostLiveProviderConfigPath,
`${plan.vmName}:${plan.guestLiveProviderConfigPath}`,
]);
}
async function tryCopyGuestBootstrapLog(plan: QaMultipassPlan) {
try {
await runMultipassCommand(plan.hostLogPath, [
"transfer" ,
`${plan.vmName}:${plan.guestBootstrapLogPath}`,
plan.hostBootstrapLogPath,
]);
} catch (error) {
await appendMultipassLog(
plan.hostLogPath,
`bootstrap log transfer skipped: ${error instanceof Error ? error.message : String(error)}\n\n`,
);
}
}
export async function runQaMultipass(params: {
repoRoot: string;
outputDir?: string;
transportId?: string;
providerMode?: QaProviderMode;
primaryModel?: string;
alternateModel?: string;
fastMode?: boolean ;
allowFailures?: boolean ;
scenarioIds?: string[];
concurrency?: number;
image?: string;
cpus?: number;
memory?: string;
disk?: string;
}) {
const plan = createQaMultipassPlan(params);
await mkdir(plan.outputDir, { recursive: true });
await writeFile(
plan.hostLogPath,
`# OpenClaw QA Multipass host log\nvmName=${plan.vmName}\noutputDir=${plan.outputDir}\n\n`,
"utf8" ,
);
await writeFile(
plan.hostGuestScriptPath,
renderQaMultipassGuestScript(plan, { redactSecrets: true }),
{
encoding: "utf8" ,
mode: 0 o600,
},
);
try {
await execFileAsync("multipass" , ["version" ]);
} catch (error) {
if ((error as ExecFileError).code !== "ENOENT" ) {
throw new Error(
`Unable to verify Multipass availability: ${error instanceof Error ? error.message : String(error)}.`,
{ cause: error },
);
}
throw new Error(
`Multipass is not installed on this host. Install it with '${resolveMultipassInstallHint()}' , then rerun 'pnpm openclaw qa suite --runner multipass' .`,
{ cause: error },
);
}
const hostTransferDirPath = await fs.promises.mkdtemp(
path.join(resolvePreferredOpenClawTmpDir(), `${plan.vmName}-qa-suite-`),
);
const hostTransferScriptPath = path.join(hostTransferDirPath, "guest-run.sh" );
await writeFile(hostTransferScriptPath, renderQaMultipassGuestScript(plan), {
encoding: "utf8" ,
mode: 0 o600,
});
let launched = false ;
try {
await runMultipassCommand(plan.hostLogPath, [
"launch" ,
"--name" ,
plan.vmName,
"--cpus" ,
String(plan.cpus),
"--memory" ,
plan.memory,
"--disk" ,
plan.disk,
plan.image,
]);
launched = true ;
await waitForGuestReady(plan.hostLogPath, plan.vmName);
await mountRepo(plan.hostLogPath, plan.repoRoot, plan.vmName);
if (plan.hostCodexHomePath) {
await mountCodexHome(plan.hostLogPath, plan.hostCodexHomePath, plan.vmName);
}
await transferLiveProviderConfig(plan);
await runMultipassCommand(plan.hostLogPath, [
"transfer" ,
hostTransferScriptPath,
`${plan.vmName}:${plan.guestScriptPath}`,
]);
await runMultipassCommand(plan.hostLogPath, [
"exec" ,
plan.vmName,
"--" ,
"chmod" ,
"+x" ,
plan.guestScriptPath,
]);
await runMultipassCommand(plan.hostLogPath, ["exec" , plan.vmName, "--" , plan.guestScriptPath], {
timeoutMs: MULTIPASS_GUEST_RUN_TIMEOUT_MS,
});
await tryCopyGuestBootstrapLog(plan);
} catch (error) {
if (launched) {
await tryCopyGuestBootstrapLog(plan);
}
throw new Error(
`QA Multipass run failed: ${error instanceof Error ? error.message : String(error)}. See ${plan.hostLogPath}.`,
{ cause: error },
);
} finally {
await fs.promises.rm(hostTransferDirPath, { recursive: true , force: true });
if (launched) {
try {
await runMultipassCommand(plan.hostLogPath, ["delete" , "--purge" , plan.vmName]);
} catch (error) {
await appendMultipassLog(
plan.hostLogPath,
`cleanup error: ${error instanceof Error ? error.message : String(error)}\n\n`,
);
}
}
}
await access(plan.reportPath);
await access(plan.summaryPath);
return {
outputDir: plan.outputDir,
reportPath: plan.reportPath,
summaryPath: plan.summaryPath,
hostLogPath: plan.hostLogPath,
bootstrapLogPath: plan.hostBootstrapLogPath,
guestScriptPath: plan.hostGuestScriptPath,
vmName: plan.vmName,
scenarioIds: plan.scenarioIds,
} satisfies QaMultipassRunResult;
}
Messung V0.5 in Prozent C=100 H=94 G=96
¤ Dauer der Verarbeitung: 0.12 Sekunden
(vorverarbeitet am 2026-06-09)
¤
*© Formatika GbR, Deutschland