test: use testEffect for instruction tests (#25046)
This commit is contained in:
@@ -4,6 +4,7 @@ import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir"
|
|||||||
import os from "os"
|
import os from "os"
|
||||||
import { Context, Effect, Layer } from "effect"
|
import { Context, Effect, Layer } from "effect"
|
||||||
import { Flock } from "./util/flock"
|
import { Flock } from "./util/flock"
|
||||||
|
import { Flag } from "./flag/flag"
|
||||||
|
|
||||||
const app = "opencode"
|
const app = "opencode"
|
||||||
const data = path.join(xdgData!, app)
|
const data = path.join(xdgData!, app)
|
||||||
@@ -47,19 +48,28 @@ export interface Interface {
|
|||||||
readonly log: string
|
readonly log: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const layer = Layer.effect(
|
export function make(input: Partial<Interface> = {}): Interface {
|
||||||
Service,
|
return {
|
||||||
Effect.gen(function* () {
|
|
||||||
return Service.of({
|
|
||||||
home: Path.home,
|
home: Path.home,
|
||||||
data: Path.data,
|
data: Path.data,
|
||||||
cache: Path.cache,
|
cache: Path.cache,
|
||||||
config: Path.config,
|
config: Flag.OPENCODE_CONFIG_DIR ?? Path.config,
|
||||||
state: Path.state,
|
state: Path.state,
|
||||||
bin: Path.bin,
|
bin: Path.bin,
|
||||||
log: Path.log,
|
log: Path.log,
|
||||||
})
|
...input,
|
||||||
}),
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const layer = Layer.effect(
|
||||||
|
Service,
|
||||||
|
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"
|
export * as Global from "./global"
|
||||||
|
|||||||
@@ -18,11 +18,9 @@ function sleep(ms: number) {
|
|||||||
return new Promise<void>((resolve) => setTimeout(resolve, ms))
|
return new Promise<void>((resolve) => setTimeout(resolve, ms))
|
||||||
}
|
}
|
||||||
|
|
||||||
const msg: Msg = JSON.parse(process.argv[2]!)
|
const msg: Msg = JSON.parse(process.argv[2])
|
||||||
|
|
||||||
const testGlobal = Layer.succeed(
|
const testGlobal = Global.layerWith({
|
||||||
Global.Service,
|
|
||||||
Global.Service.of({
|
|
||||||
home: os.homedir(),
|
home: os.homedir(),
|
||||||
data: os.tmpdir(),
|
data: os.tmpdir(),
|
||||||
cache: os.tmpdir(),
|
cache: os.tmpdir(),
|
||||||
@@ -30,8 +28,7 @@ const testGlobal = Layer.succeed(
|
|||||||
state: os.tmpdir(),
|
state: os.tmpdir(),
|
||||||
bin: os.tmpdir(),
|
bin: os.tmpdir(),
|
||||||
log: os.tmpdir(),
|
log: os.tmpdir(),
|
||||||
}),
|
})
|
||||||
)
|
|
||||||
|
|
||||||
const testLayer = EffectFlock.layer.pipe(Layer.provide(testGlobal), Layer.provide(AppFileSystem.defaultLayer))
|
const testLayer = EffectFlock.layer.pipe(Layer.provide(testGlobal), Layer.provide(AppFileSystem.defaultLayer))
|
||||||
|
|
||||||
|
|||||||
@@ -93,9 +93,7 @@ async function waitForFile(file: string, timeout = 3_000) {
|
|||||||
// Test layer
|
// Test layer
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const testGlobal = Layer.succeed(
|
const testGlobal = Global.layerWith({
|
||||||
Global.Service,
|
|
||||||
Global.Service.of({
|
|
||||||
home: os.homedir(),
|
home: os.homedir(),
|
||||||
data: os.tmpdir(),
|
data: os.tmpdir(),
|
||||||
cache: os.tmpdir(),
|
cache: os.tmpdir(),
|
||||||
@@ -103,8 +101,7 @@ const testGlobal = Layer.succeed(
|
|||||||
state: os.tmpdir(),
|
state: os.tmpdir(),
|
||||||
bin: os.tmpdir(),
|
bin: os.tmpdir(),
|
||||||
log: os.tmpdir(),
|
log: os.tmpdir(),
|
||||||
}),
|
})
|
||||||
)
|
|
||||||
|
|
||||||
const testLayer = EffectFlock.layer.pipe(Layer.provide(testGlobal), Layer.provide(AppFileSystem.defaultLayer))
|
const testLayer = EffectFlock.layer.pipe(Layer.provide(testGlobal), Layer.provide(AppFileSystem.defaultLayer))
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import os from "os"
|
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect, Layer, Context } from "effect"
|
import { Effect, Layer, Context } from "effect"
|
||||||
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"
|
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 { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||||
import { withTransientReadRetry } from "@/util/effect-http-client"
|
import { withTransientReadRetry } from "@/util/effect-http-client"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import * as Log from "@opencode-ai/core/util/log"
|
|
||||||
import type { MessageV2 } from "./message-v2"
|
import type { MessageV2 } from "./message-v2"
|
||||||
import type { MessageID } from "./schema"
|
import type { MessageID } from "./schema"
|
||||||
|
|
||||||
const log = Log.create({ service: "instruction" })
|
|
||||||
|
|
||||||
const FILES = [
|
const FILES = [
|
||||||
"AGENTS.md",
|
"AGENTS.md",
|
||||||
...(Flag.OPENCODE_DISABLE_CLAUDE_CODE_PROMPT ? [] : ["CLAUDE.md"]),
|
...(Flag.OPENCODE_DISABLE_CLAUDE_CODE_PROMPT ? [] : ["CLAUDE.md"]),
|
||||||
"CONTEXT.md", // deprecated
|
"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[]) {
|
function extract(messages: MessageV2.WithParts[]) {
|
||||||
const paths = new Set<string>()
|
const paths = new Set<string>()
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
@@ -63,13 +47,21 @@ export interface Interface {
|
|||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Instruction") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/Instruction") {}
|
||||||
|
|
||||||
export const layer: Layer.Layer<Service, never, AppFileSystem.Service | Config.Service | HttpClient.HttpClient> =
|
export const layer: Layer.Layer<
|
||||||
Layer.effect(
|
Service,
|
||||||
|
never,
|
||||||
|
AppFileSystem.Service | Config.Service | Global.Service | HttpClient.HttpClient
|
||||||
|
> = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const cfg = yield* Config.Service
|
const cfg = yield* Config.Service
|
||||||
const fs = yield* AppFileSystem.Service
|
const fs = yield* AppFileSystem.Service
|
||||||
|
const global = yield* Global.Service
|
||||||
const http = HttpClient.filterStatusOk(withTransientReadRetry(yield* HttpClient.HttpClient))
|
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(
|
const state = yield* InstanceState.make(
|
||||||
Effect.fn("Instruction.state")(() =>
|
Effect.fn("Instruction.state")(() =>
|
||||||
@@ -87,14 +79,8 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | Config.S
|
|||||||
.globUp(instruction, ctx.directory, ctx.worktree)
|
.globUp(instruction, ctx.directory, ctx.worktree)
|
||||||
.pipe(Effect.catch(() => Effect.succeed([] as string[])))
|
.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 []
|
|
||||||
}
|
|
||||||
return yield* fs
|
return yield* fs
|
||||||
.globUp(instruction, Flag.OPENCODE_CONFIG_DIR, Flag.OPENCODE_CONFIG_DIR)
|
.globUp(instruction, global.config, global.config)
|
||||||
.pipe(Effect.catch(() => Effect.succeed([] as string[])))
|
.pipe(Effect.catch(() => Effect.succeed([] as string[])))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -122,7 +108,7 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | Config.S
|
|||||||
const ctx = yield* InstanceState.context
|
const ctx = yield* InstanceState.context
|
||||||
const paths = new Set<string>()
|
const paths = new Set<string>()
|
||||||
|
|
||||||
for (const file of globalFiles()) {
|
for (const file of globalFiles) {
|
||||||
if (yield* fs.existsSafe(file)) {
|
if (yield* fs.existsSafe(file)) {
|
||||||
paths.add(path.resolve(file))
|
paths.add(path.resolve(file))
|
||||||
break
|
break
|
||||||
@@ -143,7 +129,7 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | Config.S
|
|||||||
if (config.instructions) {
|
if (config.instructions) {
|
||||||
for (const raw of config.instructions) {
|
for (const raw of config.instructions) {
|
||||||
if (raw.startsWith("https://") || raw.startsWith("http://")) continue
|
if (raw.startsWith("https://") || raw.startsWith("http://")) continue
|
||||||
const instruction = raw.startsWith("~/") ? path.join(os.homedir(), raw.slice(2)) : raw
|
const instruction = raw.startsWith("~/") ? path.join(global.home, raw.slice(2)) : raw
|
||||||
const matches = yield* (
|
const matches = yield* (
|
||||||
path.isAbsolute(instruction)
|
path.isAbsolute(instruction)
|
||||||
? fs.glob(path.basename(instruction), {
|
? fs.glob(path.basename(instruction), {
|
||||||
@@ -181,6 +167,7 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | Config.S
|
|||||||
const filepath = path.resolve(path.join(dir, file))
|
const filepath = path.resolve(path.join(dir, file))
|
||||||
if (yield* fs.existsSafe(filepath)) return filepath
|
if (yield* fs.existsSafe(filepath)) return filepath
|
||||||
}
|
}
|
||||||
|
return undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
const resolve = Effect.fn("Instruction.resolve")(function* (
|
const resolve = Effect.fn("Instruction.resolve")(function* (
|
||||||
@@ -233,6 +220,7 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | Config.S
|
|||||||
|
|
||||||
export const defaultLayer = layer.pipe(
|
export const defaultLayer = layer.pipe(
|
||||||
Layer.provide(Config.defaultLayer),
|
Layer.provide(Config.defaultLayer),
|
||||||
|
Layer.provide(Global.layer),
|
||||||
Layer.provide(AppFileSystem.defaultLayer),
|
Layer.provide(AppFileSystem.defaultLayer),
|
||||||
Layer.provide(FetchHttpClient.layer),
|
Layer.provide(FetchHttpClient.layer),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 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 { ModelID, ProviderID } from "../../src/provider/schema"
|
||||||
import { Instruction } from "../../src/session/instruction"
|
import { Instruction } from "../../src/session/instruction"
|
||||||
import type { MessageV2 } from "../../src/session/message-v2"
|
import type { MessageV2 } from "../../src/session/message-v2"
|
||||||
import { Instance } from "../../src/project/instance"
|
|
||||||
import { MessageID, PartID, SessionID } from "../../src/session/schema"
|
import { MessageID, PartID, SessionID } from "../../src/session/schema"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
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>) =>
|
const it = testEffect(Layer.mergeAll(CrossSpawnSpawner.defaultLayer, NodeFileSystem.layer))
|
||||||
Effect.runPromise(effect.pipe(Effect.provide(Instruction.defaultLayer)))
|
|
||||||
|
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 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[] {
|
function loaded(filepath: string): MessageV2.WithParts[] {
|
||||||
const sessionID = SessionID.make("session-loaded-1")
|
const sessionID = SessionID.make("session-loaded-1")
|
||||||
@@ -52,77 +112,42 @@ function loaded(filepath: string): MessageV2.WithParts[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("Instruction.resolve", () => {
|
describe("Instruction.resolve", () => {
|
||||||
test("returns empty when AGENTS.md is at project root (already in systemPaths)", async () => {
|
it.live("returns empty when AGENTS.md is at project root (already in systemPaths)", () =>
|
||||||
await using tmp = await tmpdir({
|
withFiles({ "AGENTS.md": "# Root Instructions", "src/file.ts": "const x = 1" }, (dir) =>
|
||||||
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* () {
|
Effect.gen(function* () {
|
||||||
|
const svc = yield* Instruction.Service
|
||||||
const system = yield* svc.systemPaths()
|
const system = yield* svc.systemPaths()
|
||||||
expect(system.has(path.join(tmp.path, "AGENTS.md"))).toBe(true)
|
expect(system.has(path.join(dir, "AGENTS.md"))).toBe(true)
|
||||||
|
|
||||||
const results = yield* svc.resolve(
|
const results = yield* svc.resolve([], path.join(dir, "src", "file.ts"), MessageID.make("message-test-1"))
|
||||||
[],
|
|
||||||
path.join(tmp.path, "src", "file.ts"),
|
|
||||||
MessageID.make("message-test-1"),
|
|
||||||
)
|
|
||||||
expect(results).toEqual([])
|
expect(results).toEqual([])
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("returns AGENTS.md from subdirectory (not in systemPaths)", async () => {
|
it.live("returns AGENTS.md from subdirectory (not in systemPaths)", () =>
|
||||||
await using tmp = await tmpdir({
|
withFiles({ "subdir/AGENTS.md": "# Subdir Instructions", "subdir/nested/file.ts": "const x = 1" }, (dir) =>
|
||||||
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* () {
|
Effect.gen(function* () {
|
||||||
|
const svc = yield* Instruction.Service
|
||||||
const system = yield* svc.systemPaths()
|
const system = yield* svc.systemPaths()
|
||||||
expect(system.has(path.join(tmp.path, "subdir", "AGENTS.md"))).toBe(false)
|
expect(system.has(path.join(dir, "subdir", "AGENTS.md"))).toBe(false)
|
||||||
|
|
||||||
const results = yield* svc.resolve(
|
const results = yield* svc.resolve(
|
||||||
[],
|
[],
|
||||||
path.join(tmp.path, "subdir", "nested", "file.ts"),
|
path.join(dir, "subdir", "nested", "file.ts"),
|
||||||
MessageID.make("message-test-2"),
|
MessageID.make("message-test-2"),
|
||||||
)
|
)
|
||||||
expect(results.length).toBe(1)
|
expect(results.length).toBe(1)
|
||||||
expect(results[0].filepath).toBe(path.join(tmp.path, "subdir", "AGENTS.md"))
|
expect(results[0].filepath).toBe(path.join(dir, "subdir", "AGENTS.md"))
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("doesn't reload AGENTS.md when reading it directly", async () => {
|
it.live("doesn't reload AGENTS.md when reading it directly", () =>
|
||||||
await using tmp = await tmpdir({
|
withFiles({ "subdir/AGENTS.md": "# Subdir Instructions", "subdir/nested/file.ts": "const x = 1" }, (dir) =>
|
||||||
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* () {
|
Effect.gen(function* () {
|
||||||
const filepath = path.join(tmp.path, "subdir", "AGENTS.md")
|
const svc = yield* Instruction.Service
|
||||||
|
const filepath = path.join(dir, "subdir", "AGENTS.md")
|
||||||
const system = yield* svc.systemPaths()
|
const system = yield* svc.systemPaths()
|
||||||
expect(system.has(filepath)).toBe(false)
|
expect(system.has(filepath)).toBe(false)
|
||||||
|
|
||||||
@@ -130,52 +155,30 @@ describe("Instruction.resolve", () => {
|
|||||||
expect(results).toEqual([])
|
expect(results).toEqual([])
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("does not reattach the same nearby instructions twice for one message", async () => {
|
it.live("does not reattach the same nearby instructions twice for one message", () =>
|
||||||
await using tmp = await tmpdir({
|
withFiles({ "subdir/AGENTS.md": "# Subdir Instructions", "subdir/nested/file.ts": "const x = 1" }, (dir) =>
|
||||||
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* () {
|
Effect.gen(function* () {
|
||||||
const filepath = path.join(tmp.path, "subdir", "nested", "file.ts")
|
const svc = yield* Instruction.Service
|
||||||
|
const filepath = path.join(dir, "subdir", "nested", "file.ts")
|
||||||
const id = MessageID.make("message-claim-1")
|
const id = MessageID.make("message-claim-1")
|
||||||
|
|
||||||
const first = yield* svc.resolve([], filepath, id)
|
const first = yield* svc.resolve([], filepath, id)
|
||||||
const second = yield* svc.resolve([], filepath, id)
|
const second = yield* svc.resolve([], filepath, id)
|
||||||
|
|
||||||
expect(first).toHaveLength(1)
|
expect(first).toHaveLength(1)
|
||||||
expect(first[0].filepath).toBe(path.join(tmp.path, "subdir", "AGENTS.md"))
|
expect(first[0].filepath).toBe(path.join(dir, "subdir", "AGENTS.md"))
|
||||||
expect(second).toEqual([])
|
expect(second).toEqual([])
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("clear allows nearby instructions to be attached again for the same message", async () => {
|
it.live("clear allows nearby instructions to be attached again for the same message", () =>
|
||||||
await using tmp = await tmpdir({
|
withFiles({ "subdir/AGENTS.md": "# Subdir Instructions", "subdir/nested/file.ts": "const x = 1" }, (dir) =>
|
||||||
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* () {
|
Effect.gen(function* () {
|
||||||
const filepath = path.join(tmp.path, "subdir", "nested", "file.ts")
|
const svc = yield* Instruction.Service
|
||||||
|
const filepath = path.join(dir, "subdir", "nested", "file.ts")
|
||||||
const id = MessageID.make("message-claim-2")
|
const id = MessageID.make("message-claim-2")
|
||||||
|
|
||||||
const first = yield* svc.resolve([], filepath, id)
|
const first = yield* svc.resolve([], filepath, id)
|
||||||
@@ -184,204 +187,60 @@ describe("Instruction.resolve", () => {
|
|||||||
|
|
||||||
expect(first).toHaveLength(1)
|
expect(first).toHaveLength(1)
|
||||||
expect(second).toHaveLength(1)
|
expect(second).toHaveLength(1)
|
||||||
expect(second[0].filepath).toBe(path.join(tmp.path, "subdir", "AGENTS.md"))
|
expect(second[0].filepath).toBe(path.join(dir, "subdir", "AGENTS.md"))
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("skips instructions already reported by prior read metadata", async () => {
|
it.live("skips instructions already reported by prior read metadata", () =>
|
||||||
await using tmp = await tmpdir({
|
withFiles({ "subdir/AGENTS.md": "# Subdir Instructions", "subdir/nested/file.ts": "const x = 1" }, (dir) =>
|
||||||
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* () {
|
Effect.gen(function* () {
|
||||||
const agents = path.join(tmp.path, "subdir", "AGENTS.md")
|
const svc = yield* Instruction.Service
|
||||||
const filepath = path.join(tmp.path, "subdir", "nested", "file.ts")
|
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 id = MessageID.make("message-claim-3")
|
||||||
|
|
||||||
const results = yield* svc.resolve(loaded(agents), filepath, id)
|
const results = yield* svc.resolve(loaded(agents), filepath, id)
|
||||||
expect(results).toEqual([])
|
expect(results).toEqual([])
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test.todo("fetches remote instructions from config URLs via HttpClient", () => {})
|
test.todo("fetches remote instructions from config URLs via HttpClient", () => {})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("Instruction.system", () => {
|
describe("Instruction.system", () => {
|
||||||
test("loads both project and global AGENTS.md when both exist", async () => {
|
it.live("loads both project and global AGENTS.md when both exist", () =>
|
||||||
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* () {
|
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()
|
const paths = yield* svc.systemPaths()
|
||||||
expect(paths.has(path.join(projectTmp.path, "AGENTS.md"))).toBe(true)
|
expect(paths.has(path.join(projectTmp, "AGENTS.md"))).toBe(true)
|
||||||
expect(paths.has(path.join(globalTmp.path, "AGENTS.md"))).toBe(true)
|
expect(paths.has(path.join(globalTmp, "AGENTS.md"))).toBe(true)
|
||||||
|
|
||||||
const rules = yield* svc.system()
|
const rules = yield* svc.system()
|
||||||
expect(rules).toHaveLength(2)
|
expect(rules).toHaveLength(2)
|
||||||
expect(rules[0]).toBe(
|
expect(rules[0]).toBe(`Instructions from: ${path.join(globalTmp, "AGENTS.md")}\n# Global Instructions`)
|
||||||
`Instructions from: ${path.join(globalTmp.path, "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 }))
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
expect(rules[1]).toBe(
|
})
|
||||||
`Instructions from: ${path.join(projectTmp.path, "AGENTS.md")}\n# Project Instructions`,
|
|
||||||
|
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 }))
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
} 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", () => {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user