



























@@ -4,7 +4,13 @@ import fsp from "node:fs/promises";
44import os from "node:os";
55import path from "node:path";
66import JSON5 from "json5";
7-import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
7+import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
8+import { executeSqliteQueryTakeFirstSync, getNodeSqliteKysely } from "../infra/kysely-sync.js";
9+import type { DB as OpenClawStateKyselyDatabase } from "../state/openclaw-state-db.generated.js";
10+import {
11+closeOpenClawStateDatabaseForTest,
12+openOpenClawStateDatabase,
13+} from "../state/openclaw-state-db.js";
814import { createConfigIO } from "./io.js";
915import {
1016maybeRecoverSuspiciousConfigRead,
@@ -17,6 +23,7 @@ import {
1723import type { ConfigFileSnapshot } from "./types.js";
18241925const CONFIG_CLOBBER_SNAPSHOT_LIMIT = 32;
26+type ConfigHealthDatabase = Pick<OpenClawStateKyselyDatabase, "config_health_entries">;
20272128describe("config observe recovery", () => {
2229let fixtureRoot = "";
@@ -41,9 +48,31 @@ describe("config observe recovery", () => {
4148});
42494350afterAll(async () => {
51+closeOpenClawStateDatabaseForTest();
4452await fsp.rm(fixtureRoot, { recursive: true, force: true });
4553});
465455+afterEach(() => {
56+closeOpenClawStateDatabaseForTest();
57+});
58+59+function readConfigHealthRow(home: string, configPath: string) {
60+const { db } = openOpenClawStateDatabase({ env: { HOME: home } as NodeJS.ProcessEnv });
61+const healthDb = getNodeSqliteKysely<ConfigHealthDatabase>(db);
62+return executeSqliteQueryTakeFirstSync(
63+db,
64+healthDb
65+.selectFrom("config_health_entries")
66+.select([
67+"config_path",
68+"last_known_good_json",
69+"last_promoted_good_json",
70+"last_observed_suspicious_signature",
71+])
72+.where("config_path", "=", configPath),
73+);
74+}
75+4776async function seedConfig(configPath: string, config: Record<string, unknown>): Promise<void> {
4877await fsp.mkdir(path.dirname(configPath), { recursive: true });
4978await fsp.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
@@ -243,47 +272,6 @@ describe("config observe recovery", () => {
243272};
244273}
245274246-function withAsyncHealthWriteFailure(
247-deps: ObserveRecoveryDeps,
248-healthPath: string,
249-): ObserveRecoveryDeps {
250-const writeFile = deps.fs.promises.writeFile.bind(deps.fs.promises);
251-return {
252- ...deps,
253-fs: {
254- ...deps.fs,
255-promises: {
256- ...deps.fs.promises,
257-writeFile: async (target, data, options) => {
258-if (target === healthPath) {
259-throw new Error("health write failed");
260-}
261-return await writeFile(target, data, options);
262-},
263-},
264-},
265-};
266-}
267-268-function withSyncHealthWriteFailure(
269-deps: ObserveRecoveryDeps,
270-healthPath: string,
271-): ObserveRecoveryDeps {
272-const writeFileSync = deps.fs.writeFileSync.bind(deps.fs);
273-return {
274- ...deps,
275-fs: {
276- ...deps.fs,
277-writeFileSync: (target, data, options) => {
278-if (target === healthPath) {
279-throw new Error("health write failed");
280-}
281-return writeFileSync(target, data, options);
282-},
283-},
284-};
285-}
286-287275it("auto-restores suspicious update-channel-only roots from backup", async () => {
288276await withSuiteHome(async (home) => {
289277const { deps, configPath, auditPath, warn } = makeDeps(home);
@@ -953,43 +941,42 @@ describe("config observe recovery", () => {
953941});
954942});
955943956-it("logs async health-state write failures", async () => {
944+it("writes async health state to SQLite", async () => {
957945await withSuiteHome(async (home) => {
958946const { deps, configPath, warn } = makeDeps(home);
959947const snapshot = await makeSnapshot(configPath, recoverableTelegramConfig);
960-const healthPath = path.join(home, ".openclaw", "logs", "config-health.json");
961948962949await expect(
963-promoteConfigSnapshotToLastKnownGood({
964-deps: withAsyncHealthWriteFailure(deps, healthPath),
965- snapshot,
966-logger: deps.logger,
967-}),
950+promoteConfigSnapshotToLastKnownGood({ deps, snapshot, logger: deps.logger }),
968951).resolves.toBe(true);
969952970-expectWarnContaining(
971-warn,
972-`Config health-state write failed: ${healthPath}: health write failed`,
973-);
953+await expectPathMissing(path.join(home, ".openclaw", "logs", "config-health.json"));
954+const row = readConfigHealthRow(home, configPath);
955+expect(row).toMatchObject({
956+config_path: configPath,
957+last_known_good_json: expect.any(String),
958+last_promoted_good_json: expect.any(String),
959+last_observed_suspicious_signature: null,
960+});
961+expectWarnNotContaining(warn, "Config health-state write failed");
974962});
975963});
976964977-it("logs sync health-state write failures", async () => {
965+it("writes sync health state to SQLite", async () => {
978966await withSuiteHome(async (home) => {
979967const { deps, configPath, warn } = makeDeps(home);
980-const healthPath = path.join(home, ".openclaw", "logs", "config-health.json");
981968await seedConfigBackup(configPath, recoverableTelegramConfig);
982969await writeClobberedUpdateChannel(configPath);
983970984-recoverClobberedUpdateChannelSync({
985-deps: withSyncHealthWriteFailure(deps, healthPath),
986- configPath,
987-});
971+recoverClobberedUpdateChannelSync({ deps, configPath });
988972989-expectWarnContaining(
990-warn,
991-`Config health-state write failed: ${healthPath}: health write failed`,
992-);
973+await expectPathMissing(path.join(home, ".openclaw", "logs", "config-health.json"));
974+const row = readConfigHealthRow(home, configPath);
975+expect(row).toMatchObject({
976+config_path: configPath,
977+last_observed_suspicious_signature: expect.any(String),
978+});
979+expectWarnNotContaining(warn, "Config health-state write failed");
993980});
994981});
995982此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。