opencode/packages/core/test/workspace.test.ts

106 lines
4.2 KiB
TypeScript

import { beforeEach, expect } from "bun:test"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { Database } from "@opencode-ai/core/database/database"
import { makeMemoryDriver } from "@opencode-ai/core/environment"
import { Workspace } from "@opencode-ai/core/workspace"
import { WorkspaceDriver } from "@opencode-ai/core/workspace/driver"
import { WorkspaceTable } from "@opencode-ai/core/workspace/sql"
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
import { eq } from "drizzle-orm"
import { Effect } from "effect"
import { TestClock } from "effect/testing"
import { ChildProcess } from "effect/unstable/process"
import { testEffect } from "./lib/effect"
const calls: Array<{ readonly operation: string; readonly binding?: WorkspaceDriver.Binding }> = []
const memory = makeMemoryDriver()
let failConnect = false
const driver = WorkspaceDriver.make({
create: ({ workspaceID }) => {
calls.push({ operation: "create" })
return Effect.succeed({ binding: { workspaceID, generation: 0 } })
},
connect: ({ binding }) => {
calls.push({ operation: "connect", binding })
if (failConnect) return Effect.fail(new WorkspaceDriver.Error({ message: "wake failed" }))
return Effect.succeed(memory)
},
suspendForIdle: ({ binding, saveBinding }) => {
calls.push({ operation: "suspendForIdle", binding })
return saveBinding({ ...binding, generation: Number(binding.generation) + 1, suspended: true })
},
destroy: ({ binding }) => {
calls.push({ operation: "destroy", binding })
return Effect.void
},
})
const it = testEffect(
AppNodeBuilder.build(
LayerNode.group([Database.node, Workspace.configured({ idleThreshold: "5 minutes", pollInterval: "1 minute" })]),
[[WorkspaceDriver.node, WorkspaceDriver.registryNode({ fake: driver })]],
),
)
beforeEach(() => {
calls.splice(0)
failConnect = false
})
it.effect("persists the workspace lifecycle and reconnects after idle suspension", () =>
Effect.gen(function* () {
const workspace = yield* Workspace.Service
const created = yield* workspace.create("fake")
expect(created.id.startsWith("wrk_")).toBe(true)
expect(created.binding).toEqual({ workspaceID: created.id, generation: 0 })
const environment = yield* workspace.connect(created.id)
expect(calls.map((call) => call.operation)).toEqual(["create"])
yield* TestClock.adjust("4 minutes")
yield* Effect.scoped(environment.spawner.spawn(ChildProcess.make("activity"))).pipe(Effect.exit)
yield* TestClock.adjust("4 minutes")
expect(calls.map((call) => call.operation)).toEqual(["create", "connect"])
yield* TestClock.adjust("2 minutes")
expect(calls.map((call) => call.operation)).toEqual(["create", "connect", "suspendForIdle"])
const stored = yield* Database.Service.use(({ db }) =>
db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, created.id)).get(),
).pipe(Effect.orDie)
expect(stored?.binding).toEqual({ workspaceID: created.id, generation: 1, suspended: true })
expect(stored?.last_used_at).toBe(4 * 60 * 1000)
yield* Effect.scoped(environment.spawner.spawn(ChildProcess.make("wake"))).pipe(Effect.exit)
expect(calls.map((call) => call.operation)).toEqual(["create", "connect", "suspendForIdle", "connect"])
expect(calls.at(-1)?.binding).toEqual({ workspaceID: created.id, generation: 1, suspended: true })
yield* workspace.destroy(created.id)
expect(calls.at(-1)?.operation).toBe("destroy")
}),
)
it.effect("surfaces wake failures through the spawn error channel", () =>
Effect.gen(function* () {
const workspace = yield* Workspace.Service
const created = yield* workspace.create("fake")
const environment = yield* workspace.connect(created.id)
yield* Effect.scoped(environment.spawner.spawn(ChildProcess.make("connect"))).pipe(Effect.exit)
yield* TestClock.adjust("6 minutes")
failConnect = true
const error = yield* Effect.scoped(environment.spawner.spawn(ChildProcess.make("wake"))).pipe(Effect.flip)
expect(error).toMatchObject({
_tag: "PlatformError",
reason: {
_tag: "Unknown",
module: "Workspace",
method: "spawn",
description: `Failed to wake workspace ${created.id}`,
},
})
}),
)