




















@@ -454,6 +454,87 @@ describe("config observe recovery", () => {
454454});
455455});
456456457+it("retries recovery on next launch after a failed copyFile restore", async () => {
458+await withSuiteHome(async (home) => {
459+const { deps, configPath, auditPath, warn } = makeDeps(home);
460+await seedConfigBackup(configPath, recoverableTelegramConfig);
461+const clobbered = await writeClobberedUpdateChannel(configPath);
462+463+const copyError = Object.assign(new Error("EACCES: permission denied"), { code: "EACCES" });
464+const failingFs: ObserveRecoveryDeps["fs"] = {
465+ ...deps.fs,
466+promises: {
467+ ...deps.fs.promises,
468+copyFile: () => Promise.reject(copyError),
469+},
470+};
471+await maybeRecoverSuspiciousConfigRead({
472+deps: { ...deps, fs: failingFs },
473+ configPath,
474+raw: clobbered.raw,
475+parsed: clobbered.parsed,
476+});
477+478+expectWarnContaining(warn, "Config auto-restore from backup failed:");
479+const firstEvents = await readObserveEvents(auditPath);
480+expect(firstEvents).toHaveLength(1);
481+expect(firstEvents[0]?.restoredFromBackup).toBe(false);
482+483+const retryResult = await maybeRecoverSuspiciousConfigRead({
484+ deps,
485+ configPath,
486+raw: clobbered.raw,
487+parsed: clobbered.parsed,
488+});
489+490+expect((retryResult.parsed as { gateway?: { mode?: string } }).gateway?.mode).toBe("local");
491+await expect(fsp.readFile(configPath, "utf-8")).resolves.not.toBe(clobbered.raw);
492+const retryEvents = await readObserveEvents(auditPath);
493+expect(retryEvents).toHaveLength(2);
494+expect(retryEvents[1]?.restoredFromBackup).toBe(true);
495+});
496+});
497+498+it("sync recovery retries on next launch after a failed copyFileSync restore", async () => {
499+await withSuiteHome(async (home) => {
500+const { deps, configPath, auditPath, warn } = makeDeps(home);
501+await seedConfigBackup(configPath, recoverableTelegramConfig);
502+const clobbered = await writeClobberedUpdateChannel(configPath);
503+504+const copyError = Object.assign(new Error("EACCES: permission denied"), { code: "EACCES" });
505+const failingFs: ObserveRecoveryDeps["fs"] = {
506+ ...deps.fs,
507+copyFileSync: () => {
508+throw copyError;
509+},
510+};
511+maybeRecoverSuspiciousConfigReadSync({
512+deps: { ...deps, fs: failingFs },
513+ configPath,
514+raw: clobbered.raw,
515+parsed: clobbered.parsed,
516+});
517+518+expectWarnContaining(warn, "Config auto-restore from backup failed:");
519+const firstEvents = await readObserveEvents(auditPath);
520+expect(firstEvents).toHaveLength(1);
521+expect(firstEvents[0]?.restoredFromBackup).toBe(false);
522+523+const retryResult = maybeRecoverSuspiciousConfigReadSync({
524+ deps,
525+ configPath,
526+raw: clobbered.raw,
527+parsed: clobbered.parsed,
528+});
529+530+expect((retryResult.parsed as { gateway?: { mode?: string } }).gateway?.mode).toBe("local");
531+await expect(fsp.readFile(configPath, "utf-8")).resolves.not.toBe(clobbered.raw);
532+const retryEvents = await readObserveEvents(auditPath);
533+expect(retryEvents).toHaveLength(2);
534+expect(retryEvents[1]?.restoredFromBackup).toBe(true);
535+});
536+});
537+457538it("dedupes repeated suspicious hashes", async () => {
458539await withSuiteHome(async (home) => {
459540const { deps, configPath, auditPath } = makeDeps(home);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。