mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:10:45 +00:00
test: harden Parallels fresh install smoke
This commit is contained in:
@@ -36,7 +36,7 @@ TIMEOUT_INSTALL_S=420
|
||||
TIMEOUT_VERIFY_S=90
|
||||
TIMEOUT_ONBOARD_S=180
|
||||
TIMEOUT_AGENT_S=180
|
||||
TIMEOUT_GATEWAY_S=90
|
||||
TIMEOUT_GATEWAY_S=240
|
||||
|
||||
FRESH_MAIN_STATUS="skip"
|
||||
FRESH_MAIN_VERSION="skip"
|
||||
@@ -473,6 +473,10 @@ else:
|
||||
PY
|
||||
}
|
||||
|
||||
source_tree_dirty_for_build() {
|
||||
[[ -n "$(git status --porcelain -- src ui packages extensions package.json pnpm-lock.yaml 'tsconfig*.json' 2>/dev/null)" ]]
|
||||
}
|
||||
|
||||
acquire_build_lock() {
|
||||
local owner_pid=""
|
||||
while ! mkdir "$BUILD_LOCK_DIR" 2>/dev/null; do
|
||||
@@ -500,7 +504,7 @@ ensure_current_build() {
|
||||
acquire_build_lock
|
||||
head="$(git rev-parse HEAD)"
|
||||
build_commit="$(current_build_commit)"
|
||||
if [[ "$build_commit" == "$head" ]]; then
|
||||
if [[ "$build_commit" == "$head" ]] && ! source_tree_dirty_for_build; then
|
||||
release_build_lock
|
||||
return
|
||||
fi
|
||||
@@ -866,8 +870,8 @@ run_fresh_main_lane() {
|
||||
phase_run "fresh.install-main" "$TIMEOUT_INSTALL_S" install_main_tgz "$host_ip" "openclaw-main-fresh.tgz"
|
||||
FRESH_MAIN_VERSION="$(extract_last_version "$(phase_log_path fresh.install-main)")"
|
||||
phase_run "fresh.verify-main-version" "$TIMEOUT_VERIFY_S" verify_target_version
|
||||
phase_run "fresh.inject-bad-plugin" "$TIMEOUT_VERIFY_S" inject_bad_plugin_fixture
|
||||
phase_run "fresh.onboard-ref" "$TIMEOUT_ONBOARD_S" run_ref_onboard
|
||||
phase_run "fresh.inject-bad-plugin" "$TIMEOUT_VERIFY_S" inject_bad_plugin_fixture
|
||||
phase_run "fresh.gateway-start" "$TIMEOUT_GATEWAY_S" start_gateway_background
|
||||
phase_run "fresh.bad-plugin-diagnostic" "$TIMEOUT_VERIFY_S" verify_bad_plugin_diagnostic
|
||||
phase_run "fresh.gateway-status" "$TIMEOUT_VERIFY_S" show_gateway_status_compat
|
||||
|
||||
@@ -44,11 +44,11 @@ TIMEOUT_INSTALL_S=420
|
||||
TIMEOUT_UPDATE_S=300
|
||||
TIMEOUT_UPDATE_POLL_GRACE_S=60
|
||||
TIMEOUT_VERIFY_S=120
|
||||
TIMEOUT_ONBOARD_S=240
|
||||
TIMEOUT_ONBOARD_S=600
|
||||
TIMEOUT_ONBOARD_PHASE_S=$((TIMEOUT_ONBOARD_S + 120))
|
||||
# verify_gateway_reachable runs six 30s probes plus short retry sleeps.
|
||||
TIMEOUT_GATEWAY_S=240
|
||||
TIMEOUT_AGENT_S=360
|
||||
TIMEOUT_GATEWAY_S=420
|
||||
TIMEOUT_AGENT_S=600
|
||||
|
||||
FRESH_MAIN_STATUS="skip"
|
||||
FRESH_MAIN_VERSION="skip"
|
||||
@@ -860,6 +860,10 @@ else:
|
||||
PY
|
||||
}
|
||||
|
||||
source_tree_dirty_for_build() {
|
||||
[[ -n "$(git status --porcelain -- src ui packages extensions package.json pnpm-lock.yaml 'tsconfig*.json' 2>/dev/null)" ]]
|
||||
}
|
||||
|
||||
acquire_build_lock() {
|
||||
local owner_pid=""
|
||||
while ! mkdir "$BUILD_LOCK_DIR" 2>/dev/null; do
|
||||
@@ -887,7 +891,7 @@ ensure_current_build() {
|
||||
acquire_build_lock
|
||||
head="$(git rev-parse HEAD)"
|
||||
build_commit="$(current_build_commit)"
|
||||
if [[ "$build_commit" == "$head" ]]; then
|
||||
if [[ "$build_commit" == "$head" ]] && ! source_tree_dirty_for_build; then
|
||||
release_build_lock
|
||||
return
|
||||
fi
|
||||
|
||||
@@ -39,17 +39,16 @@ const resolveGatewayPort = vi.hoisted(() => vi.fn((_cfg?: unknown, _env?: unknow
|
||||
const findVerifiedGatewayListenerPidsOnPortSync = vi.fn<(port: number) => number[]>(() => []);
|
||||
const signalVerifiedGatewayPidSync = vi.fn<(pid: number, signal: "SIGTERM" | "SIGUSR1") => void>();
|
||||
const formatGatewayPidList = vi.fn<(pids: number[]) => string>((pids) => pids.join(", "));
|
||||
const probeGateway =
|
||||
vi.fn<
|
||||
(opts: {
|
||||
url: string;
|
||||
auth?: { token?: string; password?: string };
|
||||
timeoutMs: number;
|
||||
}) => Promise<{
|
||||
ok: boolean;
|
||||
configSnapshot: unknown;
|
||||
}>
|
||||
>();
|
||||
const probeGateway = vi.fn<
|
||||
(opts: {
|
||||
url: string;
|
||||
auth?: { token?: string; password?: string };
|
||||
timeoutMs: number;
|
||||
}) => Promise<{
|
||||
ok: boolean;
|
||||
configSnapshot: unknown;
|
||||
}>
|
||||
>();
|
||||
const isRestartEnabled = vi.fn<(config?: { commands?: unknown }) => boolean>(() => true);
|
||||
const loadConfig = vi.hoisted(() => vi.fn(() => ({})));
|
||||
const recoverInstalledLaunchAgent = vi.hoisted(() => vi.fn());
|
||||
@@ -290,6 +289,27 @@ describe("runDaemonRestart health checks", () => {
|
||||
expect(renderRestartDiagnostics).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("waits longer for Windows gateway restart health", async () => {
|
||||
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
||||
waitForGatewayHealthyRestart.mockResolvedValue({
|
||||
healthy: true,
|
||||
staleGatewayPids: [],
|
||||
runtime: { status: "running" },
|
||||
portUsage: { port: 18789, status: "busy", listeners: [], hints: [] },
|
||||
});
|
||||
|
||||
await runDaemonRestart({ json: true });
|
||||
|
||||
expect(waitForGatewayHealthyRestart).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
attempts: 360,
|
||||
delayMs: 500,
|
||||
includeUnknownListenersAsStale: true,
|
||||
port: 18789,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("fails restart with a stopped-free message when the waiter exits early", async () => {
|
||||
const { formatCliCommand } = await import("../command-format.js");
|
||||
const unhealthy: RestartHealthSnapshot = {
|
||||
|
||||
@@ -33,6 +33,13 @@ import type { DaemonLifecycleOptions } from "./types.js";
|
||||
|
||||
const POST_RESTART_HEALTH_ATTEMPTS = DEFAULT_RESTART_HEALTH_ATTEMPTS;
|
||||
const POST_RESTART_HEALTH_DELAY_MS = DEFAULT_RESTART_HEALTH_DELAY_MS;
|
||||
const WINDOWS_POST_RESTART_HEALTH_TIMEOUT_MS = 180_000;
|
||||
|
||||
function postRestartHealthAttempts(): number {
|
||||
return process.platform === "win32"
|
||||
? Math.ceil(WINDOWS_POST_RESTART_HEALTH_TIMEOUT_MS / POST_RESTART_HEALTH_DELAY_MS)
|
||||
: POST_RESTART_HEALTH_ATTEMPTS;
|
||||
}
|
||||
|
||||
function formatRestartFailure(params: {
|
||||
health: GatewayRestartSnapshot;
|
||||
@@ -183,7 +190,8 @@ export async function runDaemonRestart(opts: DaemonLifecycleOptions = {}): Promi
|
||||
const restartPort = await resolveGatewayLifecyclePort(service).catch(() =>
|
||||
resolveGatewayPortFallback(),
|
||||
);
|
||||
const restartWaitMs = POST_RESTART_HEALTH_ATTEMPTS * POST_RESTART_HEALTH_DELAY_MS;
|
||||
const restartHealthAttempts = postRestartHealthAttempts();
|
||||
const restartWaitMs = restartHealthAttempts * POST_RESTART_HEALTH_DELAY_MS;
|
||||
const restartWaitSeconds = Math.round(restartWaitMs / 1000);
|
||||
|
||||
return await runServiceRestart({
|
||||
@@ -204,7 +212,7 @@ export async function runDaemonRestart(opts: DaemonLifecycleOptions = {}): Promi
|
||||
if (restartedWithoutServiceManager) {
|
||||
const health = await waitForGatewayHealthyListener({
|
||||
port: restartPort,
|
||||
attempts: POST_RESTART_HEALTH_ATTEMPTS,
|
||||
attempts: restartHealthAttempts,
|
||||
delayMs: POST_RESTART_HEALTH_DELAY_MS,
|
||||
});
|
||||
if (health.healthy) {
|
||||
@@ -233,7 +241,7 @@ export async function runDaemonRestart(opts: DaemonLifecycleOptions = {}): Promi
|
||||
let health = await waitForGatewayHealthyRestart({
|
||||
service,
|
||||
port: restartPort,
|
||||
attempts: POST_RESTART_HEALTH_ATTEMPTS,
|
||||
attempts: restartHealthAttempts,
|
||||
delayMs: POST_RESTART_HEALTH_DELAY_MS,
|
||||
includeUnknownListenersAsStale: process.platform === "win32",
|
||||
});
|
||||
@@ -254,7 +262,7 @@ export async function runDaemonRestart(opts: DaemonLifecycleOptions = {}): Promi
|
||||
health = await waitForGatewayHealthyRestart({
|
||||
service,
|
||||
port: restartPort,
|
||||
attempts: POST_RESTART_HEALTH_ATTEMPTS,
|
||||
attempts: restartHealthAttempts,
|
||||
delayMs: POST_RESTART_HEALTH_DELAY_MS,
|
||||
includeUnknownListenersAsStale: process.platform === "win32",
|
||||
});
|
||||
|
||||
@@ -89,6 +89,7 @@ async function inspectAmbiguousOwnershipWithProbe(
|
||||
}
|
||||
|
||||
async function waitForStoppedFreeGatewayRestart() {
|
||||
const attempts = process.platform === "win32" ? 360 : 120;
|
||||
const service = makeGatewayService({ status: "stopped" });
|
||||
inspectPortUsage.mockResolvedValue({
|
||||
port: 18789,
|
||||
@@ -101,7 +102,7 @@ async function waitForStoppedFreeGatewayRestart() {
|
||||
return waitForGatewayHealthyRestart({
|
||||
service,
|
||||
port: 18789,
|
||||
attempts: 120,
|
||||
attempts,
|
||||
delayMs: 500,
|
||||
});
|
||||
}
|
||||
@@ -292,9 +293,9 @@ describe("inspectGatewayRestart", () => {
|
||||
runtime: { status: "stopped" },
|
||||
portUsage: { status: "free" },
|
||||
waitOutcome: "stopped-free",
|
||||
elapsedMs: 27_500,
|
||||
elapsedMs: 92_500,
|
||||
});
|
||||
expect(sleep).toHaveBeenCalledTimes(55);
|
||||
expect(sleep).toHaveBeenCalledTimes(185);
|
||||
});
|
||||
|
||||
it("annotates timeout waits when the health loop exhausts all attempts", async () => {
|
||||
|
||||
@@ -20,7 +20,7 @@ export const DEFAULT_RESTART_HEALTH_ATTEMPTS = Math.ceil(
|
||||
DEFAULT_RESTART_HEALTH_TIMEOUT_MS / DEFAULT_RESTART_HEALTH_DELAY_MS,
|
||||
);
|
||||
const STOPPED_FREE_EARLY_EXIT_GRACE_MS = 10_000;
|
||||
const WINDOWS_STOPPED_FREE_EARLY_EXIT_GRACE_MS = 25_000;
|
||||
const WINDOWS_STOPPED_FREE_EARLY_EXIT_GRACE_MS = 90_000;
|
||||
|
||||
export type GatewayRestartWaitOutcome = "healthy" | "stale-pids" | "stopped-free" | "timeout";
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ describe("execSchtasks", () => {
|
||||
});
|
||||
expect(runCommandWithTimeout).toHaveBeenCalledWith(["schtasks", "/Query"], {
|
||||
timeoutMs: 15_000,
|
||||
noOutputTimeoutMs: 5_000,
|
||||
noOutputTimeoutMs: 30_000,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { runCommandWithTimeout } from "../process/exec.js";
|
||||
|
||||
const SCHTASKS_TIMEOUT_MS = 15_000;
|
||||
const SCHTASKS_NO_OUTPUT_TIMEOUT_MS = 5_000;
|
||||
const SCHTASKS_NO_OUTPUT_TIMEOUT_MS = 30_000;
|
||||
|
||||
export async function execSchtasks(
|
||||
args: string[],
|
||||
|
||||
@@ -220,6 +220,21 @@ describe("Windows startup fallback", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to a Startup-folder launcher when schtasks availability is slow", async () => {
|
||||
await withWindowsEnv("openclaw-win-startup-", async ({ env }) => {
|
||||
schtasksResponses.push(
|
||||
{ code: 124, stdout: "", stderr: "schtasks produced no output for 30000ms" },
|
||||
{ code: 124, stdout: "", stderr: "schtasks produced no output for 30000ms" },
|
||||
{ code: 124, stdout: "", stderr: "schtasks produced no output for 30000ms" },
|
||||
);
|
||||
|
||||
await installGatewayScheduledTask(env);
|
||||
|
||||
await expect(fs.access(resolveStartupEntryPath(env))).resolves.toBeUndefined();
|
||||
expectStartupFallbackSpawn(env);
|
||||
});
|
||||
});
|
||||
|
||||
it("launches the task script directly when schtasks /Run is accepted but never starts the task", async () => {
|
||||
await withWindowsEnv("openclaw-win-startup-", async ({ env }) => {
|
||||
fastForwardTaskStartWait();
|
||||
|
||||
@@ -578,7 +578,7 @@ async function writeScheduledTaskScript({
|
||||
scriptPath: string;
|
||||
taskDescription: string;
|
||||
}> {
|
||||
await assertSchtasksAvailable();
|
||||
await assertSchtasksAvailable().catch(() => undefined);
|
||||
const scriptPath = resolveTaskScriptPath(env);
|
||||
await fs.mkdir(path.dirname(scriptPath), { recursive: true });
|
||||
const taskDescription = resolveGatewayServiceDescription({ env, environment, description });
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
ensureBundledPluginRuntimeDeps,
|
||||
installBundledRuntimeDeps,
|
||||
resolveBundledRuntimeDepsNpmRunner,
|
||||
} from "./bundled-runtime-deps.js";
|
||||
|
||||
vi.mock("node:child_process", () => ({
|
||||
spawnSync: vi.fn(),
|
||||
}));
|
||||
|
||||
const spawnSyncMock = vi.mocked(spawnSync);
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
function makeTempDir(): string {
|
||||
@@ -16,12 +23,29 @@ function makeTempDir(): string {
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
spawnSyncMock.mockReset();
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe("resolveBundledRuntimeDepsNpmRunner", () => {
|
||||
it("uses npm_execpath through node on Windows when available", () => {
|
||||
const runner = resolveBundledRuntimeDepsNpmRunner({
|
||||
env: { npm_execpath: "C:\\node\\node_modules\\npm\\bin\\npm-cli.js" },
|
||||
execPath: "C:\\Program Files\\nodejs\\node.exe",
|
||||
existsSync: (candidate) => candidate === "C:\\node\\node_modules\\npm\\bin\\npm-cli.js",
|
||||
npmArgs: ["install", "acpx@0.5.3"],
|
||||
platform: "win32",
|
||||
});
|
||||
|
||||
expect(runner).toEqual({
|
||||
command: "C:\\Program Files\\nodejs\\node.exe",
|
||||
args: ["C:\\node\\node_modules\\npm\\bin\\npm-cli.js", "install", "acpx@0.5.3"],
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the Node-adjacent npm CLI on Windows", () => {
|
||||
const execPath = "C:\\Program Files\\nodejs\\node.exe";
|
||||
const npmCliPath = path.win32.resolve(
|
||||
@@ -43,16 +67,20 @@ describe("resolveBundledRuntimeDepsNpmRunner", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not fall back to bare npm on Windows", () => {
|
||||
expect(() =>
|
||||
resolveBundledRuntimeDepsNpmRunner({
|
||||
env: {},
|
||||
execPath: "C:\\Program Files\\nodejs\\node.exe",
|
||||
existsSync: () => false,
|
||||
npmArgs: ["install"],
|
||||
platform: "win32",
|
||||
}),
|
||||
).toThrow("failed to resolve a toolchain-local npm");
|
||||
it("falls back to npm.cmd through shell on Windows", () => {
|
||||
const runner = resolveBundledRuntimeDepsNpmRunner({
|
||||
env: {},
|
||||
execPath: "C:\\Program Files\\nodejs\\node.exe",
|
||||
existsSync: () => false,
|
||||
npmArgs: ["install"],
|
||||
platform: "win32",
|
||||
});
|
||||
|
||||
expect(runner).toEqual({
|
||||
command: "npm.cmd",
|
||||
args: ["install"],
|
||||
shell: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("prefixes PATH with the active Node directory on POSIX", () => {
|
||||
@@ -74,7 +102,72 @@ describe("resolveBundledRuntimeDepsNpmRunner", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("installBundledRuntimeDeps", () => {
|
||||
it("uses the npm cmd shim on Windows", () => {
|
||||
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
||||
vi.spyOn(fs, "existsSync").mockReturnValue(false);
|
||||
spawnSyncMock.mockReturnValue({
|
||||
pid: 123,
|
||||
output: [],
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
signal: null,
|
||||
status: 0,
|
||||
});
|
||||
|
||||
installBundledRuntimeDeps({
|
||||
installRoot: "C:\\openclaw",
|
||||
missingSpecs: ["acpx@0.5.3"],
|
||||
env: { npm_config_prefix: "C:\\prefix", PATH: "C:\\node" },
|
||||
});
|
||||
|
||||
expect(spawnSyncMock).toHaveBeenCalledWith(
|
||||
"npm.cmd",
|
||||
[
|
||||
"install",
|
||||
"--prefix",
|
||||
"C:\\openclaw",
|
||||
"--omit=dev",
|
||||
"--no-save",
|
||||
"--package-lock=false",
|
||||
"--ignore-scripts",
|
||||
"--legacy-peer-deps",
|
||||
"acpx@0.5.3",
|
||||
],
|
||||
expect.objectContaining({
|
||||
cwd: "C:\\openclaw",
|
||||
shell: true,
|
||||
env: expect.not.objectContaining({
|
||||
npm_config_prefix: expect.any(String),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("includes spawn errors in install failures", () => {
|
||||
spawnSyncMock.mockReturnValue({
|
||||
pid: 0,
|
||||
output: [],
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
signal: null,
|
||||
status: null,
|
||||
error: new Error("spawn npm ENOENT"),
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
installBundledRuntimeDeps({
|
||||
installRoot: "/tmp/openclaw",
|
||||
missingSpecs: ["browser-runtime@1.0.0"],
|
||||
env: {},
|
||||
}),
|
||||
).toThrow("spawn npm ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ensureBundledPluginRuntimeDeps", () => {
|
||||
it("installs all direct plugin runtime deps when one is missing", () => {
|
||||
const packageRoot = makeTempDir();
|
||||
const extensionsRoot = path.join(packageRoot, "dist", "extensions");
|
||||
|
||||
@@ -30,6 +30,7 @@ export type BundledRuntimeDepsNpmRunner = {
|
||||
command: string;
|
||||
args: string[];
|
||||
env?: NodeJS.ProcessEnv;
|
||||
shell?: boolean;
|
||||
};
|
||||
|
||||
function dependencySentinelPath(depName: string): string {
|
||||
@@ -99,12 +100,18 @@ export function resolveBundledRuntimeDepsNpmRunner(params: {
|
||||
const platform = params.platform ?? process.platform;
|
||||
const pathImpl = platform === "win32" ? path.win32 : path.posix;
|
||||
const nodeDir = pathImpl.dirname(execPath);
|
||||
const npmExecPath = normalizeOptionalLowercaseString(env.npm_execpath)
|
||||
? env.npm_execpath
|
||||
: undefined;
|
||||
|
||||
const npmCliCandidates = [
|
||||
npmExecPath,
|
||||
pathImpl.resolve(nodeDir, "../lib/node_modules/npm/bin/npm-cli.js"),
|
||||
pathImpl.resolve(nodeDir, "node_modules/npm/bin/npm-cli.js"),
|
||||
];
|
||||
const npmCliPath = npmCliCandidates.find((candidate) => existsSync(candidate));
|
||||
].filter((candidate): candidate is string => Boolean(candidate));
|
||||
const npmCliPath = npmCliCandidates.find(
|
||||
(candidate) => pathImpl.isAbsolute(candidate) && existsSync(candidate),
|
||||
);
|
||||
if (npmCliPath) {
|
||||
return {
|
||||
command: execPath,
|
||||
@@ -120,10 +127,11 @@ export function resolveBundledRuntimeDepsNpmRunner(params: {
|
||||
args: params.npmArgs,
|
||||
};
|
||||
}
|
||||
throw new Error(
|
||||
`failed to resolve a toolchain-local npm next to ${execPath}. ` +
|
||||
`Checked: ${[...npmCliCandidates, npmExePath].join(", ")}.`,
|
||||
);
|
||||
return {
|
||||
command: "npm.cmd",
|
||||
args: params.npmArgs,
|
||||
shell: true,
|
||||
};
|
||||
}
|
||||
|
||||
const pathKey = resolvePathEnvKey(env, platform);
|
||||
@@ -140,7 +148,6 @@ export function resolveBundledRuntimeDepsNpmRunner(params: {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function readBundledPluginChannels(pluginDir: string): string[] {
|
||||
const manifest = readJsonObject(path.join(pluginDir, "openclaw.plugin.json"));
|
||||
const channels = manifest?.channels;
|
||||
@@ -370,13 +377,13 @@ export function installBundledRuntimeDeps(params: {
|
||||
encoding: "utf8",
|
||||
env: createNestedNpmInstallEnv(npmRunner.env ?? params.env),
|
||||
stdio: "pipe",
|
||||
shell: false,
|
||||
shell: npmRunner.shell ?? false,
|
||||
});
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
if (result.status !== 0) {
|
||||
const output = [result.stderr, result.stdout].filter(Boolean).join("\n").trim();
|
||||
if (result.status !== 0 || result.error) {
|
||||
const output = [result.error?.message, result.stderr, result.stdout]
|
||||
.filter(Boolean)
|
||||
.join("\n")
|
||||
.trim();
|
||||
throw new Error(output || "npm install failed");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user