session.test.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. import { describe, expect, test } from "bun:test"
  2. import path from "path"
  3. import { Session as SessionNs } from "../../src/session"
  4. import { Bus } from "../../src/bus"
  5. import { Log } from "../../src/util/log"
  6. import { Instance } from "../../src/project/instance"
  7. import { MessageV2 } from "../../src/session/message-v2"
  8. import { MessageID, PartID, type SessionID } from "../../src/session/schema"
  9. import { AppRuntime } from "../../src/effect/app-runtime"
  10. import { tmpdir } from "../fixture/fixture"
  11. const projectRoot = path.join(__dirname, "../..")
  12. Log.init({ print: false })
  13. function create(input?: SessionNs.CreateInput) {
  14. return AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create(input)))
  15. }
  16. function get(id: SessionID) {
  17. return AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.get(id)))
  18. }
  19. function remove(id: SessionID) {
  20. return AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.remove(id)))
  21. }
  22. function updateMessage<T extends MessageV2.Info>(msg: T) {
  23. return AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.updateMessage(msg)))
  24. }
  25. function updatePart<T extends MessageV2.Part>(part: T) {
  26. return AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.updatePart(part)))
  27. }
  28. describe("session.created event", () => {
  29. test("should emit session.created event when session is created", async () => {
  30. await Instance.provide({
  31. directory: projectRoot,
  32. fn: async () => {
  33. let eventReceived = false
  34. let receivedInfo: SessionNs.Info | undefined
  35. const unsub = Bus.subscribe(SessionNs.Event.Created, (event) => {
  36. eventReceived = true
  37. receivedInfo = event.properties.info as SessionNs.Info
  38. })
  39. const info = await create({})
  40. await new Promise((resolve) => setTimeout(resolve, 100))
  41. unsub()
  42. expect(eventReceived).toBe(true)
  43. expect(receivedInfo).toBeDefined()
  44. expect(receivedInfo?.id).toBe(info.id)
  45. expect(receivedInfo?.projectID).toBe(info.projectID)
  46. expect(receivedInfo?.directory).toBe(info.directory)
  47. expect(receivedInfo?.title).toBe(info.title)
  48. await remove(info.id)
  49. },
  50. })
  51. })
  52. test("session.created event should be emitted before session.updated", async () => {
  53. await Instance.provide({
  54. directory: projectRoot,
  55. fn: async () => {
  56. const events: string[] = []
  57. const unsubCreated = Bus.subscribe(SessionNs.Event.Created, () => {
  58. events.push("created")
  59. })
  60. const unsubUpdated = Bus.subscribe(SessionNs.Event.Updated, () => {
  61. events.push("updated")
  62. })
  63. const info = await create({})
  64. await new Promise((resolve) => setTimeout(resolve, 100))
  65. unsubCreated()
  66. unsubUpdated()
  67. expect(events).toContain("created")
  68. expect(events).toContain("updated")
  69. expect(events.indexOf("created")).toBeLessThan(events.indexOf("updated"))
  70. await remove(info.id)
  71. },
  72. })
  73. })
  74. })
  75. describe("step-finish token propagation via Bus event", () => {
  76. test(
  77. "non-zero tokens propagate through PartUpdated event",
  78. async () => {
  79. await Instance.provide({
  80. directory: projectRoot,
  81. fn: async () => {
  82. const info = await create({})
  83. const messageID = MessageID.ascending()
  84. await updateMessage({
  85. id: messageID,
  86. sessionID: info.id,
  87. role: "user",
  88. time: { created: Date.now() },
  89. agent: "user",
  90. model: { providerID: "test", modelID: "test" },
  91. tools: {},
  92. mode: "",
  93. } as unknown as MessageV2.Info)
  94. let received: MessageV2.Part | undefined
  95. const unsub = Bus.subscribe(MessageV2.Event.PartUpdated, (event) => {
  96. received = event.properties.part
  97. })
  98. const tokens = {
  99. total: 1500,
  100. input: 500,
  101. output: 800,
  102. reasoning: 200,
  103. cache: { read: 100, write: 50 },
  104. }
  105. const partInput = {
  106. id: PartID.ascending(),
  107. messageID,
  108. sessionID: info.id,
  109. type: "step-finish" as const,
  110. reason: "stop",
  111. cost: 0.005,
  112. tokens,
  113. }
  114. await updatePart(partInput)
  115. await new Promise((resolve) => setTimeout(resolve, 100))
  116. expect(received).toBeDefined()
  117. expect(received!.type).toBe("step-finish")
  118. const finish = received as MessageV2.StepFinishPart
  119. expect(finish.tokens.input).toBe(500)
  120. expect(finish.tokens.output).toBe(800)
  121. expect(finish.tokens.reasoning).toBe(200)
  122. expect(finish.tokens.total).toBe(1500)
  123. expect(finish.tokens.cache.read).toBe(100)
  124. expect(finish.tokens.cache.write).toBe(50)
  125. expect(finish.cost).toBe(0.005)
  126. expect(received).not.toBe(partInput)
  127. unsub()
  128. await remove(info.id)
  129. },
  130. })
  131. },
  132. { timeout: 30000 },
  133. )
  134. })
  135. describe("Session", () => {
  136. test("remove works without an instance", async () => {
  137. await using tmp = await tmpdir({ git: true })
  138. const info = await Instance.provide({
  139. directory: tmp.path,
  140. fn: () => create({ title: "remove-without-instance" }),
  141. })
  142. await expect(async () => {
  143. await remove(info.id)
  144. }).not.toThrow()
  145. let missing = false
  146. await get(info.id).catch(() => {
  147. missing = true
  148. })
  149. expect(missing).toBe(true)
  150. })
  151. })