| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264 |
- import { describe, expect, test } from "bun:test"
- import { SessionV1 } from "@kirincode-ai/core/v1/session"
- import path from "path"
- import { Effect, FileSystem, Layer } from "effect"
- import { CrossSpawnSpawner } from "@kirincode-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 "@kirincode-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 "@kirincode-ai/core/provider"
- import { ModelV2 } from "@kirincode-ai/core/model"
- import { AppNodeBuilder } from "@kirincode-ai/core/effect/app-node-builder"
- import { LayerNode } from "@kirincode-ai/core/effect/layer-node"
- import { LayerNodePlatform } from "@kirincode-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 }))
- }),
- )
- })
|