8c94e9005f
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Affan Ali <93028901+affanali2k3@users.noreply.github.com> Co-authored-by: affanali2k3 <affanalikhanxx@gmail.com> Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Jay V <air@live.ca> Co-authored-by: Dax Raad <d@ironbay.co> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: Ben Guthrie <benjee.012@gmail.com> Co-authored-by: Dax <mail@thdxr.com> Co-authored-by: Filip <34747899+neriousy@users.noreply.github.com> Co-authored-by: Max Anderson <max.a.anderson95@gmail.com> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box> Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: runvip <164729189+runvip@users.noreply.github.com> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Julian Coy <julian@ex-machina.co> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com>
116 lines
3.8 KiB
TypeScript
116 lines
3.8 KiB
TypeScript
import { afterEach, expect } from "bun:test"
|
|
import { existsSync } from "node:fs"
|
|
import path from "node:path"
|
|
import { pathToFileURL } from "node:url"
|
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
|
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
|
import { Cause, Effect, Exit, Fiber } from "effect"
|
|
import { bootstrap as cliBootstrap } from "../../src/cli/bootstrap"
|
|
import { InstanceBootstrap } from "../../src/project/bootstrap"
|
|
import { InstanceStore } from "../../src/project/instance-store"
|
|
import { disposeAllInstances, tmpdirScoped } from "../fixture/fixture"
|
|
import { testEffect } from "../lib/effect"
|
|
import { waitGlobalBusEvent } from "../server/global-bus"
|
|
|
|
const it = testEffect(
|
|
LayerNode.compile(LayerNode.group([InstanceStore.node, CrossSpawnSpawner.node]), [
|
|
[InstanceStore.bootstrapNode, InstanceBootstrap.node],
|
|
]),
|
|
)
|
|
|
|
// InstanceBootstrap must run before any code touches the instance —
|
|
// originally tracked by PRs #25389 and #25449, now a permanent
|
|
// invariant. The plugin config hook writes a marker file; the test
|
|
// bodies deliberately avoid Plugin/config directly. The marker only
|
|
// appears if InstanceBootstrap ran at the instance boundary.
|
|
//
|
|
// The boundaries below are transport-agnostic and stay.
|
|
|
|
afterEach(async () => {
|
|
await disposeAllInstances()
|
|
})
|
|
|
|
const bootstrapFixture = Effect.gen(function* () {
|
|
const dir = yield* tmpdirScoped({ git: true })
|
|
const marker = path.join(dir, "config-hook-fired")
|
|
const pluginFile = path.join(dir, "plugin.ts")
|
|
yield* Effect.promise(() =>
|
|
Bun.write(
|
|
pluginFile,
|
|
[
|
|
`const MARKER = ${JSON.stringify(marker)}`,
|
|
"export default async () => ({",
|
|
" config: async () => {",
|
|
' await Bun.write(MARKER, "ran")',
|
|
" },",
|
|
"})",
|
|
"",
|
|
].join("\n"),
|
|
),
|
|
)
|
|
yield* Effect.promise(() =>
|
|
Bun.write(
|
|
path.join(dir, "opencode.json"),
|
|
JSON.stringify({
|
|
$schema: "https://opencode.ai/config.json",
|
|
plugin: [pathToFileURL(pluginFile).href],
|
|
}),
|
|
),
|
|
)
|
|
return { directory: dir, marker }
|
|
})
|
|
|
|
function waitDisposed(directory: string) {
|
|
return waitGlobalBusEvent({
|
|
message: "timed out waiting for CLI bootstrap instance disposal",
|
|
predicate: (event) => event.payload.type === "server.instance.disposed" && event.directory === directory,
|
|
})
|
|
}
|
|
|
|
it.live("InstanceStore.provide runs InstanceBootstrap before effect", () =>
|
|
Effect.gen(function* () {
|
|
const tmp = yield* bootstrapFixture
|
|
const store = yield* InstanceStore.Service
|
|
|
|
yield* store.provide({ directory: tmp.directory }, Effect.succeed("ok"))
|
|
|
|
expect(existsSync(tmp.marker)).toBe(true)
|
|
}),
|
|
)
|
|
|
|
it.live("CLI bootstrap runs InstanceBootstrap before callback", () =>
|
|
Effect.gen(function* () {
|
|
const tmp = yield* bootstrapFixture
|
|
|
|
yield* Effect.promise(() => cliBootstrap(tmp.directory, async () => "ok"))
|
|
|
|
expect(existsSync(tmp.marker)).toBe(true)
|
|
}),
|
|
)
|
|
|
|
it.live("CLI bootstrap disposes the instance when the callback rejects", () =>
|
|
Effect.gen(function* () {
|
|
const tmp = yield* bootstrapFixture
|
|
const disposed = yield* waitDisposed(tmp.directory).pipe(Effect.forkScoped({ startImmediately: true }))
|
|
|
|
const exit = yield* Effect.promise(() =>
|
|
cliBootstrap(tmp.directory, async () => Promise.reject(new Error("boom"))),
|
|
).pipe(Effect.exit)
|
|
|
|
expect(Exit.isFailure(exit)).toBe(true)
|
|
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toMatchObject({ message: "boom" })
|
|
yield* Fiber.join(disposed)
|
|
}),
|
|
)
|
|
|
|
it.live("InstanceStore.reload runs InstanceBootstrap", () =>
|
|
Effect.gen(function* () {
|
|
const tmp = yield* bootstrapFixture
|
|
const store = yield* InstanceStore.Service
|
|
|
|
yield* store.reload({ directory: tmp.directory })
|
|
|
|
expect(existsSync(tmp.marker)).toBe(true)
|
|
}),
|
|
)
|