mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-14 03:56:55 +03:00
455 lines
19 KiB
TypeScript
455 lines
19 KiB
TypeScript
import { expect, test, type Page } from "@playwright/test"
|
|
import { holdRoute, NO_PROVIDER, sessionHref } from "../utils/app"
|
|
import type { MockServerConfig } from "../utils/mock-server"
|
|
import { mockWorkspace } from "../utils/workspace"
|
|
import { expectSessionTitle } from "../utils/waits"
|
|
|
|
const directory = "C:\\OpenCode\\main"
|
|
|
|
const workspace = "C:\\OpenCode\\worktree"
|
|
|
|
const sessionID = "ses_mcp_workspace"
|
|
|
|
const title = "Workspace MCP routing"
|
|
|
|
type Surface = "popover" | "dialog" | "settings"
|
|
|
|
// A session in a worktree of the project. The harness answers the MCP list for each location directory and stores
|
|
// connect/disconnect results per directory; `requests` records every MCP request and the directory it targeted.
|
|
// The popover is the session details' MCP list; the dialog (Ctrl+;) is the app's own MCP manager; settings is the
|
|
// Extensions page opened (Ctrl+,) from the session.
|
|
async function open(
|
|
page: Page,
|
|
input: { surface: Surface } & Pick<MockServerConfig, "mcp" | "onMcpAction" | "integrations" | "onIntegrationOAuth">,
|
|
) {
|
|
const requests: { path: string; directory: string }[] = []
|
|
page.on("request", (request) => {
|
|
const url = new URL(request.url())
|
|
|
|
if (request.method() === "OPTIONS" || !/^\/api\/(?:experimental\/)?mcp(?:\/|$)/.test(url.pathname)) return
|
|
requests.push({ path: url.pathname, directory: url.searchParams.get("location[directory]") || directory })
|
|
})
|
|
|
|
const mock = await mockWorkspace(page, {
|
|
name: "mcp-workspace",
|
|
directory,
|
|
project: { sandboxes: [workspace] },
|
|
provider: NO_PROVIDER,
|
|
sessions: [{ id: sessionID, title, directory: workspace }],
|
|
seed: { settings: { keybinds: { "mcp.toggle": "ctrl+;" } } },
|
|
mcp: input.mcp,
|
|
onMcpAction: input.onMcpAction,
|
|
integrations: input.integrations,
|
|
onIntegrationOAuth: input.onIntegrationOAuth,
|
|
})
|
|
|
|
await page.goto(sessionHref(sessionID))
|
|
await expectSessionTitle(page, title)
|
|
await expect(page.getByRole("textbox", { name: "Prompt", exact: true })).toBeEditable()
|
|
|
|
const panel = {
|
|
settings: page.getByTestId("settings-screen"),
|
|
popover: page.getByRole("group", { name: "MCP", exact: true }),
|
|
dialog: page.getByRole("dialog", { name: "MCPs", exact: true }),
|
|
}[input.surface]
|
|
|
|
const show = async () => {
|
|
if (input.surface === "dialog") return page.keyboard.press("Control+;")
|
|
|
|
if (input.surface === "settings") {
|
|
await page.keyboard.press("Control+,")
|
|
|
|
return panel.getByRole("tab", { name: "Extensions", exact: true }).click()
|
|
}
|
|
|
|
await page.getByRole("button", { name: "Session details", exact: true }).click()
|
|
const item = page.getByRole("button", { name: "MCP", exact: true })
|
|
await item.click()
|
|
await expect(panel).toBeVisible()
|
|
}
|
|
|
|
await show()
|
|
// The settings row also names its switch with a visually hidden label.
|
|
await expect(
|
|
input.surface === "settings"
|
|
? panel.getByRole("switch", { name: "figma-desktop", exact: true })
|
|
: panel.locator('[data-slot="truncated-text-label"]').filter({ hasText: "figma-desktop" }),
|
|
).toBeVisible()
|
|
|
|
return { requests, panel, show, push: mock.push, toggle: panel.getByRole("switch") }
|
|
}
|
|
|
|
test("settings shows and toggles the MCP of the session it was opened from", async ({ page }) => {
|
|
const actions: Parameters<NonNullable<MockServerConfig["onMcpAction"]>>[0][] = []
|
|
|
|
const view = await open(page, {
|
|
surface: "settings",
|
|
mcp: (target) => [{ name: "figma-desktop", status: { status: target === workspace ? "connected" : "disabled" } }],
|
|
onMcpAction: (action) => void actions.push(action),
|
|
})
|
|
|
|
await expect(view.toggle).toBeChecked()
|
|
await expect(view.toggle).toBeEnabled()
|
|
view.requests.length = 0
|
|
|
|
await view.panel.locator('[data-slot="switch-control"]').click()
|
|
await expect(view.toggle).not.toBeChecked()
|
|
await expect(view.toggle).toBeEnabled()
|
|
expect(actions).toEqual([{ server: "figma-desktop", action: "disconnect", directory: workspace }])
|
|
expect(view.requests).toContainEqual({ path: "/api/experimental/mcp/figma-desktop/disconnect", directory: workspace })
|
|
})
|
|
|
|
for (const surface of ["popover", "dialog"] as const) {
|
|
for (const shared of [true, false]) {
|
|
test(`toggles the workspace MCP from the ${surface} when the default location ${shared ? "has" : "does not have"} the server`, async ({
|
|
page,
|
|
}) => {
|
|
const actions: Parameters<NonNullable<MockServerConfig["onMcpAction"]>>[0][] = []
|
|
|
|
const view = await open(page, {
|
|
surface,
|
|
mcp: (target) =>
|
|
!shared && target !== workspace ? [] : [{ name: "figma-desktop", status: { status: "disabled" } }],
|
|
onMcpAction: (action) => void actions.push(action),
|
|
})
|
|
|
|
await expect(view.toggle).not.toBeChecked()
|
|
await expect(view.toggle).toBeEnabled()
|
|
view.requests.length = 0
|
|
|
|
await view.panel.locator('[data-slot="switch-control"]').click()
|
|
await expect(view.toggle).toBeChecked()
|
|
await expect(view.toggle).toBeEnabled()
|
|
expect(actions).toEqual([{ server: "figma-desktop", action: "connect", directory: workspace }])
|
|
expect(view.requests).toContainEqual({
|
|
path: "/api/experimental/mcp/figma-desktop/connect",
|
|
directory: workspace,
|
|
})
|
|
expect(view.requests).toContainEqual({ path: "/api/mcp/resource", directory: workspace })
|
|
expect(view.requests.every((request) => request.directory === workspace)).toBe(true)
|
|
|
|
view.requests.length = 0
|
|
await view.panel.locator('[data-slot="switch-control"]').click()
|
|
await expect(view.toggle).not.toBeChecked()
|
|
await expect(view.toggle).toBeEnabled()
|
|
expect(actions).toEqual([
|
|
{ server: "figma-desktop", action: "connect", directory: workspace },
|
|
{ server: "figma-desktop", action: "disconnect", directory: workspace },
|
|
])
|
|
expect(view.requests).toContainEqual({
|
|
path: "/api/experimental/mcp/figma-desktop/disconnect",
|
|
directory: workspace,
|
|
})
|
|
expect(view.requests.every((request) => request.directory === workspace)).toBe(true)
|
|
})
|
|
}
|
|
}
|
|
|
|
test("the MCP popover shows a failed server and reconnects it from its details", async ({ page }) => {
|
|
const error = "Streamable HTTP error: Error POSTing to endpoint: 404 Not Found"
|
|
|
|
const view = await open(page, {
|
|
surface: "popover",
|
|
mcp: (target) => [
|
|
{ name: "figma-desktop", status: target !== workspace ? { status: "connected" } : { status: "failed", error } },
|
|
],
|
|
})
|
|
|
|
const row = view.panel.locator(".session-mcp-row").filter({ hasText: "figma-desktop" })
|
|
// The red dot is the status, and opens the error.
|
|
await expect(view.toggle).toHaveAccessibleDescription("Failed")
|
|
const failed = row.getByRole("button", { name: "Details for figma-desktop", exact: true })
|
|
view.requests.length = 0
|
|
await failed.click()
|
|
const details = page.getByRole("dialog", { name: "figma-desktop", exact: true })
|
|
await expect(details).toHaveAccessibleDescription("Connection failed")
|
|
await expect(details.getByText(error, { exact: true })).toBeVisible()
|
|
await details.getByRole("button", { name: "Try again", exact: true }).click()
|
|
await expect(details).toBeHidden()
|
|
expect(view.requests.filter((request) => request.path.endsWith("/connect"))).toEqual([
|
|
{ path: "/api/experimental/mcp/figma-desktop/connect", directory: workspace },
|
|
])
|
|
|
|
if (!(await view.panel.isVisible())) await view.show()
|
|
await expect(view.toggle).toHaveAccessibleDescription("Connected")
|
|
})
|
|
|
|
for (const surface of ["dialog"] as const) {
|
|
test(`shows connection failures from the MCP ${surface} and allows reconnecting`, async ({ page }) => {
|
|
const error = "Streamable HTTP error: Error POSTing to endpoint: 404 Not Found"
|
|
const state = { fail: true }
|
|
|
|
const view = await open(page, {
|
|
surface,
|
|
mcp: (target) => [
|
|
{ name: "figma-desktop", status: { status: target !== workspace ? "connected" : "disabled", error } },
|
|
],
|
|
// Connection failures are reported by the refreshed status, not the HTTP response.
|
|
onMcpAction: (input) => (input.action === "connect" && state.fail ? { status: "failed", error } : undefined),
|
|
})
|
|
|
|
// The dialog opens with its search focused and the first server highlighted.
|
|
if (surface === "dialog") {
|
|
await expect(view.panel.getByRole("combobox", { name: "Search MCPs", exact: true })).toBeFocused()
|
|
await expect(view.panel.getByRole("option")).toHaveAttribute("aria-selected", "true")
|
|
}
|
|
|
|
await expect(view.toggle).not.toBeChecked()
|
|
await expect(view.toggle).toBeEnabled()
|
|
view.requests.length = 0
|
|
|
|
await view.panel.locator('[data-slot="switch-control"]').click()
|
|
|
|
const toast = page
|
|
.getByRole("listitem", { includeHidden: true })
|
|
.filter({ has: page.getByText("Request failed", { exact: true }) })
|
|
|
|
await expect(toast.getByText(`figma-desktop: ${error}`, { exact: true })).toBeVisible()
|
|
expect(view.requests.filter((request) => request.path.endsWith("/connect"))).toEqual([
|
|
{ path: "/api/experimental/mcp/figma-desktop/connect", directory: workspace },
|
|
])
|
|
expect(view.requests.every((request) => request.directory === workspace)).toBe(true)
|
|
|
|
// The red dot is the status and the switch stays on; a ghost action beside it opens the error.
|
|
const failed = view.panel.getByRole("button", { name: "View error for figma-desktop", exact: true })
|
|
const details = page.getByRole("dialog", { name: "figma-desktop", exact: true })
|
|
await expect(view.toggle).toBeChecked()
|
|
await expect(view.toggle).toHaveAccessibleDescription("Failed")
|
|
await expect(failed).toHaveText("View error")
|
|
await expect(failed).toHaveAttribute("data-variant", "ghost-muted")
|
|
await failed.click()
|
|
await expect(details).toHaveAccessibleDescription("Connection failed")
|
|
await expect(details.getByText(error, { exact: true })).toBeVisible()
|
|
|
|
if (surface === "dialog") {
|
|
const back = details.getByRole("button", { name: "Navigate back", exact: true })
|
|
|
|
await expect
|
|
.poll(async () => {
|
|
const navigation = await back.boundingBox()
|
|
const heading = await details.getByRole("heading", { name: "figma-desktop", exact: true }).boundingBox()
|
|
const error = await details.locator('[data-slot="error-details-text"]').boundingBox()
|
|
const status = await details.getByText("Connection failed", { exact: true }).boundingBox()
|
|
|
|
return (
|
|
!!navigation &&
|
|
!!heading &&
|
|
!!error &&
|
|
!!status &&
|
|
Math.abs(heading.y - navigation.y) < 1 &&
|
|
heading.x >= navigation.x + navigation.width &&
|
|
Math.abs(status.x - error.x) < 1
|
|
)
|
|
})
|
|
.toBe(true)
|
|
await back.click()
|
|
await expect(failed).toBeVisible()
|
|
await failed.click()
|
|
}
|
|
|
|
state.fail = false
|
|
await details.getByRole("button", { name: "Try again", exact: true }).click()
|
|
await expect(view.toggle).toBeChecked()
|
|
await expect(view.toggle).toBeEnabled()
|
|
})
|
|
}
|
|
|
|
test("the MCP list shrinks with matches and keeps its search anchored while scrolling", async ({ page }) => {
|
|
const view = await open(page, {
|
|
surface: "dialog",
|
|
mcp: ["figma-desktop", ...Array.from({ length: 24 }, (_, index) => `server-${String(index).padStart(2, "0")}`)].map(
|
|
(name) => ({ name, status: { status: "disabled" } }),
|
|
),
|
|
})
|
|
|
|
const input = view.panel.getByRole("combobox", { name: "Search MCPs", exact: true })
|
|
const container = page.locator('[data-component="dialog-v2"] [data-slot="dialog-container"]')
|
|
|
|
const geometry = () =>
|
|
container.evaluate((element) => ({
|
|
height: element.getBoundingClientRect().height,
|
|
inputTop: element.querySelector('[role="combobox"]')!.getBoundingClientRect().top,
|
|
}))
|
|
|
|
await expect(input).toBeFocused()
|
|
await expect(view.panel.getByRole("option")).toHaveCount(25)
|
|
await input.hover()
|
|
const initial = await geometry()
|
|
|
|
await input.fill("figma-desktop")
|
|
await expect(view.panel.getByRole("option")).toHaveCount(1)
|
|
await expect.poll(async () => (await geometry()).height).toBeLessThan(initial.height - 100)
|
|
await expect.poll(async () => (await geometry()).inputTop).toBeCloseTo(initial.inputTop, 0)
|
|
|
|
await input.clear()
|
|
await expect(view.panel.getByRole("option")).toHaveCount(25)
|
|
const scroll = view.panel.locator('[data-slot="search-list-scroll"]')
|
|
|
|
await expect.poll(() => scroll.evaluate((element) => element.scrollHeight - element.clientHeight)).toBeGreaterThan(0)
|
|
await expect.poll(async () => (await geometry()).height).toBeLessThanOrEqual(480)
|
|
await input.press("End")
|
|
const last = view.panel.getByRole("option", { name: "server-23", exact: true })
|
|
|
|
await expect(last).toHaveAttribute("aria-selected", "true")
|
|
await expect(last).toBeInViewport()
|
|
await expect.poll(() => scroll.evaluate((element) => element.scrollTop)).toBeGreaterThan(0)
|
|
await expect.poll(async () => (await geometry()).inputTop).toBeCloseTo(initial.inputTop, 0)
|
|
await expect(input).toBeFocused()
|
|
})
|
|
|
|
test("toggling an MCP from the dialog keeps it highlighted", async ({ page }) => {
|
|
const connected = new Set<string>()
|
|
const names = ["alpha-mcp", "figma-desktop", "zeta-mcp"]
|
|
|
|
const view = await open(page, {
|
|
surface: "dialog",
|
|
mcp: () => names.map((name) => ({ name, status: { status: connected.has(name) ? "connected" : "disabled" } })),
|
|
onMcpAction: (input) =>
|
|
void (input.action === "connect" ? connected.add(input.server) : connected.delete(input.server)),
|
|
})
|
|
|
|
const row = view.panel.getByRole("option").filter({ hasText: "figma-desktop" })
|
|
const toggle = row.getByRole("switch", { name: "figma-desktop", exact: true })
|
|
|
|
// From the keyboard, Enter toggles the highlighted server and the highlight stays on it.
|
|
await page.keyboard.press("ArrowDown")
|
|
await expect(row).toHaveAttribute("aria-selected", "true")
|
|
await page.keyboard.press("Enter")
|
|
await expect(toggle).toBeChecked()
|
|
await expect(row).toHaveAttribute("aria-selected", "true")
|
|
|
|
// With the pointer, the switch toggles the row under it and the highlight stays there too.
|
|
await row.locator('[data-slot="switch-control"]').click()
|
|
await expect(toggle).not.toBeChecked()
|
|
await expect(toggle).toBeEnabled()
|
|
await expect(row).toHaveAttribute("aria-selected", "true")
|
|
})
|
|
|
|
for (const surface of ["popover", "dialog"] as const) {
|
|
test(`signs in to an auth-gated MCP from the ${surface}`, async ({ page, context }) => {
|
|
const state = { signedIn: false }
|
|
const attempts: string[] = []
|
|
await context.route("https://auth.example.test/**", (route) => route.fulfill({ body: "Sign in" }))
|
|
|
|
const view = await open(page, {
|
|
surface,
|
|
mcp: () => [
|
|
{
|
|
name: "figma-desktop",
|
|
integrationID: "figma-oauth",
|
|
status: state.signedIn ? { status: "connected" } : { status: "needs_auth", error: "Sign in required" },
|
|
},
|
|
],
|
|
integrations: [{ id: "figma-oauth", name: "Figma", methods: [{ id: "oauth", type: "oauth" }], connections: [] }],
|
|
onIntegrationOAuth: (input) => {
|
|
attempts.push(input.directory)
|
|
|
|
return { url: "https://auth.example.test/authorize" }
|
|
},
|
|
})
|
|
|
|
// The switch would only start sign-in, so both surfaces offer that action in its place.
|
|
await expect(view.toggle).toHaveCount(0)
|
|
await expect(view.panel.getByText("Sign in required", { exact: true })).toHaveCount(0)
|
|
const popup = page.waitForEvent("popup")
|
|
const signIn = view.panel.getByRole("button", { name: "Sign in to figma-desktop", exact: true })
|
|
|
|
const authorization = await holdRoute(page, /\/api\/integration\/figma-oauth\/connect\/oauth(?:\?|$)/, {
|
|
method: "POST",
|
|
})
|
|
|
|
await expect(signIn).toHaveAttribute("data-component", "button-v2")
|
|
await expect(signIn).toHaveAttribute("data-variant", "ghost-muted")
|
|
await expect(signIn.locator("svg")).toHaveCount(0)
|
|
await signIn.click()
|
|
await expect(signIn).toHaveText("Signing in…")
|
|
await expect(signIn).toBeDisabled()
|
|
await authorization.arrived
|
|
authorization.release()
|
|
await expect(await popup).toHaveURL("https://auth.example.test/authorize")
|
|
expect(attempts).toEqual([workspace])
|
|
|
|
// Completing sign-in reconnects the server, which reports the change.
|
|
state.signedIn = true
|
|
await view.push([
|
|
{
|
|
id: "evt_mcp_signed_in",
|
|
created: 1,
|
|
type: "mcp.status.changed",
|
|
location: { directory: workspace },
|
|
data: { server: "figma-desktop" },
|
|
},
|
|
])
|
|
|
|
if (!(await view.panel.isVisible())) await view.show()
|
|
|
|
if (surface === "dialog") await expect(view.toggle).toBeChecked()
|
|
else
|
|
await expect(view.panel.getByRole("switch", { name: "figma-desktop", exact: true })).toHaveAccessibleDescription(
|
|
"Connected",
|
|
)
|
|
await expect(view.panel.getByRole("button", { name: "Sign in to figma-desktop", exact: true })).toHaveCount(0)
|
|
})
|
|
}
|
|
|
|
for (const surface of ["popover", "dialog"] as const) {
|
|
test(`long MCP names keep their status and controls visible in the ${surface}`, async ({ page }) => {
|
|
const names = ["connected", "disabled", "failed", "needs_auth"].map(
|
|
(status) => `long-${status}-mcp-server-for-the-internal-knowledge-base-search`,
|
|
)
|
|
|
|
const actions: string[] = []
|
|
|
|
const view = await open(page, {
|
|
surface,
|
|
mcp: [
|
|
{ name: "figma-desktop", status: { status: "disabled" } },
|
|
...names.map((name, index) => ({
|
|
name,
|
|
integrationID: index === 3 ? "long-oauth" : undefined,
|
|
status: { status: ["connected", "disabled", "failed", "needs_auth"][index], error: "Connection refused" },
|
|
})),
|
|
],
|
|
onMcpAction: (input) => void actions.push(input.server),
|
|
})
|
|
|
|
for (const [index, name] of names.entries()) {
|
|
const row = view.panel
|
|
.locator(surface === "dialog" ? '[role="option"]' : ".session-mcp-row")
|
|
.filter({ hasText: name })
|
|
|
|
// The dot is the status, so the control beside the name must stay whole.
|
|
const status =
|
|
index === 3
|
|
? row.getByText(["Connected", "Disabled", "Failed", "Sign in"][index], { exact: true })
|
|
: row.locator('[data-slot="switch-control"]')
|
|
|
|
await expect(status).toBeVisible()
|
|
await expect
|
|
.poll(() => status.evaluate((el) => el.scrollWidth <= el.clientWidth && el.getBoundingClientRect().width > 0))
|
|
.toBe(true)
|
|
}
|
|
|
|
const name = view.panel.locator('[data-slot="truncated-text-label"]').filter({ hasText: names[0] })
|
|
await name.hover()
|
|
// The modal aria-hides body portals; the full name remains the trigger's accessible text.
|
|
const tooltip = page.getByRole("tooltip", { includeHidden: true })
|
|
await expect(tooltip).toHaveText(names[0])
|
|
await expect(tooltip).toBeVisible()
|
|
await page.mouse.move(0, 0)
|
|
await view.panel.locator(".truncated-text").filter({ hasText: names[0] }).focus()
|
|
await expect(tooltip).toHaveText(names[0])
|
|
expect(actions).toEqual([])
|
|
await view.panel.locator(".truncated-text").filter({ hasText: names[0] }).blur()
|
|
await view.panel.locator('[data-slot="truncated-text-label"]').filter({ hasText: "figma-desktop" }).hover()
|
|
await expect(tooltip).toBeHidden()
|
|
|
|
if (surface !== "dialog") return
|
|
await page.setViewportSize({ width: 390, height: 844 })
|
|
await expect(name).toHaveCSS("white-space", "normal")
|
|
await expect.poll(() => name.evaluate((el) => el.scrollWidth <= el.clientWidth)).toBe(true)
|
|
await view.panel.getByRole("button", { name: `View error for ${names[2]}`, exact: true }).scrollIntoViewIfNeeded()
|
|
await expect(view.panel.getByRole("button", { name: `View error for ${names[2]}`, exact: true })).toBeInViewport()
|
|
})
|
|
}
|