fix(instance): run bootstrap from instance store (#25475)

This commit is contained in:
Kit Langton
2026-05-02 19:33:38 -04:00
committed by GitHub
parent 36007aecf4
commit f98053c34e
42 changed files with 540 additions and 249 deletions

View File

@@ -0,0 +1,85 @@
import { afterEach, expect, test } from "bun:test"
import { Hono } from "hono"
import { existsSync } from "node:fs"
import path from "node:path"
import { pathToFileURL } from "node:url"
import { bootstrap as cliBootstrap } from "../../src/cli/bootstrap"
import { Instance } from "../../src/project/instance"
import { InstanceRuntime } from "../../src/project/instance-runtime"
import { InstanceMiddleware } from "../../src/server/routes/instance/middleware"
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
// These regressions cover the legacy instance-loading paths fixed by PRs
// #25389 and #25449. The plugin config hook writes a marker file, and the test
// bodies deliberately avoid touching Plugin or config directly. The marker only
// exists if InstanceBootstrap ran at the instance boundary.
afterEach(async () => {
await disposeAllInstances()
})
async function bootstrapFixture() {
return tmpdir({
init: async (dir) => {
const marker = path.join(dir, "config-hook-fired")
const pluginFile = path.join(dir, "plugin.ts")
await Bun.write(
pluginFile,
[
`const MARKER = ${JSON.stringify(marker)}`,
"export default async () => ({",
" config: async () => {",
' await Bun.write(MARKER, "ran")',
" },",
"})",
"",
].join("\n"),
)
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
plugin: [pathToFileURL(pluginFile).href],
}),
)
return marker
},
})
}
test("Instance.provide runs InstanceBootstrap before fn (boundary invariant)", async () => {
await using tmp = await bootstrapFixture()
await Instance.provide({
directory: tmp.path,
fn: async () => "ok",
})
expect(existsSync(tmp.extra)).toBe(true)
})
test("CLI bootstrap runs InstanceBootstrap before callback", async () => {
await using tmp = await bootstrapFixture()
await cliBootstrap(tmp.path, async () => "ok")
expect(existsSync(tmp.extra)).toBe(true)
})
test("legacy Hono instance middleware runs InstanceBootstrap before next handler", async () => {
await using tmp = await bootstrapFixture()
const app = new Hono().use(InstanceMiddleware()).get("/probe", (c) => c.text("ok"))
const response = await app.request("/probe", { headers: { "x-opencode-directory": tmp.path } })
expect(response.status).toBe(200)
expect(existsSync(tmp.extra)).toBe(true)
})
test("InstanceRuntime.reloadInstance runs InstanceBootstrap", async () => {
await using tmp = await bootstrapFixture()
await InstanceRuntime.reloadInstance({ directory: tmp.path })
expect(existsSync(tmp.extra)).toBe(true)
})

View File

@@ -3,12 +3,17 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Effect, Fiber, Layer } from "effect"
import { InstanceRef } from "../../src/effect/instance-ref"
import { registerDisposer } from "../../src/effect/instance-registry"
import { InstanceBootstrap } from "../../src/project/bootstrap-service"
import { Instance } from "../../src/project/instance"
import { InstanceStore } from "../../src/project/instance-store"
import { disposeAllInstances, tmpdirScoped } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const it = testEffect(Layer.mergeAll(InstanceStore.defaultLayer, CrossSpawnSpawner.defaultLayer))
const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
const it = testEffect(
Layer.mergeAll(InstanceStore.defaultLayer, CrossSpawnSpawner.defaultLayer).pipe(Layer.provide(noopBootstrap)),
)
afterEach(async () => {
await disposeAllInstances()

View File

@@ -5,7 +5,7 @@ import path from "path"
import { Cause, Effect, Exit, Layer } from "effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Instance } from "../../src/project/instance"
import { InstanceStore } from "../../src/project/instance-store"
import { InstanceRuntime } from "../../src/project/instance-runtime"
import { Worktree } from "../../src/worktree"
import { disposeAllInstances, provideInstance, provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
@@ -138,9 +138,10 @@ describe("Worktree", () => {
expect(props.branch).toBe(info.branch)
yield* Effect.promise(() =>
InstanceStore.runtime.runPromise((s) =>
s.load({ directory: info.directory }).pipe(Effect.flatMap(s.dispose)),
),
Instance.provide({
directory: info.directory,
fn: () => InstanceRuntime.disposeInstance(Instance.current),
}),
)
yield* Effect.promise(() => Bun.sleep(100))
yield* svc.remove({ directory: info.directory })
@@ -162,9 +163,10 @@ describe("Worktree", () => {
yield* Effect.promise(() => ready)
yield* Effect.promise(() =>
InstanceStore.runtime.runPromise((s) =>
s.load({ directory: info.directory }).pipe(Effect.flatMap(s.dispose)),
),
Instance.provide({
directory: info.directory,
fn: () => InstanceRuntime.disposeInstance(Instance.current),
}),
)
yield* Effect.promise(() => Bun.sleep(100))
yield* svc.remove({ directory: info.directory })