diff --git a/src/gateway/server-close.model-acquisition.test.ts b/src/gateway/server-close.model-acquisition.test.ts index 4da832e07acc..ac4bad9a2b2b 100644 --- a/src/gateway/server-close.model-acquisition.test.ts +++ b/src/gateway/server-close.model-acquisition.test.ts @@ -10,6 +10,7 @@ import { registerPreparedModelRuntimePublicationListener } from "../agents/prepa import { registerPreparedModelRuntimeClose } from "../agents/prepared-model-runtime.lifecycle.js"; import { getPreparedModelRuntimeStartupStatus } from "../agents/prepared-model-runtime.startup-status.js"; import { GATEWAY_SHUTDOWN_TIMEOUT_MS } from "../infra/gateway-shutdown-budget.js"; +import { waitForPluginCacheRetirement } from "../plugins/plugin-cache.js"; import { getPluginValueInstance } from "../plugins/plugin-instance-scope.js"; import { createDeferredCore } from "../shared/deferred.js"; import { createGatewayMetadataCloseFixture } from "./server-close.metadata.test-support.js"; @@ -189,6 +190,10 @@ it.each(["static catalog", "synthetic auth"] as const)( if (cleanupFailure) { // Other shutdown work must not hide a discarded plugin cleanup outcome. expect(collectNestedErrorCandidates(closeError)).toContain(cleanupFailure); + // The process-cache reset retains the same outcome for its next observer. + expect((await waitForPluginCacheRetirement()).failures).toEqual([ + { pluginId: fixture.pluginId, hookId: "instance", error: cleanupFailure }, + ]); } else { expect(closeError).toBeUndefined(); } diff --git a/src/gateway/server-close.plugin-failure.test.ts b/src/gateway/server-close.plugin-failure.test.ts index 369f76fd2cfc..c9469129cabd 100644 --- a/src/gateway/server-close.plugin-failure.test.ts +++ b/src/gateway/server-close.plugin-failure.test.ts @@ -11,6 +11,7 @@ import { import { SUPERVISOR_HINT_ENV_VARS } from "../infra/supervisor-markers.js"; import { flushLogger, setLoggerOverride } from "../logging/logger.js"; import { getGatewayPluginMetadataSnapshot } from "../plugins/current-plugin-metadata-state.js"; +import { waitForPluginCacheRetirement } from "../plugins/plugin-cache.js"; import { getPluginValueInstance } from "../plugins/plugin-instance-scope.js"; import { PluginInstance } from "../plugins/plugin-instance.js"; import type { MemoryPluginRuntime } from "../plugins/registry-contribution-types.js"; @@ -218,6 +219,12 @@ it.each(["final", "sibling", "cache", "restart", "memory-and-plugin", "memory-on if (hasPluginFailure) { expect.soft(collectNestedErrorCandidates(error)).toContain(pluginFailure); expect(pluginSawOpenDatabase).toBe(true); + if (mode === "cache" || mode === "restart") { + // The process-cache reset retains the same outcome for its next observer. + expect((await waitForPluginCacheRetirement()).failures).toEqual([ + { pluginId: fixture.pluginId, hookId: "instance", error: pluginFailure }, + ]); + } } else { expect(error).toBeUndefined(); } diff --git a/test/setup-openclaw-runtime.ts b/test/setup-openclaw-runtime.ts index 172f55b651fc..082df76f2f6e 100644 --- a/test/setup-openclaw-runtime.ts +++ b/test/setup-openclaw-runtime.ts @@ -340,6 +340,16 @@ function resolveDefaultPluginRegistryProxy(): PluginRegistry { return workerRuntimeState.defaultPluginRegistry; } +async function settlePluginCacheRetirements(): Promise { + const { waitForPluginCacheRetirement } = await vi.importActual< + typeof import("../src/plugins/plugin-cache.js") + >("../src/plugins/plugin-cache.js"); + const { failures } = await waitForPluginCacheRetirement(); + if (failures.length > 0) { + throw new AggregateError(failures, "Plugin cache retirement failed during test cleanup"); + } +} + async function installDefaultPluginRegistry(): Promise { // Worker module resets retire the lifecycle maps. Activate through the current // real module, never a cached closure or a suite's partial runtime mock. @@ -349,6 +359,7 @@ async function installDefaultPluginRegistry(): Promise { workerRuntimeState.materializedDefaultPluginRegistry = null; resetPluginRuntimeStateForTest(); setActivePluginRegistry(resolveDefaultPluginRegistryProxy()); + await settlePluginCacheRetirements(); } // Some suites import channel/plugin consumers at module top level, before @@ -393,4 +404,5 @@ afterAll(async () => { await drainSessionStoreWriterQueuesForTest(); clearSessionStoreCacheForTest(); await drainFileLockStateForTest(); + await settlePluginCacheRetirements(); });