mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 02:06:43 +00:00
test: clear plugin runtimes between shared files (#111556)
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
type NamedPluginRuntimeStoreSlot = { runtime: unknown };
|
||||
type NamedPluginRuntimeStoreRegistry = Map<string, NamedPluginRuntimeStoreSlot>;
|
||||
|
||||
const pluginRuntimeStoreRegistryKey = Symbol.for("openclaw.plugin-sdk.runtime-store-registry");
|
||||
|
||||
function getNamedPluginRuntimeStoreRegistry(): NamedPluginRuntimeStoreRegistry {
|
||||
const globalRecord = globalThis as typeof globalThis & {
|
||||
[pluginRuntimeStoreRegistryKey]?: NamedPluginRuntimeStoreRegistry;
|
||||
};
|
||||
globalRecord[pluginRuntimeStoreRegistryKey] ??= new Map();
|
||||
return globalRecord[pluginRuntimeStoreRegistryKey];
|
||||
}
|
||||
|
||||
export function getNamedPluginRuntimeStoreSlot(key: string): NamedPluginRuntimeStoreSlot {
|
||||
const registry = getNamedPluginRuntimeStoreRegistry();
|
||||
let slot = registry.get(key);
|
||||
if (!slot) {
|
||||
slot = { runtime: null };
|
||||
registry.set(key, slot);
|
||||
}
|
||||
return slot;
|
||||
}
|
||||
|
||||
export function clearNamedPluginRuntimeStoresForTest(): void {
|
||||
const registry = getNamedPluginRuntimeStoreRegistry();
|
||||
for (const slot of registry.values()) {
|
||||
slot.runtime = null;
|
||||
}
|
||||
registry.clear();
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
import { importFreshModule } from "openclaw/plugin-sdk/test-fixtures";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { clearNamedPluginRuntimeStoresForTest } from "./runtime-store-registry.js";
|
||||
import { createPluginRuntimeStore } from "./runtime-store.js";
|
||||
|
||||
describe("createPluginRuntimeStore", () => {
|
||||
@@ -118,4 +119,35 @@ describe("createPluginRuntimeStore", () => {
|
||||
|
||||
expect(secondStore.getRuntime()).toEqual({ value: "shared" });
|
||||
});
|
||||
|
||||
test("clears and detaches every named runtime slot without changing legacy stores", () => {
|
||||
const firstNamedStore = createPluginRuntimeStore<{ value: string }>({
|
||||
pluginId: "first-reset-plugin",
|
||||
errorMessage: "first runtime not initialized",
|
||||
});
|
||||
const secondNamedStore = createPluginRuntimeStore<{ value: string }>({
|
||||
pluginId: "second-reset-plugin",
|
||||
errorMessage: "second runtime not initialized",
|
||||
});
|
||||
const legacyStore = createPluginRuntimeStore<{ value: string }>(
|
||||
"legacy runtime not initialized",
|
||||
);
|
||||
|
||||
firstNamedStore.setRuntime({ value: "first" });
|
||||
secondNamedStore.setRuntime({ value: "second" });
|
||||
legacyStore.setRuntime({ value: "legacy" });
|
||||
|
||||
clearNamedPluginRuntimeStoresForTest();
|
||||
|
||||
expect(firstNamedStore.tryGetRuntime()).toBeNull();
|
||||
expect(secondNamedStore.tryGetRuntime()).toBeNull();
|
||||
expect(legacyStore.getRuntime()).toEqual({ value: "legacy" });
|
||||
|
||||
const replacementStore = createPluginRuntimeStore<{ value: string }>({
|
||||
pluginId: "first-reset-plugin",
|
||||
errorMessage: "replacement runtime not initialized",
|
||||
});
|
||||
firstNamedStore.setRuntime({ value: "stale" });
|
||||
expect(replacementStore.tryGetRuntime()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
// Runtime store exports expose plugin runtime type contracts without loading runtime code.
|
||||
export type { PluginRuntime } from "../plugins/runtime/types.js";
|
||||
|
||||
const pluginRuntimeStoreRegistryKey = Symbol.for("openclaw.plugin-sdk.runtime-store-registry");
|
||||
|
||||
type PluginRuntimeStoreRegistry = Map<string, { runtime: unknown }>;
|
||||
import { getNamedPluginRuntimeStoreSlot } from "./runtime-store-registry.js";
|
||||
type PluginRuntimeStoreKeyOptions = {
|
||||
/** Explicit global registry key for shared runtime slots. */
|
||||
key: string;
|
||||
@@ -18,14 +15,6 @@ type PluginRuntimeStorePluginOptions = {
|
||||
};
|
||||
type PluginRuntimeStoreOptions = PluginRuntimeStoreKeyOptions | PluginRuntimeStorePluginOptions;
|
||||
|
||||
function getPluginRuntimeStoreRegistry(): PluginRuntimeStoreRegistry {
|
||||
const globalRecord = globalThis as typeof globalThis & {
|
||||
[pluginRuntimeStoreRegistryKey]?: PluginRuntimeStoreRegistry;
|
||||
};
|
||||
globalRecord[pluginRuntimeStoreRegistryKey] ??= new Map();
|
||||
return globalRecord[pluginRuntimeStoreRegistryKey];
|
||||
}
|
||||
|
||||
function pluginRuntimeStoreKeyForPluginId(pluginId: string): string {
|
||||
const normalizedPluginId = pluginId.trim();
|
||||
if (!normalizedPluginId) {
|
||||
@@ -82,13 +71,7 @@ export function createPluginRuntimeStore<T>(options: string | PluginRuntimeStore
|
||||
: (() => {
|
||||
// Store named slots on globalThis so duplicate SDK module instances
|
||||
// still share one runtime for the same plugin id or explicit key.
|
||||
const registry = getPluginRuntimeStoreRegistry();
|
||||
let existingSlot = registry.get(resolved.key);
|
||||
if (!existingSlot) {
|
||||
existingSlot = { runtime: null };
|
||||
registry.set(resolved.key, existingSlot);
|
||||
}
|
||||
return existingSlot;
|
||||
return getNamedPluginRuntimeStoreSlot(resolved.key);
|
||||
})();
|
||||
|
||||
return {
|
||||
|
||||
@@ -118,3 +118,80 @@ it("applies vi.mock factories after a sibling file fails during collection", asy
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("clears named plugin runtime slots between files", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-runtime-store-runner-"));
|
||||
try {
|
||||
const write = (name: string, content: string) =>
|
||||
fs.writeFile(path.join(root, name), content, "utf-8");
|
||||
const runtimeStorePath = JSON.stringify(
|
||||
path.join(repoRoot, "src", "plugin-sdk", "runtime-store.ts"),
|
||||
);
|
||||
await fs.symlink(
|
||||
path.join(repoRoot, "node_modules"),
|
||||
path.join(root, "node_modules"),
|
||||
"junction",
|
||||
);
|
||||
await write(
|
||||
"a-seed.test.ts",
|
||||
[
|
||||
`import { createPluginRuntimeStore } from ${runtimeStorePath};`,
|
||||
'import { expect, it } from "vitest";',
|
||||
'const store = createPluginRuntimeStore({ pluginId: "fixture", errorMessage: "missing" });',
|
||||
'it("seeds a named runtime slot", () => {',
|
||||
' store.setRuntime({ source: "first-file" });',
|
||||
' expect(store.getRuntime()).toEqual({ source: "first-file" });',
|
||||
"});",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
await write(
|
||||
"b-observe.test.ts",
|
||||
[
|
||||
`import { createPluginRuntimeStore } from ${runtimeStorePath};`,
|
||||
'import { expect, it } from "vitest";',
|
||||
'const store = createPluginRuntimeStore({ pluginId: "fixture", errorMessage: "missing" });',
|
||||
'it("starts without a runtime from the previous file", () => {',
|
||||
" expect(store.tryGetRuntime()).toBeNull();",
|
||||
"});",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
await write(
|
||||
"vitest.config.ts",
|
||||
[
|
||||
'import { defineConfig } from "vitest/config";',
|
||||
'import { BaseSequencer } from "vitest/node";',
|
||||
"class AlphabeticalSequencer extends BaseSequencer {",
|
||||
' override async sort(files: Parameters<BaseSequencer["sort"]>[0]) {',
|
||||
" return [...files].sort((a, b) => a.moduleId.localeCompare(b.moduleId));",
|
||||
" }",
|
||||
"}",
|
||||
"export default defineConfig({",
|
||||
` cacheDir: ${JSON.stringify(path.join(root, ".vite"))},`,
|
||||
" test: {",
|
||||
" isolate: false,",
|
||||
" fileParallelism: false,",
|
||||
" maxWorkers: 1,",
|
||||
" sequence: { sequencer: AlphabeticalSequencer },",
|
||||
` runner: ${JSON.stringify(path.join(repoRoot, "test", "non-isolated-runner.ts"))},`,
|
||||
" },",
|
||||
"});",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
const vitestEntry = path.join(repoRoot, "node_modules", "vitest", "vitest.mjs");
|
||||
const result = await execFileAsync(
|
||||
process.execPath,
|
||||
[vitestEntry, "run", "--root", root, "--config", path.join(root, "vitest.config.ts")],
|
||||
{ cwd: repoRoot, env: childEnv(), maxBuffer: 16 * 1024 * 1024 },
|
||||
).catch((error: unknown) => error as { stdout?: string; stderr?: string });
|
||||
const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
|
||||
|
||||
expect(output).toContain("2 passed");
|
||||
expect(output).not.toContain("first-file");
|
||||
} finally {
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { TestRunner, type RunnerTask, type RunnerTestFile, vi } from "vitest";
|
||||
import { clearNamedPluginRuntimeStoresForTest } from "../src/plugin-sdk/runtime-store-registry.js";
|
||||
|
||||
type EvaluatedModuleNode = {
|
||||
promise?: unknown;
|
||||
@@ -363,6 +364,9 @@ export default class OpenClawNonIsolatedRunner extends TestRunner {
|
||||
vi.clearAllMocks();
|
||||
resetOpenClawGlobalRunState();
|
||||
resetOpenClawGlobalDiagnosticState();
|
||||
// Named plugin runtimes intentionally survive duplicate module evaluation in production.
|
||||
// Clear their shared slots here so one test file cannot lend a partial runtime to the next.
|
||||
clearNamedPluginRuntimeStoresForTest();
|
||||
vi.resetModules();
|
||||
const internals = this as unknown as TestRunnerInternals;
|
||||
internals.moduleRunner?.mocker?.reset?.();
|
||||
|
||||
Reference in New Issue
Block a user