promise.test.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import { expect, test } from "bun:test"
  2. import { isUnauthorizedError, OpenCode } from "../src"
  3. test("sessions.get returns the wire projection", async () => {
  4. const client = OpenCode.make({
  5. baseUrl: "http://localhost:3000",
  6. fetch: async (input) => {
  7. expect(typeof input === "string" ? input : input instanceof URL ? input.href : input.url).toBe(
  8. "http://localhost:3000/api/session/ses_test",
  9. )
  10. return Response.json(session)
  11. },
  12. })
  13. const result = await client.sessions.get({ sessionID: "ses_test" })
  14. expect(result.time.created).toBe(1_717_171_717_000)
  15. })
  16. test("session methods use the public HTTP contract", async () => {
  17. const requests: Array<{ url: string; init?: RequestInit }> = []
  18. const client = OpenCode.make({
  19. baseUrl: "http://localhost:3000",
  20. fetch: async (input, init) => {
  21. const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url
  22. requests.push({ url, init })
  23. if (url.includes("/event")) {
  24. return new Response(`data: ${JSON.stringify(modelSwitchedEvent)}\n\n`, {
  25. headers: { "content-type": "text/event-stream" },
  26. })
  27. }
  28. if (url.includes("/prompt")) return Response.json(admission)
  29. if (url.includes("/context")) return Response.json({ data: [] })
  30. if (url.includes("/message/")) return Response.json({ data: modelSwitchedMessage })
  31. if (url.endsWith("/api/session/active")) return Response.json({ data: { ses_test: { type: "running" } } })
  32. if (init?.method === "POST" && url.endsWith("/api/session")) return Response.json(session)
  33. if (init?.method === "POST") return new Response(null, { status: 204 })
  34. return Response.json({ data: [session.data], cursor: { next: "next" } })
  35. },
  36. })
  37. const page = await client.sessions.list({ limit: "10", order: "desc" })
  38. const active = await client.sessions.active()
  39. const created = await client.sessions.create({ location: { directory: "/tmp/project" } })
  40. await client.sessions.switchAgent({ sessionID: "ses_test", agent: "build" })
  41. await client.sessions.switchModel({
  42. sessionID: "ses_test",
  43. model: { id: "claude", providerID: "anthropic" },
  44. })
  45. const admitted = await client.sessions.prompt({
  46. sessionID: "ses_test",
  47. prompt: { text: "Hello" },
  48. resume: false,
  49. })
  50. await client.sessions.compact({ sessionID: "ses_test" })
  51. await client.sessions.wait({ sessionID: "ses_test" })
  52. const context = await client.sessions.context({ sessionID: "ses_test" })
  53. const events = []
  54. for await (const event of client.sessions.events({ sessionID: "ses_test", after: "0" })) events.push(event)
  55. await client.sessions.interrupt({ sessionID: "ses_test" })
  56. const message = await client.sessions.message({ sessionID: "ses_test", messageID: "msg_model" })
  57. expect(page.cursor.next).toBe("next")
  58. expect(active).toEqual({ ses_test: { type: "running" } })
  59. expect(created.id).toBe("ses_test")
  60. expect(admitted.id).toBe("msg_test")
  61. expect(context).toEqual([])
  62. expect(events).toEqual([modelSwitchedEvent])
  63. expect(message).toEqual(modelSwitchedMessage)
  64. expect(requests.map((request) => [request.init?.method, request.url])).toEqual([
  65. ["GET", "http://localhost:3000/api/session?limit=10&order=desc"],
  66. ["GET", "http://localhost:3000/api/session/active"],
  67. ["POST", "http://localhost:3000/api/session"],
  68. ["POST", "http://localhost:3000/api/session/ses_test/agent"],
  69. ["POST", "http://localhost:3000/api/session/ses_test/model"],
  70. ["POST", "http://localhost:3000/api/session/ses_test/prompt"],
  71. ["POST", "http://localhost:3000/api/session/ses_test/compact"],
  72. ["POST", "http://localhost:3000/api/session/ses_test/wait"],
  73. ["GET", "http://localhost:3000/api/session/ses_test/context"],
  74. ["GET", "http://localhost:3000/api/session/ses_test/event?after=0"],
  75. ["POST", "http://localhost:3000/api/session/ses_test/interrupt"],
  76. ["GET", "http://localhost:3000/api/session/ses_test/message/msg_model"],
  77. ])
  78. const body = requests.find((request) => request.url.endsWith("/api/session/ses_test/prompt"))?.init?.body
  79. if (typeof body !== "string") throw new Error("Expected JSON request body")
  80. expect(JSON.parse(body)).toEqual({
  81. prompt: { text: "Hello" },
  82. resume: false,
  83. })
  84. })
  85. test("middleware errors remain declared client errors", async () => {
  86. const client = OpenCode.make({
  87. baseUrl: "http://localhost:3000",
  88. fetch: async () =>
  89. Response.json({ _tag: "UnauthorizedError", message: "Authentication required" }, { status: 401 }),
  90. })
  91. try {
  92. await client.sessions.create({})
  93. throw new Error("Expected request to fail")
  94. } catch (error) {
  95. expect(isUnauthorizedError(error)).toBe(true)
  96. }
  97. })
  98. const session = {
  99. data: {
  100. id: "ses_test",
  101. projectID: "project",
  102. cost: 0,
  103. tokens: {
  104. input: 1,
  105. output: 2,
  106. reasoning: 3,
  107. cache: { read: 4, write: 5 },
  108. },
  109. time: {
  110. created: 1_717_171_717_000,
  111. updated: 1_717_171_717_000,
  112. },
  113. title: "Test",
  114. location: { directory: "/tmp/project" },
  115. },
  116. }
  117. const admission = {
  118. data: {
  119. admittedSeq: 0,
  120. id: "msg_test",
  121. sessionID: "ses_test",
  122. prompt: { text: "Hello" },
  123. delivery: "steer",
  124. timeCreated: 1_717_171_717_000,
  125. },
  126. }
  127. const modelSwitchedMessage = {
  128. id: "msg_model",
  129. type: "model-switched",
  130. time: { created: 1_717_171_717_000 },
  131. model: { id: "claude", providerID: "anthropic" },
  132. }
  133. const modelSwitchedEvent = {
  134. id: "evt_model",
  135. type: "session.next.model.switched",
  136. durable: { aggregateID: "ses_test", seq: 1, version: 1 },
  137. data: {
  138. timestamp: 1_717_171_717_000,
  139. sessionID: "ses_test",
  140. messageID: "msg_model",
  141. model: { id: "claude", providerID: "anthropic" },
  142. },
  143. }