+21

![opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)



![opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)



James Long
Brendan Allan
Kit Langton
opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Affan Ali
affanali2k3
Frank
opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴
Aiden Cline
Jay V
Dax Raad
Aarav Sareen
OpeOginni
Luke Parker
Ben Guthrie
Dax
Filip
Max Anderson
Brendan Allan
Jack
Shoubhit Dash
Dustin Deus
starptech
Aiden Cline
usrnk1
Jay
runvip
opencode
Julian Coy
Vladimir Glafirov
8c94e9005f
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Affan Ali <93028901+affanali2k3@users.noreply.github.com> Co-authored-by: affanali2k3 <affanalikhanxx@gmail.com> Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Jay V <air@live.ca> Co-authored-by: Dax Raad <d@ironbay.co> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: Ben Guthrie <benjee.012@gmail.com> Co-authored-by: Dax <mail@thdxr.com> Co-authored-by: Filip <34747899+neriousy@users.noreply.github.com> Co-authored-by: Max Anderson <max.a.anderson95@gmail.com> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box> Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: runvip <164729189+runvip@users.noreply.github.com> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Julian Coy <julian@ex-machina.co> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com>
265 lines
10 KiB
TypeScript
265 lines
10 KiB
TypeScript
import { describe, expect, test } from "bun:test"
|
|
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
|
import path from "path"
|
|
import { Effect, FileSystem, Layer } from "effect"
|
|
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
|
|
|
import { Instruction } from "../../src/session/instruction"
|
|
import type { MessageV2 } from "../../src/session/message-v2"
|
|
import { MessageID, PartID, SessionID } from "../../src/session/schema"
|
|
import { Global } from "@opencode-ai/core/global"
|
|
import { RuntimeFlags } from "../../src/effect/runtime-flags"
|
|
import { provideInstance, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture"
|
|
import { testEffect } from "../lib/effect"
|
|
import { TestConfig } from "../fixture/config"
|
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
|
import { ModelV2 } from "@opencode-ai/core/model"
|
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
|
import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
|
|
import { InstanceStore } from "@/project/instance-store"
|
|
import { InstanceBootstrap } from "@/project/bootstrap"
|
|
import { Config } from "@/config/config"
|
|
|
|
const it = testEffect(
|
|
AppNodeBuilder.build(LayerNode.group([CrossSpawnSpawner.node, LayerNodePlatform.filesystem, InstanceStore.node]), [
|
|
[
|
|
InstanceBootstrap.node,
|
|
Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void })),
|
|
],
|
|
]),
|
|
)
|
|
|
|
const configLayer = Layer.succeed(Config.Service, TestConfig.make())
|
|
|
|
const instructionLayer = (global: Partial<Global.Interface>, flags: Partial<RuntimeFlags.Info> = {}) =>
|
|
AppNodeBuilder.build(Instruction.node, [
|
|
[Config.node, configLayer],
|
|
[Global.node, Global.layerWith(global)],
|
|
[RuntimeFlags.node, RuntimeFlags.layer(flags)],
|
|
])
|
|
|
|
const provideInstruction =
|
|
(global: Partial<Global.Interface>, flags?: Partial<RuntimeFlags.Info>) =>
|
|
<A, E, R>(self: Effect.Effect<A, E, R>) =>
|
|
self.pipe(Effect.provide(instructionLayer(global, flags)))
|
|
|
|
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): SessionV1.WithParts[] {
|
|
const sessionID = SessionID.make("session-loaded-1")
|
|
const messageID = MessageID.make("msg_message-loaded-1")
|
|
|
|
return [
|
|
{
|
|
info: {
|
|
id: messageID,
|
|
sessionID,
|
|
role: "user",
|
|
time: { created: 0 },
|
|
agent: "build",
|
|
model: {
|
|
providerID: ProviderV2.ID.make("anthropic"),
|
|
modelID: ModelV2.ID.make("claude-sonnet-4-20250514"),
|
|
},
|
|
},
|
|
parts: [
|
|
{
|
|
id: PartID.make("prt_part-loaded-1"),
|
|
messageID,
|
|
sessionID,
|
|
type: "tool",
|
|
callID: "call-loaded-1",
|
|
tool: "read",
|
|
state: {
|
|
status: "completed",
|
|
input: {},
|
|
output: "done",
|
|
title: "Read",
|
|
metadata: { loaded: [filepath] },
|
|
time: { start: 0, end: 1 },
|
|
},
|
|
},
|
|
],
|
|
},
|
|
]
|
|
}
|
|
|
|
describe("Instruction.resolve", () => {
|
|
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("msg_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("msg_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("msg_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("msg_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("msg_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("msg_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", () => {
|
|
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 }))
|
|
}),
|
|
)
|
|
|
|
it.live("skips project and global CLAUDE.md when Claude Code prompt is disabled", () =>
|
|
Effect.gen(function* () {
|
|
const globalTmp = yield* tmpWithFiles({ ".claude/CLAUDE.md": "# Global Claude" })
|
|
const projectTmp = yield* tmpWithFiles({ "CLAUDE.md": "# Project Claude" })
|
|
|
|
yield* Effect.gen(function* () {
|
|
const svc = yield* Instruction.Service
|
|
const paths = yield* svc.systemPaths()
|
|
expect(paths.has(path.join(globalTmp, ".claude", "CLAUDE.md"))).toBe(false)
|
|
expect(paths.has(path.join(projectTmp, "CLAUDE.md"))).toBe(false)
|
|
expect(yield* svc.system()).toEqual([])
|
|
}).pipe(
|
|
provideInstance(projectTmp),
|
|
provideInstruction({ home: globalTmp, config: globalTmp }, { disableClaudeCodePrompt: true }),
|
|
)
|
|
}),
|
|
)
|
|
})
|
|
|
|
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 }))
|
|
}),
|
|
)
|
|
})
|