test: clear plugin runtimes between shared files (#111556)

This commit is contained in:
Peter Steinberger
2026-07-19 15:28:46 -07:00
committed by GitHub
parent a1e5b6ef0e
commit 5fcf7e75de
5 changed files with 145 additions and 19 deletions
+30
View File
@@ -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();
}
+32
View File
@@ -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();
});
});
+2 -19
View File
@@ -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 {
+77
View File
@@ -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 });
}
});
+4
View File
@@ -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?.();