From 360953cb49320dc63da9d87405f0c00612f318b5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 20 Apr 2026 16:04:13 +0100 Subject: [PATCH] test: share config observe recovery helpers --- src/config/io.observe-recovery.test.ts | 136 +++++++++++++------------ 1 file changed, 72 insertions(+), 64 deletions(-) diff --git a/src/config/io.observe-recovery.test.ts b/src/config/io.observe-recovery.test.ts index cd74942d19c..5b0dba8ce26 100644 --- a/src/config/io.observe-recovery.test.ts +++ b/src/config/io.observe-recovery.test.ts @@ -17,6 +17,13 @@ import type { ConfigFileSnapshot } from "./types.js"; describe("config observe recovery", () => { let fixtureRoot = ""; let homeCaseId = 0; + const clobberedUpdateChannelConfig = { update: { channel: "beta" } }; + const clobberedUpdateChannelRaw = `${JSON.stringify(clobberedUpdateChannelConfig, null, 2)}\n`; + const recoverableTelegramConfig = { + update: { channel: "beta" }, + gateway: { mode: "local" }, + channels: { telegram: { enabled: true, dmPolicy: "pairing", groupPolicy: "allowlist" } }, + }; async function withSuiteHome(fn: (home: string) => Promise): Promise { const home = path.join(fixtureRoot, `case-${homeCaseId++}`); @@ -37,6 +44,56 @@ describe("config observe recovery", () => { await fsp.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8"); } + async function seedConfigBackup(configPath: string, config: Record) { + await seedConfig(configPath, config); + await fsp.copyFile(configPath, `${configPath}.bak`); + } + + async function writeClobberedUpdateChannel(configPath: string) { + await fsp.writeFile(configPath, clobberedUpdateChannelRaw, "utf-8"); + return { + raw: clobberedUpdateChannelRaw, + parsed: clobberedUpdateChannelConfig, + }; + } + + async function readObserveEvents(auditPath: string): Promise[]> { + const lines = (await fsp.readFile(auditPath, "utf-8")).trim().split("\n").filter(Boolean); + return lines + .map((line) => JSON.parse(line) as Record) + .filter((line) => line.event === "config.observe"); + } + + async function readLastObserveEvent( + auditPath: string, + ): Promise | undefined> { + return (await readObserveEvents(auditPath)).at(-1); + } + + async function recoverClobberedUpdateChannel(params: { + deps: ObserveRecoveryDeps; + configPath: string; + }) { + return await maybeRecoverSuspiciousConfigRead({ + deps: params.deps, + configPath: params.configPath, + raw: clobberedUpdateChannelRaw, + parsed: clobberedUpdateChannelConfig, + }); + } + + function recoverClobberedUpdateChannelSync(params: { + deps: ObserveRecoveryDeps; + configPath: string; + }) { + return maybeRecoverSuspiciousConfigReadSync({ + deps: params.deps, + configPath: params.configPath, + raw: clobberedUpdateChannelRaw, + parsed: clobberedUpdateChannelConfig, + }); + } + async function makeSnapshot(configPath: string, config: Record) { const raw = `${JSON.stringify(config, null, 2)}\n`; await fsp.mkdir(path.dirname(configPath), { recursive: true }); @@ -84,34 +141,23 @@ describe("config observe recovery", () => { it("auto-restores suspicious update-channel-only roots from backup", async () => { await withSuiteHome(async (home) => { const { deps, configPath, auditPath, warn } = makeDeps(home); - await seedConfig(configPath, { + await seedConfigBackup(configPath, { update: { channel: "beta" }, browser: { enabled: true }, gateway: { mode: "local", auth: { mode: "token", token: "secret-token" } }, channels: { discord: { enabled: true, dmPolicy: "pairing" } }, }); - await fsp.copyFile(configPath, `${configPath}.bak`); + await writeClobberedUpdateChannel(configPath); - const clobberedRaw = `${JSON.stringify({ update: { channel: "beta" } }, null, 2)}\n`; - await fsp.writeFile(configPath, clobberedRaw, "utf-8"); - - const recovered = await maybeRecoverSuspiciousConfigRead({ - deps, - configPath, - raw: clobberedRaw, - parsed: { update: { channel: "beta" } }, - }); + const recovered = await recoverClobberedUpdateChannel({ deps, configPath }); expect((recovered.parsed as { gateway?: { mode?: string } }).gateway?.mode).toBe("local"); - await expect(fsp.readFile(configPath, "utf-8")).resolves.not.toBe(clobberedRaw); + await expect(fsp.readFile(configPath, "utf-8")).resolves.not.toBe(clobberedUpdateChannelRaw); expect(warn).toHaveBeenCalledWith( expect.stringContaining("Config auto-restored from backup:"), ); - const lines = (await fsp.readFile(auditPath, "utf-8")).trim().split("\n").filter(Boolean); - const observe = lines - .map((line) => JSON.parse(line) as Record) - .findLast((line) => line.event === "config.observe"); + const observe = await readLastObserveEvent(auditPath); expect(observe?.restoredFromBackup).toBe(true); expect(observe?.suspicious).toEqual( expect.arrayContaining(["gateway-mode-missing-vs-last-good", "update-channel-only-root"]), @@ -122,33 +168,13 @@ describe("config observe recovery", () => { it("dedupes repeated suspicious hashes", async () => { await withSuiteHome(async (home) => { const { deps, configPath, auditPath } = makeDeps(home); - await seedConfig(configPath, { - update: { channel: "beta" }, - gateway: { mode: "local" }, - channels: { telegram: { enabled: true, dmPolicy: "pairing", groupPolicy: "allowlist" } }, - }); - await fsp.copyFile(configPath, `${configPath}.bak`); + await seedConfigBackup(configPath, recoverableTelegramConfig); + await writeClobberedUpdateChannel(configPath); - const clobberedRaw = `${JSON.stringify({ update: { channel: "beta" } }, null, 2)}\n`; - await fsp.writeFile(configPath, clobberedRaw, "utf-8"); + await recoverClobberedUpdateChannel({ deps, configPath }); + await recoverClobberedUpdateChannel({ deps, configPath }); - await maybeRecoverSuspiciousConfigRead({ - deps, - configPath, - raw: clobberedRaw, - parsed: { update: { channel: "beta" } }, - }); - await maybeRecoverSuspiciousConfigRead({ - deps, - configPath, - raw: clobberedRaw, - parsed: { update: { channel: "beta" } }, - }); - - const lines = (await fsp.readFile(auditPath, "utf-8")).trim().split("\n").filter(Boolean); - const observeEvents = lines - .map((line) => JSON.parse(line) as Record) - .filter((line) => line.event === "config.observe"); + const observeEvents = await readObserveEvents(auditPath); expect(observeEvents).toHaveLength(1); }); }); @@ -156,28 +182,13 @@ describe("config observe recovery", () => { it("sync recovery uses backup baseline when health state is absent", async () => { await withSuiteHome(async (home) => { const { deps, configPath, auditPath } = makeDeps(home); - await seedConfig(configPath, { - update: { channel: "beta" }, - gateway: { mode: "local" }, - channels: { telegram: { enabled: true, dmPolicy: "pairing", groupPolicy: "allowlist" } }, - }); - await fsp.copyFile(configPath, `${configPath}.bak`); + await seedConfigBackup(configPath, recoverableTelegramConfig); + await writeClobberedUpdateChannel(configPath); - const clobberedRaw = `${JSON.stringify({ update: { channel: "beta" } }, null, 2)}\n`; - await fsp.writeFile(configPath, clobberedRaw, "utf-8"); - - const recovered = maybeRecoverSuspiciousConfigReadSync({ - deps, - configPath, - raw: clobberedRaw, - parsed: { update: { channel: "beta" } }, - }); + const recovered = recoverClobberedUpdateChannelSync({ deps, configPath }); expect((recovered.parsed as { gateway?: { mode?: string } }).gateway?.mode).toBe("local"); - const lines = (await fsp.readFile(auditPath, "utf-8")).trim().split("\n").filter(Boolean); - const observe = lines - .map((line) => JSON.parse(line) as Record) - .findLast((line) => line.event === "config.observe"); + const observe = await readLastObserveEvent(auditPath); expect(observe?.backupHash).toBeTypeOf("string"); expect(observe?.lastKnownGoodIno ?? null).toBeNull(); }); @@ -217,10 +228,7 @@ describe("config observe recovery", () => { expect(warn).toHaveBeenCalledWith( expect.stringContaining("Config auto-restored from last-known-good:"), ); - const lines = (await fsp.readFile(auditPath, "utf-8")).trim().split("\n").filter(Boolean); - const observe = lines - .map((line) => JSON.parse(line) as Record) - .findLast((line) => line.event === "config.observe"); + const observe = await readLastObserveEvent(auditPath); expect(observe?.restoredFromBackup).toBe(true); expect(observe?.restoredBackupPath).toBe(resolveLastKnownGoodConfigPath(configPath)); });