mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 01:11:44 +00:00
* 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
1129 lines
38 KiB
TypeScript
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. */
|