mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-02 11:24:55 +00:00
fix(config): do not suppress recovery retry after failed backup restore (#85787)
maybeRecoverSuspiciousConfigRead unconditionally recorded lastObservedSuspiciousSignature in health state even when restoredFromBackup was false (copyFile failed). The guard at resolveConfigReadRecoveryContext then prevented the same signature from ever being retried, permanently accepting the suspicious config on every subsequent launch. Only record the dedup signature when the backup restore actually succeeded.
This commit is contained in:
@@ -454,6 +454,87 @@ describe("config observe recovery", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("retries recovery on next launch after a failed copyFile restore", async () => {
|
||||
await withSuiteHome(async (home) => {
|
||||
const { deps, configPath, auditPath, warn } = makeDeps(home);
|
||||
await seedConfigBackup(configPath, recoverableTelegramConfig);
|
||||
const clobbered = await writeClobberedUpdateChannel(configPath);
|
||||
|
||||
const copyError = Object.assign(new Error("EACCES: permission denied"), { code: "EACCES" });
|
||||
const failingFs: ObserveRecoveryDeps["fs"] = {
|
||||
...deps.fs,
|
||||
promises: {
|
||||
...deps.fs.promises,
|
||||
copyFile: () => Promise.reject(copyError),
|
||||
},
|
||||
};
|
||||
await maybeRecoverSuspiciousConfigRead({
|
||||
deps: { ...deps, fs: failingFs },
|
||||
configPath,
|
||||
raw: clobbered.raw,
|
||||
parsed: clobbered.parsed,
|
||||
});
|
||||
|
||||
expectWarnContaining(warn, "Config auto-restore from backup failed:");
|
||||
const firstEvents = await readObserveEvents(auditPath);
|
||||
expect(firstEvents).toHaveLength(1);
|
||||
expect(firstEvents[0]?.restoredFromBackup).toBe(false);
|
||||
|
||||
const retryResult = await maybeRecoverSuspiciousConfigRead({
|
||||
deps,
|
||||
configPath,
|
||||
raw: clobbered.raw,
|
||||
parsed: clobbered.parsed,
|
||||
});
|
||||
|
||||
expect((retryResult.parsed as { gateway?: { mode?: string } }).gateway?.mode).toBe("local");
|
||||
await expect(fsp.readFile(configPath, "utf-8")).resolves.not.toBe(clobbered.raw);
|
||||
const retryEvents = await readObserveEvents(auditPath);
|
||||
expect(retryEvents).toHaveLength(2);
|
||||
expect(retryEvents[1]?.restoredFromBackup).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("sync recovery retries on next launch after a failed copyFileSync restore", async () => {
|
||||
await withSuiteHome(async (home) => {
|
||||
const { deps, configPath, auditPath, warn } = makeDeps(home);
|
||||
await seedConfigBackup(configPath, recoverableTelegramConfig);
|
||||
const clobbered = await writeClobberedUpdateChannel(configPath);
|
||||
|
||||
const copyError = Object.assign(new Error("EACCES: permission denied"), { code: "EACCES" });
|
||||
const failingFs: ObserveRecoveryDeps["fs"] = {
|
||||
...deps.fs,
|
||||
copyFileSync: () => {
|
||||
throw copyError;
|
||||
},
|
||||
};
|
||||
maybeRecoverSuspiciousConfigReadSync({
|
||||
deps: { ...deps, fs: failingFs },
|
||||
configPath,
|
||||
raw: clobbered.raw,
|
||||
parsed: clobbered.parsed,
|
||||
});
|
||||
|
||||
expectWarnContaining(warn, "Config auto-restore from backup failed:");
|
||||
const firstEvents = await readObserveEvents(auditPath);
|
||||
expect(firstEvents).toHaveLength(1);
|
||||
expect(firstEvents[0]?.restoredFromBackup).toBe(false);
|
||||
|
||||
const retryResult = maybeRecoverSuspiciousConfigReadSync({
|
||||
deps,
|
||||
configPath,
|
||||
raw: clobbered.raw,
|
||||
parsed: clobbered.parsed,
|
||||
});
|
||||
|
||||
expect((retryResult.parsed as { gateway?: { mode?: string } }).gateway?.mode).toBe("local");
|
||||
await expect(fsp.readFile(configPath, "utf-8")).resolves.not.toBe(clobbered.raw);
|
||||
const retryEvents = await readObserveEvents(auditPath);
|
||||
expect(retryEvents).toHaveLength(2);
|
||||
expect(retryEvents[1]?.restoredFromBackup).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("dedupes repeated suspicious hashes", async () => {
|
||||
await withSuiteHome(async (home) => {
|
||||
const { deps, configPath, auditPath } = makeDeps(home);
|
||||
|
||||
@@ -680,12 +680,14 @@ export async function maybeRecoverSuspiciousConfigRead(params: {
|
||||
}),
|
||||
);
|
||||
|
||||
healthState = setConfigHealthEntry(
|
||||
healthState,
|
||||
params.configPath,
|
||||
createLastObservedSuspiciousEntry(entry, suspiciousSignature),
|
||||
);
|
||||
await writeConfigHealthState(params.deps, healthState);
|
||||
if (restoredFromBackup) {
|
||||
healthState = setConfigHealthEntry(
|
||||
healthState,
|
||||
params.configPath,
|
||||
createLastObservedSuspiciousEntry(entry, suspiciousSignature),
|
||||
);
|
||||
await writeConfigHealthState(params.deps, healthState);
|
||||
}
|
||||
return { raw: backupRaw, parsed: backupParsed };
|
||||
}
|
||||
|
||||
@@ -790,12 +792,14 @@ export function maybeRecoverSuspiciousConfigReadSync(params: {
|
||||
}),
|
||||
);
|
||||
|
||||
healthState = setConfigHealthEntry(
|
||||
healthState,
|
||||
params.configPath,
|
||||
createLastObservedSuspiciousEntry(entry, suspiciousSignature),
|
||||
);
|
||||
writeConfigHealthStateSync(params.deps, healthState);
|
||||
if (restoredFromBackup) {
|
||||
healthState = setConfigHealthEntry(
|
||||
healthState,
|
||||
params.configPath,
|
||||
createLastObservedSuspiciousEntry(entry, suspiciousSignature),
|
||||
);
|
||||
writeConfigHealthStateSync(params.deps, healthState);
|
||||
}
|
||||
return { raw: backupRaw, parsed: backupParsed };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user