import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { buildSystemPrompt } from "./system-prompt.js";
const tempDirs = [];
async function makeProject(files = {}) {
    const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "ezcoder-system-prompt-"));
    tempDirs.push(cwd);
    for (const [relativePath, content] of Object.entries(files)) {
        const filePath = path.join(cwd, relativePath);
        await fs.mkdir(path.dirname(filePath), { recursive: true });
        await fs.writeFile(filePath, content, "utf-8");
    }
    return cwd;
}
function sectionIndex(prompt, heading) {
    const index = prompt.indexOf(heading);
    expect(index, `${heading} should exist`).toBeGreaterThanOrEqual(0);
    return index;
}
function toolsSection(prompt) {
    const start = sectionIndex(prompt, "## Tools");
    const rest = prompt.slice(start);
    const next = rest.indexOf("\n\n## ", "## Tools".length);
    return next === -1 ? rest : rest.slice(0, next);
}
function promptSize(prompt) {
    return {
        characters: prompt.length,
        lines: prompt.split("\n").length,
        sections: prompt.match(/^## /gm)?.length ?? 0,
    };
}
function promptAudit(prompt) {
    const flags = [];
    const obsoleteOrContradictory = [
        "what observable artifact would prove the requested outcome worked end-to-end",
        "the simplest reliable local/free proof path",
        "generic tests, scripts, screenshots, benchmarks, or simulations; use them by default",
        "After meaningful edits, run the relevant verification commands below",
        "Run relevant checks after edits",
    ];
    for (const phrase of obsoleteOrContradictory) {
        if (prompt.includes(phrase))
            flags.push(`obsolete/contradictory guidance: ${phrase}`);
    }
    const repeatedSentences = new Map();
    for (const sentence of prompt
        .replace(/\s+/g, " ")
        .split(/(?<=[.!?])\s+/)
        .map((item) => item.trim())
        .filter((item) => item.length >= 80)) {
        repeatedSentences.set(sentence, (repeatedSentences.get(sentence) ?? 0) + 1);
    }
    for (const [sentence, count] of repeatedSentences) {
        if (count > 1)
            flags.push(`duplicate sentence x${count}: ${sentence}`);
    }
    return { size: promptSize(prompt), flags };
}
afterEach(async () => {
    await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })));
});
describe("buildSystemPrompt", () => {
    it("renders deterministic section order and keeps only the volatile date after the marker", async () => {
        const cwd = await makeProject({
            "CLAUDE.md": "Project rules win.",
            "package.json": JSON.stringify({ scripts: { check: "tsc --noEmit" } }),
            "tsconfig.json": "{}",
        });
        const prompt = await buildSystemPrompt(cwd, [{ name: "find-skills", description: "Find skills.", content: "", source: "test" }], false, undefined, ["read", "edit", "web_search", "skill"], new Set(["typescript"]));
        expect(prompt.startsWith("You are EZ Coder by Nolan Grout")).toBe(true);
        expect(sectionIndex(prompt, "## How to Talk")).toBeLessThan(sectionIndex(prompt, "## How to Work"));
        expect(sectionIndex(prompt, "## How to Work")).toBeLessThan(sectionIndex(prompt, "## Research & Verification"));
        expect(sectionIndex(prompt, "## Research & Verification")).toBeLessThan(sectionIndex(prompt, "## Code Quality"));
        expect(prompt).toContain("Woops I just farted!");
        expect(prompt).toContain("don't force it or repeat one line");
        expect(prompt).not.toContain("Do not default to generic tests, scripts, screenshots, benchmarks, or simulations");
        expect(sectionIndex(prompt, "## Code Quality")).toBeLessThan(sectionIndex(prompt, "## Tools"));
        expect(sectionIndex(prompt, "## Tools")).toBeLessThan(sectionIndex(prompt, "## Project Context"));
        expect(sectionIndex(prompt, "## Project Context")).toBeLessThan(sectionIndex(prompt, "## Language Style Packs"));
        expect(sectionIndex(prompt, "## Language Style Packs")).toBeLessThan(sectionIndex(prompt, "## Verification"));
        expect(sectionIndex(prompt, "## Verification")).toBeLessThan(sectionIndex(prompt, "## Skills"));
        expect(sectionIndex(prompt, "## Skills")).toBeLessThan(sectionIndex(prompt, "## Environment"));
        const marker = "<!-- uncached -->";
        expect(prompt.match(new RegExp(marker, "g"))).toHaveLength(1);
        const afterMarker = prompt.slice(prompt.indexOf(marker) + marker.length).trim();
        expect(afterMarker).toMatch(/^Today's date: \d{1,2} [A-Za-z]+ \d{4}$/);
    });
    it("lists exactly available known tools", async () => {
        const cwd = await makeProject();
        const prompt = await buildSystemPrompt(cwd, undefined, false, undefined, [
            "read",
            "write",
            "edit",
            "web_search",
            "not_a_tool",
        ]);
        const renderedTools = toolsSection(prompt);
        // Core file tools (read/write/edit) no longer carry a per-tool hint — they
        // rely on their schema description plus the cross-tool steering line (which
        // renders here because edit + write are both active). Tools with non-obvious
        // usage (web_search) still render a hint. Unknown tools never do.
        expect(renderedTools).toContain("Prefer `edit` over `write`");
        expect(renderedTools).toContain("**web_search**");
        expect(renderedTools).not.toContain("not_a_tool");
        expect(renderedTools).not.toContain("**read**");
        expect(renderedTools).not.toContain("**edit**");
    });
    it("states rule precedence exactly once and keeps project context before style packs", async () => {
        const cwd = await makeProject({
            "AGENTS.md": "Use tabs for this fixture.",
            "tsconfig.json": "{}",
        });
        const prompt = await buildSystemPrompt(cwd, undefined, false, undefined, ["read"], new Set(["typescript"]));
        // Precedence lives in How to Work only — not restated in Project Context or Style Packs.
        expect(prompt).toContain("Rule precedence: project context files");
        expect(prompt.match(/Rule precedence/g)).toHaveLength(1);
        expect(prompt).not.toContain("**Highest precedence**");
        expect(prompt).not.toContain("override default guidance");
        expect(prompt).not.toContain("override these defaults");
        expect(sectionIndex(prompt, "## Project Context")).toBeLessThan(sectionIndex(prompt, "## Language Style Packs"));
    });
    it("renders normal mode as direct coding mode", async () => {
        const cwd = await makeProject();
        const prompt = await buildSystemPrompt(cwd, undefined, false, undefined, [
            "read",
            "edit",
            "write",
            "bash",
            "subagent",
        ]);
        expect(prompt).toContain("works directly in the user's codebase");
        expect(prompt).toContain("completing tasks end-to-end");
    });
    it("preserves critical operating rules concisely", async () => {
        const cwd = await makeProject({ "AGENTS.md": "Project rules win." });
        const prompt = await buildSystemPrompt(cwd, undefined, true, undefined, [
            "read",
            "edit",
            "write",
            "bash",
            "web_search",
            "web_fetch",
            "source_path",
            "mcp__kencode-search__referenceSources",
            "mcp__kencode-search__discoverRepos",
            "mcp__kencode-search__searchCode",
        ]);
        for (const required of [
            "works directly in the user's codebase",
            "completing tasks end-to-end",
            "Final replies: 1–2 sentences, hard cap 5",
            "Read before `edit`/`write`",
            "re-read after formatters",
            "Compute in bash; write with `edit`/`write`",
            "Match neighbors",
            "Keep edits small",
            "Do routine follow-up yourself",
            "Ask first for destructive actions",
            "Preserve user work",
            "Rule precedence: project context files",
            "Your training data has a cutoff",
            "treat it as a stale hint to verify, never as ground truth",
            "Do not rely on memory for APIs",
            "Use `source_path`",
            "web_search` then `web_fetch",
            "ReferenceSources",
            "DiscoverRepos",
            "SearchCode literal text/RE2 (not semantic)",
            "Choose targeted verification appropriate to the change",
        ]) {
            expect(prompt).toContain(required);
        }
    });
    it("keeps kencode guidance concise while separating repo discovery from exact search", async () => {
        const cwd = await makeProject();
        const prompt = await buildSystemPrompt(cwd, undefined, false, undefined, [
            "mcp__kencode-search__referenceSources",
            "mcp__kencode-search__discoverRepos",
            "mcp__kencode-search__searchCode",
        ]);
        const tools = toolsSection(prompt);
        expect(tools).toContain("curated, categorized reference repos");
        expect(tools).toContain("Search GitHub repos live");
        expect(tools).toContain("returns metadata, not snippets");
        expect(tools).toContain("literal text or RE2 regex");
        expect(tools).toContain("NOT semantic");
        expect(tools).toContain("path` is a literal file-path substring");
        expect(tools).not.toContain("zero hits, every time");
        expect(tools.length).toBeLessThan(950);
    });
    it("omits kencode research guidance when the kencode tools are not connected", async () => {
        const cwd = await makeProject();
        const prompt = await buildSystemPrompt(cwd, undefined, false, undefined, [
            "read",
            "web_search",
            "web_fetch",
            "source_path",
        ]);
        // No kencode tool in the set → don't tell the model to call ReferenceSources/
        // DiscoverRepos/SearchCode (they aren't in its toolMap → "Unknown tool").
        expect(prompt).not.toContain("ReferenceSources");
        expect(prompt).not.toContain("DiscoverRepos");
        expect(prompt).not.toContain("SearchCode literal text/RE2");
        // The research section still exists, with a docs-first fallback.
        expect(prompt).toContain("Do not rely on memory for APIs");
        expect(prompt).toContain("For public code patterns, prefer official/live docs");
    });
    it("drops the kencode probe from the goal planner section when the tools are absent", async () => {
        const cwd = await makeProject();
        const withKencode = await buildSystemPrompt(cwd, undefined, false, undefined, ["read", "mcp__kencode-search__searchCode"], undefined, undefined, "planner");
        const withoutKencode = await buildSystemPrompt(cwd, undefined, false, undefined, ["read"], undefined, undefined, "planner");
        expect(withKencode).toContain("kencode reference/discover/searchCode");
        expect(withoutKencode).not.toContain("kencode reference/discover/searchCode");
    });
    it("measures representative system prompt sizes", async () => {
        const normalCwd = await makeProject();
        const normalToolNames = [
            "read",
            "grep",
            "find",
            "ls",
            "web_search",
            "web_fetch",
            "source_path",
            "mcp__kencode-search__referenceSources",
            "mcp__kencode-search__discoverRepos",
            "mcp__kencode-search__searchCode",
        ];
        const normalPrompt = await buildSystemPrompt(normalCwd, undefined, false, undefined, normalToolNames);
        const planModePrompt = await buildSystemPrompt(normalCwd, undefined, true, undefined, normalToolNames);
        const typescriptCwd = await makeProject({
            "AGENTS.md": "Prefer strict TypeScript. Run the focused test before reporting completion.",
            "package.json": JSON.stringify({
                scripts: {
                    test: "vitest",
                    typecheck: "tsc --noEmit",
                },
                devDependencies: {
                    typescript: "^5.0.0",
                    vitest: "^3.0.0",
                },
            }),
            "tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }),
        });
        const typescriptPrompt = await buildSystemPrompt(typescriptCwd, [
            {
                name: "find-skills",
                description: "Find and install agent skills from the open ecosystem.",
                content: "Use this when the user asks whether a skill exists for a task.",
                source: "test-fixture",
            },
        ], false, undefined, [
            "read",
            "edit",
            "bash",
            "grep",
            "find",
            "ls",
            "web_search",
            "web_fetch",
            "source_path",
            "skill",
            "mcp__kencode-search__referenceSources",
            "mcp__kencode-search__discoverRepos",
            "mcp__kencode-search__searchCode",
        ], new Set(["typescript"]));
        const measurements = {
            normal: promptSize(normalPrompt),
            planMode: promptSize(planModePrompt),
            typescriptProjectContextToolsSkills: promptSize(typescriptPrompt),
        };
        console.info(`system prompt size measurements: ${JSON.stringify(measurements)}`);
        expect(measurements.normal.characters).toBeLessThan(5_100);
        // Ceiling is ~300 chars above upstream's 5_900: this fork's branding strings
        // ("~/.ezcoder/", "EZ Coder") are longer than upstream's, which inflates the
        // plan-mode block that embeds them.
        expect(measurements.planMode.characters).toBeLessThan(6_200);
        expect(measurements.typescriptProjectContextToolsSkills.characters).toBeLessThan(9_800);
        expect(measurements.planMode.characters).toBeGreaterThan(measurements.normal.characters);
        expect(measurements.typescriptProjectContextToolsSkills.characters).toBeGreaterThan(measurements.normal.characters);
    });
    it("audits representative prompts for obsolete, duplicate, or contradictory guidance", async () => {
        const cwd = await makeProject({
            "AGENTS.md": "Prefer project-specific rules.",
            "package.json": JSON.stringify({ scripts: { test: "vitest" } }),
            "tsconfig.json": "{}",
        });
        const prompt = await buildSystemPrompt(cwd, [{ name: "find-skills", description: "Find skills.", content: "", source: "test" }], false, undefined, [
            "read",
            "edit",
            "write",
            "bash",
            "web_search",
            "web_fetch",
            "source_path",
            "skill",
            "mcp__kencode-search__referenceSources",
            "mcp__kencode-search__discoverRepos",
            "mcp__kencode-search__searchCode",
        ], new Set(["typescript"]));
        const audit = promptAudit(prompt);
        console.info(`system prompt audit: ${JSON.stringify(audit)}`);
        expect(audit.flags).toEqual([]);
        expect(audit.size.characters).toBeLessThan(9_500);
        expect(audit.size.sections).toBeGreaterThanOrEqual(8);
    });
    it("uses the Claude Code identity for Anthropic and EZ Coder for other providers", async () => {
        const cwd = await makeProject();
        const anthropic = await buildSystemPrompt(cwd, undefined, false, undefined, ["read"], undefined, "anthropic");
        const openai = await buildSystemPrompt(cwd, undefined, false, undefined, ["read"], undefined, "openai");
        expect(anthropic.startsWith("You are Claude Code")).toBe(true);
        expect(anthropic).not.toContain("EZ Coder by Nolan Grout");
        expect(openai.startsWith("You are EZ Coder by Nolan Grout")).toBe(true);
        expect(openai).not.toContain("You are Claude Code");
    });
});
//# sourceMappingURL=system-prompt.test.js.map