Files
my-pi/tests/system-prompt-cover.test.ts
T
云服务部-叶林立andDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 2aa1be0b54 feat: 组合包配置以包内 config/ 为唯一来源,并新增受保护写入人工审批
配置统一:权限策略、硬防御、Tool Search、提示词封面以及新迁入的
RTK、pi-compaction、pi-worktree、pi-hermes-memory 配置全部由扩展
直接读取已安装组合包内的 config/,pi install / pi update 即完成部署;
删除 rebuild-bundle-config.mjs 与 bundle-manifest.json,my-pi/ 只保留
日志与缓存。update.sh 成功升级后将默认 git 来源的已安装副本
reset 回发布版本。AGENTS.md 新增「组合包配置原则」。

脚本整合:search_config.sh / ssh_config.sh 合并为 configure.sh
search|ssh;install.sh / update.sh 共享函数抽到 scripts/lib.sh,远程
安装器改为先下载到临时文件再执行,下载失败可被检测。

权限:hard-guards 新增 protectedWrites,写入 Pi 凭据/设置、已安装
组合包与扩展目录、pi-ssh 凭据库时一律人工确认(跳过 AutoReview、
yolo 与 Session grant,无 UI 时拒绝);放行 ls、LSP 查询、ctx 只读、
web_search、Chrome 只读与 git 只读命令,skill_manage 改为 ask。

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-20 01:05:31 -07:00

161 lines
5.4 KiB
TypeScript

import assert from "node:assert/strict";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
import {
readCoverFile,
resolveCoverPath,
spliceCover,
} from "../extensions/system-prompt-cover-config.ts";
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const bundledCoverPath = join(repositoryRoot, "config", "system-prompt-cover.md");
const DEFAULT_PREAMBLE =
"You are an expert coding assistant operating inside pi, a coding agent harness. " +
"You help users by reading files, executing commands, editing code, and writing new files.";
/** A rendered prompt shaped like Pi's: preamble, then tag-wrapped top-level sections. */
function renderedPrompt(preamble = DEFAULT_PREAMBLE) {
return [
preamble,
"<tools>",
"- read: reads files",
"</tools>",
"<rules>",
"- Be concise",
"</rules>",
"<skills>",
"<available_skills>",
" <skill>",
" <name>s</name>",
" </skill>",
"</available_skills>",
"</skills>",
"<project_context>",
'<project_instructions path="/work/AGENTS.md">',
"Nested content with a </project_context>-looking line is not a tag.",
"</project_instructions>",
"</project_context>",
"<cwd>",
"/work",
"</cwd>",
].join("\n");
}
function tagsOf(text: string): string[] {
return [...text.matchAll(/^<([a-z][a-z0-9_-]*)>$/gm)].map((match) => match[1]);
}
async function makeTempDir() {
return mkdtemp(join(tmpdir(), "my-pi-cover-"));
}
test("bundled cover holds Pi's default preamble text", async () => {
const content = await readFile(bundledCoverPath, "utf8");
assert.equal(content.replace(/\s+$/, ""), DEFAULT_PREAMBLE);
});
test("spliceCover replaces only the preamble and preserves the remaining bytes", () => {
const rendered = renderedPrompt();
const spliced = spliceCover(rendered, "MY CUSTOM COVER");
assert.ok(spliced.startsWith("MY CUSTOM COVER\n"));
// Everything after the preamble's terminating newline is untouched.
const originalTail = rendered.slice(rendered.indexOf("\n<tools>"));
assert.ok(spliced.endsWith(originalTail));
// Section order — including nested tags — is identical to the unmodified prompt.
assert.deepEqual(tagsOf(spliced), tagsOf(rendered));
assert.deepEqual(tagsOf(spliced), [
"tools",
"rules",
"skills",
"available_skills",
"project_context",
"cwd",
]);
});
test("spliceCover keeps section order rather than appending recovered sections", () => {
const rendered = renderedPrompt("ORIGINAL COVER");
const spliced = spliceCover(rendered, "NEW COVER");
const order = tagsOf(spliced).join(",");
assert.equal(order, "tools,rules,skills,available_skills,project_context,cwd");
assert.ok(
order.indexOf("tools") < order.indexOf("cwd"),
"tools must still precede cwd; a re-appended section list would invert this",
);
assert.ok(
order.indexOf("project_context") < order.indexOf("cwd"),
"project_context must keep its original position",
);
});
test("spliceCover tolerates a trailing newline from the editor and a blank cover", () => {
const rendered = renderedPrompt();
assert.equal(spliceCover(rendered, "COVER\n\n"), spliceCover(rendered, "COVER"));
// A whitespace-only cover leaves an empty preamble but still keeps every section.
const blank = spliceCover(rendered, " \n");
assert.equal(blank, rendered.slice(rendered.indexOf("\n<tools>")));
assert.deepEqual(tagsOf(blank), tagsOf(rendered));
});
test("spliceCover degenerates to the cover when the prompt has no sections", () => {
assert.equal(spliceCover("just a preamble", "NEW"), "NEW");
assert.equal(spliceCover("", "NEW"), "NEW");
});
test("spliceCover keeps an attribute-bearing nested tag inside its parent section", () => {
// Pi nests <project_instructions path="..."> inside the bare <project_context> tag, so the
// boundary must be the first *bare* tag line and the nested markup must survive verbatim.
const rendered = [
"ORIGINAL COVER",
"<tools>",
"- read: reads files",
"</tools>",
"<project_context>",
'<project_instructions path="/work/AGENTS.md">',
"nested instructions",
"</project_instructions>",
"</project_context>",
"<cwd>",
"/work",
"</cwd>",
].join("\n");
const spliced = spliceCover(rendered, "NEW");
assert.equal(tagsOf(spliced).join(","), "tools,project_context,cwd");
assert.ok(spliced.startsWith("NEW\n<tools>"));
assert.ok(
spliced.includes('<project_instructions path="/work/AGENTS.md">\nnested instructions\n</project_instructions>'),
"nested project instructions must be preserved byte-for-byte",
);
});
test("resolveCoverPath points at the bundle config file", () => {
assert.equal(resolveCoverPath(), bundledCoverPath);
});
test("readCoverFile returns undefined when the file is absent or blank", async () => {
const dir = await makeTempDir();
try {
const coverPath = join(dir, "system-prompt-cover.md");
assert.equal(readCoverFile(coverPath), undefined);
await writeFile(coverPath, " \n\n", "utf8");
assert.equal(readCoverFile(coverPath), undefined);
await writeFile(coverPath, " REAL COVER \n", "utf8");
assert.equal(readCoverFile(coverPath), " REAL COVER");
} finally {
await rm(dir, { recursive: true, force: true });
}
});