/** * KHAELOR * File: tests/context/system-prompt.test.ts * Description: System-prompt tests — instruction discovery precedence (temp dirs) and byte-stable tier construction. * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { buildSystemPrompt, discoverProjectInstructions } from "../../src/context/index.js"; import { LocalWorkspace } from "../../src/workspace/index.js"; let root: string; beforeEach(async () => { root = await mkdtemp(join(tmpdir(), "khaelor-ctx-")); }); afterEach(async () => { await rm(root, { recursive: true, force: true }); }); describe("discoverProjectInstructions", () => { it("orders global → project root → nested dirs toward cwd (CLAUDE.md §12 precedence)", async () => { const userDir = join(root, "userdir"); const projectRoot = join(root, "proj"); const nested = join(projectRoot, "packages"); const deep = join(nested, "core"); await mkdir(userDir, { recursive: true }); await mkdir(deep, { recursive: true }); await writeFile(join(userDir, "KHAELOR.md"), "global rules"); await writeFile(join(projectRoot, "KHAELOR.md"), "project rules"); await writeFile(join(nested, "CLAUDE.md"), "packages rules"); await writeFile(join(deep, "AGENTS.md"), "core rules"); const reader = new LocalWorkspace(deep); const found = await discoverProjectInstructions(reader, { userDir, projectRoot }); expect(found.map((f) => ({ path: f.path, scope: f.scope, content: f.content }))).toEqual([ { path: join(userDir, "KHAELOR.md"), scope: "user", content: "global rules" }, { path: join(projectRoot, "KHAELOR.md"), scope: "project", content: "project rules" }, { path: join(nested, "CLAUDE.md"), scope: "nested", content: "packages rules" }, { path: join(deep, "AGENTS.md"), scope: "nested", content: "core rules" }, ]); }); it("prefers KHAELOR.md over CLAUDE.md over AGENTS.md within one directory", async () => { const projectRoot = join(root, "proj"); await mkdir(projectRoot, { recursive: true }); await writeFile(join(projectRoot, "KHAELOR.md"), "khaelor wins"); await writeFile(join(projectRoot, "CLAUDE.md"), "claude loses"); await writeFile(join(projectRoot, "AGENTS.md"), "agents loses"); const reader = new LocalWorkspace(projectRoot); const found = await discoverProjectInstructions(reader, { projectRoot }); expect(found).toHaveLength(1); expect(found[0]?.content).toBe("khaelor wins"); await rm(join(projectRoot, "KHAELOR.md")); const fallback = await discoverProjectInstructions(reader, { projectRoot }); expect(fallback[0]?.content).toBe("claude loses"); }); it("returns empty when nothing is discoverable and skips the user tier when userDir is omitted", async () => { const projectRoot = join(root, "empty"); await mkdir(projectRoot, { recursive: true }); const reader = new LocalWorkspace(projectRoot); expect(await discoverProjectInstructions(reader)).toEqual([]); }); }); describe("buildSystemPrompt", () => { const options = { workingDirectory: "/home/u/proj", toolNames: ["read", "write", "edit", "grep", "glob", "bash", "process"], instructions: [ { path: "/home/u/.khaelor/KHAELOR.md", scope: "user" as const, content: "be brief" }, { path: "/home/u/proj/KHAELOR.md", scope: "project" as const, content: "use tabs" }, ], }; it("builds three stable tiers in order: identity → tools → project instructions", () => { const tiers = buildSystemPrompt(options); expect(tiers.map((t) => t.name)).toEqual(["identity", "tools", "project-instructions"]); expect(tiers[0]?.text).toContain("KHAELOR"); expect(tiers[0]?.text).toContain("/home/u/proj"); expect(tiers[1]?.text).toContain("read, write, edit, grep, glob, bash, process"); expect(tiers[2]?.text).toContain("be brief"); expect(tiers[2]?.text).toContain("use tabs"); expect(tiers[2]?.text.indexOf("be brief")).toBeLessThan(tiers[2]?.text.indexOf("use tabs") ?? -1); }); it("is byte-stable: same inputs produce byte-identical tiers", () => { expect(JSON.stringify(buildSystemPrompt(options))).toBe(JSON.stringify(buildSystemPrompt(options))); }); it("omits the project-instructions tier when no instructions were discovered", () => { const tiers = buildSystemPrompt({ ...options, instructions: [] }); expect(tiers.map((t) => t.name)).toEqual(["identity", "tools"]); }); });