mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-07 19:48:31 +08:00
fix(config): archive matching config health legacy state
This commit is contained in:
@@ -1980,6 +1980,149 @@ describe("state migrations", () => {
|
||||
await expect(fs.readFile(`${sourcePath}.migrated`, "utf8")).resolves.toContain("abc123");
|
||||
});
|
||||
|
||||
it("archives legacy config health JSON when SQLite already has the same config fingerprint", async () => {
|
||||
const root = await createTempDir();
|
||||
const stateDir = path.join(root, ".openclaw");
|
||||
const env = createEnv(stateDir);
|
||||
const cfg = createConfig();
|
||||
const configPath = path.join(stateDir, "openclaw.json");
|
||||
const logsDir = path.join(stateDir, "logs");
|
||||
const sourcePath = path.join(logsDir, "config-health.json");
|
||||
const sqliteFingerprint = {
|
||||
hash: "abc123",
|
||||
bytes: 42,
|
||||
mtimeMs: 100,
|
||||
ctimeMs: 200,
|
||||
dev: "3",
|
||||
ino: "4",
|
||||
mode: 0o100600,
|
||||
nlink: 1,
|
||||
uid: 501,
|
||||
gid: 20,
|
||||
hasMeta: true,
|
||||
gatewayMode: "local",
|
||||
observedAt: "2026-01-17T09:30:00.000Z",
|
||||
};
|
||||
const legacyFingerprint = {
|
||||
...sqliteFingerprint,
|
||||
mtimeMs: 101,
|
||||
ctimeMs: 201,
|
||||
mode: 0o600,
|
||||
observedAt: "2026-01-17T10:30:00.000Z",
|
||||
};
|
||||
const { db } = openOpenClawStateDatabase({ env });
|
||||
const stateDb = getNodeSqliteKysely<ConfigHealthDatabase>(db);
|
||||
executeSqliteQuerySync(
|
||||
db,
|
||||
stateDb.insertInto("config_health_entries").values({
|
||||
config_path: configPath,
|
||||
last_known_good_json: JSON.stringify(sqliteFingerprint),
|
||||
last_promoted_good_json: JSON.stringify(sqliteFingerprint),
|
||||
last_observed_suspicious_signature: "abc123:size-drop",
|
||||
updated_at_ms: 1,
|
||||
}),
|
||||
);
|
||||
await fs.mkdir(logsDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
sourcePath,
|
||||
JSON.stringify({
|
||||
entries: {
|
||||
[configPath]: {
|
||||
lastKnownGood: legacyFingerprint,
|
||||
lastPromotedGood: legacyFingerprint,
|
||||
lastObservedSuspiciousSignature: "abc123:size-drop",
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const detected = await detectLegacyStateMigrations({ cfg, env, homedir: () => root });
|
||||
const result = await runLegacyStateMigrations({ detected, config: cfg });
|
||||
|
||||
expect(result.warnings).toStrictEqual([]);
|
||||
expect(result.changes).not.toContain("Migrated 1 config health entry → shared SQLite state");
|
||||
expect(result.changes).toContain(
|
||||
`Archived config health state legacy source → ${sourcePath}.migrated`,
|
||||
);
|
||||
expect(readConfigHealthRows(env)).toEqual([
|
||||
{
|
||||
config_path: configPath,
|
||||
last_known_good_json: JSON.stringify(sqliteFingerprint),
|
||||
last_promoted_good_json: JSON.stringify(sqliteFingerprint),
|
||||
last_observed_suspicious_signature: "abc123:size-drop",
|
||||
},
|
||||
]);
|
||||
await expectMissingPath(sourcePath);
|
||||
await expect(fs.readFile(`${sourcePath}.migrated`, "utf8")).resolves.toContain("abc123");
|
||||
});
|
||||
|
||||
it("keeps legacy config health JSON when the stored config hash differs", async () => {
|
||||
const root = await createTempDir();
|
||||
const stateDir = path.join(root, ".openclaw");
|
||||
const env = createEnv(stateDir);
|
||||
const cfg = createConfig();
|
||||
const configPath = path.join(stateDir, "openclaw.json");
|
||||
const logsDir = path.join(stateDir, "logs");
|
||||
const sourcePath = path.join(logsDir, "config-health.json");
|
||||
const sqliteFingerprint = {
|
||||
hash: "abc123",
|
||||
bytes: 42,
|
||||
mtimeMs: 100,
|
||||
ctimeMs: 200,
|
||||
dev: "3",
|
||||
ino: "4",
|
||||
mode: 0o100600,
|
||||
nlink: 1,
|
||||
uid: 501,
|
||||
gid: 20,
|
||||
hasMeta: true,
|
||||
gatewayMode: "local",
|
||||
observedAt: "2026-01-17T09:30:00.000Z",
|
||||
};
|
||||
const legacyFingerprint = {
|
||||
...sqliteFingerprint,
|
||||
hash: "def456",
|
||||
mode: 0o600,
|
||||
observedAt: "2026-01-17T10:30:00.000Z",
|
||||
};
|
||||
const { db } = openOpenClawStateDatabase({ env });
|
||||
const stateDb = getNodeSqliteKysely<ConfigHealthDatabase>(db);
|
||||
executeSqliteQuerySync(
|
||||
db,
|
||||
stateDb.insertInto("config_health_entries").values({
|
||||
config_path: configPath,
|
||||
last_known_good_json: JSON.stringify(sqliteFingerprint),
|
||||
last_promoted_good_json: JSON.stringify(sqliteFingerprint),
|
||||
last_observed_suspicious_signature: "abc123:size-drop",
|
||||
updated_at_ms: 1,
|
||||
}),
|
||||
);
|
||||
await fs.mkdir(logsDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
sourcePath,
|
||||
JSON.stringify({
|
||||
entries: {
|
||||
[configPath]: {
|
||||
lastKnownGood: legacyFingerprint,
|
||||
lastPromotedGood: legacyFingerprint,
|
||||
lastObservedSuspiciousSignature: "abc123:size-drop",
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const detected = await detectLegacyStateMigrations({ cfg, env, homedir: () => root });
|
||||
const result = await runLegacyStateMigrations({ detected, config: cfg });
|
||||
|
||||
expect(result.warnings).toEqual([
|
||||
`Left legacy config health state in place because 1 entry conflicts with shared SQLite state: ${sourcePath}`,
|
||||
]);
|
||||
expect(result.changes).toStrictEqual([]);
|
||||
await expect(fs.stat(sourcePath)).resolves.toMatchObject({ isFile: expect.any(Function) });
|
||||
});
|
||||
|
||||
it("migrates legacy current-conversation bindings JSON into shared SQLite state", async () => {
|
||||
const root = await createTempDir();
|
||||
const stateDir = path.join(root, ".openclaw");
|
||||
|
||||
@@ -2087,14 +2087,65 @@ function configHealthRow(entry: LegacyConfigHealthEntry): {
|
||||
};
|
||||
}
|
||||
|
||||
function configHealthComparable(entry: LegacyConfigHealthEntry): string {
|
||||
const row = configHealthRow(entry);
|
||||
return JSON.stringify({
|
||||
config_path: row.config_path,
|
||||
last_known_good_json: row.last_known_good_json,
|
||||
last_promoted_good_json: row.last_promoted_good_json,
|
||||
last_observed_suspicious_signature: row.last_observed_suspicious_signature,
|
||||
});
|
||||
type ConfigHealthComparableRow = {
|
||||
config_path: string;
|
||||
last_known_good_json: string | null;
|
||||
last_promoted_good_json: string | null;
|
||||
last_observed_suspicious_signature: string | null;
|
||||
};
|
||||
|
||||
type ConfigHealthComparableFingerprint = {
|
||||
hash?: unknown;
|
||||
bytes?: unknown;
|
||||
mode?: unknown;
|
||||
hasMeta?: unknown;
|
||||
gatewayMode?: unknown;
|
||||
};
|
||||
|
||||
function configHealthFingerprintKey(value: string | null): string | null {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(value) as unknown;
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
return null;
|
||||
}
|
||||
const fingerprint = parsed as ConfigHealthComparableFingerprint;
|
||||
const mode =
|
||||
typeof fingerprint.mode === "number" && Number.isFinite(fingerprint.mode)
|
||||
? fingerprint.mode & 0o777
|
||||
: fingerprint.mode;
|
||||
return JSON.stringify({
|
||||
hash: fingerprint.hash,
|
||||
bytes: fingerprint.bytes,
|
||||
mode,
|
||||
hasMeta: fingerprint.hasMeta,
|
||||
gatewayMode: fingerprint.gatewayMode,
|
||||
});
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function sameConfigHealthFingerprintJson(left: string | null, right: string | null): boolean {
|
||||
if (left === right) {
|
||||
return true;
|
||||
}
|
||||
const leftKey = configHealthFingerprintKey(left);
|
||||
return leftKey !== null && leftKey === configHealthFingerprintKey(right);
|
||||
}
|
||||
|
||||
function configHealthEntryMatchesRow(
|
||||
row: ConfigHealthComparableRow,
|
||||
entry: LegacyConfigHealthEntry,
|
||||
): boolean {
|
||||
return (
|
||||
row.config_path === entry.configPath &&
|
||||
sameConfigHealthFingerprintJson(row.last_known_good_json, entry.lastKnownGoodJson) &&
|
||||
sameConfigHealthFingerprintJson(row.last_promoted_good_json, entry.lastPromotedGoodJson) &&
|
||||
row.last_observed_suspicious_signature === entry.lastObservedSuspiciousSignature
|
||||
);
|
||||
}
|
||||
|
||||
function migrateLegacyConfigHealth(params: {
|
||||
@@ -2133,27 +2184,14 @@ function migrateLegacyConfigHealth(params: {
|
||||
"last_observed_suspicious_signature",
|
||||
]),
|
||||
).rows;
|
||||
const existingByPath = new Map(
|
||||
existing.map(
|
||||
(row) =>
|
||||
[
|
||||
row.config_path,
|
||||
JSON.stringify({
|
||||
config_path: row.config_path,
|
||||
last_known_good_json: row.last_known_good_json,
|
||||
last_promoted_good_json: row.last_promoted_good_json,
|
||||
last_observed_suspicious_signature: row.last_observed_suspicious_signature,
|
||||
}),
|
||||
] as const,
|
||||
),
|
||||
);
|
||||
const existingByPath = new Map(existing.map((row) => [row.config_path, row] as const));
|
||||
const entriesToInsert: LegacyConfigHealthEntry[] = [];
|
||||
let conflictCount = 0;
|
||||
for (const entry of entries) {
|
||||
const existingEntryJson = existingByPath.get(entry.configPath);
|
||||
if (existingEntryJson === undefined) {
|
||||
const existingEntry = existingByPath.get(entry.configPath);
|
||||
if (existingEntry === undefined) {
|
||||
entriesToInsert.push(entry);
|
||||
} else if (existingEntryJson !== configHealthComparable(entry)) {
|
||||
} else if (!configHealthEntryMatchesRow(existingEntry, entry)) {
|
||||
conflictCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user