test(plugins): cover session extension registry lifecycle

Exercise pinned startup session extensions through WebSocket patching, release cleanup, standalone loading, and active-registry churn.
This commit is contained in:
杨浩宇0668001029
2026-06-16 11:36:43 +08:00
committed by Vincent Koc
parent ffa736f713
commit 0e71ce1174
4 changed files with 203 additions and 2 deletions
@@ -3,7 +3,20 @@
*/
import fs from "node:fs/promises";
import path from "node:path";
import { expect, test, vi } from "vitest";
import {
createPluginRegistryFixture,
registerTestPlugin,
} from "openclaw/plugin-sdk/plugin-test-contracts";
import { afterEach, expect, test, vi } from "vitest";
import { loadSessionStore } from "../config/sessions.js";
import { createEmptyPluginRegistry } from "../plugins/registry-empty.js";
import {
pinActivePluginSessionExtensionRegistry,
releasePinnedPluginSessionExtensionRegistry,
setActivePluginRegistry,
} from "../plugins/runtime.js";
import { createPluginRecord } from "../plugins/status.test-helpers.js";
import { buildGatewaySessionRow } from "./session-utils.js";
import { embeddedRunMock, rpcReq, testState, writeSessionStore } from "./test-helpers.js";
import {
setupGatewaySessionsTestHarness,
@@ -20,6 +33,11 @@ const {
resetConfiguredGlobalAgentSessionStore,
} = setupGatewaySessionsTestHarness();
afterEach(() => {
releasePinnedPluginSessionExtensionRegistry();
setActivePluginRegistry(createEmptyPluginRegistry());
});
type MockCalls = {
mock: { calls: unknown[][] };
};
@@ -186,6 +204,72 @@ function expectMainPatchBroadcast(
});
}
test("sessions.pluginPatch over WebSocket keeps pinned startup extensions after active churn", async () => {
const { config, registry } = createPluginRegistryFixture();
registerTestPlugin({
registry,
config,
record: createPluginRecord({
id: "session-pin-ws-fixture",
name: "Session Pin WS Fixture",
}),
register(api) {
api.registerSessionExtension({
namespace: "workflow",
description: "Pinned workflow state",
});
},
});
setActivePluginRegistry(registry.registry);
pinActivePluginSessionExtensionRegistry(registry.registry);
setActivePluginRegistry(createEmptyPluginRegistry());
const { storePath } = await createSessionStoreDir();
await writeSessionStore({
entries: {
main: sessionStoreEntry("sess-main"),
},
});
const { ws } = await openClient();
const patched = await rpcReq<{ ok: boolean; key: string; value: { state: string } }>(
ws,
"sessions.pluginPatch",
{
key: "main",
pluginId: "session-pin-ws-fixture",
namespace: "workflow",
value: { state: "after-active-registry-churn" },
},
);
ws.close();
expect(patched.ok).toBe(true);
expect(patched.payload).toEqual({
ok: true,
key: "agent:main:main",
value: { state: "after-active-registry-churn" },
});
const store = loadSessionStore(storePath);
const entry = store.main ?? store["agent:main:main"];
expect(entry).toBeDefined();
const row = buildGatewaySessionRow({
cfg: { session: { store: storePath } },
storePath,
store,
key: "agent:main:main",
entry,
});
expect(row.pluginExtensions).toEqual([
{
pluginId: "session-pin-ws-fixture",
namespace: "workflow",
value: { state: "after-active-registry-churn" },
},
]);
});
async function invokeSessionsCompact({
getRuntimeConfig,
params,
@@ -1626,6 +1626,75 @@ describe("host-hook fixture plugin contract", () => {
);
});
it("stops patching released pinned session extensions after registry removal", async () => {
const { config, registry } = createPluginRegistryFixture();
registerTestPlugin({
registry,
config,
record: createPluginRecord({
id: "session-release-fixture",
name: "Session Release Fixture",
}),
register(api) {
api.registerSessionExtension({
namespace: "workflow",
description: "Released workflow state",
});
},
});
setActivePluginRegistry(registry.registry);
pinActivePluginSessionExtensionRegistry(registry.registry);
setActivePluginRegistry(createEmptyPluginRegistry());
await withHostHookState(
"openclaw-host-hooks-session-release-",
async ({ storePath, tempConfig }) => {
await updateSessionStore(storePath, (store) => {
store["agent:main:main"] = {
sessionId: "session-1",
updatedAt: Date.now(),
};
return undefined;
});
await expect(
patchPluginSessionExtension({
cfg: tempConfig,
sessionKey: "agent:main:main",
pluginId: "session-release-fixture",
namespace: "workflow",
value: { state: "before-release" },
}),
).resolves.toEqual({
ok: true,
key: "agent:main:main",
value: { state: "before-release" },
});
releasePinnedPluginSessionExtensionRegistry(registry.registry);
await expect(
patchPluginSessionExtension({
cfg: tempConfig,
sessionKey: "agent:main:main",
pluginId: "session-release-fixture",
namespace: "workflow",
value: { state: "after-release" },
}),
).resolves.toEqual({
ok: false,
error: "unknown plugin session extension: session-release-fixture/workflow",
});
const entry = loadSessionStore(storePath)["agent:main:main"];
expect(entry).toBeDefined();
expect(
projectPluginSessionExtensionsSync({ sessionKey: "agent:main:main", entry }),
).toEqual([]);
},
);
});
it("models queued next-turn injections and agent_turn_prepare as one prompt context", async () => {
const { config, registry } = createPluginRegistryFixture();
registerTestPlugin({
+14
View File
@@ -213,6 +213,20 @@ describe("plugin runtime route registry", () => {
);
});
it("keeps non-empty pinned session extension registries authoritative over active extensions", () => {
const startupRegistry = createRegistryWithSessionExtension("startup", "presence");
const laterRegistry = createRegistryWithSessionExtension("later", "presence");
setActivePluginRegistry(startupRegistry);
pinActivePluginSessionExtensionRegistry(startupRegistry);
setActivePluginRegistry(laterRegistry);
expect(getActivePluginSessionExtensionRegistry()).toBe(startupRegistry);
expect(
getActivePluginSessionExtensionRegistry()?.sessionExtensions?.map((entry) => entry.pluginId),
).toEqual(["startup"]);
});
it("resolves the gateway command registry from pinned startup surfaces before active churn", () => {
const { startupRegistry, laterRegistry } = createRuntimeRegistryPair();
startupRegistry.commands.push({
@@ -3,7 +3,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { clearPluginLoaderCache, testing } from "../loader.js";
import { createEmptyPluginRegistry } from "../registry-empty.js";
import type { PluginRegistry } from "../registry-types.js";
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../runtime.js";
import {
getActivePluginSessionExtensionRegistry,
resetPluginRuntimeStateForTest,
setActivePluginRegistry,
} from "../runtime.js";
const loaderMocks = vi.hoisted(() => ({
loadOpenClawPlugins: vi.fn<typeof import("../loader.js").loadOpenClawPlugins>(),
@@ -30,6 +34,19 @@ function createRegistryWithPlugin(pluginId: string): PluginRegistry {
return registry;
}
function createRegistryWithSessionExtension(pluginId: string): PluginRegistry {
const registry = createRegistryWithPlugin(pluginId);
registry.sessionExtensions?.push({
pluginId,
extension: {
namespace: "workflow",
description: "Standalone workflow state",
},
source: "test",
});
return registry;
}
beforeEach(() => {
loaderMocks.loadOpenClawPlugins.mockReset();
});
@@ -114,4 +131,21 @@ describe("ensureStandaloneRuntimePluginRegistryLoaded", () => {
expect(result).toBe(loadedRegistry);
expect(loaderMocks.loadOpenClawPlugins).toHaveBeenCalledOnce();
});
it("pins loaded registries for the session-extension surface", () => {
const loadedRegistry = createRegistryWithSessionExtension("session-plugin");
loaderMocks.loadOpenClawPlugins.mockReturnValue(loadedRegistry);
const result = ensureStandaloneRuntimePluginRegistryLoaded({
loadOptions: {
config: { plugins: { allow: ["session-plugin"] } },
onlyPluginIds: ["session-plugin"],
workspaceDir: "/tmp/ws",
},
surface: "session-extension",
});
expect(result).toBe(loadedRegistry);
expect(getActivePluginSessionExtensionRegistry()).toBe(loadedRegistry);
});
});