mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(cli): silent drop of malformed post-core install-records JSON (#109989)
* fix(cli): fail closed on malformed post-core install-records JSON Missing handoff files stay optional. Corrupt JSON previously returned undefined and dropped parent recovery context during update resume. * test(cli): cover post-core install-records missing vs malformed JSON Includes live temp-file proof that corrupt handoff is rejected. * fix(cli): direct corrupt post-core handoffs to doctor --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
// Post-core install-records handoff reader: missing vs malformed JSON.
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { readPostCorePluginInstallRecordsFile } from "./update-command-post-core.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
tempDirs.splice(0).map(async (dir) => {
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
async function withTempDir(): Promise<string> {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-post-core-records-"));
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
describe("readPostCorePluginInstallRecordsFile", () => {
|
||||
it("returns undefined when the path is omitted", async () => {
|
||||
await expect(readPostCorePluginInstallRecordsFile(undefined)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined when the handoff file is missing", async () => {
|
||||
const dir = await withTempDir();
|
||||
const missing = path.join(dir, "missing-plugin-install-records.json");
|
||||
await expect(readPostCorePluginInstallRecordsFile(missing)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("loads a valid install-records handoff", async () => {
|
||||
const dir = await withTempDir();
|
||||
const filePath = path.join(dir, "plugin-install-records.json");
|
||||
await fs.writeFile(
|
||||
filePath,
|
||||
`${JSON.stringify({
|
||||
demo: {
|
||||
source: "npm",
|
||||
spec: "@openclaw/demo@1.0.0",
|
||||
installPath: "/tmp/demo-plugin",
|
||||
},
|
||||
})}\n`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
await expect(readPostCorePluginInstallRecordsFile(filePath)).resolves.toEqual({
|
||||
demo: {
|
||||
source: "npm",
|
||||
spec: "@openclaw/demo@1.0.0",
|
||||
installPath: "/tmp/demo-plugin",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("fails closed on malformed handoff JSON with a path-labelled error", async () => {
|
||||
const dir = await withTempDir();
|
||||
const filePath = path.join(dir, "plugin-install-records.json");
|
||||
await fs.writeFile(filePath, "{invalid json", "utf-8");
|
||||
|
||||
await expect(readPostCorePluginInstallRecordsFile(filePath)).rejects.toThrow(
|
||||
`Malformed JSON in plugin install records file: ${filePath}`,
|
||||
);
|
||||
await expect(readPostCorePluginInstallRecordsFile(filePath)).rejects.toThrow(
|
||||
"Run openclaw doctor to inspect and repair plugin installation state.",
|
||||
);
|
||||
});
|
||||
|
||||
it("live FS: corrupt handoff is not silently dropped as empty records", async () => {
|
||||
// L3: real temp file + real fs.readFile/JSON.parse (no stubs).
|
||||
const dir = await withTempDir();
|
||||
const filePath = path.join(dir, "plugin-install-records.json");
|
||||
await fs.writeFile(filePath, '[{"not":"a-record-map"', "utf-8");
|
||||
|
||||
let threw = false;
|
||||
try {
|
||||
await readPostCorePluginInstallRecordsFile(filePath);
|
||||
} catch (err) {
|
||||
threw = true;
|
||||
expect(String(err)).toContain(`Malformed JSON in plugin install records file: ${filePath}`);
|
||||
}
|
||||
expect(threw).toBe(true);
|
||||
|
||||
console.info(
|
||||
`[post-core install-records live proof] path=${filePath} outcome=malformed-json-rejected`,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import type { PluginInstallRecord } from "../../config/types.plugins.js";
|
||||
import { resolveGatewayInstallEntrypoint } from "../../daemon/gateway-entrypoint.js";
|
||||
import { hasErrnoCode } from "../../infra/errors.js";
|
||||
import { readJsonIfExists, writeJson } from "../../infra/json-files.js";
|
||||
import { parseStrictPositiveInteger } from "../../infra/parse-finite-number.js";
|
||||
import {
|
||||
@@ -326,12 +327,31 @@ export async function readPostCorePluginInstallRecordsFile(
|
||||
if (!filePath) {
|
||||
return undefined;
|
||||
}
|
||||
// Missing handoff is optional (parent may omit the path). Corrupt / unreadable
|
||||
// handoff must fail closed: silent undefined previously dropped parent install
|
||||
// recovery context when the post-doctor index was still empty.
|
||||
let raw: string;
|
||||
try {
|
||||
const parsed = JSON.parse(await fs.readFile(filePath, "utf-8")) as unknown;
|
||||
return normalizePluginInstallRecordMap(parsed);
|
||||
} catch {
|
||||
return undefined;
|
||||
raw = await fs.readFile(filePath, "utf-8");
|
||||
} catch (err) {
|
||||
if (hasErrnoCode(err, "ENOENT")) {
|
||||
return undefined;
|
||||
}
|
||||
throw new Error(
|
||||
`Unable to read plugin install records file: ${filePath}. Run openclaw doctor to inspect and repair plugin installation state.`,
|
||||
{ cause: err },
|
||||
);
|
||||
}
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Malformed JSON in plugin install records file: ${filePath}. Run openclaw doctor to inspect and repair plugin installation state.`,
|
||||
{ cause: err },
|
||||
);
|
||||
}
|
||||
return normalizePluginInstallRecordMap(parsed);
|
||||
}
|
||||
|
||||
async function execFileStdout(file: string, args: string[]): Promise<string | undefined> {
|
||||
|
||||
Reference in New Issue
Block a user