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

@@ -1,20 +1,44 @@
import { $ } from "bun"
import * as Observability from "@opencode-ai/core/effect/observability"
import * as fs from "fs/promises"
import os from "os"
import path from "path"
import { Effect, Context } from "effect"
import { Effect, Context, Layer, ManagedRuntime } from "effect"
import type * as PlatformError from "effect/PlatformError"
import type * as Scope from "effect/Scope"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import type { Config } from "@/config/config"
import { InstanceRef } from "../../src/effect/instance-ref"
import { InstanceBootstrap } from "../../src/project/bootstrap-service"
import { InstanceRuntime } from "../../src/project/instance-runtime"
import { InstanceStore } from "../../src/project/instance-store"
import { Instance } from "../../src/project/instance"
import { TestLLMServer } from "../lib/llm-server"
// Re-export for test ergonomics. The implementation lives next to the runtime
// it consumes; see `InstanceStore.disposeAllInstances` for the rationale.
export { disposeAllInstances } from "../../src/project/instance-store"
const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
const testInstanceRuntime = ManagedRuntime.make(
InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrap), Layer.provideMerge(Observability.layer)),
)
const runTestInstanceStore = <A>(fn: (store: InstanceStore.Interface) => Effect.Effect<A>) =>
testInstanceRuntime.runPromise(InstanceStore.Service.use(fn))
export async function provideTestInstance<R>(input: { directory: string; init?: Effect.Effect<void>; fn: () => R }) {
const ctx = await runTestInstanceStore((store) => store.load({ directory: input.directory, init: input.init }))
try {
return await Instance.restore(ctx, () => input.fn())
} finally {
await runTestInstanceStore((store) => store.dispose(ctx))
}
}
export async function reloadTestInstance(input: { directory: string }) {
return runTestInstanceStore((store) => store.reload(input))
}
export async function disposeAllInstances() {
await Promise.all([InstanceRuntime.disposeAllInstances(), runTestInstanceStore((store) => store.disposeAll())])
}
// Strip null bytes from paths (defensive fix for CI environment issues)
function sanitizePath(p: string): string {
@@ -129,12 +153,10 @@ export const provideInstance =
(directory: string) =>
<A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
Effect.contextWith((services: Context.Context<R>) =>
Effect.promise<A>(async () =>
Instance.provide({
directory,
fn: () => Effect.runPromiseWith(services)(self.pipe(Effect.provideService(InstanceRef, Instance.current))),
}),
),
Effect.promise<A>(async () => {
const ctx = await runTestInstanceStore((store) => store.load({ directory }))
return Instance.restore(ctx, () => Effect.runPromiseWith(services)(self.pipe(Effect.provideService(InstanceRef, ctx))))
}),
)
export function provideTmpdirInstance<A, E, R>(
@@ -148,10 +170,7 @@ export function provideTmpdirInstance<A, E, R>(
yield* Effect.addFinalizer(() =>
provided
? Effect.promise(() =>
Instance.provide({
directory: path,
fn: () => InstanceStore.disposeInstance(Instance.current),
}),
runTestInstanceStore((store) => store.load({ directory: path }).pipe(Effect.flatMap((ctx) => store.dispose(ctx)))),
).pipe(Effect.ignore)
: Effect.void,
)