Преглед на файлове

test: use testEffect for instruction tests (#25046)

Kit Langton преди 3 месеца
родител
ревизия
65c15afe9f

+ 21 - 11
packages/core/src/global.ts

@@ -4,6 +4,7 @@ import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir"
 import os from "os"
 import { Context, Effect, Layer } from "effect"
 import { Flock } from "./util/flock"
+import { Flag } from "./flag/flag"
 
 const app = "opencode"
 const data = path.join(xdgData!, app)
@@ -47,19 +48,28 @@ export interface Interface {
   readonly log: string
 }
 
+export function make(input: Partial<Interface> = {}): Interface {
+  return {
+    home: Path.home,
+    data: Path.data,
+    cache: Path.cache,
+    config: Flag.OPENCODE_CONFIG_DIR ?? Path.config,
+    state: Path.state,
+    bin: Path.bin,
+    log: Path.log,
+    ...input,
+  }
+}
+
 export const layer = Layer.effect(
   Service,
-  Effect.gen(function* () {
-    return Service.of({
-      home: Path.home,
-      data: Path.data,
-      cache: Path.cache,
-      config: Path.config,
-      state: Path.state,
-      bin: Path.bin,
-      log: Path.log,
-    })
-  }),
+  Effect.sync(() => Service.of(make())),
 )
 
+export const layerWith = (input: Partial<Interface>) =>
+  Layer.effect(
+    Service,
+    Effect.sync(() => Service.of(make(input))),
+  )
+
 export * as Global from "./global"

+ 11 - 14
packages/core/test/fixture/effect-flock-worker.ts

@@ -18,20 +18,17 @@ function sleep(ms: number) {
   return new Promise<void>((resolve) => setTimeout(resolve, ms))
 }
 
-const msg: Msg = JSON.parse(process.argv[2]!)
-
-const testGlobal = Layer.succeed(
-  Global.Service,
-  Global.Service.of({
-    home: os.homedir(),
-    data: os.tmpdir(),
-    cache: os.tmpdir(),
-    config: os.tmpdir(),
-    state: os.tmpdir(),
-    bin: os.tmpdir(),
-    log: os.tmpdir(),
-  }),
-)
+const msg: Msg = JSON.parse(process.argv[2])
+
+const testGlobal = Global.layerWith({
+  home: os.homedir(),
+  data: os.tmpdir(),
+  cache: os.tmpdir(),
+  config: os.tmpdir(),
+  state: os.tmpdir(),
+  bin: os.tmpdir(),
+  log: os.tmpdir(),
+})
 
 const testLayer = EffectFlock.layer.pipe(Layer.provide(testGlobal), Layer.provide(AppFileSystem.defaultLayer))
 

+ 9 - 12
packages/core/test/util/effect-flock.test.ts

@@ -93,18 +93,15 @@ async function waitForFile(file: string, timeout = 3_000) {
 // Test layer
 // ---------------------------------------------------------------------------
 
-const testGlobal = Layer.succeed(
-  Global.Service,
-  Global.Service.of({
-    home: os.homedir(),
-    data: os.tmpdir(),
-    cache: os.tmpdir(),
-    config: os.tmpdir(),
-    state: os.tmpdir(),
-    bin: os.tmpdir(),
-    log: os.tmpdir(),
-  }),
-)
+const testGlobal = Global.layerWith({
+  home: os.homedir(),
+  data: os.tmpdir(),
+  cache: os.tmpdir(),
+  config: os.tmpdir(),
+  state: os.tmpdir(),
+  bin: os.tmpdir(),
+  log: os.tmpdir(),
+})
 
 const testLayer = EffectFlock.layer.pipe(Layer.provide(testGlobal), Layer.provide(AppFileSystem.defaultLayer))
 

+ 152 - 164
packages/opencode/src/session/instruction.ts

@@ -1,4 +1,3 @@
-import os from "os"
 import path from "path"
 import { Effect, Layer, Context } from "effect"
 import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"
@@ -8,30 +7,15 @@ import { Flag } from "@opencode-ai/core/flag/flag"
 import { AppFileSystem } from "@opencode-ai/core/filesystem"
 import { withTransientReadRetry } from "@/util/effect-http-client"
 import { Global } from "@opencode-ai/core/global"
-import * as Log from "@opencode-ai/core/util/log"
 import type { MessageV2 } from "./message-v2"
 import type { MessageID } from "./schema"
 
-const log = Log.create({ service: "instruction" })
-
 const FILES = [
   "AGENTS.md",
   ...(Flag.OPENCODE_DISABLE_CLAUDE_CODE_PROMPT ? [] : ["CLAUDE.md"]),
   "CONTEXT.md", // deprecated
 ]
 
-function globalFiles() {
-  const files = []
-  if (Flag.OPENCODE_CONFIG_DIR) {
-    files.push(path.join(Flag.OPENCODE_CONFIG_DIR, "AGENTS.md"))
-  }
-  files.push(path.join(Global.Path.config, "AGENTS.md"))
-  if (!Flag.OPENCODE_DISABLE_CLAUDE_CODE_PROMPT) {
-    files.push(path.join(os.homedir(), ".claude", "CLAUDE.md"))
-  }
-  return files
-}
-
 function extract(messages: MessageV2.WithParts[]) {
   const paths = new Set<string>()
   for (const msg of messages) {
@@ -63,176 +47,180 @@ export interface Interface {
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/Instruction") {}
 
-export const layer: Layer.Layer<Service, never, AppFileSystem.Service | Config.Service | HttpClient.HttpClient> =
-  Layer.effect(
-    Service,
-    Effect.gen(function* () {
-      const cfg = yield* Config.Service
-      const fs = yield* AppFileSystem.Service
-      const http = HttpClient.filterStatusOk(withTransientReadRetry(yield* HttpClient.HttpClient))
-
-      const state = yield* InstanceState.make(
-        Effect.fn("Instruction.state")(() =>
-          Effect.succeed({
-            // Track which instruction files have already been attached for a given assistant message.
-            claims: new Map<MessageID, Set<string>>(),
-          }),
-        ),
-      )
-
-      const relative = Effect.fnUntraced(function* (instruction: string) {
-        const ctx = yield* InstanceState.context
-        if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
-          return yield* fs
-            .globUp(instruction, ctx.directory, ctx.worktree)
-            .pipe(Effect.catch(() => Effect.succeed([] as string[])))
-        }
-        if (!Flag.OPENCODE_CONFIG_DIR) {
-          log.warn(
-            `Skipping relative instruction "${instruction}" - no OPENCODE_CONFIG_DIR set while project config is disabled`,
-          )
-          return []
-        }
+export const layer: Layer.Layer<
+  Service,
+  never,
+  AppFileSystem.Service | Config.Service | Global.Service | HttpClient.HttpClient
+> = Layer.effect(
+  Service,
+  Effect.gen(function* () {
+    const cfg = yield* Config.Service
+    const fs = yield* AppFileSystem.Service
+    const global = yield* Global.Service
+    const http = HttpClient.filterStatusOk(withTransientReadRetry(yield* HttpClient.HttpClient))
+    const globalFiles = [
+      path.join(global.config, "AGENTS.md"),
+      ...(!Flag.OPENCODE_DISABLE_CLAUDE_CODE_PROMPT ? [path.join(global.home, ".claude", "CLAUDE.md")] : []),
+    ]
+
+    const state = yield* InstanceState.make(
+      Effect.fn("Instruction.state")(() =>
+        Effect.succeed({
+          // Track which instruction files have already been attached for a given assistant message.
+          claims: new Map<MessageID, Set<string>>(),
+        }),
+      ),
+    )
+
+    const relative = Effect.fnUntraced(function* (instruction: string) {
+      const ctx = yield* InstanceState.context
+      if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
         return yield* fs
-          .globUp(instruction, Flag.OPENCODE_CONFIG_DIR, Flag.OPENCODE_CONFIG_DIR)
+          .globUp(instruction, ctx.directory, ctx.worktree)
           .pipe(Effect.catch(() => Effect.succeed([] as string[])))
-      })
-
-      const read = Effect.fnUntraced(function* (filepath: string) {
-        return yield* fs.readFileString(filepath).pipe(Effect.catch(() => Effect.succeed("")))
-      })
-
-      const fetch = Effect.fnUntraced(function* (url: string) {
-        const res = yield* http.execute(HttpClientRequest.get(url)).pipe(
-          Effect.timeout(5000),
-          Effect.catch(() => Effect.succeed(null)),
-        )
-        if (!res) return ""
-        const body = yield* res.arrayBuffer.pipe(Effect.catch(() => Effect.succeed(new ArrayBuffer(0))))
-        return new TextDecoder().decode(body)
-      })
-
-      const clear = Effect.fn("Instruction.clear")(function* (messageID: MessageID) {
-        const s = yield* InstanceState.get(state)
-        s.claims.delete(messageID)
-      })
-
-      const systemPaths = Effect.fn("Instruction.systemPaths")(function* () {
-        const config = yield* cfg.get()
-        const ctx = yield* InstanceState.context
-        const paths = new Set<string>()
-
-        for (const file of globalFiles()) {
-          if (yield* fs.existsSafe(file)) {
-            paths.add(path.resolve(file))
-            break
-          }
+      }
+      return yield* fs
+        .globUp(instruction, global.config, global.config)
+        .pipe(Effect.catch(() => Effect.succeed([] as string[])))
+    })
+
+    const read = Effect.fnUntraced(function* (filepath: string) {
+      return yield* fs.readFileString(filepath).pipe(Effect.catch(() => Effect.succeed("")))
+    })
+
+    const fetch = Effect.fnUntraced(function* (url: string) {
+      const res = yield* http.execute(HttpClientRequest.get(url)).pipe(
+        Effect.timeout(5000),
+        Effect.catch(() => Effect.succeed(null)),
+      )
+      if (!res) return ""
+      const body = yield* res.arrayBuffer.pipe(Effect.catch(() => Effect.succeed(new ArrayBuffer(0))))
+      return new TextDecoder().decode(body)
+    })
+
+    const clear = Effect.fn("Instruction.clear")(function* (messageID: MessageID) {
+      const s = yield* InstanceState.get(state)
+      s.claims.delete(messageID)
+    })
+
+    const systemPaths = Effect.fn("Instruction.systemPaths")(function* () {
+      const config = yield* cfg.get()
+      const ctx = yield* InstanceState.context
+      const paths = new Set<string>()
+
+      for (const file of globalFiles) {
+        if (yield* fs.existsSafe(file)) {
+          paths.add(path.resolve(file))
+          break
         }
+      }
 
-        // The first project-level match wins so we don't stack AGENTS.md/CLAUDE.md from every ancestor.
-        if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
-          for (const file of FILES) {
-            const matches = yield* fs.findUp(file, ctx.directory, ctx.worktree)
-            if (matches.length > 0) {
-              matches.forEach((item) => paths.add(path.resolve(item)))
-              break
-            }
+      // The first project-level match wins so we don't stack AGENTS.md/CLAUDE.md from every ancestor.
+      if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
+        for (const file of FILES) {
+          const matches = yield* fs.findUp(file, ctx.directory, ctx.worktree)
+          if (matches.length > 0) {
+            matches.forEach((item) => paths.add(path.resolve(item)))
+            break
           }
         }
+      }
 
-        if (config.instructions) {
-          for (const raw of config.instructions) {
-            if (raw.startsWith("https://") || raw.startsWith("http://")) continue
-            const instruction = raw.startsWith("~/") ? path.join(os.homedir(), raw.slice(2)) : raw
-            const matches = yield* (
-              path.isAbsolute(instruction)
-                ? fs.glob(path.basename(instruction), {
-                    cwd: path.dirname(instruction),
-                    absolute: true,
-                    include: "file",
-                  })
-                : relative(instruction)
-            ).pipe(Effect.catch(() => Effect.succeed([] as string[])))
-            matches.forEach((item) => paths.add(path.resolve(item)))
-          }
+      if (config.instructions) {
+        for (const raw of config.instructions) {
+          if (raw.startsWith("https://") || raw.startsWith("http://")) continue
+          const instruction = raw.startsWith("~/") ? path.join(global.home, raw.slice(2)) : raw
+          const matches = yield* (
+            path.isAbsolute(instruction)
+              ? fs.glob(path.basename(instruction), {
+                  cwd: path.dirname(instruction),
+                  absolute: true,
+                  include: "file",
+                })
+              : relative(instruction)
+          ).pipe(Effect.catch(() => Effect.succeed([] as string[])))
+          matches.forEach((item) => paths.add(path.resolve(item)))
         }
+      }
 
-        return paths
-      })
+      return paths
+    })
 
-      const system = Effect.fn("Instruction.system")(function* () {
-        const config = yield* cfg.get()
-        const paths = yield* systemPaths()
-        const urls = (config.instructions ?? []).filter(
-          (item) => item.startsWith("https://") || item.startsWith("http://"),
-        )
+    const system = Effect.fn("Instruction.system")(function* () {
+      const config = yield* cfg.get()
+      const paths = yield* systemPaths()
+      const urls = (config.instructions ?? []).filter(
+        (item) => item.startsWith("https://") || item.startsWith("http://"),
+      )
 
-        const files = yield* Effect.forEach(Array.from(paths), read, { concurrency: 8 })
-        const remote = yield* Effect.forEach(urls, fetch, { concurrency: 4 })
+      const files = yield* Effect.forEach(Array.from(paths), read, { concurrency: 8 })
+      const remote = yield* Effect.forEach(urls, fetch, { concurrency: 4 })
 
-        return [
-          ...Array.from(paths).flatMap((item, i) => (files[i] ? [`Instructions from: ${item}\n${files[i]}`] : [])),
-          ...urls.flatMap((item, i) => (remote[i] ? [`Instructions from: ${item}\n${remote[i]}`] : [])),
-        ]
-      })
+      return [
+        ...Array.from(paths).flatMap((item, i) => (files[i] ? [`Instructions from: ${item}\n${files[i]}`] : [])),
+        ...urls.flatMap((item, i) => (remote[i] ? [`Instructions from: ${item}\n${remote[i]}`] : [])),
+      ]
+    })
 
-      const find = Effect.fn("Instruction.find")(function* (dir: string) {
-        for (const file of FILES) {
-          const filepath = path.resolve(path.join(dir, file))
-          if (yield* fs.existsSafe(filepath)) return filepath
+    const find = Effect.fn("Instruction.find")(function* (dir: string) {
+      for (const file of FILES) {
+        const filepath = path.resolve(path.join(dir, file))
+        if (yield* fs.existsSafe(filepath)) return filepath
+      }
+      return undefined
+    })
+
+    const resolve = Effect.fn("Instruction.resolve")(function* (
+      messages: MessageV2.WithParts[],
+      filepath: string,
+      messageID: MessageID,
+    ) {
+      const sys = yield* systemPaths()
+      const already = extract(messages)
+      const results: { filepath: string; content: string }[] = []
+      const s = yield* InstanceState.get(state)
+      const root = path.resolve(yield* InstanceState.directory)
+
+      const target = path.resolve(filepath)
+      let current = path.dirname(target)
+
+      // Walk upward from the file being read and attach nearby instruction files once per message.
+      while (current.startsWith(root) && current !== root) {
+        const found = yield* find(current)
+        if (!found || found === target || sys.has(found) || already.has(found)) {
+          current = path.dirname(current)
+          continue
         }
-      })
-
-      const resolve = Effect.fn("Instruction.resolve")(function* (
-        messages: MessageV2.WithParts[],
-        filepath: string,
-        messageID: MessageID,
-      ) {
-        const sys = yield* systemPaths()
-        const already = extract(messages)
-        const results: { filepath: string; content: string }[] = []
-        const s = yield* InstanceState.get(state)
-        const root = path.resolve(yield* InstanceState.directory)
-
-        const target = path.resolve(filepath)
-        let current = path.dirname(target)
-
-        // Walk upward from the file being read and attach nearby instruction files once per message.
-        while (current.startsWith(root) && current !== root) {
-          const found = yield* find(current)
-          if (!found || found === target || sys.has(found) || already.has(found)) {
-            current = path.dirname(current)
-            continue
-          }
-
-          let set = s.claims.get(messageID)
-          if (!set) {
-            set = new Set()
-            s.claims.set(messageID, set)
-          }
-          if (set.has(found)) {
-            current = path.dirname(current)
-            continue
-          }
-
-          set.add(found)
-          const content = yield* read(found)
-          if (content) {
-            results.push({ filepath: found, content: `Instructions from: ${found}\n${content}` })
-          }
 
+        let set = s.claims.get(messageID)
+        if (!set) {
+          set = new Set()
+          s.claims.set(messageID, set)
+        }
+        if (set.has(found)) {
           current = path.dirname(current)
+          continue
         }
 
-        return results
-      })
+        set.add(found)
+        const content = yield* read(found)
+        if (content) {
+          results.push({ filepath: found, content: `Instructions from: ${found}\n${content}` })
+        }
 
-      return Service.of({ clear, systemPaths, system, find, resolve })
-    }),
-  )
+        current = path.dirname(current)
+      }
+
+      return results
+    })
+
+    return Service.of({ clear, systemPaths, system, find, resolve })
+  }),
+)
 
 export const defaultLayer = layer.pipe(
   Layer.provide(Config.defaultLayer),
+  Layer.provide(Global.layer),
   Layer.provide(AppFileSystem.defaultLayer),
   Layer.provide(FetchHttpClient.layer),
 )

+ 190 - 331
packages/opencode/test/session/instruction.test.ts

@@ -1,16 +1,76 @@
-import { afterEach, beforeEach, describe, expect, test } from "bun:test"
+import { describe, expect, test } from "bun:test"
 import path from "path"
-import { Effect } from "effect"
+import { Effect, FileSystem, Layer } from "effect"
+import { FetchHttpClient } from "effect/unstable/http"
+import { NodeFileSystem } from "@effect/platform-node"
+import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
+import { AppFileSystem } from "@opencode-ai/core/filesystem"
+import { Config } from "@/config/config"
+import { emptyConsoleState } from "@/config/console-state"
 import { ModelID, ProviderID } from "../../src/provider/schema"
 import { Instruction } from "../../src/session/instruction"
 import type { MessageV2 } from "../../src/session/message-v2"
-import { Instance } from "../../src/project/instance"
 import { MessageID, PartID, SessionID } from "../../src/session/schema"
 import { Global } from "@opencode-ai/core/global"
-import { tmpdir } from "../fixture/fixture"
+import { provideInstance, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture"
+import { testEffect } from "../lib/effect"
+
+const it = testEffect(Layer.mergeAll(CrossSpawnSpawner.defaultLayer, NodeFileSystem.layer))
+
+const configLayer = Layer.succeed(
+  Config.Service,
+  Config.Service.of({
+    get: () => Effect.succeed({}),
+    getGlobal: () => Effect.succeed({}),
+    getConsoleState: () => Effect.succeed(emptyConsoleState),
+    update: () => Effect.void,
+    updateGlobal: (config) => Effect.succeed(config),
+    invalidate: () => Effect.void,
+    directories: () => Effect.succeed([]),
+    waitForDependencies: () => Effect.void,
+  }),
+)
+
+const instructionLayer = (global: Partial<Global.Interface>) =>
+  Instruction.layer.pipe(
+    Layer.provide(configLayer),
+    Layer.provide(AppFileSystem.defaultLayer),
+    Layer.provide(FetchHttpClient.layer),
+    Layer.provide(Global.layerWith(global)),
+  )
+
+const provideInstruction =
+  (global: Partial<Global.Interface>) =>
+  <A, E, R>(self: Effect.Effect<A, E, R>) =>
+    self.pipe(Effect.provide(instructionLayer(global)))
+
+const write = (filepath: string, content: string) =>
+  Effect.gen(function* () {
+    const fs = yield* FileSystem.FileSystem
+    yield* fs.makeDirectory(path.dirname(filepath), { recursive: true })
+    yield* fs.writeFileString(filepath, content)
+  })
 
-const run = <A>(effect: Effect.Effect<A, any, Instruction.Service>) =>
-  Effect.runPromise(effect.pipe(Effect.provide(Instruction.defaultLayer)))
+const writeFiles = (dir: string, files: Record<string, string>) =>
+  Effect.all(
+    Object.entries(files).map(([file, content]) => write(path.join(dir, file), content)),
+    { discard: true },
+  )
+
+const withFiles = <A, E, R>(files: Record<string, string>, self: (dir: string) => Effect.Effect<A, E, R>) =>
+  provideTmpdirInstance((dir) =>
+    Effect.gen(function* () {
+      yield* writeFiles(dir, files)
+      return yield* self(dir).pipe(provideInstruction({ home: dir, config: dir }))
+    }),
+  )
+
+const tmpWithFiles = (files: Record<string, string>) =>
+  Effect.gen(function* () {
+    const dir = yield* tmpdirScoped()
+    yield* writeFiles(dir, files)
+    return dir
+  })
 
 function loaded(filepath: string): MessageV2.WithParts[] {
   const sessionID = SessionID.make("session-loaded-1")
@@ -52,336 +112,135 @@ function loaded(filepath: string): MessageV2.WithParts[] {
 }
 
 describe("Instruction.resolve", () => {
-  test("returns empty when AGENTS.md is at project root (already in systemPaths)", async () => {
-    await using tmp = await tmpdir({
-      init: async (dir) => {
-        await Bun.write(path.join(dir, "AGENTS.md"), "# Root Instructions")
-        await Bun.write(path.join(dir, "src", "file.ts"), "const x = 1")
-      },
-    })
-    await Instance.provide({
-      directory: tmp.path,
-      fn: () =>
-        run(
-          Instruction.Service.use((svc) =>
-            Effect.gen(function* () {
-              const system = yield* svc.systemPaths()
-              expect(system.has(path.join(tmp.path, "AGENTS.md"))).toBe(true)
-
-              const results = yield* svc.resolve(
-                [],
-                path.join(tmp.path, "src", "file.ts"),
-                MessageID.make("message-test-1"),
-              )
-              expect(results).toEqual([])
-            }),
-          ),
-        ),
-    })
-  })
-
-  test("returns AGENTS.md from subdirectory (not in systemPaths)", async () => {
-    await using tmp = await tmpdir({
-      init: async (dir) => {
-        await Bun.write(path.join(dir, "subdir", "AGENTS.md"), "# Subdir Instructions")
-        await Bun.write(path.join(dir, "subdir", "nested", "file.ts"), "const x = 1")
-      },
-    })
-    await Instance.provide({
-      directory: tmp.path,
-      fn: () =>
-        run(
-          Instruction.Service.use((svc) =>
-            Effect.gen(function* () {
-              const system = yield* svc.systemPaths()
-              expect(system.has(path.join(tmp.path, "subdir", "AGENTS.md"))).toBe(false)
-
-              const results = yield* svc.resolve(
-                [],
-                path.join(tmp.path, "subdir", "nested", "file.ts"),
-                MessageID.make("message-test-2"),
-              )
-              expect(results.length).toBe(1)
-              expect(results[0].filepath).toBe(path.join(tmp.path, "subdir", "AGENTS.md"))
-            }),
-          ),
-        ),
-    })
-  })
-
-  test("doesn't reload AGENTS.md when reading it directly", async () => {
-    await using tmp = await tmpdir({
-      init: async (dir) => {
-        await Bun.write(path.join(dir, "subdir", "AGENTS.md"), "# Subdir Instructions")
-        await Bun.write(path.join(dir, "subdir", "nested", "file.ts"), "const x = 1")
-      },
-    })
-    await Instance.provide({
-      directory: tmp.path,
-      fn: () =>
-        run(
-          Instruction.Service.use((svc) =>
-            Effect.gen(function* () {
-              const filepath = path.join(tmp.path, "subdir", "AGENTS.md")
-              const system = yield* svc.systemPaths()
-              expect(system.has(filepath)).toBe(false)
-
-              const results = yield* svc.resolve([], filepath, MessageID.make("message-test-3"))
-              expect(results).toEqual([])
-            }),
-          ),
-        ),
-    })
-  })
-
-  test("does not reattach the same nearby instructions twice for one message", async () => {
-    await using tmp = await tmpdir({
-      init: async (dir) => {
-        await Bun.write(path.join(dir, "subdir", "AGENTS.md"), "# Subdir Instructions")
-        await Bun.write(path.join(dir, "subdir", "nested", "file.ts"), "const x = 1")
-      },
-    })
-    await Instance.provide({
-      directory: tmp.path,
-      fn: () =>
-        run(
-          Instruction.Service.use((svc) =>
-            Effect.gen(function* () {
-              const filepath = path.join(tmp.path, "subdir", "nested", "file.ts")
-              const id = MessageID.make("message-claim-1")
-
-              const first = yield* svc.resolve([], filepath, id)
-              const second = yield* svc.resolve([], filepath, id)
-
-              expect(first).toHaveLength(1)
-              expect(first[0].filepath).toBe(path.join(tmp.path, "subdir", "AGENTS.md"))
-              expect(second).toEqual([])
-            }),
-          ),
-        ),
-    })
-  })
-
-  test("clear allows nearby instructions to be attached again for the same message", async () => {
-    await using tmp = await tmpdir({
-      init: async (dir) => {
-        await Bun.write(path.join(dir, "subdir", "AGENTS.md"), "# Subdir Instructions")
-        await Bun.write(path.join(dir, "subdir", "nested", "file.ts"), "const x = 1")
-      },
-    })
-    await Instance.provide({
-      directory: tmp.path,
-      fn: () =>
-        run(
-          Instruction.Service.use((svc) =>
-            Effect.gen(function* () {
-              const filepath = path.join(tmp.path, "subdir", "nested", "file.ts")
-              const id = MessageID.make("message-claim-2")
-
-              const first = yield* svc.resolve([], filepath, id)
-              yield* svc.clear(id)
-              const second = yield* svc.resolve([], filepath, id)
-
-              expect(first).toHaveLength(1)
-              expect(second).toHaveLength(1)
-              expect(second[0].filepath).toBe(path.join(tmp.path, "subdir", "AGENTS.md"))
-            }),
-          ),
-        ),
-    })
-  })
-
-  test("skips instructions already reported by prior read metadata", async () => {
-    await using tmp = await tmpdir({
-      init: async (dir) => {
-        await Bun.write(path.join(dir, "subdir", "AGENTS.md"), "# Subdir Instructions")
-        await Bun.write(path.join(dir, "subdir", "nested", "file.ts"), "const x = 1")
-      },
-    })
-    await Instance.provide({
-      directory: tmp.path,
-      fn: () =>
-        run(
-          Instruction.Service.use((svc) =>
-            Effect.gen(function* () {
-              const agents = path.join(tmp.path, "subdir", "AGENTS.md")
-              const filepath = path.join(tmp.path, "subdir", "nested", "file.ts")
-              const id = MessageID.make("message-claim-3")
-
-              const results = yield* svc.resolve(loaded(agents), filepath, id)
-              expect(results).toEqual([])
-            }),
-          ),
-        ),
-    })
-  })
+  it.live("returns empty when AGENTS.md is at project root (already in systemPaths)", () =>
+    withFiles({ "AGENTS.md": "# Root Instructions", "src/file.ts": "const x = 1" }, (dir) =>
+      Effect.gen(function* () {
+        const svc = yield* Instruction.Service
+        const system = yield* svc.systemPaths()
+        expect(system.has(path.join(dir, "AGENTS.md"))).toBe(true)
+
+        const results = yield* svc.resolve([], path.join(dir, "src", "file.ts"), MessageID.make("message-test-1"))
+        expect(results).toEqual([])
+      }),
+    ),
+  )
+
+  it.live("returns AGENTS.md from subdirectory (not in systemPaths)", () =>
+    withFiles({ "subdir/AGENTS.md": "# Subdir Instructions", "subdir/nested/file.ts": "const x = 1" }, (dir) =>
+      Effect.gen(function* () {
+        const svc = yield* Instruction.Service
+        const system = yield* svc.systemPaths()
+        expect(system.has(path.join(dir, "subdir", "AGENTS.md"))).toBe(false)
+
+        const results = yield* svc.resolve(
+          [],
+          path.join(dir, "subdir", "nested", "file.ts"),
+          MessageID.make("message-test-2"),
+        )
+        expect(results.length).toBe(1)
+        expect(results[0].filepath).toBe(path.join(dir, "subdir", "AGENTS.md"))
+      }),
+    ),
+  )
+
+  it.live("doesn't reload AGENTS.md when reading it directly", () =>
+    withFiles({ "subdir/AGENTS.md": "# Subdir Instructions", "subdir/nested/file.ts": "const x = 1" }, (dir) =>
+      Effect.gen(function* () {
+        const svc = yield* Instruction.Service
+        const filepath = path.join(dir, "subdir", "AGENTS.md")
+        const system = yield* svc.systemPaths()
+        expect(system.has(filepath)).toBe(false)
+
+        const results = yield* svc.resolve([], filepath, MessageID.make("message-test-3"))
+        expect(results).toEqual([])
+      }),
+    ),
+  )
+
+  it.live("does not reattach the same nearby instructions twice for one message", () =>
+    withFiles({ "subdir/AGENTS.md": "# Subdir Instructions", "subdir/nested/file.ts": "const x = 1" }, (dir) =>
+      Effect.gen(function* () {
+        const svc = yield* Instruction.Service
+        const filepath = path.join(dir, "subdir", "nested", "file.ts")
+        const id = MessageID.make("message-claim-1")
+
+        const first = yield* svc.resolve([], filepath, id)
+        const second = yield* svc.resolve([], filepath, id)
+
+        expect(first).toHaveLength(1)
+        expect(first[0].filepath).toBe(path.join(dir, "subdir", "AGENTS.md"))
+        expect(second).toEqual([])
+      }),
+    ),
+  )
+
+  it.live("clear allows nearby instructions to be attached again for the same message", () =>
+    withFiles({ "subdir/AGENTS.md": "# Subdir Instructions", "subdir/nested/file.ts": "const x = 1" }, (dir) =>
+      Effect.gen(function* () {
+        const svc = yield* Instruction.Service
+        const filepath = path.join(dir, "subdir", "nested", "file.ts")
+        const id = MessageID.make("message-claim-2")
+
+        const first = yield* svc.resolve([], filepath, id)
+        yield* svc.clear(id)
+        const second = yield* svc.resolve([], filepath, id)
+
+        expect(first).toHaveLength(1)
+        expect(second).toHaveLength(1)
+        expect(second[0].filepath).toBe(path.join(dir, "subdir", "AGENTS.md"))
+      }),
+    ),
+  )
+
+  it.live("skips instructions already reported by prior read metadata", () =>
+    withFiles({ "subdir/AGENTS.md": "# Subdir Instructions", "subdir/nested/file.ts": "const x = 1" }, (dir) =>
+      Effect.gen(function* () {
+        const svc = yield* Instruction.Service
+        const agents = path.join(dir, "subdir", "AGENTS.md")
+        const filepath = path.join(dir, "subdir", "nested", "file.ts")
+        const id = MessageID.make("message-claim-3")
+
+        const results = yield* svc.resolve(loaded(agents), filepath, id)
+        expect(results).toEqual([])
+      }),
+    ),
+  )
 
   test.todo("fetches remote instructions from config URLs via HttpClient", () => {})
 })
 
 describe("Instruction.system", () => {
-  test("loads both project and global AGENTS.md when both exist", async () => {
-    const originalConfigDir = process.env["OPENCODE_CONFIG_DIR"]
-    delete process.env["OPENCODE_CONFIG_DIR"]
-
-    await using globalTmp = await tmpdir({
-      init: async (dir) => {
-        await Bun.write(path.join(dir, "AGENTS.md"), "# Global Instructions")
-      },
-    })
-    await using projectTmp = await tmpdir({
-      init: async (dir) => {
-        await Bun.write(path.join(dir, "AGENTS.md"), "# Project Instructions")
-      },
-    })
-
-    const originalGlobalConfig = Global.Path.config
-    ;(Global.Path as { config: string }).config = globalTmp.path
-
-    try {
-      await Instance.provide({
-        directory: projectTmp.path,
-        fn: () =>
-          run(
-            Instruction.Service.use((svc) =>
-              Effect.gen(function* () {
-                const paths = yield* svc.systemPaths()
-                expect(paths.has(path.join(projectTmp.path, "AGENTS.md"))).toBe(true)
-                expect(paths.has(path.join(globalTmp.path, "AGENTS.md"))).toBe(true)
-
-                const rules = yield* svc.system()
-                expect(rules).toHaveLength(2)
-                expect(rules[0]).toBe(
-                  `Instructions from: ${path.join(globalTmp.path, "AGENTS.md")}\n# Global Instructions`,
-                )
-                expect(rules[1]).toBe(
-                  `Instructions from: ${path.join(projectTmp.path, "AGENTS.md")}\n# Project Instructions`,
-                )
-              }),
-            ),
-          ),
-      })
-    } finally {
-      ;(Global.Path as { config: string }).config = originalGlobalConfig
-      if (originalConfigDir === undefined) {
-        delete process.env["OPENCODE_CONFIG_DIR"]
-      } else {
-        process.env["OPENCODE_CONFIG_DIR"] = originalConfigDir
-      }
-    }
-  })
+  it.live("loads both project and global AGENTS.md when both exist", () =>
+    Effect.gen(function* () {
+      const globalTmp = yield* tmpWithFiles({ "AGENTS.md": "# Global Instructions" })
+      const projectTmp = yield* tmpWithFiles({ "AGENTS.md": "# Project Instructions" })
+
+      yield* Effect.gen(function* () {
+        const svc = yield* Instruction.Service
+        const paths = yield* svc.systemPaths()
+        expect(paths.has(path.join(projectTmp, "AGENTS.md"))).toBe(true)
+        expect(paths.has(path.join(globalTmp, "AGENTS.md"))).toBe(true)
+
+        const rules = yield* svc.system()
+        expect(rules).toHaveLength(2)
+        expect(rules[0]).toBe(`Instructions from: ${path.join(globalTmp, "AGENTS.md")}\n# Global Instructions`)
+        expect(rules[1]).toBe(`Instructions from: ${path.join(projectTmp, "AGENTS.md")}\n# Project Instructions`)
+      }).pipe(provideInstance(projectTmp), provideInstruction({ home: globalTmp, config: globalTmp }))
+    }),
+  )
 })
 
-describe("Instruction.systemPaths OPENCODE_CONFIG_DIR", () => {
-  let originalConfigDir: string | undefined
-
-  beforeEach(() => {
-    originalConfigDir = process.env["OPENCODE_CONFIG_DIR"]
-  })
-
-  afterEach(() => {
-    if (originalConfigDir === undefined) {
-      delete process.env["OPENCODE_CONFIG_DIR"]
-    } else {
-      process.env["OPENCODE_CONFIG_DIR"] = originalConfigDir
-    }
-  })
-
-  test("prefers OPENCODE_CONFIG_DIR AGENTS.md over global when both exist", async () => {
-    await using profileTmp = await tmpdir({
-      init: async (dir) => {
-        await Bun.write(path.join(dir, "AGENTS.md"), "# Profile Instructions")
-      },
-    })
-    await using globalTmp = await tmpdir({
-      init: async (dir) => {
-        await Bun.write(path.join(dir, "AGENTS.md"), "# Global Instructions")
-      },
-    })
-    await using projectTmp = await tmpdir()
-
-    process.env["OPENCODE_CONFIG_DIR"] = profileTmp.path
-    const originalGlobalConfig = Global.Path.config
-    ;(Global.Path as { config: string }).config = globalTmp.path
-
-    try {
-      await Instance.provide({
-        directory: projectTmp.path,
-        fn: () =>
-          run(
-            Instruction.Service.use((svc) =>
-              Effect.gen(function* () {
-                const paths = yield* svc.systemPaths()
-                expect(paths.has(path.join(profileTmp.path, "AGENTS.md"))).toBe(true)
-                expect(paths.has(path.join(globalTmp.path, "AGENTS.md"))).toBe(false)
-              }),
-            ),
-          ),
-      })
-    } finally {
-      ;(Global.Path as { config: string }).config = originalGlobalConfig
-    }
-  })
-
-  test("falls back to global AGENTS.md when OPENCODE_CONFIG_DIR has no AGENTS.md", async () => {
-    await using profileTmp = await tmpdir()
-    await using globalTmp = await tmpdir({
-      init: async (dir) => {
-        await Bun.write(path.join(dir, "AGENTS.md"), "# Global Instructions")
-      },
-    })
-    await using projectTmp = await tmpdir()
-
-    process.env["OPENCODE_CONFIG_DIR"] = profileTmp.path
-    const originalGlobalConfig = Global.Path.config
-    ;(Global.Path as { config: string }).config = globalTmp.path
-
-    try {
-      await Instance.provide({
-        directory: projectTmp.path,
-        fn: () =>
-          run(
-            Instruction.Service.use((svc) =>
-              Effect.gen(function* () {
-                const paths = yield* svc.systemPaths()
-                expect(paths.has(path.join(profileTmp.path, "AGENTS.md"))).toBe(false)
-                expect(paths.has(path.join(globalTmp.path, "AGENTS.md"))).toBe(true)
-              }),
-            ),
-          ),
-      })
-    } finally {
-      ;(Global.Path as { config: string }).config = originalGlobalConfig
-    }
-  })
-
-  test("uses global AGENTS.md when OPENCODE_CONFIG_DIR is not set", async () => {
-    await using globalTmp = await tmpdir({
-      init: async (dir) => {
-        await Bun.write(path.join(dir, "AGENTS.md"), "# Global Instructions")
-      },
-    })
-    await using projectTmp = await tmpdir()
-
-    delete process.env["OPENCODE_CONFIG_DIR"]
-    const originalGlobalConfig = Global.Path.config
-    ;(Global.Path as { config: string }).config = globalTmp.path
-
-    try {
-      await Instance.provide({
-        directory: projectTmp.path,
-        fn: () =>
-          run(
-            Instruction.Service.use((svc) =>
-              Effect.gen(function* () {
-                const paths = yield* svc.systemPaths()
-                expect(paths.has(path.join(globalTmp.path, "AGENTS.md"))).toBe(true)
-              }),
-            ),
-          ),
-      })
-    } finally {
-      ;(Global.Path as { config: string }).config = originalGlobalConfig
-    }
-  })
+describe("Instruction.systemPaths global config", () => {
+  it.live("uses Global.Service config AGENTS.md", () =>
+    Effect.gen(function* () {
+      const globalTmp = yield* tmpWithFiles({ "AGENTS.md": "# Global Instructions" })
+      const projectTmp = yield* tmpdirScoped()
+
+      yield* Effect.gen(function* () {
+        const svc = yield* Instruction.Service
+        const paths = yield* svc.systemPaths()
+        expect(paths.has(path.join(globalTmp, "AGENTS.md"))).toBe(true)
+      }).pipe(provideInstance(projectTmp), provideInstruction({ home: globalTmp, config: globalTmp }))
+    }),
+  )
 })