

























@@ -61,6 +61,15 @@ vi.mock("../infra/openclaw-root.js", () => ({
6161}));
62626363vi.mock("../config/config.js", () => ({
64+ConfigMutationConflictError: class ConfigMutationConflictError extends Error {
65+readonly currentHash: string | null;
66+67+constructor(message: string, params: { currentHash: string | null }) {
68+super(message);
69+this.name = "ConfigMutationConflictError";
70+this.currentHash = params.currentHash;
71+}
72+},
6473readConfigFileSnapshot: vi.fn(),
6574replaceConfigFile: vi.fn(),
6675resolveGatewayPort: vi.fn(() => 18789),
@@ -226,7 +235,8 @@ vi.mock("../runtime.js", () => ({
226235227236const { runGatewayUpdate } = await import("../infra/update-runner.js");
228237const { resolveOpenClawPackageRoot } = await import("../infra/openclaw-root.js");
229-const { readConfigFileSnapshot, replaceConfigFile } = await import("../config/config.js");
238+const { ConfigMutationConflictError, readConfigFileSnapshot, replaceConfigFile } =
239+await import("../config/config.js");
230240const { checkUpdateStatus, fetchNpmPackageTargetStatus, fetchNpmTagVersion, resolveNpmChannelTag } =
231241await import("../infra/update-check.js");
232242const { runCommandWithTimeout } = await import("../process/exec.js");
@@ -757,6 +767,78 @@ describe("update-cli", () => {
757767);
758768});
759769770+it("post-core resume mode retries update channel persistence after config hash drift", async () => {
771+vi.mocked(readConfigFileSnapshot)
772+.mockResolvedValueOnce({
773+ ...baseSnapshot,
774+parsed: { update: { channel: "stable" } },
775+resolved: { update: { channel: "stable" } } as OpenClawConfig,
776+sourceConfig: { update: { channel: "stable" } } as OpenClawConfig,
777+runtimeConfig: { update: { channel: "stable" } } as OpenClawConfig,
778+config: { update: { channel: "stable" } } as OpenClawConfig,
779+hash: "stable-hash",
780+})
781+.mockResolvedValueOnce({
782+ ...baseSnapshot,
783+parsed: {
784+meta: { lastTouchedVersion: "2026.4.30" },
785+update: { channel: "stable" },
786+},
787+resolved: {
788+meta: { lastTouchedVersion: "2026.4.30" },
789+update: { channel: "stable" },
790+} as OpenClawConfig,
791+sourceConfig: {
792+meta: { lastTouchedVersion: "2026.4.30" },
793+update: { channel: "stable" },
794+} as OpenClawConfig,
795+runtimeConfig: {
796+meta: { lastTouchedVersion: "2026.4.30" },
797+update: { channel: "stable" },
798+} as OpenClawConfig,
799+config: {
800+meta: { lastTouchedVersion: "2026.4.30" },
801+update: { channel: "stable" },
802+} as OpenClawConfig,
803+hash: "newer-hash",
804+});
805+vi.mocked(replaceConfigFile)
806+.mockRejectedValueOnce(
807+new ConfigMutationConflictError("config changed since last load", {
808+currentHash: "newer-hash",
809+}),
810+)
811+.mockResolvedValueOnce({} as Awaited<ReturnType<typeof replaceConfigFile>>);
812+813+await withEnvAsync(
814+{
815+OPENCLAW_UPDATE_POST_CORE: "1",
816+OPENCLAW_UPDATE_POST_CORE_CHANNEL: "dev",
817+OPENCLAW_UPDATE_POST_CORE_REQUESTED_CHANNEL: "dev",
818+},
819+async () => {
820+await updateCommand({ restart: false });
821+},
822+);
823+824+expect(replaceConfigFile).toHaveBeenCalledTimes(2);
825+expect(replaceConfigFile).toHaveBeenLastCalledWith({
826+nextConfig: {
827+meta: { lastTouchedVersion: "2026.4.30" },
828+update: { channel: "dev" },
829+},
830+baseHash: "newer-hash",
831+});
832+expect(syncPluginsForUpdateChannel).toHaveBeenCalledWith(
833+expect.objectContaining({
834+config: expect.objectContaining({
835+meta: expect.objectContaining({ lastTouchedVersion: "2026.4.30" }),
836+update: expect.objectContaining({ channel: "dev" }),
837+}),
838+}),
839+);
840+});
841+760842it("passes the update timeout budget into post-core plugin updates", async () => {
761843await withEnvAsync(
762844{
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。