// Selfhost-only (browser): a spec/probe-DETECTED auth method is immutable in // the add flow. The shared AuthMethodListEditor renders detected methods as a // disabled, read-only summary ("Pulled from spec. Remove to override.") with no // kind selector, so a user can't silently retype the spec's method into a kind // nothing backs. A method the user adds by hand stays fully editable. Both the // MCP and OpenAPI add flows compose the same editor, so one behavior, two // surfaces. Selfhost runs with EXECUTOR_ALLOW_LOCAL_NETWORK so the probe/analyze // can reach the loopback fixtures. Video is the artifact. import { randomBytes } from "node:crypto"; import { createServer } from "node:http"; import { expect } from "@effect/vitest"; import { Effect } from "effect"; import { makeGreetingMcpServer, serveMcpServerWithOAuth } from "@executor-js/plugin-mcp/testing"; import { OAuthTestServer } from "@executor-js/sdk/testing"; import { scenario } from "../src/scenario"; import { Browser, Target } from "../src/services"; import { visit } from "../src/surfaces/browser"; const REMOVE_HINT = "Pulled from spec. Remove to override."; scenario( "Detected auth · an MCP probe's OAuth method is immutable in the add flow", {}, Effect.scoped( Effect.gen(function* () { const target = yield* Target; const browser = yield* Browser; // OAuth-protected server: the probe 401s with resource metadata, so the // method list seeds a single detected OAuth row (discovered metadata). const server = yield* serveMcpServerWithOAuth( () => makeGreetingMcpServer({ name: `oauth-mcp-${randomBytes(3).toString("hex")}` }), { path: "/mcp" }, ); const identity = yield* target.newIdentity(); yield* browser.session(identity, async ({ page, step }) => { await step("Open the add-MCP flow pointed at the OAuth server", async () => { await visit(page, `/integrations/add/mcp?url=${encodeURIComponent(server.endpoint)}`); await page.getByText("How does this server authenticate?").waitFor(); await page.getByText("Method 1 · Detected").waitFor(); }); await step("The detected method is locked: read-only, named, no selector", async () => { // The kind is named explicitly ("OAuth"), the discovered-OAuth summary // and override hint sit inside a disabled block, and there is NO // editable kind selector (the FilterTabs render as buttons). await page.getByText("OAuth", { exact: true }).first().waitFor(); await page.getByText("OAuth metadata is discovered from this server").waitFor(); await page.getByText(REMOVE_HINT).waitFor(); expect( await page.locator("[aria-disabled]").count(), "the detected method renders a disabled (non-interactive) block", ).toBeGreaterThan(0); expect( await page.getByRole("button", { name: "API key", exact: true }).count(), "no editable kind selector is shown for the detected method", ).toBe(0); }); await step("A hand-added method keeps the full editable selector", async () => { await page.getByRole("button", { name: "Add method" }).click(); await page.getByText("Method 2").waitFor(); // The added row defaults to API key and exposes the None/API key/OAuth // kind tabs (buttons) — the detected row above still shows none. expect( await page.getByRole("button", { name: "API key", exact: true }).count(), "the added method exposes the kind selector", ).toBeGreaterThan(0); await page.getByText(REMOVE_HINT).waitFor(); }); }); }), ).pipe(Effect.provide(OAuthTestServer.layer())), ); /** A real 127.0.0.1 server that serves a static OpenAPI spec for the add flow. */ const serveSpec = (body: string) => Effect.acquireRelease( Effect.callback<{ readonly url: string; readonly close: () => void }>((resume) => { const server = createServer((_request, response) => { response.writeHead(200, { "content-type": "application/json" }); response.end(body); }); server.listen(0, "127.0.0.1", () => { const address = server.address(); const port = typeof address === "object" && address ? address.port : 0; resume( Effect.succeed({ url: `http://127.0.0.1:${port}/spec.json`, close: () => { server.close(); server.closeAllConnections(); }, }), ); }); }), (server) => Effect.sync(server.close), ); const apiKeyAndOAuthSpec = (): string => JSON.stringify({ openapi: "3.0.3", info: { title: "Acme Immutable Auth Fixture", version: "1.0.0" }, servers: [{ url: "https://api.acme.test" }], security: [{ bearerAuth: [] }, { acmeOAuth: ["read"] }], components: { securitySchemes: { bearerAuth: { type: "http", scheme: "bearer" }, acmeOAuth: { type: "oauth2", flows: { authorizationCode: { authorizationUrl: "https://api.acme.test/oauth/authorize", tokenUrl: "https://api.acme.test/oauth/token", scopes: { read: "Read access" }, }, }, }, }, }, paths: { "/widgets": { get: { operationId: "listWidgets", summary: "List widgets", responses: { "200": { description: "ok" } }, }, }, }, }); scenario( "Detected auth · OpenAPI spec-detected methods are immutable in the add flow", {}, Effect.scoped( Effect.gen(function* () { const target = yield* Target; const browser = yield* Browser; const spec = yield* serveSpec(apiKeyAndOAuthSpec()); const identity = yield* target.newIdentity(); yield* browser.session(identity, async ({ page, step }) => { await step("Analyze a spec that declares both API key and OAuth", async () => { await visit(page, `/integrations/add/openapi`); await page .getByPlaceholder(/openapi\.json/i) .first() .fill(spec.url); await page.getByText("How does this API authenticate?").waitFor(); await page.getByText("Method 2").waitFor(); }); await step("Both detected methods are locked, named, read-only", async () => { // Two detected methods, each with the override hint and its kind named // ("API key" / "OAuth"); the OAuth one shows the spec's real endpoints // read-only. No editable kind selector (FilterTabs render as buttons). expect( await page.getByText(REMOVE_HINT).count(), "both detected methods show the remove-to-override hint", ).toBe(2); await page.getByText("https://api.acme.test/oauth/authorize").waitFor(); await page.getByText("API key", { exact: true }).first().waitFor(); await page.getByText("OAuth", { exact: true }).first().waitFor(); expect( await page.getByRole("button", { name: "OAuth", exact: true }).count(), "no editable kind selector is shown for the detected methods", ).toBe(0); }); await step("A hand-added method keeps the full editable selector", async () => { await page.getByRole("button", { name: "Add method" }).click(); await page.getByText("Method 3").waitFor(); expect( await page.getByRole("button", { name: "API key", exact: true }).count(), "the added method exposes the kind selector", ).toBeGreaterThan(0); }); }); }), ), );