| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302 |
- import { base64Encode } from "@opencode-ai/core/util/encode"
- import { expect, test, type Page, type Route } from "@playwright/test"
- import { installSseTransport } from "../utils/sse-transport"
- import { currentSession } from "../utils/mock-server"
- const serverA = "http://127.0.0.1:4096"
- const serverB = "http://127.0.0.1:4097"
- const directoryA = "C:/server-a"
- const directoryB = "/home/server-b"
- const sessionA = session("ses_server_a", directoryA, "Server A session")
- const childSessionA = { ...session("ses_server_a_child", directoryA, "Server A child session"), parentID: sessionA.id }
- const sessionB = session("ses_server_b", directoryB, "Server B session")
- test("session settings use the remote server context", async ({ page }) => {
- const permissionRequests: string[] = []
- await mockServers(page, permissionRequests)
- await configureServers(page)
- await page.goto(`/server/${base64Encode(serverB)}/session/${sessionB.id}`)
- await expect(page.getByText(sessionB.title).first()).toBeVisible()
- await page.keyboard.press("Control+,")
- const dialog = page.locator(".settings-v2-dialog")
- const autoAccept = dialog.locator('[data-action="settings-auto-accept-permissions"]')
- const input = autoAccept.getByRole("switch")
- await expect(autoAccept).toBeVisible()
- await expect(input).toBeEnabled()
- permissionRequests.length = 0
- await autoAccept.locator('[data-slot="switch-control"]').click()
- await expect(input).toBeChecked()
- await expect
- .poll(() =>
- permissionRequests.some((request) => {
- const url = new URL(request)
- return url.origin === serverB && url.searchParams.get("directory") === directoryB
- }),
- )
- .toBe(true)
- expect(permissionRequests.every((request) => new URL(request).origin === serverB)).toBe(true)
- await dialog.getByRole("tab", { name: "Models" }).click()
- await expect(dialog.getByRole("switch", { name: "Server B Model" })).toBeEnabled()
- await expect(dialog.getByRole("switch", { name: "Server A Model" })).toHaveCount(0)
- })
- test("auto-accept responds for an unfocused server session", async ({ page }) => {
- const permissionRequests: string[] = []
- const permissionResponses: PermissionResponse[] = []
- const transport = await installSseTransport<{ directory: string; payload: Record<string, unknown> }>(page, {
- server: serverA,
- retry: 20,
- })
- await mockServers(page, permissionRequests, permissionResponses)
- await configureServers(page, [
- { type: "session", server: serverA, sessionId: sessionA.id },
- { type: "session", server: serverB, sessionId: sessionB.id },
- ])
- const hrefB = `/server/${base64Encode(serverB)}/session/${sessionB.id}`
- await page.goto(`/server/${base64Encode(serverA)}/session/${sessionA.id}`)
- await expect(page.getByText(sessionA.title).first()).toBeVisible()
- await page.keyboard.press("Control+,")
- const autoAccept = page.locator(".settings-v2-dialog").locator('[data-action="settings-auto-accept-permissions"]')
- await autoAccept.locator('[data-slot="switch-control"]').click()
- await expect(autoAccept.getByRole("switch")).toBeChecked()
- await expect
- .poll(() =>
- permissionRequests.some((request) => {
- const url = new URL(request)
- return url.origin === serverA && url.searchParams.get("directory") === directoryA
- }),
- )
- .toBe(true)
- await page.keyboard.press("Escape")
- await page.locator(`[data-titlebar-tab-slot]:has(a[href="${hrefB}"])`).click()
- await expect(page).toHaveURL(new RegExp(`${hrefB.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`))
- await expect(page.getByText(sessionB.title).first()).toBeVisible()
- await transport.waitForConnection()
- await transport.send({
- directory: directoryA,
- payload: {
- id: "event-permission-background-a",
- type: "permission.asked",
- properties: {
- id: "permission-background-a",
- sessionID: sessionA.id,
- permission: "bash",
- patterns: ["git status"],
- metadata: {},
- always: [],
- },
- },
- })
- await expect
- .poll(() => permissionResponses)
- .toEqual([
- {
- origin: serverA,
- directory: directoryA,
- sessionID: sessionA.id,
- permissionID: "permission-background-a",
- body: { response: "once" },
- },
- ])
- await transport.send({
- directory: directoryA,
- payload: {
- id: "event-permission-background-a-child",
- type: "permission.asked",
- properties: {
- id: "permission-background-a-child",
- sessionID: childSessionA.id,
- permission: "bash",
- patterns: ["git diff"],
- metadata: {},
- always: [],
- },
- },
- })
- await expect
- .poll(() => permissionResponses)
- .toEqual([
- {
- origin: serverA,
- directory: directoryA,
- sessionID: sessionA.id,
- permissionID: "permission-background-a",
- body: { response: "once" },
- },
- {
- origin: serverA,
- directory: directoryA,
- sessionID: childSessionA.id,
- permissionID: "permission-background-a-child",
- body: { response: "once" },
- },
- ])
- })
- type PermissionResponse = {
- origin: string
- directory?: string
- sessionID: string
- permissionID: string
- body: unknown
- }
- async function configureServers(page: Page, tabs: { type: "session"; server: string; sessionId: string }[] = []) {
- await page.addInitScript(
- ({ serverB, tabs }) => {
- localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
- localStorage.setItem("opencode.global.dat:server", JSON.stringify({ list: [serverB] }))
- localStorage.setItem("opencode.window.browser.dat:tabs", JSON.stringify(tabs))
- },
- { serverB, tabs },
- )
- }
- async function mockServers(page: Page, permissionRequests: string[], permissionResponses: PermissionResponse[] = []) {
- await page.route("**/*", async (route) => {
- const url = new URL(route.request().url())
- if (url.origin !== serverA && url.origin !== serverB) return route.fallback()
- const remote = url.origin === serverB
- const directory = remote ? directoryB : directoryA
- const sessions = remote ? [sessionB] : [sessionA, childSessionA]
- const requestDirectory = url.searchParams.get("directory")
- const response = url.pathname.match(/^\/session\/([^/]+)\/permissions\/([^/]+)$/)
- if (route.request().method() === "POST" && response) {
- permissionResponses.push({
- origin: url.origin,
- directory: requestDirectory ?? undefined,
- sessionID: response[1]!,
- permissionID: response[2]!,
- body: route.request().postDataJSON(),
- })
- return json(route, true)
- }
- if (requestDirectory && requestDirectory !== directory) return json(route, { name: "InvalidDirectory" }, 500)
- if (url.pathname === "/global/event" || url.pathname === "/event" || url.pathname === "/api/event")
- return sse(route)
- if (url.pathname === "/global/health") return json(route, { healthy: true })
- if (url.pathname === "/api/provider" || url.pathname === "/api/model" || url.pathname === "/api/agent")
- return json(route, { data: [] })
- if (url.pathname === "/api/model/default") return json(route, { data: null })
- if (["/api/command", "/api/reference", "/api/permission/request", "/api/question/request"].includes(url.pathname))
- return json(route, { location: { directory }, data: [] })
- if (url.pathname === "/api/mcp") return json(route, { location: { directory }, data: [] })
- if (url.pathname === "/api/mcp/resource")
- return json(route, { location: { directory }, data: { resources: [], templates: [] } })
- if (url.pathname === "/api/project") {
- return json(route, [
- {
- id: remote ? sessionB.projectID : "project-server-a",
- worktree: directory,
- vcs: "git",
- time: { created: 1, updated: 1 },
- sandboxes: [],
- },
- ])
- }
- if (url.pathname === "/api/project/current")
- return json(route, { id: remote ? sessionB.projectID : "project-server-a", directory })
- if (url.pathname === "/api/session") return json(route, { data: sessions.map(currentSession), cursor: {} })
- if (url.pathname === "/api/session/active") return json(route, { data: {} })
- const currentSessionInfo = sessions.find((session) => url.pathname === `/api/session/${session.id}`)
- if (currentSessionInfo) return json(route, { data: currentSession(currentSessionInfo) })
- if (sessions.some((session) => url.pathname === `/api/session/${session.id}/message`))
- return json(route, { data: [], cursor: {} })
- const current = sessions.find((session) => url.pathname === `/session/${session.id}`)
- if (current) return json(route, current)
- if (/^\/session\/[^/]+$/.test(url.pathname)) return json(route, { name: "NotFoundError" }, 404)
- if (/^\/session\/[^/]+\/message$/.test(url.pathname)) return json(route, [])
- if (/^\/session\/[^/]+\/(children|todo|diff)$/.test(url.pathname)) return json(route, [])
- if (url.pathname === "/permission") {
- permissionRequests.push(url.toString())
- return json(route, [])
- }
- if (["/skill", "/command", "/lsp", "/formatter", "/question", "/vcs/diff", "/pty/shells"].includes(url.pathname))
- return json(route, [])
- if (["/global/config", "/config", "/provider/auth", "/mcp"].includes(url.pathname)) return json(route, {})
- if (url.pathname === "/provider") return json(route, provider(remote ? "server-b" : "server-a"))
- if (url.pathname === "/agent") return json(route, [{ name: "build", mode: "primary" }])
- if (url.pathname === "/project" || url.pathname === "/project/current") {
- const project = {
- id: remote ? sessionB.projectID : "project-server-a",
- worktree: directory,
- vcs: "git",
- time: { created: 1, updated: 1 },
- sandboxes: [],
- }
- return json(route, url.pathname === "/project" ? [project] : project)
- }
- if (url.pathname === "/path")
- return json(route, {
- state: directory,
- config: directory,
- worktree: directory,
- directory,
- home: directory,
- })
- if (url.pathname === "/api/path")
- return json(route, { state: directory, config: directory, worktree: directory, directory, home: directory })
- if (url.pathname === "/vcs") return json(route, { branch: "main", default_branch: "main" })
- if (url.pathname === "/api/vcs")
- return json(route, { location: { directory }, data: { branch: "main", defaultBranch: "main" } })
- if (url.pathname === "/api/pty/shells") return json(route, { location: { directory }, data: [] })
- return json(route, {})
- })
- }
- function session(id: string, directory: string, title: string) {
- return {
- id,
- slug: id,
- projectID: `project-${id}`,
- directory,
- title,
- version: "dev",
- time: { created: 1, updated: 1 },
- }
- }
- function provider(id: string) {
- const name = id === "server-b" ? "Server B" : "Server A"
- return {
- all: [
- {
- id,
- name: `${name} Provider`,
- models: {
- [id]: {
- id,
- name: `${name} Model`,
- family: id,
- release_date: "2026-01-01",
- limit: { context: 200_000 },
- },
- },
- },
- ],
- connected: [id],
- default: { providerID: id, modelID: id },
- }
- }
- function json(route: Route, body: unknown, status = 200) {
- return route.fulfill({
- status,
- contentType: "application/json",
- headers: { "access-control-allow-origin": "*" },
- body: JSON.stringify(body),
- })
- }
- function sse(route: Route) {
- return route.fulfill({ status: 200, contentType: "text/event-stream", body: ": ok\n\n" })
- }
|