mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
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:
committed by
Vincent Koc
parent
ffa736f713
commit
0e71ce1174
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user