mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06:00
refactor(update): simplify finalization heap fix
Co-authored-by: hannesrudolph <49103247+hannesrudolph@users.noreply.github.com>
This commit is contained in:
@@ -20,6 +20,7 @@ import type {
|
||||
} from "../../config/types.js";
|
||||
import { resolveSecretInputRef } from "../../config/types.secrets.js";
|
||||
import { readLastGatewayErrorLine } from "../../daemon/diagnostics.js";
|
||||
import { inspectGatewayHeapLimit, type GatewayHeapLimitReport } from "../../daemon/gateway-heap.js";
|
||||
import type { ExtraGatewayService, FindExtraGatewayServicesOptions } from "../../daemon/inspect.js";
|
||||
import type { StaleOpenClawUpdateLaunchdJob } from "../../daemon/launchd.js";
|
||||
import type { ServiceConfigAudit } from "../../daemon/service-audit.js";
|
||||
@@ -42,7 +43,6 @@ import {
|
||||
inspectBestEffortPrimaryTailnetIPv4,
|
||||
resolveBestEffortGatewayBindHostForDisplay,
|
||||
} from "../../infra/network-discovery-display.js";
|
||||
import { inspectGatewayHeapLimit, type GatewayHeapLimitReport } from "../../infra/node-heap.js";
|
||||
import { formatPortDiagnostics } from "../../infra/ports-format.js";
|
||||
import {
|
||||
inspectPortConnections,
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
resolveGatewayLaunchAgentLabel,
|
||||
resolveGatewaySystemdServiceName,
|
||||
} from "../../daemon/constants.js";
|
||||
import { formatGatewayHeapLimitReport } from "../../daemon/gateway-heap.js";
|
||||
import { renderGatewayServiceCleanupHints } from "../../daemon/inspect.js";
|
||||
import {
|
||||
resolveGatewayRestartLogPath,
|
||||
@@ -17,7 +18,6 @@ import {
|
||||
} from "../../daemon/systemd-hints.js";
|
||||
import { classifySystemdUnavailableDetail } from "../../daemon/systemd-unavailable.js";
|
||||
import { resolveControlUiLinks } from "../../gateway/control-ui-links.js";
|
||||
import { formatGatewayHeapLimitReport } from "../../infra/node-heap.js";
|
||||
import { formatGatewayRestartHandoffDiagnostic } from "../../infra/restart-handoff.js";
|
||||
import { isWSLEnv } from "../../infra/wsl.js";
|
||||
import { resolvePluginVersionDriftUpdateCommand } from "../../plugins/plugin-version-drift.js";
|
||||
|
||||
+32
-53
@@ -104,15 +104,11 @@ const execFile = vi.fn((...args: unknown[]) => {
|
||||
});
|
||||
const spawn = vi.fn();
|
||||
const { defaultRuntime: runtimeCapture, resetRuntimeCapture } = createCliRuntimeCapture();
|
||||
const inheritedServiceEnvKeys = [
|
||||
const serviceEnvSnapshot = captureEnv([
|
||||
"OPENCLAW_SERVICE_MARKER",
|
||||
"OPENCLAW_SERVICE_KIND",
|
||||
GATEWAY_SERVICE_RUNTIME_PID_ENV,
|
||||
"OPENCLAW_LAUNCHD_LABEL",
|
||||
"OPENCLAW_SYSTEMD_UNIT",
|
||||
"OPENCLAW_WINDOWS_TASK_NAME",
|
||||
] as const;
|
||||
const serviceEnvSnapshot = captureEnv([...inheritedServiceEnvKeys]);
|
||||
]);
|
||||
|
||||
vi.mock("@clack/prompts", () => ({
|
||||
confirm,
|
||||
@@ -1001,14 +997,6 @@ describe("update-cli", () => {
|
||||
|
||||
const FRESH_POST_UPDATE_ENTRYPOINT = "/tmp/openclaw-updated-entry.mjs";
|
||||
|
||||
const mockFreshDoctorEntrypoints = (count = 1) => {
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
vi.mocked(resolveGatewayInstallEntrypoint).mockResolvedValueOnce(
|
||||
FRESH_POST_UPDATE_ENTRYPOINT,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const mockCurrentProcessFreshDoctor = (params: { postCoreResumeAttempt?: boolean } = {}) => {
|
||||
if (params.postCoreResumeAttempt !== false) {
|
||||
vi.mocked(resolveGatewayInstallEntrypoint).mockResolvedValueOnce(undefined);
|
||||
@@ -1355,9 +1343,9 @@ describe("update-cli", () => {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
for (const key of inheritedServiceEnvKeys) {
|
||||
delete process.env[key];
|
||||
}
|
||||
delete process.env.OPENCLAW_SERVICE_MARKER;
|
||||
delete process.env.OPENCLAW_SERVICE_KIND;
|
||||
delete process.env[GATEWAY_SERVICE_RUNTIME_PID_ENV];
|
||||
restartHealthTestControl.snapshot = undefined;
|
||||
vi.clearAllMocks();
|
||||
serviceEnabled.mockResolvedValue(true);
|
||||
@@ -7548,8 +7536,7 @@ describe("update-cli", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("updateFinalizeCommand defers plugin installation during fresh pre-plugin doctor", async () => {
|
||||
mockFreshDoctorEntrypoints();
|
||||
it("updateFinalizeCommand defers plugin installation during pre-plugin doctor", async () => {
|
||||
await withEnvAsync(
|
||||
{
|
||||
OPENCLAW_UPDATE_IN_PROGRESS: undefined,
|
||||
@@ -7558,6 +7545,10 @@ describe("update-cli", () => {
|
||||
OPENCLAW_UPDATE_POST_CORE_CONVERGENCE: "1",
|
||||
},
|
||||
async () => {
|
||||
let doctorEnv: NodeJS.ProcessEnv | undefined;
|
||||
vi.mocked(doctorCommand).mockImplementationOnce(async () => {
|
||||
doctorEnv = { ...process.env };
|
||||
});
|
||||
vi.mocked(defaultRuntime.writeJson).mockClear();
|
||||
|
||||
await updateFinalizeCommand({
|
||||
@@ -7568,30 +7559,19 @@ describe("update-cli", () => {
|
||||
acknowledgeClawHubRisk: true,
|
||||
});
|
||||
|
||||
const doctorCall = vi
|
||||
.mocked(runExec)
|
||||
.mock.calls.find(([, args]) => args[0] === FRESH_POST_UPDATE_ENTRYPOINT);
|
||||
const doctorOptions = doctorCall?.[2];
|
||||
const doctorEnv = typeof doctorOptions === "number" ? undefined : doctorOptions?.env;
|
||||
expect(doctorCall?.[1]).toEqual([
|
||||
FRESH_POST_UPDATE_ENTRYPOINT,
|
||||
"doctor",
|
||||
"--repair",
|
||||
"--non-interactive",
|
||||
"--no-workspace-suggestions",
|
||||
"--yes",
|
||||
]);
|
||||
expect(doctorEnv).toMatchObject({
|
||||
OPENCLAW_UPDATE_IN_PROGRESS: "1",
|
||||
OPENCLAW_UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR: "1",
|
||||
OPENCLAW_UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE: "1",
|
||||
});
|
||||
expect(doctorEnv?.OPENCLAW_UPDATE_IN_PROGRESS).toBe("1");
|
||||
expect(doctorEnv?.OPENCLAW_UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR).toBe("1");
|
||||
expect(doctorEnv?.OPENCLAW_UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE).toBe("1");
|
||||
expect(doctorEnv?.OPENCLAW_UPDATE_POST_CORE_CONVERGENCE).toBeUndefined();
|
||||
expect(process.env.OPENCLAW_UPDATE_IN_PROGRESS).toBeUndefined();
|
||||
expect(process.env.OPENCLAW_UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR).toBeUndefined();
|
||||
expect(process.env.OPENCLAW_UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE).toBeUndefined();
|
||||
expect(process.env.OPENCLAW_UPDATE_POST_CORE_CONVERGENCE).toBe("1");
|
||||
expect(doctorCommand).not.toHaveBeenCalled();
|
||||
expect(doctorCommand).toHaveBeenCalledWith(defaultRuntime, {
|
||||
nonInteractive: true,
|
||||
repair: true,
|
||||
yes: true,
|
||||
});
|
||||
expect(syncPluginCall()?.channel).toBe("stable");
|
||||
expect(syncPluginCall()?.acknowledgeClawHubRisk).toBe(true);
|
||||
expect(lastNpmPluginUpdateCall()?.timeoutMs).toBe(9_000);
|
||||
@@ -7635,7 +7615,7 @@ describe("update-cli", () => {
|
||||
});
|
||||
|
||||
it("updateFinalizeCommand repairs doctor by default and refreshes plugin state after doctor", async () => {
|
||||
mockFreshDoctorEntrypoints(2);
|
||||
vi.mocked(resolveGatewayInstallEntrypoint).mockResolvedValueOnce("/tmp/openclaw-entry.mjs");
|
||||
const preDoctorConfig = {
|
||||
update: { channel: "stable" },
|
||||
plugins: { entries: { pre: { enabled: true } } },
|
||||
@@ -7670,29 +7650,31 @@ describe("update-cli", () => {
|
||||
|
||||
await updateFinalizeCommand({ json: true, timeout: "9", restart: false });
|
||||
|
||||
expect(doctorCommand).not.toHaveBeenCalled();
|
||||
const freshDoctorCalls = vi
|
||||
expect(doctorCommand).toHaveBeenCalledWith(defaultRuntime, {
|
||||
nonInteractive: true,
|
||||
repair: true,
|
||||
yes: false,
|
||||
});
|
||||
expect(doctorCommand).toHaveBeenCalledTimes(1);
|
||||
const freshDoctorCall = vi
|
||||
.mocked(runExec)
|
||||
.mock.calls.filter(([, args]) => args.includes("doctor"));
|
||||
expect(freshDoctorCalls).toHaveLength(2);
|
||||
expect(freshDoctorCalls[0]?.[1]).toEqual([
|
||||
FRESH_POST_UPDATE_ENTRYPOINT,
|
||||
.mock.calls.find(([, args]) => args.includes("doctor"));
|
||||
expect(freshDoctorCall?.[1]).toEqual([
|
||||
"/tmp/openclaw-entry.mjs",
|
||||
"doctor",
|
||||
"--repair",
|
||||
"--non-interactive",
|
||||
"--no-workspace-suggestions",
|
||||
]);
|
||||
expect(freshDoctorCalls[0]?.[2]).toMatchObject({
|
||||
expect(freshDoctorCall?.[2]).toMatchObject({
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
OPENCLAW_UPDATE_IN_PROGRESS: "1",
|
||||
OPENCLAW_UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR: "1",
|
||||
OPENCLAW_UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE: "1",
|
||||
OPENCLAW_UPDATE_POST_CORE_CONVERGENCE: "1",
|
||||
},
|
||||
});
|
||||
expect(freshDoctorCalls[1]?.[2]).toMatchObject({
|
||||
env: { OPENCLAW_UPDATE_POST_CORE_CONVERGENCE: "1" },
|
||||
});
|
||||
expect(syncPluginCall()?.channel).toBe("beta");
|
||||
expect(syncPluginCall()?.config).toEqual({
|
||||
...postDoctorConfig,
|
||||
@@ -7702,14 +7684,13 @@ describe("update-cli", () => {
|
||||
},
|
||||
});
|
||||
expect(lastReplaceConfigCall()?.baseHash).toBe("post-doctor");
|
||||
expect(vi.mocked(runExec).mock.invocationCallOrder[0] ?? 0).toBeLessThan(
|
||||
expect(vi.mocked(doctorCommand).mock.invocationCallOrder[0] ?? 0).toBeLessThan(
|
||||
loadInstalledPluginIndexInstallRecords.mock.invocationCallOrder[0] ?? 0,
|
||||
);
|
||||
expect((lastWriteJsonCall() as { channel?: string } | undefined)?.channel).toBe("beta");
|
||||
});
|
||||
|
||||
it("updateFinalizeCommand restores channels from the RPC pre-update config payload", async () => {
|
||||
mockFreshDoctorEntrypoints();
|
||||
const tempDir = createCaseDir("openclaw-rpc-finalize");
|
||||
const sourceConfigPath = path.join(tempDir, "source-config.json");
|
||||
const preUpdateConfig = {
|
||||
@@ -7756,7 +7737,6 @@ describe("update-cli", () => {
|
||||
});
|
||||
|
||||
it("updateFinalizeCommand reapplies requested channel against post-doctor config", async () => {
|
||||
mockFreshDoctorEntrypoints();
|
||||
const preDoctorConfig = { update: { channel: "stable" } } as OpenClawConfig;
|
||||
const postDoctorConfig = { update: { channel: "beta" } } as OpenClawConfig;
|
||||
const preDoctorSnapshot = configSnapshot(preDoctorConfig, {
|
||||
@@ -7784,7 +7764,6 @@ describe("update-cli", () => {
|
||||
});
|
||||
|
||||
it("updateFinalizeCommand converges on the effective channel from env without persisting update.channel", async () => {
|
||||
mockFreshDoctorEntrypoints();
|
||||
const noChannelConfig = {} as OpenClawConfig;
|
||||
const noChannelSnapshot = configSnapshot(noChannelConfig, {
|
||||
parsed: baseSnapshot.parsed,
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { useAutoCleanupTempDirTracker } from "../../../test/helpers/temp-dir.js";
|
||||
import { withEnvAsync } from "../../test-utils/env.js";
|
||||
import { runUpdateFinalizationDoctorInFreshProcess } from "./update-command-fresh-doctor.js";
|
||||
|
||||
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
||||
|
||||
describe("runUpdateFinalizationDoctorInFreshProcess", () => {
|
||||
it("gives the updater-owned Doctor enough heap while preserving other Node options", async () => {
|
||||
const root = tempDirs.make("openclaw-update-doctor-heap-");
|
||||
const entryPath = path.join(root, "capture-heap.mjs");
|
||||
const outputPath = path.join(root, "heap.json");
|
||||
await fs.writeFile(
|
||||
entryPath,
|
||||
`import fs from "node:fs";
|
||||
import { getHeapStatistics } from "node:v8";
|
||||
fs.writeFileSync(process.env.OPENCLAW_TEST_OUTPUT_PATH, JSON.stringify({
|
||||
heapSizeLimitMiB: Math.floor(getHeapStatistics().heap_size_limit / 1024 / 1024),
|
||||
nodeOptions: process.env.NODE_OPTIONS ?? "",
|
||||
}));`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
await withEnvAsync(
|
||||
{
|
||||
NODE_OPTIONS: "--trace-warnings --max-old-space-size=1024",
|
||||
OPENCLAW_TEST_OUTPUT_PATH: outputPath,
|
||||
},
|
||||
async () => {
|
||||
await runUpdateFinalizationDoctorInFreshProcess({
|
||||
phase: "pre-plugin",
|
||||
root,
|
||||
yes: true,
|
||||
json: true,
|
||||
timeoutMs: 10_000,
|
||||
nodeRunner: process.execPath,
|
||||
entryPath,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const result = JSON.parse(await fs.readFile(outputPath, "utf8")) as {
|
||||
heapSizeLimitMiB: number;
|
||||
nodeOptions: string;
|
||||
};
|
||||
expect(result.heapSizeLimitMiB).toBeGreaterThanOrEqual(8192);
|
||||
expect(result.nodeOptions).toContain("--trace-warnings");
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
import { readConfigFileSnapshot } from "../../config/config.js";
|
||||
import type { ConfigFileSnapshot } from "../../config/types.openclaw.js";
|
||||
import { resolveGatewayInstallEntrypoint } from "../../daemon/gateway-entrypoint.js";
|
||||
import { resolveUpdateNodeOptions } from "../../infra/update-runner-doctor.js";
|
||||
import { runExec } from "../../process/exec.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { resolveNodeRunner } from "./shared.js";
|
||||
@@ -23,10 +22,43 @@ import {
|
||||
|
||||
type UpdateDoctorPhase = "pre-plugin" | "post-plugin";
|
||||
|
||||
function resolveUpdateFinalizationBaseEnv(): NodeJS.ProcessEnv {
|
||||
const env = stripGatewayServiceMarkerEnv(disableUpdatedPackageCompileCacheEnv(process.env));
|
||||
env.NODE_OPTIONS = resolveUpdateNodeOptions(env.NODE_OPTIONS);
|
||||
return env;
|
||||
export async function withPrePluginUpdateDoctorEnv<T>(run: () => Promise<T>): Promise<T> {
|
||||
const previousUpdateInProgress = process.env.OPENCLAW_UPDATE_IN_PROGRESS;
|
||||
const previousDeferConfiguredPluginInstallRepair =
|
||||
process.env[UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR_ENV];
|
||||
const previousParentSupportsDoctorConfigWrite =
|
||||
process.env[UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV];
|
||||
const previousPostCoreConvergence = process.env[UPDATE_POST_CORE_CONVERGENCE_ENV];
|
||||
process.env.OPENCLAW_UPDATE_IN_PROGRESS = "1";
|
||||
process.env[UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR_ENV] = "1";
|
||||
process.env[UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV] = "1";
|
||||
delete process.env[UPDATE_POST_CORE_CONVERGENCE_ENV];
|
||||
try {
|
||||
return await run();
|
||||
} finally {
|
||||
if (previousUpdateInProgress === undefined) {
|
||||
delete process.env.OPENCLAW_UPDATE_IN_PROGRESS;
|
||||
} else {
|
||||
process.env.OPENCLAW_UPDATE_IN_PROGRESS = previousUpdateInProgress;
|
||||
}
|
||||
if (previousDeferConfiguredPluginInstallRepair === undefined) {
|
||||
delete process.env[UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR_ENV];
|
||||
} else {
|
||||
process.env[UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR_ENV] =
|
||||
previousDeferConfiguredPluginInstallRepair;
|
||||
}
|
||||
if (previousParentSupportsDoctorConfigWrite === undefined) {
|
||||
delete process.env[UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV];
|
||||
} else {
|
||||
process.env[UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV] =
|
||||
previousParentSupportsDoctorConfigWrite;
|
||||
}
|
||||
if (previousPostCoreConvergence === undefined) {
|
||||
delete process.env[UPDATE_POST_CORE_CONVERGENCE_ENV];
|
||||
} else {
|
||||
process.env[UPDATE_POST_CORE_CONVERGENCE_ENV] = previousPostCoreConvergence;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function withNormalConfigValidation<T>(run: () => Promise<T>): Promise<T> {
|
||||
@@ -83,7 +115,7 @@ export async function runUpdateFinalizationDoctorInFreshProcess(params: {
|
||||
"--no-workspace-suggestions",
|
||||
...(params.yes ? ["--yes"] : []),
|
||||
];
|
||||
const baseEnv = resolveUpdateFinalizationBaseEnv();
|
||||
const baseEnv = stripGatewayServiceMarkerEnv(disableUpdatedPackageCompileCacheEnv(process.env));
|
||||
delete baseEnv[UPDATE_POST_CORE_CONVERGENCE_ENV];
|
||||
const result = await runExec(params.nodeRunner ?? resolveNodeRunner(), args, {
|
||||
cwd: params.root,
|
||||
@@ -123,7 +155,7 @@ async function validatePostPluginConfigInFreshProcess(params: {
|
||||
timeoutMs: params.timeoutMs,
|
||||
maxBuffer: 4 * 1024 * 1024,
|
||||
logOutput: false,
|
||||
baseEnv: resolveUpdateFinalizationBaseEnv(),
|
||||
baseEnv: stripGatewayServiceMarkerEnv(disableUpdatedPackageCompileCacheEnv(process.env)),
|
||||
env: { OPENCLAW_UPDATE_IN_PROGRESS: "0" },
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import path from "node:path";
|
||||
import { theme } from "../../../packages/terminal-core/src/theme.js";
|
||||
import {
|
||||
UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR_ENV,
|
||||
UPDATE_PARENT_ALLOWS_GATEWAY_ACTIVATION_ENV,
|
||||
UPDATE_PARENT_ALLOWS_GATEWAY_SERVICE_REPAIR_ENV,
|
||||
UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV,
|
||||
UPDATE_PARENT_SUPPORTS_GATEWAY_RESTART_ENV,
|
||||
} from "../../commands/doctor/shared/update-phase.js";
|
||||
import { resolveGatewayInstallEntrypoint } from "../../daemon/gateway-entrypoint.js";
|
||||
import { createLowDiskSpaceWarning } from "../../infra/disk-space.js";
|
||||
import {
|
||||
@@ -17,7 +24,6 @@ import {
|
||||
resolveGlobalInstallTarget,
|
||||
type ResolvedGlobalInstallTarget,
|
||||
} from "../../infra/update-global.js";
|
||||
import { buildUpdateDoctorEnv } from "../../infra/update-runner-doctor.js";
|
||||
import {
|
||||
resolveUpdateDoctorExecutionPolicy,
|
||||
type UpdateRunResult,
|
||||
@@ -139,27 +145,30 @@ export async function runPackageInstallUpdate(params: {
|
||||
total: 0,
|
||||
};
|
||||
params.progress?.onStepStart?.(doctorProgressInfo);
|
||||
const doctorBaseEnv = resolvePostInstallDoctorEnv({
|
||||
serviceEnv: params.managedServiceEnv,
|
||||
invocationCwd: params.invocationCwd,
|
||||
});
|
||||
const doctorStep = await runUpdateStep({
|
||||
name: `${CLI_NAME} doctor`,
|
||||
argv: doctorArgv,
|
||||
cwd: verifiedPackageRoot,
|
||||
env: {
|
||||
...doctorBaseEnv,
|
||||
...buildUpdateDoctorEnv({
|
||||
allowGatewayServiceRepair: params.allowGatewayServiceRepair,
|
||||
allowGatewayActivation: params.allowGatewayActivation,
|
||||
...(doctorPolicy.serviceRepairPolicy
|
||||
? { serviceRepairPolicy: doctorPolicy.serviceRepairPolicy }
|
||||
: {}),
|
||||
deferConfiguredPluginInstallRepair: true,
|
||||
compatibilityHostVersion: candidateHostVersion,
|
||||
...(doctorBaseEnv.NODE_OPTIONS ? { nodeOptions: doctorBaseEnv.NODE_OPTIONS } : {}),
|
||||
...resolvePostInstallDoctorEnv({
|
||||
serviceEnv: params.managedServiceEnv,
|
||||
invocationCwd: params.invocationCwd,
|
||||
}),
|
||||
OPENCLAW_UPDATE_IN_PROGRESS: "1",
|
||||
[UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR_ENV]: "1",
|
||||
[UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV]: "1",
|
||||
[UPDATE_PARENT_SUPPORTS_GATEWAY_RESTART_ENV]: "1",
|
||||
[UPDATE_PARENT_ALLOWS_GATEWAY_SERVICE_REPAIR_ENV]: params.allowGatewayServiceRepair
|
||||
? "1"
|
||||
: "0",
|
||||
[UPDATE_PARENT_ALLOWS_GATEWAY_ACTIVATION_ENV]: params.allowGatewayActivation ? "1" : "0",
|
||||
...(doctorPolicy.serviceRepairPolicy
|
||||
? { OPENCLAW_SERVICE_REPAIR_POLICY: doctorPolicy.serviceRepairPolicy }
|
||||
: {}),
|
||||
[UPDATE_POST_INSTALL_DOCTOR_RESULT_PATH_ENV]: doctorResultPath,
|
||||
...(candidateHostVersion === null
|
||||
? {}
|
||||
: { OPENCLAW_COMPATIBILITY_HOST_VERSION: candidateHostVersion }),
|
||||
},
|
||||
timeoutMs: params.timeoutMs,
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import { parseStrictPositiveInteger } from "@openclaw/normalization-core/number-
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import { theme } from "../../../packages/terminal-core/src/theme.js";
|
||||
import { doctorCommand } from "../../commands/doctor.js";
|
||||
import {
|
||||
assertConfigWriteAllowedInCurrentMode,
|
||||
readConfigFileSnapshot,
|
||||
@@ -44,7 +45,6 @@ import {
|
||||
POST_CORE_UPDATE_SOURCE_CONFIG_PATH_ENV,
|
||||
type PreUpdateConfigRestoreInput,
|
||||
} from "../../infra/update-post-core-context.js";
|
||||
import { resolveUpdateNodeOptions } from "../../infra/update-runner-doctor.js";
|
||||
import type { UpdateRunResult } from "../../infra/update-runner.js";
|
||||
import { getWindowsSystem32ExePath } from "../../infra/windows-install-roots.js";
|
||||
import {
|
||||
@@ -79,7 +79,7 @@ import {
|
||||
} from "./update-command-config.js";
|
||||
import {
|
||||
completePostCorePluginUpdate,
|
||||
runUpdateFinalizationDoctorInFreshProcess,
|
||||
withPrePluginUpdateDoctorEnv,
|
||||
} from "./update-command-fresh-doctor.js";
|
||||
import {
|
||||
updatePluginsAfterCoreUpdate,
|
||||
@@ -220,48 +220,48 @@ export async function updateFinalizeCommand(opts: UpdateFinalizeOptions): Promis
|
||||
}
|
||||
|
||||
const completedPluginUpdate = await withPluginLifecycleLease({}, async () => {
|
||||
await createUpdateConfigSnapshot();
|
||||
await runUpdateFinalizationDoctorInFreshProcess({
|
||||
phase: "pre-plugin",
|
||||
root,
|
||||
yes: opts.yes === true,
|
||||
json: opts.json === true,
|
||||
timeoutMs: timeoutMs ?? DEFAULT_UPDATE_STEP_TIMEOUT_MS,
|
||||
});
|
||||
configSnapshot = await readConfigFileSnapshot({ skipPluginValidation: true });
|
||||
if (requestedChannel) {
|
||||
configSnapshot = await persistRequestedUpdateChannel({
|
||||
configSnapshot,
|
||||
requestedChannel,
|
||||
const initialPluginUpdate = await withPrePluginUpdateDoctorEnv(async () => {
|
||||
await createUpdateConfigSnapshot();
|
||||
await doctorCommand(defaultRuntime, {
|
||||
nonInteractive: true,
|
||||
repair: true,
|
||||
yes: opts.yes === true,
|
||||
});
|
||||
configSnapshot = await readConfigFileSnapshot({ skipPluginValidation: true });
|
||||
if (requestedChannel) {
|
||||
configSnapshot = await persistRequestedUpdateChannel({
|
||||
configSnapshot,
|
||||
requestedChannel,
|
||||
});
|
||||
}
|
||||
const restoredConfig = restoreDroppedPreUpdateChannels(configSnapshot, preFinalizeConfig);
|
||||
configSnapshot = restoredConfig.snapshot;
|
||||
const postDoctorStoredChannel = configSnapshot.valid
|
||||
? normalizeUpdateChannel(configSnapshot.config.update?.channel)
|
||||
: null;
|
||||
const postDoctorChannel =
|
||||
requestedChannel ??
|
||||
postDoctorStoredChannel ??
|
||||
storedChannel ??
|
||||
effectiveChannel ??
|
||||
DEFAULT_PACKAGE_CHANNEL;
|
||||
const pluginInstallRecords = await loadInstalledPluginIndexInstallRecords();
|
||||
return await updatePluginsAfterCoreUpdate({
|
||||
root,
|
||||
channel: postDoctorChannel,
|
||||
configSnapshot,
|
||||
configChanged: restoredConfig.changed,
|
||||
restoredAuthoredChannels: restoredConfig.authoredChannels,
|
||||
opts: {
|
||||
json: opts.json,
|
||||
timeout: opts.timeout,
|
||||
yes: opts.yes,
|
||||
restart: false,
|
||||
acknowledgeClawHubRisk: opts.acknowledgeClawHubRisk,
|
||||
},
|
||||
timeoutMs: timeoutMs ?? DEFAULT_UPDATE_STEP_TIMEOUT_MS,
|
||||
pluginInstallRecords,
|
||||
});
|
||||
}
|
||||
const restoredConfig = restoreDroppedPreUpdateChannels(configSnapshot, preFinalizeConfig);
|
||||
configSnapshot = restoredConfig.snapshot;
|
||||
const postDoctorStoredChannel = configSnapshot.valid
|
||||
? normalizeUpdateChannel(configSnapshot.config.update?.channel)
|
||||
: null;
|
||||
const postDoctorChannel =
|
||||
requestedChannel ??
|
||||
postDoctorStoredChannel ??
|
||||
storedChannel ??
|
||||
effectiveChannel ??
|
||||
DEFAULT_PACKAGE_CHANNEL;
|
||||
const pluginInstallRecords = await loadInstalledPluginIndexInstallRecords();
|
||||
const initialPluginUpdate = await updatePluginsAfterCoreUpdate({
|
||||
root,
|
||||
channel: postDoctorChannel,
|
||||
configSnapshot,
|
||||
configChanged: restoredConfig.changed,
|
||||
restoredAuthoredChannels: restoredConfig.authoredChannels,
|
||||
opts: {
|
||||
json: opts.json,
|
||||
timeout: opts.timeout,
|
||||
yes: opts.yes,
|
||||
restart: false,
|
||||
acknowledgeClawHubRisk: opts.acknowledgeClawHubRisk,
|
||||
},
|
||||
timeoutMs: timeoutMs ?? DEFAULT_UPDATE_STEP_TIMEOUT_MS,
|
||||
pluginInstallRecords,
|
||||
});
|
||||
return await completePostCorePluginUpdate({
|
||||
root,
|
||||
@@ -579,7 +579,6 @@ export async function continuePostCoreUpdateInFreshProcess(params: {
|
||||
requestedChannel: params.requestedChannel,
|
||||
sourceConfigPath: params.preUpdateConfig ? sourceConfigPath : undefined,
|
||||
});
|
||||
handoffEnv.NODE_OPTIONS = resolveUpdateNodeOptions(handoffEnv.NODE_OPTIONS);
|
||||
const child = spawn(params.nodeRunner ?? resolveNodeRunner(), argv, {
|
||||
stdio: childStdio,
|
||||
env: {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { note } from "../../packages/terminal-core/src/note.js";
|
||||
import { replaceConfigFile, type OpenClawConfig } from "../config/config.js";
|
||||
import { isDefaultInstallIdentity, resolveGatewayPort, resolveIsNixMode } from "../config/paths.js";
|
||||
import { resolveSecretInputRef } from "../config/types.secrets.js";
|
||||
import { formatGatewayHeapLimitReport, inspectGatewayHeapLimit } from "../daemon/gateway-heap.js";
|
||||
import {
|
||||
findExtraGatewayServices,
|
||||
renderGatewayServiceCleanupHints,
|
||||
@@ -40,7 +41,6 @@ import {
|
||||
import type { HealthFinding, HealthRepairEffect } from "../flows/health-checks.js";
|
||||
import { isTruthyEnvValue } from "../infra/env.js";
|
||||
import { NON_DEFAULT_INSTALL_SERVICE_SKIP_REASON } from "../infra/gateway-supervision.js";
|
||||
import { formatGatewayHeapLimitReport, inspectGatewayHeapLimit } from "../infra/node-heap.js";
|
||||
import { readWindowsProcessArgsSync } from "../infra/windows-port-pids.js";
|
||||
import { runExec } from "../process/exec.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
|
||||
@@ -4,8 +4,7 @@ import {
|
||||
formatGatewayHeapLimitReport,
|
||||
inspectGatewayHeapLimit,
|
||||
resolveGatewayHeapNodeOptions,
|
||||
resolveNodeOptionsWithMinimumOldSpaceSize,
|
||||
} from "./node-heap.js";
|
||||
} from "./gateway-heap.js";
|
||||
|
||||
const MIB = 1024 * 1024;
|
||||
|
||||
@@ -115,20 +114,3 @@ describe("Gateway service NODE_OPTIONS", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("minimum Node heap options", () => {
|
||||
it("raises a lower limit without dropping other options", () => {
|
||||
expect(
|
||||
resolveNodeOptionsWithMinimumOldSpaceSize("--trace-warnings --max-old-space-size=1024", 8192),
|
||||
).toBe("--trace-warnings --max-old-space-size=1024 --max-old-space-size=8192");
|
||||
});
|
||||
|
||||
it("preserves a larger explicit limit in any Node-supported spelling", () => {
|
||||
expect(
|
||||
resolveNodeOptionsWithMinimumOldSpaceSize(
|
||||
"--max_old_space_size 12288 --trace-warnings",
|
||||
8192,
|
||||
),
|
||||
).toBe("--max_old_space_size 12288 --trace-warnings");
|
||||
});
|
||||
});
|
||||
@@ -127,18 +127,6 @@ function parseMaxOldSpaceSizeMiB(nodeOptions: string | undefined): number | null
|
||||
return result;
|
||||
}
|
||||
|
||||
export function resolveNodeOptionsWithMinimumOldSpaceSize(
|
||||
nodeOptions: string | undefined,
|
||||
minimumMiB: number,
|
||||
): string {
|
||||
const current = nodeOptions?.trim() ?? "";
|
||||
const existingLimit = parseMaxOldSpaceSizeMiB(current);
|
||||
if (existingLimit !== null && existingLimit >= minimumMiB) {
|
||||
return current;
|
||||
}
|
||||
return [current, `--max-old-space-size=${minimumMiB}`].filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
export function resolveGatewayHeapNodeOptions(
|
||||
existingNodeOptions: string | undefined,
|
||||
memory: GatewayHeapMemoryInputs = {},
|
||||
@@ -4,7 +4,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { resolveAutoNodeExtraCaCerts } from "../bootstrap/node-extra-ca-certs.js";
|
||||
import { inspectGatewayHeapLimit } from "../infra/node-heap.js";
|
||||
import { inspectGatewayHeapLimit } from "./gateway-heap.js";
|
||||
import { resolveGatewayStateDir } from "./paths.js";
|
||||
import {
|
||||
buildNodeServiceEnvironment,
|
||||
|
||||
@@ -4,7 +4,6 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import { resolveNodeStartupTlsEnvironment } from "../bootstrap/node-startup-env.js";
|
||||
import { resolveGatewayHeapNodeOptions } from "../infra/node-heap.js";
|
||||
import {
|
||||
GATEWAY_SERVICE_KIND,
|
||||
GATEWAY_SERVICE_MARKER,
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
resolveGatewayWindowsTaskName,
|
||||
resolveNodeServiceIdentityEnvironment,
|
||||
} from "./constants.js";
|
||||
import { resolveGatewayHeapNodeOptions } from "./gateway-heap.js";
|
||||
import { resolveGatewayStateDir } from "./paths.js";
|
||||
|
||||
type MinimalServicePathOptions = {
|
||||
|
||||
@@ -163,10 +163,13 @@ describe("buildCliRespawnPlan", () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("does not overwrite an existing NODE_EXTRA_CA_CERTS value", () => {
|
||||
it("preserves startup env while applying the update heap budget", () => {
|
||||
const plan = buildCliRespawnPlan({
|
||||
argv: ["node", "openclaw", "status"],
|
||||
env: { NODE_EXTRA_CA_CERTS: "/custom/ca.pem" },
|
||||
argv: ["node", "openclaw", "update", "repair"],
|
||||
env: {
|
||||
NODE_EXTRA_CA_CERTS: "/custom/ca.pem",
|
||||
NODE_OPTIONS: "--trace-warnings --max-old-space-size=1024",
|
||||
},
|
||||
execArgv: [],
|
||||
autoNodeExtraCaCerts: "/etc/ssl/certs/ca-certificates.crt",
|
||||
platform: "linux",
|
||||
@@ -174,6 +177,7 @@ describe("buildCliRespawnPlan", () => {
|
||||
|
||||
const respawnPlan = expectCliRespawnPlan(plan);
|
||||
expect(respawnPlan.env.NODE_EXTRA_CA_CERTS).toBe("/custom/ca.pem");
|
||||
expect(respawnPlan.env.NODE_OPTIONS).toBe("--trace-warnings --max-old-space-size=8192");
|
||||
});
|
||||
|
||||
it("returns null when both respawn guards are already satisfied", () => {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { resolveNodeStartupTlsEnvironment } from "./bootstrap/node-startup-env.js";
|
||||
import { resolveCliArgvInvocation } from "./cli/argv-invocation.js";
|
||||
import {
|
||||
isTerminalInteractiveRespawnArgv,
|
||||
shouldSkipRespawnForArgv,
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
} from "./cli/respawn-policy.js";
|
||||
import { normalizeWindowsArgv } from "./cli/windows-argv.js";
|
||||
import { isTruthyEnvValue } from "./infra/env.js";
|
||||
import { resolveUpdateNodeOptions } from "./infra/update-node-options.js";
|
||||
import { attachChildProcessBridge } from "./process/child-process-bridge.js";
|
||||
import {
|
||||
runRespawnChildWithSignalBridge,
|
||||
@@ -101,6 +103,17 @@ export function buildCliRespawnPlan(
|
||||
const childExecArgv = [...execArgv];
|
||||
let needsRespawn = false;
|
||||
|
||||
const invocation = resolveCliArgvInvocation(normalizedArgv);
|
||||
if (invocation.primary === "update" && invocation.commandPath[1] !== "status") {
|
||||
// Update Doctor may inspect large recovered state before spawning another process.
|
||||
// Apply the existing build heap budget at startup, while V8 can still honor it.
|
||||
const nodeOptions = resolveUpdateNodeOptions(childEnv.NODE_OPTIONS);
|
||||
if (nodeOptions !== childEnv.NODE_OPTIONS) {
|
||||
childEnv.NODE_OPTIONS = nodeOptions;
|
||||
needsRespawn = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (platform === "win32") {
|
||||
if (!hasStackSizeConfigured(childExecArgv)) {
|
||||
childExecArgv.unshift(WINDOWS_STACK_SIZE_FLAG);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// Shared Node heap policy for update builds and updater-owned CLI work.
|
||||
const UPDATE_MAX_OLD_SPACE_MB = 8192;
|
||||
|
||||
export function resolveUpdateNodeOptions(baseOptions: string | undefined): string {
|
||||
const current = baseOptions?.trim() ?? "";
|
||||
const desired = `--max-old-space-size=${UPDATE_MAX_OLD_SPACE_MB}`;
|
||||
const existingMatch = /(?:^|\s)--max-old-space-size=(\d+)(?=\s|$)/.exec(current);
|
||||
if (!existingMatch) {
|
||||
return current ? `${current} ${desired}` : desired;
|
||||
}
|
||||
const existingValue = Number(existingMatch[1]);
|
||||
if (Number.isFinite(existingValue) && existingValue >= UPDATE_MAX_OLD_SPACE_MB) {
|
||||
return current;
|
||||
}
|
||||
return current.replace(/(?:^|\s)--max-old-space-size=\d+(?=\s|$)/, ` ${desired}`).trim();
|
||||
}
|
||||
@@ -120,7 +120,6 @@ describe("runPostCoreFinalizeAfterGatewayUpdate", () => {
|
||||
spawnFinalize,
|
||||
env: {
|
||||
PATH: "/usr/bin",
|
||||
NODE_OPTIONS: "--trace-warnings --max-old-space-size=1024",
|
||||
OPENCLAW_SERVICE_MARKER: "openclaw",
|
||||
OPENCLAW_SERVICE_KIND: "gateway",
|
||||
OPENCLAW_GATEWAY_SERVICE_PID: "4242",
|
||||
@@ -131,9 +130,6 @@ describe("runPostCoreFinalizeAfterGatewayUpdate", () => {
|
||||
"spawnFinalize.mock.calls[0] test invariant",
|
||||
)[0];
|
||||
expect(env.PATH).toBe("/usr/bin");
|
||||
expect(env.NODE_OPTIONS).toBe(
|
||||
"--trace-warnings --max-old-space-size=1024 --max-old-space-size=8192",
|
||||
);
|
||||
expect(env.OPENCLAW_SERVICE_MARKER).toBeUndefined();
|
||||
expect(env.OPENCLAW_SERVICE_KIND).toBeUndefined();
|
||||
expect(env.OPENCLAW_GATEWAY_SERVICE_PID).toBeUndefined();
|
||||
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
buildPostCoreHandoffEnv,
|
||||
type PreUpdateConfigRestoreInput,
|
||||
} from "./update-post-core-context.js";
|
||||
import { resolveUpdateNodeOptions } from "./update-runner-doctor.js";
|
||||
import type { UpdateRunResult } from "./update-runner.js";
|
||||
|
||||
// Whole-process backstop for the finalizer. `update finalize` runs several timed
|
||||
@@ -64,7 +63,6 @@ function buildFinalizeEnv(
|
||||
compatHostVersion,
|
||||
sourceConfigPath,
|
||||
});
|
||||
env.NODE_OPTIONS = resolveUpdateNodeOptions(env.NODE_OPTIONS);
|
||||
delete env.OPENCLAW_SERVICE_MARKER;
|
||||
delete env.OPENCLAW_SERVICE_KIND;
|
||||
delete env[GATEWAY_SERVICE_RUNTIME_PID_ENV];
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { resolveNodeOptionsWithMinimumOldSpaceSize } from "./node-heap.js";
|
||||
import { compareSemverStrings } from "./update-check.js";
|
||||
|
||||
const UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR_ENV =
|
||||
@@ -13,13 +12,6 @@ const UPDATE_PARENT_ALLOWS_GATEWAY_ACTIVATION_ENV =
|
||||
"OPENCLAW_UPDATE_PARENT_ALLOWS_GATEWAY_ACTIVATION";
|
||||
const UPDATE_DOCTOR_SERVICE_REPAIR_POLICY_ENV = "OPENCLAW_SERVICE_REPAIR_POLICY";
|
||||
const EXTERNAL_SERVICE_REPAIR_POLICY_MIN_VERSION = "2026.4.25-beta.1";
|
||||
const UPDATE_MIN_OLD_SPACE_MIB = 8192;
|
||||
|
||||
export function resolveUpdateNodeOptions(nodeOptions: string | undefined): string {
|
||||
// Source builds and post-core Doctor/finalizer processes share one update budget. Keep larger
|
||||
// operator limits, but never let a fresh handoff fall back to V8's much smaller default.
|
||||
return resolveNodeOptionsWithMinimumOldSpaceSize(nodeOptions, UPDATE_MIN_OLD_SPACE_MIB);
|
||||
}
|
||||
|
||||
export function resolveUpdateDoctorExecutionPolicy(params: {
|
||||
targetVersion: string | null;
|
||||
@@ -45,10 +37,8 @@ export function buildUpdateDoctorEnv(params: {
|
||||
serviceRepairPolicy?: "external";
|
||||
deferConfiguredPluginInstallRepair?: boolean;
|
||||
compatibilityHostVersion?: string | null;
|
||||
nodeOptions?: string;
|
||||
}): NodeJS.ProcessEnv {
|
||||
return {
|
||||
NODE_OPTIONS: resolveUpdateNodeOptions(params.nodeOptions ?? process.env.NODE_OPTIONS),
|
||||
OPENCLAW_UPDATE_IN_PROGRESS: "1",
|
||||
...(params.deferConfiguredPluginInstallRepair
|
||||
? { [UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR_ENV]: "1" }
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { DEV_BRANCH } from "./update-channels.js";
|
||||
import { resolveUpdateNodeOptions } from "./update-node-options.js";
|
||||
import {
|
||||
managerInstallIgnoreScriptsArgs,
|
||||
type UpdatePackageManagerFailureReason,
|
||||
} from "./update-package-manager.js";
|
||||
import { resolveUpdateNodeOptions } from "./update-runner-doctor.js";
|
||||
import type { UpdateRunResult, UpdateStepResult } from "./update-runner-types.js";
|
||||
|
||||
const DEV_PREFLIGHT_LINT_ENV: NodeJS.ProcessEnv = {
|
||||
@@ -28,16 +28,12 @@ export function shouldPreferIgnoreScriptsForWindowsPreflight(
|
||||
return process.platform === "win32" && manager === "pnpm";
|
||||
}
|
||||
|
||||
function resolveBuildNodeOptions(baseOptions: string | undefined): string {
|
||||
return resolveUpdateNodeOptions(baseOptions);
|
||||
}
|
||||
|
||||
export function resolveBuildEnv(
|
||||
env?: NodeJS.ProcessEnv,
|
||||
buildCacheRoot?: string,
|
||||
): NodeJS.ProcessEnv | undefined {
|
||||
const currentNodeOptions = env?.NODE_OPTIONS ?? process.env.NODE_OPTIONS;
|
||||
const nextNodeOptions = resolveBuildNodeOptions(currentNodeOptions);
|
||||
const nextNodeOptions = resolveUpdateNodeOptions(currentNodeOptions);
|
||||
if (nextNodeOptions === currentNodeOptions && !buildCacheRoot) {
|
||||
return env;
|
||||
}
|
||||
|
||||
@@ -1366,7 +1366,6 @@ describe("runGatewayUpdate", () => {
|
||||
expect(doctorEnv?.OPENCLAW_UPDATE_PARENT_SUPPORTS_GATEWAY_RESTART).toBe("1");
|
||||
expect(doctorEnv?.OPENCLAW_UPDATE_PARENT_ALLOWS_GATEWAY_SERVICE_REPAIR).toBe("1");
|
||||
expect(doctorEnv?.OPENCLAW_UPDATE_PARENT_ALLOWS_GATEWAY_ACTIVATION).toBe("1");
|
||||
expect(doctorEnv?.NODE_OPTIONS).toBe("--max-old-space-size=8192");
|
||||
});
|
||||
|
||||
it("uses the pre-mutation activation decision for the git update doctor pass", async () => {
|
||||
|
||||
Reference in New Issue
Block a user