ci / check (push) Waiting to run
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>
82 lines
5.1 KiB
TypeScript
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`.")
|
|
})
|