mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-23 18:55:37 -04:00
cfdb9ab705
Builtins and instruction context become ordinary Location-scoped services exposing load(), the same shape as skill, reference, and MCP guidance. The runner composes all six producers explicitly in loadSystemContext, so the full context roster is readable at one call site. Dynamic registration and scoped deregistration had no consumers.
323 lines
11 KiB
TypeScript
323 lines
11 KiB
TypeScript
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 { 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<Location.Service>
|
|
filesystemLayer?: Layer.Layer<FSUtil.Service>
|
|
}) =>
|
|
AppNodeBuilder.build(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 = InstructionContext.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.text).toBe(
|
|
[
|
|
`Instructions from: ${globalFile}\nglobal`,
|
|
`Instructions from: ${packageFile}\npackage`,
|
|
`Instructions from: ${projectFile}\nproject`,
|
|
].join("\n\n"),
|
|
)
|
|
expect(initialized.text).not.toContain("outside")
|
|
|
|
yield* Effect.promise(() => fs.writeFile(packageFile, "changed"))
|
|
expect(yield* SystemContext.reconcile(yield* load, initialized.applied)).toMatchObject({
|
|
_tag: "Updated",
|
|
text: expect.stringContaining(`Instructions from: ${packageFile}\nchanged`),
|
|
})
|
|
|
|
yield* Effect.promise(() => fs.rm(packageFile))
|
|
const partial = yield* SystemContext.reconcile(yield* load, initialized.applied)
|
|
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"),
|
|
applied: expect.any(Object),
|
|
})
|
|
|
|
yield* Effect.promise(() => Promise.all([fs.rm(globalFile), fs.rm(projectFile)]))
|
|
expect(yield* SystemContext.reconcile(yield* load, initialized.applied)).toEqual({
|
|
_tag: "Updated",
|
|
text: "Previously loaded instructions no longer apply.",
|
|
applied: {},
|
|
})
|
|
}),
|
|
),
|
|
),
|
|
)
|
|
|
|
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* InstructionContext.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)).text).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* InstructionContext.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* InstructionContext.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* InstructionContext.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* InstructionContext.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* InstructionContext.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)
|
|
}),
|
|
)
|
|
})
|