diff --git a/packages/core/src/instruction-context.ts b/packages/core/src/instruction-context.ts index b0cc330678..94d9e787ce 100644 --- a/packages/core/src/instruction-context.ts +++ b/packages/core/src/instruction-context.ts @@ -1,6 +1,6 @@ export * as InstructionContext from "./instruction-context" -import { Array, Effect, Layer, Schema } from "effect" +import { Array, Context, Effect, Layer, Schema } from "effect" import { isAbsolute, join, relative, sep } from "path" import { FSUtil } from "./fs-util" import { Flag } from "./flag/flag" @@ -8,7 +8,6 @@ import { Global } from "./global" import { Location } from "./location" import { AbsolutePath } from "./schema" import { SystemContext } from "./system-context/index" -import { SystemContextRegistry } from "./system-context/registry" import { makeLocationNode } from "./effect/app-node" class File extends Schema.Class("InstructionContext.File")({ @@ -19,12 +18,18 @@ class File extends Schema.Class("InstructionContext.File")({ const Files = Schema.Array(File) const key = SystemContext.Key.make("core/instructions") -const layer = Layer.effectDiscard( +export interface Interface { + readonly load: () => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/InstructionContext") {} + +const layer = Layer.effect( + Service, Effect.gen(function* () { const fs = yield* FSUtil.Service const global = yield* Global.Service const location = yield* Location.Service - const registry = yield* SystemContextRegistry.Service const source = (value: ReadonlyArray | SystemContext.Unavailable) => SystemContext.make({ @@ -71,28 +76,24 @@ const layer = Layer.effectDiscard( return files.filter((file): file is File => file !== undefined) }) - yield* registry.register({ - key, - load: observe().pipe( - Effect.map((files) => - files === SystemContext.unavailable - ? source(files) - : files.length === 0 - ? SystemContext.empty - : source(files), + return Service.of({ + load: () => + observe().pipe( + Effect.map((files) => + files === SystemContext.unavailable + ? source(files) + : files.length === 0 + ? SystemContext.empty + : source(files), + ), + Effect.catch(() => Effect.succeed(source(SystemContext.unavailable))), + Effect.catchDefect(() => Effect.succeed(source(SystemContext.unavailable))), ), - Effect.catch(() => Effect.succeed(source(SystemContext.unavailable))), - Effect.catchDefect(() => Effect.succeed(source(SystemContext.unavailable))), - ), }) }), ) -export const node = makeLocationNode({ - name: "instruction-context", - layer, - deps: [FSUtil.node, Global.node, Location.node, SystemContextRegistry.node], -}) +export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node, Global.node, Location.node] }) function render(files: ReadonlyArray) { return files.map((file) => `Instructions from: ${file.path}\n${file.content}`).join("\n\n") diff --git a/packages/core/src/location-services.ts b/packages/core/src/location-services.ts index 0d913c10fb..c4d177ef25 100644 --- a/packages/core/src/location-services.ts +++ b/packages/core/src/location-services.ts @@ -36,8 +36,8 @@ import { SessionTodo } from "./session/todo" import { SkillV2 } from "./skill" import { SkillGuidance } from "./skill/guidance" import { Snapshot } from "./snapshot" +import { InstructionContext } from "./instruction-context" import { SystemContextBuiltIns } from "./system-context/builtins" -import { SystemContextRegistry } from "./system-context/registry" import { SessionInstructions } from "./session/instructions" import { BuiltInTools } from "./tool/builtins" import { McpTool } from "./tool/mcp" @@ -68,8 +68,8 @@ export const locationServices = LayerNode.group([ Pty.node, Shell.node, SkillV2.node, - SystemContextRegistry.node, SystemContextBuiltIns.node, + InstructionContext.node, LocationMutation.node, FileMutation.node, MCP.node, diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index 7b34f1aedb..cb89e9f9af 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -18,7 +18,8 @@ import { ModelV2 } from "../../model" import { ProviderV2 } from "../../provider" import { QuestionV2 } from "../../question" import { SystemContext } from "../../system-context/index" -import { SystemContextRegistry } from "../../system-context/registry" +import { SystemContextBuiltIns } from "../../system-context/builtins" +import { InstructionContext } from "../../instruction-context" import { SkillGuidance } from "../../skill/guidance" import { ReferenceGuidance } from "../../reference/guidance" import { McpGuidance } from "../../mcp/guidance" @@ -102,7 +103,8 @@ const layer = Layer.effect( const models = yield* SessionRunnerModel.Service const store = yield* SessionStore.Service const location = yield* Location.Service - const systemContext = yield* SystemContextRegistry.Service + const builtins = yield* SystemContextBuiltIns.Service + const instructions = yield* InstructionContext.Service const skillGuidance = yield* SkillGuidance.Service const referenceGuidance = yield* ReferenceGuidance.Service const mcpGuidance = yield* McpGuidance.Service @@ -154,9 +156,16 @@ const layer = Layer.effect( cause.reasons.some((reason) => Cause.isDieReason(reason) && reason.defect instanceof QuestionV2.RejectedError) const loadSystemContext = (agent: AgentV2.Selection) => - Effect.all([systemContext.load(), skillGuidance.load(agent), referenceGuidance.load(), mcpGuidance.load(agent)], { - concurrency: "unbounded", - }).pipe(Effect.map(SystemContext.combine)) + Effect.all( + [ + builtins.load(), + instructions.load(), + skillGuidance.load(agent), + referenceGuidance.load(), + mcpGuidance.load(agent), + ], + { concurrency: "unbounded" }, + ).pipe(Effect.map(SystemContext.combine)) const runTurnAttempt = Effect.fn("SessionRunner.runTurn")(function* ( sessionID: SessionSchema.ID, @@ -467,7 +476,8 @@ export const node = makeLocationNode({ SessionRunnerModel.node, SessionStore.node, Location.node, - SystemContextRegistry.node, + SystemContextBuiltIns.node, + InstructionContext.node, SkillGuidance.node, ReferenceGuidance.node, McpGuidance.node, diff --git a/packages/core/src/system-context/builtins.ts b/packages/core/src/system-context/builtins.ts index b8b50577cc..1cf470fb4a 100644 --- a/packages/core/src/system-context/builtins.ts +++ b/packages/core/src/system-context/builtins.ts @@ -1,18 +1,20 @@ export * as SystemContextBuiltIns from "./builtins" import { makeLocationNode } from "../effect/app-node" -import { DateTime, Effect, Layer, Schema } from "effect" +import { Context, DateTime, Effect, Layer, Schema } from "effect" import { Location } from "../location" import { SystemContext } from "./index" -import { InstructionContext } from "../instruction-context" -import { SystemContextRegistry } from "./registry" -import { FSUtil } from "../fs-util" -import { Global } from "../global" -const builtIns = Layer.effectDiscard( +export interface Interface { + readonly load: () => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/SystemContextBuiltIns") {} + +const layer = Layer.effect( + Service, Effect.gen(function* () { const location = yield* Location.Service - const registry = yield* SystemContextRegistry.Service const environment = [ "", ` Working directory: ${location.directory}`, @@ -39,12 +41,8 @@ const builtIns = Layer.effectDiscard( }), ]) - yield* registry.register({ key: SystemContext.Key.make("core/builtins"), load: Effect.succeed(context) }) + return Service.of({ load: () => Effect.succeed(context) }) }), ) -export const node = makeLocationNode({ - name: "system-context-builtins", - layer: builtIns, - deps: [Location.node, SystemContextRegistry.node, InstructionContext.node, FSUtil.node, Global.node], -}) +export const node = makeLocationNode({ service: Service, layer, deps: [Location.node] }) diff --git a/packages/core/src/system-context/registry.ts b/packages/core/src/system-context/registry.ts deleted file mode 100644 index c1e7ca5e85..0000000000 --- a/packages/core/src/system-context/registry.ts +++ /dev/null @@ -1,49 +0,0 @@ -export * as SystemContextRegistry from "./registry" - -import { Context, Effect, Layer, Ref, Scope } from "effect" -import { SystemContext } from "./index" -import { makeLocationNode } from "../effect/app-node" - -export interface Entry { - readonly key: SystemContext.Key - readonly load: Effect.Effect -} - -export interface Interface { - readonly register: (entry: Entry) => Effect.Effect - readonly load: () => Effect.Effect -} - -export class Service extends Context.Service()("@opencode/v2/SystemContextRegistry") {} - -const layer = Layer.effect( - Service, - Effect.gen(function* () { - const entries = yield* Ref.make>([]) - - return Service.of({ - register: Effect.fn("SystemContextRegistry.register")(function* (entry) { - yield* Effect.acquireRelease( - Ref.modify(entries, (current) => { - if (current.some((item) => item.key === entry.key)) return [false, current] - return [true, [...current, entry]] - }).pipe( - Effect.flatMap((added) => - added ? Effect.void : Effect.die(`Duplicate system context entry key: ${entry.key}`), - ), - Effect.as(entry), - ), - (entry) => Ref.update(entries, (current) => current.filter((item) => item !== entry)), - ) - }), - load: Effect.fn("SystemContextRegistry.load")(function* () { - const current = (yield* Ref.get(entries)).toSorted((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)) - return SystemContext.combine( - yield* Effect.forEach(current, (entry) => entry.load, { concurrency: "unbounded" }), - ) - }), - }) - }), -) - -export const node = makeLocationNode({ service: Service, layer, deps: [] }) diff --git a/packages/core/test/instruction-context.test.ts b/packages/core/test/instruction-context.test.ts index 1c3c764ee9..8bd04d305a 100644 --- a/packages/core/test/instruction-context.test.ts +++ b/packages/core/test/instruction-context.test.ts @@ -10,7 +10,6 @@ import { InstructionContext } from "@opencode-ai/core/instruction-context" import { Location } from "@opencode-ai/core/location" import { AbsolutePath } from "@opencode-ai/core/schema" import { SystemContext } from "@opencode-ai/core/system-context" -import { SystemContextRegistry } from "@opencode-ai/core/system-context/registry" import { location } from "./fixture/location" import { tmpdir } from "./fixture/tmpdir" import { testEffect } from "./lib/effect" @@ -22,7 +21,7 @@ const instructionLayer = (input: { locationServiceLayer: Layer.Layer filesystemLayer?: Layer.Layer }) => - AppNodeBuilder.build(LayerNode.group([SystemContextRegistry.node, InstructionContext.node]), [ + AppNodeBuilder.build(InstructionContext.node, [ [Global.node, Global.layerWith({ config: input.config })], [Location.node, input.locationServiceLayer], ...(input.filesystemLayer ? [[FSUtil.node, input.filesystemLayer] as const] : []), @@ -52,7 +51,7 @@ describe("InstructionContext", () => { await fs.writeFile(packageFile, "package") }) - const load = SystemContextRegistry.Service.pipe( + const load = InstructionContext.Service.pipe( Effect.flatMap((service) => service.load()), Effect.provide( instructionLayer({ @@ -118,7 +117,7 @@ describe("InstructionContext", () => { Effect.gen(function* () { const file = path.join(tmp.path, "AGENTS.md") yield* Effect.promise(() => fs.writeFile(file, "")) - const context = yield* SystemContextRegistry.Service.pipe( + const context = yield* InstructionContext.Service.pipe( Effect.flatMap((service) => service.load()), Effect.provide( instructionLayer({ @@ -147,7 +146,7 @@ describe("InstructionContext", () => { ), ), ).pipe(Layer.provide(LayerNode.compile(FSUtil.node))) - const context = yield* SystemContextRegistry.Service.pipe( + const context = yield* InstructionContext.Service.pipe( Effect.flatMap((service) => service.load()), Effect.provide( instructionLayer({ @@ -187,7 +186,7 @@ describe("InstructionContext", () => { ), ), ).pipe(Layer.provide(LayerNode.compile(FSUtil.node))) - const context = yield* SystemContextRegistry.Service.pipe( + const context = yield* InstructionContext.Service.pipe( Effect.flatMap((service) => service.load()), Effect.provide( instructionLayer({ @@ -231,7 +230,7 @@ describe("InstructionContext", () => { ), ).pipe(Layer.provide(LayerNode.compile(FSUtil.node))) - yield* SystemContextRegistry.Service.pipe( + yield* InstructionContext.Service.pipe( Effect.flatMap((service) => service.load()), Effect.provide( instructionLayer({ @@ -261,7 +260,7 @@ describe("InstructionContext", () => { let scanned = false process.env.OPENCODE_DISABLE_PROJECT_CONFIG = "1" - yield* SystemContextRegistry.Service.pipe( + yield* InstructionContext.Service.pipe( Effect.flatMap((service) => service.load()), Effect.provide( instructionLayer({ @@ -293,7 +292,7 @@ describe("InstructionContext", () => { it.effect("does not discover project instructions outside the canonical project root", () => Effect.gen(function* () { let scanned = false - yield* SystemContextRegistry.Service.pipe( + yield* InstructionContext.Service.pipe( Effect.flatMap((service) => service.load()), Effect.provide( instructionLayer({ diff --git a/packages/core/test/session-runner-recorded.test.ts b/packages/core/test/session-runner-recorded.test.ts index 7366105a7c..57e0ba42f6 100644 --- a/packages/core/test/session-runner-recorded.test.ts +++ b/packages/core/test/session-runner-recorded.test.ts @@ -31,7 +31,8 @@ import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" import { SessionTable } from "@opencode-ai/core/session/sql" import { SessionStore } from "@opencode-ai/core/session/store" import { Location } from "@opencode-ai/core/location" -import { SystemContextRegistry } from "@opencode-ai/core/system-context/registry" +import { SystemContextBuiltIns } from "@opencode-ai/core/system-context/builtins" +import { InstructionContext } from "@opencode-ai/core/instruction-context" import { SystemContext } from "@opencode-ai/core/system-context" import { SkillGuidance } from "@opencode-ai/core/skill/guidance" import { ReferenceGuidance } from "@opencode-ai/core/reference/guidance" @@ -72,7 +73,8 @@ const model = OpenAIChat.route }) .model({ id: "gpt-4o-mini" }) const models = SessionRunnerModel.layerWith(() => Effect.succeed(model)) -const systemContext = AppNodeBuilder.build(SystemContextRegistry.node) +const systemContext = Layer.mock(SystemContextBuiltIns.Service, { load: () => Effect.succeed(SystemContext.empty) }) +const instructionContext = Layer.mock(InstructionContext.Service, { load: () => Effect.succeed(SystemContext.empty) }) const skillGuidance = Layer.mock(SkillGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) }) const referenceGuidance = Layer.mock(ReferenceGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) }) const mcpGuidance = Layer.mock(McpGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) }) @@ -81,7 +83,8 @@ const runnerLayer = AppNodeBuilder.build(SessionRunnerLLM.node, [ [Snapshot.node, Snapshot.noopLayer], [LayerNodePlatform.llmClient, client], [SessionRunnerModel.node, models], - [SystemContextRegistry.node, systemContext], + [SystemContextBuiltIns.node, systemContext], + [InstructionContext.node, instructionContext], [Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })], [SkillGuidance.node, skillGuidance], [ReferenceGuidance.node, referenceGuidance], @@ -116,7 +119,8 @@ const it = testEffect( AgentV2.node, ToolRegistry.node, SessionRunnerModel.node, - SystemContextRegistry.node, + SystemContextBuiltIns.node, + InstructionContext.node, SkillGuidance.node, ReferenceGuidance.node, Config.node, @@ -129,7 +133,8 @@ const it = testEffect( [PermissionV2.node, permission], [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], [SessionRunnerModel.node, models], - [SystemContextRegistry.node, systemContext], + [SystemContextBuiltIns.node, systemContext], + [InstructionContext.node, instructionContext], [Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })], [SkillGuidance.node, skillGuidance], [ReferenceGuidance.node, referenceGuidance], diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index f822fbe9c6..5830d4c699 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -52,7 +52,8 @@ import { } from "@opencode-ai/core/session/sql" import { SessionStore } from "@opencode-ai/core/session/store" import { SystemContext } from "@opencode-ai/core/system-context" -import { SystemContextRegistry } from "@opencode-ai/core/system-context/registry" +import { SystemContextBuiltIns } from "@opencode-ai/core/system-context/builtins" +import { InstructionContext } from "@opencode-ai/core/instruction-context" import { SkillGuidance } from "@opencode-ai/core/skill/guidance" import { ReferenceGuidance } from "@opencode-ai/core/reference/guidance" import { McpGuidance } from "@opencode-ai/core/mcp/guidance" @@ -176,35 +177,28 @@ let systemRemoved = false let systemUnavailable = false let systemLoadHook = Effect.void const skillBaselines = new Map() -const systemContext = Layer.effectDiscard( - SystemContextRegistry.Service.pipe( - Effect.flatMap((registry) => - registry.register({ - key: systemContextKey, - load: Effect.sync(() => - SystemContext.combine( - systemRemoved - ? [] - : [ - SystemContext.make({ - key: systemContextKey, - codec: Schema.toCodecJson(Schema.String), - load: systemLoadHook.pipe( - Effect.andThen( - Effect.sync(() => (systemUnavailable ? SystemContext.unavailable : systemBaseline)), - ), - ), - baseline: String, - update: (_previous, current) => current, - removed: () => "System context source removed: test/context", - }), - ], - ), - ), - }), +const systemContext = Layer.mock(SystemContextBuiltIns.Service, { + load: () => + Effect.sync(() => + SystemContext.combine( + systemRemoved + ? [] + : [ + SystemContext.make({ + key: systemContextKey, + codec: Schema.toCodecJson(Schema.String), + load: systemLoadHook.pipe( + Effect.andThen(Effect.sync(() => (systemUnavailable ? SystemContext.unavailable : systemBaseline))), + ), + baseline: String, + update: (_previous, current) => current, + removed: () => "System context source removed: test/context", + }), + ], + ), ), - ), -).pipe(Layer.provideMerge(AppNodeBuilder.build(SystemContextRegistry.node))) +}) +const instructionContext = Layer.mock(InstructionContext.Service, { load: () => Effect.succeed(SystemContext.empty) }) const skillGuidance = Layer.mock(SkillGuidance.Service, { load: (agent) => Effect.succeed( @@ -243,7 +237,8 @@ const runnerLayer = AppNodeBuilder.build(SessionRunnerLLM.node, [ [Snapshot.node, Snapshot.noopLayer], [LayerNodePlatform.llmClient, client], [SessionRunnerModel.node, models], - [SystemContextRegistry.node, systemContext], + [SystemContextBuiltIns.node, systemContext], + [InstructionContext.node, instructionContext], [Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })], [SkillGuidance.node, skillGuidance], [ReferenceGuidance.node, referenceGuidance], @@ -281,7 +276,8 @@ const it = testEffect( ToolRegistry.toolsNode, echoNode, SessionRunnerModel.node, - SystemContextRegistry.node, + SystemContextBuiltIns.node, + InstructionContext.node, SkillGuidance.node, ReferenceGuidance.node, Config.node, @@ -294,7 +290,8 @@ const it = testEffect( [LayerNodePlatform.llmClient, client], [PermissionV2.node, permission], [SessionRunnerModel.node, models], - [SystemContextRegistry.node, systemContext], + [SystemContextBuiltIns.node, systemContext], + [InstructionContext.node, instructionContext], [Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })], [SkillGuidance.node, skillGuidance], [ReferenceGuidance.node, referenceGuidance], diff --git a/packages/core/test/system-context/builtins.test.ts b/packages/core/test/system-context/builtins.test.ts index 8545c90449..2d4ac356b9 100644 --- a/packages/core/test/system-context/builtins.test.ts +++ b/packages/core/test/system-context/builtins.test.ts @@ -9,7 +9,7 @@ import { Global } from "@opencode-ai/core/global" import { AbsolutePath } from "@opencode-ai/core/schema" import { SystemContext } from "@opencode-ai/core/system-context" import { SystemContextBuiltIns } from "@opencode-ai/core/system-context/builtins" -import { SystemContextRegistry } from "@opencode-ai/core/system-context/registry" +import { InstructionContext } from "@opencode-ai/core/instruction-context" import { location } from "../fixture/location" import { testEffect } from "../lib/effect" @@ -27,7 +27,7 @@ const locationLayer = Layer.succeed( ), ), ) -const builtInsNode = LayerNode.group([SystemContextBuiltIns.node, SystemContextRegistry.node]) +const builtInsNode = LayerNode.group([SystemContextBuiltIns.node, InstructionContext.node]) const it = testEffect( AppNodeBuilder.build(builtInsNode, [ [Location.node, locationLayer], @@ -58,7 +58,7 @@ describe("SystemContextBuiltIns", () => { it.effect("loads location-scoped environment and host-local date context", () => Effect.gen(function* () { yield* TestClock.setTime(timestamp) - const context = yield* SystemContextRegistry.Service + const context = yield* SystemContextBuiltIns.Service const initialized = yield* SystemContext.initialize(yield* context.load()) expect(initialized.text).toBe( @@ -80,7 +80,7 @@ describe("SystemContextBuiltIns", () => { it.effect("reconciles the date without repeating unchanged environment context", () => Effect.gen(function* () { yield* TestClock.setTime(timestamp) - const context = yield* SystemContextRegistry.Service + const context = yield* SystemContextBuiltIns.Service const initialized = yield* SystemContext.initialize(yield* context.load()) yield* TestClock.setTime(timestamp + 24 * 60 * 60 * 1000) @@ -96,7 +96,7 @@ describe("SystemContextBuiltIns", () => { it.effect("does not update again within the same local calendar day", () => Effect.gen(function* () { yield* TestClock.setTime(timestamp) - const context = yield* SystemContextRegistry.Service + const context = yield* SystemContextBuiltIns.Service const initialized = yield* SystemContext.initialize(yield* context.load()) yield* TestClock.setTime(timestamp + 60 * 60 * 1000) @@ -107,7 +107,11 @@ describe("SystemContextBuiltIns", () => { itWithInstructions.effect("composes ambient instructions after built-in context", () => Effect.gen(function* () { yield* TestClock.setTime(timestamp) - const context = yield* SystemContextRegistry.Service + const builtIns = yield* SystemContextBuiltIns.Service + const instructions = yield* InstructionContext.Service + const context = { + load: () => Effect.all([builtIns.load(), instructions.load()]).pipe(Effect.map(SystemContext.combine)), + } expect((yield* SystemContext.initialize(yield* context.load())).text).toBe( [ diff --git a/packages/core/test/system-context/registry.test.ts b/packages/core/test/system-context/registry.test.ts deleted file mode 100644 index 3fd3bf5aac..0000000000 --- a/packages/core/test/system-context/registry.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { describe, expect } from "bun:test" -import { Cause, Effect, Exit, Schema, Scope } from "effect" -import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" -import { SystemContext } from "@opencode-ai/core/system-context" -import { SystemContextRegistry } from "@opencode-ai/core/system-context/registry" -import { testEffect } from "../lib/effect" - -const entry = (key: string, text: string, sourceKey = key) => ({ - key: SystemContext.Key.make(key), - load: Effect.succeed( - SystemContext.make({ - key: SystemContext.Key.make(sourceKey), - codec: Schema.toCodecJson(Schema.String), - load: Effect.succeed(text), - baseline: String, - update: (_previous, current) => current, - }), - ), -}) - -const it = testEffect(AppNodeBuilder.build(SystemContextRegistry.node)) - -describe("SystemContextRegistry", () => { - it.effect("loads empty system context when there are no entries", () => - Effect.gen(function* () { - const registry = yield* SystemContextRegistry.Service - - expect(yield* SystemContext.initialize(yield* registry.load())).toEqual({ text: "", applied: {} }) - }), - ) - - it.effect("loads scoped entries in stable key order", () => - Effect.gen(function* () { - const registry = yield* SystemContextRegistry.Service - yield* registry.register(entry("test/second", "second")) - yield* registry.register(entry("test/first", "first")) - - expect((yield* SystemContext.initialize(yield* registry.load())).text).toBe("first\n\nsecond") - }), - ) - - it.effect("re-evaluates entry producers on each load", () => - Effect.gen(function* () { - const registry = yield* SystemContextRegistry.Service - let loads = 0 - yield* registry.register({ - key: SystemContext.Key.make("test/dynamic"), - load: Effect.sync(() => { - loads++ - return SystemContext.empty - }), - }) - - yield* registry.load() - yield* registry.load() - - expect(loads).toBe(2) - }), - ) - - it.effect("propagates entry producer failures", () => - Effect.gen(function* () { - const registry = yield* SystemContextRegistry.Service - const failure = new Error("entry failed") - yield* registry.register({ key: SystemContext.Key.make("test/failure"), load: Effect.die(failure) }) - - const exit = yield* registry.load().pipe(Effect.exit) - - expect(Exit.isFailure(exit)).toBe(true) - if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBe(failure) - }), - ) - - it.effect("rejects duplicate source keys from separate entries", () => - Effect.gen(function* () { - const registry = yield* SystemContextRegistry.Service - yield* registry.register(entry("test/first", "first", "test/duplicate")) - yield* registry.register(entry("test/second", "second", "test/duplicate")) - - const exit = yield* registry.load().pipe(Effect.exit) - - expect(Exit.isFailure(exit)).toBe(true) - if (Exit.isFailure(exit)) { - expect(Cause.squash(exit.cause)).toBeInstanceOf(SystemContext.DuplicateKeyError) - expect(Cause.squash(exit.cause)).toMatchObject({ key: SystemContext.Key.make("test/duplicate") }) - } - }), - ) - - it.effect("rejects duplicate entry keys", () => - Effect.gen(function* () { - const registry = yield* SystemContextRegistry.Service - yield* registry.register(entry("test/duplicate", "first")) - - const exit = yield* registry.register(entry("test/duplicate", "second", "test/other")).pipe(Effect.exit) - - expect(Exit.isFailure(exit)).toBe(true) - if (Exit.isFailure(exit)) expect(Cause.pretty(exit.cause)).toContain("Duplicate system context entry key") - }), - ) - - it.effect("removes an entry when its owning scope closes", () => - Effect.gen(function* () { - const registry = yield* SystemContextRegistry.Service - const scope = yield* Scope.make() - yield* registry.register(entry("test/scoped", "scoped")).pipe(Scope.provide(scope)) - - expect((yield* SystemContext.initialize(yield* registry.load())).text).toBe("scoped") - - yield* Scope.close(scope, Exit.void) - expect(yield* SystemContext.initialize(yield* registry.load())).toEqual({ text: "", applied: {} }) - }), - ) -})