fix: logo 右半部分从 CODING 改为 CODE
去掉难以正确渲染的 N 和 G 字母,右半部分简化为 CODE(4 字母), 与左半部分 AIR 组合为 AIR CODE。
This commit is contained in:
108
packages/opencode/test/effect/app-graph-types.test.ts
Normal file
108
packages/opencode/test/effect/app-graph-types.test.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { test } from "bun:test"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
|
||||
class A extends Context.Service<A, { readonly value: "a" }>()("test/A") {}
|
||||
class B extends Context.Service<B, { readonly value: "b" }>()("test/B") {}
|
||||
class C extends Context.Service<C, { readonly value: "c" }>()("test/C") {}
|
||||
class LayerError {
|
||||
readonly _tag = "LayerError"
|
||||
}
|
||||
class NotFoundError {
|
||||
readonly _tag = "NotFoundError"
|
||||
}
|
||||
class DiskError {
|
||||
readonly _tag = "DiskError"
|
||||
}
|
||||
class NetworkError {
|
||||
readonly _tag = "NetworkError"
|
||||
}
|
||||
|
||||
const aImplementation = Layer.succeed(A, A.of({ value: "a" }))
|
||||
const bImplementation = Layer.effect(
|
||||
B,
|
||||
Effect.gen(function* () {
|
||||
yield* A
|
||||
return B.of({ value: "b" })
|
||||
}),
|
||||
)
|
||||
const cImplementation = Layer.effect(
|
||||
C,
|
||||
Effect.gen(function* () {
|
||||
yield* A
|
||||
yield* B
|
||||
return C.of({ value: "c" })
|
||||
}),
|
||||
)
|
||||
const failingAImplementation = Layer.effect(A, Effect.fail(new LayerError()))
|
||||
const notFoundAImplementation = Layer.effect(A, Effect.fail(new NotFoundError()))
|
||||
const diskAImplementation = Layer.effect(A, Effect.fail(new DiskError()))
|
||||
const networkAImplementation = Layer.effect(A, Effect.fail(new NetworkError()))
|
||||
const notFoundOrDiskAImplementation = Layer.effect(A, Effect.fail(new NotFoundError() as NotFoundError | DiskError))
|
||||
|
||||
type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false
|
||||
type Assert<T extends true> = T
|
||||
|
||||
type AProvides = Assert<Equal<Layer.Success<typeof aImplementation>, A>>
|
||||
type ARequires = Assert<Equal<Layer.Services<typeof aImplementation>, never>>
|
||||
type BProvides = Assert<Equal<Layer.Success<typeof bImplementation>, B>>
|
||||
type BRequires = Assert<Equal<Layer.Services<typeof bImplementation>, A>>
|
||||
type CRequires = Assert<Equal<Layer.Services<typeof cImplementation>, A | B>>
|
||||
void (0 as unknown as AProvides)
|
||||
void (0 as unknown as ARequires)
|
||||
void (0 as unknown as BProvides)
|
||||
void (0 as unknown as BRequires)
|
||||
void (0 as unknown as CRequires)
|
||||
|
||||
const a = LayerNode.make(aImplementation, [])
|
||||
const b = LayerNode.make(bImplementation, [a])
|
||||
const c = LayerNode.make(cImplementation, [a, b])
|
||||
const failingA = LayerNode.make(failingAImplementation, [])
|
||||
const bWithFailingA = LayerNode.make(bImplementation, [failingA])
|
||||
const notFoundA = LayerNode.make(notFoundAImplementation, [])
|
||||
const diskA = LayerNode.make(diskAImplementation, [])
|
||||
const networkA = LayerNode.make(networkAImplementation, [])
|
||||
const notFoundOrDiskA = LayerNode.make(notFoundOrDiskAImplementation, [])
|
||||
|
||||
// @ts-expect-error B requires A
|
||||
LayerNode.make(bImplementation, [])
|
||||
|
||||
// @ts-expect-error C requires both A and B
|
||||
LayerNode.make(cImplementation, [a])
|
||||
|
||||
type ANodeProvides = Assert<Equal<typeof a, LayerNode.Node<A, never>>>
|
||||
type BNodeProvides = Assert<Equal<typeof b, LayerNode.Node<B, never>>>
|
||||
type CNodeProvides = Assert<Equal<typeof c, LayerNode.Node<C, never>>>
|
||||
type FailingANodeError = Assert<Equal<typeof failingA, LayerNode.Node<A, LayerError>>>
|
||||
type DependentNodeError = Assert<Equal<typeof bWithFailingA, LayerNode.Node<B, LayerError>>>
|
||||
void (0 as unknown as ANodeProvides)
|
||||
void (0 as unknown as BNodeProvides)
|
||||
void (0 as unknown as CNodeProvides)
|
||||
void (0 as unknown as FailingANodeError)
|
||||
void (0 as unknown as DependentNodeError)
|
||||
|
||||
const closed = LayerNode.buildLayer(c)
|
||||
const closedWithError = LayerNode.buildLayer(bWithFailingA)
|
||||
type ClosedProvides = Assert<Equal<Layer.Success<typeof closed>, C>>
|
||||
type ClosedRequires = Assert<Equal<Layer.Services<typeof closed>, never>>
|
||||
type ClosedError = Assert<Equal<Layer.Error<typeof closedWithError>, LayerError>>
|
||||
void (0 as unknown as ClosedProvides)
|
||||
void (0 as unknown as ClosedRequires)
|
||||
void (0 as unknown as ClosedError)
|
||||
|
||||
const replacement = LayerNode.make(Layer.succeed(A, A.of({ value: "a" })), [])
|
||||
LayerNode.replace(a, Layer.succeed(A, A.of({ value: "a" })))
|
||||
LayerNode.replace(notFoundOrDiskA, notFoundAImplementation)
|
||||
LayerNode.replace(notFoundOrDiskA, diskAImplementation)
|
||||
LayerNode.replaceWithNode(a, replacement)
|
||||
|
||||
// @ts-expect-error An override for A must still provide A
|
||||
LayerNode.replaceWithNode(a, b)
|
||||
|
||||
// @ts-expect-error A replacement cannot introduce NetworkError
|
||||
LayerNode.replace(notFoundOrDiskA, networkAImplementation)
|
||||
|
||||
// @ts-expect-error A replacement layer must not have unresolved dependencies
|
||||
LayerNode.replace(b, bImplementation)
|
||||
|
||||
test("type exploration compiles", () => {})
|
||||
204
packages/opencode/test/effect/app-graph.test.ts
Normal file
204
packages/opencode/test/effect/app-graph.test.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Cause, Context, Effect, Exit, Layer } from "effect"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
|
||||
const { buildLayer: build, group, replace, replaceWithNode } = LayerNode
|
||||
const node = LayerNode.make
|
||||
|
||||
class Value extends Context.Service<Value, { readonly value: string }>()("test/Value") {}
|
||||
class Greeting extends Context.Service<Greeting, { readonly text: string }>()("test/Greeting") {}
|
||||
|
||||
const value = LayerNode.make(Layer.succeed(Value, Value.of({ value: "production" })), [])
|
||||
const greetingImplementation = Layer.effect(
|
||||
Greeting,
|
||||
Effect.gen(function* () {
|
||||
return Greeting.of({ text: `hello ${(yield* Value).value}` })
|
||||
}),
|
||||
)
|
||||
const greeting = LayerNode.make(greetingImplementation, [value])
|
||||
|
||||
// @ts-expect-error Greeting requires Value
|
||||
LayerNode.make(greetingImplementation, [])
|
||||
|
||||
describe("app graph", () => {
|
||||
test("creates any selected dependency layer", async () => {
|
||||
const result = Effect.gen(function* () {
|
||||
return (yield* Greeting).text
|
||||
}).pipe(Effect.provide(build(greeting)))
|
||||
|
||||
expect(await Effect.runPromise(result)).toBe("hello production")
|
||||
})
|
||||
|
||||
test("applies overrides before dependency materialization", async () => {
|
||||
const replacement = Layer.succeed(Value, Value.of({ value: "simulation" }))
|
||||
const graph = build(greeting, { replacements: [replace(value, replacement)] })
|
||||
const result = Effect.gen(function* () {
|
||||
return (yield* Greeting).text
|
||||
}).pipe(Effect.provide(graph))
|
||||
|
||||
expect(await Effect.runPromise(result)).toBe("hello simulation")
|
||||
})
|
||||
|
||||
test("acquires a shared dependency once", async () => {
|
||||
class Shared extends Context.Service<Shared, { readonly value: string }>()("test/Shared") {}
|
||||
class Left extends Context.Service<Left, { readonly value: string }>()("test/Left") {}
|
||||
class Right extends Context.Service<Right, { readonly value: string }>()("test/Right") {}
|
||||
let acquisitions = 0
|
||||
const shared = node(
|
||||
Layer.effect(
|
||||
Shared,
|
||||
Effect.sync(() => {
|
||||
acquisitions++
|
||||
return Shared.of({ value: "shared" })
|
||||
}),
|
||||
),
|
||||
[],
|
||||
)
|
||||
const left = node(
|
||||
Layer.effect(
|
||||
Left,
|
||||
Effect.gen(function* () {
|
||||
return Left.of({ value: `${(yield* Shared).value}-left` })
|
||||
}),
|
||||
),
|
||||
[shared],
|
||||
)
|
||||
const right = node(
|
||||
Layer.effect(
|
||||
Right,
|
||||
Effect.gen(function* () {
|
||||
return Right.of({ value: `${(yield* Shared).value}-right` })
|
||||
}),
|
||||
),
|
||||
[shared],
|
||||
)
|
||||
|
||||
const result = Effect.gen(function* () {
|
||||
return [(yield* Left).value, (yield* Right).value]
|
||||
}).pipe(Effect.provide(build(group([left, right]))))
|
||||
|
||||
expect(await Effect.runPromise(result)).toEqual(["shared-left", "shared-right"])
|
||||
expect(acquisitions).toBe(1)
|
||||
})
|
||||
|
||||
test("applies a replacement to every transitive consumer", async () => {
|
||||
class Left extends Context.Service<Left, { readonly value: string }>()("test/ReplacementLeft") {}
|
||||
class Right extends Context.Service<Right, { readonly value: string }>()("test/ReplacementRight") {}
|
||||
const left = node(
|
||||
Layer.effect(
|
||||
Left,
|
||||
Effect.gen(function* () {
|
||||
return Left.of({ value: (yield* Value).value })
|
||||
}),
|
||||
),
|
||||
[value],
|
||||
)
|
||||
const right = node(
|
||||
Layer.effect(
|
||||
Right,
|
||||
Effect.gen(function* () {
|
||||
return Right.of({ value: (yield* Value).value })
|
||||
}),
|
||||
),
|
||||
[value],
|
||||
)
|
||||
const replacement = Layer.succeed(Value, Value.of({ value: "simulation" }))
|
||||
const graph = build(group([left, right]), { replacements: [replace(value, replacement)] })
|
||||
|
||||
const result = Effect.gen(function* () {
|
||||
return [(yield* Left).value, (yield* Right).value]
|
||||
}).pipe(Effect.provide(graph))
|
||||
|
||||
expect(await Effect.runPromise(result)).toEqual(["simulation", "simulation"])
|
||||
})
|
||||
|
||||
test("propagates layer acquisition errors", async () => {
|
||||
class AcquisitionError {
|
||||
readonly _tag = "AcquisitionError"
|
||||
}
|
||||
const failing = node(Layer.effect(Value, Effect.fail(new AcquisitionError())), [])
|
||||
const exit = await Effect.runPromiseExit(Effect.provide(Value, build(failing)))
|
||||
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(AcquisitionError)
|
||||
})
|
||||
|
||||
test("groups expose every selected service", async () => {
|
||||
class Count extends Context.Service<Count, { readonly value: number }>()("test/Count") {}
|
||||
const count = node(Layer.succeed(Count, Count.of({ value: 3 })), [])
|
||||
const result = Effect.gen(function* () {
|
||||
return { text: (yield* Value).value, count: (yield* Count).value }
|
||||
}).pipe(Effect.provide(build(group([value, count]))))
|
||||
|
||||
expect(await Effect.runPromise(result)).toEqual({ text: "production", count: 3 })
|
||||
})
|
||||
|
||||
test("builds an empty group", async () => {
|
||||
expect(await Effect.runPromise(Effect.succeed("ok").pipe(Effect.provide(build(group([])))))).toBe("ok")
|
||||
})
|
||||
|
||||
test("builds replacements with their own dependencies", async () => {
|
||||
class ReplacementConfig extends Context.Service<ReplacementConfig, { readonly value: string }>()(
|
||||
"test/ReplacementConfig",
|
||||
) {}
|
||||
const replacementConfig = node(Layer.succeed(ReplacementConfig, ReplacementConfig.of({ value: "replacement" })), [])
|
||||
const replacement = node(
|
||||
Layer.effect(
|
||||
Value,
|
||||
Effect.gen(function* () {
|
||||
return Value.of({ value: (yield* ReplacementConfig).value })
|
||||
}),
|
||||
),
|
||||
[replacementConfig],
|
||||
)
|
||||
const result = Effect.gen(function* () {
|
||||
return (yield* Greeting).text
|
||||
}).pipe(Effect.provide(build(greeting, { replacements: [replaceWithNode(value, replacement)] })))
|
||||
|
||||
expect(await Effect.runPromise(result)).toBe("hello replacement")
|
||||
})
|
||||
|
||||
test("does not acquire unreachable replacements", async () => {
|
||||
let acquisitions = 0
|
||||
const unreachable = node(Layer.succeed(Value, Value.of({ value: "unreachable" })), [])
|
||||
const replacement = Layer.effect(
|
||||
Value,
|
||||
Effect.sync(() => {
|
||||
acquisitions++
|
||||
return Value.of({ value: "replacement" })
|
||||
}),
|
||||
)
|
||||
|
||||
await Effect.runPromise(
|
||||
Effect.provide(Greeting, build(greeting, { replacements: [replace(unreachable, replacement)] })),
|
||||
)
|
||||
|
||||
expect(acquisitions).toBe(0)
|
||||
})
|
||||
|
||||
test("rejects a direct cycle", () => {
|
||||
const cyclic = node(Layer.succeed(Value, Value.of({ value: "cyclic" })), [])
|
||||
;(cyclic.dependencies as LayerNode.Node<unknown, unknown>[]).push(cyclic)
|
||||
|
||||
expect(() => build(cyclic)).toThrow("Cycle detected in app graph: layer#1 -> layer#1")
|
||||
})
|
||||
|
||||
test("rejects an indirect cycle", () => {
|
||||
const first = node(Layer.succeed(Value, Value.of({ value: "first" })), [])
|
||||
const second = node(Layer.succeed(Value, Value.of({ value: "second" })), [first])
|
||||
const third = node(Layer.succeed(Value, Value.of({ value: "third" })), [second])
|
||||
;(first.dependencies as LayerNode.Node<unknown, unknown>[]).push(third)
|
||||
|
||||
expect(() => build(first)).toThrow("Cycle detected in app graph: layer#1 -> layer#2 -> layer#3 -> layer#1")
|
||||
})
|
||||
|
||||
test("rejects a cycle introduced by a replacement", () => {
|
||||
const replacement = node(Layer.succeed(Value, Value.of({ value: "replacement" })), [])
|
||||
const consumer = node(greetingImplementation, [value])
|
||||
;(replacement.dependencies as LayerNode.Node<unknown, unknown>[]).push(consumer)
|
||||
|
||||
expect(() => build(consumer, { replacements: [replaceWithNode(value, replacement)] })).toThrow(
|
||||
"Cycle detected in app graph: layer#1 -> layer#2 -> layer#1",
|
||||
)
|
||||
})
|
||||
})
|
||||
99
packages/opencode/test/effect/app-runtime-logger.test.ts
Normal file
99
packages/opencode/test/effect/app-runtime-logger.test.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { expect } from "bun:test"
|
||||
import { Context, Deferred, Effect, Fiber, Layer, Logger } from "effect"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { AppLayer } from "../../src/effect/app-runtime"
|
||||
import { EffectBridge } from "@/effect/bridge"
|
||||
import { InstanceRef } from "../../src/effect/instance-ref"
|
||||
import * as Observability from "@opencode-ai/core/observability"
|
||||
import { attach } from "../../src/effect/run-service"
|
||||
import { TestInstance } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const it = testEffect(CrossSpawnSpawner.defaultLayer)
|
||||
|
||||
function check(loggers: ReadonlySet<Logger.Logger<unknown, any>>) {
|
||||
return {
|
||||
tracerLogger: loggers.has(Logger.tracerLogger),
|
||||
size: loggers.size,
|
||||
}
|
||||
}
|
||||
|
||||
it.live("makeRuntime installs the observability logger", () =>
|
||||
Effect.gen(function* () {
|
||||
class Dummy extends Context.Service<Dummy, { readonly current: () => Effect.Effect<ReturnType<typeof check>> }>()(
|
||||
"@test/Dummy",
|
||||
) {}
|
||||
|
||||
const layer = Layer.effect(
|
||||
Dummy,
|
||||
Effect.gen(function* () {
|
||||
return Dummy.of({
|
||||
current: () => Effect.map(Effect.service(Logger.CurrentLoggers), check),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
const current = yield* Dummy.use((svc) => svc.current()).pipe(
|
||||
Effect.provide(Layer.provideMerge(layer, Observability.layer)),
|
||||
)
|
||||
|
||||
expect(current.size).toBeGreaterThan(0)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("AppLayer also installs the observability logger", () =>
|
||||
Effect.gen(function* () {
|
||||
const current = yield* Effect.map(Effect.service(Logger.CurrentLoggers), check).pipe(Effect.provide(AppLayer))
|
||||
|
||||
expect(current.size).toBeGreaterThan(0)
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance(
|
||||
"attach preserves InstanceRef from the current fiber context",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* TestInstance
|
||||
const current = yield* attach(
|
||||
Effect.gen(function* () {
|
||||
return (yield* InstanceRef)?.directory
|
||||
}),
|
||||
)
|
||||
|
||||
expect(current).toBe(test.directory)
|
||||
}),
|
||||
{ git: true },
|
||||
)
|
||||
|
||||
it.instance(
|
||||
"EffectBridge preserves logger and instance context across async boundaries",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* TestInstance
|
||||
const bridge = yield* EffectBridge.make()
|
||||
const started = yield* Deferred.make<void>()
|
||||
|
||||
const fiber = yield* Effect.gen(function* () {
|
||||
yield* Deferred.succeed(started, undefined)
|
||||
return yield* Effect.promise(() =>
|
||||
Promise.resolve().then(() =>
|
||||
bridge.promise(
|
||||
Effect.gen(function* () {
|
||||
return {
|
||||
directory: (yield* InstanceRef)?.directory,
|
||||
...check(yield* Effect.service(Logger.CurrentLoggers)),
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}).pipe(Effect.forkScoped)
|
||||
|
||||
yield* Deferred.await(started)
|
||||
const result = yield* Fiber.join(fiber)
|
||||
|
||||
expect(result.directory).toBe(test.directory)
|
||||
expect(result.size).toBeGreaterThan(0)
|
||||
}).pipe(Effect.provide(Observability.layer)),
|
||||
{ git: true },
|
||||
)
|
||||
65
packages/opencode/test/effect/config-service.test.ts
Normal file
65
packages/opencode/test/effect/config-service.test.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Config, ConfigProvider, Context, Effect, Layer, Option } from "effect"
|
||||
import { ConfigService } from "../../src/effect/config-service"
|
||||
import { it } from "../lib/effect"
|
||||
|
||||
class TestConfig extends ConfigService.Service<TestConfig>()("@test/ConfigService", {
|
||||
name: Config.string("NAME"),
|
||||
token: Config.string("TOKEN").pipe(Config.option),
|
||||
port: Config.number("PORT").pipe(Config.withDefault(3000)),
|
||||
}) {}
|
||||
|
||||
const fromConfig = (input: Record<string, unknown>) =>
|
||||
TestConfig.defaultLayer.pipe(Layer.provide(ConfigProvider.layer(ConfigProvider.fromUnknown(input))))
|
||||
|
||||
const readConfig = TestConfig.useSync((config) => config)
|
||||
|
||||
describe("ConfigService", () => {
|
||||
it.effect("defaultLayer parses values from the active ConfigProvider", () =>
|
||||
Effect.gen(function* () {
|
||||
const config = yield* readConfig.pipe(
|
||||
Effect.provide(
|
||||
fromConfig({
|
||||
NAME: "kit",
|
||||
TOKEN: "secret",
|
||||
PORT: "4096",
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
expect(config.name).toBe("kit")
|
||||
expect(config.token).toEqual(Option.some("secret"))
|
||||
expect(config.port).toBe(4096)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("defaultLayer applies Effect Config defaults", () =>
|
||||
Effect.gen(function* () {
|
||||
const config = yield* readConfig.pipe(Effect.provide(fromConfig({ NAME: "kit" })))
|
||||
|
||||
expect(config.name).toBe("kit")
|
||||
expect(config.token).toEqual(Option.none())
|
||||
expect(config.port).toBe(3000)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("layer provides an already parsed service value", () =>
|
||||
Effect.gen(function* () {
|
||||
const config = yield* readConfig.pipe(
|
||||
Effect.provide(
|
||||
TestConfig.layer({
|
||||
name: "direct",
|
||||
token: Option.some("parsed"),
|
||||
port: 9000,
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
expect(config).toEqual({
|
||||
name: "direct",
|
||||
token: Option.some("parsed"),
|
||||
port: 9000,
|
||||
} satisfies Context.Service.Shape<typeof TestConfig>)
|
||||
}),
|
||||
)
|
||||
})
|
||||
391
packages/opencode/test/effect/instance-state.test.ts
Normal file
391
packages/opencode/test/effect/instance-state.test.ts
Normal file
@@ -0,0 +1,391 @@
|
||||
import { expect } from "bun:test"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { $ } from "bun"
|
||||
import { Context, Deferred, Duration, Effect, Exit, Fiber, Layer } from "effect"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import {
|
||||
disposeAllInstancesEffect,
|
||||
provideInstanceEffect,
|
||||
reloadInstance,
|
||||
testInstanceStoreLayer,
|
||||
tmpdirScoped,
|
||||
} from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const it = testEffect(Layer.mergeAll(CrossSpawnSpawner.defaultLayer, testInstanceStoreLayer))
|
||||
|
||||
const access = <A, E>(state: InstanceState.InstanceState<A, E>, dir: string) =>
|
||||
InstanceState.get(state).pipe(provideInstanceEffect(dir))
|
||||
|
||||
const tmpdirGitScoped = Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped({ git: true })
|
||||
yield* Effect.promise(() => $`git commit --allow-empty --amend -m ${`root commit ${dir}`}`.cwd(dir).quiet())
|
||||
return dir
|
||||
})
|
||||
|
||||
it.live("InstanceState caches values per directory", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
let n = 0
|
||||
const state = yield* InstanceState.make(() => Effect.sync(() => ({ n: ++n })))
|
||||
|
||||
const a = yield* access(state, dir)
|
||||
const b = yield* access(state, dir)
|
||||
|
||||
expect(a).toBe(b)
|
||||
expect(n).toBe(1)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("InstanceState isolates directories", () =>
|
||||
Effect.gen(function* () {
|
||||
const one = yield* tmpdirScoped()
|
||||
const two = yield* tmpdirScoped()
|
||||
let n = 0
|
||||
const state = yield* InstanceState.make((dir) => Effect.sync(() => ({ dir, n: ++n })))
|
||||
|
||||
const a = yield* access(state, one)
|
||||
const b = yield* access(state, two)
|
||||
const c = yield* access(state, one)
|
||||
|
||||
expect(a).toBe(c)
|
||||
expect(a).not.toBe(b)
|
||||
expect(n).toBe(2)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("InstanceState invalidates on reload", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
const seen: string[] = []
|
||||
let n = 0
|
||||
const state = yield* InstanceState.make(() =>
|
||||
Effect.acquireRelease(
|
||||
Effect.sync(() => ({ n: ++n })),
|
||||
(value) =>
|
||||
Effect.sync(() => {
|
||||
seen.push(String(value.n))
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
const a = yield* access(state, dir)
|
||||
yield* reloadInstance({ directory: dir })
|
||||
const b = yield* access(state, dir)
|
||||
|
||||
expect(a).not.toBe(b)
|
||||
expect(seen).toEqual(["1"])
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("InstanceState invalidates on disposeAll", () =>
|
||||
Effect.gen(function* () {
|
||||
const one = yield* tmpdirScoped()
|
||||
const two = yield* tmpdirScoped()
|
||||
const seen: string[] = []
|
||||
const state = yield* InstanceState.make((ctx) =>
|
||||
Effect.acquireRelease(
|
||||
Effect.sync(() => ({ dir: ctx.directory })),
|
||||
(value) =>
|
||||
Effect.sync(() => {
|
||||
seen.push(value.dir)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
yield* access(state, one)
|
||||
yield* access(state, two)
|
||||
yield* disposeAllInstancesEffect
|
||||
|
||||
expect(seen.sort()).toEqual([one, two].sort())
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("InstanceState.get reads the current directory lazily", () =>
|
||||
Effect.gen(function* () {
|
||||
const one = yield* tmpdirScoped()
|
||||
const two = yield* tmpdirScoped()
|
||||
|
||||
interface Api {
|
||||
readonly get: () => Effect.Effect<string>
|
||||
}
|
||||
|
||||
class Test extends Context.Service<Test, Api>()("@test/InstanceStateLazy") {
|
||||
static readonly layer = Layer.effect(
|
||||
Test,
|
||||
Effect.gen(function* () {
|
||||
const state = yield* InstanceState.make((ctx) => Effect.sync(() => ctx.directory))
|
||||
const get = InstanceState.get(state)
|
||||
|
||||
return Test.of({
|
||||
get: Effect.fn("Test.get")(function* () {
|
||||
return yield* get
|
||||
}),
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
yield* Effect.gen(function* () {
|
||||
const a = yield* Test.use((svc) => svc.get()).pipe(provideInstanceEffect(one))
|
||||
const b = yield* Test.use((svc) => svc.get()).pipe(provideInstanceEffect(two))
|
||||
|
||||
expect(a).toBe(one)
|
||||
expect(b).toBe(two)
|
||||
}).pipe(Effect.provide(Test.layer))
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("InstanceState preserves directory across async boundaries", () =>
|
||||
Effect.gen(function* () {
|
||||
const one = yield* tmpdirGitScoped
|
||||
const two = yield* tmpdirGitScoped
|
||||
const three = yield* tmpdirGitScoped
|
||||
|
||||
interface Api {
|
||||
readonly get: () => Effect.Effect<{ directory: string; worktree: string; project: string }>
|
||||
}
|
||||
|
||||
class Test extends Context.Service<Test, Api>()("@test/InstanceStateAsync") {
|
||||
static readonly layer = Layer.effect(
|
||||
Test,
|
||||
Effect.gen(function* () {
|
||||
const state = yield* InstanceState.make((ctx) =>
|
||||
Effect.sync(() => ({
|
||||
directory: ctx.directory,
|
||||
worktree: ctx.worktree,
|
||||
project: ctx.project.id,
|
||||
})),
|
||||
)
|
||||
|
||||
return Test.of({
|
||||
get: Effect.fn("Test.get")(function* () {
|
||||
yield* Effect.sleep(Duration.millis(1))
|
||||
yield* Effect.sleep(Duration.millis(1))
|
||||
for (let i = 0; i < 100; i++) {
|
||||
yield* Effect.yieldNow
|
||||
}
|
||||
for (let i = 0; i < 100; i++) {
|
||||
yield* Effect.promise(() => Promise.resolve())
|
||||
}
|
||||
yield* Effect.sleep(Duration.millis(2))
|
||||
yield* Effect.sleep(Duration.millis(1))
|
||||
return yield* InstanceState.get(state)
|
||||
}),
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
yield* Effect.gen(function* () {
|
||||
const [a, b, c] = yield* Effect.all(
|
||||
[one, two, three].map((dir) => Test.use((svc) => svc.get()).pipe(provideInstanceEffect(dir))),
|
||||
{ concurrency: "unbounded" },
|
||||
)
|
||||
|
||||
expect(a).toEqual({ directory: one, worktree: one, project: a.project })
|
||||
expect(b).toEqual({ directory: two, worktree: two, project: b.project })
|
||||
expect(c).toEqual({ directory: three, worktree: three, project: c.project })
|
||||
expect(a.project).not.toBe(b.project)
|
||||
expect(a.project).not.toBe(c.project)
|
||||
expect(b.project).not.toBe(c.project)
|
||||
}).pipe(Effect.provide(Test.layer))
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("InstanceState survives high-contention concurrent access", () =>
|
||||
Effect.gen(function* () {
|
||||
const dirs = yield* Effect.all(
|
||||
Array.from({ length: 20 }, () => tmpdirScoped()),
|
||||
{ concurrency: "unbounded" },
|
||||
)
|
||||
|
||||
interface Api {
|
||||
readonly get: () => Effect.Effect<string>
|
||||
}
|
||||
|
||||
class Test extends Context.Service<Test, Api>()("@test/HighContention") {
|
||||
static readonly layer = Layer.effect(
|
||||
Test,
|
||||
Effect.gen(function* () {
|
||||
const state = yield* InstanceState.make((ctx) => Effect.sync(() => ctx.directory))
|
||||
|
||||
return Test.of({
|
||||
get: Effect.fn("Test.get")(function* () {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
yield* Effect.sleep(Duration.millis(Math.random() * 3))
|
||||
yield* Effect.yieldNow
|
||||
yield* Effect.promise(() => Promise.resolve())
|
||||
}
|
||||
return yield* InstanceState.get(state)
|
||||
}),
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
yield* Effect.gen(function* () {
|
||||
const results = yield* Effect.all(
|
||||
dirs.map((dir) => Test.use((svc) => svc.get()).pipe(provideInstanceEffect(dir))),
|
||||
{ concurrency: "unbounded" },
|
||||
)
|
||||
|
||||
expect(results).toEqual(dirs)
|
||||
}).pipe(Effect.provide(Test.layer))
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("InstanceState correct after interleaved init and dispose", () =>
|
||||
Effect.gen(function* () {
|
||||
const one = yield* tmpdirScoped()
|
||||
const two = yield* tmpdirScoped()
|
||||
|
||||
interface Api {
|
||||
readonly get: () => Effect.Effect<string>
|
||||
}
|
||||
|
||||
class Test extends Context.Service<Test, Api>()("@test/InterleavedDispose") {
|
||||
static readonly layer = Layer.effect(
|
||||
Test,
|
||||
Effect.gen(function* () {
|
||||
const state = yield* InstanceState.make((ctx) =>
|
||||
Effect.gen(function* () {
|
||||
yield* Effect.sleep(Duration.millis(5))
|
||||
return ctx.directory
|
||||
}),
|
||||
)
|
||||
|
||||
return Test.of({
|
||||
get: Effect.fn("Test.get")(function* () {
|
||||
return yield* InstanceState.get(state)
|
||||
}),
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
yield* Effect.gen(function* () {
|
||||
const a = yield* Test.use((svc) => svc.get()).pipe(provideInstanceEffect(one))
|
||||
expect(a).toBe(one)
|
||||
|
||||
const [, b] = yield* Effect.all(
|
||||
[reloadInstance({ directory: one }), Test.use((svc) => svc.get()).pipe(provideInstanceEffect(two))],
|
||||
{ concurrency: "unbounded" },
|
||||
)
|
||||
expect(b).toBe(two)
|
||||
|
||||
const c = yield* Test.use((svc) => svc.get()).pipe(provideInstanceEffect(one))
|
||||
expect(c).toBe(one)
|
||||
}).pipe(Effect.provide(Test.layer))
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("InstanceState mutation in one directory does not leak to another", () =>
|
||||
Effect.gen(function* () {
|
||||
const one = yield* tmpdirScoped()
|
||||
const two = yield* tmpdirScoped()
|
||||
const state = yield* InstanceState.make(() => Effect.sync(() => ({ count: 0 })))
|
||||
|
||||
const s1 = yield* access(state, one)
|
||||
s1.count = 42
|
||||
|
||||
const s2 = yield* access(state, two)
|
||||
expect(s2.count).toBe(0)
|
||||
|
||||
const s1again = yield* access(state, one)
|
||||
expect(s1again.count).toBe(42)
|
||||
expect(s1again).toBe(s1)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("InstanceState dedupes concurrent lookups", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
let n = 0
|
||||
const state = yield* InstanceState.make(() =>
|
||||
Effect.gen(function* () {
|
||||
n += 1
|
||||
yield* Effect.sleep(Duration.millis(10))
|
||||
return { n }
|
||||
}),
|
||||
)
|
||||
|
||||
const [a, b] = yield* Effect.all([access(state, dir), access(state, dir)], { concurrency: "unbounded" })
|
||||
expect(a).toBe(b)
|
||||
expect(n).toBe(1)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("InstanceState survives deferred resume from the same instance context", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped({ git: true })
|
||||
|
||||
interface Api {
|
||||
readonly get: (gate: Deferred.Deferred<void>) => Effect.Effect<string>
|
||||
}
|
||||
|
||||
class Test extends Context.Service<Test, Api>()("@test/DeferredResume") {
|
||||
static readonly layer = Layer.effect(
|
||||
Test,
|
||||
Effect.gen(function* () {
|
||||
const state = yield* InstanceState.make((ctx) => Effect.sync(() => ctx.directory))
|
||||
|
||||
return Test.of({
|
||||
get: Effect.fn("Test.get")(function* (gate: Deferred.Deferred<void>) {
|
||||
yield* Deferred.await(gate)
|
||||
return yield* InstanceState.get(state)
|
||||
}),
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
yield* Effect.gen(function* () {
|
||||
const gate = yield* Deferred.make<void>()
|
||||
const fiber = yield* Test.use((svc) => svc.get(gate)).pipe(provideInstanceEffect(dir), Effect.forkScoped)
|
||||
|
||||
yield* Deferred.succeed(gate, undefined).pipe(provideInstanceEffect(dir))
|
||||
const exit = yield* Fiber.await(fiber)
|
||||
|
||||
expect(Exit.isSuccess(exit)).toBe(true)
|
||||
if (Exit.isSuccess(exit)) expect(exit.value).toBe(dir)
|
||||
}).pipe(Effect.provide(Test.layer))
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("InstanceState survives deferred resume outside ALS when InstanceRef is set", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped({ git: true })
|
||||
|
||||
interface Api {
|
||||
readonly get: (gate: Deferred.Deferred<void>) => Effect.Effect<string>
|
||||
}
|
||||
|
||||
class Test extends Context.Service<Test, Api>()("@test/DeferredResumeOutside") {
|
||||
static readonly layer = Layer.effect(
|
||||
Test,
|
||||
Effect.gen(function* () {
|
||||
const state = yield* InstanceState.make((ctx) => Effect.sync(() => ctx.directory))
|
||||
|
||||
return Test.of({
|
||||
get: Effect.fn("Test.get")(function* (gate: Deferred.Deferred<void>) {
|
||||
yield* Deferred.await(gate)
|
||||
return yield* InstanceState.get(state)
|
||||
}),
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
yield* Effect.gen(function* () {
|
||||
const gate = yield* Deferred.make<void>()
|
||||
const fiber = yield* Test.use((svc) => svc.get(gate)).pipe(provideInstanceEffect(dir), Effect.forkScoped)
|
||||
|
||||
yield* Deferred.succeed(gate, undefined)
|
||||
const exit = yield* Fiber.await(fiber)
|
||||
|
||||
expect(Exit.isSuccess(exit)).toBe(true)
|
||||
if (Exit.isSuccess(exit)) expect(exit.value).toBe(dir)
|
||||
}).pipe(Effect.provide(Test.layer))
|
||||
}),
|
||||
)
|
||||
89
packages/opencode/test/effect/run-service.test.ts
Normal file
89
packages/opencode/test/effect/run-service.test.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { expect } from "bun:test"
|
||||
import { Effect, Layer, Context } from "effect"
|
||||
import { InstanceRef } from "../../src/effect/instance-ref"
|
||||
import { makeRuntime } from "../../src/effect/run-service"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { it } from "../lib/effect"
|
||||
|
||||
class Shared extends Context.Service<Shared, { readonly id: number }>()("@test/Shared") {}
|
||||
const testDirectory = "/tmp/opencode-test"
|
||||
|
||||
it.live("makeRuntime shares dependent layers through the shared memo map", () =>
|
||||
Effect.gen(function* () {
|
||||
let n = 0
|
||||
|
||||
const shared = Layer.effect(
|
||||
Shared,
|
||||
Effect.sync(() => {
|
||||
n += 1
|
||||
return Shared.of({ id: n })
|
||||
}),
|
||||
)
|
||||
|
||||
class One extends Context.Service<One, { readonly get: () => Effect.Effect<number> }>()("@test/One") {}
|
||||
const one = Layer.effect(
|
||||
One,
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* Shared
|
||||
return One.of({
|
||||
get: Effect.fn("One.get")(() => Effect.succeed(svc.id)),
|
||||
})
|
||||
}),
|
||||
).pipe(Layer.provide(shared))
|
||||
|
||||
class Two extends Context.Service<Two, { readonly get: () => Effect.Effect<number> }>()("@test/Two") {}
|
||||
const two = Layer.effect(
|
||||
Two,
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* Shared
|
||||
return Two.of({
|
||||
get: Effect.fn("Two.get")(() => Effect.succeed(svc.id)),
|
||||
})
|
||||
}),
|
||||
).pipe(Layer.provide(shared))
|
||||
|
||||
const { runPromise: runOne } = makeRuntime(One, one)
|
||||
const { runPromise: runTwo } = makeRuntime(Two, two)
|
||||
|
||||
expect(yield* Effect.promise(() => runOne((svc) => svc.get()))).toBe(1)
|
||||
expect(yield* Effect.promise(() => runTwo((svc) => svc.get()))).toBe(1)
|
||||
expect(n).toBe(1)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("makeRuntime inherits InstanceRef from the current fiber", () =>
|
||||
Effect.gen(function* () {
|
||||
class NeedsInstance extends Context.Service<
|
||||
NeedsInstance,
|
||||
{ readonly directory: () => Effect.Effect<string | undefined> }
|
||||
>()("@test/NeedsInstance") {}
|
||||
|
||||
const runtime = makeRuntime(
|
||||
NeedsInstance,
|
||||
Layer.succeed(
|
||||
NeedsInstance,
|
||||
NeedsInstance.of({
|
||||
directory: () =>
|
||||
Effect.gen(function* () {
|
||||
return (yield* InstanceRef)?.directory
|
||||
}),
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
const actual = yield* Effect.promise(() => runtime.runPromise((svc) => svc.directory()))
|
||||
|
||||
expect(actual).toBe(testDirectory)
|
||||
}).pipe(
|
||||
Effect.provideService(InstanceRef, {
|
||||
directory: testDirectory,
|
||||
worktree: testDirectory,
|
||||
project: {
|
||||
id: ProjectV2.ID.global,
|
||||
worktree: testDirectory,
|
||||
time: { created: 0, updated: 0 },
|
||||
sandboxes: [],
|
||||
},
|
||||
}),
|
||||
),
|
||||
)
|
||||
514
packages/opencode/test/effect/runner.test.ts
Normal file
514
packages/opencode/test/effect/runner.test.ts
Normal file
@@ -0,0 +1,514 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Cause, Deferred, Effect, Exit, Fiber, Latch, Ref, Scope } from "effect"
|
||||
import { Runner } from "@/effect/runner"
|
||||
import { it } from "../lib/effect"
|
||||
|
||||
const waitForState = <A, E>(runner: Runner.Runner<A, E>, tag: Runner.State<A, E>["_tag"]) =>
|
||||
Effect.gen(function* () {
|
||||
while (runner.state._tag !== tag) yield* Effect.yieldNow
|
||||
}).pipe(Effect.timeout("1 second"))
|
||||
|
||||
describe("Runner", () => {
|
||||
// --- ensureRunning semantics ---
|
||||
|
||||
it.live(
|
||||
"ensureRunning starts work and returns result",
|
||||
Effect.gen(function* () {
|
||||
const s = yield* Scope.Scope
|
||||
const runner = Runner.make<string>(s)
|
||||
const result = yield* runner.ensureRunning(Effect.succeed("hello"))
|
||||
expect(result).toBe("hello")
|
||||
expect(runner.state._tag).toBe("Idle")
|
||||
expect(runner.busy).toBe(false)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live(
|
||||
"ensureRunning propagates work failures",
|
||||
Effect.gen(function* () {
|
||||
const s = yield* Scope.Scope
|
||||
const runner = Runner.make<string, string>(s)
|
||||
const exit = yield* runner.ensureRunning(Effect.fail("boom")).pipe(Effect.exit)
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
expect(runner.state._tag).toBe("Idle")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live(
|
||||
"concurrent callers share the same run",
|
||||
Effect.gen(function* () {
|
||||
const s = yield* Scope.Scope
|
||||
const runner = Runner.make<string>(s)
|
||||
const calls = yield* Ref.make(0)
|
||||
const work = Effect.gen(function* () {
|
||||
yield* Ref.update(calls, (n) => n + 1)
|
||||
yield* Effect.sleep("10 millis")
|
||||
return "shared"
|
||||
})
|
||||
|
||||
const [a, b] = yield* Effect.all([runner.ensureRunning(work), runner.ensureRunning(work)], {
|
||||
concurrency: "unbounded",
|
||||
})
|
||||
|
||||
expect(a).toBe("shared")
|
||||
expect(b).toBe("shared")
|
||||
expect(yield* Ref.get(calls)).toBe(1)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live(
|
||||
"concurrent callers all receive same error",
|
||||
Effect.gen(function* () {
|
||||
const s = yield* Scope.Scope
|
||||
const runner = Runner.make<string, string>(s)
|
||||
const work = Effect.gen(function* () {
|
||||
yield* Effect.sleep("10 millis")
|
||||
return yield* Effect.fail("boom")
|
||||
})
|
||||
|
||||
const [a, b] = yield* Effect.all(
|
||||
[runner.ensureRunning(work).pipe(Effect.exit), runner.ensureRunning(work).pipe(Effect.exit)],
|
||||
{ concurrency: "unbounded" },
|
||||
)
|
||||
|
||||
expect(Exit.isFailure(a)).toBe(true)
|
||||
expect(Exit.isFailure(b)).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live(
|
||||
"ensureRunning can be called again after previous run completes",
|
||||
Effect.gen(function* () {
|
||||
const s = yield* Scope.Scope
|
||||
const runner = Runner.make<string>(s)
|
||||
expect(yield* runner.ensureRunning(Effect.succeed("first"))).toBe("first")
|
||||
expect(yield* runner.ensureRunning(Effect.succeed("second"))).toBe("second")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live(
|
||||
"second ensureRunning ignores new work if already running",
|
||||
Effect.gen(function* () {
|
||||
const s = yield* Scope.Scope
|
||||
const runner = Runner.make<string>(s)
|
||||
const ran = yield* Ref.make<string[]>([])
|
||||
|
||||
const first = Effect.gen(function* () {
|
||||
yield* Ref.update(ran, (a) => [...a, "first"])
|
||||
yield* Effect.sleep("50 millis")
|
||||
return "first-result"
|
||||
})
|
||||
const second = Effect.gen(function* () {
|
||||
yield* Ref.update(ran, (a) => [...a, "second"])
|
||||
return "second-result"
|
||||
})
|
||||
|
||||
const [a, b] = yield* Effect.all([runner.ensureRunning(first), runner.ensureRunning(second)], {
|
||||
concurrency: "unbounded",
|
||||
})
|
||||
|
||||
expect(a).toBe("first-result")
|
||||
expect(b).toBe("first-result")
|
||||
expect(yield* Ref.get(ran)).toEqual(["first"])
|
||||
}),
|
||||
)
|
||||
|
||||
// --- cancel semantics ---
|
||||
|
||||
it.live(
|
||||
"cancel interrupts running work",
|
||||
Effect.gen(function* () {
|
||||
const s = yield* Scope.Scope
|
||||
const runner = Runner.make<string>(s)
|
||||
const started = yield* Deferred.make<void>()
|
||||
const fiber = yield* runner
|
||||
.ensureRunning(
|
||||
Effect.gen(function* () {
|
||||
yield* Deferred.succeed(started, void 0)
|
||||
return yield* Effect.never.pipe(Effect.as("never"))
|
||||
}),
|
||||
)
|
||||
.pipe(Effect.forkChild)
|
||||
yield* Deferred.await(started)
|
||||
expect(runner.busy).toBe(true)
|
||||
expect(runner.state._tag).toBe("Running")
|
||||
|
||||
yield* runner.cancel
|
||||
expect(runner.busy).toBe(false)
|
||||
|
||||
const exit = yield* Fiber.await(fiber)
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live(
|
||||
"cancel on idle is a no-op",
|
||||
Effect.gen(function* () {
|
||||
const s = yield* Scope.Scope
|
||||
const runner = Runner.make<string>(s)
|
||||
yield* runner.cancel
|
||||
expect(runner.busy).toBe(false)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live(
|
||||
"cancel with onInterrupt resolves callers gracefully",
|
||||
Effect.gen(function* () {
|
||||
const s = yield* Scope.Scope
|
||||
const runner = Runner.make<string>(s, { onInterrupt: Effect.succeed("fallback") })
|
||||
const fiber = yield* runner.ensureRunning(Effect.never.pipe(Effect.as("never"))).pipe(Effect.forkChild)
|
||||
yield* waitForState(runner, "Running")
|
||||
|
||||
yield* runner.cancel
|
||||
|
||||
const exit = yield* Fiber.await(fiber)
|
||||
expect(Exit.isSuccess(exit)).toBe(true)
|
||||
if (Exit.isSuccess(exit)) expect(exit.value).toBe("fallback")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live(
|
||||
"cancel with queued callers resolves all",
|
||||
Effect.gen(function* () {
|
||||
const s = yield* Scope.Scope
|
||||
const runner = Runner.make<string>(s, { onInterrupt: Effect.succeed("fallback") })
|
||||
|
||||
const a = yield* runner.ensureRunning(Effect.never.pipe(Effect.as("x"))).pipe(Effect.forkChild)
|
||||
yield* waitForState(runner, "Running")
|
||||
const b = yield* runner.ensureRunning(Effect.succeed("y")).pipe(Effect.forkChild)
|
||||
yield* Effect.yieldNow
|
||||
|
||||
yield* runner.cancel
|
||||
|
||||
const [exitA, exitB] = yield* Effect.all([Fiber.await(a), Fiber.await(b)])
|
||||
expect(Exit.isSuccess(exitA)).toBe(true)
|
||||
expect(Exit.isSuccess(exitB)).toBe(true)
|
||||
if (Exit.isSuccess(exitA)) expect(exitA.value).toBe("fallback")
|
||||
if (Exit.isSuccess(exitB)) expect(exitB.value).toBe("fallback")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live(
|
||||
"work can be started after cancel",
|
||||
Effect.gen(function* () {
|
||||
const s = yield* Scope.Scope
|
||||
const runner = Runner.make<string>(s)
|
||||
const fiber = yield* runner.ensureRunning(Effect.never.pipe(Effect.as("x"))).pipe(Effect.forkChild)
|
||||
yield* waitForState(runner, "Running")
|
||||
yield* runner.cancel
|
||||
yield* Fiber.await(fiber)
|
||||
|
||||
const result = yield* runner.ensureRunning(Effect.succeed("after-cancel"))
|
||||
expect(result).toBe("after-cancel")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live(
|
||||
"cancel does not deadlock when replacement work starts before interrupted run exits",
|
||||
Effect.gen(function* () {
|
||||
const s = yield* Scope.Scope
|
||||
const hit = yield* Deferred.make<void>()
|
||||
const hold = yield* Deferred.make<void>()
|
||||
const done = yield* Deferred.make<void>()
|
||||
|
||||
yield* Effect.gen(function* () {
|
||||
const runner = Runner.make<string>(s)
|
||||
const first = Effect.never.pipe(
|
||||
Effect.onInterrupt(() => Deferred.succeed(hit, undefined)),
|
||||
Effect.ensuring(Deferred.await(hold)),
|
||||
Effect.as("first"),
|
||||
)
|
||||
|
||||
const a = yield* runner.ensureRunning(first).pipe(Effect.exit, Effect.forkChild)
|
||||
yield* waitForState(runner, "Running")
|
||||
|
||||
const stop = yield* runner.cancel.pipe(Effect.forkChild)
|
||||
yield* Deferred.await(hit).pipe(Effect.timeout("250 millis"))
|
||||
|
||||
const b = yield* runner.ensureRunning(Deferred.await(done).pipe(Effect.as("second"))).pipe(Effect.forkChild)
|
||||
yield* Effect.yieldNow
|
||||
expect(runner.busy).toBe(true)
|
||||
|
||||
yield* Deferred.succeed(hold, undefined)
|
||||
const stopExit = yield* Fiber.await(stop).pipe(Effect.timeout("250 millis"))
|
||||
expect(Exit.isSuccess(stopExit)).toBe(true)
|
||||
|
||||
expect(runner.busy).toBe(true)
|
||||
yield* Deferred.succeed(done, undefined)
|
||||
expect(yield* Fiber.join(b).pipe(Effect.timeout("250 millis"))).toBe("second")
|
||||
expect(runner.busy).toBe(false)
|
||||
|
||||
const exit = yield* Fiber.join(a)
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
}).pipe(
|
||||
Effect.ensuring(
|
||||
Effect.all([Deferred.succeed(hold, undefined), Deferred.succeed(done, undefined)], { discard: true }).pipe(
|
||||
Effect.ignore,
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
// --- shell semantics ---
|
||||
|
||||
it.live(
|
||||
"shell runs exclusively",
|
||||
Effect.gen(function* () {
|
||||
const s = yield* Scope.Scope
|
||||
const runner = Runner.make<string>(s)
|
||||
const result = yield* runner.startShell(Effect.succeed("shell-done"))
|
||||
expect(result).toBe("shell-done")
|
||||
expect(runner.busy).toBe(false)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live(
|
||||
"shell rejects when run is active",
|
||||
Effect.gen(function* () {
|
||||
const s = yield* Scope.Scope
|
||||
const runner = Runner.make<string>(s)
|
||||
const started = yield* Deferred.make<void>()
|
||||
const fiber = yield* runner
|
||||
.ensureRunning(
|
||||
Effect.gen(function* () {
|
||||
yield* Deferred.succeed(started, undefined)
|
||||
return yield* Effect.never.pipe(Effect.as("x"))
|
||||
}),
|
||||
)
|
||||
.pipe(Effect.forkChild)
|
||||
yield* Deferred.await(started).pipe(Effect.timeout("250 millis"))
|
||||
yield* Effect.gen(function* () {
|
||||
while (runner.state._tag !== "Running") yield* Effect.yieldNow
|
||||
}).pipe(Effect.timeout("250 millis"))
|
||||
|
||||
const exit = yield* runner.startShell(Effect.succeed("nope")).pipe(Effect.exit)
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
|
||||
yield* runner.cancel
|
||||
yield* Fiber.await(fiber).pipe(Effect.timeout("250 millis"))
|
||||
}),
|
||||
)
|
||||
|
||||
it.live(
|
||||
"shell rejects when another shell is running",
|
||||
Effect.gen(function* () {
|
||||
const s = yield* Scope.Scope
|
||||
const runner = Runner.make<string>(s)
|
||||
const gate = yield* Deferred.make<void>()
|
||||
|
||||
const sh = yield* runner.startShell(Deferred.await(gate).pipe(Effect.as("first"))).pipe(Effect.forkChild)
|
||||
yield* waitForState(runner, "Shell")
|
||||
|
||||
const exit = yield* runner.startShell(Effect.succeed("second")).pipe(Effect.exit)
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(Runner.Busy)
|
||||
|
||||
yield* Deferred.succeed(gate, undefined)
|
||||
yield* Fiber.await(sh)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live(
|
||||
"cancel interrupts shell",
|
||||
Effect.gen(function* () {
|
||||
const s = yield* Scope.Scope
|
||||
const runner = Runner.make<string>(s)
|
||||
const gate = yield* Deferred.make<void>()
|
||||
|
||||
const sh = yield* runner.startShell(Deferred.await(gate).pipe(Effect.as("ignored"))).pipe(Effect.forkChild)
|
||||
yield* waitForState(runner, "Shell")
|
||||
|
||||
const stop = yield* runner.cancel.pipe(Effect.forkChild)
|
||||
const stopExit = yield* Fiber.await(stop).pipe(Effect.timeout("250 millis"))
|
||||
expect(Exit.isSuccess(stopExit)).toBe(true)
|
||||
expect(runner.busy).toBe(false)
|
||||
|
||||
const shellExit = yield* Fiber.await(sh)
|
||||
expect(Exit.isFailure(shellExit)).toBe(true)
|
||||
|
||||
yield* Deferred.succeed(gate, undefined).pipe(Effect.ignore)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live(
|
||||
"cancel does not mask shell defects",
|
||||
Effect.gen(function* () {
|
||||
const s = yield* Scope.Scope
|
||||
const runner = Runner.make<string>(s, { onInterrupt: Effect.succeed("interrupted") })
|
||||
const ready = yield* Latch.make()
|
||||
|
||||
const sh = yield* runner
|
||||
.startShell(
|
||||
Effect.gen(function* () {
|
||||
yield* ready.open
|
||||
return yield* Effect.never.pipe(Effect.as("ignored"))
|
||||
}).pipe(Effect.ensuring(Effect.die("boom"))),
|
||||
ready,
|
||||
)
|
||||
.pipe(Effect.forkChild)
|
||||
yield* ready.await.pipe(Effect.timeout("250 millis"))
|
||||
|
||||
yield* runner.cancel
|
||||
expect(Exit.isFailure(yield* Fiber.await(sh))).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
// --- shell→run handoff ---
|
||||
|
||||
it.live(
|
||||
"ensureRunning queues behind shell then runs after",
|
||||
Effect.gen(function* () {
|
||||
const s = yield* Scope.Scope
|
||||
const runner = Runner.make<string>(s)
|
||||
const gate = yield* Deferred.make<void>()
|
||||
|
||||
const sh = yield* runner.startShell(Deferred.await(gate).pipe(Effect.as("shell-result"))).pipe(Effect.forkChild)
|
||||
yield* waitForState(runner, "Shell")
|
||||
expect(runner.state._tag).toBe("Shell")
|
||||
|
||||
const run = yield* runner.ensureRunning(Effect.succeed("run-result")).pipe(Effect.forkChild)
|
||||
yield* waitForState(runner, "ShellThenRun")
|
||||
expect(runner.state._tag).toBe("ShellThenRun")
|
||||
|
||||
yield* Deferred.succeed(gate, undefined)
|
||||
yield* Fiber.await(sh)
|
||||
|
||||
const exit = yield* Fiber.await(run)
|
||||
expect(Exit.isSuccess(exit)).toBe(true)
|
||||
if (Exit.isSuccess(exit)) expect(exit.value).toBe("run-result")
|
||||
expect(runner.state._tag).toBe("Idle")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live(
|
||||
"multiple ensureRunning callers share the queued run behind shell",
|
||||
Effect.gen(function* () {
|
||||
const s = yield* Scope.Scope
|
||||
const runner = Runner.make<string>(s)
|
||||
const calls = yield* Ref.make(0)
|
||||
const gate = yield* Deferred.make<void>()
|
||||
|
||||
const sh = yield* runner.startShell(Deferred.await(gate).pipe(Effect.as("shell"))).pipe(Effect.forkChild)
|
||||
yield* waitForState(runner, "Shell")
|
||||
|
||||
const work = Effect.gen(function* () {
|
||||
yield* Ref.update(calls, (n) => n + 1)
|
||||
return "run"
|
||||
})
|
||||
const a = yield* runner.ensureRunning(work).pipe(Effect.forkChild)
|
||||
const b = yield* runner.ensureRunning(work).pipe(Effect.forkChild)
|
||||
yield* waitForState(runner, "ShellThenRun")
|
||||
|
||||
yield* Deferred.succeed(gate, undefined)
|
||||
yield* Fiber.await(sh)
|
||||
|
||||
const [exitA, exitB] = yield* Effect.all([Fiber.await(a), Fiber.await(b)])
|
||||
expect(Exit.isSuccess(exitA)).toBe(true)
|
||||
expect(Exit.isSuccess(exitB)).toBe(true)
|
||||
expect(yield* Ref.get(calls)).toBe(1)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live(
|
||||
"cancel during shell_then_run cancels both",
|
||||
Effect.gen(function* () {
|
||||
const s = yield* Scope.Scope
|
||||
const runner = Runner.make<string>(s)
|
||||
|
||||
const sh = yield* runner.startShell(Effect.never.pipe(Effect.as("aborted"))).pipe(Effect.forkChild)
|
||||
yield* waitForState(runner, "Shell")
|
||||
|
||||
const run = yield* runner.ensureRunning(Effect.succeed("y")).pipe(Effect.forkChild)
|
||||
yield* waitForState(runner, "ShellThenRun")
|
||||
expect(runner.state._tag).toBe("ShellThenRun")
|
||||
|
||||
yield* runner.cancel
|
||||
expect(runner.busy).toBe(false)
|
||||
|
||||
yield* Fiber.await(sh)
|
||||
const exit = yield* Fiber.await(run)
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
// --- lifecycle callbacks ---
|
||||
|
||||
it.live(
|
||||
"onIdle fires when returning to idle from running",
|
||||
Effect.gen(function* () {
|
||||
const s = yield* Scope.Scope
|
||||
const count = yield* Ref.make(0)
|
||||
const runner = Runner.make<string>(s, {
|
||||
onIdle: Ref.update(count, (n) => n + 1),
|
||||
})
|
||||
yield* runner.ensureRunning(Effect.succeed("ok"))
|
||||
expect(yield* Ref.get(count)).toBe(1)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live(
|
||||
"onIdle fires on cancel",
|
||||
Effect.gen(function* () {
|
||||
const s = yield* Scope.Scope
|
||||
const count = yield* Ref.make(0)
|
||||
const runner = Runner.make<string>(s, {
|
||||
onIdle: Ref.update(count, (n) => n + 1),
|
||||
})
|
||||
const fiber = yield* runner.ensureRunning(Effect.never.pipe(Effect.as("x"))).pipe(Effect.forkChild)
|
||||
yield* waitForState(runner, "Running")
|
||||
yield* runner.cancel
|
||||
yield* Fiber.await(fiber)
|
||||
expect(yield* Ref.get(count)).toBeGreaterThanOrEqual(1)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live(
|
||||
"onBusy fires when shell starts",
|
||||
Effect.gen(function* () {
|
||||
const s = yield* Scope.Scope
|
||||
const count = yield* Ref.make(0)
|
||||
const runner = Runner.make<string>(s, {
|
||||
onBusy: Ref.update(count, (n) => n + 1),
|
||||
})
|
||||
yield* runner.startShell(Effect.succeed("done"))
|
||||
expect(yield* Ref.get(count)).toBe(1)
|
||||
}),
|
||||
)
|
||||
|
||||
// --- busy flag ---
|
||||
|
||||
it.live(
|
||||
"busy is true during run",
|
||||
Effect.gen(function* () {
|
||||
const s = yield* Scope.Scope
|
||||
const runner = Runner.make<string>(s)
|
||||
const gate = yield* Deferred.make<void>()
|
||||
|
||||
const fiber = yield* runner.ensureRunning(Deferred.await(gate).pipe(Effect.as("ok"))).pipe(Effect.forkChild)
|
||||
yield* waitForState(runner, "Running")
|
||||
expect(runner.busy).toBe(true)
|
||||
|
||||
yield* Deferred.succeed(gate, undefined)
|
||||
yield* Fiber.await(fiber)
|
||||
expect(runner.busy).toBe(false)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live(
|
||||
"busy is true during shell",
|
||||
Effect.gen(function* () {
|
||||
const s = yield* Scope.Scope
|
||||
const runner = Runner.make<string>(s)
|
||||
const gate = yield* Deferred.make<void>()
|
||||
|
||||
const fiber = yield* runner.startShell(Deferred.await(gate).pipe(Effect.as("ok"))).pipe(Effect.forkChild)
|
||||
yield* waitForState(runner, "Shell")
|
||||
expect(runner.busy).toBe(true)
|
||||
|
||||
yield* Deferred.succeed(gate, undefined)
|
||||
yield* Fiber.await(fiber)
|
||||
expect(runner.busy).toBe(false)
|
||||
}),
|
||||
)
|
||||
})
|
||||
373
packages/opencode/test/effect/runtime-flags.test.ts
Normal file
373
packages/opencode/test/effect/runtime-flags.test.ts
Normal file
@@ -0,0 +1,373 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { ConfigProvider, Effect, Layer } from "effect"
|
||||
import { RuntimeFlags } from "../../src/effect/runtime-flags"
|
||||
import { it } from "../lib/effect"
|
||||
|
||||
const fromConfig = (input: Record<string, unknown>) =>
|
||||
RuntimeFlags.defaultLayer.pipe(Layer.provide(ConfigProvider.layer(ConfigProvider.fromUnknown(input))))
|
||||
|
||||
const readFlags = RuntimeFlags.Service.useSync((flags) => flags)
|
||||
|
||||
describe("RuntimeFlags", () => {
|
||||
it.effect("defaultLayer defaults autoShare to false", () =>
|
||||
Effect.gen(function* () {
|
||||
const flags = yield* readFlags.pipe(Effect.provide(fromConfig({})))
|
||||
|
||||
expect(flags.autoShare).toBe(false)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("defaultLayer parses plugin flags from the active ConfigProvider", () =>
|
||||
Effect.gen(function* () {
|
||||
const flags = yield* readFlags.pipe(
|
||||
Effect.provide(
|
||||
fromConfig({
|
||||
OPENCODE_PURE: "true",
|
||||
OPENCODE_DISABLE_DEFAULT_PLUGINS: "true",
|
||||
OPENCODE_AUTO_SHARE: "true",
|
||||
OPENCODE_DISABLE_EMBEDDED_WEB_UI: "true",
|
||||
OPENCODE_DISABLE_EXTERNAL_SKILLS: "true",
|
||||
OPENCODE_DISABLE_LSP_DOWNLOAD: "true",
|
||||
OPENCODE_EXPERIMENTAL: "true",
|
||||
OPENCODE_ENABLE_EXA: "true",
|
||||
OPENCODE_ENABLE_PARALLEL: "true",
|
||||
OPENCODE_ENABLE_EXPERIMENTAL_MODELS: "true",
|
||||
OPENCODE_ENABLE_QUESTION_TOOL: "true",
|
||||
OPENCODE_CLIENT: "desktop",
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
expect(flags.pure).toBe(true)
|
||||
expect(flags.autoShare).toBe(true)
|
||||
expect(flags.disableDefaultPlugins).toBe(true)
|
||||
expect(flags.disableEmbeddedWebUi).toBe(true)
|
||||
expect(flags.disableExternalSkills).toBe(true)
|
||||
expect(flags.disableLspDownload).toBe(true)
|
||||
expect(flags.disableClaudeCodePrompt).toBe(false)
|
||||
expect(flags.enableExa).toBe(true)
|
||||
expect(flags.enableParallel).toBe(true)
|
||||
expect(flags.enableExperimentalModels).toBe(true)
|
||||
expect(flags.enableQuestionTool).toBe(true)
|
||||
expect(flags.experimentalReferences).toBe(true)
|
||||
expect(flags.experimentalBackgroundSubagents).toBe(true)
|
||||
expect(flags.experimentalLspTy).toBe(false)
|
||||
expect(flags.experimentalLspTool).toBe(true)
|
||||
expect(flags.experimentalOxfmt).toBe(true)
|
||||
expect(flags.experimentalPlanMode).toBe(true)
|
||||
expect(flags.experimentalEventSystem).toBe(true)
|
||||
expect(flags.experimentalWorkspaces).toBe(true)
|
||||
expect(flags.experimentalIconDiscovery).toBe(true)
|
||||
expect(flags.experimentalNativeLlm).toBe(false)
|
||||
expect(flags.experimentalWebSockets).toBe(false)
|
||||
expect(flags.client).toBe("desktop")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("defaultLayer parses OPENCODE_EXPERIMENTAL_LSP_TY", () =>
|
||||
Effect.gen(function* () {
|
||||
const flags = yield* readFlags.pipe(
|
||||
Effect.provide(
|
||||
fromConfig({
|
||||
OPENCODE_EXPERIMENTAL_LSP_TY: "true",
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
expect(flags.experimentalLspTy).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("enables native LLM via dedicated flag only", () =>
|
||||
Effect.gen(function* () {
|
||||
const explicit = yield* readFlags.pipe(Effect.provide(fromConfig({ OPENCODE_EXPERIMENTAL_NATIVE_LLM: "true" })))
|
||||
const umbrella = yield* readFlags.pipe(Effect.provide(fromConfig({ OPENCODE_EXPERIMENTAL: "true" })))
|
||||
|
||||
expect(explicit.experimentalNativeLlm).toBe(true)
|
||||
expect(umbrella.experimentalNativeLlm).toBe(false)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("enables WebSockets via dedicated flag only", () =>
|
||||
Effect.gen(function* () {
|
||||
const explicit = yield* readFlags.pipe(Effect.provide(fromConfig({ OPENCODE_EXPERIMENTAL_WEBSOCKETS: "true" })))
|
||||
const umbrella = yield* readFlags.pipe(Effect.provide(fromConfig({ OPENCODE_EXPERIMENTAL: "true" })))
|
||||
|
||||
expect(explicit.experimentalWebSockets).toBe(true)
|
||||
expect(umbrella.experimentalWebSockets).toBe(false)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("layer accepts partial test overrides and fills defaults from Config definitions", () =>
|
||||
Effect.gen(function* () {
|
||||
const flags = yield* readFlags.pipe(
|
||||
Effect.provide(RuntimeFlags.layer({ disableDefaultPlugins: true, bashDefaultTimeoutMs: 1_000 })),
|
||||
)
|
||||
|
||||
expect(flags.pure).toBe(false)
|
||||
expect(flags.autoShare).toBe(false)
|
||||
expect(flags.disableDefaultPlugins).toBe(true)
|
||||
expect(flags.disableEmbeddedWebUi).toBe(false)
|
||||
expect(flags.disableExternalSkills).toBe(false)
|
||||
expect(flags.disableLspDownload).toBe(false)
|
||||
expect(flags.disableClaudeCodePrompt).toBe(false)
|
||||
expect(flags.disableClaudeCodeSkills).toBe(false)
|
||||
expect(flags.enableExa).toBe(false)
|
||||
expect(flags.experimentalIconDiscovery).toBe(false)
|
||||
expect(flags.experimentalOxfmt).toBe(false)
|
||||
expect(flags.outputTokenMax).toBeUndefined()
|
||||
expect(flags.bashDefaultTimeoutMs).toBe(1_000)
|
||||
expect(flags.enableExperimentalModels).toBe(false)
|
||||
expect(flags.client).toBe("cli")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("experimentalIconDiscovery defaults to false", () =>
|
||||
Effect.gen(function* () {
|
||||
const flags = yield* readFlags.pipe(Effect.provide(fromConfig({})))
|
||||
|
||||
expect(flags.experimentalIconDiscovery).toBe(false)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("disableExternalSkills defaults to false", () =>
|
||||
Effect.gen(function* () {
|
||||
const flags = yield* readFlags.pipe(Effect.provide(fromConfig({})))
|
||||
|
||||
expect(flags.disableExternalSkills).toBe(false)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("disableExternalSkills reads OPENCODE_DISABLE_EXTERNAL_SKILLS", () =>
|
||||
Effect.gen(function* () {
|
||||
const flags = yield* readFlags.pipe(Effect.provide(fromConfig({ OPENCODE_DISABLE_EXTERNAL_SKILLS: "true" })))
|
||||
|
||||
expect(flags.disableExternalSkills).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("disableLspDownload defaults to false", () =>
|
||||
Effect.gen(function* () {
|
||||
const flags = yield* readFlags.pipe(Effect.provide(fromConfig({})))
|
||||
|
||||
expect(flags.disableLspDownload).toBe(false)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("disableLspDownload reads OPENCODE_DISABLE_LSP_DOWNLOAD", () =>
|
||||
Effect.gen(function* () {
|
||||
const flags = yield* readFlags.pipe(Effect.provide(fromConfig({ OPENCODE_DISABLE_LSP_DOWNLOAD: "true" })))
|
||||
|
||||
expect(flags.disableLspDownload).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("disableClaudeCodePrompt defaults to false", () =>
|
||||
Effect.gen(function* () {
|
||||
const flags = yield* readFlags.pipe(Effect.provide(fromConfig({})))
|
||||
|
||||
expect(flags.disableClaudeCodePrompt).toBe(false)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("disableClaudeCodePrompt reads OPENCODE_DISABLE_CLAUDE_CODE_PROMPT", () =>
|
||||
Effect.gen(function* () {
|
||||
const flags = yield* readFlags.pipe(Effect.provide(fromConfig({ OPENCODE_DISABLE_CLAUDE_CODE_PROMPT: "true" })))
|
||||
|
||||
expect(flags.disableClaudeCodePrompt).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("disableClaudeCodePrompt inherits OPENCODE_DISABLE_CLAUDE_CODE", () =>
|
||||
Effect.gen(function* () {
|
||||
const flags = yield* readFlags.pipe(Effect.provide(fromConfig({ OPENCODE_DISABLE_CLAUDE_CODE: "true" })))
|
||||
|
||||
expect(flags.disableClaudeCodePrompt).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("experimentalIconDiscovery reads OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", () =>
|
||||
Effect.gen(function* () {
|
||||
const flags = yield* readFlags.pipe(Effect.provide(fromConfig({ OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: "true" })))
|
||||
|
||||
expect(flags.experimentalIconDiscovery).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("experimentalIconDiscovery inherits OPENCODE_EXPERIMENTAL", () =>
|
||||
Effect.gen(function* () {
|
||||
const flags = yield* readFlags.pipe(Effect.provide(fromConfig({ OPENCODE_EXPERIMENTAL: "true" })))
|
||||
|
||||
expect(flags.experimentalIconDiscovery).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("specific experimental flags override OPENCODE_EXPERIMENTAL", () =>
|
||||
Effect.gen(function* () {
|
||||
const flags = yield* readFlags.pipe(
|
||||
Effect.provide(
|
||||
fromConfig({
|
||||
OPENCODE_EXPERIMENTAL: "true",
|
||||
OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: "false",
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
expect(flags.experimentalIconDiscovery).toBe(false)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("experimentalOxfmt defaults to false", () =>
|
||||
Effect.gen(function* () {
|
||||
const flags = yield* readFlags.pipe(Effect.provide(fromConfig({})))
|
||||
|
||||
expect(flags.experimentalOxfmt).toBe(false)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("experimentalOxfmt is enabled by OPENCODE_EXPERIMENTAL_OXFMT", () =>
|
||||
Effect.gen(function* () {
|
||||
const flags = yield* readFlags.pipe(
|
||||
Effect.provide(
|
||||
fromConfig({
|
||||
OPENCODE_EXPERIMENTAL_OXFMT: "true",
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
expect(flags.experimentalOxfmt).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("experimentalOxfmt inherits OPENCODE_EXPERIMENTAL", () =>
|
||||
Effect.gen(function* () {
|
||||
const flags = yield* readFlags.pipe(
|
||||
Effect.provide(
|
||||
fromConfig({
|
||||
OPENCODE_EXPERIMENTAL: "true",
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
expect(flags.experimentalOxfmt).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
for (const input of [
|
||||
{ name: "absent", config: {}, expected: undefined },
|
||||
{
|
||||
name: "valid positive integer",
|
||||
config: { OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS: "1234" },
|
||||
expected: 1234,
|
||||
},
|
||||
{
|
||||
name: "invalid string",
|
||||
config: { OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS: "nope" },
|
||||
expected: undefined,
|
||||
},
|
||||
{ name: "zero", config: { OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS: "0" }, expected: undefined },
|
||||
{ name: "negative", config: { OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS: "-1" }, expected: undefined },
|
||||
{
|
||||
name: "non-integer",
|
||||
config: { OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS: "1.5" },
|
||||
expected: undefined,
|
||||
},
|
||||
]) {
|
||||
it.effect(`parses bashDefaultTimeoutMs from config: ${input.name}`, () =>
|
||||
Effect.gen(function* () {
|
||||
const flags = yield* readFlags.pipe(Effect.provide(fromConfig(input.config)))
|
||||
|
||||
expect(flags.bashDefaultTimeoutMs).toBe(input.expected)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
for (const input of [
|
||||
{ name: "absent", config: {}, expected: undefined },
|
||||
{
|
||||
name: "valid positive integer",
|
||||
config: { OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX: "1234" },
|
||||
expected: 1234,
|
||||
},
|
||||
{
|
||||
name: "invalid string",
|
||||
config: { OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX: "nope" },
|
||||
expected: undefined,
|
||||
},
|
||||
{ name: "zero", config: { OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX: "0" }, expected: undefined },
|
||||
{ name: "negative", config: { OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX: "-1" }, expected: undefined },
|
||||
{
|
||||
name: "non-integer",
|
||||
config: { OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX: "1.5" },
|
||||
expected: undefined,
|
||||
},
|
||||
]) {
|
||||
it.effect(`parses outputTokenMax from config: ${input.name}`, () =>
|
||||
Effect.gen(function* () {
|
||||
const flags = yield* readFlags.pipe(Effect.provide(fromConfig(input.config)))
|
||||
|
||||
expect(flags.outputTokenMax).toBe(input.expected)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
it.effect("layer ignores the active ConfigProvider for omitted test overrides", () =>
|
||||
Effect.gen(function* () {
|
||||
const flags = yield* readFlags.pipe(
|
||||
Effect.provide(RuntimeFlags.layer()),
|
||||
Effect.provide(
|
||||
ConfigProvider.layer(
|
||||
ConfigProvider.fromUnknown({
|
||||
OPENCODE_PURE: "true",
|
||||
OPENCODE_DISABLE_DEFAULT_PLUGINS: "true",
|
||||
OPENCODE_DISABLE_EXTERNAL_SKILLS: "true",
|
||||
OPENCODE_DISABLE_LSP_DOWNLOAD: "true",
|
||||
OPENCODE_EXPERIMENTAL: "true",
|
||||
OPENCODE_ENABLE_EXA: "true",
|
||||
OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS: "1234",
|
||||
OPENCODE_CLIENT: "desktop",
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
expect(flags.pure).toBe(false)
|
||||
expect(flags.disableDefaultPlugins).toBe(false)
|
||||
expect(flags.disableEmbeddedWebUi).toBe(false)
|
||||
expect(flags.disableExternalSkills).toBe(false)
|
||||
expect(flags.disableLspDownload).toBe(false)
|
||||
expect(flags.disableClaudeCodePrompt).toBe(false)
|
||||
expect(flags.disableClaudeCodeSkills).toBe(false)
|
||||
expect(flags.enableExa).toBe(false)
|
||||
expect(flags.experimentalIconDiscovery).toBe(false)
|
||||
expect(flags.experimentalOxfmt).toBe(false)
|
||||
expect(flags.outputTokenMax).toBeUndefined()
|
||||
expect(flags.bashDefaultTimeoutMs).toBeUndefined()
|
||||
expect(flags.client).toBe("cli")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("disableClaudeCodeSkills defaults to false", () =>
|
||||
Effect.gen(function* () {
|
||||
const flags = yield* readFlags.pipe(Effect.provide(fromConfig({})))
|
||||
|
||||
expect(flags.disableClaudeCodeSkills).toBe(false)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("disableClaudeCodeSkills reads OPENCODE_DISABLE_CLAUDE_CODE_SKILLS", () =>
|
||||
Effect.gen(function* () {
|
||||
const flags = yield* readFlags.pipe(Effect.provide(fromConfig({ OPENCODE_DISABLE_CLAUDE_CODE_SKILLS: "true" })))
|
||||
|
||||
expect(flags.disableClaudeCodeSkills).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("disableClaudeCodeSkills inherits OPENCODE_DISABLE_CLAUDE_CODE", () =>
|
||||
Effect.gen(function* () {
|
||||
const flags = yield* readFlags.pipe(Effect.provide(fromConfig({ OPENCODE_DISABLE_CLAUDE_CODE: "true" })))
|
||||
|
||||
expect(flags.disableClaudeCodeSkills).toBe(true)
|
||||
}),
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user