session-request-docks.spec.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import { base64Encode } from "@opencode-ai/core/util/encode"
  2. import { expect, test, type Page } from "@playwright/test"
  3. import { mockOpenCodeServer } from "../utils/mock-server"
  4. import { expectSessionTitle } from "../utils/waits"
  5. const directory = "C:/OpenCode/RequestDocks"
  6. const projectID = "proj_request_docks"
  7. const sessionID = "ses_request_docks"
  8. const title = "Request dock regression"
  9. test("shows a pending question dock", async ({ page }) => {
  10. await mockServer(page, {
  11. questions: [
  12. {
  13. id: "question-request",
  14. sessionID,
  15. questions: [
  16. {
  17. header: "Implementation",
  18. question: "Which implementation should be used?",
  19. options: [
  20. { label: "Minimal", description: "Use the smallest correct change" },
  21. { label: "Extended", description: "Include additional behavior" },
  22. ],
  23. },
  24. ],
  25. },
  26. ],
  27. })
  28. await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
  29. await expectSessionTitle(page, title)
  30. const question = page.locator('[data-component="dock-prompt"][data-kind="question"]')
  31. await expect(question).toBeVisible()
  32. await expect(question.getByText("Which implementation should be used?")).toBeVisible()
  33. await expect(question.getByRole("radio", { name: /Minimal/ })).toBeVisible()
  34. await expect(question.getByRole("radio", { name: /Extended/ })).toBeVisible()
  35. await expect(page.locator('[data-component="session-composer"]')).toHaveCount(0)
  36. const rejectRequests: string[] = []
  37. page.on("request", (request) => {
  38. if (request.method() !== "POST") return
  39. if (new URL(request.url()).pathname === "/question/question-request/reject") rejectRequests.push(request.url())
  40. })
  41. await question.locator('[data-component="icon-button"][data-icon="chevron-down"]').click()
  42. await expect(question).toBeVisible()
  43. await expect(question.getByText("Which implementation should be used?")).toBeVisible()
  44. await expect(question.getByText("Select one answer")).toBeHidden()
  45. await expect(question.getByRole("radio", { name: /Minimal/ })).toBeHidden()
  46. await expect(question.getByRole("radio", { name: /Extended/ })).toBeHidden()
  47. await expect(question.getByRole("button", { name: "Dismiss" })).toBeVisible()
  48. await expect(question.getByRole("button", { name: "Submit" })).toBeVisible()
  49. await expect(page.locator('[data-component="question-minimized-dock"]')).toHaveCount(0)
  50. expect(rejectRequests).toEqual([])
  51. await question.locator('[data-component="icon-button"][data-icon="chevron-down"]').click()
  52. await expect(question).toBeVisible()
  53. await expect(question.getByText("Which implementation should be used?")).toBeVisible()
  54. await expect(question.getByRole("radio", { name: /Minimal/ })).toBeVisible()
  55. expect(rejectRequests).toEqual([])
  56. await question.getByRole("radio", { name: /Minimal/ }).click()
  57. const reply = page.waitForRequest(
  58. (request) => request.method() === "POST" && new URL(request.url()).pathname === "/question/question-request/reply",
  59. )
  60. await question.getByRole("button", { name: "Submit" }).click()
  61. expect((await reply).postDataJSON()).toEqual({ answers: [["Minimal"]] })
  62. })
  63. test("shows a pending permission dock", async ({ page }) => {
  64. await mockServer(page, {
  65. permissions: [
  66. {
  67. id: "permission-request",
  68. sessionID,
  69. permission: "bash",
  70. patterns: ["git status", "git diff"],
  71. metadata: {},
  72. always: [],
  73. },
  74. ],
  75. })
  76. await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
  77. await expectSessionTitle(page, title)
  78. const permission = page.locator('[data-component="dock-prompt"][data-kind="permission"]')
  79. await expect(permission).toBeVisible()
  80. await expect(permission.getByText("git status")).toBeVisible()
  81. await expect(permission.getByText("git diff")).toBeVisible()
  82. await expect(permission.locator('[data-slot="permission-footer-actions"] button')).toHaveCount(3)
  83. await expect(page.locator('[data-component="session-composer"]')).toHaveCount(0)
  84. const reply = page.waitForRequest((request) => request.method() === "POST")
  85. await permission.getByRole("button", { name: "Allow once" }).click()
  86. const request = await reply
  87. expect(new URL(request.url()).pathname).toBe(`/session/${sessionID}/permissions/permission-request`)
  88. expect(request.postDataJSON()).toEqual({ response: "once" })
  89. })
  90. async function mockServer(
  91. page: Page,
  92. requests: {
  93. permissions?: unknown[] | (() => unknown[])
  94. questions?: unknown[] | (() => unknown[])
  95. },
  96. ) {
  97. await mockOpenCodeServer(page, {
  98. directory,
  99. project: {
  100. id: projectID,
  101. worktree: directory,
  102. vcs: "git",
  103. name: "request-docks",
  104. time: { created: 1700000000000, updated: 1700000000000 },
  105. sandboxes: [],
  106. },
  107. provider: {
  108. all: [
  109. {
  110. id: "opencode",
  111. name: "OpenCode",
  112. models: {
  113. "claude-opus-4-6": {
  114. id: "claude-opus-4-6",
  115. name: "Claude Opus 4.6",
  116. limit: { context: 200_000 },
  117. },
  118. },
  119. },
  120. ],
  121. connected: ["opencode"],
  122. default: { providerID: "opencode", modelID: "claude-opus-4-6" },
  123. },
  124. sessions: [
  125. {
  126. id: sessionID,
  127. slug: "request-docks",
  128. projectID,
  129. directory,
  130. title,
  131. version: "dev",
  132. time: { created: 1700000000000, updated: 1700000000000 },
  133. },
  134. ],
  135. pageMessages: () => ({ items: [] }),
  136. permissions: requests.permissions,
  137. questions: requests.questions,
  138. })
  139. await page.addInitScript(() => {
  140. localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
  141. })
  142. }