session-view.test.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. import { describe, expect } from "bun:test"
  2. import { Bus } from "@opencode-ai/core/bus"
  3. import { Database } from "@opencode-ai/core/database/database"
  4. import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
  5. import { EventTable } from "@opencode-ai/core/event/sql"
  6. import { Location } from "@opencode-ai/core/location"
  7. import { Project } from "@opencode-ai/core/project"
  8. import { AbsolutePath } from "@opencode-ai/core/schema"
  9. import { Session } from "@opencode-ai/core/session"
  10. import { SessionEvent } from "@opencode-ai/core/session/event"
  11. import { SessionExecution } from "@opencode-ai/core/session/execution"
  12. import { SessionProjector } from "@opencode-ai/core/session/projector"
  13. import { SessionTable } from "@opencode-ai/core/session/sql"
  14. import { SessionStore } from "@opencode-ai/core/session/store"
  15. import { LayerNode } from "@opencode-ai/util/effect/layer-node"
  16. import { DateTime, Effect } from "effect"
  17. import { eq } from "drizzle-orm"
  18. import { testEffect } from "./lib/effect"
  19. import { globalProjectLayer } from "./lib/project"
  20. const it = testEffect(
  21. AppNodeBuilder.build(
  22. LayerNode.group([Database.node, Bus.node, SessionProjector.node, SessionStore.node, Session.node]),
  23. [
  24. [Bus.node, Bus.configured({ persist: true })],
  25. [Project.node, globalProjectLayer],
  26. [SessionExecution.node, SessionExecution.noopLayer],
  27. ],
  28. ),
  29. )
  30. const location = Location.Ref.make({ directory: AbsolutePath.make("/project") })
  31. describe("Session.view", () => {
  32. it.effect("copies the latest idle time without changing session recency", () =>
  33. Effect.gen(function* () {
  34. const session = yield* Session.Service
  35. const bus = yield* Bus.Service
  36. const { db } = yield* Database.Service
  37. const created = yield* session.create({ location })
  38. expect(created.time.idle).toBeUndefined()
  39. expect(created.time.viewed).toBeUndefined()
  40. yield* session.view({ sessionID: created.id })
  41. expect((yield* session.get(created.id)).time.viewed).toBeUndefined()
  42. yield* bus.publish(SessionEvent.Execution.Succeeded, { sessionID: created.id })
  43. const idle = yield* session.get(created.id)
  44. expect(idle.time.idle).toBeDefined()
  45. expect(idle.time.viewed).toBeUndefined()
  46. expect(idle.time.updated).toEqual(created.time.updated)
  47. yield* session.view({ sessionID: created.id })
  48. const viewed = yield* session.get(created.id)
  49. if (!viewed.time.idle || !viewed.time.viewed) return yield* Effect.die(new Error("Expected attention times"))
  50. expect(viewed.time.viewed).toEqual(viewed.time.idle)
  51. expect(viewed.time.updated).toEqual(created.time.updated)
  52. expect(
  53. yield* db
  54. .select({ idle: SessionTable.time_idle, viewed: SessionTable.time_viewed })
  55. .from(SessionTable)
  56. .where(eq(SessionTable.id, created.id))
  57. .get(),
  58. ).toEqual({
  59. idle: DateTime.toEpochMillis(viewed.time.idle),
  60. viewed: DateTime.toEpochMillis(viewed.time.viewed),
  61. })
  62. expect((yield* session.list()).data.find((item) => item.id === created.id)?.time).toEqual(viewed.time)
  63. yield* session.view({ sessionID: created.id })
  64. expect((yield* session.get(created.id)).time).toEqual(viewed.time)
  65. yield* bus.publish(SessionEvent.Execution.Failed, {
  66. sessionID: created.id,
  67. error: { type: "unknown", message: "failed" },
  68. })
  69. const unread = yield* session.get(created.id)
  70. if (!unread.time.idle || !unread.time.viewed) return yield* Effect.die(new Error("Expected attention times"))
  71. expect(DateTime.toEpochMillis(unread.time.idle)).toBeGreaterThan(DateTime.toEpochMillis(unread.time.viewed))
  72. yield* session.view({ sessionID: created.id })
  73. expect((yield* session.get(created.id)).time.viewed).toEqual(unread.time.idle)
  74. yield* bus.publish(SessionEvent.Execution.Interrupted, { sessionID: created.id, reason: "shutdown" })
  75. expect((yield* session.get(created.id)).time.idle).toEqual(unread.time.idle)
  76. yield* bus.publish(SessionEvent.Execution.Interrupted, { sessionID: created.id, reason: "user" })
  77. const interrupted = yield* session.get(created.id)
  78. if (!interrupted.time.idle || !interrupted.time.viewed)
  79. return yield* Effect.die(new Error("Expected attention times"))
  80. expect(DateTime.toEpochMillis(interrupted.time.idle)).toBeGreaterThan(
  81. DateTime.toEpochMillis(interrupted.time.viewed),
  82. )
  83. expect(
  84. (yield* db
  85. .select({ type: EventTable.type })
  86. .from(EventTable)
  87. .where(eq(EventTable.aggregate_id, created.id))
  88. .all()).filter((event) => event.type === "session.viewed.1"),
  89. ).toHaveLength(2)
  90. }),
  91. )
  92. it.effect("rejects an unknown session", () =>
  93. Effect.gen(function* () {
  94. const session = yield* Session.Service
  95. const sessionID = Session.ID.make("ses_missing_view")
  96. expect(yield* Effect.flip(session.view({ sessionID }))).toEqual(new Session.NotFoundError({ sessionID }))
  97. }),
  98. )
  99. })