






























@@ -12,6 +12,12 @@ import type { PluginInstallRecord } from "../config/types.plugins.js";
1212import { GATEWAY_SERVICE_RUNTIME_PID_ENV } from "../daemon/constants.js";
1313import { writePackageDistInventory } from "../infra/package-dist-inventory.js";
1414import { isBetaTag } from "../infra/update-channels.js";
15+import {
16+createDeferredConfiguredPluginRepairDoctorResult,
17+UPDATE_POST_INSTALL_DOCTOR_ADVISORY_EXIT_CODE,
18+UPDATE_POST_INSTALL_DOCTOR_RESULT_PATH_ENV,
19+writeUpdatePostInstallDoctorResult,
20+} from "../infra/update-doctor-result.js";
1521import type { UpdateRunResult } from "../infra/update-runner.js";
1622import { withEnvAsync } from "../test-utils/env.js";
1723import { VERSION } from "../version.js";
@@ -2633,6 +2639,9 @@ describe("update-cli", () => {
26332639);
26342640await fs.writeFile(entryPath, "export {};\n", "utf-8");
26352641await writePackageDistInventory(pkgRoot);
2642+readPackageVersion.mockImplementation(async (packageRoot: string) =>
2643+packageRoot === pkgRoot ? "2026.4.21" : "1.0.0",
2644+);
26362645pathExists.mockImplementation(async (candidate: string) => {
26372646try {
26382647await fs.access(candidate);
@@ -2672,6 +2681,165 @@ describe("update-cli", () => {
26722681).toBe("1");
26732682});
267426832684+it("continues package post-core work for explicit post-update doctor advisories", async () => {
2685+const tempDir = await createTrackedTempDir("openclaw-update-package-doctor-warning-");
2686+const nodeModules = path.join(tempDir, "node_modules");
2687+const pkgRoot = path.join(nodeModules, "openclaw");
2688+const entryPath = path.join(pkgRoot, "dist", "index.js");
2689+mockPackageInstallStatus(pkgRoot);
2690+await fs.mkdir(path.dirname(entryPath), { recursive: true });
2691+await fs.writeFile(
2692+path.join(pkgRoot, "package.json"),
2693+JSON.stringify({ name: "openclaw", version: "2026.4.21" }),
2694+"utf-8",
2695+);
2696+await fs.writeFile(entryPath, "export {};\n", "utf-8");
2697+await writePackageDistInventory(pkgRoot);
2698+pathExists.mockImplementation(async (candidate: string) => {
2699+try {
2700+await fs.access(candidate);
2701+return true;
2702+} catch {
2703+return false;
2704+}
2705+});
2706+vi.mocked(runCommandWithTimeout).mockImplementation(async (argv, options) => {
2707+if (Array.isArray(argv) && argv[0] === "npm" && argv[1] === "root" && argv[2] === "-g") {
2708+return {
2709+stdout: `${nodeModules}\n`,
2710+stderr: "",
2711+code: 0,
2712+signal: null,
2713+killed: false,
2714+termination: "exit",
2715+};
2716+}
2717+if (Array.isArray(argv) && argv[1] === entryPath && argv[2] === "doctor") {
2718+const env = options && typeof options !== "number" ? options.env : undefined;
2719+const resultPath = env?.[UPDATE_POST_INSTALL_DOCTOR_RESULT_PATH_ENV];
2720+if (!resultPath) {
2721+throw new Error("missing doctor result path");
2722+}
2723+await writeUpdatePostInstallDoctorResult({
2724+ resultPath,
2725+result: createDeferredConfiguredPluginRepairDoctorResult([
2726+"deferred configured plugin repair",
2727+]),
2728+});
2729+return {
2730+stdout: "",
2731+stderr: "doctor deferred configured plugin repair",
2732+code: UPDATE_POST_INSTALL_DOCTOR_ADVISORY_EXIT_CODE,
2733+signal: null,
2734+killed: false,
2735+termination: "exit",
2736+};
2737+}
2738+return {
2739+stdout: "",
2740+stderr: "",
2741+code: 0,
2742+signal: null,
2743+killed: false,
2744+termination: "exit",
2745+};
2746+});
2747+2748+await updateCommand({ yes: true, restart: false, json: true });
2749+2750+const doctorCall = doctorCommandCall();
2751+expect(doctorCall?.[0].slice(1)).toEqual([entryPath, "doctor", "--non-interactive", "--fix"]);
2752+const postCoreCall = spawnCall();
2753+expect(postCoreCall?.[0]).toMatch(/node/);
2754+expect(postCoreCall?.[1]).toEqual([entryPath, "update", "--json", "--no-restart", "--yes"]);
2755+expect(postCoreCall?.[2]?.env?.OPENCLAW_UPDATE_POST_CORE).toBe("1");
2756+expect(updateNpmInstalledPlugins).not.toHaveBeenCalled();
2757+expect(defaultRuntime.exit).not.toHaveBeenCalledWith(1);
2758+const jsonOutput = lastWriteJsonCall() as UpdateRunResult | undefined;
2759+const doctorStep = jsonOutput?.steps.find((step) => step.name === "openclaw doctor");
2760+expect(jsonOutput?.status).toBe("ok");
2761+expect(doctorStep?.exitCode).toBe(UPDATE_POST_INSTALL_DOCTOR_ADVISORY_EXIT_CODE);
2762+expect(doctorStep?.advisory).toEqual({
2763+kind: "package-post-install-doctor",
2764+message: expect.stringContaining("recoverable update-time repair warning"),
2765+});
2766+expect(doctorStep?.advisory?.message).not.toContain("gateway restart");
2767+expect(doctorStep?.stderrTail).toContain("doctor deferred configured plugin repair");
2768+expect(doctorStep?.stderrTail).toContain("deferred configured plugin repair");
2769+});
2770+2771+it("fails package updates when the post-update doctor is killed after verification", async () => {
2772+const tempDir = await createTrackedTempDir("openclaw-update-package-doctor-timeout-");
2773+const nodeModules = path.join(tempDir, "node_modules");
2774+const pkgRoot = path.join(nodeModules, "openclaw");
2775+const entryPath = path.join(pkgRoot, "dist", "index.js");
2776+mockPackageInstallStatus(pkgRoot);
2777+await fs.mkdir(path.dirname(entryPath), { recursive: true });
2778+await fs.writeFile(
2779+path.join(pkgRoot, "package.json"),
2780+JSON.stringify({ name: "openclaw", version: "2026.4.21" }),
2781+"utf-8",
2782+);
2783+await fs.writeFile(entryPath, "export {};\n", "utf-8");
2784+await writePackageDistInventory(pkgRoot);
2785+pathExists.mockImplementation(async (candidate: string) => {
2786+try {
2787+await fs.access(candidate);
2788+return true;
2789+} catch {
2790+return false;
2791+}
2792+});
2793+vi.mocked(runCommandWithTimeout).mockImplementation(async (argv) => {
2794+if (Array.isArray(argv) && argv[0] === "npm" && argv[1] === "root" && argv[2] === "-g") {
2795+return {
2796+stdout: `${nodeModules}\n`,
2797+stderr: "",
2798+code: 0,
2799+signal: null,
2800+killed: false,
2801+termination: "exit",
2802+};
2803+}
2804+if (Array.isArray(argv) && argv[1] === entryPath && argv[2] === "doctor") {
2805+return {
2806+stdout: "",
2807+stderr: "doctor timed out",
2808+code: 124,
2809+signal: null,
2810+killed: true,
2811+termination: "timeout",
2812+};
2813+}
2814+return {
2815+stdout: "",
2816+stderr: "",
2817+code: 0,
2818+signal: null,
2819+killed: false,
2820+termination: "exit",
2821+};
2822+});
2823+2824+await updateCommand({ yes: true, restart: false, json: true });
2825+2826+const doctorCall = doctorCommandCall();
2827+expect(doctorCall?.[0].slice(1)).toEqual([entryPath, "doctor", "--non-interactive", "--fix"]);
2828+expect(spawn).not.toHaveBeenCalled();
2829+expect(defaultRuntime.exit).toHaveBeenCalledWith(1);
2830+const jsonOutput = lastWriteJsonCall() as UpdateRunResult | undefined;
2831+const doctorStep = jsonOutput?.steps.find((step) => step.name === "openclaw doctor");
2832+expect(doctorStep?.exitCode).toBe(124);
2833+expect(doctorStep?.advisory).toBeUndefined();
2834+expect(doctorStep?.termination).toBe("timeout");
2835+expect(
2836+vi
2837+.mocked(defaultRuntime.log)
2838+.mock.calls.map((call) => String(call[0]))
2839+.join("\n"),
2840+).not.toContain("Post-install doctor failed after the package install was verified");
2841+});
2842+26752843it("runs package post-update doctor from the verified package root after a staged swap", async () => {
26762844const tempDir = await createTrackedTempDir("openclaw-update-staged-doctor-");
26772845const nodeModules = path.join(tempDir, "lib", "node_modules");
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。