background-job.test.ts 3.7 KB

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