


















@@ -20,6 +20,7 @@ describe("config observe recovery", () => {
2020const clobberedUpdateChannelConfig = { update: { channel: "beta" } };
2121const clobberedUpdateChannelRaw = `${JSON.stringify(clobberedUpdateChannelConfig, null, 2)}\n`;
2222const recoverableTelegramConfig = {
23+meta: { lastTouchedAt: "2026-04-22T00:00:00.000Z" },
2324update: { channel: "beta" },
2425gateway: { mode: "local" },
2526channels: { telegram: { enabled: true, dmPolicy: "pairing", groupPolicy: "allowlist" } },
@@ -49,6 +50,12 @@ describe("config observe recovery", () => {
4950await fsp.copyFile(configPath, `${configPath}.bak`);
5051}
515253+async function writeConfigRaw(configPath: string, config: Record<string, unknown>) {
54+const raw = `${JSON.stringify(config, null, 2)}\n`;
55+await fsp.writeFile(configPath, raw, "utf-8");
56+return { raw, parsed: config };
57+}
58+5259async function writeClobberedUpdateChannel(configPath: string) {
5360await fsp.writeFile(configPath, clobberedUpdateChannelRaw, "utf-8");
5461return {
@@ -82,6 +89,20 @@ describe("config observe recovery", () => {
8289});
8390}
849192+async function recoverSuspiciousConfigRead(params: {
93+deps: ObserveRecoveryDeps;
94+configPath: string;
95+raw: string;
96+parsed: unknown;
97+}) {
98+return await maybeRecoverSuspiciousConfigRead({
99+deps: params.deps,
100+configPath: params.configPath,
101+raw: params.raw,
102+parsed: params.parsed,
103+});
104+}
105+85106function recoverClobberedUpdateChannelSync(params: {
86107deps: ObserveRecoveryDeps;
87108configPath: string;
@@ -142,6 +163,7 @@ describe("config observe recovery", () => {
142163await withSuiteHome(async (home) => {
143164const { deps, configPath, auditPath, warn } = makeDeps(home);
144165await seedConfigBackup(configPath, {
166+meta: { lastTouchedAt: "2026-04-22T00:00:00.000Z" },
145167update: { channel: "beta" },
146168browser: { enabled: true },
147169gateway: { mode: "local", auth: { mode: "token", token: "secret-token" } },
@@ -165,6 +187,97 @@ describe("config observe recovery", () => {
165187});
166188});
167189190+it("auto-restores when metadata disappears from an otherwise valid config", async () => {
191+await withSuiteHome(async (home) => {
192+const { deps, configPath, auditPath } = makeDeps(home);
193+await seedConfigBackup(configPath, recoverableTelegramConfig);
194+const clobbered = await writeConfigRaw(configPath, {
195+update: { channel: "beta" },
196+gateway: { mode: "local" },
197+channels: { telegram: { enabled: true, dmPolicy: "pairing", groupPolicy: "allowlist" } },
198+});
199+200+const recovered = await recoverSuspiciousConfigRead({ deps, configPath, ...clobbered });
201+202+expect((recovered.parsed as { meta?: unknown }).meta).toEqual(recoverableTelegramConfig.meta);
203+const observe = await readLastObserveEvent(auditPath);
204+expect(observe?.restoredFromBackup).toBe(true);
205+expect(observe?.suspicious).toEqual(expect.arrayContaining(["missing-meta-vs-last-good"]));
206+});
207+});
208+209+it("auto-restores when gateway mode disappears from the last-good shape", async () => {
210+await withSuiteHome(async (home) => {
211+const { deps, configPath, auditPath } = makeDeps(home);
212+await seedConfigBackup(configPath, recoverableTelegramConfig);
213+const clobbered = await writeConfigRaw(configPath, {
214+meta: { lastTouchedAt: "2026-04-22T00:00:00.000Z" },
215+update: { channel: "beta" },
216+channels: { telegram: { enabled: true, dmPolicy: "pairing", groupPolicy: "allowlist" } },
217+});
218+219+const recovered = await recoverSuspiciousConfigRead({ deps, configPath, ...clobbered });
220+221+expect((recovered.parsed as { gateway?: { mode?: string } }).gateway?.mode).toBe("local");
222+const observe = await readLastObserveEvent(auditPath);
223+expect(observe?.restoredFromBackup).toBe(true);
224+expect(observe?.suspicious).toEqual(
225+expect.arrayContaining(["gateway-mode-missing-vs-last-good"]),
226+);
227+});
228+});
229+230+it("auto-restores after a large size drop against last-good config", async () => {
231+await withSuiteHome(async (home) => {
232+const { deps, configPath, auditPath } = makeDeps(home);
233+await seedConfigBackup(configPath, {
234+ ...recoverableTelegramConfig,
235+channels: {
236+telegram: {
237+enabled: true,
238+dmPolicy: "pairing",
239+groupPolicy: "allowlist",
240+allowFrom: Array.from({ length: 60 }, (_, index) => `telegram-user-${index}`),
241+},
242+},
243+});
244+const clobbered = await writeConfigRaw(configPath, {
245+meta: { lastTouchedAt: "2026-04-22T00:00:00.000Z" },
246+gateway: { mode: "local" },
247+});
248+249+const recovered = await recoverSuspiciousConfigRead({ deps, configPath, ...clobbered });
250+251+expect(
252+(recovered.parsed as { channels?: { telegram?: { allowFrom?: string[] } } }).channels
253+?.telegram?.allowFrom,
254+).toHaveLength(60);
255+const observe = await readLastObserveEvent(auditPath);
256+expect(observe?.restoredFromBackup).toBe(true);
257+expect(observe?.suspicious).toEqual(
258+expect.arrayContaining([expect.stringMatching(/^size-drop-vs-last-good:/)]),
259+);
260+});
261+});
262+263+it("does not restore noncritical config edits", async () => {
264+await withSuiteHome(async (home) => {
265+const { deps, configPath, auditPath } = makeDeps(home);
266+await seedConfigBackup(configPath, recoverableTelegramConfig);
267+const editedConfig = {
268+ ...recoverableTelegramConfig,
269+update: { channel: "stable" },
270+};
271+const edited = await writeConfigRaw(configPath, editedConfig);
272+273+const recovered = await recoverSuspiciousConfigRead({ deps, configPath, ...edited });
274+275+expect(recovered.parsed).toEqual(editedConfig);
276+await expect(fsp.readFile(configPath, "utf-8")).resolves.toBe(edited.raw);
277+await expect(fsp.stat(auditPath)).rejects.toThrow();
278+});
279+});
280+168281it("dedupes repeated suspicious hashes", async () => {
169282await withSuiteHome(async (home) => {
170283const { deps, configPath, auditPath } = makeDeps(home);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。