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

This commit is contained in:
Kit Langton
2026-05-12 22:43:41 -04:00
committed by GitHub
parent 80543fb5dc
commit 588b5240d0

View File

@@ -1,13 +1,14 @@
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { Effect, Layer, Queue } from "effect"
import { HttpRouter } from "effect/unstable/http"
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 { ExperimentalPaths } from "../../src/server/routes/instance/httpapi/groups/experimental"
import { WorkspacePaths } from "../../src/server/routes/instance/httpapi/groups/workspace"
import { withTimeout } from "../../src/util/timeout"
import { resetDatabase } from "../fixture/db"
import { TestInstance } from "../fixture/fixture"
import { disposeAllInstances, TestInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const stateLayer = Layer.effectDiscard(
@@ -28,7 +29,10 @@ const stateLayer = Layer.effectDiscard(
)
const it = testEffect(stateLayer)
const worktreeTest = process.platform === "win32" ? it.instance.skip : it.instance
type TestServer = ReturnType<typeof HttpRouter.toWebHandler>
type CreatedWorktree = { directory: string }
type ScopedWorktree = { directory: string; body: CreatedWorktree; ready: Effect.Effect<void, Error> }
function serverScoped() {
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) {
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 }) {
return Effect.gen(function* () {
const current = yield* request(input.server, `/project/current?directory=${encodeURIComponent(input.directory)}`)
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(
input.server,
`/project/${project.id}?directory=${encodeURIComponent(input.directory)}`,
@@ -66,47 +162,51 @@ function setProjectStartCommand(input: { server: TestServer; directory: string;
}
describe("worktree endpoint reproduction", () => {
it.instance(
worktreeTest(
"direct HttpApi worktree create returns without waiting for boot",
() =>
Effect.gen(function* () {
const test = yield* TestInstance
const server = yield* serverScoped()
const response = yield* withRequestTimeout(
request(server, `${ExperimentalPaths.worktree}?directory=${encodeURIComponent(test.directory)}`, {
const response = yield* createWorktreeScoped({
server,
directory: test.directory,
path: `${ExperimentalPaths.worktree}?directory=${encodeURIComponent(test.directory)}`,
init: {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({}),
}),
"direct worktree create",
)
},
timeoutLabel: "direct worktree create",
})
expect(response.status).toBe(200)
expect(yield* Effect.promise(() => response.json())).toMatchObject({ directory: expect.any(String) })
expect(response).toMatchObject({ directory: expect.any(String) })
}),
{ git: true },
)
it.instance(
worktreeTest(
"workspace worktree create does not hang",
() =>
Effect.gen(function* () {
const test = yield* TestInstance
const server = yield* serverScoped()
const response = yield* withRequestTimeout(
request(server, `${WorkspacePaths.list}?directory=${encodeURIComponent(test.directory)}`, {
const response = yield* createWorktreeScoped({
server,
directory: test.directory,
path: `${WorkspacePaths.list}?directory=${encodeURIComponent(test.directory)}`,
init: {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ type: "worktree", branch: null }),
}),
"workspace worktree create",
8_000,
)
},
timeoutLabel: "workspace worktree create",
timeoutMs: 8_000,
})
expect(response.status).toBe(200)
expect(yield* Effect.promise(() => response.json())).toMatchObject({
expect(response).toMatchObject({
type: "worktree",
directory: expect.any(String),
})
@@ -114,7 +214,7 @@ describe("worktree endpoint reproduction", () => {
{ git: true },
)
it.instance(
worktreeTest(
"workspace worktree create returns without waiting for project start command",
() =>
Effect.gen(function* () {
@@ -127,17 +227,19 @@ describe("worktree endpoint reproduction", () => {
})
const started = Date.now()
const response = yield* withRequestTimeout(
request(server, `${WorkspacePaths.list}?directory=${encodeURIComponent(test.directory)}`, {
yield* createWorktreeScoped({
server,
directory: test.directory,
path: `${WorkspacePaths.list}?directory=${encodeURIComponent(test.directory)}`,
init: {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ type: "worktree", branch: null }),
}),
"workspace worktree create with project start command",
6_000,
)
},
timeoutLabel: "workspace worktree create with project start command",
timeoutMs: 6_000,
})
expect(response.status).toBe(200)
expect(Date.now() - started).toBeLessThan(1_500)
}),
{ git: true },