effect.test.ts 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. import { expect, test } from "bun:test"
  2. import { DateTime, Effect, Stream } from "effect"
  3. import { HttpClient, HttpClientResponse } from "effect/unstable/http"
  4. import { AbsolutePath, Agent, Location, Model, OpenCode, Prompt, Session, SessionMessage } from "../src/effect"
  5. test("sessions.get returns the decoded Effect projection", async () => {
  6. const httpClient = HttpClient.make((request) =>
  7. Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(session))),
  8. )
  9. const result = await Effect.gen(function* () {
  10. const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" })
  11. return yield* client.sessions.get({ sessionID: Session.ID.make("ses_test") })
  12. }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
  13. expect(DateTime.toEpochMillis(result.time.created)).toBe(1_717_171_717_000)
  14. })
  15. test("session methods retain decoded Effect inputs and outputs", async () => {
  16. const httpClient = HttpClient.make((request) => {
  17. const url = request.url
  18. if (url.includes("/event")) {
  19. return Effect.succeed(
  20. HttpClientResponse.fromWeb(
  21. request,
  22. new Response(`data: ${JSON.stringify(modelSwitchedEvent)}\n\n`, {
  23. headers: { "content-type": "text/event-stream" },
  24. }),
  25. ),
  26. )
  27. }
  28. if (url.includes("/prompt")) {
  29. return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(admission)))
  30. }
  31. if (url.includes("/context")) {
  32. return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json({ data: [] })))
  33. }
  34. if (url.includes("/message/")) {
  35. return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json({ data: modelSwitchedMessage })))
  36. }
  37. if (url.endsWith("/api/session/active")) {
  38. return Effect.succeed(
  39. HttpClientResponse.fromWeb(request, Response.json({ data: { ses_test: { type: "running" } } })),
  40. )
  41. }
  42. if (request.method === "POST" && url.endsWith("/api/session")) {
  43. return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(session)))
  44. }
  45. if (request.method === "POST") {
  46. return Effect.succeed(HttpClientResponse.fromWeb(request, new Response(null, { status: 204 })))
  47. }
  48. return Effect.succeed(
  49. HttpClientResponse.fromWeb(request, Response.json({ data: [session.data], cursor: { next: "next" } })),
  50. )
  51. })
  52. const result = await Effect.gen(function* () {
  53. const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" })
  54. const page = yield* client.sessions.list({ limit: 10 })
  55. const active = yield* client.sessions.active()
  56. const created = yield* client.sessions.create({
  57. location: Location.Ref.make({ directory: AbsolutePath.make("/tmp/project") }),
  58. })
  59. yield* client.sessions.switchAgent({ sessionID: Session.ID.make("ses_test"), agent: Agent.ID.make("build") })
  60. yield* client.sessions.switchModel({
  61. sessionID: Session.ID.make("ses_test"),
  62. model: Model.Ref.make({ id: "claude", providerID: "anthropic" }),
  63. })
  64. const admitted = yield* client.sessions.prompt({
  65. sessionID: Session.ID.make("ses_test"),
  66. prompt: Prompt.make({ text: "Hello" }),
  67. resume: false,
  68. })
  69. yield* client.sessions.compact({ sessionID: Session.ID.make("ses_test") })
  70. yield* client.sessions.wait({ sessionID: Session.ID.make("ses_test") })
  71. const context = yield* client.sessions.context({ sessionID: Session.ID.make("ses_test") })
  72. const events = yield* client.sessions
  73. .events({ sessionID: Session.ID.make("ses_test"), after: 0 })
  74. .pipe(Stream.runCollect)
  75. yield* client.sessions.interrupt({ sessionID: Session.ID.make("ses_test") })
  76. const message = yield* client.sessions.message({
  77. sessionID: Session.ID.make("ses_test"),
  78. messageID: SessionMessage.ID.make("msg_model"),
  79. })
  80. return { page, active, created, admitted, context, events, message }
  81. }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
  82. expect(DateTime.toEpochMillis(result.page.data[0].time.created)).toBe(1_717_171_717_000)
  83. expect(result.active).toEqual({ ses_test: { type: "running" } })
  84. expect(Object.getPrototypeOf(result.page.data[0])).toBe(Object.prototype)
  85. expect(Object.getPrototypeOf(result.created)).toBe(Object.prototype)
  86. expect(result.created.id).toBe("ses_test")
  87. expect(Object.getPrototypeOf(result.admitted)).toBe(Object.prototype)
  88. expect(Object.getPrototypeOf(result.admitted.prompt)).toBe(Object.prototype)
  89. expect(DateTime.toEpochMillis(result.admitted.timeCreated)).toBe(1_717_171_717_000)
  90. expect(result.context).toEqual([])
  91. expect(DateTime.toEpochMillis(result.events[0].data.timestamp)).toBe(1_717_171_717_000)
  92. expect(result.message).toEqual(expect.objectContaining({ id: "msg_model", type: "model-switched" }))
  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. }