session-todo.test.ts 3.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. import { describe, expect } from "bun:test"
  2. import { asc } from "drizzle-orm"
  3. import { Effect, Layer } from "effect"
  4. import { Database } from "@opencode-ai/core/database/database"
  5. import { EventV2 } from "@opencode-ai/core/event"
  6. import { Project } from "@opencode-ai/core/project"
  7. import { ProjectTable } from "@opencode-ai/core/project/sql"
  8. import { AbsolutePath } from "@opencode-ai/core/schema"
  9. import { SessionV2 } from "@opencode-ai/core/session"
  10. import { SessionTable, TodoTable } from "@opencode-ai/core/session/sql"
  11. import { SessionTodo } from "@opencode-ai/core/session/todo"
  12. import { testEffect } from "./lib/effect"
  13. const it = testEffect(Layer.mergeAll(Database.defaultLayer, EventV2.defaultLayer, SessionTodo.defaultLayer))
  14. const sessionID = SessionV2.ID.make("ses_todo_test")
  15. const setup = Effect.gen(function* () {
  16. const { db } = yield* Database.Service
  17. yield* db
  18. .insert(ProjectTable)
  19. .values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] })
  20. .run()
  21. .pipe(Effect.orDie)
  22. yield* db
  23. .insert(SessionTable)
  24. .values({
  25. id: sessionID,
  26. project_id: Project.ID.global,
  27. slug: "todo",
  28. directory: "/project",
  29. title: "todo",
  30. version: "test",
  31. })
  32. .run()
  33. .pipe(Effect.orDie)
  34. })
  35. describe("SessionTodo", () => {
  36. it.effect("replaces persisted todos in order and publishes updates", () =>
  37. Effect.gen(function* () {
  38. yield* setup
  39. const { db } = yield* Database.Service
  40. const events = yield* EventV2.Service
  41. const todos = yield* SessionTodo.Service
  42. const published = new Array<EventV2.Payload>()
  43. const unsubscribe = yield* events.listen((event) =>
  44. Effect.sync(() => {
  45. if (event.type === SessionTodo.Event.Updated.type) published.push(event)
  46. }),
  47. )
  48. yield* Effect.addFinalizer(() => unsubscribe)
  49. yield* todos.update({
  50. sessionID,
  51. todos: [
  52. { content: "second", status: "pending", priority: "low" },
  53. { content: "first", status: "in_progress", priority: "high" },
  54. ],
  55. })
  56. expect(yield* todos.get(sessionID)).toEqual([
  57. { content: "second", status: "pending", priority: "low" },
  58. { content: "first", status: "in_progress", priority: "high" },
  59. ])
  60. expect(
  61. (yield* db.select().from(TodoTable).orderBy(asc(TodoTable.position)).all().pipe(Effect.orDie)).map((row) => ({
  62. content: row.content,
  63. position: row.position,
  64. })),
  65. ).toEqual([
  66. { content: "second", position: 0 },
  67. { content: "first", position: 1 },
  68. ])
  69. yield* todos.update({ sessionID, todos: [{ content: "replacement", status: "completed", priority: "medium" }] })
  70. expect(yield* todos.get(sessionID)).toEqual([{ content: "replacement", status: "completed", priority: "medium" }])
  71. yield* todos.update({ sessionID, todos: [] })
  72. expect(yield* todos.get(sessionID)).toEqual([])
  73. expect(published.map((event) => event.data)).toEqual([
  74. {
  75. sessionID,
  76. todos: [
  77. { content: "second", status: "pending", priority: "low" },
  78. { content: "first", status: "in_progress", priority: "high" },
  79. ],
  80. },
  81. { sessionID, todos: [{ content: "replacement", status: "completed", priority: "medium" }] },
  82. { sessionID, todos: [] },
  83. ])
  84. }),
  85. )
  86. })