Files
LLeMbas-CLI/tests/instructions.test.ts
T
HomerandClaude Opus 5.5 5c23087cef
ci / check (push) Waiting to run
Harness spec 3.2.0: version, README, changelog
VERSION 3.2.0; README's file table, the `team` requirement and a 3.2.0
section; CHANGELOG Unreleased. Two tests that pinned the AGENTS.md
block's absolute root now pin the relative text (context.md §11).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 08:18:21 +00:00

82 lines
5.1 KiB
TypeScript

// Instruction files: the stacked chain from the project root to the working directory (spec 3.0.0),
// and config.yaml `instruction_files`, more files for the system prompt.
import { expect, test } from "bun:test"
import { mkdirSync, mkdtempSync, realpathSync, symlinkSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { loadConfig } from "../src/config/load.ts"
import { paths } from "../src/config/paths.ts"
import { assembleSystem, instructionFiles } from "../src/prompt/assemble.ts"
test("global entries may be anywhere; a project's only inside the project, links followed", () => {
const outside = mkdtempSync(join(tmpdir(), "ph-outside-"))
writeFileSync(join(outside, "style.md"), "Write tersely.")
writeFileSync(join(outside, "secret"), "PRIVATE KEY")
const root = mkdtempSync(join(tmpdir(), "ph-instr-"))
mkdirSync(join(root, "docs"))
writeFileSync(join(root, "docs", "rules.md"), "Tabs, not spaces.")
symlinkSync(join(outside, "secret"), join(root, "docs", "link.md"))
mkdirSync(join(root, ".agent"))
writeFileSync(join(root, ".agent", "config.yaml"), `instruction_files: [docs/rules.md, ${join(outside, "secret")}, docs/link.md, docs/missing.md]\n`)
mkdirSync(paths.config, { recursive: true })
writeFileSync(join(paths.config, "config.yaml"), `instruction_files: [${join(outside, "style.md")}]\n`)
const loaded = loadConfig({ projectConfigDir: join(root, ".agent"), trusted: true })
expect(loaded.instructions).toEqual([
{ path: join(outside, "style.md"), global: true },
{ path: "docs/rules.md", global: false },
{ path: join(outside, "secret"), global: false },
{ path: "docs/link.md", global: false },
{ path: "docs/missing.md", global: false },
])
const files = instructionFiles(root, root, loaded.instructions)
expect(files.map((f) => f.text)).toEqual(["Write tersely.", "Tabs, not spaces."])
writeFileSync(join(paths.config, "config.yaml"), "")
})
// Spec 3.0.0: instruction files stack from the project root down to the working directory,
// outermost first, one per directory (AGENTS.md before CLAUDE.md); before, only the nearest counted.
test("instruction files stack from the root down to the working directory, outermost first", () => {
const root = realpathSync(mkdtempSync(join(tmpdir(), "ph-stack-")))
const outside = mkdtempSync(join(tmpdir(), "ph-stack-out-"))
writeFileSync(join(outside, "AGENTS.md"), "NOT THE PROJECT'S")
mkdirSync(join(root, "pkg", "src", "deep"), { recursive: true })
writeFileSync(join(root, "AGENTS.md"), "root rules")
writeFileSync(join(root, "CLAUDE.md"), "never both")
writeFileSync(join(root, "pkg", "CLAUDE.md"), "package rules\n## Git\ncommit freely")
// A link out of the project is no instruction file: the walk goes on below it.
symlinkSync(join(outside, "AGENTS.md"), join(root, "pkg", "src", "AGENTS.md"))
writeFileSync(join(root, "pkg", "src", "deep", "AGENTS.md"), "deep rules")
// Below the working directory: not in the chain (the read tool hands it over when reached).
mkdirSync(join(root, "pkg", "src", "deep", "below"))
writeFileSync(join(root, "pkg", "src", "deep", "below", "AGENTS.md"), "below")
writeFileSync(join(paths.config, "config.yaml"), "")
const files = instructionFiles(join(root, "pkg", "src", "deep"), root)
expect(files.filter((f) => f.chain).map((f) => [f.path.slice(root.length + 1), f.text])).toEqual([
["AGENTS.md", "root rules"],
["pkg/CLAUDE.md", "package rules\n## Git\ncommit freely"],
["pkg/src/deep/AGENTS.md", "deep rules"],
])
// A working directory outside the root is read as the root.
expect(instructionFiles(outside, root).filter((f) => f.chain).map((f) => f.path)).toEqual([join(root, "AGENTS.md")])
// In the prompt: each by its path in the project, outermost first; git is settled by any of them.
const system = assembleSystem({ modelRef: "s/m", family: "local", cwd: join(root, "pkg", "src", "deep"), root, isGit: true, mode: "manual", planDir: join(root, ".agent", "plans"), toolNames: [] })
const at = (s: string) => system.indexOf(s)
expect(at("Instructions from AGENTS.md:")).toBeGreaterThan(-1)
expect(at("Instructions from AGENTS.md:")).toBeLessThan(at("Instructions from pkg/CLAUDE.md:"))
expect(at("Instructions from pkg/CLAUDE.md:")).toBeLessThan(at("Instructions from pkg/src/deep/AGENTS.md:"))
expect(system).not.toContain("NOT THE PROJECT'S")
expect(system).not.toContain("never both")
expect(system).toContain("## Keeping AGENTS.md")
expect(system).not.toContain("## Git in this project")
expect(system).not.toContain("This project has no AGENTS.md yet")
})
test("no instruction file anywhere on the way: the project is told to make one", () => {
const root = realpathSync(mkdtempSync(join(tmpdir(), "ph-stack-none-")))
mkdirSync(join(root, "a"))
writeFileSync(join(paths.config, "config.yaml"), "")
const system = assembleSystem({ modelRef: "s/m", family: "local", cwd: join(root, "a"), root, isGit: false, mode: "manual", planDir: join(root, ".agent", "plans"), toolNames: [] })
// Spec 3.2.0 (context.md §11): the block names no absolute path.
expect(system).toContain("It goes at the project root: `AGENTS.md`.")
})