Run doctor after global gateway updates

This commit is contained in:
Steven Chou
2026-05-03 12:06:56 +08:00
committed by Peter Steinberger
parent 74f9a2aedd
commit fa533101d8
3 changed files with 112 additions and 0 deletions

View File

@@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Gateway/update: run `doctor --non-interactive --fix` after Control UI global package updates before reporting success, so legacy config is migrated before the gateway restart. Thanks @stevenchouai.
- Active Memory: apply `setupGraceTimeoutMs` to the embedded recall runner as well as the outer prompt-build watchdog, so very-cold first recalls keep the configured setup grace end-to-end. (#74480) Thanks @volcano303.
- CLI/config: keep JSON dry-run patches validating touched channel configuration against bundled channel schemas even when the patch only contains SecretRef objects.
- Plugins/tools: keep disabled bundled tool plugins out of explicit runtime allowlist ownership and fall back from loaded-but-empty channel registries to tool-bearing plugin registries, so Active Memory can use bundled `memory-core` search/get tools even when `memory-lancedb` is disabled. Fixes #76603. Thanks @jwong-art.

View File

@@ -265,6 +265,14 @@ describe("runGatewayUpdate", () => {
}
}
async function writeGatewayEntrypoint(pkgRoot: string) {
const entrypoint = path.join(pkgRoot, "dist", "index.js");
await fs.mkdir(path.dirname(entrypoint), { recursive: true });
await fs.writeFile(entrypoint, "export {};\n", "utf-8");
await writePackageDistInventory(pkgRoot);
return entrypoint;
}
async function createGlobalPackageFixture(rootDir: string) {
const nodeModules = path.join(rootDir, "node_modules");
const pkgRoot = path.join(nodeModules, "openclaw");
@@ -1456,6 +1464,87 @@ describe("runGatewayUpdate", () => {
);
});
it("runs doctor after global npm updates before reporting success", async () => {
const nodeModules = path.join(tempDir, "node_modules");
const pkgRoot = path.join(nodeModules, "openclaw");
await seedGlobalPackageRoot(pkgRoot);
let doctorEnv: NodeJS.ProcessEnv | undefined;
const { calls, runCommand } = createGlobalInstallHarness({
pkgRoot,
npmRootOutput: nodeModules,
installCommand: "npm i -g openclaw@latest --no-fund --no-audit --loglevel=error",
onInstall: async () => {
await writeGlobalPackageVersion(pkgRoot);
await writeGatewayEntrypoint(pkgRoot);
},
});
const doctorNodePath = await resolveStableNodePath(process.execPath);
const doctorCommand = `${doctorNodePath} ${path.join(
pkgRoot,
"dist",
"index.js",
)} doctor --non-interactive --fix`;
const runCommandWithDoctor = async (argv: string[], options?: { env?: NodeJS.ProcessEnv }) => {
const key = argv.join(" ");
if (key === doctorCommand) {
calls.push(key);
doctorEnv = options?.env;
return { stdout: "doctor repaired config", stderr: "", code: 0 };
}
return runCommand(argv, options);
};
const result = await runWithCommand(runCommandWithDoctor, { cwd: pkgRoot });
expect(result.status).toBe("ok");
expect(calls).toContain(doctorCommand);
expect(result.steps.map((step) => step.name)).toContain("openclaw doctor");
expect(doctorEnv?.OPENCLAW_UPDATE_IN_PROGRESS).toBe("1");
expect(doctorEnv?.OPENCLAW_UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE).toBe("1");
});
it("fails global npm updates when post-update doctor fails", async () => {
const nodeModules = path.join(tempDir, "node_modules");
const pkgRoot = path.join(nodeModules, "openclaw");
await seedGlobalPackageRoot(pkgRoot);
const { calls, runCommand } = createGlobalInstallHarness({
pkgRoot,
npmRootOutput: nodeModules,
installCommand: "npm i -g openclaw@latest --no-fund --no-audit --loglevel=error",
onInstall: async () => {
await writeGlobalPackageVersion(pkgRoot);
await writeGatewayEntrypoint(pkgRoot);
},
});
const doctorNodePath = await resolveStableNodePath(process.execPath);
const doctorCommand = `${doctorNodePath} ${path.join(
pkgRoot,
"dist",
"index.js",
)} doctor --non-interactive --fix`;
const runCommandWithDoctor = async (argv: string[], options?: { env?: NodeJS.ProcessEnv }) => {
const key = argv.join(" ");
if (key === doctorCommand) {
calls.push(key);
return { stdout: "", stderr: "doctor refused migration", code: 1 };
}
return runCommand(argv, options);
};
const result = await runWithCommand(runCommandWithDoctor, { cwd: pkgRoot });
expect(result.status).toBe("error");
expect(result.reason).toBe("doctor-failed");
expect(calls).toContain(doctorCommand);
expect(result.steps.at(-1)).toMatchObject({
name: "openclaw doctor",
exitCode: 1,
stderrTail: "doctor refused migration",
});
});
it("falls back to global npm update when git is missing from PATH", async () => {
const { nodeModules, pkgRoot } = await createGlobalPackageFixture(tempDir);
const { calls, runCommand } = createGlobalInstallHarness({

View File

@@ -1,6 +1,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { resolveGatewayInstallEntrypoint } from "../daemon/gateway-entrypoint.js";
import { type CommandOptions, runCommandWithTimeout } from "../process/exec.js";
import {
resolveControlUiDistIndexHealth,
@@ -1443,6 +1444,27 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<
stepIndex: 0,
totalSteps: 1,
}),
postVerifyStep: async (verifiedPackageRoot) => {
const doctorEntry = await resolveGatewayInstallEntrypoint(verifiedPackageRoot);
if (!doctorEntry) {
return null;
}
const doctorNodePath = await resolveStableNodePath(process.execPath);
return await runStep({
runCommand,
name: "openclaw doctor",
argv: [doctorNodePath, doctorEntry, "doctor", "--non-interactive", "--fix"],
cwd: verifiedPackageRoot,
timeoutMs,
env: {
OPENCLAW_UPDATE_IN_PROGRESS: "1",
[UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV]: "1",
},
progress,
stepIndex: 0,
totalSteps: 1,
});
},
});
return {
status: packageUpdate.failedStep ? "error" : "ok",