fix(qa): run plugin MCP probes from repo root

This commit is contained in:
Vincent Koc
2026-06-01 06:56:36 +02:00
parent c0195f7ed5
commit 4550cfa6a7
5 changed files with 61 additions and 17 deletions

View File

@@ -71,6 +71,7 @@ Docs: https://docs.openclaw.ai
- Security/config parsing: reject unsafe OAuth/token lifetimes, retry-after delays, inbound timestamps, response body sizes, command timeout config, sandbox observer token TTLs, and gateway WebSocket calls after close.
- Providers/media: cap local service, model, usage, queue, generated media, TTS, music, workflow polling, and provider OAuth request timers across hosted and local providers.
- Release/CI/E2E: bound release candidate reads, beta smoke REST calls, changelog restore, kitchen-sink and bundled plugin readiness probes, secret-provider probes, Vitest routing, and mainline test flakes. (#88127, #88137, #88155, #88160)
- Release/CI/E2E: keep Kitchen Sink live plugin MCP probes resolving source-checkout workspace packages and align the live gauntlet with current Kitchen Sink diagnostics.
- Release/CI/E2E: run the secret-provider integration proof through the repo pnpm runner so native macOS and Windows validation use the hydrated package-manager shim.
- Release/CI/E2E: run the Telegram desktop proof gateway through the repo pnpm runner so native macOS proof uses the hydrated package-manager shim.
- Docs/CI: run Mintlify anchor checks through the repo pnpm runner so docs link validation works when pnpm is only available through the hydrated package-manager shim.

View File

@@ -405,6 +405,15 @@ describe("qa scenario catalog", () => {
expect(config?.expectedAdversarialDiagnostics).toContain(
"control UI descriptor registration requires id, surface, label, and valid optional fields",
);
expect(config?.expectedAdversarialDiagnostics).toContain(
"hosted media resolver registration missing resolver",
);
expect(config?.expectedAdversarialDiagnostics).toContain(
"plugin must declare contracts.embeddingProviders for adapter: kitchen-sink-embedding-provider",
);
expect(config?.expectedAdversarialDiagnostics).toContain(
"model catalog provider registration missing provider",
);
expect(
config?.expectedAdversarialDiagnostics?.every((entry) => typeof entry === "string"),
).toBe(true);

View File

@@ -143,7 +143,7 @@ describe("qa suite runtime agent tools helpers", () => {
path.join(repoRoot, "src", "mcp", "plugin-tools-serve.ts"),
],
stderr: "pipe",
cwd: gatewayTempRoot,
cwd: repoRoot,
env: {
PATH: "/usr/bin",
OPENCLAW_KEY: "1",
@@ -225,4 +225,37 @@ describe("qa suite runtime agent tools helpers", () => {
expect(message).not.toContain("old stderr");
expect(closeMock).toHaveBeenCalled();
});
it("keeps plugin-tools MCP stderr on startup failures", async () => {
connectMock.mockImplementationOnce(async () => {
const stderrListener = stderrOnMock.mock.calls[0]?.[1] as
| ((chunk: unknown) => void)
| undefined;
stderrListener?.(
Buffer.from("Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@openclaw/example'\n"),
);
throw new Error("MCP error -32000: Connection closed");
});
const error = await callPluginToolsMcp({
env: {
gateway: {
tempRoot: gatewayTempRoot,
runtimeEnv: {
PATH: "/usr/bin",
},
},
repoRoot,
} as never,
toolName: "plugin.echo",
args: { text: "hello" },
}).catch((value: unknown) => value);
expect(error).toBeInstanceOf(Error);
const message = error instanceof Error ? error.message : String(error);
expect(message).toContain("MCP error -32000: Connection closed");
expect(message).toContain("MCP stderr tail:");
expect(message).toContain("Cannot find package '@openclaw/example'");
expect(closeMock).toHaveBeenCalled();
});
});

View File

@@ -56,7 +56,7 @@ async function callPluginToolsMcp(params: {
path.join(params.env.repoRoot, "src/mcp/plugin-tools-serve.ts"),
],
stderr: "pipe",
cwd: params.env.gateway.tempRoot,
cwd: params.env.repoRoot,
env: transportEnv,
});
const stderrTail = createQaChildOutputTail(MCP_STDERR_TAIL_LIMIT);
@@ -80,22 +80,20 @@ async function callPluginToolsMcp(params: {
`MCP tool missing: ${params.toolName}; available tools: ${availableTools.join(", ") || "<none>"}`,
);
}
try {
return await client.callTool(
{
name: params.toolName,
arguments: params.args,
},
undefined,
{ timeout: MCP_REQUEST_TIMEOUT_MS },
);
} catch (error) {
const tail = formatQaChildOutputTail(stderrTail, "MCP stderr").trim();
if (!tail || !(error instanceof Error)) {
throw error;
}
throw new Error(`${error.message}\nMCP stderr tail:\n${tail}`, { cause: error });
return await client.callTool(
{
name: params.toolName,
arguments: params.args,
},
undefined,
{ timeout: MCP_REQUEST_TIMEOUT_MS },
);
} catch (error) {
const tail = formatQaChildOutputTail(stderrTail, "MCP stderr").trim();
if (!tail || !(error instanceof Error)) {
throw error;
}
throw new Error(`${error.message}\nMCP stderr tail:\n${tail}`, { cause: error });
} finally {
await client.close().catch(() => {});
}

View File

@@ -95,9 +95,12 @@ execution:
- compaction provider "kitchen-sink-compaction-provider" registration missing summarize
- context engine registration missing id
- control UI descriptor registration requires id, surface, label, and valid optional fields
- hosted media resolver registration missing resolver
- "http route registration missing or invalid auth: /kitchen-sink/http-route"
- "plugin must declare contracts.embeddingProviders for adapter: kitchen-sink-embedding-provider"
- "plugin must own memory slot or declare contracts.memoryEmbeddingProviders for adapter: kitchen-sink-memory-embedding-provider"
- memory prompt supplement registration missing builder
- model catalog provider registration missing provider
- node invoke policy registration missing commands
- session extension registration requires namespace and description
- session scheduler job registration requires unique id, sessionKey, and kind