1
0

session-move.test.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. import { describe, expect } from "bun:test"
  2. import path from "path"
  3. import { mkdir, rm } from "fs/promises"
  4. import { Effect } from "effect"
  5. import { Worktree } from "@opencode-ai/schema/worktree"
  6. import { Bus } from "@opencode-ai/core/bus"
  7. import { Database } from "@opencode-ai/core/database/database"
  8. import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
  9. import { Location } from "@opencode-ai/core/location"
  10. import { Project } from "@opencode-ai/core/project"
  11. import { AbsolutePath } from "@opencode-ai/core/schema"
  12. import { Session } from "@opencode-ai/core/session"
  13. import { SessionEvent } from "@opencode-ai/core/session/event"
  14. import { SessionExecution } from "@opencode-ai/core/session/execution"
  15. import { SessionProjector } from "@opencode-ai/core/session/projector"
  16. import { SessionStore } from "@opencode-ai/core/session/store"
  17. import { LayerNode } from "@opencode-ai/util/effect/layer-node"
  18. import { tmpdir } from "./fixture/tmpdir"
  19. import { testEffect } from "./lib/effect"
  20. import { globalProjectLayer } from "./lib/project"
  21. const it = testEffect(
  22. AppNodeBuilder.build(
  23. LayerNode.group([Database.node, Bus.node, SessionProjector.node, SessionStore.node, Session.node]),
  24. [
  25. [Project.node, globalProjectLayer],
  26. [SessionExecution.node, SessionExecution.noopLayer],
  27. ],
  28. ),
  29. )
  30. describe("Session.move", () => {
  31. it.effect("applies a move immediately when the source directory no longer exists", () =>
  32. Effect.acquireRelease(
  33. Effect.promise(() => tmpdir()),
  34. (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
  35. ).pipe(
  36. Effect.flatMap((tmp) =>
  37. Effect.gen(function* () {
  38. const session = yield* Session.Service
  39. const destination = AbsolutePath.make(tmp.path)
  40. const source = path.join(tmp.path, "source")
  41. yield* Effect.promise(() => mkdir(source))
  42. const created = yield* session.create({
  43. location: Location.Ref.make({ directory: AbsolutePath.make(source) }),
  44. })
  45. yield* session.move({ sessionID: created.id, directory: destination })
  46. expect((yield* session.get(created.id)).location.directory).toBe(AbsolutePath.make(source))
  47. expect(yield* session.inbox(created.id)).toHaveLength(1)
  48. yield* Effect.promise(() => rm(source, { recursive: true }))
  49. yield* session.move({ sessionID: created.id, directory: destination })
  50. expect((yield* session.get(created.id)).location.directory).toBe(destination)
  51. expect(yield* session.inbox(created.id)).toEqual([])
  52. yield* session.move({ sessionID: created.id, directory: destination })
  53. expect(yield* session.inbox(created.id)).toHaveLength(1)
  54. yield* Effect.promise(() => mkdir(path.join(tmp.path, "other")))
  55. const steered = yield* session.create({
  56. location: Location.Ref.make({ directory: AbsolutePath.make(path.join(tmp.path, "other")) }),
  57. })
  58. yield* session.move({ sessionID: steered.id, directory: destination, delivery: "queue" })
  59. expect(yield* session.inbox(steered.id)).toMatchObject([{ type: "move", delivery: "queue" }])
  60. }),
  61. ),
  62. ),
  63. )
  64. it.effect("keeps a moved session out of its former directory's new identity", () =>
  65. Effect.acquireRelease(
  66. Effect.promise(() => tmpdir()),
  67. (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
  68. ).pipe(
  69. Effect.flatMap((tmp) =>
  70. Effect.gen(function* () {
  71. const session = yield* Session.Service
  72. const bus = yield* Bus.Service
  73. const previous = AbsolutePath.make(path.join(tmp.path, "previous"))
  74. const destination = AbsolutePath.make(tmp.path)
  75. const created = yield* session.create({ location: Location.Ref.make({ directory: previous }) })
  76. // Moves are admitted through the inbox and applied by the drain;
  77. // publish the applied move directly since execution is a no-op here.
  78. yield* bus.publish(SessionEvent.Moved, {
  79. sessionID: created.id,
  80. location: Location.Ref.make({ directory: destination }),
  81. projectID: Project.ID.global,
  82. })
  83. // The former directory becomes a project after the session left it.
  84. yield* bus.publish(Worktree.Event.Resolved, {
  85. projectID: Project.ID.make("adopting"),
  86. directory: previous,
  87. previous: Project.ID.global,
  88. })
  89. expect(yield* session.get(created.id)).toMatchObject({
  90. projectID: Project.ID.global,
  91. location: { directory: destination },
  92. subpath: undefined,
  93. })
  94. }),
  95. ),
  96. ),
  97. )
  98. })