+21





![opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)

![opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)
![opencode-agent[bot]](/assets/img/avatar_default.png)
opencode-agent[bot]
opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
Aiden Cline
opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Luke Parker
usrnk1
Aarav Sareen
Brendan Allan
Jack
David Hill
Jay
Brendan Allan
Simon Klee
opencode
Aiden Cline
Adam
James Long
James Long
Dustin Deus
冯基魁
Frank
Aiden Cline
Kit Langton
Victor Navarro
Dax Raad
Dax
Nabs
Vladimir Glafirov
AidenGeunGeun
Mark
Jay
0d4f9797f4
Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: David Hill <1879069+iamdavidhill@users.noreply.github.com> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Simon Klee <hello@simonklee.dk> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com> Co-authored-by: James Long <longster@gmail.com> Co-authored-by: James Long <jlongster@users.noreply.github.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: 冯基魁 <56265583+fengjikui@users.noreply.github.com> Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: Victor Navarro <vn4varro@gmail.com> Co-authored-by: Dax Raad <d@ironbay.co> Co-authored-by: Dax <mail@thdxr.com> Co-authored-by: Nabs <nabil@instafork.com> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com> Co-authored-by: AidenGeunGeun <eastlandwyvern@gmail.com> Co-authored-by: Mark <geraint0923@users.noreply.github.com> Co-authored-by: Jay <air@live.ca>
271 lines
9.6 KiB
TypeScript
271 lines
9.6 KiB
TypeScript
import { base64Encode } from "@opencode-ai/core/util/encode"
|
|
import { expect, test, type Page, type Route } from "@playwright/test"
|
|
import { installSseTransport } from "../utils/sse-transport"
|
|
|
|
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(process.platform === "darwin" ? "Meta+," : "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(process.platform === "darwin" ? "Meta+," : "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") return sse(route)
|
|
if (url.pathname === "/global/health") return json(route, { healthy: true })
|
|
if (url.pathname === "/session/status") return json(route, {})
|
|
if (url.pathname === "/session") return json(route, sessions)
|
|
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 === "/vcs") return json(route, { branch: "main", default_branch: "main" })
|
|
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" })
|
|
}
|