mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 10:40:44 +00:00
272 lines
8.3 KiB
TypeScript
272 lines
8.3 KiB
TypeScript
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
clearCurrentPluginMetadataSnapshot,
|
|
resolvePluginMetadataControlPlaneFingerprint,
|
|
setCurrentPluginMetadataSnapshot,
|
|
} from "./current-plugin-metadata-snapshot.js";
|
|
import { resolveInstalledPluginIndexPolicyHash } from "./installed-plugin-index-policy.js";
|
|
import type { InstalledPluginIndex } from "./installed-plugin-index.js";
|
|
import { listOpenClawPluginManifestMetadata } from "./manifest-metadata-scan.js";
|
|
import { normalizeProviderModelIdWithManifest } from "./manifest-model-id-normalization.js";
|
|
import type { PluginMetadataSnapshot } from "./plugin-metadata-snapshot.js";
|
|
import { createEmptyPluginRegistry } from "./registry-empty.js";
|
|
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "./runtime.js";
|
|
|
|
const ORIGINAL_ENV = {
|
|
OPENCLAW_STATE_DIR: process.env.OPENCLAW_STATE_DIR,
|
|
OPENCLAW_HOME: process.env.OPENCLAW_HOME,
|
|
OPENCLAW_DISABLE_BUNDLED_PLUGINS: process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS,
|
|
OPENCLAW_BUNDLED_PLUGINS_DIR: process.env.OPENCLAW_BUNDLED_PLUGINS_DIR,
|
|
} as const;
|
|
|
|
const tempDirs: string[] = [];
|
|
|
|
function makeTempDir(): string {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-model-id-normalization-"));
|
|
tempDirs.push(dir);
|
|
return dir;
|
|
}
|
|
|
|
function restoreEnv(): void {
|
|
for (const [key, value] of Object.entries(ORIGINAL_ENV)) {
|
|
if (value === undefined) {
|
|
delete process.env[key];
|
|
} else {
|
|
process.env[key] = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
function writeInstallIndex(params: { stateDir: string; pluginDir: string }): void {
|
|
const indexPath = path.join(params.stateDir, "plugins", "installs.json");
|
|
fs.mkdirSync(path.dirname(indexPath), { recursive: true });
|
|
fs.writeFileSync(
|
|
indexPath,
|
|
JSON.stringify({
|
|
plugins: [
|
|
{
|
|
id: "normalizer",
|
|
rootDir: params.pluginDir,
|
|
origin: "global",
|
|
},
|
|
],
|
|
}),
|
|
"utf-8",
|
|
);
|
|
}
|
|
|
|
function writeNormalizerManifest(params: { pluginDir: string; prefix: string }): void {
|
|
fs.mkdirSync(params.pluginDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(params.pluginDir, "index.ts"),
|
|
"throw new Error('runtime entry should not load while reading manifests');\n",
|
|
"utf-8",
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(params.pluginDir, "openclaw.plugin.json"),
|
|
JSON.stringify({
|
|
id: "normalizer",
|
|
configSchema: { type: "object" },
|
|
providers: ["demo"],
|
|
modelIdNormalization: {
|
|
providers: {
|
|
demo: {
|
|
prefixWhenBare: params.prefix,
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
"utf-8",
|
|
);
|
|
}
|
|
|
|
function createCurrentSnapshot(params: {
|
|
manifestHash: string;
|
|
prefix: string;
|
|
workspaceDir?: string;
|
|
}): PluginMetadataSnapshot {
|
|
const policyHash = resolveInstalledPluginIndexPolicyHash({});
|
|
const index: InstalledPluginIndex = {
|
|
version: 1,
|
|
hostContractVersion: "test-host",
|
|
compatRegistryVersion: "test-compat",
|
|
migrationVersion: 1,
|
|
policyHash,
|
|
generatedAtMs: 0,
|
|
installRecords: {},
|
|
plugins: [
|
|
{
|
|
pluginId: "normalizer",
|
|
manifestPath: `/tmp/normalizer-${params.manifestHash}/openclaw.plugin.json`,
|
|
manifestHash: params.manifestHash,
|
|
source: `/tmp/normalizer-${params.manifestHash}/index.ts`,
|
|
rootDir: `/tmp/normalizer-${params.manifestHash}`,
|
|
origin: "global",
|
|
enabled: true,
|
|
startup: {
|
|
sidecar: false,
|
|
memory: false,
|
|
deferConfiguredChannelFullLoadUntilAfterListen: false,
|
|
agentHarnesses: [],
|
|
},
|
|
compat: [],
|
|
},
|
|
],
|
|
diagnostics: [],
|
|
};
|
|
return {
|
|
policyHash,
|
|
configFingerprint: resolvePluginMetadataControlPlaneFingerprint(
|
|
{},
|
|
{
|
|
env: process.env,
|
|
index,
|
|
policyHash,
|
|
workspaceDir: params.workspaceDir,
|
|
},
|
|
),
|
|
workspaceDir: params.workspaceDir,
|
|
index,
|
|
plugins: [
|
|
{
|
|
id: "normalizer",
|
|
modelIdNormalization: {
|
|
providers: {
|
|
demo: {
|
|
prefixWhenBare: params.prefix,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
} as unknown as PluginMetadataSnapshot;
|
|
}
|
|
|
|
function normalizeDemoModel(modelId = "demo-model"): string | undefined {
|
|
return normalizeProviderModelIdWithManifest({
|
|
provider: "demo",
|
|
context: { provider: "demo", modelId },
|
|
});
|
|
}
|
|
|
|
describe("manifest model id normalization", () => {
|
|
beforeEach(() => {
|
|
resetPluginRuntimeStateForTest();
|
|
});
|
|
|
|
afterEach(() => {
|
|
clearCurrentPluginMetadataSnapshot();
|
|
resetPluginRuntimeStateForTest();
|
|
restoreEnv();
|
|
for (const dir of tempDirs.splice(0)) {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("refreshes cached policies when the current metadata snapshot changes", () => {
|
|
setCurrentPluginMetadataSnapshot(
|
|
createCurrentSnapshot({
|
|
manifestHash: "alpha",
|
|
prefix: "alpha",
|
|
}),
|
|
{ config: {}, env: process.env },
|
|
);
|
|
|
|
expect(normalizeDemoModel()).toBe("alpha/demo-model");
|
|
expect(normalizeDemoModel("second-model")).toBe("alpha/second-model");
|
|
|
|
setCurrentPluginMetadataSnapshot(
|
|
createCurrentSnapshot({
|
|
manifestHash: "bravo",
|
|
prefix: "bravo",
|
|
}),
|
|
{ config: {}, env: process.env },
|
|
);
|
|
|
|
expect(normalizeDemoModel()).toBe("bravo/demo-model");
|
|
});
|
|
|
|
it("uses workspace-scoped current metadata through the active plugin runtime", () => {
|
|
setActivePluginRegistry(
|
|
createEmptyPluginRegistry(),
|
|
"workspace-a",
|
|
"gateway-bindable",
|
|
"/workspace/a",
|
|
);
|
|
setCurrentPluginMetadataSnapshot(
|
|
createCurrentSnapshot({
|
|
manifestHash: "alpha",
|
|
prefix: "alpha",
|
|
workspaceDir: "/workspace/a",
|
|
}),
|
|
{ config: {}, env: process.env },
|
|
);
|
|
|
|
expect(normalizeDemoModel()).toBe("alpha/demo-model");
|
|
expect(normalizeDemoModel("second-model")).toBe("alpha/second-model");
|
|
|
|
setCurrentPluginMetadataSnapshot(
|
|
createCurrentSnapshot({
|
|
manifestHash: "bravo",
|
|
prefix: "bravo",
|
|
workspaceDir: "/workspace/a",
|
|
}),
|
|
{ config: {}, env: process.env },
|
|
);
|
|
|
|
expect(normalizeDemoModel()).toBe("bravo/demo-model");
|
|
});
|
|
|
|
it("reflects manifest edits and state-dir changes on the next lookup", () => {
|
|
const stateDirA = makeTempDir();
|
|
const pluginDirA = path.join(stateDirA, "extensions", "normalizer");
|
|
writeInstallIndex({ stateDir: stateDirA, pluginDir: pluginDirA });
|
|
writeNormalizerManifest({ pluginDir: pluginDirA, prefix: "alpha" });
|
|
|
|
process.env.OPENCLAW_STATE_DIR = stateDirA;
|
|
process.env.OPENCLAW_HOME = undefined;
|
|
process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS = "1";
|
|
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = undefined;
|
|
|
|
expect(normalizeDemoModel()).toBe("alpha/demo-model");
|
|
|
|
writeNormalizerManifest({ pluginDir: pluginDirA, prefix: "bravo-local" });
|
|
expect(normalizeDemoModel()).toBe("bravo-local/demo-model");
|
|
|
|
const stateDirB = makeTempDir();
|
|
const pluginDirB = path.join(stateDirB, "extensions", "normalizer");
|
|
writeInstallIndex({ stateDir: stateDirB, pluginDir: pluginDirB });
|
|
writeNormalizerManifest({ pluginDir: pluginDirB, prefix: "charlie" });
|
|
|
|
process.env.OPENCLAW_STATE_DIR = stateDirB;
|
|
expect(normalizeDemoModel()).toBe("charlie/demo-model");
|
|
});
|
|
|
|
it("reuses manifest metadata while file fingerprints are unchanged", () => {
|
|
const stateDir = makeTempDir();
|
|
const pluginDir = path.join(stateDir, "extensions", "normalizer");
|
|
const manifestPath = path.join(pluginDir, "openclaw.plugin.json");
|
|
writeInstallIndex({ stateDir, pluginDir });
|
|
writeNormalizerManifest({ pluginDir, prefix: "alpha" });
|
|
|
|
process.env.OPENCLAW_STATE_DIR = stateDir;
|
|
process.env.OPENCLAW_HOME = undefined;
|
|
process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS = "1";
|
|
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = undefined;
|
|
|
|
const readFileSyncSpy = vi.spyOn(fs, "readFileSync");
|
|
|
|
expect(listOpenClawPluginManifestMetadata(process.env)).toHaveLength(1);
|
|
expect(listOpenClawPluginManifestMetadata(process.env)).toHaveLength(1);
|
|
|
|
const manifestReads = readFileSyncSpy.mock.calls.filter(
|
|
([filePath]) => String(filePath) === manifestPath,
|
|
);
|
|
expect(manifestReads).toHaveLength(1);
|
|
readFileSyncSpy.mockRestore();
|
|
});
|
|
});
|