| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248 |
- import { describe, expect } from "bun:test"
- import { LayerNode } from "@kirincode-ai/core/effect/layer-node"
- import { CrossSpawnSpawner } from "@kirincode-ai/core/cross-spawn-spawner"
- import { Deferred, 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"
- import { InstanceStore } from "../../src/project/instance-store"
- import { tmpdirScoped } from "../fixture/fixture"
- import { testEffect } from "../lib/effect"
- let bootstrapRun: Effect.Effect<void> = Effect.void
- const noopBootstrap = Layer.succeed(
- InstanceBootstrap.Service,
- InstanceBootstrap.Service.of({ run: Effect.suspend(() => bootstrapRun) }),
- )
- const it = testEffect(
- LayerNode.compile(LayerNode.group([InstanceStore.node, CrossSpawnSpawner.node]), [
- [InstanceStore.bootstrapNode, noopBootstrap],
- ]),
- )
- const setBootstrap = (run: Effect.Effect<void>) =>
- Effect.acquireRelease(
- Effect.sync(() => {
- bootstrapRun = run
- }),
- () =>
- Effect.sync(() => {
- bootstrapRun = Effect.void
- }),
- )
- const registerDisposerScoped = (disposer: (directory: string) => Promise<void>) =>
- Effect.acquireRelease(
- Effect.sync(() => registerDisposer(disposer)),
- (off) => Effect.sync(off),
- )
- describe("InstanceStore", () => {
- it.live("loads instance context", () =>
- Effect.gen(function* () {
- const dir = yield* tmpdirScoped({ git: true })
- const store = yield* InstanceStore.Service
- const ctx = yield* store.load({ directory: dir })
- expect(ctx.directory).toBe(dir)
- expect(ctx.worktree).toBe(dir)
- }),
- )
- it.live("runs bootstrap with InstanceRef provided", () =>
- Effect.gen(function* () {
- const dir = yield* tmpdirScoped({ git: true })
- const store = yield* InstanceStore.Service
- let initializedDirectory: string | undefined
- yield* setBootstrap(
- Effect.gen(function* () {
- initializedDirectory = (yield* InstanceRef)?.directory
- }),
- )
- yield* store.load({ directory: dir })
- expect(initializedDirectory).toBe(dir)
- }),
- )
- it.live("caches loaded instance context by directory", () =>
- Effect.gen(function* () {
- const dir = yield* tmpdirScoped({ git: true })
- const store = yield* InstanceStore.Service
- let initialized = 0
- yield* setBootstrap(
- Effect.sync(() => {
- initialized++
- }),
- )
- const first = yield* store.load({ directory: dir })
- const second = yield* store.load({ directory: dir })
- expect(second).toBe(first)
- expect(initialized).toBe(1)
- }),
- )
- it.live("dedupes concurrent loads while init is in flight", () =>
- Effect.gen(function* () {
- const dir = yield* tmpdirScoped({ git: true })
- const store = yield* InstanceStore.Service
- const started = yield* Deferred.make<void>()
- const release = yield* Deferred.make<void>()
- let initialized = 0
- yield* setBootstrap(
- Effect.gen(function* () {
- initialized++
- yield* Deferred.succeed(started, undefined)
- yield* Deferred.await(release)
- }),
- )
- const first = yield* store.load({ directory: dir }).pipe(Effect.forkScoped)
- yield* Deferred.await(started)
- yield* setBootstrap(
- Effect.sync(() => {
- initialized++
- }),
- )
- const second = yield* store.load({ directory: dir }).pipe(Effect.forkScoped)
- expect(initialized).toBe(1)
- yield* Deferred.succeed(release, undefined)
- const [firstCtx, secondCtx] = yield* Effect.all([Fiber.join(first), Fiber.join(second)])
- expect(secondCtx).toBe(firstCtx)
- expect(initialized).toBe(1)
- }),
- )
- it.live("removes failed loads from the cache", () =>
- Effect.gen(function* () {
- const dir = yield* tmpdirScoped({ git: true })
- const store = yield* InstanceStore.Service
- let attempts = 0
- yield* setBootstrap(
- Effect.sync(() => {
- attempts++
- throw new Error("init failed")
- }),
- )
- const failed = yield* store.load({ directory: dir }).pipe(
- Effect.as(false),
- Effect.catchCause(() => Effect.succeed(true)),
- )
- expect(failed).toBe(true)
- yield* setBootstrap(
- Effect.sync(() => {
- attempts++
- }),
- )
- const ctx = yield* store.load({ directory: dir })
- expect(ctx.directory).toBe(dir)
- expect(attempts).toBe(2)
- }),
- )
- it.live("reload replaces the cached context", () =>
- Effect.gen(function* () {
- const dir = yield* tmpdirScoped({ git: true })
- const store = yield* InstanceStore.Service
- const first = yield* store.load({ directory: dir })
- const second = yield* store.reload({ directory: dir })
- const cached = yield* store.load({ directory: dir })
- expect(second).not.toBe(first)
- expect(cached).toBe(second)
- }),
- )
- it.live("stale dispose does not delete an in-flight reload", () =>
- Effect.gen(function* () {
- const dir = yield* tmpdirScoped({ git: true })
- const store = yield* InstanceStore.Service
- const reloading = yield* Deferred.make<void>()
- const releaseReload = yield* Deferred.make<void>()
- const disposed: Array<string> = []
- yield* registerDisposerScoped(async (directory) => {
- disposed.push(directory)
- })
- const first = yield* store.load({ directory: dir })
- yield* setBootstrap(
- Effect.gen(function* () {
- yield* Deferred.succeed(reloading, undefined)
- yield* Deferred.await(releaseReload)
- }),
- )
- const reload = yield* store.reload({ directory: dir }).pipe(Effect.forkScoped)
- yield* Deferred.await(reloading)
- const staleDispose = yield* store.dispose(first).pipe(Effect.forkScoped)
- yield* Deferred.succeed(releaseReload, undefined)
- const second = yield* Fiber.join(reload)
- yield* Fiber.join(staleDispose)
- expect(disposed).toEqual([dir])
- expect(yield* store.load({ directory: dir })).toBe(second)
- }),
- )
- it.live("dedupes concurrent disposeAll calls", () =>
- Effect.gen(function* () {
- const dir = yield* tmpdirScoped({ git: true })
- const store = yield* InstanceStore.Service
- const disposing = yield* Deferred.make<void>()
- const releaseDispose = yield* Deferred.make<() => void>()
- const disposed: Array<string> = []
- yield* registerDisposerScoped((directory) => {
- disposed.push(directory)
- Deferred.doneUnsafe(disposing, Effect.void)
- return new Promise<void>((resolve) => {
- Deferred.doneUnsafe(releaseDispose, Effect.succeed(resolve))
- })
- })
- yield* store.load({ directory: dir })
- const first = yield* store.disposeAll().pipe(Effect.forkScoped)
- yield* Deferred.await(disposing)
- const release = yield* Deferred.await(releaseDispose)
- const second = yield* store.disposeAll().pipe(Effect.forkScoped)
- expect(disposed).toEqual([dir])
- yield* Effect.sync(release)
- yield* Effect.all([Fiber.join(first), Fiber.join(second)])
- expect(disposed).toEqual([dir])
- }),
- )
- it.live("re-arms disposeAll after completion", () =>
- Effect.gen(function* () {
- const dir1 = yield* tmpdirScoped({ git: true })
- const dir2 = yield* tmpdirScoped({ git: true })
- const store = yield* InstanceStore.Service
- const disposed: Array<string> = []
- yield* registerDisposerScoped(async (directory) => {
- disposed.push(directory)
- })
- yield* store.load({ directory: dir1 })
- yield* store.disposeAll()
- expect(disposed).toEqual([dir1])
- yield* store.load({ directory: dir2 })
- yield* store.disposeAll()
- expect(disposed).toEqual([dir1, dir2])
- }),
- )
- })
|