background-job.test.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. import { describe, expect } from "bun:test"
  2. import { BackgroundJob } from "@opencode-ai/core/background-job"
  3. import { LayerNode } from "@opencode-ai/core/effect/layer-node"
  4. import { Deferred, Effect, Exit, Scope } from "effect"
  5. import { it } from "./lib/effect"
  6. const jobsLayer = LayerNode.compile(BackgroundJob.node)
  7. describe("BackgroundJob", () => {
  8. it.live("tracks process-local work through explicit observation", () =>
  9. Effect.gen(function* () {
  10. const jobs = yield* BackgroundJob.Service
  11. const latch = yield* Deferred.make<void>()
  12. const job = yield* jobs.start({
  13. type: "test",
  14. metadata: { durable: false },
  15. run: Deferred.await(latch).pipe(Effect.as("done")),
  16. })
  17. expect(job).toMatchObject({ type: "test", status: "running", metadata: { durable: false } })
  18. expect(yield* jobs.wait({ id: job.id, timeout: 0 })).toMatchObject({
  19. timedOut: true,
  20. info: { status: "running" },
  21. })
  22. yield* Deferred.succeed(latch, undefined)
  23. expect(yield* jobs.wait({ id: job.id })).toMatchObject({
  24. timedOut: false,
  25. info: { status: "completed", output: "done" },
  26. })
  27. }).pipe(Effect.provide(jobsLayer)),
  28. )
  29. it.live("publishes jobs before starting immediately settling work", () =>
  30. Effect.gen(function* () {
  31. const jobs = yield* BackgroundJob.Service
  32. yield* Effect.forEach(Array.from({ length: 100 }), (_, index) => {
  33. const id = `job_immediate_start_${index}`
  34. return Effect.gen(function* () {
  35. const job = yield* jobs.start({
  36. id,
  37. type: "test",
  38. run: jobs
  39. .get(id)
  40. .pipe(
  41. Effect.flatMap((info) =>
  42. info?.status === "running"
  43. ? Effect.succeed(`done-${index}`)
  44. : Effect.fail("job started before publish"),
  45. ),
  46. ),
  47. })
  48. expect(yield* jobs.wait({ id: job.id })).toMatchObject({
  49. timedOut: false,
  50. info: { status: "completed", output: `done-${index}` },
  51. })
  52. })
  53. })
  54. }).pipe(Effect.provide(jobsLayer)),
  55. )
  56. it.live("increments pending work before starting immediately settling extensions", () =>
  57. Effect.gen(function* () {
  58. const jobs = yield* BackgroundJob.Service
  59. yield* Effect.forEach(Array.from({ length: 100 }), (_, index) =>
  60. Effect.gen(function* () {
  61. const first = yield* Deferred.make<void>()
  62. const job = yield* jobs.start({
  63. type: "test",
  64. run: Deferred.await(first).pipe(Effect.as(`first-${index}`)),
  65. })
  66. expect(yield* jobs.extend({ id: job.id, run: Effect.succeed(`second-${index}`) })).toBe(true)
  67. expect((yield* jobs.get(job.id))?.status).toBe("running")
  68. yield* Deferred.succeed(first, undefined)
  69. expect(yield* jobs.wait({ id: job.id })).toMatchObject({
  70. timedOut: false,
  71. info: { status: "completed", output: `second-${index}` },
  72. })
  73. }),
  74. )
  75. }).pipe(Effect.provide(jobsLayer)),
  76. )
  77. it.live("interrupts live work without promising settlement after the owning process-local scope closes", () =>
  78. Effect.gen(function* () {
  79. const scope = yield* Scope.make()
  80. const interrupted = yield* Deferred.make<void>()
  81. const jobs = yield* BackgroundJob.make.pipe(Scope.provide(scope))
  82. const job = yield* jobs.start({
  83. type: "test",
  84. run: Effect.never.pipe(Effect.ensuring(Deferred.succeed(interrupted, undefined))),
  85. })
  86. yield* Scope.close(scope, Exit.void)
  87. yield* Deferred.await(interrupted).pipe(Effect.timeout("1 second"))
  88. // The abandoned in-memory registry is not a durable observation channel.
  89. expect((yield* jobs.get(job.id))?.status).toBe("running")
  90. }),
  91. )
  92. })