Files
openclaw/src/plugins/plugin-registry-snapshot.test.ts
Peter Steinberger f81f9d8570 chore: enforce max-lines suppression ratchet (#107315)
* ci: enforce max-lines suppression ratchet

* chore: prune stale max-lines suppression

* fix: close max-lines ratchet enforcement gaps

* fix: harden max-lines ratchet checks

* fix(ci): satisfy max-lines ratchet checks

* style: format max-lines declarations

* fix(ci): match oxlint suppression grammar

* test: isolate max-lines git fixtures

* chore: prune resolved max-lines debt

* test: skip newline path fixture on Windows

* fix: harden max-lines suppression ratchet

* chore: refresh max-lines baseline

* fix: close max-lines ratchet bypasses

* fix: derive ratchet base from PR merge tree

* fix: support older Git in staged ratchet

* fix: align max-lines declarations and baseline

* chore: refresh max-lines baseline for current main

* fix: exclude generated wizard locales from max-lines

* chore: prune resolved max-lines debt
2026-07-14 09:27:02 -07:00

1129 lines
38 KiB
TypeScript

// Covers stable plugin registry snapshot generation.
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import {
clearCurrentPluginMetadataSnapshot,
setCurrentPluginMetadataSnapshot,
} from "./current-plugin-metadata-snapshot.js";
import type { PluginCandidate } from "./discovery.js";
import { loadInstalledPluginIndexInstallRecordsSync } from "./installed-plugin-index-records.js";
import { writePersistedInstalledPluginIndexSync } from "./installed-plugin-index-store.js";
import {
loadInstalledPluginIndex,
resolveInstalledPluginIndexPolicyHash,
type InstalledPluginIndex,
} from "./installed-plugin-index.js";
import { markRetainedManagedNpmInstall } from "./managed-npm-retention.js";
import { loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed.js";
import type { PluginMetadataSnapshot } from "./plugin-metadata-snapshot.types.js";
import { loadPluginRegistrySnapshotWithMetadata } from "./plugin-registry-snapshot.js";
import { cleanupTrackedTempDirs, makeTrackedTempDir } from "./test-helpers/fs-fixtures.js";
import { writeManagedNpmPlugin } from "./test-helpers/managed-npm-plugin.js";
const tempDirs: string[] = [];
afterEach(() => {
vi.restoreAllMocks();
clearCurrentPluginMetadataSnapshot();
cleanupTrackedTempDirs(tempDirs);
});
function makeTempDir() {
return makeTrackedTempDir("openclaw-plugin-registry-snapshot", tempDirs);
}
function createHermeticEnv(rootDir: string): NodeJS.ProcessEnv {
return {
OPENCLAW_BUNDLED_PLUGINS_DIR: path.join(rootDir, "bundled"),
OPENCLAW_STATE_DIR: path.join(rootDir, "state"),
OPENCLAW_VERSION: "2026.4.26",
VITEST: "true",
};
}
function writeManifestlessClaudeBundle(rootDir: string) {
fs.mkdirSync(path.join(rootDir, "skills"), { recursive: true });
fs.writeFileSync(path.join(rootDir, "skills", "SKILL.md"), "# Workspace skill\n", "utf8");
}
function writePackagePlugin(
rootDir: string,
options: {
configPaths?: readonly string[];
pluginId?: string;
requiresPlugins?: readonly string[];
} = {},
) {
const pluginId = options.pluginId ?? "demo";
fs.mkdirSync(rootDir, { recursive: true });
fs.writeFileSync(path.join(rootDir, "index.ts"), "export default { register() {} };\n", "utf8");
fs.writeFileSync(
path.join(rootDir, "openclaw.plugin.json"),
JSON.stringify({
id: pluginId,
name: pluginId,
description: "one",
configSchema: { type: "object" },
...(options.configPaths ? { activation: { onConfigPaths: options.configPaths } } : {}),
...(options.requiresPlugins ? { requiresPlugins: options.requiresPlugins } : {}),
}),
"utf8",
);
fs.writeFileSync(
path.join(rootDir, "package.json"),
JSON.stringify({ name: pluginId, version: "1.0.0" }),
"utf8",
);
}
function writeBundledPlugin(rootDir: string, pluginId: string, entryPath: string) {
fs.mkdirSync(rootDir, { recursive: true });
fs.writeFileSync(path.join(rootDir, entryPath), "export default { register() {} };\n", "utf8");
fs.writeFileSync(
path.join(rootDir, "openclaw.plugin.json"),
JSON.stringify({
id: pluginId,
name: pluginId,
description: pluginId,
configSchema: { type: "object" },
}),
"utf8",
);
fs.writeFileSync(
path.join(rootDir, "package.json"),
JSON.stringify({
name: `@openclaw/${pluginId}`,
version: "1.0.0",
openclaw: { extensions: [`./${entryPath}`] },
}),
"utf8",
);
}
function mockLinuxMountInfo(mountPoints: readonly string[]) {
const originalReadFileSync = fs.readFileSync;
return vi.spyOn(fs, "readFileSync").mockImplementation((filePath, options) => {
if (filePath === "/proc/self/mountinfo") {
return mountPoints
.map(
(mountPoint, index) => `${100 + index} 99 0:${index} / ${mountPoint} rw - tmpfs tmpfs rw`,
)
.join("\n");
}
return originalReadFileSync(filePath, options as never) as never;
});
}
function createCandidate(rootDir: string, pluginId = "demo"): PluginCandidate {
fs.mkdirSync(rootDir, { recursive: true });
fs.writeFileSync(path.join(rootDir, "index.ts"), "export default { register() {} };\n", "utf8");
fs.writeFileSync(
path.join(rootDir, "openclaw.plugin.json"),
JSON.stringify({
id: pluginId,
name: pluginId,
description: pluginId,
configSchema: { type: "object" },
providers: [pluginId],
}),
"utf8",
);
return {
idHint: pluginId,
source: path.join(rootDir, "index.ts"),
rootDir,
origin: "global",
};
}
function replaceFilePreservingSizeAndMtime(filePath: string, contents: string) {
const previous = fs.statSync(filePath);
expect(Buffer.byteLength(contents)).toBe(previous.size);
fs.writeFileSync(filePath, contents, "utf8");
fs.utimesSync(filePath, previous.atime, previous.mtime);
}
function fileHash(filePath: string): string {
return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
}
function fileSignature(filePath: string) {
const stat = fs.statSync(filePath);
return {
size: stat.size,
mtimeMs: stat.mtimeMs,
ctimeMs: stat.ctimeMs,
};
}
function createManifestlessClaudeBundleIndex(params: {
rootDir: string;
env: NodeJS.ProcessEnv;
}): InstalledPluginIndex {
return loadInstalledPluginIndex({
config: {
plugins: {
load: { paths: [params.rootDir] },
},
},
env: params.env,
});
}
function expectDiagnosticsContainCode(diagnostics: readonly { code?: unknown }[], code: string) {
expect(diagnostics.map((diagnostic) => diagnostic.code)).toContain(code);
}
function expectDiagnosticsContainSource(
diagnostics: readonly { source?: unknown }[],
source: string,
) {
expect(diagnostics.map((diagnostic) => diagnostic.source)).toContain(source);
}
function expectDiagnosticsDoNotContainSource(
diagnostics: readonly { source?: unknown }[],
source: string,
) {
expect(diagnostics.map((diagnostic) => diagnostic.source)).not.toContain(source);
}
function requirePluginRecord(
plugins: InstalledPluginIndex["plugins"],
pluginId: string,
): InstalledPluginIndex["plugins"][number] {
const plugin = plugins.find((candidate) => candidate.pluginId === pluginId);
if (!plugin) {
throw new Error(`expected plugin ${pluginId}`);
}
return plugin;
}
function dropStartupConfigPaths(
plugin: InstalledPluginIndex["plugins"][number],
): InstalledPluginIndex["plugins"][number] {
return {
...plugin,
startup: {
sidecar: plugin.startup.sidecar,
memory: plugin.startup.memory,
deferConfiguredChannelFullLoadUntilAfterListen:
plugin.startup.deferConfiguredChannelFullLoadUntilAfterListen,
agentHarnesses: plugin.startup.agentHarnesses,
},
};
}
describe("loadPluginRegistrySnapshotWithMetadata", () => {
it("reuses a compatible current metadata snapshot", () => {
const env = createHermeticEnv(makeTempDir());
const config = {};
const workspaceDir = path.join(makeTempDir(), "workspace");
const policyHash = resolveInstalledPluginIndexPolicyHash(config);
const index: InstalledPluginIndex = {
version: 1,
hostContractVersion: "test",
compatRegistryVersion: "test",
migrationVersion: 1,
policyHash,
generatedAtMs: 0,
installRecords: {},
plugins: [],
diagnostics: [],
};
const snapshot: PluginMetadataSnapshot = {
policyHash,
configFingerprint: "",
workspaceDir,
index,
registryDiagnostics: [],
manifestRegistry: { plugins: [], diagnostics: [] },
plugins: [],
diagnostics: [],
byPluginId: new Map(),
normalizePluginId: (pluginId: string) => pluginId,
owners: {
channels: new Map(),
channelConfigs: new Map(),
providers: new Map(),
modelCatalogProviders: new Map(),
cliBackends: new Map(),
setupProviders: new Map(),
commandAliases: new Map(),
contracts: new Map(),
},
metrics: {
registrySnapshotMs: 0,
manifestRegistryMs: 0,
ownerMapsMs: 0,
totalMs: 0,
indexPluginCount: 0,
manifestPluginCount: 0,
},
};
setCurrentPluginMetadataSnapshot(snapshot, { config, env, workspaceDir });
const result = loadPluginRegistrySnapshotWithMetadata({ config, env, workspaceDir });
expect(result).toEqual({
snapshot: index,
source: "provided",
diagnostics: [],
});
});
it("does not treat diagnostic current metadata as provided registry input", () => {
const env = {
...createHermeticEnv(makeTempDir()),
OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1",
};
const config = {};
const workspaceDir = path.join(makeTempDir(), "workspace");
const policyHash = resolveInstalledPluginIndexPolicyHash(config);
const index: InstalledPluginIndex = {
version: 1,
hostContractVersion: "test",
compatRegistryVersion: "test",
migrationVersion: 1,
policyHash,
generatedAtMs: 0,
installRecords: {},
plugins: [],
diagnostics: [],
};
setCurrentPluginMetadataSnapshot(
{
policyHash,
configFingerprint: "",
workspaceDir,
index,
registryDiagnostics: [
{
level: "info",
code: "persisted-registry-missing",
message: "missing",
},
],
manifestRegistry: { plugins: [], diagnostics: [] },
plugins: [],
diagnostics: [],
byPluginId: new Map(),
normalizePluginId: (pluginId: string) => pluginId,
owners: {
channels: new Map(),
channelConfigs: new Map(),
providers: new Map(),
modelCatalogProviders: new Map(),
cliBackends: new Map(),
setupProviders: new Map(),
commandAliases: new Map(),
contracts: new Map(),
},
metrics: {
registrySnapshotMs: 0,
manifestRegistryMs: 0,
ownerMapsMs: 0,
totalMs: 0,
indexPluginCount: 0,
manifestPluginCount: 0,
},
},
{ config, env, workspaceDir },
);
const result = loadPluginRegistrySnapshotWithMetadata({ config, env, workspaceDir });
expect(result.source).not.toBe("provided");
});
it("does not reuse current metadata when explicit derivation inputs are supplied", () => {
const tempRoot = makeTempDir();
const env = {
...createHermeticEnv(tempRoot),
OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1",
};
const config = {};
const workspaceDir = path.join(tempRoot, "workspace");
const policyHash = resolveInstalledPluginIndexPolicyHash(config);
const currentIndex: InstalledPluginIndex = {
version: 1,
hostContractVersion: "test",
compatRegistryVersion: "test",
migrationVersion: 1,
policyHash,
generatedAtMs: 0,
installRecords: {},
plugins: [],
diagnostics: [],
};
setCurrentPluginMetadataSnapshot(
{
policyHash,
configFingerprint: "",
workspaceDir,
index: currentIndex,
registryDiagnostics: [],
manifestRegistry: { plugins: [], diagnostics: [] },
plugins: [],
diagnostics: [],
byPluginId: new Map(),
normalizePluginId: (pluginId: string) => pluginId,
owners: {
channels: new Map(),
channelConfigs: new Map(),
providers: new Map(),
modelCatalogProviders: new Map(),
cliBackends: new Map(),
setupProviders: new Map(),
commandAliases: new Map(),
contracts: new Map(),
},
metrics: {
registrySnapshotMs: 0,
manifestRegistryMs: 0,
ownerMapsMs: 0,
totalMs: 0,
indexPluginCount: 0,
manifestPluginCount: 0,
},
},
{ config, env, workspaceDir },
);
const result = loadPluginRegistrySnapshotWithMetadata({
config,
env,
workspaceDir,
candidates: [createCandidate(path.join(tempRoot, "candidate"), "explicit")],
});
expect(result.source).toBe("derived");
expect(result.snapshot.plugins.map((plugin) => plugin.pluginId)).toEqual(["explicit"]);
});
it("recovers managed npm plugins missing from a stale persisted registry", () => {
const tempRoot = makeTempDir();
const stateDir = path.join(tempRoot, "state");
const env = {
...createHermeticEnv(tempRoot),
OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1",
OPENCLAW_STATE_DIR: stateDir,
};
const config = {};
const whatsappDir = writeManagedNpmPlugin({
stateDir,
packageName: "@openclaw/whatsapp",
pluginId: "whatsapp",
version: "2026.5.2",
});
const staleIndex = loadInstalledPluginIndex({
config,
env,
stateDir,
installRecords: {},
});
expect(staleIndex.plugins.map((plugin) => plugin.pluginId)).not.toContain("whatsapp");
writePersistedInstalledPluginIndexSync(staleIndex, { stateDir });
const result = loadPluginRegistrySnapshotWithMetadata({
config,
env,
stateDir,
});
expect(result.source).toBe("derived");
expectDiagnosticsContainCode(result.diagnostics, "persisted-registry-stale-source");
expect(result.snapshot.installRecords.whatsapp).toEqual({
source: "npm",
spec: "@openclaw/whatsapp@2026.5.2",
installPath: whatsappDir,
version: "2026.5.2",
resolvedName: "@openclaw/whatsapp",
resolvedVersion: "2026.5.2",
resolvedSpec: "@openclaw/whatsapp@2026.5.2",
});
const whatsappPlugin = requirePluginRecord(result.snapshot.plugins, "whatsapp");
expect(whatsappPlugin.origin).toBe("global");
});
it("does not recover retained managed npm generations as install records", async () => {
const tempRoot = makeTempDir();
const stateDir = path.join(tempRoot, "state");
const env = {
...createHermeticEnv(tempRoot),
OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1",
OPENCLAW_STATE_DIR: stateDir,
};
const config = {};
const codexDir = writeManagedNpmPlugin({
stateDir,
packageName: "@openclaw/codex",
pluginId: "codex",
version: "2026.6.10-beta.1",
});
await markRetainedManagedNpmInstall({
packageDir: codexDir,
pluginId: "codex",
retainedAt: "2026-06-21T00:00:00.000Z",
reason: "test-retained-generation",
});
const staleIndex = loadInstalledPluginIndex({
config,
env,
stateDir,
installRecords: {},
});
writePersistedInstalledPluginIndexSync(staleIndex, { stateDir });
expect(loadInstalledPluginIndexInstallRecordsSync({ env, stateDir }).codex).toBeUndefined();
const result = loadPluginRegistrySnapshotWithMetadata({
config,
env,
stateDir,
});
expect(result.snapshot.installRecords.codex).toBeUndefined();
expect(result.snapshot.plugins.map((plugin) => plugin.pluginId)).not.toContain("codex");
});
it("does not trust package-owned retained npm marker files during recovery", () => {
const tempRoot = makeTempDir();
const stateDir = path.join(tempRoot, "state");
const env = {
...createHermeticEnv(tempRoot),
OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1",
OPENCLAW_STATE_DIR: stateDir,
};
const codexDir = writeManagedNpmPlugin({
stateDir,
packageName: "@openclaw/codex",
pluginId: "codex",
version: "2026.6.10-beta.1",
});
fs.writeFileSync(
path.join(codexDir, ".openclaw-retained-npm-install.json"),
'{"version":1,"pluginId":"codex"}\n',
"utf8",
);
expect(loadInstalledPluginIndexInstallRecordsSync({ env, stateDir }).codex).toBeDefined();
});
it("keeps vanished recovered install records on the persisted fast path", () => {
const tempRoot = makeTempDir();
const stateDir = path.join(tempRoot, "state");
const goneDir = path.join(tempRoot, "gone");
const env = {
...createHermeticEnv(tempRoot),
OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1",
OPENCLAW_STATE_DIR: stateDir,
};
writePersistedInstalledPluginIndexSync(
{
...loadInstalledPluginIndex({ config: {}, env, stateDir, installRecords: {} }),
installRecords: { gone: { source: "npm", spec: "gone@1.0.0", installPath: goneDir } },
},
{ stateDir },
);
const result = loadPluginRegistrySnapshotWithMetadata({ config: {}, env, stateDir });
expect(result.source).toBe("persisted");
expect(result.diagnostics).toStrictEqual([]);
});
it("keeps persisted manifestless Claude bundles on the fast path", () => {
const tempRoot = makeTempDir();
const rootDir = path.join(tempRoot, "workspace");
const stateDir = path.join(tempRoot, "state");
const env = { ...createHermeticEnv(tempRoot), OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1" };
const config = {
plugins: {
load: { paths: [rootDir] },
},
};
writeManifestlessClaudeBundle(rootDir);
const index = createManifestlessClaudeBundleIndex({ rootDir, env });
writePersistedInstalledPluginIndexSync(index, { stateDir });
const result = loadPluginRegistrySnapshotWithMetadata({
config,
env,
stateDir,
});
expect(result.source).toBe("persisted");
expect(result.diagnostics).toStrictEqual([]);
});
it("refreshes a memoized derived snapshot when workspace plugins are installed", () => {
const tempRoot = makeTempDir();
const workspaceDir = path.join(tempRoot, "workspace");
const env = { ...createHermeticEnv(tempRoot), OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1" };
const first = loadPluginRegistrySnapshotWithMetadata({ config: {}, env, workspaceDir });
expect(first.snapshot.plugins.map((plugin) => plugin.pluginId)).not.toContain("demo");
writePackagePlugin(path.join(workspaceDir, ".openclaw", "extensions", "demo"));
const second = loadPluginRegistrySnapshotWithMetadata({ config: {}, env, workspaceDir });
expect(second.snapshot.plugins.map((plugin) => plugin.pluginId)).toContain("demo");
});
it("ignores malformed load paths while fingerprinting memoized snapshots", () => {
const tempRoot = makeTempDir();
const env = { ...createHermeticEnv(tempRoot), OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1" };
const config = {
plugins: {
load: { paths: "not-an-array" },
},
} as unknown as OpenClawConfig;
expect(() => loadPluginRegistrySnapshotWithMetadata({ config, env })).not.toThrow();
});
it("keeps persisted package plugins when file hashes match", () => {
const tempRoot = makeTempDir();
const rootDir = path.join(tempRoot, "workspace");
const stateDir = path.join(tempRoot, "state");
const env = { ...createHermeticEnv(tempRoot), OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1" };
const config = {
plugins: {
load: { paths: [rootDir] },
},
};
writePackagePlugin(rootDir);
const index = loadInstalledPluginIndex({ config, env });
const [record] = index.plugins;
if (!record?.packageJson?.fileSignature || !record.manifestFile) {
throw new Error("expected package plugin index record with file signatures");
}
expect(record.manifestFile.size).toBe(
fs.statSync(path.join(rootDir, "openclaw.plugin.json")).size,
);
expect(record.packageJson.fileSignature.size).toBe(
fs.statSync(path.join(rootDir, "package.json")).size,
);
writePersistedInstalledPluginIndexSync(index, { stateDir });
const result = loadPluginRegistrySnapshotWithMetadata({
config,
env,
stateDir,
});
expect(result.source).toBe("persisted");
expect(result.diagnostics).toStrictEqual([]);
});
it("derives a complete index when a configured load-path plugin is missing", () => {
const tempRoot = makeTempDir();
const firstRoot = path.join(tempRoot, "first");
const secondRoot = path.join(tempRoot, "second");
const stateDir = path.join(tempRoot, "state");
const env = { ...createHermeticEnv(tempRoot), OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1" };
const staleConfig = {
plugins: {
load: { paths: [firstRoot] },
},
};
const config = {
plugins: {
load: { paths: [firstRoot, secondRoot] },
},
};
writePackagePlugin(firstRoot, {
pluginId: "first",
requiresPlugins: ["second"],
});
writePackagePlugin(secondRoot, { pluginId: "second" });
const staleIndex = loadInstalledPluginIndex({ config: staleConfig, env });
expect(staleIndex.policyHash).toBe(resolveInstalledPluginIndexPolicyHash(config));
writePersistedInstalledPluginIndexSync(staleIndex, { stateDir });
const result = loadPluginRegistrySnapshotWithMetadata({
config,
env,
stateDir,
});
expect(result.source).toBe("derived");
expectDiagnosticsContainCode(result.diagnostics, "persisted-registry-stale-source");
expect(result.snapshot.plugins.map((plugin) => plugin.pluginId)).toEqual(["first", "second"]);
expect(result.snapshot.plugins.map((plugin) => plugin.origin)).toEqual(["config", "config"]);
const scopedRegistry = loadPluginManifestRegistryForInstalledIndex({
index: result.snapshot,
config,
env,
pluginIds: ["first"],
includeDisabled: true,
});
expect(scopedRegistry.plugins.map((plugin) => plugin.id)).toEqual(["first"]);
expect(scopedRegistry.diagnostics).not.toContainEqual(
expect.objectContaining({
pluginId: "first",
message: expect.stringContaining('requires plugin "second"'),
}),
);
});
it("rebuilds when configured load-path precedence changes", () => {
const tempRoot = makeTempDir();
const firstRoot = path.join(tempRoot, "first");
const secondRoot = path.join(tempRoot, "second");
const stateDir = path.join(tempRoot, "state");
const env = { ...createHermeticEnv(tempRoot), OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1" };
const originalConfig = {
plugins: {
load: { paths: [firstRoot, secondRoot] },
},
};
const reorderedConfig = {
plugins: {
load: { paths: [secondRoot, firstRoot] },
},
};
writePackagePlugin(firstRoot, { pluginId: "duplicate" });
writePackagePlugin(secondRoot, { pluginId: "duplicate" });
const originalIndex = loadInstalledPluginIndex({ config: originalConfig, env });
expect(originalIndex.plugins.map((plugin) => plugin.rootDir)).toEqual([firstRoot]);
writePersistedInstalledPluginIndexSync(originalIndex, { stateDir });
const unchanged = loadPluginRegistrySnapshotWithMetadata({
config: originalConfig,
env,
stateDir,
});
expect(unchanged.source).toBe("persisted");
const reordered = loadPluginRegistrySnapshotWithMetadata({
config: reorderedConfig,
env,
stateDir,
});
expect(reordered.source).toBe("derived");
expectDiagnosticsContainCode(reordered.diagnostics, "persisted-registry-stale-source");
expect(reordered.snapshot.plugins.map((plugin) => plugin.rootDir)).toEqual([secondRoot]);
});
it("rebuilds legacy config-path persisted registries before startup scoping", () => {
const tempRoot = makeTempDir();
const rootDir = path.join(tempRoot, "workspace");
const stateDir = path.join(tempRoot, "state");
const env = { ...createHermeticEnv(tempRoot), OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1" };
const config = {
plugins: {
load: { paths: [rootDir] },
},
};
writePackagePlugin(rootDir, { configPaths: ["browser"] });
const index = loadInstalledPluginIndex({ config, env });
const legacyIndex: InstalledPluginIndex = {
...index,
plugins: index.plugins.map(dropStartupConfigPaths),
};
writePersistedInstalledPluginIndexSync(legacyIndex, { stateDir });
const result = loadPluginRegistrySnapshotWithMetadata({
config,
env,
stateDir,
});
expect(result.source).toBe("derived");
expectDiagnosticsContainCode(result.diagnostics, "persisted-registry-stale-source");
expect(result.snapshot.plugins[0]?.startup.configPaths).toEqual(["browser"]);
});
it("keeps persisted package plugins with dot-prefixed package metadata paths", () => {
const tempRoot = makeTempDir();
const rootDir = path.join(tempRoot, "workspace");
const stateDir = path.join(tempRoot, "state");
const env = { ...createHermeticEnv(tempRoot), OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1" };
const config = {
plugins: {
load: { paths: [rootDir] },
},
};
writePackagePlugin(rootDir);
const metaDir = path.join(rootDir, "..meta");
fs.mkdirSync(metaDir, { recursive: true });
const packageJsonPath = path.join(metaDir, "package.json");
fs.writeFileSync(packageJsonPath, JSON.stringify({ name: "demo", version: "1.0.0" }), "utf8");
const index = loadInstalledPluginIndex({ config, env });
const [plugin] = index.plugins;
if (!plugin) {
throw new Error("expected test plugin");
}
writePersistedInstalledPluginIndexSync(
{
...index,
plugins: [
{
...plugin,
packageJson: {
path: "..meta/package.json",
hash: fileHash(packageJsonPath),
fileSignature: fileSignature(packageJsonPath),
},
},
...index.plugins.slice(1),
],
},
{ stateDir },
);
const result = loadPluginRegistrySnapshotWithMetadata({
config,
env,
stateDir,
});
expect(result.source).toBe("persisted");
expect(result.diagnostics).toStrictEqual([]);
});
it.runIf(process.platform !== "win32")(
"treats persisted package metadata symlinks outside the plugin root as stale",
() => {
const tempRoot = makeTempDir();
const rootDir = path.join(tempRoot, "workspace");
const stateDir = path.join(tempRoot, "state");
const outsideDir = path.join(tempRoot, "outside");
const packageJsonPath = path.join(rootDir, "package.json");
const outsidePackageJsonPath = path.join(outsideDir, "package.json");
const env = { ...createHermeticEnv(tempRoot), OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1" };
const config = {
plugins: {
load: { paths: [rootDir] },
},
};
writePackagePlugin(rootDir);
fs.mkdirSync(outsideDir, { recursive: true });
fs.rmSync(packageJsonPath);
fs.writeFileSync(
outsidePackageJsonPath,
JSON.stringify({ name: "demo", version: "1.0.0" }),
"utf8",
);
fs.symlinkSync(outsidePackageJsonPath, packageJsonPath);
const index = loadInstalledPluginIndex({ config, env });
const [plugin] = index.plugins;
if (!plugin) {
throw new Error("expected test plugin");
}
writePersistedInstalledPluginIndexSync(
{
...index,
plugins: [
{
...plugin,
packageJson: {
path: "package.json",
hash: fileHash(packageJsonPath),
fileSignature: fileSignature(packageJsonPath),
},
},
...index.plugins.slice(1),
],
},
{ stateDir },
);
const result = loadPluginRegistrySnapshotWithMetadata({
config,
env,
stateDir,
});
expect(result.source).toBe("derived");
expectDiagnosticsContainCode(result.diagnostics, "persisted-registry-stale-source");
},
);
it("detects same-size same-mtime manifest replacements", () => {
const tempRoot = makeTempDir();
const rootDir = path.join(tempRoot, "workspace");
const stateDir = path.join(tempRoot, "state");
const env = { ...createHermeticEnv(tempRoot), OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1" };
const config = {
plugins: {
load: { paths: [rootDir] },
},
};
writePackagePlugin(rootDir);
const index = loadInstalledPluginIndex({ config, env });
writePersistedInstalledPluginIndexSync(index, { stateDir });
replaceFilePreservingSizeAndMtime(
path.join(rootDir, "openclaw.plugin.json"),
JSON.stringify({
id: "demo",
name: "Demo",
description: "two",
configSchema: { type: "object" },
}),
);
const result = loadPluginRegistrySnapshotWithMetadata({
config,
env,
stateDir,
});
expect(result.source).toBe("derived");
expectDiagnosticsContainCode(result.diagnostics, "persisted-registry-stale-source");
});
it("detects same-size same-mtime package.json replacements", () => {
const tempRoot = makeTempDir();
const rootDir = path.join(tempRoot, "workspace");
const stateDir = path.join(tempRoot, "state");
const env = { ...createHermeticEnv(tempRoot), OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1" };
const config = {
plugins: {
load: { paths: [rootDir] },
},
};
writePackagePlugin(rootDir);
const index = loadInstalledPluginIndex({ config, env });
writePersistedInstalledPluginIndexSync(index, { stateDir });
replaceFilePreservingSizeAndMtime(
path.join(rootDir, "package.json"),
JSON.stringify({ name: "demo", version: "1.0.1" }),
);
const result = loadPluginRegistrySnapshotWithMetadata({
config,
env,
stateDir,
});
expect(result.source).toBe("derived");
expectDiagnosticsContainCode(result.diagnostics, "persisted-registry-stale-source");
});
it("detects package.json replacements even when stored stat fields still match", () => {
const tempRoot = makeTempDir();
const rootDir = path.join(tempRoot, "workspace");
const stateDir = path.join(tempRoot, "state");
const env = { ...createHermeticEnv(tempRoot), OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1" };
const config = {
plugins: {
load: { paths: [rootDir] },
},
};
writePackagePlugin(rootDir);
const index = loadInstalledPluginIndex({ config, env });
replaceFilePreservingSizeAndMtime(
path.join(rootDir, "package.json"),
JSON.stringify({ name: "demo", version: "1.0.1" }),
);
const stat = fs.statSync(path.join(rootDir, "package.json"));
const [plugin] = index.plugins;
if (!plugin?.packageJson) {
throw new Error("expected test plugin package metadata");
}
const stalePlugin = {
...plugin,
packageJson: {
...plugin.packageJson,
fileSignature: {
size: stat.size,
mtimeMs: stat.mtimeMs,
ctimeMs: stat.ctimeMs,
},
},
};
const staleIndex: InstalledPluginIndex = {
...index,
plugins: [stalePlugin, ...index.plugins.slice(1)],
};
writePersistedInstalledPluginIndexSync(staleIndex, { stateDir });
const result = loadPluginRegistrySnapshotWithMetadata({
config,
env,
stateDir,
});
expect(result.source).toBe("derived");
expectDiagnosticsContainCode(result.diagnostics, "persisted-registry-stale-source");
});
it("keeps mixed source-checkout bundled roots from the same checkout", () => {
const tempRoot = makeTempDir();
const packageRoot = path.join(tempRoot, "openclaw");
const bundledRoot = path.join(packageRoot, "dist", "extensions");
const sourceRoot = path.join(packageRoot, "extensions");
const stateDir = path.join(tempRoot, "state");
const env = {
OPENCLAW_BUNDLED_PLUGINS_DIR: bundledRoot,
OPENCLAW_STATE_DIR: stateDir,
OPENCLAW_VERSION: "2026.4.26",
VITEST: "true",
};
fs.mkdirSync(path.join(packageRoot, "src"), { recursive: true });
fs.writeFileSync(path.join(packageRoot, ".git"), "gitdir: /tmp/mock\n", "utf8");
fs.writeFileSync(path.join(packageRoot, "pnpm-workspace.yaml"), "packages: []\n", "utf8");
writeBundledPlugin(path.join(bundledRoot, "codex"), "codex", "index.js");
writeBundledPlugin(path.join(sourceRoot, "whatsapp"), "whatsapp", "index.ts");
const index = loadInstalledPluginIndex({ config: {}, env, stateDir });
expect(index.plugins.map((plugin) => plugin.pluginId)).toEqual(["codex", "whatsapp"]);
expect(index.plugins.map((plugin) => plugin.rootDir)).toEqual([
fs.realpathSync(path.join(bundledRoot, "codex")),
fs.realpathSync(path.join(sourceRoot, "whatsapp")),
]);
writePersistedInstalledPluginIndexSync(index, { stateDir });
const result = loadPluginRegistrySnapshotWithMetadata({ config: {}, env, stateDir });
expect(result.source).toBe("persisted");
expect(result.diagnostics).toStrictEqual([]);
expect(result.snapshot.plugins.map((plugin) => plugin.pluginId)).toEqual(["codex", "whatsapp"]);
});
it("treats a persisted source bundled root as stale once its built peer appears", () => {
const tempRoot = makeTempDir();
const packageRoot = path.join(tempRoot, "openclaw");
const bundledRoot = path.join(packageRoot, "dist", "extensions");
const sourceRoot = path.join(packageRoot, "extensions");
const stateDir = path.join(tempRoot, "state");
const env = {
OPENCLAW_BUNDLED_PLUGINS_DIR: bundledRoot,
OPENCLAW_STATE_DIR: stateDir,
OPENCLAW_VERSION: "2026.4.26",
VITEST: "true",
};
fs.mkdirSync(path.join(packageRoot, "src"), { recursive: true });
fs.mkdirSync(bundledRoot, { recursive: true });
fs.writeFileSync(path.join(packageRoot, ".git"), "gitdir: /tmp/mock\n", "utf8");
fs.writeFileSync(path.join(packageRoot, "pnpm-workspace.yaml"), "packages: []\n", "utf8");
writeBundledPlugin(path.join(sourceRoot, "whatsapp"), "whatsapp", "index.ts");
const sourceIndex = loadInstalledPluginIndex({ config: {}, env, stateDir });
expect(sourceIndex.plugins.map((plugin) => plugin.rootDir)).toEqual([
fs.realpathSync(path.join(sourceRoot, "whatsapp")),
]);
writePersistedInstalledPluginIndexSync(sourceIndex, { stateDir });
writeBundledPlugin(path.join(bundledRoot, "whatsapp"), "whatsapp", "index.js");
const result = loadPluginRegistrySnapshotWithMetadata({ config: {}, env, stateDir });
expect(result.source).toBe("derived");
expectDiagnosticsContainCode(result.diagnostics, "persisted-registry-stale-source");
expect(result.snapshot.plugins.map((plugin) => plugin.rootDir)).toEqual([
fs.realpathSync(path.join(bundledRoot, "whatsapp")),
]);
});
it("keeps a persisted bind-mounted source overlay when its built peer exists", () => {
const tempRoot = makeTempDir();
const packageRoot = path.join(tempRoot, "openclaw");
const bundledRoot = path.join(packageRoot, "dist", "extensions");
const sourcePluginDir = path.join(packageRoot, "extensions", "whatsapp");
const stateDir = path.join(tempRoot, "state");
const env = {
OPENCLAW_BUNDLED_PLUGINS_DIR: bundledRoot,
OPENCLAW_STATE_DIR: stateDir,
OPENCLAW_VERSION: "2026.4.26",
VITEST: "true",
};
fs.mkdirSync(path.join(packageRoot, "src"), { recursive: true });
fs.mkdirSync(bundledRoot, { recursive: true });
fs.writeFileSync(path.join(packageRoot, ".git"), "gitdir: /tmp/mock\n", "utf8");
fs.writeFileSync(path.join(packageRoot, "pnpm-workspace.yaml"), "packages: []\n", "utf8");
writeBundledPlugin(sourcePluginDir, "whatsapp", "index.ts");
const sourceIndex = loadInstalledPluginIndex({ config: {}, env, stateDir });
writePersistedInstalledPluginIndexSync(sourceIndex, { stateDir });
writeBundledPlugin(path.join(bundledRoot, "whatsapp"), "whatsapp", "index.js");
mockLinuxMountInfo([sourcePluginDir]);
const result = loadPluginRegistrySnapshotWithMetadata({ config: {}, env, stateDir });
expect(result.source).toBe("persisted");
expect(result.diagnostics).toStrictEqual([]);
expect(result.snapshot.plugins.map((plugin) => plugin.rootDir)).toEqual([
fs.realpathSync(sourcePluginDir),
]);
});
it("treats persisted registry as stale when a plugin diagnostic source path no longer exists", () => {
const tempRoot = makeTempDir();
const stateDir = path.join(tempRoot, "state");
const env = {
...createHermeticEnv(tempRoot),
OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1",
OPENCLAW_STATE_DIR: stateDir,
};
const config = {};
const ghostDir = path.join(tempRoot, "extensions", "lossless-claw");
const npmPluginDir = writeManagedNpmPlugin({
stateDir,
packageName: "@martian-engineering/lossless-claw",
pluginId: "lossless-claw",
version: "0.9.4",
});
const staleIndex: InstalledPluginIndex = {
...loadInstalledPluginIndex({ config, env, stateDir, installRecords: {} }),
diagnostics: [
{
level: "warn",
message:
"installed plugin package requires compiled runtime output for TypeScript entry index.ts: expected ./dist/index.js",
pluginId: "lossless-claw",
source: ghostDir,
},
],
};
writePersistedInstalledPluginIndexSync(staleIndex, { stateDir });
const result = loadPluginRegistrySnapshotWithMetadata({ config, env, stateDir });
expect(result.source).toBe("derived");
expectDiagnosticsDoNotContainSource(result.snapshot.diagnostics, ghostDir);
const losslessPlugin = requirePluginRecord(result.snapshot.plugins, "lossless-claw");
expect(losslessPlugin.origin).toBe("global");
expect(losslessPlugin.source).toBe(
fs.realpathSync(path.join(npmPluginDir, "dist", "index.js")),
);
expectDiagnosticsContainCode(result.diagnostics, "persisted-registry-stale-source");
});
it("keeps persisted registry when a non-plugin diagnostic source path still does not exist", () => {
const tempRoot = makeTempDir();
const stateDir = path.join(tempRoot, "state");
const env = { ...createHermeticEnv(tempRoot), OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1" };
const config = {};
const missingConfiguredPath = path.join(tempRoot, "missing-configured-plugin");
const index: InstalledPluginIndex = {
...loadInstalledPluginIndex({ config, env, stateDir, installRecords: {} }),
diagnostics: [
{
level: "error",
message: `plugin path not found: ${missingConfiguredPath}`,
source: missingConfiguredPath,
},
],
};
writePersistedInstalledPluginIndexSync(index, { stateDir });
const result = loadPluginRegistrySnapshotWithMetadata({ config, env, stateDir });
expect(result.source).toBe("persisted");
expectDiagnosticsContainSource(result.snapshot.diagnostics, missingConfiguredPath);
expect(result.diagnostics).toStrictEqual([]);
});
});
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */