mock-server.test.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. import { expect, test } from "bun:test"
  2. import type { SessionMessageInfo } from "@opencode-ai/client/promise"
  3. import type { Page, Route } from "@playwright/test"
  4. import { currentMessage, mockOpenCodeServer } from "../../utils/mock-server"
  5. test("preserves current messages", () => {
  6. const message = {
  7. id: "msg_current",
  8. type: "user",
  9. time: { created: 1 },
  10. text: "current",
  11. files: [{ data: "e30=", mime: "application/json", source: { type: "inline" } }],
  12. } satisfies SessionMessageInfo
  13. expect(currentMessage(message)).toBe(message)
  14. })
  15. test("converts rich legacy messages to current message types", () => {
  16. expect(
  17. currentMessage({
  18. info: { id: "msg_user", role: "user", time: { created: 1 } },
  19. parts: [
  20. { type: "text", text: "Use @src/a.ts with @explore" },
  21. {
  22. type: "file",
  23. mime: "application/json",
  24. filename: "data.json",
  25. url: "data:application/json;base64,e30=",
  26. },
  27. {
  28. type: "file",
  29. mime: "text/plain",
  30. filename: "a.ts",
  31. url: "src/a.ts",
  32. source: { type: "file", text: { value: "@src/a.ts", start: 4, end: 13 } },
  33. },
  34. { type: "agent", name: "explore", source: { value: "@explore", start: 19, end: 27 } },
  35. ],
  36. }),
  37. ).toEqual({
  38. id: "msg_user",
  39. type: "user",
  40. time: { created: 1 },
  41. text: "Use @src/a.ts with @explore",
  42. files: [
  43. { data: "e30=", mime: "application/json", name: "data.json", source: { type: "inline" } },
  44. {
  45. data: "",
  46. mime: "text/plain",
  47. name: "a.ts",
  48. source: { type: "uri", uri: "src/a.ts" },
  49. mention: { text: "@src/a.ts", start: 4, end: 13 },
  50. },
  51. ],
  52. agents: [{ name: "explore", mention: { text: "@explore", start: 19, end: 27 } }],
  53. })
  54. expect(
  55. currentMessage({
  56. info: {
  57. id: "msg_assistant",
  58. role: "assistant",
  59. time: { created: 2, completed: 5 },
  60. agent: "explore",
  61. modelID: "model",
  62. providerID: "provider",
  63. variant: "high",
  64. cost: 0.5,
  65. tokens: { input: 1, output: 2, reasoning: 3, cache: { read: 4, write: 5 } },
  66. finish: "tool-calls",
  67. error: { name: "MessageAbortedError", data: { message: "Stopped" } },
  68. },
  69. parts: [
  70. { type: "text", text: "Answer" },
  71. { type: "reasoning", text: "Thinking", time: { start: 2, end: 3 } },
  72. {
  73. id: "prt_tool",
  74. callID: "call_tool",
  75. type: "tool",
  76. tool: "read",
  77. state: {
  78. status: "completed",
  79. input: { filePath: "src/a.ts" },
  80. output: "contents",
  81. metadata: { title: "a.ts" },
  82. time: { start: 3, end: 4 },
  83. },
  84. },
  85. ],
  86. }),
  87. ).toEqual({
  88. id: "msg_assistant",
  89. type: "assistant",
  90. time: { created: 2, completed: 5 },
  91. agent: "explore",
  92. model: { id: "model", providerID: "provider", variant: "high" },
  93. cost: 0.5,
  94. tokens: { input: 1, output: 2, reasoning: 3, cache: { read: 4, write: 5 } },
  95. finish: "tool-calls",
  96. error: { type: "MessageAbortedError", message: "Stopped" },
  97. content: [
  98. { type: "text", text: "Answer" },
  99. { type: "reasoning", text: "Thinking", time: { created: 2, completed: 3 } },
  100. {
  101. type: "tool",
  102. id: "call_tool",
  103. name: "read",
  104. time: { created: 3, ran: 3, completed: 4 },
  105. state: {
  106. status: "completed",
  107. input: { filePath: "src/a.ts" },
  108. content: [{ type: "text", text: "contents" }],
  109. metadata: { title: "a.ts" },
  110. },
  111. },
  112. ],
  113. })
  114. })
  115. test("applies message latency after a list response gate is released", async () => {
  116. const events: string[] = []
  117. const gate = Promise.withResolvers<void>()
  118. let handler: ((route: Route) => Promise<void>) | undefined
  119. const page = {
  120. route: (_url: string, callback: (route: Route) => Promise<void>) => {
  121. handler = callback
  122. return Promise.resolve()
  123. },
  124. } as unknown as Page
  125. await mockOpenCodeServer(page, {
  126. provider: {},
  127. directory: "C:/OpenCode",
  128. project: {},
  129. sessions: [{ id: "session" }],
  130. messageDelay: 25,
  131. beforeMessagesResponse: () => {
  132. events.push("before")
  133. return gate.promise
  134. },
  135. onMessages: (request) => events.push(request.phase),
  136. pageMessages: () => {
  137. events.push("page")
  138. return { items: [] }
  139. },
  140. })
  141. const response = handler!({
  142. request: () => ({ url: () => "http://127.0.0.1:4096/api/session/session/message" }),
  143. fulfill: () => {
  144. events.push("fulfill")
  145. return Promise.resolve()
  146. },
  147. } as unknown as Route)
  148. expect(events).toEqual(["start", "before"])
  149. const released = performance.now()
  150. gate.resolve()
  151. await response
  152. expect(performance.now() - released).toBeGreaterThanOrEqual(20)
  153. expect(events).toEqual(["start", "before", "page", "end", "fulfill"])
  154. })