test(server): migrate worktree endpoint repro to effect runner (#27220)

This commit is contained in:
Kit Langton
2026-05-13 02:43:41 +00:00
committed by GitHub
parent 80543fb5dc
commit 588b5240d0
@@ -1,13 +1,14 @@
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect" import { Effect, Layer, Queue } from "effect"
import { HttpRouter } from "effect/unstable/http" import { HttpRouter } from "effect/unstable/http"
import { Flag } from "@opencode-ai/core/flag/flag" import { Flag } from "@opencode-ai/core/flag/flag"
import { GlobalBus, type GlobalEvent } from "@/bus/global"
import { Worktree } from "@/worktree"
import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server" import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server"
import { ExperimentalPaths } from "../../src/server/routes/instance/httpapi/groups/experimental" import { ExperimentalPaths } from "../../src/server/routes/instance/httpapi/groups/experimental"
import { WorkspacePaths } from "../../src/server/routes/instance/httpapi/groups/workspace" import { WorkspacePaths } from "../../src/server/routes/instance/httpapi/groups/workspace"
import { withTimeout } from "../../src/util/timeout"
import { resetDatabase } from "../fixture/db" import { resetDatabase } from "../fixture/db"
import { TestInstance } from "../fixture/fixture" import { disposeAllInstances, TestInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect" import { testEffect } from "../lib/effect"
const stateLayer = Layer.effectDiscard( const stateLayer = Layer.effectDiscard(
@@ -28,7 +29,10 @@ const stateLayer = Layer.effectDiscard(
) )
const it = testEffect(stateLayer) const it = testEffect(stateLayer)
const worktreeTest = process.platform === "win32" ? it.instance.skip : it.instance
type TestServer = ReturnType<typeof HttpRouter.toWebHandler> type TestServer = ReturnType<typeof HttpRouter.toWebHandler>
type CreatedWorktree = { directory: string }
type ScopedWorktree = { directory: string; body: CreatedWorktree; ready: Effect.Effect<void, Error> }
function serverScoped() { function serverScoped() {
return Effect.acquireRelease( return Effect.acquireRelease(
@@ -44,14 +48,106 @@ function request(server: TestServer, input: string, init?: RequestInit) {
} }
function withRequestTimeout(effect: Effect.Effect<Response>, label: string, ms = 5_000) { function withRequestTimeout(effect: Effect.Effect<Response>, label: string, ms = 5_000) {
return Effect.promise(() => withTimeout(Effect.runPromise(effect), ms, label)) return effect.pipe(
Effect.timeoutOrElse({
duration: `${ms} millis`,
orElse: () => Effect.fail(new Error(`${label} timed out after ${ms}ms`)),
}),
)
}
function json<T>(response: Response) {
return Effect.promise(() => response.json() as Promise<T>)
}
function readyWatcher() {
return Effect.gen(function* () {
const events = yield* Queue.bounded<GlobalEvent>(1)
const on = (event: GlobalEvent) => {
if (event.payload.type === Worktree.Event.Ready.type) Queue.offerUnsafe(events, event)
}
GlobalBus.on("event", on)
yield* Effect.addFinalizer(() => Effect.sync(() => GlobalBus.off("event", on)))
return (directory: string) =>
Effect.gen(function* () {
while (true) {
const event = yield* Queue.take(events)
if (event.directory === directory) return
}
}).pipe(
Effect.timeoutOrElse({
duration: "10 seconds",
orElse: () => Effect.fail(new Error(`timed out waiting for worktree.ready: ${directory}`)),
}),
)
})
}
function removeCreatedWorktree(input: {
server: TestServer
rootDirectory: string
worktreeDirectory: string
ready: Effect.Effect<void, Error>
}) {
return Effect.gen(function* () {
yield* input.ready.pipe(Effect.timeout("1 second"), Effect.ignore)
yield* Effect.promise(() => disposeAllInstances()).pipe(Effect.ignore)
const removed = yield* request(
input.server,
`${ExperimentalPaths.worktree}?directory=${encodeURIComponent(input.rootDirectory)}`,
{
method: "DELETE",
headers: { "content-type": "application/json" },
body: JSON.stringify({ directory: input.worktreeDirectory }),
},
)
if (removed.status !== 200) {
const message = yield* Effect.promise(() => removed.text())
throw new Error(`failed to remove worktree: ${removed.status} ${message}`)
}
const ok = yield* json<boolean>(removed)
if (!ok) throw new Error(`failed to remove worktree ${input.worktreeDirectory}`)
})
}
function createWorktreeScoped(input: {
server: TestServer
directory: string
path: string
init: RequestInit
timeoutLabel: string
timeoutMs?: number
}) {
return Effect.acquireRelease(
Effect.gen(function* () {
const waitReady = yield* readyWatcher()
const response = yield* withRequestTimeout(
request(input.server, input.path, input.init),
input.timeoutLabel,
input.timeoutMs,
)
expect(response.status).toBe(200)
const body = yield* json<CreatedWorktree>(response)
return { directory: body.directory, body, ready: waitReady(body.directory) } satisfies ScopedWorktree
}),
(created) =>
removeCreatedWorktree({
server: input.server,
rootDirectory: input.directory,
worktreeDirectory: created.directory,
ready: created.ready,
}).pipe(Effect.orDie),
).pipe(Effect.map((created) => created.body))
} }
function setProjectStartCommand(input: { server: TestServer; directory: string; command: string }) { function setProjectStartCommand(input: { server: TestServer; directory: string; command: string }) {
return Effect.gen(function* () { return Effect.gen(function* () {
const current = yield* request(input.server, `/project/current?directory=${encodeURIComponent(input.directory)}`) const current = yield* request(input.server, `/project/current?directory=${encodeURIComponent(input.directory)}`)
expect(current.status).toBe(200) expect(current.status).toBe(200)
const project = (yield* Effect.promise(() => current.json())) as { id: string } const project = yield* json<{ id: string }>(current)
const updated = yield* request( const updated = yield* request(
input.server, input.server,
`/project/${project.id}?directory=${encodeURIComponent(input.directory)}`, `/project/${project.id}?directory=${encodeURIComponent(input.directory)}`,
@@ -66,47 +162,51 @@ function setProjectStartCommand(input: { server: TestServer; directory: string;
} }
describe("worktree endpoint reproduction", () => { describe("worktree endpoint reproduction", () => {
it.instance( worktreeTest(
"direct HttpApi worktree create returns without waiting for boot", "direct HttpApi worktree create returns without waiting for boot",
() => () =>
Effect.gen(function* () { Effect.gen(function* () {
const test = yield* TestInstance const test = yield* TestInstance
const server = yield* serverScoped() const server = yield* serverScoped()
const response = yield* withRequestTimeout( const response = yield* createWorktreeScoped({
request(server, `${ExperimentalPaths.worktree}?directory=${encodeURIComponent(test.directory)}`, { server,
directory: test.directory,
path: `${ExperimentalPaths.worktree}?directory=${encodeURIComponent(test.directory)}`,
init: {
method: "POST", method: "POST",
headers: { "content-type": "application/json" }, headers: { "content-type": "application/json" },
body: JSON.stringify({}), body: JSON.stringify({}),
}), },
"direct worktree create", timeoutLabel: "direct worktree create",
) })
expect(response.status).toBe(200) expect(response).toMatchObject({ directory: expect.any(String) })
expect(yield* Effect.promise(() => response.json())).toMatchObject({ directory: expect.any(String) })
}), }),
{ git: true }, { git: true },
) )
it.instance( worktreeTest(
"workspace worktree create does not hang", "workspace worktree create does not hang",
() => () =>
Effect.gen(function* () { Effect.gen(function* () {
const test = yield* TestInstance const test = yield* TestInstance
const server = yield* serverScoped() const server = yield* serverScoped()
const response = yield* withRequestTimeout( const response = yield* createWorktreeScoped({
request(server, `${WorkspacePaths.list}?directory=${encodeURIComponent(test.directory)}`, { server,
directory: test.directory,
path: `${WorkspacePaths.list}?directory=${encodeURIComponent(test.directory)}`,
init: {
method: "POST", method: "POST",
headers: { "content-type": "application/json" }, headers: { "content-type": "application/json" },
body: JSON.stringify({ type: "worktree", branch: null }), body: JSON.stringify({ type: "worktree", branch: null }),
}), },
"workspace worktree create", timeoutLabel: "workspace worktree create",
8_000, timeoutMs: 8_000,
) })
expect(response.status).toBe(200) expect(response).toMatchObject({
expect(yield* Effect.promise(() => response.json())).toMatchObject({
type: "worktree", type: "worktree",
directory: expect.any(String), directory: expect.any(String),
}) })
@@ -114,7 +214,7 @@ describe("worktree endpoint reproduction", () => {
{ git: true }, { git: true },
) )
it.instance( worktreeTest(
"workspace worktree create returns without waiting for project start command", "workspace worktree create returns without waiting for project start command",
() => () =>
Effect.gen(function* () { Effect.gen(function* () {
@@ -127,17 +227,19 @@ describe("worktree endpoint reproduction", () => {
}) })
const started = Date.now() const started = Date.now()
const response = yield* withRequestTimeout( yield* createWorktreeScoped({
request(server, `${WorkspacePaths.list}?directory=${encodeURIComponent(test.directory)}`, { server,
directory: test.directory,
path: `${WorkspacePaths.list}?directory=${encodeURIComponent(test.directory)}`,
init: {
method: "POST", method: "POST",
headers: { "content-type": "application/json" }, headers: { "content-type": "application/json" },
body: JSON.stringify({ type: "worktree", branch: null }), body: JSON.stringify({ type: "worktree", branch: null }),
}), },
"workspace worktree create with project start command", timeoutLabel: "workspace worktree create with project start command",
6_000, timeoutMs: 6_000,
) })
expect(response.status).toBe(200)
expect(Date.now() - started).toBeLessThan(1_500) expect(Date.now() - started).toBeLessThan(1_500)
}), }),
{ git: true }, { git: true },