mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 19:01:54 +00:00
Every role-local MCP spec gates itself on the service flag and skips when the surface is off, so nothing ever checked that switching it off removes it. A surface still serving after the flag went false was indistinguishable from one that was never deployed: both read as a green skip. The gating helper had no inverse at all, so there was no way to assert what a disabled service must not do. A shared assertion now probes the endpoint with both a GET and a POST while the flag is false and fails on any answer that shows something listening. It counts 401 and 403 as listening, which is the point: a refused credential still means the surface is there. The check is transport-agnostic because the roles differ in their enabled-state probes, over SSE, Streamable HTTP or plain JSON-RPC, while an endpoint that is gone answers none of them. Each role passes its endpoint URL as a thunk, since some resolve it only once a test is running. The coverage lint requires the registration, and derives the role set from meta/mcp.yml rather than from a filename convention, which is how the tenth role surfaced: its spec is not named like the other nine. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
35 lines
1.3 KiB
JavaScript
35 lines
1.3 KiB
JavaScript
const { test, expect } = require("@playwright/test");
|
|
const { skipUnlessServiceEnabled } = require("./service-gating");
|
|
const { decodeDotenvQuotedValue, normalizeBaseUrl } = require("./personas");
|
|
const { registerMcpDisabledState } = require("./mcp-endpoint");
|
|
|
|
const baseUrl = normalizeBaseUrl(process.env.JENKINS_BASE_URL || "");
|
|
const mcpEndpointPath = decodeDotenvQuotedValue(process.env.MCP_ENDPOINT_PATH || "");
|
|
|
|
test.use({ ignoreHTTPSErrors: true });
|
|
|
|
test("MCP: the plugin endpoint serves no tool without credentials", async ({ page }) => {
|
|
skipUnlessServiceEnabled("mcp");
|
|
expect(mcpEndpointPath, "MCP_ENDPOINT_PATH must be set").toBeTruthy();
|
|
|
|
const response = await page.request.post(`${baseUrl.replace(/\/$/, "")}${mcpEndpointPath}`, {
|
|
failOnStatusCode: false,
|
|
headers: { "Content-Type": "application/json", Accept: "application/json, text/event-stream" },
|
|
data: { jsonrpc: "2.0", id: 1, method: "tools/list", params: {} },
|
|
});
|
|
|
|
expect(
|
|
response.status(),
|
|
"an unauthenticated tools/list must not be served a 2xx; the tool surface is credential-guarded",
|
|
).toBeGreaterThanOrEqual(400);
|
|
|
|
expect(
|
|
await response.text(),
|
|
"a refused tools/list must not leak a tool inventory",
|
|
).not.toContain('"tools"');
|
|
});
|
|
|
|
registerMcpDisabledState(
|
|
() => `${baseUrl.replace(/\/$/, "")}${mcpEndpointPath}`,
|
|
);
|