Files
openclaw/src/config/plugin-install-record-map.test.ts
T
Vincent Koc 1471881af7 fix(plugins): preserve install records during recovery (#121045)
* fix(plugins): canonicalize install record persistence

Preserve canonical fields, passthrough metadata, untouched record identity, and deterministic serialized ordering across SQLite and post-core handoff.

Punchcard-Session: amber-workshop-workshop-36
Co-authored-by: Galin Iliev <5711535+galiniliev@users.noreply.github.com>

* fix(doctor): block invalid plugin record mutations

Preflight persisted and legacy install-record state before registry repair, package cleanup, generation retirement, archival, or state-directory moves.

Punchcard-Session: amber-workshop-workshop-36
Co-authored-by: Galin Iliev <5711535+galiniliev@users.noreply.github.com>

* fix(plugins): harden install record map ownership

Punchcard-Session: amber-workshop-workshop-36
Co-authored-by: Galin Iliev <5711535+galiniliev@users.noreply.github.com>

* fix(update): preserve special plugin ids in payload checks

Punchcard-Session: amber-workshop-workshop-36
Co-authored-by: Galin Iliev <5711535+galiniliev@users.noreply.github.com>

* test(doctor): assert canonical install record maps

Punchcard-Session: amber-workshop-workshop-36
Co-authored-by: Galin Iliev <5711535+galiniliev@users.noreply.github.com>

* test(doctor): preserve invalid plugin index fixtures

Punchcard-Session: amber-workshop-workshop-36
Co-authored-by: Galin Iliev <5711535+galiniliev@users.noreply.github.com>

---------

Co-authored-by: Galin Iliev <5711535+galiniliev@users.noreply.github.com>
2026-08-09 20:24:40 +08:00

96 lines
3.5 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
createPluginInstallRecordMap,
inspectPluginInstallRecordMap,
parsePluginInstallRecordMap,
serializePluginInstallRecordMap,
setPluginInstallRecordMapEntry,
} from "./plugin-install-record-map.js";
describe("plugin install record maps", () => {
it("normalizes known fields while preserving passthrough fields", () => {
const records = parsePluginInstallRecordMap({
demo: {
source: "npm",
spec: " demo@1.0.0 ",
clawhubTrustReasons: [" keep ", ""],
futureMetadata: { retained: true },
},
});
expect(records).toEqual({
demo: {
source: "npm",
spec: "demo@1.0.0",
clawhubTrustReasons: ["keep"],
futureMetadata: { retained: true },
},
});
expect(Object.getPrototypeOf(records)).toBeNull();
});
it("distinguishes missing maps from invalid maps", () => {
expect(inspectPluginInstallRecordMap(undefined)).toEqual({ status: "missing" });
expect(inspectPluginInstallRecordMap({ demo: { source: "bogus" } })).toEqual({
status: "invalid",
});
const empty = inspectPluginInstallRecordMap({});
expect(empty.status).toBe("valid");
if (empty.status === "valid") {
expect(Object.keys(empty.records)).toEqual([]);
expect(Object.getPrototypeOf(empty.records)).toBeNull();
}
});
it("rejects invalid records atomically", () => {
expect(
parsePluginInstallRecordMap({
valid: { source: "npm" },
invalid: { source: "npm", clawpackSize: -1 },
}),
).toBeNull();
});
it.each(["constructor", "toString", "__proto__"])(
"rejects an invalid %s record atomically",
(pluginId) => {
const records = createPluginInstallRecordMap<unknown>();
setPluginInstallRecordMapEntry(records, "valid", { source: "npm" });
setPluginInstallRecordMapEntry(records, pluginId, { source: "bogus" });
expect(parsePluginInstallRecordMap(records)).toBeNull();
},
);
it("preserves prototype-named plugin ids as inert own properties", () => {
const records = parsePluginInstallRecordMap(
JSON.parse(
'{"__proto__":{"source":"npm"},"constructor":{"source":"path"},"toString":{"source":"git"}}',
) as Record<string, unknown>,
);
expect(Object.getPrototypeOf(records)).toBeNull();
expect(Object.keys(records ?? {})).toEqual(["__proto__", "constructor", "toString"]);
expect(Object.hasOwn(records ?? {}, "__proto__")).toBe(true);
expect(Object.hasOwn(records ?? {}, "constructor")).toBe(true);
expect(Object.hasOwn(records ?? {}, "toString")).toBe(true);
expect(Object.getOwnPropertyDescriptor(records, "__proto__")?.value).toEqual({
source: "npm",
});
expect(({} as Record<string, unknown>).source).toBeUndefined();
});
it("serializes numeric-looking and Unicode keys in UTF-8 byte order", () => {
const records = createPluginInstallRecordMap<{ source: "npm" | "path" | "git" | "archive" }>();
setPluginInstallRecordMapEntry(records, "\u{10000}", { source: "git" });
setPluginInstallRecordMapEntry(records, "2", { source: "npm" });
setPluginInstallRecordMapEntry(records, "\uE000", { source: "path" });
setPluginInstallRecordMapEntry(records, "10", { source: "path" });
setPluginInstallRecordMapEntry(records, "1", { source: "archive" });
expect(serializePluginInstallRecordMap(records)).toBe(
'{"1":{"source":"archive"},"10":{"source":"path"},"2":{"source":"npm"},"\uE000":{"source":"path"},"\u{10000}":{"source":"git"}}',
);
});
});