+16




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


![opencode-agent[bot] <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] <opencode-agent[bot]@users.noreply.github.com>
usrnk1
James Long
opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
Aiden Cline
Brendan Allan
Jack
opencode
Frank
Jay
Luke Parker
Aarav Sareen
Julian Coy
Brendan Allan
Vladimir Glafirov
Adam
Dustin Deus
Kit Langton
Simon Klee
Jay
David Hill
Aiden Cline
James Long
冯基魁
Aiden Cline
a7746379d5
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: James Long <longster@gmail.com> 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: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: Julian Coy <julian@ex-machina.co> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com> Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: Simon Klee <hello@simonklee.dk> Co-authored-by: Jay <air@live.ca> Co-authored-by: David Hill <1879069+iamdavidhill@users.noreply.github.com> Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: James Long <jlongster@users.noreply.github.com> Co-authored-by: 冯基魁 <56265583+fengjikui@users.noreply.github.com> Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
152 lines
6.7 KiB
TypeScript
152 lines
6.7 KiB
TypeScript
import type { Page, Route } from "@playwright/test"
|
|
|
|
const emptyList = new Set(["/skill", "/command", "/lsp", "/formatter", "/vcs/status", "/vcs/diff"])
|
|
const emptyObject = new Set(["/global/config", "/config", "/provider/auth", "/mcp", "/experimental/resource"])
|
|
|
|
export interface MockServerConfig {
|
|
provider: unknown
|
|
directory: string
|
|
project: unknown
|
|
sessions: ({ id: string } & Record<string, unknown>)[]
|
|
pageMessages: (sessionId: string, limit: number, before?: string) => { items: unknown[]; cursor?: string }
|
|
vcsDiff?: unknown[]
|
|
messageDelay?: number
|
|
beforeMessagesResponse?: (input: { sessionID: string; before?: string }) => Promise<void>
|
|
onMessages?: (input: { sessionID: string; before?: string; phase: "start" | "end" }) => void
|
|
message?: (sessionID: string, messageID: string) => unknown
|
|
onMessage?: (input: { sessionID: string; messageID: string }) => void
|
|
events?: () => unknown[]
|
|
eventRetry?: number
|
|
permissions?: unknown[] | (() => unknown[])
|
|
questions?: unknown[] | (() => unknown[])
|
|
fileList?: (path: string) => unknown | Promise<unknown>
|
|
fileContent?: (path: string) => unknown | Promise<unknown>
|
|
findFiles?: (input: { query: string; dirs?: string; limit?: number }) => unknown
|
|
sessionStatus?: unknown
|
|
}
|
|
|
|
export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
|
|
const cursors = new Map<string, string>()
|
|
let nextCursor = 0
|
|
const staticRoutes: Record<string, unknown> = {
|
|
"/provider": config.provider,
|
|
"/path": {
|
|
state: config.directory,
|
|
config: config.directory,
|
|
worktree: config.directory,
|
|
directory: config.directory,
|
|
home: "C:/OpenCode",
|
|
},
|
|
"/project": [config.project],
|
|
"/project/current": config.project,
|
|
"/agent": [{ name: "build", mode: "primary" }],
|
|
"/vcs": { branch: "main", default_branch: "main" },
|
|
"/session": config.sessions,
|
|
}
|
|
|
|
await page.route("**/*", async (route) => {
|
|
const url = new URL(route.request().url())
|
|
const targetPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"
|
|
const appPort = new URL(
|
|
process.env.PLAYWRIGHT_BASE_URL ?? `http://127.0.0.1:${process.env.PLAYWRIGHT_PORT ?? "3000"}`,
|
|
).port
|
|
if (url.port !== targetPort && url.port !== appPort) return route.fallback()
|
|
|
|
const path = url.pathname
|
|
if (path === "/global/event" || path === "/event") return sse(route, config.events?.(), config.eventRetry)
|
|
if (path === "/global/health") return json(route, { healthy: true })
|
|
if (path === "/experimental/capabilities") return json(route, { backgroundSubagents: true })
|
|
if (path === "/permission")
|
|
return json(route, typeof config.permissions === "function" ? config.permissions() : (config.permissions ?? []))
|
|
if (path === "/question")
|
|
return json(route, typeof config.questions === "function" ? config.questions() : (config.questions ?? []))
|
|
if (path === "/session/status") return json(route, config.sessionStatus ?? {})
|
|
if (path === "/vcs/diff" && config.vcsDiff) return json(route, config.vcsDiff)
|
|
if (path === "/file" && config.fileList)
|
|
return json(route, await config.fileList(url.searchParams.get("path") ?? ""))
|
|
if (path === "/file/content" && config.fileContent)
|
|
return json(route, await config.fileContent(url.searchParams.get("path") ?? ""))
|
|
if (path === "/find/file" && config.findFiles)
|
|
return json(
|
|
route,
|
|
await config.findFiles({
|
|
query: url.searchParams.get("query") ?? "",
|
|
dirs: url.searchParams.get("dirs") ?? undefined,
|
|
limit: url.searchParams.has("limit") ? Number(url.searchParams.get("limit")) : undefined,
|
|
}),
|
|
)
|
|
if (path === "/api/reference")
|
|
return json(route, {
|
|
location: {
|
|
directory: config.directory,
|
|
project: { id: (config.project as { id?: string }).id, directory: config.directory },
|
|
},
|
|
data: [],
|
|
})
|
|
if (emptyObject.has(path)) return json(route, {})
|
|
if (emptyList.has(path)) return json(route, [])
|
|
if (path in staticRoutes) return json(route, staticRoutes[path])
|
|
|
|
const sessionMatch = path.match(/^\/session\/([^/]+)$/)
|
|
if (sessionMatch) {
|
|
const session = config.sessions.find((s) => s.id === sessionMatch[1])
|
|
return json(route, session ?? {})
|
|
}
|
|
|
|
const projectMatch = path.match(/^\/project\/([^/]+)$/)
|
|
if (projectMatch) return json(route, config.project)
|
|
|
|
const messageMatch = path.match(/^\/session\/([^/]+)\/message\/([^/]+)$/)
|
|
if (messageMatch) {
|
|
config.onMessage?.({ sessionID: messageMatch[1]!, messageID: messageMatch[2]! })
|
|
if (config.messageDelay !== undefined) await new Promise((resolve) => setTimeout(resolve, config.messageDelay))
|
|
const message = config.message?.(messageMatch[1]!, messageMatch[2]!)
|
|
if (message === undefined) return json(route, { error: "Message not found" }, undefined, 404)
|
|
return json(route, message)
|
|
}
|
|
|
|
if (/^\/session\/[^/]+\/(children|diff)$/.test(path)) return json(route, [])
|
|
|
|
const messagesMatch = path.match(/^\/session\/([^/]+)\/message$/)
|
|
if (messagesMatch) {
|
|
const token = url.searchParams.get("before") ?? undefined
|
|
const before = token ? cursors.get(token) : undefined
|
|
if (token && !before) return json(route, { error: "Invalid cursor" }, undefined, 400)
|
|
config.onMessages?.({ sessionID: messagesMatch[1], before, phase: "start" })
|
|
await config.beforeMessagesResponse?.({ sessionID: messagesMatch[1]!, before })
|
|
if (config.messageDelay !== undefined) await new Promise((resolve) => setTimeout(resolve, config.messageDelay))
|
|
const limit = Number(url.searchParams.get("limit") ?? 80)
|
|
const pageData = config.pageMessages(messagesMatch[1], limit, before)
|
|
config.onMessages?.({ sessionID: messagesMatch[1], before, phase: "end" })
|
|
if (!pageData.cursor) return json(route, pageData.items)
|
|
const cursor = `cursor_${++nextCursor}`
|
|
cursors.set(cursor, pageData.cursor)
|
|
return json(route, pageData.items, { "x-next-cursor": cursor })
|
|
}
|
|
|
|
if (url.port === targetPort && targetPort !== appPort) return json(route, {})
|
|
return route.fallback()
|
|
})
|
|
}
|
|
|
|
function json(route: Route, body: unknown, headers?: Record<string, string>, status = 200) {
|
|
return route.fulfill({
|
|
status,
|
|
contentType: "application/json",
|
|
headers: {
|
|
"access-control-allow-origin": "*",
|
|
"access-control-expose-headers": "x-next-cursor",
|
|
...headers,
|
|
},
|
|
body: JSON.stringify(body ?? null),
|
|
})
|
|
}
|
|
|
|
function sse(route: Route, events?: unknown[], retry?: number) {
|
|
return route.fulfill({
|
|
status: 200,
|
|
contentType: "text/event-stream",
|
|
body: `${retry === undefined ? "" : `retry: ${retry}\n\n`}${events?.map((event) => `data: ${JSON.stringify(event)}\n\n`).join("") || ": ok\n\n"}`,
|
|
})
|
|
}
|