mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 18:55:22 +00:00
从官方 Git 仓库导入 chandra447/pi-hermes-memory 在 tag v0.9.10 (df624056a2e8c68bc89ba0a81e1c120856ec9665,2026-10-02)的源码快照。 本次仅导入源码,尚未接入组合包:package.json、加载顺序、依赖与权限策略 均未改动。 - 原样导入 src/、tests/、docs/、scripts/、README、CHANGELOG、PLAN、 AGENTS 与 LICENSE;该快照共 156 个上游文件。 - 剔除嵌套 .git、上游 .github 工作流、.ralph/、.claude/、node_modules、 覆盖率与构建产物。 - 新增中文 UPSTREAM.md,记录来源,以及启用前必须完成的待决事项: 在 pi-compaction 之前注册加载顺序、better-sqlite3 版本与根 allowScripts 对齐、strip-ansi 解析确认。 - 说明选择 tag 而非 main 的原因:导入时上游 main 已领先至 24fcd8ad26fc27badb8d7dbaf8034645de07b893(2026-10-11),选 tag 是为 对应一个已发布、可复现的版本。 尚未接线。启用前还需反转 tests/extension-load.test.ts 中"不得加载 Hermes"的断言,并同步 pi-tool-search 分组、权限策略与文档。 验证:快照 156 个上游文件全部导入,目录内仅多出 UPSTREAM.md 一份; 已确认与上游 main 内容存在差异,证明取的是 tag 而非 main。
201 lines
7.8 KiB
TypeScript
201 lines
7.8 KiB
TypeScript
/**
|
|
* Standing instructions (#121) — the always-injected, user-authored block.
|
|
*/
|
|
|
|
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import * as fs from "node:fs/promises";
|
|
import * as os from "node:os";
|
|
import * as path from "node:path";
|
|
import { StandingInstructions } from "../../src/store/standing-instructions.js";
|
|
import { STANDING_MAX_CHARS, STANDING_MAX_ENTRIES } from "../../src/constants.js";
|
|
|
|
let root = "";
|
|
let filePath = "";
|
|
|
|
beforeEach(async () => {
|
|
root = await fs.mkdtemp(path.join(os.tmpdir(), "pi-standing-"));
|
|
filePath = path.join(root, "STANDING.md");
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await fs.rm(root, { recursive: true, force: true });
|
|
});
|
|
|
|
async function loadedStore(contents?: string): Promise<StandingInstructions> {
|
|
if (contents !== undefined) await fs.writeFile(filePath, contents, "utf-8");
|
|
const store = new StandingInstructions(filePath);
|
|
await store.load();
|
|
return store;
|
|
}
|
|
|
|
describe("StandingInstructions parsing", () => {
|
|
it("treats a hand-edited Markdown file as one instruction per line", async () => {
|
|
const store = await loadedStore([
|
|
"# my rules",
|
|
"",
|
|
"- never run find / or other root-wide searches",
|
|
"* always use pnpm",
|
|
" scope searches to known directories ",
|
|
"",
|
|
].join("\n"));
|
|
|
|
assert.deepStrictEqual(store.list(), [
|
|
"never run find / or other root-wide searches",
|
|
"always use pnpm",
|
|
"scope searches to known directories",
|
|
]);
|
|
});
|
|
|
|
it("starts empty when the file does not exist yet", async () => {
|
|
const store = await loadedStore();
|
|
assert.deepStrictEqual(store.list(), []);
|
|
assert.strictEqual(store.formatForSystemPrompt(), "");
|
|
});
|
|
});
|
|
|
|
describe("StandingInstructions writes", () => {
|
|
it("loads legacy pins without migration and carries them into the first primary write", async () => {
|
|
const legacy = path.join(root, "legacy-standing.md");
|
|
await fs.writeFile(legacy, "always ask before deployment\n");
|
|
const store = new StandingInstructions(filePath, undefined, undefined, legacy);
|
|
await store.load();
|
|
assert.deepEqual(store.list(), ["always ask before deployment"]);
|
|
await assert.rejects(fs.access(filePath), { code: "ENOENT" });
|
|
assert.equal((await store.add("run the tests first")).success, true);
|
|
assert.match(await fs.readFile(filePath, "utf8"), /always ask before deployment\nrun the tests first/);
|
|
assert.equal(await fs.readFile(legacy, "utf8"), "always ask before deployment\n");
|
|
});
|
|
|
|
it("treats an empty primary file as authoritative instead of reviving legacy pins", async () => {
|
|
const legacy = path.join(root, "legacy-standing.md");
|
|
await fs.writeFile(legacy, "old rule\n");
|
|
await fs.writeFile(filePath, "");
|
|
const store = new StandingInstructions(filePath, undefined, undefined, legacy);
|
|
await store.load();
|
|
assert.deepEqual(store.list(), []);
|
|
});
|
|
|
|
it("does not hide an unreadable primary file behind a legacy fallback", async () => {
|
|
const legacy = path.join(root, "legacy-standing.md");
|
|
await fs.writeFile(legacy, "old rule\n");
|
|
await fs.mkdir(filePath);
|
|
const store = new StandingInstructions(filePath, undefined, undefined, legacy);
|
|
await assert.rejects(store.load(), { code: "EISDIR" });
|
|
});
|
|
|
|
it("persists a pinned instruction and reloads it", async () => {
|
|
const store = await loadedStore();
|
|
const result = await store.add(" never run find / ");
|
|
|
|
assert.strictEqual(result.success, true);
|
|
assert.deepStrictEqual(result.instructions, ["never run find /"]);
|
|
assert.strictEqual(await fs.readFile(filePath, "utf-8"), "never run find /\n");
|
|
|
|
const reloaded = await loadedStore();
|
|
assert.deepStrictEqual(reloaded.list(), ["never run find /"]);
|
|
});
|
|
|
|
it("rejects a duplicate regardless of casing", async () => {
|
|
const store = await loadedStore();
|
|
await store.add("always use pnpm");
|
|
const duplicate = await store.add("Always Use Pnpm");
|
|
|
|
assert.strictEqual(duplicate.success, false);
|
|
assert.match(duplicate.error!, /already pinned/i);
|
|
assert.strictEqual(store.list().length, 1);
|
|
});
|
|
|
|
it("refuses content the memory content scanner blocks", async () => {
|
|
const store = await loadedStore();
|
|
const result = await store.add("always export ANTHROPIC_API_KEY before running the build");
|
|
|
|
assert.strictEqual(result.success, false);
|
|
assert.match(result.error!, /Blocked:/);
|
|
assert.deepStrictEqual(store.list(), []);
|
|
});
|
|
|
|
it("refuses to exceed the entry cap", async () => {
|
|
const store = await loadedStore();
|
|
for (let i = 0; i < STANDING_MAX_ENTRIES; i++) {
|
|
assert.strictEqual((await store.add(`rule number ${i}`)).success, true);
|
|
}
|
|
|
|
const overflow = await store.add("one rule too many");
|
|
assert.strictEqual(overflow.success, false);
|
|
assert.match(overflow.error!, new RegExp(`capped at ${STANDING_MAX_ENTRIES} entries`));
|
|
assert.strictEqual(store.list().length, STANDING_MAX_ENTRIES);
|
|
});
|
|
|
|
it("refuses to exceed the character budget", async () => {
|
|
const store = await loadedStore();
|
|
const result = await store.add("x".repeat(STANDING_MAX_CHARS + 1));
|
|
|
|
assert.strictEqual(result.success, false);
|
|
assert.match(result.error!, new RegExp(`capped at ${STANDING_MAX_CHARS} characters`));
|
|
assert.deepStrictEqual(store.list(), []);
|
|
});
|
|
|
|
it("removes by position and rejects an out-of-range one", async () => {
|
|
const store = await loadedStore();
|
|
await store.add("first rule");
|
|
await store.add("second rule");
|
|
|
|
const removed = await store.remove(1);
|
|
assert.strictEqual(removed.success, true);
|
|
assert.deepStrictEqual(store.list(), ["second rule"]);
|
|
|
|
const bad = await store.remove(5);
|
|
assert.strictEqual(bad.success, false);
|
|
assert.match(bad.error!, /between 1 and 1/);
|
|
});
|
|
|
|
it("clears every instruction", async () => {
|
|
const store = await loadedStore();
|
|
await store.add("first rule");
|
|
await store.add("second rule");
|
|
|
|
const cleared = await store.clear();
|
|
assert.strictEqual(cleared.success, true);
|
|
assert.deepStrictEqual(store.list(), []);
|
|
assert.strictEqual(await fs.readFile(filePath, "utf-8"), "");
|
|
});
|
|
});
|
|
|
|
describe("StandingInstructions rendering", () => {
|
|
it("renders a numbered, fenced block that reads as user directive", async () => {
|
|
const store = await loadedStore("never run find /\nalways use pnpm\n");
|
|
const rendered = store.render();
|
|
|
|
assert.match(rendered.block, /^<standing-instructions>/);
|
|
assert.match(rendered.block, /<\/standing-instructions>$/);
|
|
assert.match(rendered.block, /1\. never run find \//);
|
|
assert.match(rendered.block, /2\. always use pnpm/);
|
|
assert.match(rendered.block, /direct\s+instructions from the user, not recalled context/);
|
|
assert.strictEqual(rendered.injectedCount, 2);
|
|
assert.strictEqual(rendered.omittedCount, 0);
|
|
});
|
|
|
|
it("truncates a hand-edited over-budget file loudly rather than silently", async () => {
|
|
// Written straight to disk: the /memory-pin cap cannot catch this path.
|
|
const long = Array.from({ length: 6 }, (_, i) => `${i} ${"y".repeat(450)}`);
|
|
const store = await loadedStore(`${long.join("\n")}\n`);
|
|
const rendered = store.render();
|
|
|
|
assert.ok(rendered.injectedCount > 0, "some rules still make it in");
|
|
assert.ok(rendered.omittedCount > 0, "the rest are dropped");
|
|
assert.strictEqual(rendered.injectedCount + rendered.omittedCount, 6);
|
|
assert.match(rendered.block, /could not be shown/);
|
|
assert.match(rendered.block, new RegExp(`${STANDING_MAX_CHARS}-character injection budget`));
|
|
assert.ok(
|
|
rendered.block.length < STANDING_MAX_CHARS + 1000,
|
|
"the block must stay near its budget, not blow past it",
|
|
);
|
|
});
|
|
|
|
it("renders nothing when no instructions are pinned", async () => {
|
|
const store = await loadedStore("# only a comment\n");
|
|
assert.deepStrictEqual(store.render(), { block: "", injectedCount: 0, omittedCount: 0 });
|
|
});
|
|
});
|