promise.test.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  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 (init?.method === "POST" && url.endsWith("/api/session")) return Response.json(session)
  32. if (init?.method === "POST") return new Response(null, { status: 204 })
  33. return Response.json({ data: [session.data], cursor: { next: "next" } })
  34. },
  35. })
  36. const page = await client.sessions.list({ limit: "10", order: "desc" })
  37. const created = await client.sessions.create({ location: { directory: "/tmp/project" } })
  38. await client.sessions.switchAgent({ sessionID: "ses_test", agent: "build" })
  39. await client.sessions.switchModel({
  40. sessionID: "ses_test",
  41. model: { id: "claude", providerID: "anthropic" },
  42. })
  43. const admitted = await client.sessions.prompt({
  44. sessionID: "ses_test",
  45. prompt: { text: "Hello" },
  46. resume: false,
  47. })
  48. await client.sessions.compact({ sessionID: "ses_test" })
  49. await client.sessions.wait({ sessionID: "ses_test" })
  50. const context = await client.sessions.context({ sessionID: "ses_test" })
  51. const events = []
  52. for await (const event of client.sessions.events({ sessionID: "ses_test", after: "0" })) events.push(event)
  53. await client.sessions.interrupt({ sessionID: "ses_test" })
  54. const message = await client.sessions.message({ sessionID: "ses_test", messageID: "msg_model" })
  55. expect(page.cursor.next).toBe("next")
  56. expect(created.id).toBe("ses_test")
  57. expect(admitted.id).toBe("msg_test")
  58. expect(context).toEqual([])
  59. expect(events).toEqual([modelSwitchedEvent])
  60. expect(message).toEqual(modelSwitchedMessage)
  61. expect(requests.map((request) => [request.init?.method, request.url])).toEqual([
  62. ["GET", "http://localhost:3000/api/session?limit=10&order=desc"],
  63. ["POST", "http://localhost:3000/api/session"],
  64. ["POST", "http://localhost:3000/api/session/ses_test/agent"],
  65. ["POST", "http://localhost:3000/api/session/ses_test/model"],
  66. ["POST", "http://localhost:3000/api/session/ses_test/prompt"],
  67. ["POST", "http://localhost:3000/api/session/ses_test/compact"],
  68. ["POST", "http://localhost:3000/api/session/ses_test/wait"],
  69. ["GET", "http://localhost:3000/api/session/ses_test/context"],
  70. ["GET", "http://localhost:3000/api/session/ses_test/event?after=0"],
  71. ["POST", "http://localhost:3000/api/session/ses_test/interrupt"],
  72. ["GET", "http://localhost:3000/api/session/ses_test/message/msg_model"],
  73. ])
  74. const body = requests[4]?.init?.body
  75. if (typeof body !== "string") throw new Error("Expected JSON request body")
  76. expect(JSON.parse(body)).toEqual({
  77. prompt: { text: "Hello" },
  78. resume: false,
  79. })
  80. })
  81. test("middleware errors remain declared client errors", async () => {
  82. const client = OpenCode.make({
  83. baseUrl: "http://localhost:3000",
  84. fetch: async () =>
  85. Response.json({ _tag: "UnauthorizedError", message: "Authentication required" }, { status: 401 }),
  86. })
  87. try {
  88. await client.sessions.create({})
  89. throw new Error("Expected request to fail")
  90. } catch (error) {
  91. expect(isUnauthorizedError(error)).toBe(true)
  92. }
  93. })
  94. const session = {
  95. data: {
  96. id: "ses_test",
  97. projectID: "project",
  98. cost: 0,
  99. tokens: {
  100. input: 1,
  101. output: 2,
  102. reasoning: 3,
  103. cache: { read: 4, write: 5 },
  104. },
  105. time: {
  106. created: 1_717_171_717_000,
  107. updated: 1_717_171_717_000,
  108. },
  109. title: "Test",
  110. location: { directory: "/tmp/project" },
  111. },
  112. }
  113. const admission = {
  114. data: {
  115. admittedSeq: 0,
  116. id: "msg_test",
  117. sessionID: "ses_test",
  118. prompt: { text: "Hello" },
  119. delivery: "steer",
  120. timeCreated: 1_717_171_717_000,
  121. },
  122. }
  123. const modelSwitchedMessage = {
  124. id: "msg_model",
  125. type: "model-switched",
  126. time: { created: 1_717_171_717_000 },
  127. model: { id: "claude", providerID: "anthropic" },
  128. }
  129. const modelSwitchedEvent = {
  130. id: "evt_model",
  131. type: "session.next.model.switched",
  132. durable: { aggregateID: "ses_test", seq: 1, version: 1 },
  133. data: {
  134. timestamp: 1_717_171_717_000,
  135. sessionID: "ses_test",
  136. messageID: "msg_model",
  137. model: { id: "claude", providerID: "anthropic" },
  138. },
  139. }