Files
oh-my-pi/packages/coding-agent/test/system-prompt-inventory.test.ts
T
roboomp b112a7d381 fix(prompting): instructed agents to load matching skills
- Added default system-prompt guidance to scan skill descriptions and read applicable skill:// content before work.
- Covered the rendered prompt contract with a frontend-design skill regression test.

Fixes #2829
2026-06-17 04:28:03 +00:00

126 lines
4.2 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { buildSystemPrompt, type SystemPromptToolMetadata } from "@oh-my-pi/pi-coding-agent/system-prompt";
import { cleanupTempHome } from "./helpers/temp-home-cleanup";
const EMPTY_TREE = {
rootPath: "",
rendered: "",
truncated: false,
totalLines: 0,
agentsMdFiles: [],
};
const TOOLS = new Map<string, SystemPromptToolMetadata>([
[
"read",
{
label: "Read",
description: "Reads files from disk.",
parameters: { type: "object", properties: { path: { type: "string" } } },
},
],
[
"bash",
{
label: "Bash",
description: "Executes a shell command.",
parameters: { type: "object", properties: { command: { type: "string" } } },
},
],
]);
describe("system prompt tool inventory", () => {
let tempDir = "";
let tempHomeDir = "";
let originalHome: string | undefined;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-prompt-inv-"));
tempHomeDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-prompt-inv-home-"));
originalHome = process.env.HOME;
process.env.HOME = tempHomeDir;
});
afterEach(cleanupTempHome(() => ({ tempDir, tempHomeDir, originalHome })));
async function render(opts: { nativeTools: boolean; repeatToolDescriptions: boolean }): Promise<string> {
const { systemPrompt } = await buildSystemPrompt({
cwd: tempDir,
contextFiles: [],
skills: [],
rules: [],
toolNames: ["read", "bash"],
tools: TOOLS,
workspaceTree: { ...EMPTY_TREE, rootPath: tempDir },
...opts,
});
return systemPrompt.join("\n\n");
}
it("renders a compact name list only when native tools are active and descriptions are not repeated", async () => {
const text = await render({ nativeTools: true, repeatToolDescriptions: false });
expect(text).toContain("- Read: `read`");
expect(text).toContain("- Bash: `bash`");
// No full per-tool sections in list mode.
expect(text).not.toContain("# Tool: read");
expect(text).not.toContain("Reads files from disk.");
});
it("renders `# Tool:` sections (not a name list) when tools are not native", async () => {
const text = await render({ nativeTools: false, repeatToolDescriptions: false });
expect(text).toContain("# Tool: read");
expect(text).toContain("# Tool: bash");
expect(text).toContain("Reads files from disk.");
expect(text).not.toContain("- Read: `read`");
// The legacy `<tool>` wrapper is gone.
expect(text).not.toContain("<tool name=");
});
it("renders `# Tool:` sections when descriptions are repeated even with native tools", async () => {
const text = await render({ nativeTools: true, repeatToolDescriptions: true });
expect(text).toContain("# Tool: read");
expect(text).toContain("Executes a shell command.");
expect(text).not.toContain("- Read: `read`");
});
it("tells the agent to read matching skills before work", async () => {
const { systemPrompt } = await buildSystemPrompt({
cwd: tempDir,
contextFiles: [],
skills: [
{
name: "frontend-design",
description: "Frontend UI workflow",
filePath: path.join(tempDir, "SKILL.md"),
baseDir: tempDir,
source: "test",
},
],
rules: [],
toolNames: ["read"],
tools: TOOLS,
workspaceTree: { ...EMPTY_TREE, rootPath: tempDir },
});
const text = systemPrompt.join("\n\n");
expect(text).toContain("Skills are specialized knowledge. Scan descriptions for your task domain.");
expect(text).toContain("If a skill applies, you MUST read `skill://<name>` before proceeding.");
expect(text).toContain("- frontend-design: Frontend UI workflow");
});
it("places the inventory at the bottom of the TOOLS section (after I/O and Exploration)", async () => {
const text = await render({ nativeTools: true, repeatToolDescriptions: false });
const inventoryIdx = text.indexOf("# Inventory");
const ioIdx = text.indexOf("# I/O");
const explorationIdx = text.indexOf("# Exploration");
expect(inventoryIdx).toBeGreaterThan(-1);
expect(ioIdx).toBeGreaterThan(-1);
expect(explorationIdx).toBeGreaterThan(-1);
expect(inventoryIdx).toBeGreaterThan(ioIdx);
expect(inventoryIdx).toBeGreaterThan(explorationIdx);
});
});