mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-24 06:33:01 -04:00
496 lines
19 KiB
TypeScript
496 lines
19 KiB
TypeScript
import { describe, expect } from "bun:test"
|
|
import { Deferred, Effect, Fiber, Layer, Stream } from "effect"
|
|
import fs from "fs/promises"
|
|
import path from "path"
|
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
|
import { Bus } from "@opencode-ai/core/bus"
|
|
import { ConfigInstructionPlugin } from "@opencode-ai/core/config/plugin/instruction"
|
|
import { Watcher } from "@opencode-ai/core/filesystem/watcher"
|
|
import { InstructionDiscovery } from "@opencode-ai/core/instruction-discovery"
|
|
import { Instructions } from "@opencode-ai/core/instructions/index"
|
|
import { Location } from "@opencode-ai/core/location"
|
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
|
import { FSUtil } from "@opencode-ai/util/fs-util"
|
|
import { Global } from "@opencode-ai/util/global"
|
|
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
|
import { tempGlobalLayer } from "./fixture/global"
|
|
import { location } from "./fixture/location"
|
|
import { tmpdir } from "./fixture/tmpdir"
|
|
import { readInitial, readUpdate, state } from "./lib/instructions"
|
|
import { testEffect } from "./lib/effect"
|
|
import { host } from "./plugin/host"
|
|
|
|
const it = testEffect(Layer.empty)
|
|
|
|
const instructionLayer = (input: {
|
|
config?: string
|
|
home?: string
|
|
locationServiceLayer: Layer.Layer<Location.Service>
|
|
filesystemLayer?: Layer.Layer<FSUtil.Service>
|
|
project?: boolean
|
|
}) => {
|
|
const watcher = Watcher.testLayer
|
|
return Layer.mergeAll(
|
|
AppNodeBuilder.build(
|
|
LayerNode.group([InstructionDiscovery.node, Bus.node, FSUtil.node, Global.node, Location.node, Watcher.node]),
|
|
[
|
|
[InstructionDiscovery.node, InstructionDiscovery.configured({ project: input.project })],
|
|
[
|
|
Global.node,
|
|
input.config || input.home
|
|
? Global.layerWith({
|
|
...(input.config ? { config: input.config } : {}),
|
|
...(input.home ? { home: input.home } : {}),
|
|
})
|
|
: tempGlobalLayer,
|
|
],
|
|
[Location.node, input.locationServiceLayer],
|
|
[Watcher.node, watcher],
|
|
...(input.filesystemLayer ? [[FSUtil.node, input.filesystemLayer] as const] : []),
|
|
],
|
|
),
|
|
watcher,
|
|
)
|
|
}
|
|
|
|
const start = Effect.fnUntraced(function* () {
|
|
yield* ConfigInstructionPlugin.Plugin.effect(host())
|
|
return yield* InstructionDiscovery.Service
|
|
})
|
|
|
|
const file = (path: string, content: string) =>
|
|
new InstructionDiscovery.File({ path: AbsolutePath.make(path), content })
|
|
|
|
function emitAndWait(update: Watcher.Update) {
|
|
return Effect.gen(function* () {
|
|
const watcher = yield* Watcher.Test
|
|
const bus = yield* Bus.Service
|
|
const updated = yield* Deferred.make<void>()
|
|
const fiber = yield* bus.subscribe(InstructionDiscovery.Event.Updated).pipe(
|
|
Stream.runForEach(() => Deferred.succeed(updated, undefined).pipe(Effect.asVoid)),
|
|
Effect.forkScoped,
|
|
)
|
|
yield* Effect.yieldNow
|
|
yield* watcher.emit(update)
|
|
yield* Deferred.await(updated).pipe(Effect.timeout("2 seconds"))
|
|
yield* Fiber.interrupt(fiber)
|
|
})
|
|
}
|
|
|
|
describe("InstructionDiscovery", () => {
|
|
it.effect("stores ordered values with last-write-wins precedence", () =>
|
|
Effect.gen(function* () {
|
|
const discovery = yield* InstructionDiscovery.Service
|
|
yield* discovery.transform((draft) => {
|
|
draft.add(file("/repo/AGENTS.md", "first"))
|
|
draft.add(file("/repo/packages/AGENTS.md", "package"))
|
|
draft.add(file("/repo/AGENTS.md", "last"))
|
|
draft.update("/repo/packages/AGENTS.md", (current) => {
|
|
current.content = "updated"
|
|
current.path = AbsolutePath.make("/ignored")
|
|
})
|
|
draft.remove("/missing")
|
|
})
|
|
|
|
expect(yield* discovery.list()).toEqual([
|
|
file("/repo/AGENTS.md", "last"),
|
|
file("/repo/packages/AGENTS.md", "updated"),
|
|
])
|
|
}).pipe(Effect.provide(AppNodeBuilder.build(LayerNode.group([InstructionDiscovery.node, Bus.node])))),
|
|
)
|
|
|
|
it.effect("preserves admitted values while the source is unavailable", () =>
|
|
Effect.gen(function* () {
|
|
const discovery = yield* InstructionDiscovery.Service
|
|
yield* discovery.transform((draft) => draft.unavailable())
|
|
expect(
|
|
(yield* readUpdate(
|
|
yield* discovery.load(),
|
|
state({ "core/instructions": [{ path: "/repo/AGENTS.md", content: "old" }] }),
|
|
)).changed,
|
|
).toBe(false)
|
|
}).pipe(Effect.provide(AppNodeBuilder.build(LayerNode.group([InstructionDiscovery.node, Bus.node])))),
|
|
)
|
|
|
|
it.effect("renders granular instruction updates", () =>
|
|
Effect.gen(function* () {
|
|
const discovery = yield* InstructionDiscovery.Service
|
|
yield* discovery.transform((draft) => {
|
|
draft.add(file("/global/AGENTS.md", "global"))
|
|
draft.add(
|
|
file("/repo/AGENTS.md", ["old", ...Array.from({ length: 20 }, (_, index) => `keep ${index}`)].join("\n")),
|
|
)
|
|
})
|
|
const initial = yield* readInitial(yield* discovery.load())
|
|
|
|
yield* discovery.transform((draft) => {
|
|
draft.update("/repo/AGENTS.md", (current) => {
|
|
current.content = ["new", ...Array.from({ length: 20 }, (_, index) => `keep ${index}`)].join("\n")
|
|
})
|
|
})
|
|
const modified = (yield* readUpdate(yield* discovery.load(), initial)).text
|
|
expect(modified).toContain("The instructions from /repo/AGENTS.md changed. Here's the diff:")
|
|
expect(modified).toContain("-old\n+new")
|
|
expect(modified).not.toContain("global")
|
|
|
|
const rewritten = state({
|
|
"core/instructions": [{ path: "/repo/AGENTS.md", content: "old one\nold two\nold three\nold four" }],
|
|
})
|
|
yield* discovery.transform((draft) => {
|
|
draft.remove("/global/AGENTS.md")
|
|
draft.update("/repo/AGENTS.md", (current) => {
|
|
current.content = "new"
|
|
})
|
|
})
|
|
expect((yield* readUpdate(yield* discovery.load(), rewritten)).text).toBe(
|
|
"The instructions changed:\nInstructions from: /repo/AGENTS.md\nnew",
|
|
)
|
|
|
|
yield* discovery.transform((draft) => {
|
|
draft.add(file("/repo/packages/AGENTS.md", "package"))
|
|
})
|
|
const structural = (yield* readUpdate(yield* discovery.load(), initial)).text
|
|
expect(structural).toContain("The instructions from /global/AGENTS.md no longer apply.")
|
|
expect(structural).toContain("New instructions apply from:\nInstructions from: /repo/packages/AGENTS.md\npackage")
|
|
expect(structural).not.toContain("Instructions from: /global/AGENTS.md\nglobal")
|
|
}).pipe(Effect.provide(AppNodeBuilder.build(LayerNode.group([InstructionDiscovery.node, Bus.node])))),
|
|
)
|
|
})
|
|
|
|
describe("ConfigInstructionPlugin.Plugin", () => {
|
|
it.live("loads global and upward project files and rescans them on change", () =>
|
|
Effect.acquireRelease(
|
|
Effect.promise(() => tmpdir()),
|
|
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
|
).pipe(
|
|
Effect.flatMap((tmp) => {
|
|
const global = path.join(tmp.path, "global")
|
|
const home = path.join(tmp.path, "home")
|
|
const shared = path.join(home, "code")
|
|
const project = path.join(shared, "repo")
|
|
const directory = path.join(project, "packages", "core")
|
|
const outside = path.join(tmp.path, "AGENTS.md")
|
|
const globalFile = path.join(global, "AGENTS.md")
|
|
const sharedFile = path.join(shared, "AGENTS.md")
|
|
const projectFile = path.join(project, "AGENTS.md")
|
|
const packageFile = path.join(directory, "AGENTS.md")
|
|
return Effect.gen(function* () {
|
|
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(sharedFile, "shared")
|
|
await fs.writeFile(projectFile, "project")
|
|
await fs.writeFile(packageFile, "package")
|
|
})
|
|
|
|
const discovery = yield* start()
|
|
const watcher = yield* Watcher.Test
|
|
expect(yield* watcher.subscriptions()).toEqual([
|
|
{ path: globalFile, type: "file" },
|
|
{ path: packageFile, type: "file" },
|
|
{ path: path.join(project, "packages", "AGENTS.md"), type: "file" },
|
|
{ path: projectFile, type: "file" },
|
|
{ path: sharedFile, type: "file" },
|
|
{ path: path.join(home, "AGENTS.md"), type: "file" },
|
|
])
|
|
expect(yield* watcher.subscriptions()).not.toContainEqual({
|
|
path: path.join(tmp.path, "AGENTS.md"),
|
|
type: "file",
|
|
})
|
|
const initialized = yield* readInitial(yield* discovery.load())
|
|
expect(initialized.text).toBe(
|
|
[
|
|
`Instructions from: ${globalFile}\nglobal`,
|
|
`Instructions from: ${packageFile}\npackage`,
|
|
`Instructions from: ${projectFile}\nproject`,
|
|
`Instructions from: ${sharedFile}\nshared`,
|
|
].join("\n\n"),
|
|
)
|
|
expect(initialized.text).not.toContain("outside")
|
|
|
|
yield* Effect.promise(() => fs.writeFile(packageFile, "changed"))
|
|
yield* emitAndWait({ type: "update", path: packageFile })
|
|
const changed = (yield* readUpdate(yield* discovery.load(), initialized)).text
|
|
expect(changed).toContain(`The instructions changed:\nInstructions from: ${packageFile}\nchanged`)
|
|
expect(changed).not.toContain(`Instructions from: ${globalFile}\nglobal`)
|
|
|
|
yield* Effect.promise(() => fs.rm(packageFile))
|
|
yield* emitAndWait({ type: "delete", path: packageFile })
|
|
const removed = (yield* readUpdate(yield* discovery.load(), initialized)).text
|
|
expect(removed).toContain(`The instructions from ${packageFile} no longer apply.`)
|
|
expect(removed).not.toContain(`Instructions from: ${globalFile}\nglobal`)
|
|
|
|
yield* Effect.promise(() => fs.rm(globalFile))
|
|
yield* emitAndWait({ type: "delete", path: globalFile })
|
|
yield* Effect.promise(() => fs.rm(projectFile))
|
|
yield* emitAndWait({ type: "delete", path: projectFile })
|
|
yield* Effect.promise(() => fs.rm(sharedFile))
|
|
yield* emitAndWait({ type: "delete", path: sharedFile })
|
|
expect((yield* readUpdate(yield* discovery.load(), initialized)).text).toBe(
|
|
"Previously loaded instructions no longer apply.",
|
|
)
|
|
}).pipe(
|
|
Effect.provide(
|
|
instructionLayer({
|
|
config: global,
|
|
home,
|
|
locationServiceLayer: Layer.succeed(
|
|
Location.Service,
|
|
Location.Service.of(
|
|
location(
|
|
{ directory: AbsolutePath.make(directory) },
|
|
{ projectDirectory: AbsolutePath.make(project) },
|
|
),
|
|
),
|
|
),
|
|
}),
|
|
),
|
|
)
|
|
}),
|
|
),
|
|
)
|
|
|
|
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 discovery = yield* start()
|
|
expect((yield* readInitial(yield* discovery.load())).text).toBe(`Instructions from: ${file}\n`)
|
|
}).pipe(
|
|
Effect.provide(
|
|
instructionLayer({
|
|
config: path.join(tmp.path, "global"),
|
|
locationServiceLayer: Layer.succeed(
|
|
Location.Service,
|
|
Location.Service.of(location({ directory: AbsolutePath.make(tmp.path) })),
|
|
),
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
|
|
it.live("discovers a newly created instruction file above the project root", () =>
|
|
Effect.acquireRelease(
|
|
Effect.promise(() => tmpdir()),
|
|
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
|
).pipe(
|
|
Effect.flatMap((tmp) => {
|
|
const home = path.join(tmp.path, "home")
|
|
const shared = path.join(home, "code")
|
|
const project = path.join(shared, "repo")
|
|
const intermediate = path.join(shared, "AGENTS.md")
|
|
const directory = path.join(project, "core")
|
|
const projectFile = path.join(project, "AGENTS.md")
|
|
return Effect.gen(function* () {
|
|
yield* Effect.promise(() => fs.mkdir(directory, { recursive: true }))
|
|
yield* Effect.promise(() => fs.writeFile(projectFile, "project"))
|
|
const discovery = yield* start()
|
|
expect((yield* readInitial(yield* discovery.load())).text).toBe(`Instructions from: ${projectFile}\nproject`)
|
|
|
|
yield* Effect.promise(() => fs.writeFile(intermediate, "intermediate"))
|
|
yield* emitAndWait({ type: "create", path: intermediate })
|
|
|
|
expect((yield* readInitial(yield* discovery.load())).text).toBe(
|
|
[`Instructions from: ${projectFile}\nproject`, `Instructions from: ${intermediate}\nintermediate`].join(
|
|
"\n\n",
|
|
),
|
|
)
|
|
}).pipe(
|
|
Effect.provide(
|
|
instructionLayer({
|
|
config: path.join(tmp.path, "global"),
|
|
home,
|
|
locationServiceLayer: Layer.succeed(
|
|
Location.Service,
|
|
Location.Service.of(
|
|
location(
|
|
{ directory: AbsolutePath.make(directory) },
|
|
{ projectDirectory: AbsolutePath.make(project) },
|
|
),
|
|
),
|
|
),
|
|
}),
|
|
),
|
|
)
|
|
}),
|
|
),
|
|
)
|
|
|
|
it.live("stops instruction candidates at the project root outside home", () =>
|
|
Effect.acquireRelease(
|
|
Effect.promise(() => tmpdir()),
|
|
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
|
).pipe(
|
|
Effect.flatMap((tmp) => {
|
|
const global = path.join(tmp.path, "global")
|
|
const home = path.join(tmp.path, "home")
|
|
const project = path.join(tmp.path, "scratch", "repo")
|
|
const directory = path.join(project, "packages", "core")
|
|
return Effect.gen(function* () {
|
|
yield* Effect.promise(() => fs.mkdir(directory, { recursive: true }))
|
|
yield* start()
|
|
const watcher = yield* Watcher.Test
|
|
expect(yield* watcher.subscriptions()).toEqual([
|
|
{ path: path.join(global, "AGENTS.md"), type: "file" },
|
|
{ path: path.join(directory, "AGENTS.md"), type: "file" },
|
|
{ path: path.join(project, "packages", "AGENTS.md"), type: "file" },
|
|
{ path: path.join(project, "AGENTS.md"), type: "file" },
|
|
])
|
|
}).pipe(
|
|
Effect.provide(
|
|
instructionLayer({
|
|
config: global,
|
|
home,
|
|
locationServiceLayer: Layer.succeed(
|
|
Location.Service,
|
|
Location.Service.of(
|
|
location(
|
|
{ directory: AbsolutePath.make(directory) },
|
|
{ projectDirectory: AbsolutePath.make(project) },
|
|
),
|
|
),
|
|
),
|
|
}),
|
|
),
|
|
)
|
|
}),
|
|
),
|
|
)
|
|
|
|
it.effect("isolates source failure without failing activation", () => {
|
|
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)))
|
|
return Effect.gen(function* () {
|
|
const discovery = yield* start()
|
|
expect(
|
|
(yield* readUpdate(
|
|
yield* discovery.load(),
|
|
state({ "core/instructions": [{ path: "/repo/AGENTS.md", content: "old" }] }),
|
|
)).changed,
|
|
).toBe(false)
|
|
}).pipe(
|
|
Effect.provide(
|
|
instructionLayer({
|
|
filesystemLayer: failingFS,
|
|
locationServiceLayer: Layer.succeed(
|
|
Location.Service,
|
|
Location.Service.of(location({ directory: AbsolutePath.make("/repo") })),
|
|
),
|
|
}),
|
|
),
|
|
)
|
|
})
|
|
|
|
it.effect("marks a discovered file that disappears before read as unavailable", () => {
|
|
const discovered = AbsolutePath.make("/repo/AGENTS.md")
|
|
const racingFS = Layer.effect(
|
|
FSUtil.Service,
|
|
FSUtil.Service.pipe(
|
|
Effect.map((fs) =>
|
|
FSUtil.Service.of({
|
|
...fs,
|
|
up: () => Effect.succeed([discovered]),
|
|
readFileStringSafe: () => Effect.succeed(undefined),
|
|
}),
|
|
),
|
|
),
|
|
).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))
|
|
return Effect.gen(function* () {
|
|
const discovery = yield* start()
|
|
expect(
|
|
(yield* readUpdate(
|
|
yield* discovery.load(),
|
|
state({ "core/instructions": [{ path: discovered, content: "old" }] }),
|
|
)).changed,
|
|
).toBe(false)
|
|
}).pipe(
|
|
Effect.provide(
|
|
instructionLayer({
|
|
filesystemLayer: racingFS,
|
|
locationServiceLayer: Layer.succeed(
|
|
Location.Service,
|
|
Location.Service.of(location({ directory: AbsolutePath.make("/repo") })),
|
|
),
|
|
}),
|
|
),
|
|
)
|
|
})
|
|
|
|
it.effect("canonicalizes boundaries and honors project opt-out", () =>
|
|
Effect.gen(function* () {
|
|
const observed: { values: { targets: string[]; start: string; stop?: string }[] } = { values: [] }
|
|
const observingFS = Layer.effect(
|
|
FSUtil.Service,
|
|
FSUtil.Service.pipe(
|
|
Effect.map((fs) =>
|
|
FSUtil.Service.of({
|
|
...fs,
|
|
up: (options) => Effect.sync(() => (observed.values.push(options), [])),
|
|
}),
|
|
),
|
|
),
|
|
).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))
|
|
|
|
yield* start().pipe(
|
|
Effect.provide(
|
|
instructionLayer({
|
|
filesystemLayer: observingFS,
|
|
locationServiceLayer: Layer.succeed(
|
|
Location.Service,
|
|
Location.Service.of(
|
|
location({ directory: AbsolutePath.make("/repo/") }, { projectDirectory: AbsolutePath.make("/repo") }),
|
|
),
|
|
),
|
|
}),
|
|
),
|
|
)
|
|
yield* start().pipe(
|
|
Effect.provide(
|
|
instructionLayer({
|
|
filesystemLayer: observingFS,
|
|
project: false,
|
|
locationServiceLayer: Layer.succeed(
|
|
Location.Service,
|
|
Location.Service.of(location({ directory: AbsolutePath.make("/repo") })),
|
|
),
|
|
}),
|
|
),
|
|
)
|
|
yield* start().pipe(
|
|
Effect.provide(
|
|
instructionLayer({
|
|
filesystemLayer: observingFS,
|
|
locationServiceLayer: Layer.succeed(
|
|
Location.Service,
|
|
Location.Service.of(
|
|
location(
|
|
{ directory: AbsolutePath.make("/outside") },
|
|
{ projectDirectory: AbsolutePath.make("/repo") },
|
|
),
|
|
),
|
|
),
|
|
}),
|
|
),
|
|
)
|
|
|
|
const repo = path.resolve("/repo")
|
|
expect(observed.values).toEqual([{ targets: ["AGENTS.md"], start: repo, stop: repo }])
|
|
}),
|
|
)
|
|
})
|