mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-02 04:44:54 +00:00
fix(update): mandatory post-core plugin convergence before gateway restart
Summary:
- validate active plugin payloads, including openclaw.extensions entry files, after core package updates
- treat corrupt active install records without installPath as convergence failures
- prevent managed gateway recovery restart when post-core plugin convergence fails
Verification:
- CI=true pnpm test src/cli/update-cli/plugin-payload-validation.test.ts src/cli/update-cli/post-core-plugin-convergence.test.ts src/cli/update-cli.test.ts src/commands/doctor/shared/missing-configured-plugin-install.test.ts src/commands/doctor/shared/update-phase.test.ts
- CI=true pnpm check:changed
- PR checks green for 2afa84dffe
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
resolveGatewayInstallEntrypoint,
|
||||
} from "../../daemon/gateway-entrypoint.js";
|
||||
import {
|
||||
buildInvalidConfigPostCoreUpdateResult,
|
||||
collectMissingPluginInstallPayloads,
|
||||
recoverInstalledLaunchAgentAfterUpdate,
|
||||
recoverLaunchAgentAndRecheckGatewayHealth,
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
shouldPrepareUpdatedInstallRestart,
|
||||
resolveUpdatedGatewayRestartPort,
|
||||
shouldUseLegacyProcessRestartAfterUpdate,
|
||||
updatePluginsAfterCoreUpdate,
|
||||
} from "./update-command.js";
|
||||
|
||||
describe("resolveGatewayInstallEntrypointCandidates", () => {
|
||||
@@ -558,3 +560,62 @@ describe("resolvePostCoreUpdateChildStdio", () => {
|
||||
expect(resolvePostCoreUpdateChildStdio("darwin")).toBe("inherit");
|
||||
});
|
||||
});
|
||||
|
||||
describe("updatePluginsAfterCoreUpdate (invalid config end-to-end)", () => {
|
||||
it("returns status:error (not skipped) when configSnapshot is invalid, so the pre-restart gate fires", async () => {
|
||||
// The pre-restart gate in `updateCommand` is literally
|
||||
// if (postCorePluginUpdate?.status === "error") { exit(1) }
|
||||
// so asserting that this function returns status:"error" on invalid
|
||||
// config is sufficient to prove the gate fires end-to-end. We pass
|
||||
// `json: true` to suppress logging side-effects without mocking.
|
||||
const result = await updatePluginsAfterCoreUpdate({
|
||||
root: "/tmp/openclaw-test",
|
||||
channel: "stable",
|
||||
configSnapshot: {
|
||||
valid: false,
|
||||
issues: [],
|
||||
legacyIssues: [],
|
||||
} as unknown as Awaited<
|
||||
ReturnType<typeof import("../../config/io.js").readConfigFileSnapshot>
|
||||
>,
|
||||
opts: { json: true } as never,
|
||||
timeoutMs: 1000,
|
||||
});
|
||||
expect(result.status).toBe("error");
|
||||
expect(result.reason).toBe("invalid-config");
|
||||
expect(result.changed).toBe(false);
|
||||
expect(result.warnings).toEqual([
|
||||
expect.objectContaining({
|
||||
reason: "invalid-config",
|
||||
guidance: expect.arrayContaining([expect.stringContaining("openclaw doctor")]),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildInvalidConfigPostCoreUpdateResult", () => {
|
||||
it("returns status:error so the existing pre-restart gate exits 1 instead of restarting on invalid config", () => {
|
||||
const built = buildInvalidConfigPostCoreUpdateResult();
|
||||
expect(built.result.status).toBe("error");
|
||||
expect(built.result.reason).toBe("invalid-config");
|
||||
expect(built.result.changed).toBe(false);
|
||||
});
|
||||
|
||||
it("surfaces actionable repair guidance in both the structural warnings and the message string", () => {
|
||||
const built = buildInvalidConfigPostCoreUpdateResult();
|
||||
expect(built.guidance).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining("openclaw doctor"),
|
||||
expect.stringContaining("openclaw update"),
|
||||
]),
|
||||
);
|
||||
expect(built.result.warnings).toEqual([
|
||||
{
|
||||
reason: "invalid-config",
|
||||
message: built.message,
|
||||
guidance: built.guidance,
|
||||
},
|
||||
]);
|
||||
expect(built.message).toMatch(/refusing to restart/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user