From 84d9e93dbb46fa75d24bbc7664f128d7b2fc233a Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 4 Aug 2026 09:07:27 +0800 Subject: [PATCH] chore(qa): cover plugin testing harness contracts (#118933) * test(qa): cover plugin testing harness contracts * test(qa): narrow plugin harness tool factory --- .../plugin-testing-harness-contracts.yaml | 29 +++++ .../plugin-testing-harness.contract.test.ts | 113 ++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 qa/scenarios/plugins/plugin-testing-harness-contracts.yaml create mode 100644 src/plugins/contracts/plugin-testing-harness.contract.test.ts diff --git a/qa/scenarios/plugins/plugin-testing-harness-contracts.yaml b/qa/scenarios/plugins/plugin-testing-harness-contracts.yaml new file mode 100644 index 000000000000..612eb4b0759c --- /dev/null +++ b/qa/scenarios/plugins/plugin-testing-harness-contracts.yaml @@ -0,0 +1,29 @@ +title: Plugin testing harness contracts + +scenario: + id: plugin-testing-harness-contracts + surface: plugins + category: plugins.testing-plugins + coverage: + primary: + - plugins.test-fixtures + - plugins.local-test-environment + - plugins.harness-contracts-tools + - plugins.unit-and-integration-scaffolds + objective: Verify the repo-local plugin test helpers provide isolated state, typed fixtures, real registry registration, tool execution, and contract diagnostics. + successCriteria: + - The temporary-home helper creates isolated OpenClaw state and restores the caller environment. + - Typed fixture cases drive an actual tool registered through the plugin registry harness. + - A virtual plugin with a declared tools contract registers and executes its tool. + - A virtual plugin without a tools contract records the canonical diagnostic and registers no tool. + docsRefs: + - docs/plugins/sdk-testing.md + codeRefs: + - src/plugin-sdk/plugin-test-contracts.ts + - src/plugin-sdk/test-env.ts + - src/plugin-sdk/test-fixtures.ts + - src/plugins/contracts/plugin-testing-harness.contract.test.ts + execution: + kind: vitest + path: src/plugins/contracts/plugin-testing-harness.contract.test.ts + summary: Run the repo-local plugin harness across isolated state, typed fixtures, tool execution, and contract rejection. diff --git a/src/plugins/contracts/plugin-testing-harness.contract.test.ts b/src/plugins/contracts/plugin-testing-harness.contract.test.ts new file mode 100644 index 000000000000..d56db5fd27af --- /dev/null +++ b/src/plugins/contracts/plugin-testing-harness.contract.test.ts @@ -0,0 +1,113 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { + createPluginRegistryFixture, + registerVirtualTestPlugin, +} from "openclaw/plugin-sdk/plugin-test-contracts"; +import { withTempHome } from "openclaw/plugin-sdk/test-env"; +import { typedCases } from "openclaw/plugin-sdk/test-fixtures"; +import { describe, expect, it } from "vitest"; + +const echoCases = typedCases([ + { message: "alpha", expected: "scoped:alpha" }, + { message: "beta", expected: "scoped:beta" }, +]); + +describe("plugin testing harness contracts", () => { + it("executes declared tools and reports missing tool contracts", async () => { + const previousHome = process.env.HOME; + const previousStateDir = process.env.OPENCLAW_STATE_DIR; + + await withTempHome(async (home) => { + const stateDir = path.join(home, ".openclaw"); + await expect( + fs.stat(path.join(stateDir, "agents", "main", "sessions")), + ).resolves.toBeDefined(); + expect(process.env.HOME).toBe(home); + expect(process.env.OPENCLAW_STATE_DIR).toBe(stateDir); + + const { config, registry } = createPluginRegistryFixture({ + plugins: { + entries: { + "fixture-echo": { + config: { prefix: "scoped" }, + }, + }, + }, + }); + + registerVirtualTestPlugin({ + registry, + config, + id: "fixture-echo", + name: "Fixture Echo", + contracts: { tools: ["fixture_echo"] }, + register(api) { + const prefix = api.config.plugins?.entries?.["fixture-echo"]?.config?.prefix; + if (typeof prefix !== "string") { + throw new Error("fixture prefix missing"); + } + api.registerTool({ + name: "fixture_echo", + label: "Fixture Echo", + description: "Echo a fixture value", + parameters: {}, + execute: async (_toolCallId, params) => { + const message = (params as { message: string }).message; + return { + content: [{ type: "text", text: `${prefix}:${message}` }], + details: { home, message }, + }; + }, + }); + }, + }); + + expect(registry.registry.tools).toHaveLength(1); + const registration = registry.registry.tools[0]; + expect(registration?.pluginId).toBe("fixture-echo"); + expect(registration?.names).toEqual(["fixture_echo"]); + const tool = registration?.factory({ workspaceDir: home }); + if (!tool || Array.isArray(tool)) { + throw new Error("expected one registered fixture tool"); + } + expect(tool.name).toBe("fixture_echo"); + + for (const testCase of echoCases) { + await expect(tool.execute("fixture-call", { message: testCase.message })).resolves.toEqual({ + content: [{ type: "text", text: testCase.expected }], + details: { home, message: testCase.message }, + }); + } + expect(registry.registry.diagnostics).toEqual([]); + + const missingContract = createPluginRegistryFixture(); + registerVirtualTestPlugin({ + registry: missingContract.registry, + config: missingContract.config, + id: "missing-contract", + name: "Missing Contract", + register(api) { + api.registerTool({ + name: "fixture_echo", + label: "Fixture Echo", + description: "Echo a fixture value", + parameters: {}, + execute: async () => ({ content: [], details: {} }), + }); + }, + }); + + expect(missingContract.registry.registry.tools).toEqual([]); + expect(missingContract.registry.registry.diagnostics).toContainEqual({ + level: "error", + pluginId: "missing-contract", + source: "/virtual/missing-contract/index.ts", + message: "plugin must declare contracts.tools before registering agent tools", + }); + }); + + expect(process.env.HOME).toBe(previousHome); + expect(process.env.OPENCLAW_STATE_DIR).toBe(previousStateDir); + }); +});