Compare commits

...

1 Commits

Author SHA1 Message Date
Kit Langton 0bcdec428a test: use testEffect for instruction tests 2026-04-29 22:37:36 -04:00
+204 -290
View File
@@ -1,16 +1,39 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
import path from "path"
import { Effect } from "effect"
import { Effect, Layer } from "effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
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 run = <A>(effect: Effect.Effect<A, any, Instruction.Service>) =>
Effect.runPromise(effect.pipe(Effect.provide(Instruction.defaultLayer)))
const it = testEffect(Layer.mergeAll(Instruction.defaultLayer, CrossSpawnSpawner.defaultLayer))
const write = (filepath: string, content: string) => Effect.promise(() => Bun.write(filepath, content))
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)
}),
)
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,224 +75,143 @@ 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)
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) =>
Instruction.Service.use((svc) =>
Effect.gen(function* () {
const system = yield* svc.systemPaths()
expect(system.has(path.join(dir, "AGENTS.md"))).toBe(true)
const results = yield* svc.resolve(
[],
path.join(tmp.path, "src", "file.ts"),
MessageID.make("message-test-1"),
)
expect(results).toEqual([])
}),
),
),
})
})
const results = yield* svc.resolve([], path.join(dir, "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)
it.live("returns AGENTS.md from subdirectory (not in systemPaths)", () =>
withFiles({ "subdir/AGENTS.md": "# Subdir Instructions", "subdir/nested/file.ts": "const x = 1" }, (dir) =>
Instruction.Service.use((svc) =>
Effect.gen(function* () {
const system = yield* svc.systemPaths()
expect(system.has(path.join(dir, "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"))
}),
),
),
})
})
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"))
}),
),
),
)
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)
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) =>
Instruction.Service.use((svc) =>
Effect.gen(function* () {
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([])
}),
),
),
})
})
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")
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) =>
Instruction.Service.use((svc) =>
Effect.gen(function* () {
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)
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([])
}),
),
),
})
})
expect(first).toHaveLength(1)
expect(first[0].filepath).toBe(path.join(dir, "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")
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) =>
Instruction.Service.use((svc) =>
Effect.gen(function* () {
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)
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"))
}),
),
),
})
})
expect(first).toHaveLength(1)
expect(second).toHaveLength(1)
expect(second[0].filepath).toBe(path.join(dir, "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")
it.live("skips instructions already reported by prior read metadata", () =>
withFiles({ "subdir/AGENTS.md": "# Subdir Instructions", "subdir/nested/file.ts": "const x = 1" }, (dir) =>
Instruction.Service.use((svc) =>
Effect.gen(function* () {
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([])
}),
),
),
})
})
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"]
it.live("loads both project and global AGENTS.md when both exist", () =>
Effect.gen(function* () {
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 globalTmp = yield* tmpWithFiles({ "AGENTS.md": "# Global Instructions" })
const projectTmp = yield* tmpWithFiles({ "AGENTS.md": "# Project Instructions" })
const originalGlobalConfig = Global.Path.config
;(Global.Path as { config: string }).config = globalTmp
const originalGlobalConfig = Global.Path.config
;(Global.Path as { config: string }).config = globalTmp.path
try {
yield* Instruction.Service.use((svc) =>
Effect.gen(function* () {
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)
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
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))
} finally {
;(Global.Path as { config: string }).config = originalGlobalConfig
if (originalConfigDir === undefined) {
delete process.env["OPENCODE_CONFIG_DIR"]
} else {
process.env["OPENCODE_CONFIG_DIR"] = originalConfigDir
}
}
}
})
}),
)
})
describe("Instruction.systemPaths OPENCODE_CONFIG_DIR", () => {
@@ -287,101 +229,73 @@ describe("Instruction.systemPaths OPENCODE_CONFIG_DIR", () => {
}
})
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()
it.live("prefers OPENCODE_CONFIG_DIR AGENTS.md over global when both exist", () =>
Effect.gen(function* () {
const profileTmp = yield* tmpWithFiles({ "AGENTS.md": "# Profile Instructions" })
const globalTmp = yield* tmpWithFiles({ "AGENTS.md": "# Global Instructions" })
const projectTmp = yield* tmpdirScoped()
process.env["OPENCODE_CONFIG_DIR"] = profileTmp.path
const originalGlobalConfig = Global.Path.config
;(Global.Path as { config: string }).config = globalTmp.path
process.env["OPENCODE_CONFIG_DIR"] = profileTmp
const originalGlobalConfig = Global.Path.config
;(Global.Path as { config: string }).config = globalTmp
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
}
})
try {
yield* Instruction.Service.use((svc) =>
Effect.gen(function* () {
const paths = yield* svc.systemPaths()
expect(paths.has(path.join(profileTmp, "AGENTS.md"))).toBe(true)
expect(paths.has(path.join(globalTmp, "AGENTS.md"))).toBe(false)
}),
).pipe(provideInstance(projectTmp))
} 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()
it.live("falls back to global AGENTS.md when OPENCODE_CONFIG_DIR has no AGENTS.md", () =>
Effect.gen(function* () {
const profileTmp = yield* tmpdirScoped()
const globalTmp = yield* tmpWithFiles({ "AGENTS.md": "# Global Instructions" })
const projectTmp = yield* tmpdirScoped()
process.env["OPENCODE_CONFIG_DIR"] = profileTmp.path
const originalGlobalConfig = Global.Path.config
;(Global.Path as { config: string }).config = globalTmp.path
process.env["OPENCODE_CONFIG_DIR"] = profileTmp
const originalGlobalConfig = Global.Path.config
;(Global.Path as { config: string }).config = globalTmp
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
}
})
try {
yield* Instruction.Service.use((svc) =>
Effect.gen(function* () {
const paths = yield* svc.systemPaths()
expect(paths.has(path.join(profileTmp, "AGENTS.md"))).toBe(false)
expect(paths.has(path.join(globalTmp, "AGENTS.md"))).toBe(true)
}),
).pipe(provideInstance(projectTmp))
} 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()
it.live("uses global AGENTS.md when OPENCODE_CONFIG_DIR is not set", () =>
Effect.gen(function* () {
const globalTmp = yield* tmpWithFiles({ "AGENTS.md": "# Global Instructions" })
const projectTmp = yield* tmpdirScoped()
delete process.env["OPENCODE_CONFIG_DIR"]
const originalGlobalConfig = Global.Path.config
;(Global.Path as { config: string }).config = globalTmp.path
delete process.env["OPENCODE_CONFIG_DIR"]
const originalGlobalConfig = Global.Path.config
;(Global.Path as { config: string }).config = globalTmp
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
}
})
try {
yield* Instruction.Service.use((svc) =>
Effect.gen(function* () {
const paths = yield* svc.systemPaths()
expect(paths.has(path.join(globalTmp, "AGENTS.md"))).toBe(true)
}),
).pipe(provideInstance(projectTmp))
} finally {
;(Global.Path as { config: string }).config = originalGlobalConfig
}
}),
)
})