test(instance): add effect-native fixture helpers (#27781)
This commit is contained in:
@@ -1,15 +1,21 @@
|
|||||||
import { afterEach, expect } from "bun:test"
|
import { expect } from "bun:test"
|
||||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||||
import { $ } from "bun"
|
import { $ } from "bun"
|
||||||
import { Context, Deferred, Duration, Effect, Exit, Fiber, Layer } from "effect"
|
import { Context, Deferred, Duration, Effect, Exit, Fiber, Layer } from "effect"
|
||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
import { disposeAllInstances, provideInstance, reloadTestInstance, tmpdirScoped } from "../fixture/fixture"
|
import {
|
||||||
|
disposeAllInstancesEffect,
|
||||||
|
provideInstanceEffect,
|
||||||
|
reloadInstance,
|
||||||
|
testInstanceStoreLayer,
|
||||||
|
tmpdirScoped,
|
||||||
|
} from "../fixture/fixture"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
|
|
||||||
const it = testEffect(CrossSpawnSpawner.defaultLayer)
|
const it = testEffect(Layer.mergeAll(CrossSpawnSpawner.defaultLayer, testInstanceStoreLayer))
|
||||||
|
|
||||||
const access = <A, E>(state: InstanceState.InstanceState<A, E>, dir: string) =>
|
const access = <A, E>(state: InstanceState.InstanceState<A, E>, dir: string) =>
|
||||||
InstanceState.get(state).pipe(provideInstance(dir))
|
InstanceState.get(state).pipe(provideInstanceEffect(dir))
|
||||||
|
|
||||||
const tmpdirGitScoped = Effect.gen(function* () {
|
const tmpdirGitScoped = Effect.gen(function* () {
|
||||||
const dir = yield* tmpdirScoped({ git: true })
|
const dir = yield* tmpdirScoped({ git: true })
|
||||||
@@ -17,10 +23,6 @@ const tmpdirGitScoped = Effect.gen(function* () {
|
|||||||
return dir
|
return dir
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await disposeAllInstances()
|
|
||||||
})
|
|
||||||
|
|
||||||
it.live("InstanceState caches values per directory", () =>
|
it.live("InstanceState caches values per directory", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const dir = yield* tmpdirScoped()
|
const dir = yield* tmpdirScoped()
|
||||||
@@ -68,7 +70,7 @@ it.live("InstanceState invalidates on reload", () =>
|
|||||||
)
|
)
|
||||||
|
|
||||||
const a = yield* access(state, dir)
|
const a = yield* access(state, dir)
|
||||||
yield* Effect.promise(() => reloadTestInstance({ directory: dir }))
|
yield* reloadInstance({ directory: dir })
|
||||||
const b = yield* access(state, dir)
|
const b = yield* access(state, dir)
|
||||||
|
|
||||||
expect(a).not.toBe(b)
|
expect(a).not.toBe(b)
|
||||||
@@ -93,7 +95,7 @@ it.live("InstanceState invalidates on disposeAll", () =>
|
|||||||
|
|
||||||
yield* access(state, one)
|
yield* access(state, one)
|
||||||
yield* access(state, two)
|
yield* access(state, two)
|
||||||
yield* Effect.promise(disposeAllInstances)
|
yield* disposeAllInstancesEffect
|
||||||
|
|
||||||
expect(seen.sort()).toEqual([one, two].sort())
|
expect(seen.sort()).toEqual([one, two].sort())
|
||||||
}),
|
}),
|
||||||
@@ -125,8 +127,8 @@ it.live("InstanceState.get reads the current directory lazily", () =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
yield* Effect.gen(function* () {
|
yield* Effect.gen(function* () {
|
||||||
const a = yield* Test.use((svc) => svc.get()).pipe(provideInstance(one))
|
const a = yield* Test.use((svc) => svc.get()).pipe(provideInstanceEffect(one))
|
||||||
const b = yield* Test.use((svc) => svc.get()).pipe(provideInstance(two))
|
const b = yield* Test.use((svc) => svc.get()).pipe(provideInstanceEffect(two))
|
||||||
|
|
||||||
expect(a).toBe(one)
|
expect(a).toBe(one)
|
||||||
expect(b).toBe(two)
|
expect(b).toBe(two)
|
||||||
@@ -177,7 +179,7 @@ it.live("InstanceState preserves directory across async boundaries", () =>
|
|||||||
|
|
||||||
yield* Effect.gen(function* () {
|
yield* Effect.gen(function* () {
|
||||||
const [a, b, c] = yield* Effect.all(
|
const [a, b, c] = yield* Effect.all(
|
||||||
[one, two, three].map((dir) => Test.use((svc) => svc.get()).pipe(provideInstance(dir))),
|
[one, two, three].map((dir) => Test.use((svc) => svc.get()).pipe(provideInstanceEffect(dir))),
|
||||||
{ concurrency: "unbounded" },
|
{ concurrency: "unbounded" },
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -224,7 +226,7 @@ it.live("InstanceState survives high-contention concurrent access", () =>
|
|||||||
|
|
||||||
yield* Effect.gen(function* () {
|
yield* Effect.gen(function* () {
|
||||||
const results = yield* Effect.all(
|
const results = yield* Effect.all(
|
||||||
dirs.map((dir) => Test.use((svc) => svc.get()).pipe(provideInstance(dir))),
|
dirs.map((dir) => Test.use((svc) => svc.get()).pipe(provideInstanceEffect(dir))),
|
||||||
{ concurrency: "unbounded" },
|
{ concurrency: "unbounded" },
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -263,19 +265,19 @@ it.live("InstanceState correct after interleaved init and dispose", () =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
yield* Effect.gen(function* () {
|
yield* Effect.gen(function* () {
|
||||||
const a = yield* Test.use((svc) => svc.get()).pipe(provideInstance(one))
|
const a = yield* Test.use((svc) => svc.get()).pipe(provideInstanceEffect(one))
|
||||||
expect(a).toBe(one)
|
expect(a).toBe(one)
|
||||||
|
|
||||||
const [, b] = yield* Effect.all(
|
const [, b] = yield* Effect.all(
|
||||||
[
|
[
|
||||||
Effect.promise(() => reloadTestInstance({ directory: one })),
|
reloadInstance({ directory: one }),
|
||||||
Test.use((svc) => svc.get()).pipe(provideInstance(two)),
|
Test.use((svc) => svc.get()).pipe(provideInstanceEffect(two)),
|
||||||
],
|
],
|
||||||
{ concurrency: "unbounded" },
|
{ concurrency: "unbounded" },
|
||||||
)
|
)
|
||||||
expect(b).toBe(two)
|
expect(b).toBe(two)
|
||||||
|
|
||||||
const c = yield* Test.use((svc) => svc.get()).pipe(provideInstance(one))
|
const c = yield* Test.use((svc) => svc.get()).pipe(provideInstanceEffect(one))
|
||||||
expect(c).toBe(one)
|
expect(c).toBe(one)
|
||||||
}).pipe(Effect.provide(Test.layer))
|
}).pipe(Effect.provide(Test.layer))
|
||||||
}),
|
}),
|
||||||
@@ -343,9 +345,9 @@ it.live("InstanceState survives deferred resume from the same instance context",
|
|||||||
|
|
||||||
yield* Effect.gen(function* () {
|
yield* Effect.gen(function* () {
|
||||||
const gate = yield* Deferred.make<void>()
|
const gate = yield* Deferred.make<void>()
|
||||||
const fiber = yield* Test.use((svc) => svc.get(gate)).pipe(provideInstance(dir), Effect.forkScoped)
|
const fiber = yield* Test.use((svc) => svc.get(gate)).pipe(provideInstanceEffect(dir), Effect.forkScoped)
|
||||||
|
|
||||||
yield* Deferred.succeed(gate, undefined).pipe(provideInstance(dir))
|
yield* Deferred.succeed(gate, undefined).pipe(provideInstanceEffect(dir))
|
||||||
const exit = yield* Fiber.await(fiber)
|
const exit = yield* Fiber.await(fiber)
|
||||||
|
|
||||||
expect(Exit.isSuccess(exit)).toBe(true)
|
expect(Exit.isSuccess(exit)).toBe(true)
|
||||||
@@ -380,7 +382,7 @@ it.live("InstanceState survives deferred resume outside ALS when InstanceRef is
|
|||||||
|
|
||||||
yield* Effect.gen(function* () {
|
yield* Effect.gen(function* () {
|
||||||
const gate = yield* Deferred.make<void>()
|
const gate = yield* Deferred.make<void>()
|
||||||
const fiber = yield* Test.use((svc) => svc.get(gate)).pipe(provideInstance(dir), Effect.forkScoped)
|
const fiber = yield* Test.use((svc) => svc.get(gate)).pipe(provideInstanceEffect(dir), Effect.forkScoped)
|
||||||
|
|
||||||
yield* Deferred.succeed(gate, undefined)
|
yield* Deferred.succeed(gate, undefined)
|
||||||
const exit = yield* Fiber.await(fiber)
|
const exit = yield* Fiber.await(fiber)
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ import { InstanceStore } from "../../src/project/instance-store"
|
|||||||
import { TestLLMServer } from "../lib/llm-server"
|
import { TestLLMServer } from "../lib/llm-server"
|
||||||
|
|
||||||
const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
|
const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
|
||||||
|
export const testInstanceStoreLayer = InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrap))
|
||||||
const testInstanceRuntime = ManagedRuntime.make(
|
const testInstanceRuntime = ManagedRuntime.make(
|
||||||
InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrap), Layer.provideMerge(Observability.layer)),
|
testInstanceStoreLayer.pipe(Layer.provideMerge(Observability.layer)),
|
||||||
)
|
)
|
||||||
|
|
||||||
const runTestInstanceStore = <A>(fn: (store: InstanceStore.Interface) => Effect.Effect<A>) =>
|
const runTestInstanceStore = <A>(fn: (store: InstanceStore.Interface) => Effect.Effect<A>) =>
|
||||||
@@ -165,6 +166,16 @@ export const provideInstance =
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const provideInstanceEffect =
|
||||||
|
(directory: string) =>
|
||||||
|
<A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R | InstanceStore.Service> =>
|
||||||
|
InstanceStore.Service.use((store) => store.provide({ directory }, self))
|
||||||
|
|
||||||
|
export const reloadInstance = (input: InstanceStore.LoadInput) =>
|
||||||
|
InstanceStore.Service.use((store) => store.reload(input))
|
||||||
|
|
||||||
|
export const disposeAllInstancesEffect = InstanceStore.Service.use((store) => store.disposeAll())
|
||||||
|
|
||||||
export function provideTmpdirInstance<A, E, R>(
|
export function provideTmpdirInstance<A, E, R>(
|
||||||
self: (path: string) => Effect.Effect<A, E, R>,
|
self: (path: string) => Effect.Effect<A, E, R>,
|
||||||
options?: { git?: boolean; config?: Partial<Config.Info> },
|
options?: { git?: boolean; config?: Partial<Config.Info> },
|
||||||
@@ -195,11 +206,9 @@ export const withTmpdirInstance =
|
|||||||
<A, E, R>(self: Effect.Effect<A, E, R>) =>
|
<A, E, R>(self: Effect.Effect<A, E, R>) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const directory = yield* tmpdirScoped(options)
|
const directory = yield* tmpdirScoped(options)
|
||||||
return yield* InstanceStore.Service.use((store) =>
|
return yield* self.pipe(Effect.provideService(TestInstance, { directory }), provideInstanceEffect(directory))
|
||||||
store.provide({ directory }, self.pipe(Effect.provideService(TestInstance, { directory }))),
|
|
||||||
)
|
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Effect.provide(InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrap))),
|
Effect.provide(testInstanceStoreLayer),
|
||||||
Effect.provide(CrossSpawnSpawner.defaultLayer),
|
Effect.provide(CrossSpawnSpawner.defaultLayer),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user