























@@ -91,6 +91,7 @@ vi.mock("../config/config.js", () => ({
9191},
9292readConfigFileSnapshot: vi.fn(),
9393readSourceConfigBestEffort: vi.fn(),
94+mutateConfigFileWithRetry: vi.fn(),
9495replaceConfigFile: vi.fn(),
9596resolveGatewayPort: vi.fn(() => 18789),
9697}));
@@ -268,7 +269,7 @@ vi.mock("../runtime.js", () => ({
268269const { runGatewayUpdate } = await import("../infra/update-runner.js");
269270const { resolveOpenClawPackageRoot } = await import("../infra/openclaw-root.js");
270271const {
271-ConfigMutationConflictError,
272+mutateConfigFileWithRetry,
272273 readConfigFileSnapshot,
273274 readSourceConfigBestEffort,
274275 replaceConfigFile,
@@ -426,6 +427,31 @@ describe("update-cli", () => {
426427const replaceConfigCall = (index = 0) => vi.mocked(replaceConfigFile).mock.calls[index]?.[0];
427428const lastReplaceConfigCall = () =>
428429replaceConfigCall(vi.mocked(replaceConfigFile).mock.calls.length - 1);
430+const setupConfigMutationWithRetryMock = () => {
431+vi.mocked(mutateConfigFileWithRetry).mockImplementation(async (params) => {
432+const snapshot = await readConfigFileSnapshot();
433+const nextConfig = structuredClone(snapshot.sourceConfig) as OpenClawConfig;
434+await params.mutate(nextConfig, {
435+ snapshot,
436+previousHash: snapshot.hash ?? null,
437+attempt: 0,
438+});
439+await replaceConfigFile({
440+ nextConfig,
441+ ...(snapshot.hash !== undefined ? { baseHash: snapshot.hash } : {}),
442+});
443+return {
444+path: snapshot.path,
445+previousHash: snapshot.hash ?? null,
446+ snapshot,
447+ nextConfig,
448+result: undefined,
449+attempts: 1,
450+afterWrite: { mode: "none", reason: "test" },
451+followUp: { mode: "none", reason: "test", requiresRestart: false },
452+};
453+});
454+};
429455430456const writeJsonCall = (index = 0) => vi.mocked(defaultRuntime.writeJson).mock.calls[index]?.[0];
431457const lastWriteJsonCall = () =>
@@ -562,6 +588,7 @@ describe("update-cli", () => {
562588vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(process.cwd());
563589vi.mocked(readConfigFileSnapshot).mockResolvedValue(baseSnapshot);
564590vi.mocked(readSourceConfigBestEffort).mockResolvedValue(baseSnapshot.config);
591+setupConfigMutationWithRetryMock();
565592vi.mocked(fetchNpmTagVersion).mockResolvedValue({
566593tag: "latest",
567594version: "9999.0.0",
@@ -1118,47 +1145,57 @@ describe("update-cli", () => {
11181145});
1119114611201147it("post-core resume mode retries update channel persistence after config hash drift", async () => {
1121-vi.mocked(readConfigFileSnapshot)
1122-.mockResolvedValueOnce({
1123- ...baseSnapshot,
1124-parsed: { update: { channel: "stable" } },
1125-resolved: { update: { channel: "stable" } } as OpenClawConfig,
1126-sourceConfig: { update: { channel: "stable" } } as OpenClawConfig,
1127-runtimeConfig: { update: { channel: "stable" } } as OpenClawConfig,
1128-config: { update: { channel: "stable" } } as OpenClawConfig,
1129-hash: "stable-hash",
1130-})
1131-.mockResolvedValueOnce({
1132- ...baseSnapshot,
1133-parsed: {
1134-meta: { lastTouchedVersion: "2026.4.30" },
1135-update: { channel: "stable" },
1136-},
1137-resolved: {
1138-meta: { lastTouchedVersion: "2026.4.30" },
1139-update: { channel: "stable" },
1140-} as OpenClawConfig,
1141-sourceConfig: {
1142-meta: { lastTouchedVersion: "2026.4.30" },
1143-update: { channel: "stable" },
1144-} as OpenClawConfig,
1145-runtimeConfig: {
1146-meta: { lastTouchedVersion: "2026.4.30" },
1147-update: { channel: "stable" },
1148-} as OpenClawConfig,
1149-config: {
1150-meta: { lastTouchedVersion: "2026.4.30" },
1151-update: { channel: "stable" },
1152-} as OpenClawConfig,
1153-hash: "newer-hash",
1148+vi.mocked(readConfigFileSnapshot).mockResolvedValueOnce({
1149+ ...baseSnapshot,
1150+parsed: { update: { channel: "stable" } },
1151+resolved: { update: { channel: "stable" } } as OpenClawConfig,
1152+sourceConfig: { update: { channel: "stable" } } as OpenClawConfig,
1153+runtimeConfig: { update: { channel: "stable" } } as OpenClawConfig,
1154+config: { update: { channel: "stable" } } as OpenClawConfig,
1155+hash: "stable-hash",
1156+});
1157+const newerSnapshot = {
1158+ ...baseSnapshot,
1159+parsed: {
1160+meta: { lastTouchedVersion: "2026.4.30" },
1161+update: { channel: "stable" },
1162+},
1163+resolved: {
1164+meta: { lastTouchedVersion: "2026.4.30" },
1165+update: { channel: "stable" },
1166+} as OpenClawConfig,
1167+sourceConfig: {
1168+meta: { lastTouchedVersion: "2026.4.30" },
1169+update: { channel: "stable" },
1170+} as OpenClawConfig,
1171+runtimeConfig: {
1172+meta: { lastTouchedVersion: "2026.4.30" },
1173+update: { channel: "stable" },
1174+} as OpenClawConfig,
1175+config: {
1176+meta: { lastTouchedVersion: "2026.4.30" },
1177+update: { channel: "stable" },
1178+} as OpenClawConfig,
1179+hash: "newer-hash",
1180+};
1181+vi.mocked(mutateConfigFileWithRetry).mockImplementationOnce(async (params) => {
1182+const nextConfig = structuredClone(newerSnapshot.sourceConfig);
1183+await params.mutate(nextConfig, {
1184+snapshot: newerSnapshot,
1185+previousHash: newerSnapshot.hash,
1186+attempt: 1,
11541187});
1155-vi.mocked(replaceConfigFile)
1156-.mockRejectedValueOnce(
1157-new ConfigMutationConflictError("config changed since last load", {
1158-currentHash: "newer-hash",
1159-}),
1160-)
1161-.mockResolvedValueOnce({} as Awaited<ReturnType<typeof replaceConfigFile>>);
1188+return {
1189+path: newerSnapshot.path,
1190+previousHash: newerSnapshot.hash,
1191+snapshot: newerSnapshot,
1192+ nextConfig,
1193+result: undefined,
1194+attempts: 2,
1195+afterWrite: { mode: "none", reason: "test" },
1196+followUp: { mode: "none", reason: "test", requiresRestart: false },
1197+};
1198+});
1162119911631200await withEnvAsync(
11641201{
@@ -1171,14 +1208,7 @@ describe("update-cli", () => {
11711208},
11721209);
117312101174-expect(replaceConfigFile).toHaveBeenCalledTimes(2);
1175-expect(replaceConfigFile).toHaveBeenLastCalledWith({
1176-nextConfig: {
1177-meta: { lastTouchedVersion: "2026.4.30" },
1178-update: { channel: "dev" },
1179-},
1180-baseHash: "newer-hash",
1181-});
1211+expect(mutateConfigFileWithRetry).toHaveBeenCalledTimes(1);
11821212expect(syncPluginCall()?.config?.meta?.lastTouchedVersion).toBe("2026.4.30");
11831213expect(syncPluginCall()?.config?.update?.channel).toBe("dev");
11841214});
@@ -2717,6 +2747,16 @@ describe("update-cli", () => {
27172747},
27182748],
27192749})
2750+.mockResolvedValueOnce({
2751+ ...baseSnapshot,
2752+parsed: migratedConfig,
2753+resolved: migratedConfig,
2754+sourceConfig: migratedConfig,
2755+config: migratedConfig,
2756+runtimeConfig: migratedConfig,
2757+valid: true,
2758+hash: "migrated-hash",
2759+})
27202760.mockResolvedValueOnce({
27212761 ...baseSnapshot,
27222762parsed: migratedConfig,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。