import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" import fs from "fs/promises" import path from "path" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { FSUtil } from "@opencode-ai/core/fs-util" import { Global } from "@opencode-ai/core/global" 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" const it = testEffect(Layer.empty) const instructionLayer = (input: { config: string locationServiceLayer: Layer.Layer filesystemLayer?: Layer.Layer }) => AppNodeBuilder.build(LayerNode.group([SystemContextRegistry.node, InstructionContext.node]), [ [Global.node, Global.layerWith({ config: input.config })], [Location.node, input.locationServiceLayer], ...(input.filesystemLayer ? [[FSUtil.node, input.filesystemLayer] as const] : []), ]) describe("InstructionContext", () => { it.live("loads global and upward project AGENTS.md files as one aggregate context", () => Effect.acquireRelease( Effect.promise(() => tmpdir()), (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), ).pipe( Effect.flatMap((tmp) => Effect.gen(function* () { const global = path.join(tmp.path, "global") const project = path.join(tmp.path, "project") const directory = path.join(project, "packages", "core") const outside = path.join(tmp.path, "AGENTS.md") const globalFile = path.join(global, "AGENTS.md") const projectFile = path.join(project, "AGENTS.md") const packageFile = path.join(directory, "AGENTS.md") yield* Effect.promise(async () => { await fs.mkdir(global, { recursive: true }) await fs.mkdir(directory, { recursive: true }) await fs.writeFile(outside, "outside") await fs.writeFile(globalFile, "global") await fs.writeFile(projectFile, "project") await fs.writeFile(packageFile, "package") }) const load = SystemContextRegistry.Service.pipe( Effect.flatMap((service) => service.load()), Effect.provide( instructionLayer({ config: global, locationServiceLayer: Layer.succeed( Location.Service, Location.Service.of( location( { directory: AbsolutePath.make(directory) }, { projectDirectory: AbsolutePath.make(project) }, ), ), ), }), ), ) const initialized = yield* SystemContext.initialize(yield* load) expect(initialized.baseline).toBe( [ `Instructions from: ${globalFile}\nglobal`, `Instructions from: ${packageFile}\npackage`, `Instructions from: ${projectFile}\nproject`, ].join("\n\n"), ) expect(initialized.baseline).not.toContain("outside") yield* Effect.promise(() => fs.writeFile(packageFile, "changed")) expect(yield* SystemContext.reconcile(yield* load, initialized.snapshot)).toMatchObject({ _tag: "Updated", text: expect.stringContaining(`Instructions from: ${packageFile}\nchanged`), }) yield* Effect.promise(() => fs.rm(packageFile)) const partial = yield* SystemContext.reconcile(yield* load, initialized.snapshot) expect(partial).toEqual({ _tag: "Updated", text: [ "These instructions replace all previously loaded ambient instructions.", `Instructions from: ${globalFile}\nglobal`, `Instructions from: ${projectFile}\nproject`, ].join("\n\n"), snapshot: expect.any(Object), }) yield* Effect.promise(() => Promise.all([fs.rm(globalFile), fs.rm(projectFile)])) expect(yield* SystemContext.reconcile(yield* load, initialized.snapshot)).toEqual({ _tag: "Updated", text: "Previously loaded instructions no longer apply.", snapshot: {}, }) }), ), ), ) it.live("keeps an empty AGENTS.md as available context", () => Effect.acquireRelease( Effect.promise(() => tmpdir()), (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), ).pipe( Effect.flatMap((tmp) => Effect.gen(function* () { const file = path.join(tmp.path, "AGENTS.md") yield* Effect.promise(() => fs.writeFile(file, "")) const context = yield* SystemContextRegistry.Service.pipe( Effect.flatMap((service) => service.load()), Effect.provide( instructionLayer({ config: path.join(tmp.path, "global"), locationServiceLayer: Layer.succeed( Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(tmp.path) })), ), }), ), ) expect((yield* SystemContext.initialize(context)).baseline).toBe(`Instructions from: ${file}\n`) }), ), ), ) it.effect("preserves admitted instructions while observation is unavailable", () => Effect.gen(function* () { const failingFS = Layer.effect( FSUtil.Service, FSUtil.Service.pipe( Effect.map((fs) => FSUtil.Service.of({ ...fs, up: () => Effect.fail(new FSUtil.FileSystemError({ method: "up" })) }), ), ), ).pipe(Layer.provide(LayerNode.compile(FSUtil.node))) const context = yield* SystemContextRegistry.Service.pipe( Effect.flatMap((service) => service.load()), Effect.provide( instructionLayer({ config: "/global", filesystemLayer: failingFS, locationServiceLayer: Layer.succeed( Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("/repo") })), ), }), ), ) expect( yield* SystemContext.reconcile(context, { "core/instructions": { value: [{ path: "/repo/AGENTS.md", content: "old" }], removed: "Previously loaded instructions no longer apply.", }, }), ).toEqual({ _tag: "Unchanged" }) }), ) it.effect("preserves admitted instructions when a discovered file disappears before read", () => Effect.gen(function* () { const file = AbsolutePath.make("/repo/AGENTS.md") const racingFS = Layer.effect( FSUtil.Service, FSUtil.Service.pipe( Effect.map((fs) => FSUtil.Service.of({ ...fs, up: () => Effect.succeed([file]), readFileStringSafe: () => Effect.succeed(undefined), }), ), ), ).pipe(Layer.provide(LayerNode.compile(FSUtil.node))) const context = yield* SystemContextRegistry.Service.pipe( Effect.flatMap((service) => service.load()), Effect.provide( instructionLayer({ config: "/global", filesystemLayer: racingFS, locationServiceLayer: Layer.succeed( Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("/repo") })), ), }), ), ) expect( yield* SystemContext.reconcile(context, { "core/instructions": { value: [{ path: file, content: "old" }], removed: "Previously loaded instructions no longer apply.", }, }), ).toEqual({ _tag: "Unchanged" }) }), ) it.effect("canonicalizes upward discovery boundaries", () => Effect.gen(function* () { let observed: { targets: string[]; start: string; stop?: string } | undefined const observingFS = Layer.effect( FSUtil.Service, FSUtil.Service.pipe( Effect.map((fs) => FSUtil.Service.of({ ...fs, up: (options) => Effect.sync(() => { observed = options return [] }), }), ), ), ).pipe(Layer.provide(LayerNode.compile(FSUtil.node))) yield* SystemContextRegistry.Service.pipe( Effect.flatMap((service) => service.load()), Effect.provide( instructionLayer({ config: "/global", filesystemLayer: observingFS, locationServiceLayer: Layer.succeed( Location.Service, Location.Service.of( location({ directory: AbsolutePath.make("/repo/") }, { projectDirectory: AbsolutePath.make("/repo") }), ), ), }), ), ) expect(observed).toEqual({ targets: ["AGENTS.md"], start: FSUtil.resolve("/repo"), stop: FSUtil.resolve("/repo"), }) }), ) it.effect("honors the project instruction opt-out", () => Effect.gen(function* () { const previous = process.env.OPENCODE_DISABLE_PROJECT_CONFIG let scanned = false process.env.OPENCODE_DISABLE_PROJECT_CONFIG = "1" yield* SystemContextRegistry.Service.pipe( Effect.flatMap((service) => service.load()), Effect.provide( instructionLayer({ config: "/global", filesystemLayer: Layer.effect( FSUtil.Service, FSUtil.Service.pipe( Effect.map((fs) => FSUtil.Service.of({ ...fs, up: () => Effect.sync(() => ((scanned = true), [])) })), ), ).pipe(Layer.provide(LayerNode.compile(FSUtil.node))), locationServiceLayer: Layer.succeed( Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("/repo") })), ), }), ), Effect.ensuring( Effect.sync(() => { if (previous === undefined) delete process.env.OPENCODE_DISABLE_PROJECT_CONFIG else process.env.OPENCODE_DISABLE_PROJECT_CONFIG = previous }), ), ) expect(scanned).toBe(false) }), ) it.effect("does not discover project instructions outside the canonical project root", () => Effect.gen(function* () { let scanned = false yield* SystemContextRegistry.Service.pipe( Effect.flatMap((service) => service.load()), Effect.provide( instructionLayer({ config: "/global", filesystemLayer: Layer.effect( FSUtil.Service, FSUtil.Service.pipe( Effect.map((fs) => FSUtil.Service.of({ ...fs, up: () => Effect.sync(() => ((scanned = true), [])) })), ), ).pipe(Layer.provide(LayerNode.compile(FSUtil.node))), locationServiceLayer: Layer.succeed( Location.Service, Location.Service.of( location( { directory: AbsolutePath.make("/outside") }, { projectDirectory: AbsolutePath.make("/repo") }, ), ), ), }), ), ) expect(scanned).toBe(false) }), ) })