mock-server.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. import type { Page, Route } from "@playwright/test"
  2. const emptyList = new Set([
  3. "/skill",
  4. "/command",
  5. "/lsp",
  6. "/formatter",
  7. "/permission",
  8. "/question",
  9. "/vcs/status",
  10. "/vcs/diff",
  11. ])
  12. const emptyObject = new Set(["/global/config", "/config", "/provider/auth", "/mcp", "/session/status"])
  13. export interface MockServerConfig {
  14. provider: unknown
  15. directory: string
  16. project: unknown
  17. sessions: ({ id: string } & Record<string, unknown>)[]
  18. pageMessages: (sessionId: string, limit: number, before?: string) => { items: unknown[]; cursor?: string }
  19. vcsDiff?: unknown[]
  20. messageDelay?: number
  21. onMessages?: (input: { sessionID: string; before?: string; phase: "start" | "end" }) => void
  22. events?: () => unknown[]
  23. eventRetry?: number
  24. }
  25. export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
  26. const staticRoutes: Record<string, unknown> = {
  27. "/provider": config.provider,
  28. "/path": {
  29. state: config.directory,
  30. config: config.directory,
  31. worktree: config.directory,
  32. directory: config.directory,
  33. home: "C:/OpenCode",
  34. },
  35. "/project": [config.project],
  36. "/project/current": config.project,
  37. "/agent": [{ name: "build", mode: "primary" }],
  38. "/vcs": { branch: "main", default_branch: "main" },
  39. "/session": config.sessions,
  40. }
  41. await page.route("**/*", async (route) => {
  42. const url = new URL(route.request().url())
  43. const targetPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"
  44. const appPort = new URL(
  45. process.env.PLAYWRIGHT_BASE_URL ?? `http://127.0.0.1:${process.env.PLAYWRIGHT_PORT ?? "3000"}`,
  46. ).port
  47. if (url.port !== targetPort && url.port !== appPort) return route.fallback()
  48. const path = url.pathname
  49. if (path === "/global/event" || path === "/event") return sse(route, config.events?.(), config.eventRetry)
  50. if (path === "/global/health") return json(route, { healthy: true })
  51. if (path === "/vcs/diff" && config.vcsDiff) return json(route, config.vcsDiff)
  52. if (emptyObject.has(path)) return json(route, {})
  53. if (emptyList.has(path)) return json(route, [])
  54. if (path in staticRoutes) return json(route, staticRoutes[path])
  55. const sessionMatch = path.match(/^\/session\/([^/]+)$/)
  56. if (sessionMatch) {
  57. const session = config.sessions.find((s) => s.id === sessionMatch[1])
  58. return json(route, session ?? {})
  59. }
  60. if (/^\/session\/[^/]+\/(children|todo|diff)$/.test(path)) return json(route, [])
  61. const messagesMatch = path.match(/^\/session\/([^/]+)\/message$/)
  62. if (messagesMatch) {
  63. const before = url.searchParams.get("before") ?? undefined
  64. config.onMessages?.({ sessionID: messagesMatch[1], before, phase: "start" })
  65. if (config.messageDelay) await new Promise((resolve) => setTimeout(resolve, config.messageDelay))
  66. const limit = Number(url.searchParams.get("limit") ?? 80)
  67. const pageData = config.pageMessages(messagesMatch[1], limit, before)
  68. config.onMessages?.({ sessionID: messagesMatch[1], before, phase: "end" })
  69. return json(route, pageData.items, pageData.cursor ? { "x-next-cursor": pageData.cursor } : undefined)
  70. }
  71. if (url.port === targetPort && targetPort !== appPort) return json(route, {})
  72. return route.fallback()
  73. })
  74. }
  75. function json(route: Route, body: unknown, headers?: Record<string, string>) {
  76. return route.fulfill({
  77. status: 200,
  78. contentType: "application/json",
  79. headers: {
  80. "access-control-allow-origin": "*",
  81. "access-control-expose-headers": "x-next-cursor",
  82. ...headers,
  83. },
  84. body: JSON.stringify(body ?? null),
  85. })
  86. }
  87. function sse(route: Route, events?: unknown[], retry?: number) {
  88. return route.fulfill({
  89. status: 200,
  90. contentType: "text/event-stream",
  91. body: `${retry === undefined ? "" : `retry: ${retry}\n\n`}${events?.map((event) => `data: ${JSON.stringify(event)}\n\n`).join("") || ": ok\n\n"}`,
  92. })
  93. }