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