server-session-v2-reducer.test.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. import { describe, expect, test } from "bun:test"
  2. import type { OpenCodeEvent, SessionMessageInfo } from "@opencode-ai/client/promise"
  3. import { createV2SessionReducer } from "./server-session-v2-reducer"
  4. const event = (input: object) => input as OpenCodeEvent
  5. const base = { created: 1, location: { directory: "/repo" }, durable: { aggregateID: "ses_1", seq: 1, version: 1 } }
  6. describe("v2 session reducer", () => {
  7. test("projects promoted input and streaming assistant content", () => {
  8. const reducer = createV2SessionReducer()
  9. let messages: SessionMessageInfo[] = []
  10. const apply = (input: object) => {
  11. const result = reducer.reduce(messages, event(input))
  12. if (result) messages = result.messages
  13. return result
  14. }
  15. apply({
  16. ...base,
  17. id: "evt_admitted",
  18. type: "session.input.admitted",
  19. data: {
  20. sessionID: "ses_1",
  21. inputID: "msg_user",
  22. input: { type: "user", delivery: "steer", data: { text: "hello" } },
  23. },
  24. })
  25. apply({
  26. ...base,
  27. id: "evt_promoted",
  28. type: "session.input.promoted",
  29. data: { sessionID: "ses_1", inputID: "msg_user" },
  30. })
  31. apply({
  32. ...base,
  33. id: "evt_step",
  34. type: "session.step.started",
  35. data: {
  36. sessionID: "ses_1",
  37. assistantMessageID: "msg_assistant",
  38. agent: "build",
  39. model: { id: "model", providerID: "provider" },
  40. },
  41. })
  42. apply({
  43. ...base,
  44. id: "evt_text_start",
  45. type: "session.text.started",
  46. data: { sessionID: "ses_1", assistantMessageID: "msg_assistant", ordinal: 0 },
  47. })
  48. apply({
  49. ...base,
  50. id: "evt_text_delta",
  51. type: "session.text.delta",
  52. data: { sessionID: "ses_1", assistantMessageID: "msg_assistant", ordinal: 0, delta: "hel" },
  53. })
  54. apply({
  55. ...base,
  56. id: "evt_text_end",
  57. type: "session.text.ended",
  58. data: { sessionID: "ses_1", assistantMessageID: "msg_assistant", ordinal: 0, text: "hello" },
  59. })
  60. expect(messages[0]).toMatchObject({ id: "msg_user", type: "user", text: "hello" })
  61. expect(messages[1]).toMatchObject({
  62. id: "msg_assistant",
  63. type: "assistant",
  64. content: [{ type: "text", text: "hello" }],
  65. })
  66. })
  67. test("folds tool, retry, and completion events", () => {
  68. const reducer = createV2SessionReducer()
  69. let messages: SessionMessageInfo[] = []
  70. const apply = (input: object) => {
  71. const result = reducer.reduce(messages, event(input))
  72. if (result) messages = result.messages
  73. }
  74. apply({
  75. ...base,
  76. id: "evt_step",
  77. type: "session.step.started",
  78. data: {
  79. sessionID: "ses_1",
  80. assistantMessageID: "msg_assistant",
  81. agent: "build",
  82. model: { id: "model", providerID: "provider" },
  83. },
  84. })
  85. apply({
  86. ...base,
  87. id: "evt_tool_start",
  88. type: "session.tool.input.started",
  89. data: { sessionID: "ses_1", assistantMessageID: "msg_assistant", callID: "call_1", name: "bash" },
  90. })
  91. apply({
  92. ...base,
  93. id: "evt_tool_delta",
  94. type: "session.tool.input.delta",
  95. data: { sessionID: "ses_1", assistantMessageID: "msg_assistant", callID: "call_1", delta: "{}" },
  96. })
  97. apply({
  98. ...base,
  99. id: "evt_tool_called",
  100. type: "session.tool.called",
  101. data: { sessionID: "ses_1", assistantMessageID: "msg_assistant", callID: "call_1", input: {}, executed: true },
  102. })
  103. apply({
  104. ...base,
  105. id: "evt_tool_success",
  106. type: "session.tool.success",
  107. data: {
  108. sessionID: "ses_1",
  109. assistantMessageID: "msg_assistant",
  110. callID: "call_1",
  111. metadata: {},
  112. content: [{ type: "text", text: "done" }],
  113. executed: true,
  114. },
  115. })
  116. apply({
  117. ...base,
  118. id: "evt_retry",
  119. type: "session.retry.scheduled",
  120. data: {
  121. sessionID: "ses_1",
  122. assistantMessageID: "msg_assistant",
  123. attempt: 2,
  124. at: 10,
  125. error: { type: "ProviderError", message: "retry" },
  126. },
  127. })
  128. apply({ ...base, id: "evt_done", type: "session.execution.succeeded", data: { sessionID: "ses_1" } })
  129. expect(messages[0]).toMatchObject({
  130. type: "assistant",
  131. retry: undefined,
  132. content: [{ type: "tool", id: "call_1", state: { status: "completed", content: [{ text: "done" }] } }],
  133. })
  134. })
  135. test("requests hydration when promotion admission was missed", () => {
  136. const result = createV2SessionReducer().reduce(
  137. [],
  138. event({
  139. ...base,
  140. id: "evt_promoted",
  141. type: "session.input.promoted",
  142. data: { sessionID: "ses_1", inputID: "msg_user" },
  143. }),
  144. )
  145. expect(result).toMatchObject({ sessionID: "ses_1", missing: "msg_user", touched: [] })
  146. })
  147. })