refactor(core): replace background job service (#34559)
This commit is contained in:
@@ -172,7 +172,7 @@ Wait on a **published readiness signal**, not wall-clock time. Available afforda
|
||||
- `awaitWithTimeout(effect, message, duration?)` from `test/lib/effect.ts` — wrap any effect with `Effect.timeoutOrElse` and a custom error message.
|
||||
- `llm.wait(n)` from `test/lib/llm-server.ts` — wait until the mock LLM has received `n` HTTP calls.
|
||||
- `SessionStatus.Service` `.get(sessionID)` — observable per-session state (`{ type: "busy" | "idle" | ... }`).
|
||||
- `BackgroundJob.wait({ id, timeout })` from `src/background/job.ts` — wait for a background job to complete.
|
||||
- `Job.wait({ id, timeout })` from `src/job.ts` — wait for a job to complete.
|
||||
- Bus subscriptions — fork `Stream.runForEach(bus.subscribe(Event), ...)` and open a `Latch` inside the callback to signal first-event readiness.
|
||||
- `Deferred.await(deferred).pipe(Effect.timeoutOrElse(...))` for one-shot signals.
|
||||
|
||||
|
||||
@@ -1,243 +0,0 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Deferred, Effect } from "effect"
|
||||
import { BackgroundJob } from "@/background/job"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const it = testEffect(BackgroundJob.defaultLayer)
|
||||
|
||||
describe("background.job", () => {
|
||||
it.instance("tracks started jobs through completion", () =>
|
||||
Effect.gen(function* () {
|
||||
const jobs = yield* BackgroundJob.Service
|
||||
const latch = yield* Deferred.make<void>()
|
||||
const job = yield* jobs.start({
|
||||
type: "test",
|
||||
title: "test job",
|
||||
run: Deferred.await(latch).pipe(Effect.as("done")),
|
||||
})
|
||||
|
||||
expect(job.id.startsWith("job_")).toBe(true)
|
||||
expect(job.status).toBe("running")
|
||||
expect(job.title).toBe("test job")
|
||||
|
||||
yield* Deferred.succeed(latch, undefined)
|
||||
const done = yield* jobs.wait({ id: job.id })
|
||||
|
||||
expect(done.timedOut).toBe(false)
|
||||
expect(done.info?.status).toBe("completed")
|
||||
expect(done.info?.output).toBe("done")
|
||||
expect((yield* jobs.list()).map((item) => item.id)).toEqual([job.id])
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("returns a running snapshot when wait times out", () =>
|
||||
Effect.gen(function* () {
|
||||
const jobs = yield* BackgroundJob.Service
|
||||
const job = yield* jobs.start({
|
||||
type: "test",
|
||||
run: Effect.never,
|
||||
})
|
||||
|
||||
const result = yield* jobs.wait({ id: job.id, timeout: 1 })
|
||||
|
||||
expect(result.timedOut).toBe(true)
|
||||
expect(result.info?.status).toBe("running")
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("deduplicates concurrent starts for a running id", () =>
|
||||
Effect.gen(function* () {
|
||||
const jobs = yield* BackgroundJob.Service
|
||||
const started = yield* Deferred.make<void>()
|
||||
const id = "job_test"
|
||||
const [first, second] = yield* Effect.all(
|
||||
[
|
||||
jobs.start({
|
||||
id,
|
||||
type: "test",
|
||||
run: Deferred.succeed(started, undefined).pipe(Effect.andThen(Effect.never)),
|
||||
}),
|
||||
jobs.start({
|
||||
id,
|
||||
type: "test",
|
||||
run: Effect.fail(new Error("duplicate started")),
|
||||
}),
|
||||
],
|
||||
{ concurrency: "unbounded" },
|
||||
)
|
||||
|
||||
yield* Deferred.await(started)
|
||||
|
||||
expect(first.id).toBe(id)
|
||||
expect(second.id).toBe(id)
|
||||
expect(first.status).toBe("running")
|
||||
expect(second.status).toBe("running")
|
||||
expect((yield* jobs.list()).map((item) => item.id)).toEqual([id])
|
||||
|
||||
yield* jobs.cancel(id)
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("waits for extensions before completing a running job", () =>
|
||||
Effect.gen(function* () {
|
||||
const jobs = yield* BackgroundJob.Service
|
||||
const first = yield* Deferred.make<void>()
|
||||
const second = yield* Deferred.make<void>()
|
||||
const job = yield* jobs.start({
|
||||
type: "test",
|
||||
run: Deferred.await(first).pipe(Effect.as("first")),
|
||||
})
|
||||
|
||||
expect(yield* jobs.extend({ id: job.id, run: Deferred.await(second).pipe(Effect.as("second")) })).toBe(true)
|
||||
yield* Deferred.succeed(first, undefined)
|
||||
expect((yield* jobs.get(job.id))?.status).toBe("running")
|
||||
|
||||
yield* Deferred.succeed(second, undefined)
|
||||
const done = yield* jobs.wait({ id: job.id })
|
||||
expect(done.info?.status).toBe("completed")
|
||||
expect(done.info?.output).toBe("second")
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("runs extensions after earlier work completes", () =>
|
||||
Effect.gen(function* () {
|
||||
const jobs = yield* BackgroundJob.Service
|
||||
const first = yield* Deferred.make<void>()
|
||||
const order: string[] = []
|
||||
const job = yield* jobs.start({
|
||||
type: "test",
|
||||
run: Effect.sync(() => order.push("start")).pipe(Effect.andThen(Deferred.await(first)), Effect.as("first")),
|
||||
})
|
||||
|
||||
expect(
|
||||
yield* jobs.extend({
|
||||
id: job.id,
|
||||
run: Effect.sync(() => order.push("extend")).pipe(Effect.as("second")),
|
||||
}),
|
||||
).toBe(true)
|
||||
yield* Effect.yieldNow
|
||||
expect(order).toEqual(["start"])
|
||||
|
||||
yield* Deferred.succeed(first, undefined)
|
||||
expect((yield* jobs.wait({ id: job.id })).info?.output).toBe("second")
|
||||
expect(order).toEqual(["start", "extend"])
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("rejects extensions after a job completes", () =>
|
||||
Effect.gen(function* () {
|
||||
const jobs = yield* BackgroundJob.Service
|
||||
const job = yield* jobs.start({ type: "test", run: Effect.succeed("done") })
|
||||
yield* jobs.wait({ id: job.id })
|
||||
|
||||
expect(yield* jobs.extend({ id: job.id, run: Effect.succeed("late") })).toBe(false)
|
||||
expect((yield* jobs.get(job.id))?.output).toBe("done")
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("records failed jobs", () =>
|
||||
Effect.gen(function* () {
|
||||
const jobs = yield* BackgroundJob.Service
|
||||
const job = yield* jobs.start({
|
||||
type: "test",
|
||||
run: Effect.fail(new Error("boom")),
|
||||
})
|
||||
|
||||
const result = yield* jobs.wait({ id: job.id })
|
||||
|
||||
expect(result.info?.status).toBe("error")
|
||||
expect(result.info?.error).toBe("boom")
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("ignores stale settlements after restarting a failed job", () =>
|
||||
Effect.gen(function* () {
|
||||
const jobs = yield* BackgroundJob.Service
|
||||
const fail = yield* Deferred.make<void>()
|
||||
const interrupted = yield* Deferred.make<void>()
|
||||
const release = yield* Deferred.make<void>()
|
||||
const id = "job_test"
|
||||
yield* jobs.start({
|
||||
id,
|
||||
type: "test",
|
||||
run: Deferred.await(fail).pipe(Effect.andThen(Effect.fail(new Error("boom")))),
|
||||
})
|
||||
yield* jobs.extend({
|
||||
id,
|
||||
run: Effect.never.pipe(
|
||||
Effect.ensuring(Deferred.succeed(interrupted, undefined).pipe(Effect.andThen(Deferred.await(release)))),
|
||||
),
|
||||
})
|
||||
|
||||
yield* Deferred.succeed(fail, undefined)
|
||||
expect((yield* jobs.wait({ id })).info?.status).toBe("error")
|
||||
yield* Deferred.await(interrupted)
|
||||
yield* jobs.start({ id, type: "test", run: Effect.never })
|
||||
|
||||
yield* Deferred.succeed(release, undefined)
|
||||
yield* Effect.yieldNow
|
||||
expect((yield* jobs.get(id))?.status).toBe("running")
|
||||
yield* jobs.cancel(id)
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("can cancel running jobs", () =>
|
||||
Effect.gen(function* () {
|
||||
const jobs = yield* BackgroundJob.Service
|
||||
const interrupted = yield* Deferred.make<void>()
|
||||
const job = yield* jobs.start({
|
||||
type: "test",
|
||||
run: Effect.never.pipe(Effect.ensuring(Deferred.succeed(interrupted, undefined))),
|
||||
})
|
||||
yield* jobs.extend({
|
||||
id: job.id,
|
||||
run: Effect.never,
|
||||
})
|
||||
|
||||
const cancelled = yield* jobs.cancel(job.id)
|
||||
|
||||
expect(cancelled?.status).toBe("cancelled")
|
||||
yield* Deferred.await(interrupted).pipe(Effect.timeout("1 second"))
|
||||
expect((yield* jobs.get(job.id))?.status).toBe("cancelled")
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("promotes running jobs without interrupting them", () =>
|
||||
Effect.gen(function* () {
|
||||
const jobs = yield* BackgroundJob.Service
|
||||
const latch = yield* Deferred.make<void>()
|
||||
const promoted = yield* Deferred.make<void>()
|
||||
const job = yield* jobs.start({
|
||||
type: "test",
|
||||
metadata: { parentSessionId: "parent" },
|
||||
onPromote: Deferred.succeed(promoted, undefined).pipe(Effect.asVoid),
|
||||
run: Deferred.await(latch).pipe(Effect.as("done")),
|
||||
})
|
||||
|
||||
const info = yield* jobs.promote(job.id)
|
||||
|
||||
expect(info?.status).toBe("running")
|
||||
expect(info?.metadata?.background).toBe(true)
|
||||
yield* Deferred.await(promoted)
|
||||
expect((yield* jobs.get(job.id))?.status).toBe("running")
|
||||
|
||||
yield* Deferred.succeed(latch, undefined)
|
||||
expect((yield* jobs.wait({ id: job.id })).info?.output).toBe("done")
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("returns immutable snapshots", () =>
|
||||
Effect.gen(function* () {
|
||||
const jobs = yield* BackgroundJob.Service
|
||||
const job = yield* jobs.start({
|
||||
type: "test",
|
||||
metadata: { value: "initial" },
|
||||
run: Effect.succeed("done"),
|
||||
})
|
||||
|
||||
if (job.metadata) job.metadata.value = "changed"
|
||||
|
||||
expect((yield* jobs.get(job.id))?.metadata?.value).toBe("initial")
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,131 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Deferred, Effect, Fiber } from "effect"
|
||||
import { Job } from "@/job"
|
||||
import { SessionSchema } from "@opencode-ai/core/session/schema"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
const it = testEffect(Job.defaultLayer)
|
||||
|
||||
describe("job", () => {
|
||||
it.instance("tracks started jobs through completion", () =>
|
||||
Effect.gen(function* () {
|
||||
const jobs = yield* Job.Service
|
||||
const latch = yield* Deferred.make<void>()
|
||||
const job = yield* jobs.start({
|
||||
type: "test",
|
||||
title: "test job",
|
||||
run: Deferred.await(latch).pipe(Effect.as("done")),
|
||||
})
|
||||
|
||||
expect(job.id.startsWith("job_")).toBe(true)
|
||||
expect(job.status).toBe("running")
|
||||
expect(job.title).toBe("test job")
|
||||
|
||||
yield* Deferred.succeed(latch, undefined)
|
||||
const done = yield* jobs.wait({ id: job.id })
|
||||
|
||||
expect(done.timedOut).toBe(false)
|
||||
expect(done.info?.status).toBe("completed")
|
||||
expect(done.info?.output).toBe("done")
|
||||
expect((yield* jobs.list()).map((item) => item.id)).toEqual([job.id])
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("returns a running snapshot when wait times out", () =>
|
||||
Effect.gen(function* () {
|
||||
const jobs = yield* Job.Service
|
||||
const job = yield* jobs.start({ type: "test", run: Effect.never })
|
||||
|
||||
const result = yield* jobs.wait({ id: job.id, timeout: 1 })
|
||||
|
||||
expect(result.timedOut).toBe(true)
|
||||
expect(result.info?.status).toBe("running")
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("deduplicates concurrent starts for a running id", () =>
|
||||
Effect.gen(function* () {
|
||||
const jobs = yield* Job.Service
|
||||
const started = yield* Deferred.make<void>()
|
||||
const id = "job_test"
|
||||
const [first, second] = yield* Effect.all(
|
||||
[
|
||||
jobs.start({
|
||||
id,
|
||||
type: "test",
|
||||
run: Deferred.succeed(started, undefined).pipe(Effect.andThen(Effect.never)),
|
||||
}),
|
||||
jobs.start({ id, type: "test", run: Effect.fail(new Error("duplicate started")) }),
|
||||
],
|
||||
{ concurrency: "unbounded" },
|
||||
)
|
||||
|
||||
yield* Deferred.await(started)
|
||||
|
||||
expect(first.id).toBe(id)
|
||||
expect(second.id).toBe(id)
|
||||
expect(first.status).toBe("running")
|
||||
expect(second.status).toBe("running")
|
||||
expect((yield* jobs.list()).map((item) => item.id)).toEqual([id])
|
||||
|
||||
yield* jobs.cancel(id)
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("records failed jobs", () =>
|
||||
Effect.gen(function* () {
|
||||
const jobs = yield* Job.Service
|
||||
const job = yield* jobs.start({ type: "test", run: Effect.fail(new Error("boom")) })
|
||||
|
||||
const result = yield* jobs.wait({ id: job.id })
|
||||
|
||||
expect(result.info?.status).toBe("error")
|
||||
expect(result.info?.error).toBe("boom")
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("can cancel running jobs", () =>
|
||||
Effect.gen(function* () {
|
||||
const jobs = yield* Job.Service
|
||||
const interrupted = yield* Deferred.make<void>()
|
||||
const job = yield* jobs.start({
|
||||
type: "test",
|
||||
run: Effect.never.pipe(Effect.ensuring(Deferred.succeed(interrupted, undefined))),
|
||||
})
|
||||
|
||||
const cancelled = yield* jobs.cancel(job.id)
|
||||
|
||||
expect(cancelled?.status).toBe("cancelled")
|
||||
yield* Deferred.await(interrupted).pipe(Effect.timeout("1 second"))
|
||||
expect((yield* jobs.get(job.id))?.status).toBe("cancelled")
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("releases blocking waits when backgrounded", () =>
|
||||
Effect.gen(function* () {
|
||||
const jobs = yield* Job.Service
|
||||
const latch = yield* Deferred.make<void>()
|
||||
const job = yield* jobs.start({ type: "test", run: Deferred.await(latch).pipe(Effect.as("done")) })
|
||||
const waiting = yield* jobs
|
||||
.block({ id: job.id, sessionID: SessionSchema.ID.make("ses_parent") })
|
||||
.pipe(Effect.forkChild)
|
||||
|
||||
expect(yield* jobs.background(job.id)).toMatchObject({ id: job.id, status: "running" })
|
||||
expect(yield* Fiber.join(waiting)).toMatchObject({ type: "backgrounded", info: { id: job.id } })
|
||||
|
||||
yield* Deferred.succeed(latch, undefined)
|
||||
expect((yield* jobs.wait({ id: job.id })).info?.output).toBe("done")
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("returns immutable snapshots", () =>
|
||||
Effect.gen(function* () {
|
||||
const jobs = yield* Job.Service
|
||||
const job = yield* jobs.start({ type: "test", metadata: { value: "initial" }, run: Effect.succeed("done") })
|
||||
|
||||
if (job.metadata) job.metadata.value = "changed"
|
||||
|
||||
expect((yield* jobs.get(job.id))?.metadata?.value).toBe("initial")
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -12,7 +12,7 @@ import { testEffect } from "../lib/effect"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { Storage } from "@/storage/storage"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { BackgroundJob } from "@/background/job"
|
||||
import { Job } from "@/job"
|
||||
|
||||
const layer = (experimentalWorkspaces: boolean) =>
|
||||
Layer.mergeAll(
|
||||
@@ -24,7 +24,7 @@ const layer = (experimentalWorkspaces: boolean) =>
|
||||
Layer.provide(EventV2Bridge.defaultLayer),
|
||||
Layer.provide(SessionProjector.defaultLayer),
|
||||
Layer.provide(RuntimeFlags.layer({ experimentalWorkspaces })),
|
||||
Layer.provide(BackgroundJob.defaultLayer),
|
||||
Layer.provide(Job.defaultLayer),
|
||||
),
|
||||
)
|
||||
const it = testEffect(layer(false))
|
||||
|
||||
@@ -11,7 +11,7 @@ import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import { NamedError } from "@opencode-ai/core/util/error"
|
||||
import { Agent as AgentSvc } from "../../src/agent/agent"
|
||||
import { BackgroundJob } from "@/background/job"
|
||||
import { Job } from "@/job"
|
||||
import { Command } from "../../src/command"
|
||||
import { Config } from "@/config/config"
|
||||
import { LSP } from "@/lsp/lsp"
|
||||
@@ -183,7 +183,7 @@ function makePrompt(input?: { mcpInstructions?: MCP.ServerInstructions[]; proces
|
||||
lsp,
|
||||
makeMcp(input?.mcpInstructions),
|
||||
FSUtil.defaultLayer,
|
||||
BackgroundJob.defaultLayer,
|
||||
Job.defaultLayer,
|
||||
status,
|
||||
Database.defaultLayer,
|
||||
EventV2Bridge.defaultLayer,
|
||||
|
||||
@@ -12,7 +12,7 @@ import { provideInstance, testInstanceStoreLayer, tmpdirScoped } from "../fixtur
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { Storage } from "@/storage/storage"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { BackgroundJob } from "@/background/job"
|
||||
import { Job } from "@/job"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
|
||||
@@ -24,7 +24,7 @@ const it = testEffect(
|
||||
Layer.provideMerge(EventV2Bridge.defaultLayer),
|
||||
Layer.provide(SessionProjector.defaultLayer),
|
||||
Layer.provide(RuntimeFlags.layer({ experimentalWorkspaces: false })),
|
||||
Layer.provide(BackgroundJob.defaultLayer),
|
||||
Layer.provide(Job.defaultLayer),
|
||||
),
|
||||
CrossSpawnSpawner.defaultLayer,
|
||||
testInstanceStoreLayer,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { SessionV1 } from "@opencode-ai/core/v1/session"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { Deferred, Effect, Exit, Fiber, Layer } from "effect"
|
||||
import { Agent } from "../../src/agent/agent"
|
||||
import { BackgroundJob } from "@/background/job"
|
||||
import { Job } from "@/job"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { Config } from "@/config/config"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
@@ -19,7 +19,7 @@ import { Truncate } from "@/tool/truncate"
|
||||
import { ToolRegistry } from "@/tool/registry"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { disposeAllInstances } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { pollWithTimeout, testEffect } from "../lib/effect"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
|
||||
@@ -35,7 +35,7 @@ const ref = {
|
||||
const layer = (flags: Partial<RuntimeFlags.Info> = {}) =>
|
||||
Layer.mergeAll(
|
||||
Agent.defaultLayer,
|
||||
BackgroundJob.defaultLayer,
|
||||
Job.defaultLayer,
|
||||
EventV2Bridge.defaultLayer,
|
||||
Config.defaultLayer,
|
||||
CrossSpawnSpawner.defaultLayer,
|
||||
@@ -480,9 +480,9 @@ describe("tool.task", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("promotes a running foreground task without restarting it", () =>
|
||||
it.instance("backgrounds a running foreground task without restarting it", () =>
|
||||
Effect.gen(function* () {
|
||||
const jobs = yield* BackgroundJob.Service
|
||||
const jobs = yield* Job.Service
|
||||
const { chat, assistant } = yield* seed()
|
||||
const tool = yield* TaskTool
|
||||
const def = yield* tool.init()
|
||||
@@ -531,7 +531,12 @@ describe("tool.task", () => {
|
||||
expect(job).toBeDefined()
|
||||
if (!job) throw new Error("task job not found")
|
||||
expect(job.metadata?.parentSessionId).toBe(chat.id)
|
||||
yield* jobs.promote(job.id)
|
||||
yield* pollWithTimeout(
|
||||
jobs
|
||||
.backgroundAll({ sessionID: chat.id, type: "task" })
|
||||
.pipe(Effect.map((backgrounded) => (backgrounded.length > 0 ? backgrounded : undefined))),
|
||||
"task never blocked the parent session",
|
||||
)
|
||||
|
||||
const result = yield* Fiber.join(fiber)
|
||||
expect(result.metadata.background).toBe(true)
|
||||
@@ -548,7 +553,7 @@ describe("tool.task", () => {
|
||||
|
||||
background.instance("execute launches background tasks without waiting for completion", () =>
|
||||
Effect.gen(function* () {
|
||||
const jobs = yield* BackgroundJob.Service
|
||||
const jobs = yield* Job.Service
|
||||
const { chat, assistant } = yield* seed()
|
||||
const tool = yield* TaskTool
|
||||
const def = yield* tool.init()
|
||||
@@ -584,15 +589,13 @@ describe("tool.task", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
background.instance("background task completion waits for running updates", () =>
|
||||
background.instance("running task_id reports the existing background task", () =>
|
||||
Effect.gen(function* () {
|
||||
const jobs = yield* BackgroundJob.Service
|
||||
const jobs = yield* Job.Service
|
||||
const { chat, assistant } = yield* seed()
|
||||
const tool = yield* TaskTool
|
||||
const def = yield* tool.init()
|
||||
const first = defer<void>()
|
||||
const second = defer<void>()
|
||||
const updated = defer<SessionPrompt.PromptInput>()
|
||||
const injected = defer<SessionPrompt.PromptInput>()
|
||||
let prompts = 0
|
||||
const promptOps: TaskPromptOps = {
|
||||
@@ -603,9 +606,7 @@ describe("tool.task", () => {
|
||||
return Effect.succeed(reply(input, "done"))
|
||||
}
|
||||
prompts++
|
||||
if (prompts === 1) return Effect.promise(() => first.promise).pipe(Effect.as(reply(input, "first done")))
|
||||
updated.resolve(input)
|
||||
return Effect.promise(() => second.promise).pipe(Effect.as(reply(input, "second done")))
|
||||
return Effect.promise(() => first.promise).pipe(Effect.as(reply(input, "first done")))
|
||||
},
|
||||
}
|
||||
const context = {
|
||||
@@ -640,27 +641,22 @@ describe("tool.task", () => {
|
||||
|
||||
expect(result.metadata.sessionId).toBe(started.metadata.sessionId)
|
||||
expect(result.metadata.background).toBe(true)
|
||||
expect(result.output).toContain("Background task updated")
|
||||
expect(result.output).toContain("Background task already running")
|
||||
expect(prompts).toBe(1)
|
||||
first.resolve()
|
||||
expect((yield* jobs.get(started.metadata.sessionId))?.status).toBe("running")
|
||||
expect((yield* Effect.promise(() => updated.promise)).parts).toEqual([
|
||||
{ type: "text", text: "also inspect cancellation" },
|
||||
])
|
||||
|
||||
second.resolve()
|
||||
const waited = yield* jobs.wait({ id: started.metadata.sessionId, timeout: 1_000 })
|
||||
expect(waited.info?.status).toBe("completed")
|
||||
expect(waited.info?.output).toBe("second done")
|
||||
expect(waited.info?.output).toBe("first done")
|
||||
const notification = yield* Effect.promise(() => injected.promise)
|
||||
expect(notification.variant).toBe("xhigh")
|
||||
expect(notification.parts[0]?.type).toBe("text")
|
||||
if (notification.parts[0]?.type === "text") expect(notification.parts[0].text).toContain("second done")
|
||||
if (notification.parts[0]?.type === "text") expect(notification.parts[0].text).toContain("first done")
|
||||
}),
|
||||
)
|
||||
|
||||
background.instance("background tasks complete through the background job service", () =>
|
||||
background.instance("background tasks complete through the job service", () =>
|
||||
Effect.gen(function* () {
|
||||
const jobs = yield* BackgroundJob.Service
|
||||
const jobs = yield* Job.Service
|
||||
const { chat, assistant } = yield* seed()
|
||||
const tool = yield* TaskTool
|
||||
const def = yield* tool.init()
|
||||
@@ -693,7 +689,7 @@ describe("tool.task", () => {
|
||||
|
||||
background.instance("background task completion does not wait for the parent async prompt", () =>
|
||||
Effect.gen(function* () {
|
||||
const jobs = yield* BackgroundJob.Service
|
||||
const jobs = yield* Job.Service
|
||||
const { chat, assistant } = yield* seed()
|
||||
const tool = yield* TaskTool
|
||||
const def = yield* tool.init()
|
||||
@@ -731,7 +727,7 @@ describe("tool.task", () => {
|
||||
|
||||
background.instance("removing the parent session cancels running background tasks", () =>
|
||||
Effect.gen(function* () {
|
||||
const jobs = yield* BackgroundJob.Service
|
||||
const jobs = yield* Job.Service
|
||||
const sessions = yield* Session.Service
|
||||
const { chat, assistant } = yield* seed()
|
||||
const tool = yield* TaskTool
|
||||
@@ -770,7 +766,7 @@ describe("tool.task", () => {
|
||||
|
||||
background.instance("removing the child task session cancels its running background task", () =>
|
||||
Effect.gen(function* () {
|
||||
const jobs = yield* BackgroundJob.Service
|
||||
const jobs = yield* Job.Service
|
||||
const sessions = yield* Session.Service
|
||||
const { chat, assistant } = yield* seed()
|
||||
const tool = yield* TaskTool
|
||||
@@ -809,7 +805,7 @@ describe("tool.task", () => {
|
||||
|
||||
background.instance("cancelling the parent run cancels running background tasks", () =>
|
||||
Effect.gen(function* () {
|
||||
const jobs = yield* BackgroundJob.Service
|
||||
const jobs = yield* Job.Service
|
||||
const runState = yield* SessionRunState.Service
|
||||
const { chat, assistant } = yield* seed()
|
||||
const tool = yield* TaskTool
|
||||
@@ -848,7 +844,7 @@ describe("tool.task", () => {
|
||||
|
||||
it.instance("cancelling a child run cancels its own pre-runner task job", () =>
|
||||
Effect.gen(function* () {
|
||||
const jobs = yield* BackgroundJob.Service
|
||||
const jobs = yield* Job.Service
|
||||
const runState = yield* SessionRunState.Service
|
||||
const sessions = yield* Session.Service
|
||||
const { chat } = yield* seed()
|
||||
@@ -869,7 +865,7 @@ describe("tool.task", () => {
|
||||
|
||||
it.instance("cancelling a parent run recursively cancels descendant background tasks", () =>
|
||||
Effect.gen(function* () {
|
||||
const jobs = yield* BackgroundJob.Service
|
||||
const jobs = yield* Job.Service
|
||||
const runState = yield* SessionRunState.Service
|
||||
const sessions = yield* Session.Service
|
||||
const { chat } = yield* seed()
|
||||
|
||||
Reference in New Issue
Block a user