Compare commits

...
Sign in to create a new pull request.

1 commit

Author SHA1 Message Date
Kit Langton
84a1b0280e effect(cli): unwrap run/runtime.boot and variant.shared facades 2026-05-12 16:26:22 -04:00
5 changed files with 353 additions and 337 deletions

View file

@ -9,7 +9,6 @@ import { Context, Effect, Layer } from "effect"
import { stringifyKeyStroke } from "@opentui/keymap" import { stringifyKeyStroke } from "@opentui/keymap"
import { TuiConfig } from "@/cli/cmd/tui/config/tui" import { TuiConfig } from "@/cli/cmd/tui/config/tui"
import { TuiKeybind } from "@/cli/cmd/tui/config/keybind" import { TuiKeybind } from "@/cli/cmd/tui/config/keybind"
import { makeRuntime } from "@/effect/run-service"
import { reusePendingTask } from "./runtime.shared" import { reusePendingTask } from "./runtime.shared"
import { resolveSession, sessionHistory } from "./session.shared" import { resolveSession, sessionHistory } from "./session.shared"
import type { FooterKeybinds, RunDiffStyle, RunInput, RunPrompt, RunProvider } from "./types" import type { FooterKeybinds, RunDiffStyle, RunInput, RunPrompt, RunProvider } from "./types"
@ -28,6 +27,8 @@ const DEFAULT_KEYBINDS: FooterKeybinds = {
inputNewline: [{ key: "shift+return,ctrl+return,alt+return,ctrl+j" }], inputNewline: [{ key: "shift+return,ctrl+return,alt+return,ctrl+j" }],
} }
export const defaultKeybinds: FooterKeybinds = DEFAULT_KEYBINDS
export type ModelInfo = { export type ModelInfo = {
providers: RunProvider[] providers: RunProvider[]
variants: string[] variants: string[]
@ -41,7 +42,8 @@ export type SessionInfo = {
} }
type Config = Awaited<ReturnType<typeof TuiConfig.get>> type Config = Awaited<ReturnType<typeof TuiConfig.get>>
type BootService = {
export interface Interface {
readonly resolveModelInfo: ( readonly resolveModelInfo: (
sdk: RunInput["sdk"], sdk: RunInput["sdk"],
directory: string, directory: string,
@ -58,13 +60,13 @@ type BootService = {
const configTask: { current?: Promise<Config> } = {} const configTask: { current?: Promise<Config> } = {}
class Service extends Context.Service<Service, BootService>()("@opencode/RunBoot") {} export class Service extends Context.Service<Service, Interface>()("@opencode/RunBoot") {}
function loadConfig() { function loadConfig() {
return reusePendingTask(configTask, () => TuiConfig.get()) return reusePendingTask(configTask, () => TuiConfig.get())
} }
function emptyModelInfo(): ModelInfo { export function emptyModelInfo(): ModelInfo {
return { return {
providers: [], providers: [],
variants: [], variants: [],
@ -72,7 +74,7 @@ function emptyModelInfo(): ModelInfo {
} }
} }
function emptySessionInfo(): SessionInfo { export function emptySessionInfo(): SessionInfo {
return { return {
first: true, first: true,
history: [], history: [],
@ -105,7 +107,7 @@ function footerKeybinds(config: Config | undefined): FooterKeybinds {
} }
} }
const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const config = Effect.fn("RunBoot.config")(() => Effect.promise(() => loadConfig().catch(() => undefined))) const config = Effect.fn("RunBoot.config")(() => Effect.promise(() => loadConfig().catch(() => undefined)))
@ -192,31 +194,6 @@ const layer = Layer.effect(
}), }),
) )
const runtime = makeRuntime(Service, layer) export const defaultLayer = layer
// Fetches available variants and context limits for every provider/model pair. export * as RunBoot from "./runtime.boot"
export async function resolveModelInfo(
sdk: RunInput["sdk"],
directory: string,
model: RunInput["model"],
): Promise<ModelInfo> {
return runtime.runPromise((svc) => svc.resolveModelInfo(sdk, directory, model)).catch(() => emptyModelInfo())
}
// Fetches session messages to determine if this is the first turn and build prompt history.
export async function resolveSessionInfo(
sdk: RunInput["sdk"],
sessionID: string,
model: RunInput["model"],
): Promise<SessionInfo> {
return runtime.runPromise((svc) => svc.resolveSessionInfo(sdk, sessionID, model)).catch(() => emptySessionInfo())
}
// Reads keybind overrides from TUI config and merges them with defaults.
export async function resolveFooterKeybinds(): Promise<FooterKeybinds> {
return runtime.runPromise((svc) => svc.resolveFooterKeybinds()).catch(() => DEFAULT_KEYBINDS)
}
export async function resolveDiffStyle(): Promise<RunDiffStyle> {
return runtime.runPromise((svc) => svc.resolveDiffStyle()).catch(() => "auto")
}

View file

@ -14,13 +14,33 @@
// 4. runs the prompt queue until the footer closes. // 4. runs the prompt queue until the footer closes.
import { createOpencodeClient } from "@opencode-ai/sdk/v2" import { createOpencodeClient } from "@opencode-ai/sdk/v2"
import { Flag } from "@opencode-ai/core/flag/flag" import { Flag } from "@opencode-ai/core/flag/flag"
import { Effect, Layer } from "effect"
import { AppRuntime } from "@/effect/app-runtime"
import { createRunDemo } from "./demo" import { createRunDemo } from "./demo"
import { resolveDiffStyle, resolveFooterKeybinds, resolveModelInfo, resolveSessionInfo } from "./runtime.boot" import {
RunBoot,
defaultKeybinds,
emptyModelInfo,
emptySessionInfo,
type ModelInfo,
type SessionInfo,
} from "./runtime.boot"
import { createRuntimeLifecycle } from "./runtime.lifecycle" import { createRuntimeLifecycle } from "./runtime.lifecycle"
import { recordRunSpanError, setRunSpanAttributes, withRunSpan } from "./otel" import { recordRunSpanError, setRunSpanAttributes, withRunSpan } from "./otel"
import { trace } from "./trace" import { trace } from "./trace"
import { cycleVariant, formatModelLabel, resolveSavedVariant, resolveVariant, saveVariant } from "./variant.shared" import { Variant, cycleVariant, formatModelLabel, resolveVariant } from "./variant.shared"
import type { RunInput, RunPrompt, RunProvider } from "./types" import type { RunDiffStyle, RunInput, RunPrompt, RunProvider } from "./types"
const BootLayer = Layer.mergeAll(RunBoot.defaultLayer, Variant.defaultLayer)
function persistVariant(model: RunInput["model"], variant: string | undefined) {
AppRuntime.runFork(
Effect.gen(function* () {
const variantSvc = yield* Variant.Service
yield* variantSvc.saveVariant(model, variant).pipe(Effect.orElseSucceed(() => undefined))
}).pipe(Effect.provide(BootLayer)),
)
}
/** @internal Exported for testing */ /** @internal Exported for testing */
export { pickVariant, resolveVariant } from "./variant.shared" export { pickVariant, resolveVariant } from "./variant.shared"
@ -169,25 +189,44 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise<void> {
async (span) => { async (span) => {
const start = performance.now() const start = performance.now()
const log = trace() const log = trace()
const keybindTask = resolveFooterKeybinds() const earlyTask = AppRuntime.runPromise(
const diffTask = resolveDiffStyle() Effect.gen(function* () {
const boot = yield* RunBoot.Service
return yield* Effect.all(
{
keybinds: boot.resolveFooterKeybinds().pipe(Effect.orElseSucceed(() => defaultKeybinds)),
diffStyle: boot.resolveDiffStyle().pipe(Effect.orElseSucceed((): RunDiffStyle => "auto")),
},
{ concurrency: "unbounded" },
)
}).pipe(Effect.provide(BootLayer)),
)
const ctx = await input.boot() const ctx = await input.boot()
const modelTask = resolveModelInfo(ctx.sdk, ctx.directory, ctx.model) const modelTask = AppRuntime.runPromise(
const sessionTask = Effect.gen(function* () {
const boot = yield* RunBoot.Service
return yield* boot.resolveModelInfo(ctx.sdk, ctx.directory, ctx.model)
}).pipe(Effect.provide(BootLayer)),
).catch((): ModelInfo => emptyModelInfo())
const sessionAndSavedTask = AppRuntime.runPromise(
Effect.gen(function* () {
const boot = yield* RunBoot.Service
const variantSvc = yield* Variant.Service
return yield* Effect.all(
{
session:
ctx.resume === true ctx.resume === true
? resolveSessionInfo(ctx.sdk, ctx.sessionID, ctx.model) ? boot
: Promise.resolve({ .resolveSessionInfo(ctx.sdk, ctx.sessionID, ctx.model)
first: true, .pipe(Effect.orElseSucceed((): SessionInfo => emptySessionInfo()))
history: [], : Effect.succeed<SessionInfo>({ first: true, history: [], variant: undefined }),
variant: undefined, savedVariant: variantSvc.resolveSavedVariant(ctx.model).pipe(Effect.orElseSucceed(() => undefined)),
}) },
const savedTask = resolveSavedVariant(ctx.model) { concurrency: "unbounded" },
const [keybinds, diffStyle, session, savedVariant] = await Promise.all([ )
keybindTask, }).pipe(Effect.provide(BootLayer)),
diffTask, )
sessionTask, const [{ keybinds, diffStyle }, { session, savedVariant }] = await Promise.all([earlyTask, sessionAndSavedTask])
savedTask,
])
const state: RuntimeState = { const state: RuntimeState = {
shown: !session.first, shown: !session.first,
aborting: false, aborting: false,
@ -280,7 +319,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise<void> {
} }
state.activeVariant = cycleVariant(state.activeVariant, state.variants) state.activeVariant = cycleVariant(state.activeVariant, state.variants)
saveVariant(state.model, state.activeVariant) persistVariant(state.model, state.activeVariant)
setRunSpanAttributes(span, { setRunSpanAttributes(span, {
"opencode.model.variant": state.activeVariant, "opencode.model.variant": state.activeVariant,
}) })
@ -298,7 +337,12 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise<void> {
state.model = model state.model = model
state.activeVariant = undefined state.activeVariant = undefined
state.variants = variantsFor(state.providers, model) state.variants = variantsFor(state.providers, model)
const switching = resolveSavedVariant(model).then((saved) => { const switching = AppRuntime.runPromise(
Effect.gen(function* () {
const variantSvc = yield* Variant.Service
return yield* variantSvc.resolveSavedVariant(model).pipe(Effect.orElseSucceed(() => undefined))
}).pipe(Effect.provide(BootLayer)),
).then((saved) => {
const current = state.model const current = state.model
if (!current || current.providerID !== model.providerID || current.modelID !== model.modelID) { if (!current || current.providerID !== model.providerID || current.modelID !== model.modelID) {
return return
@ -343,7 +387,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise<void> {
} }
state.activeVariant = variant state.activeVariant = variant
saveVariant(state.model, state.activeVariant) persistVariant(state.model, state.activeVariant)
setRunSpanAttributes(span, { setRunSpanAttributes(span, {
"opencode.model.variant": state.activeVariant, "opencode.model.variant": state.activeVariant,
}) })

View file

@ -9,7 +9,6 @@
import path from "path" import path from "path"
import { AppFileSystem } from "@opencode-ai/core/filesystem" import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Context, Effect, Layer } from "effect" import { Context, Effect, Layer } from "effect"
import { makeRuntime } from "@/effect/run-service"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { isRecord } from "@/util/record" import { isRecord } from "@/util/record"
import { createSession, sessionVariant, type RunSession, type SessionMessages } from "./session.shared" import { createSession, sessionVariant, type RunSession, type SessionMessages } from "./session.shared"
@ -20,16 +19,13 @@ const MODEL_FILE = path.join(Global.Path.state, "model.json")
type ModelState = Record<string, unknown> & { type ModelState = Record<string, unknown> & {
variant?: Record<string, string | undefined> variant?: Record<string, string | undefined>
} }
type VariantService = {
export interface Interface {
readonly resolveSavedVariant: (model: RunInput["model"]) => Effect.Effect<string | undefined> readonly resolveSavedVariant: (model: RunInput["model"]) => Effect.Effect<string | undefined>
readonly saveVariant: (model: RunInput["model"], variant: string | undefined) => Effect.Effect<void> readonly saveVariant: (model: RunInput["model"], variant: string | undefined) => Effect.Effect<void>
} }
type VariantRuntime = {
resolveSavedVariant(model: RunInput["model"]): Promise<string | undefined>
saveVariant(model: RunInput["model"], variant: string | undefined): Promise<void>
}
class Service extends Context.Service<Service, VariantService>()("@opencode/RunVariant") {} export class Service extends Context.Service<Service, Interface>()("@opencode/RunVariant") {}
function modelKey(provider: string, model: string): string { function modelKey(provider: string, model: string): string {
return `${provider}/${model}` return `${provider}/${model}`
@ -135,9 +131,7 @@ function state(value: unknown): ModelState {
} }
} }
function createLayer(fs = AppFileSystem.defaultLayer) { export const layer = Layer.effect(
return Layer.fresh(
Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const file = yield* AppFileSystem.Service const file = yield* AppFileSystem.Service
@ -191,25 +185,8 @@ function createLayer(fs = AppFileSystem.defaultLayer) {
saveVariant, saveVariant,
}) })
}), }),
).pipe(Layer.provide(fs)), )
)
}
/** @internal Exported for testing. */ export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
export function createVariantRuntime(fs = AppFileSystem.defaultLayer): VariantRuntime {
const runtime = makeRuntime(Service, createLayer(fs))
return {
resolveSavedVariant: (model) => runtime.runPromise((svc) => svc.resolveSavedVariant(model)).catch(() => undefined),
saveVariant: (model, variant) => runtime.runPromise((svc) => svc.saveVariant(model, variant)).catch(() => {}),
}
}
const runtime = createVariantRuntime() export * as Variant from "./variant.shared"
export async function resolveSavedVariant(model: RunInput["model"]): Promise<string | undefined> {
return runtime.resolveSavedVariant(model)
}
export function saveVariant(model: RunInput["model"], variant: string | undefined): void {
void runtime.saveVariant(model, variant)
}

View file

@ -1,12 +1,14 @@
import { afterEach, describe, expect, mock, spyOn, test } from "bun:test" import { afterEach, describe, expect, mock, spyOn } from "bun:test"
import type { KeyEvent, Renderable } from "@opentui/core" import type { KeyEvent, Renderable } from "@opentui/core"
import type { Binding } from "@opentui/keymap" import type { Binding } from "@opentui/keymap"
import { createBindingLookup } from "@opentui/keymap/extras" import { createBindingLookup } from "@opentui/keymap/extras"
import { OpencodeClient, type Provider } from "@opencode-ai/sdk/v2" import { OpencodeClient, type Provider } from "@opencode-ai/sdk/v2"
import { Effect } from "effect"
import { TuiConfig, type Resolved } from "@/cli/cmd/tui/config/tui" import { TuiConfig, type Resolved } from "@/cli/cmd/tui/config/tui"
import { formatBindings } from "@/cli/cmd/run/keymap.shared" import { formatBindings } from "@/cli/cmd/run/keymap.shared"
import { TuiKeybind } from "@/cli/cmd/tui/config/keybind" import { TuiKeybind } from "@/cli/cmd/tui/config/keybind"
import { resolveDiffStyle, resolveFooterKeybinds, resolveModelInfo } from "@/cli/cmd/run/runtime.boot" import { RunBoot } from "@/cli/cmd/run/runtime.boot"
import { testEffect } from "../../lib/effect"
type RunBinding = Binding<Renderable, KeyEvent> type RunBinding = Binding<Renderable, KeyEvent>
@ -102,12 +104,15 @@ function config(input?: {
} }
} }
const it = testEffect(RunBoot.defaultLayer)
describe("run runtime boot", () => { describe("run runtime boot", () => {
afterEach(() => { afterEach(() => {
mock.restore() mock.restore()
}) })
test("reads footer keybinds from resolved keybind config", async () => { it.live("reads footer keybinds from resolved keybind config", () =>
Effect.gen(function* () {
spyOn(TuiConfig, "get").mockResolvedValue( spyOn(TuiConfig, "get").mockResolvedValue(
config({ config({
leader: "ctrl+g", leader: "ctrl+g",
@ -124,7 +129,8 @@ describe("run runtime boot", () => {
}), }),
) )
const result = await resolveFooterKeybinds() const boot = yield* RunBoot.Service
const result = yield* boot.resolveFooterKeybinds()
expect(result.leader).toBe("ctrl+g") expect(result.leader).toBe("ctrl+g")
expect(result.leaderTimeout).toBe(2000) expect(result.leaderTimeout).toBe(2000)
@ -136,12 +142,15 @@ describe("run runtime boot", () => {
expect(formatBindings(result.inputClear, result.leader)).toBe("ctrl+l") expect(formatBindings(result.inputClear, result.leader)).toBe("ctrl+l")
expect(formatBindings(result.inputSubmit, result.leader)).toBe("ctrl+s") expect(formatBindings(result.inputSubmit, result.leader)).toBe("ctrl+s")
expect(formatBindings(result.inputNewline, result.leader)).toBe("alt+return") expect(formatBindings(result.inputNewline, result.leader)).toBe("alt+return")
}) }),
)
test("falls back to default keybinds when config load fails", async () => { it.live("falls back to default keybinds when config load fails", () =>
Effect.gen(function* () {
spyOn(TuiConfig, "get").mockRejectedValue(new Error("boom")) spyOn(TuiConfig, "get").mockRejectedValue(new Error("boom"))
const result = await resolveFooterKeybinds() const boot = yield* RunBoot.Service
const result = yield* boot.resolveFooterKeybinds()
expect(result.leader).toBe("ctrl+x") expect(result.leader).toBe("ctrl+x")
expect(result.leaderTimeout).toBe(2000) expect(result.leaderTimeout).toBe(2000)
@ -153,18 +162,23 @@ describe("run runtime boot", () => {
expect(formatBindings(result.inputClear, result.leader)).toBe("ctrl+c") expect(formatBindings(result.inputClear, result.leader)).toBe("ctrl+c")
expect(formatBindings(result.inputSubmit, result.leader)).toBe("return") expect(formatBindings(result.inputSubmit, result.leader)).toBe("return")
expect(formatBindings(result.inputNewline, result.leader)).toBe("shift+return, ctrl+return, alt+return, ctrl+j") expect(formatBindings(result.inputNewline, result.leader)).toBe("shift+return, ctrl+return, alt+return, ctrl+j")
}) }),
)
test("reads diff style and falls back to auto", async () => { it.live("reads diff style and falls back to auto", () =>
Effect.gen(function* () {
spyOn(TuiConfig, "get").mockResolvedValue(config({ diff_style: "stacked" })) spyOn(TuiConfig, "get").mockResolvedValue(config({ diff_style: "stacked" }))
await expect(resolveDiffStyle()).resolves.toBe("stacked") const boot = yield* RunBoot.Service
expect(yield* boot.resolveDiffStyle()).toBe("stacked")
mock.restore() mock.restore()
spyOn(TuiConfig, "get").mockRejectedValue(new Error("boom")) spyOn(TuiConfig, "get").mockRejectedValue(new Error("boom"))
await expect(resolveDiffStyle()).resolves.toBe("auto") expect(yield* boot.resolveDiffStyle()).toBe("auto")
}) }),
)
test("prefers configured providers for model selector data", async () => { it.live("prefers configured providers for model selector data", () =>
Effect.gen(function* () {
const sdk = new OpencodeClient() const sdk = new OpencodeClient()
const data: { const data: {
all: Provider[] all: Provider[]
@ -220,7 +234,9 @@ describe("run runtime boot", () => {
}), }),
) )
await expect(resolveModelInfo(sdk, "/workspace", { providerID: "openai", modelID: "gpt-5" })).resolves.toEqual({ const boot = yield* RunBoot.Service
const result = yield* boot.resolveModelInfo(sdk, "/workspace", { providerID: "openai", modelID: "gpt-5" })
expect(result).toEqual({
providers: configured.providers, providers: configured.providers,
variants: ["high", "minimal"], variants: ["high", "minimal"],
limits: { limits: {
@ -228,9 +244,11 @@ describe("run runtime boot", () => {
}, },
}) })
expect(list).not.toHaveBeenCalled() expect(list).not.toHaveBeenCalled()
}) }),
)
test("falls back to provider list when configured providers are unavailable", async () => { it.live("falls back to provider list when configured providers are unavailable", () =>
Effect.gen(function* () {
const sdk = new OpencodeClient() const sdk = new OpencodeClient()
const data: { const data: {
all: Provider[] all: Provider[]
@ -275,7 +293,9 @@ describe("run runtime boot", () => {
}), }),
) )
await expect(resolveModelInfo(sdk, "/workspace", { providerID: "openai", modelID: "gpt-5" })).resolves.toEqual({ const boot = yield* RunBoot.Service
const result = yield* boot.resolveModelInfo(sdk, "/workspace", { providerID: "openai", modelID: "gpt-5" })
expect(result).toEqual({
providers: data.all, providers: data.all,
variants: ["high", "minimal"], variants: ["high", "minimal"],
limits: { limits: {
@ -283,5 +303,7 @@ describe("run runtime boot", () => {
"anthropic/sonnet": 200000, "anthropic/sonnet": 200000,
}, },
}) })
}) }),
)
}) })

View file

@ -4,13 +4,7 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"
import { Effect, FileSystem, Layer } from "effect" import { Effect, FileSystem, Layer } from "effect"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { import { Variant, cycleVariant, formatModelLabel, pickVariant, resolveVariant } from "@/cli/cmd/run/variant.shared"
createVariantRuntime,
cycleVariant,
formatModelLabel,
pickVariant,
resolveVariant,
} from "@/cli/cmd/run/variant.shared"
import type { SessionMessages } from "@/cli/cmd/run/session.shared" import type { SessionMessages } from "@/cli/cmd/run/session.shared"
import type { RunProvider } from "@/cli/cmd/run/types" import type { RunProvider } from "@/cli/cmd/run/types"
import { testEffect } from "../../lib/effect" import { testEffect } from "../../lib/effect"
@ -171,10 +165,10 @@ describe("run variant shared", () => {
}, },
}) })
const svc = createVariantRuntime(remappedFs(root)) yield* Effect.gen(function* () {
const svc = yield* Variant.Service
yield* Effect.promise(() => svc.saveVariant(model, "high")) yield* svc.saveVariant(model, "high")
expect(yield* Effect.promise(() => svc.resolveSavedVariant(model))).toBe("high") expect(yield* svc.resolveSavedVariant(model)).toBe("high")
expect(yield* fs.readJson(file)).toEqual({ expect(yield* fs.readJson(file)).toEqual({
recent: [{ providerID: "anthropic", modelID: "sonnet" }], recent: [{ providerID: "anthropic", modelID: "sonnet" }],
variant: { variant: {
@ -183,14 +177,15 @@ describe("run variant shared", () => {
}, },
}) })
yield* Effect.promise(() => svc.saveVariant(model, undefined)) yield* svc.saveVariant(model, undefined)
expect(yield* Effect.promise(() => svc.resolveSavedVariant(model))).toBeUndefined() expect(yield* svc.resolveSavedVariant(model)).toBeUndefined()
expect(yield* fs.readJson(file)).toEqual({ expect(yield* fs.readJson(file)).toEqual({
recent: [{ providerID: "anthropic", modelID: "sonnet" }], recent: [{ providerID: "anthropic", modelID: "sonnet" }],
variant: { variant: {
"openai/gpt-4.1": "low", "openai/gpt-4.1": "low",
}, },
}) })
}).pipe(Effect.provide(Variant.layer.pipe(Layer.provide(remappedFs(root)))))
}), }),
) )
@ -203,15 +198,16 @@ describe("run variant shared", () => {
yield* filesys.writeFileString(file, "{") yield* filesys.writeFileString(file, "{")
const svc = createVariantRuntime(remappedFs(root)) yield* Effect.gen(function* () {
const svc = yield* Variant.Service
yield* Effect.promise(() => svc.saveVariant(model, "high")) yield* svc.saveVariant(model, "high")
expect(yield* Effect.promise(() => svc.resolveSavedVariant(model))).toBe("high") expect(yield* svc.resolveSavedVariant(model)).toBe("high")
expect(yield* fs.readJson(file)).toEqual({ expect(yield* fs.readJson(file)).toEqual({
variant: { variant: {
"openai/gpt-5": "high", "openai/gpt-5": "high",
}, },
}) })
}).pipe(Effect.provide(Variant.layer.pipe(Layer.provide(remappedFs(root)))))
}), }),
) )
}) })