fix(worktree): fork workspace worktree boot (#25723)

This commit is contained in:
Kit Langton 2026-05-04 12:01:13 -04:00 committed by GitHub
parent b70e2700ef
commit 25dc6f09bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 156 additions and 7 deletions

View file

@ -291,16 +291,15 @@ export const layer: Layer.Layer<
const createFromInfo = Effect.fn("Worktree.createFromInfo")(function* (info: Info, startCommand?: string) {
yield* setup(info)
yield* boot(info, startCommand)
yield* boot(info, startCommand).pipe(
Effect.catchCause((cause) => Effect.sync(() => log.error("worktree bootstrap failed", { cause }))),
Effect.forkIn(scope),
)
})
const create = Effect.fn("Worktree.create")(function* (input?: CreateInput) {
const info = yield* makeWorktreeInfo(input?.name)
yield* setup(info)
yield* boot(info, input?.startCommand).pipe(
Effect.catchCause((cause) => Effect.sync(() => log.error("worktree bootstrap failed", { cause }))),
Effect.forkIn(scope),
)
yield* createFromInfo(info, input?.startCommand)
return info
})

View file

@ -178,12 +178,13 @@ describe("Worktree", () => {
})
describe("createFromInfo", () => {
wintest("creates and bootstraps git worktree", () =>
wintest("creates git worktree and boots asynchronously", () =>
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
const svc = yield* Worktree.Service
const info = yield* svc.makeWorktreeInfo("from-info-test")
const ready = waitReady()
yield* svc.createFromInfo(info)
const list = yield* Effect.promise(() => $`git worktree list --porcelain`.cwd(dir).quiet().text())
@ -191,6 +192,7 @@ describe("Worktree", () => {
const normalizedDir = info.directory.replace(/\\/g, "/")
expect(normalizedList).toContain(normalizedDir)
yield* Effect.promise(() => ready)
yield* svc.remove({ directory: info.directory })
}),
{ git: true },

View file

@ -0,0 +1,148 @@
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { HttpRouter } from "effect/unstable/http"
import { Flag } from "@opencode-ai/core/flag/flag"
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 { testEffect } from "../lib/effect"
const stateLayer = Layer.effectDiscard(
Effect.gen(function* () {
const original = {
OPENCODE_EXPERIMENTAL_HTTPAPI: Flag.OPENCODE_EXPERIMENTAL_HTTPAPI,
OPENCODE_EXPERIMENTAL_WORKSPACES: Flag.OPENCODE_EXPERIMENTAL_WORKSPACES,
}
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true
yield* Effect.addFinalizer(() =>
Effect.promise(async () => {
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original.OPENCODE_EXPERIMENTAL_HTTPAPI
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = original.OPENCODE_EXPERIMENTAL_WORKSPACES
await resetDatabase()
}),
)
}),
)
const it = testEffect(stateLayer)
type TestServer = ReturnType<typeof HttpRouter.toWebHandler>
function serverScoped() {
return Effect.acquireRelease(
Effect.sync(() => HttpRouter.toWebHandler(ExperimentalHttpApiServer.routes, { disableLogger: true })),
(server) => Effect.promise(() => server.dispose()).pipe(Effect.ignore),
)
}
function request(server: TestServer, input: string, init?: RequestInit) {
return Effect.promise(() =>
server.handler(new Request(new URL(input, "http://localhost"), init), ExperimentalHttpApiServer.context),
)
}
function withRequestTimeout(effect: Effect.Effect<Response>, label: string, ms = 5_000) {
return Effect.promise(() => withTimeout(Effect.runPromise(effect), ms, label))
}
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 updated = yield* request(
input.server,
`/project/${project.id}?directory=${encodeURIComponent(input.directory)}`,
{
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({ commands: { start: input.command } }),
},
)
expect(updated.status).toBe(200)
})
}
describe("worktree endpoint reproduction", () => {
it.instance(
"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)}`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({}),
}),
"direct worktree create",
)
expect(response.status).toBe(200)
expect(yield* Effect.promise(() => response.json())).toMatchObject({ directory: expect.any(String) })
}),
{ git: true },
)
it.instance(
"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)}`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ type: "worktree", branch: null }),
}),
"workspace worktree create",
8_000,
)
expect(response.status).toBe(200)
expect(yield* Effect.promise(() => response.json())).toMatchObject({
type: "worktree",
directory: expect.any(String),
})
}),
{ git: true },
)
it.instance(
"workspace worktree create returns without waiting for project start command",
() =>
Effect.gen(function* () {
const test = yield* TestInstance
const server = yield* serverScoped()
yield* setProjectStartCommand({
server,
directory: test.directory,
command: 'bun -e "setTimeout(() => {}, 2000)"',
})
const started = Date.now()
const response = yield* withRequestTimeout(
request(server, `${WorkspacePaths.list}?directory=${encodeURIComponent(test.directory)}`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ type: "worktree", branch: null }),
}),
"workspace worktree create with project start command",
6_000,
)
expect(response.status).toBe(200)
expect(Date.now() - started).toBeLessThan(1_500)
}),
{ git: true },
)
})