Files
opencode/packages/core/test/instruction-discovery.test.ts

726 lines
28 KiB
TypeScript

import { describe, expect } from "bun:test"
import { Deferred, Effect, Fiber, Layer, PlatformError, PubSub, Stream } from "effect"
import { HttpClient, HttpClientResponse } from "effect/http"
import fs from "fs/promises"
import path from "path"
import { AppNodeBuilder } from "@opencode/core/effect/app-node-builder"
import { Bus } from "@opencode/core/bus"
import { Config } from "@opencode/core/config"
import { ConfigInstructionPlugin } from "@opencode/core/config/plugin/instruction"
import { Watcher } from "@opencode/core/filesystem/watcher"
import { InstructionDiscovery } from "@opencode/core/instruction-discovery"
import { Location } from "@opencode/core/location"
import { AbsolutePath } from "@opencode/core/schema"
import { Document, Info } from "@opencode/schema/config"
import { Event } from "@opencode/schema/event"
import { FSUtil } from "@opencode/util/fs-util"
import { Global } from "@opencode/util/global"
import { LayerNode } from "@opencode/util/effect/layer-node"
import { tempGlobalLayer } from "./fixture/global"
import { location } from "./fixture/location"
import { tmpdirScoped } 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>
instructions?: string[]
http?: HttpClient.HttpClient
project?: boolean
}) => {
const watcher = Watcher.testLayer
const config = Config.testLayer(input.instructions ? [configured(input.instructions)] : [])
return Layer.mergeAll(
AppNodeBuilder.build(
LayerNode.group([
InstructionDiscovery.node,
Bus.node,
Config.node,
FSUtil.node,
Global.node,
Location.node,
Watcher.node,
]),
[
Config.node.replace(config),
InstructionDiscovery.node.replace(InstructionDiscovery.configured({ project: input.project })),
Global.node.replace(
input.config || input.home
? Global.layerWith({
...(input.config ? { config: input.config } : {}),
...(input.home ? { home: input.home } : {}),
})
: tempGlobalLayer,
),
Location.node.replace(input.locationServiceLayer),
Watcher.node.replace(watcher),
...(input.filesystemLayer ? [FSUtil.node.replace(input.filesystemLayer)] : []),
],
),
watcher,
config,
Layer.succeed(HttpClient.HttpClient, input.http ?? respond(() => new Response(null, { status: 404 }))),
)
}
const start = Effect.fnUntraced(function* (ctx = host()) {
yield* ConfigInstructionPlugin.Plugin.effect(ctx)
return yield* InstructionDiscovery.Service
})
const configured = (instructions: string[]) => new Document({ type: "document", info: new Info({ instructions }) })
const readFailure = (tag: "NotFound" | "Unknown", code: string, file: string) =>
PlatformError.systemError({
_tag: tag,
module: "FileSystem",
method: "readFile",
pathOrDescriptor: file,
cause: Object.assign(new Error(code), { code }),
})
const configUpdated = () => ({ id: Event.ID.create(), created: Date.now(), type: "config.updated" as const, data: {} })
const respond = (handler: (url: string) => Response) =>
HttpClient.make((request) => Effect.succeed(HttpClientResponse.fromWeb(request, handler(request.url))))
const file = (path: string, content: string) => new InstructionDiscovery.File({ path, content })
function emitAndWait(update: Watcher.Update) {
return Effect.gen(function* () {
const watcher = yield* Watcher.Test
yield* triggerAndWait(watcher.emit(update))
})
}
function triggerAndWait<E, R>(trigger: Effect.Effect<void, E, R>) {
return Effect.gen(function* () {
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* trigger
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((editor) => {
editor.add(file("/repo/AGENTS.md", "first"))
editor.add(file("/repo/packages/AGENTS.md", "package"))
editor.add(file("/repo/AGENTS.md", "last"))
editor.update("/repo/packages/AGENTS.md", (current) => {
current.content = "updated"
current.path = AbsolutePath.make("/ignored")
})
editor.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((editor) => editor.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((editor) => {
editor.add(file("/global/AGENTS.md", "global"))
editor.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((editor) => {
editor.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((editor) => {
editor.remove("/global/AGENTS.md")
editor.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((editor) => {
editor.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", () =>
tmpdirScoped().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("loads configured files, globs, home paths, and URLs alongside AGENTS.md", () =>
tmpdirScoped().pipe(
Effect.flatMap((tmp) => {
const project = path.join(tmp.path, "repo")
const directory = path.join(project, "packages", "core")
const agents = path.join(project, "AGENTS.md")
const first = path.join(project, "rules", "a.md")
const second = path.join(project, "rules", "b.md")
const absolute = path.join(tmp.path, "absolute.md")
const homeFile = path.join(tmp.path, "home.md")
const noteTop = path.join(tmp.path, "notes", "guide.md")
const noteNested = path.join(tmp.path, "notes", "projects", "memory.md")
const remote = "https://example.test/instructions.md"
const requests: string[] = []
return Effect.gen(function* () {
yield* Effect.promise(async () => {
await fs.mkdir(path.dirname(first), { recursive: true })
await fs.mkdir(directory, { recursive: true })
await fs.mkdir(path.dirname(noteNested), { recursive: true })
await Promise.all([
fs.writeFile(agents, "agents"),
fs.writeFile(first, "first"),
fs.writeFile(second, "second"),
fs.writeFile(absolute, "absolute"),
fs.writeFile(homeFile, "home"),
fs.writeFile(noteTop, "note top"),
fs.writeFile(noteNested, "note nested"),
])
})
const discovery = yield* start()
const initialized = yield* readInitial(yield* discovery.load())
// The unreachable URL and missing file are skipped without making AGENTS.md unavailable.
expect(initialized.text).toBe(
[
`Instructions from: ${agents}\nagents`,
`Instructions from: ${first}\nfirst`,
`Instructions from: ${second}\nsecond`,
`Instructions from: ${absolute}\nabsolute`,
`Instructions from: ${homeFile}\nhome`,
`Instructions from: ${noteTop}\nnote top`,
`Instructions from: ${noteNested}\nnote nested`,
`Instructions from: ${remote}\nremote`,
].join("\n\n"),
)
yield* Effect.promise(() => fs.writeFile(first, "changed"))
yield* emitAndWait({ type: "update", path: first })
expect((yield* readUpdate(yield* discovery.load(), initialized)).text).toBe(
`The instructions changed:\nInstructions from: ${first}\nchanged`,
)
// File rescans reuse fetched URLs instead of requesting them again.
expect(requests).toHaveLength(2)
}).pipe(
Effect.provide(
instructionLayer({
config: path.join(tmp.path, "global"),
home: tmp.path,
instructions: [
"rules/*.md",
absolute,
"~/home.md",
"~/notes/**/*.md",
remote,
"missing.md",
"https://example.test/missing.md",
],
http: respond((url) => {
requests.push(url)
return url === remote ? new Response("remote") : new Response(null, { status: 404 })
}),
locationServiceLayer: Layer.succeed(
Location.Service,
Location.Service.of(
location(
{ directory: AbsolutePath.make(directory) },
{ projectDirectory: AbsolutePath.make(project) },
),
),
),
}),
),
)
}),
),
)
it.live("reloads configured sources on config updates and keeps the last fetched URL", () =>
tmpdirScoped().pipe(
Effect.flatMap((tmp) => {
const first = path.join(tmp.path, "first.md")
const second = path.join(tmp.path, "second.md")
const remote = "https://example.test/instructions.md"
const responses = [new Response("remote"), new Response(null, { status: 500 })]
return Effect.gen(function* () {
yield* Effect.promise(async () => {
await fs.writeFile(first, "first")
await fs.writeFile(second, "second")
})
const events = yield* PubSub.unbounded<ReturnType<typeof configUpdated>>()
const config = yield* Config.Test
const discovery = yield* start(host({ event: { subscribe: () => Stream.fromPubSub(events) } }))
const initialized = yield* readInitial(yield* discovery.load())
expect(initialized.text).toBe(
[`Instructions from: ${first}\nfirst`, `Instructions from: ${remote}\nremote`].join("\n\n"),
)
yield* config.setEntries([configured([second, remote])])
yield* triggerAndWait(PubSub.publish(events, configUpdated()).pipe(Effect.asVoid))
expect((yield* readInitial(yield* discovery.load())).text).toBe(
[`Instructions from: ${second}\nsecond`, `Instructions from: ${remote}\nremote`].join("\n\n"),
)
}).pipe(
Effect.provide(
instructionLayer({
config: path.join(tmp.path, "global"),
instructions: [first, remote],
http: respond(() => responses.shift() ?? new Response(null, { status: 500 })),
locationServiceLayer: Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make(tmp.path) })),
),
}),
),
)
}),
),
)
it.live("keeps an empty AGENTS.md as available context", () =>
tmpdirScoped().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", () =>
tmpdirScoped().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", () =>
tmpdirScoped().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) },
),
),
),
}),
),
)
}),
),
)
// A client CWD can spell the drive `c:` while git reports the project root as `C:`.
;(process.platform === "win32" ? it.live : it.live.skip)(
"stops at the project root when the location drive letter differs in case",
() =>
tmpdirScoped().pipe(
Effect.flatMap((tmp) => {
const global = path.join(tmp.path, "global")
const home = path.join(tmp.path, "home")
const project = path.join(tmp.path, "repo")
const directory = project[0].toLowerCase() + project.slice(1)
return Effect.gen(function* () {
yield* Effect.promise(() => fs.mkdir(project, { recursive: true }))
yield* Effect.promise(() => fs.writeFile(path.join(project, "AGENTS.md"), "project"))
yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "AGENTS.md"), "outside"))
const discovery = 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" },
])
expect((yield* readInitial(yield* discovery.load())).text).toBe(
`Instructions from: ${path.join(directory, "AGENTS.md")}\nproject`,
)
}).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]),
readFileString: (file) => Effect.fail(readFailure("NotFound", "ENOENT", file)),
}),
),
),
).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("skips discovered files a sandbox denies and keeps the rest available", () => {
const denied = path.resolve("/repo/AGENTS.md")
const readable = path.resolve("/repo/packages/AGENTS.md")
const sandboxedFS = Layer.effect(
FSUtil.Service,
FSUtil.Service.pipe(
Effect.map((fs) =>
FSUtil.Service.of({
...fs,
up: () => Effect.succeed([readable, denied]),
readFileString: (file) =>
file === denied ? Effect.fail(readFailure("Unknown", "EPERM", file)) : Effect.succeed("package"),
}),
),
),
).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))
return Effect.gen(function* () {
const discovery = yield* start()
expect((yield* readInitial(yield* discovery.load())).text).toBe(`Instructions from: ${readable}\npackage`)
}).pipe(
Effect.provide(
instructionLayer({
filesystemLayer: sandboxedFS,
locationServiceLayer: Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make("/repo/packages") })),
),
}),
),
)
})
it.effect("canonicalizes boundaries and honors project opt-out", () =>
Effect.gen(function* () {
const observed: { values: FSUtil.UpOptions[] } = { 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, type: "file" }])
}),
)
})