| |
| |
| import { expectDefined } from "@openclaw/normalization-core"; |
| import { describe, expect, it, vi } from "vitest"; |
| import type { OpenClawConfig } from "../config/types.openclaw.js"; |
| import { buildPluginApi } from "./api-builder.js"; |
| import { runPluginRegisterSyncInRegistry } from "./loader-module-runtime.js"; |
| import { createEmptyPluginRegistry } from "./registry-empty.js"; |
| import type { PluginRuntime } from "./runtime/types.js"; |
| import type { OpenClawPluginApi } from "./types.js"; |
|
|
| function createPluginApi(handlers: Parameters<typeof buildPluginApi>[0]["handlers"] = {}) { |
| return buildPluginApi({ |
| id: "late-call-fixture", |
| name: "Late Call Fixture", |
| source: "test", |
| registrationMode: "full", |
| config: {} as OpenClawConfig, |
| runtime: {} as PluginRuntime, |
| logger: { info() {}, warn() {}, error() {}, debug() {} }, |
| resolvePath: (input) => input, |
| handlers, |
| }); |
| } |
|
|
| function captureRegisteredPluginApi(handlers: Parameters<typeof buildPluginApi>[0]["handlers"]) { |
| const api = createPluginApi(handlers); |
| let captured: OpenClawPluginApi | undefined; |
| runPluginRegisterSyncInRegistry( |
| (pluginApi) => { |
| captured = pluginApi; |
| }, |
| api, |
| createEmptyPluginRegistry(), |
| "late-call-fixture", |
| ); |
| return expectDefined(captured, "captured plugin api"); |
| } |
|
|
| describe("plugin api lifecycle", () => { |
| it("returns inert results for unwired runtime and scheduler handlers", async () => { |
| const api = createPluginApi(); |
| const sessionKey = "agent:main:main"; |
|
|
| expect(api.emitAgentEvent({ runId: "run", stream: "lifecycle", data: {} })).toEqual({ |
| emitted: false, |
| reason: "not wired", |
| }); |
| expect(api.setRunContext({ runId: "run", namespace: "workflow", value: 1 })).toBe(false); |
| expect(api.getRunContext({ runId: "run", namespace: "workflow" })).toBeUndefined(); |
| expect( |
| api.registerSessionSchedulerJob({ id: "job", sessionKey, kind: "session-turn" }), |
| ).toBeUndefined(); |
| await expect(api.enqueueNextTurnInjection({ sessionKey, text: "queued" })).resolves.toEqual({ |
| enqueued: false, |
| id: "", |
| sessionKey, |
| }); |
| await expect( |
| api.sendSessionAttachment({ sessionKey, files: [{ path: "/tmp/attachment.txt" }] }), |
| ).resolves.toEqual({ ok: false, error: "not wired" }); |
| await expect( |
| api.scheduleSessionTurn({ sessionKey, message: "wake", delayMs: 1_000 }), |
| ).resolves.toBeUndefined(); |
| await expect(api.unscheduleSessionTurnsByTag({ sessionKey, tag: "wake" })).resolves.toEqual({ |
| removed: 0, |
| failed: 0, |
| }); |
| }); |
|
|
| it("keeps inherited handlers, undefined defaults, and one captured CLI lookup", () => { |
| const reads: string[] = []; |
| const registerCli = vi.fn<OpenClawPluginApi["registerCli"]>(); |
| const registerHook = vi.fn<OpenClawPluginApi["registerHook"]>(); |
| class InheritedHandlers { |
| get registerCli() { |
| reads.push("cli"); |
| return registerCli; |
| } |
| get registerTool() { |
| reads.push("tool"); |
| return undefined; |
| } |
| get registerHook() { |
| reads.push("hook"); |
| return registerHook; |
| } |
| } |
| const api = createPluginApi(new InheritedHandlers()); |
| const hook: Parameters<OpenClawPluginApi["registerHook"]>[1] = () => {}; |
| const registrar: Parameters<OpenClawPluginApi["registerCli"]>[0] = () => {}; |
|
|
| const toolFactory = vi.fn(() => null); |
| expect(api.registerTool(toolFactory)).toBeUndefined(); |
| expect(toolFactory).not.toHaveBeenCalled(); |
| expect(api.registerHook).toBe(registerHook); |
| expect(api.registerCli).toBe(registerCli); |
| api.registerHook("message_received", hook); |
| expect(registerHook).toHaveBeenCalledExactlyOnceWith("message_received", hook); |
| api.registerNodeCliFeature(registrar, { commands: ["camera"] }); |
| expect(registerCli).toHaveBeenCalledExactlyOnceWith(registrar, { |
| commands: ["camera"], |
| parentPath: ["nodes"], |
| }); |
| expect(reads).toEqual(["cli", "tool", "hook"]); |
| }); |
|
|
| it("keeps both next-turn injection APIs callable after registration", async () => { |
| const enqueueNextTurnInjection = vi.fn(async (injection) => ({ |
| enqueued: true, |
| id: `injection-${injection.text}`, |
| sessionKey: injection.sessionKey, |
| })); |
| const api = captureRegisteredPluginApi({ enqueueNextTurnInjection }); |
|
|
| const groupedResult = await api.session.workflow.enqueueNextTurnInjection({ |
| sessionKey: "global", |
| text: "grouped", |
| agentId: "work", |
| }); |
| const flatResult = await api.enqueueNextTurnInjection({ |
| sessionKey: "global", |
| text: "flat", |
| agentId: "main", |
| }); |
|
|
| expect(groupedResult).toEqual({ |
| enqueued: true, |
| id: "injection-grouped", |
| sessionKey: "global", |
| }); |
| expect(flatResult).toEqual({ |
| enqueued: true, |
| id: "injection-flat", |
| sessionKey: "global", |
| }); |
| expect(enqueueNextTurnInjection).toHaveBeenCalledTimes(2); |
| expect(enqueueNextTurnInjection).toHaveBeenNthCalledWith(1, { |
| sessionKey: "global", |
| agentId: "work", |
| text: "grouped", |
| }); |
| expect(enqueueNextTurnInjection).toHaveBeenNthCalledWith(2, { |
| sessionKey: "global", |
| agentId: "main", |
| text: "flat", |
| }); |
| }); |
|
|
| it("blocks registration-phase methods after registration", () => { |
| const registerSessionExtension = vi.fn(); |
| const api = captureRegisteredPluginApi({ registerSessionExtension }); |
|
|
| const result = api.session.state.registerSessionExtension({ |
| namespace: "workflow", |
| description: "workflow", |
| }); |
|
|
| expect(result).toBeUndefined(); |
| expect(registerSessionExtension).not.toHaveBeenCalled(); |
| }); |
|
|
| it.each<[string, boolean]>([ |
| ["clearRunContext", true], |
| ["emitAgentEvent", true], |
| ["enqueueNextTurnInjection", true], |
| ["getRunContext", true], |
| ["sendSessionAttachment", true], |
| ["scheduleSessionTurn", true], |
| ["setRunContext", true], |
| ["unscheduleSessionTurnsByTag", true], |
| ["registerTool", false], |
| ["registerSessionExtension", false], |
| ["unknown", false], |
| ["", false], |
| ["constructor", false], |
| ["toString", false], |
| ["__proto__", false], |
| ])("enforces post-registration call eligibility for %j as %s", (methodName, expected) => { |
| const handler = vi.fn(); |
| const api = captureRegisteredPluginApi({ [methodName]: handler }); |
| const method = Reflect.get(api, methodName); |
| const result = typeof method === "function" ? Reflect.apply(method, api, []) : undefined; |
|
|
| expect(handler).toHaveBeenCalledTimes(expected ? 1 : 0); |
| expect(result).toBeUndefined(); |
| }); |
| }); |
|
|