feat(pi-hermes-memory): 导入上游 v0.9.10 源码快照

从官方 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。
This commit is contained in:
云服务部-叶林立
2026-10-13 23:19:35 -07:00
parent 879fc5d37f
commit 312bb2ba5a
157 changed files with 52287 additions and 0 deletions
@@ -0,0 +1,183 @@
import { afterEach, describe, it } from 'node:test';
import * as assert from 'node:assert/strict';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { DatabaseManager } from '../../src/store/db.js';
import { addMemory, getMemoryUsageSignals } from '../../src/store/sqlite-memory-store.js';
import { normalizeMemoryLookupText } from '../../src/store/memory-lookup.js';
import { registerMemorySearchTool } from '../../src/tools/memory-search-tool.js';
let ROOT_DIR = '';
afterEach(() => {
if (ROOT_DIR) fs.rmSync(ROOT_DIR, { recursive: true, force: true });
ROOT_DIR = '';
});
function makeDbManager(): DatabaseManager {
ROOT_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'pi-memory-search-tool-test-'));
return new DatabaseManager(ROOT_DIR);
}
describe('registerMemorySearchTool', () => {
it('returns a broader natural-language match when strict term matching misses', async () => {
const dbManager = makeDbManager();
addMemory(dbManager, "user's name is Naruto", 'user');
let captured: any;
const mockPi = {
registerTool: (def: any) => {
captured = def;
},
} as any;
registerMemorySearchTool(mockPi, dbManager);
const result = await captured.execute('tc-1', { query: 'name identity Naruto', target: 'user' });
assert.strictEqual(result.details.success, true);
assert.strictEqual(result.details.count, 1);
assert.match(result.content[0].text, /Naruto/);
dbManager.close();
});
it('labels every result with its mutation target and unambiguous scope', async () => {
const dbManager = makeDbManager();
addMemory(dbManager, 'global deployment convention');
addMemory(dbManager, 'user deployment preference', 'user');
addMemory(dbManager, 'failure deployment lesson', 'failure');
addMemory(dbManager, 'project deployment convention', 'memory', 'project-a');
addMemory(dbManager, 'project failure deployment lesson', 'failure', 'project-a');
let captured: any;
registerMemorySearchTool({ registerTool: (def: any) => { captured = def; } } as any, dbManager);
const result = await captured.execute('tc-1', { query: 'deployment' });
const text = result.content[0].text;
assert.match(text, /scope=global \[target=memory\] global deployment convention/);
assert.match(text, /scope=global \[target=user\] user deployment preference/);
assert.match(text, /scope=global \[target=failure\] failure deployment lesson/);
assert.match(text, /scope=project:project-a \[target=project\] project deployment convention/);
assert.match(text, /scope=project:project-a \[target=failure\] project failure deployment lesson/);
dbManager.close();
});
it('accepts target "project" as a filter and shows the schema value', async () => {
const dbManager = makeDbManager();
addMemory(dbManager, 'project deployment convention', 'memory', 'project-a');
addMemory(dbManager, 'global deployment convention');
addMemory(dbManager, 'project failure deployment lesson', 'failure', 'project-a');
let captured: any;
registerMemorySearchTool({ registerTool: (def: any) => { captured = def; } } as any, dbManager);
assert.ok(captured.parameters.properties.target.enum.includes('project'));
const result = await captured.execute('tc-1', { query: 'deployment', target: 'project' });
const text = result.content[0].text;
assert.strictEqual(result.details.success, true);
assert.strictEqual(result.details.count, 1);
assert.match(text, /scope=project:project-a \[target=project\]/);
assert.doesNotMatch(text, /scope=global/);
dbManager.close();
});
it('keeps copied results reversible when a project name contains brackets', async () => {
const dbManager = makeDbManager();
addMemory(dbManager, 'literal project entry', 'memory', 'foo] bar');
let captured: any;
registerMemorySearchTool({ registerTool: (def: any) => { captured = def; } } as any, dbManager);
const result = await captured.execute('tc-1', { query: 'literal' });
const firstResultLine = result.content[0].text.split('\n').find((line: string) => line.startsWith('🧠'))!;
assert.match(firstResultLine, /scope=project:foo%5D%20bar \[target=project\]/);
assert.equal(normalizeMemoryLookupText(firstResultLine), 'literal project entry');
dbManager.close();
});
it('records a usage hit for every returned entry', async () => {
const dbManager = makeDbManager();
addMemory(dbManager, 'recallable volcano note', 'memory');
addMemory(dbManager, 'recallable tide note', 'memory');
let captured: any;
registerMemorySearchTool({ registerTool: (def: any) => { captured = def; } } as any, dbManager);
await captured.execute('tc-1', { query: 'recallable' });
await captured.execute('tc-2', { query: 'volcano' });
const signals = getMemoryUsageSignals(dbManager, { target: 'memory', project: null });
assert.strictEqual(signals.get('recallable volcano note')?.hits, 2);
assert.strictEqual(signals.get('recallable tide note')?.hits, 1);
dbManager.close();
});
it('records nothing when the search returns no results', async () => {
const dbManager = makeDbManager();
addMemory(dbManager, 'unmatched entry', 'memory');
let captured: any;
registerMemorySearchTool({ registerTool: (def: any) => { captured = def; } } as any, dbManager);
const result = await captured.execute('tc-1', { query: 'zzz-no-match-q' });
assert.strictEqual(result.details.count, 0);
assert.strictEqual(getMemoryUsageSignals(dbManager, { target: 'memory', project: null }).size, 0);
dbManager.close();
});
it('skips recording when usage tracking is disabled', async () => {
const dbManager = makeDbManager();
addMemory(dbManager, 'tracked-off entry', 'memory');
let captured: any;
registerMemorySearchTool(
{ registerTool: (def: any) => { captured = def; } } as any,
dbManager,
{ usageTrackingEnabled: false },
);
const result = await captured.execute('tc-1', { query: 'tracked-off' });
assert.strictEqual(result.details.count, 1);
assert.strictEqual(getMemoryUsageSignals(dbManager, { target: 'memory', project: null }).size, 0);
dbManager.close();
});
it('never breaks the search when hit recording fails (fail-open)', async () => {
const dbManager = makeDbManager();
addMemory(dbManager, 'resilient entry', 'memory');
let getDbCalls = 0;
const realGetDb = dbManager.getDb.bind(dbManager);
const flaky = Object.create(dbManager) as DatabaseManager;
flaky.getDb = () => {
getDbCalls++;
if (getDbCalls > 2) throw new Error('injected recording failure');
return realGetDb();
};
let captured: any;
registerMemorySearchTool({ registerTool: (def: any) => { captured = def; } } as any, flaky);
// getDb calls: getMemoryStats(1) + searchMemories(1) + recordSearchHits(1, throws).
const result = await captured.execute('tc-1', { query: 'resilient' });
assert.strictEqual(result.details.success, true);
assert.strictEqual(result.details.count, 1);
assert.match(result.content[0].text, /resilient entry/);
// The hit did NOT land (recording failed) but the search succeeded.
assert.strictEqual(getMemoryUsageSignals(dbManager, { target: 'memory', project: null }).size, 0);
dbManager.close();
});
});
@@ -0,0 +1,745 @@
/**
* Unit tests for memory tool registration and execute function.
*/
import { describe, it, beforeEach, afterEach } from "node:test";
import * as assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { registerMemoryTool } from "../../src/tools/memory-tool.js";
import { MemoryStore } from "../../src/store/memory-store.js";
import { DatabaseManager } from "../../src/store/db.js";
import { getMemories, searchMemories, syncMemoryEntry } from "../../src/store/sqlite-memory-store.js";
import { ENTRY_DELIMITER, MEMORY_FILE } from "../../src/constants.js";
import { Value } from "typebox/value";
describe("registerMemoryTool", () => {
let tmpDir: string;
let dbManager: DatabaseManager;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "memory-tool-test-"));
dbManager = new DatabaseManager(tmpDir);
});
afterEach(() => {
dbManager.close();
fs.rmSync(tmpDir, { recursive: true, force: true });
});
it("registers action-specific memory tools with required parameters", () => {
const registeredTools: any[] = [];
const mockPi = {
registerTool: (def: any) => {
registeredTools.push(def);
},
} as unknown as ExtensionAPI;
registerMemoryTool(mockPi, {} as MemoryStore, null);
assert.deepStrictEqual(
registeredTools.map((tool) => tool.name),
["memory_add", "memory_replace", "memory_remove"],
);
for (const tool of registeredTools) {
assert.ok(tool.description.length > 0);
assert.ok(tool.promptSnippet.length > 0);
assert.ok(Array.isArray(tool.promptGuidelines));
assert.ok(tool.parameters);
}
});
it("execute add returns JSON with usage field", async () => {
let capturedResult: any;
const mockPi = {
registerTool: (def: any) => {
if (!capturedResult || def.name === "memory_add") capturedResult = def;
},
} as unknown as ExtensionAPI;
const mockStore = {
add: () => ({
success: true,
target: "memory",
entries: ["Entry one"],
usage: "5% — 110/5000 chars",
entry_count: 1,
message: "Entry added.",
}),
} as unknown as MemoryStore;
registerMemoryTool(mockPi, mockStore, null, dbManager);
const result = await capturedResult.execute("tc-1", { action: "add", target: "memory", content: "Entry one" }, undefined as any, undefined as any, undefined as any);
assert.strictEqual(result.content[0].type, "text", "content should be text type");
const parsed = JSON.parse(result.content[0].text);
assert.strictEqual(parsed.success, true, "result should be success");
assert.ok(parsed.usage.includes("chars"), "usage should contain 'chars'");
assert.ok(parsed.usage.includes("5000"), "usage should show total limit");
assert.strictEqual(parsed.entry_count, 1, "entry_count should be 1");
assert.strictEqual(result.details.success, true, "details should mirror result");
});
it("execute add with FIFO evictions returns normal text with full rotated entries", async () => {
let capturedResult: any;
const mockPi = {
registerTool: (def: any) => {
if (!capturedResult || def.name === "memory_add") capturedResult = def;
},
} as unknown as ExtensionAPI;
const evictedOne = "First rotated entry with full detail.";
const evictedTwo = "Second rotated entry with\nmultiple lines preserved.";
const mockStore = {
add: () => ({
success: true,
target: "memory",
entries: ["New entry"],
usage: "90% — 4500/5000 chars",
entry_count: 1,
message: "Memory updated. Rotated 2 older entries to stay within the limit.",
evicted_entries: [evictedOne, evictedTwo],
evicted_count: 2,
}),
} as unknown as MemoryStore;
registerMemoryTool(mockPi, mockStore, null);
const result = await capturedResult.execute("tc-1", { action: "add", target: "memory", content: "New entry" }, undefined as any, undefined as any, undefined as any);
const text = result.content[0].text;
assert.throws(() => JSON.parse(text));
assert.match(text, /Memory updated\. Rotated 2 older entries/);
assert.match(text, /Rotated active memory entries:/);
assert.ok(text.includes(`1. ${evictedOne}`));
assert.ok(text.includes(`2. ${evictedTwo}`));
assert.match(text, /If one of these entries should stay active, add it again\./);
assert.match(text, /Usage: 90%/);
assert.deepStrictEqual(result.details.evicted_entries, [evictedOne, evictedTwo]);
});
it("syncs successful adds into SQLite", async () => {
let capturedResult: any;
const mockPi = {
registerTool: (def: any) => {
if (!capturedResult || def.name === "memory_add") capturedResult = def;
},
} as unknown as ExtensionAPI;
const mockStore = {
add: () => ({
success: true,
target: "memory",
entries: ["Entry one"],
usage: "5% — 110/5000 chars",
entry_count: 1,
message: "Entry added.",
}),
} as unknown as MemoryStore;
registerMemoryTool(mockPi, mockStore, null, dbManager);
await capturedResult.execute("tc-1", { action: "add", target: "memory", content: "Entry one" }, undefined as any, undefined as any, undefined as any);
const results = getMemories(dbManager, { target: 'memory', project: null });
assert.strictEqual(results.length, 1);
assert.strictEqual(results[0].content, 'Entry one');
});
it("lands over-cap policy-only adds in searchable SQLite", async () => {
let capturedResult: any;
const mockPi = {
registerTool: (def: any) => {
if (!capturedResult || def.name === "memory_add") capturedResult = def;
},
} as unknown as ExtensionAPI;
const store = new MemoryStore({
memoryMode: "policy-only",
memoryCharLimit: 40,
userCharLimit: 5000,
projectCharLimit: 5000,
nudgeInterval: 10,
reviewEnabled: false,
flushOnCompact: false,
flushOnShutdown: false,
flushMinTurns: 6,
autoConsolidate: false,
correctionDetection: false,
failureInjectionEnabled: true,
failureInjectionMaxAgeDays: 7,
failureInjectionMaxEntries: 5,
nudgeToolCalls: 15,
consolidationTimeoutMs: 60000,
memoryDir: tmpDir,
});
await store.loadFromDisk();
registerMemoryTool(mockPi, store, null, dbManager);
const first = await capturedResult.execute("tc-1", { action: "add", target: "memory", content: "alpha entry" }, undefined as any, undefined as any, undefined as any);
assert.strictEqual(first.details.success, true);
const second = await capturedResult.execute("tc-2", { action: "add", target: "memory", content: "beta entry past the tiny cap" }, undefined as any, undefined as any, undefined as any);
assert.strictEqual(second.details.success, true);
const results = searchMemories(dbManager, "beta entry past the tiny cap", { target: "memory" });
assert.ok(results.some((r) => r.content.includes("beta entry past the tiny cap")));
});
it("reports the matching target when replace is sent to the wrong target", async () => {
let removeTool: any;
const mockPi = {
registerTool: (def: any) => {
if (def.name === "memory_remove") removeTool = def;
},
} as unknown as ExtensionAPI;
const store = new MemoryStore({
memoryMode: "policy-only",
memoryCharLimit: 5000,
userCharLimit: 5000,
projectCharLimit: 5000,
nudgeInterval: 10,
reviewEnabled: false,
flushOnCompact: false,
flushOnShutdown: false,
flushMinTurns: 6,
autoConsolidate: false,
correctionDetection: false,
failureInjectionEnabled: true,
failureInjectionMaxAgeDays: 7,
failureInjectionMaxEntries: 5,
nudgeToolCalls: 15,
consolidationTimeoutMs: 60000,
memoryDir: tmpDir,
});
await store.loadFromDisk();
await store.addFailure("use pnpm for lockfiles", { category: "correction" });
registerMemoryTool(mockPi, store, null);
const result = await removeTool.execute(
"tc-1",
{ target: "memory", old_text: "use pnpm for lockfiles" },
undefined,
undefined,
undefined,
);
assert.equal(result.details.success, false);
assert.match(result.details.error, /No match in target "memory"/);
assert.match(result.details.error, /target "failure"/);
assert.deepEqual(result.details.matching_targets, ["failure"]);
});
it("prunes same-scope SQLite orphans after a Markdown mutation", async () => {
let capturedResult: any;
const mockPi = {
registerTool: (definition: any) => {
if (!capturedResult || definition.name === "memory_add") capturedResult = definition;
},
} as unknown as ExtensionAPI;
const store = new MemoryStore({
memoryMode: "policy-only",
memoryCharLimit: 5000,
userCharLimit: 5000,
projectCharLimit: 5000,
nudgeInterval: 10,
reviewEnabled: false,
flushOnCompact: false,
flushOnShutdown: false,
flushMinTurns: 6,
autoConsolidate: false,
correctionDetection: false,
failureInjectionEnabled: true,
failureInjectionMaxAgeDays: 7,
failureInjectionMaxEntries: 5,
nudgeToolCalls: 15,
consolidationTimeoutMs: 60000,
memoryDir: tmpDir,
});
await store.loadFromDisk();
syncMemoryEntry(dbManager, { content: "orphaned row", target: "memory", project: null });
registerMemoryTool(mockPi, store, null, dbManager);
await capturedResult.execute(
"tc-1",
{ action: "add", target: "memory", content: "authoritative Markdown row" },
undefined,
undefined,
undefined,
);
assert.deepStrictEqual(
getMemories(dbManager, { target: "memory", project: null }).map((entry) => entry.content),
["authoritative Markdown row"],
);
});
it("reconciles SQLite from fresh authoritative Markdown state", async () => {
let capturedResult: any;
const mockPi = {
registerTool: (definition: any) => {
if (!capturedResult || definition.name === "memory_add") capturedResult = definition;
},
} as unknown as ExtensionAPI;
const store = new MemoryStore({
memoryMode: "policy-only",
memoryCharLimit: 5000,
userCharLimit: 5000,
projectCharLimit: 5000,
nudgeInterval: 10,
reviewEnabled: false,
flushOnCompact: false,
flushOnShutdown: false,
flushMinTurns: 6,
autoConsolidate: false,
correctionDetection: false,
failureInjectionEnabled: true,
failureInjectionMaxAgeDays: 7,
failureInjectionMaxEntries: 5,
nudgeToolCalls: 15,
consolidationTimeoutMs: 60000,
memoryDir: tmpDir,
});
await store.loadFromDisk();
const originalSave = (store as any).saveToDisk.bind(store);
(store as any).saveToDisk = async (target: "memory") => {
await originalSave(target);
const markdownPath = path.join(tmpDir, MEMORY_FILE);
const existing = fs.readFileSync(markdownPath, "utf-8");
const date = new Date().toISOString().split("T")[0];
fs.writeFileSync(markdownPath, `${existing}${ENTRY_DELIMITER}newer writer <!-- created=${date}, last=${date} -->`);
syncMemoryEntry(dbManager, { content: "newer writer", target: "memory", project: null });
};
registerMemoryTool(mockPi, store, null, dbManager);
await capturedResult.execute(
"tc-1",
{ action: "add", target: "memory", content: "first writer" },
undefined,
undefined,
undefined,
);
assert.deepStrictEqual(
getMemories(dbManager, { target: "memory", project: null }).map((entry) => entry.content).sort(),
["first writer", "newer writer"],
);
});
it("removes FIFO-evicted entries from the SQLite mirror", async () => {
let capturedResult: any;
const mockPi = {
registerTool: (def: any) => {
if (!capturedResult || def.name === "memory_add") capturedResult = def;
},
} as unknown as ExtensionAPI;
syncMemoryEntry(dbManager, {
content: "Older entry",
target: "memory",
project: null,
});
syncMemoryEntry(dbManager, {
content: "Older entry with extra detail",
target: "memory",
project: null,
});
const mockStore = {
add: () => ({
success: true,
target: "memory",
entries: ["New entry"],
usage: "90% — 4500/5000 chars",
entry_count: 1,
message: "Memory updated. Rotated 1 older entry to stay within the limit.",
evicted_entries: ["Older entry"],
evicted_count: 1,
}),
} as unknown as MemoryStore;
registerMemoryTool(mockPi, mockStore, null, dbManager);
const result = await capturedResult.execute("tc-1", { action: "add", target: "memory", content: "New entry" }, undefined as any, undefined as any, undefined as any);
assert.match(result.content[0].text, /Rotated active memory entries:/);
const rows = getMemories(dbManager, { target: "memory", project: null });
assert.deepStrictEqual(rows.map((row) => row.content).sort(), ["New entry", "Older entry with extra detail"].sort());
});
it("uses project scope when removing FIFO-evicted SQLite entries", async () => {
let capturedResult: any;
const mockPi = {
registerTool: (def: any) => {
if (!capturedResult || def.name === "memory_add") capturedResult = def;
},
} as unknown as ExtensionAPI;
syncMemoryEntry(dbManager, {
content: "Shared wording",
target: "memory",
project: null,
});
syncMemoryEntry(dbManager, {
content: "Shared wording",
target: "memory",
project: "project-a",
});
const mockProjectStore = {
add: () => ({
success: true,
target: "memory",
entries: ["Project replacement"],
usage: "90% — 4500/5000 chars",
entry_count: 1,
message: "Memory updated. Rotated 1 older entry to stay within the limit.",
evicted_entries: ["Shared wording"],
evicted_count: 1,
}),
} as unknown as MemoryStore;
registerMemoryTool(mockPi, {} as MemoryStore, mockProjectStore, dbManager, "project-a");
await capturedResult.execute("tc-1", { action: "add", target: "project", content: "Project replacement" }, undefined as any, undefined as any, undefined as any);
const globalRows = getMemories(dbManager, { target: "memory", project: null });
const projectRows = getMemories(dbManager, { target: "memory", project: "project-a" });
assert.deepStrictEqual(globalRows.map((row) => row.content), ["Shared wording"]);
assert.deepStrictEqual(projectRows.map((row) => row.content), ["Project replacement"]);
});
it("maps project target to SQLite project scope", async () => {
let capturedResult: any;
const mockPi = {
registerTool: (def: any) => {
if (!capturedResult || def.name === "memory_add") capturedResult = def;
},
} as unknown as ExtensionAPI;
const addTargets: string[] = [];
const mockProjectStore = {
add: (target: string) => {
addTargets.push(target);
return {
success: true,
target,
entries: ["Project entry"],
usage: "2% — 20/5000 chars",
entry_count: 1,
message: "Entry added.",
};
},
} as unknown as MemoryStore;
registerMemoryTool(mockPi, {} as MemoryStore, mockProjectStore, dbManager, 'project-a');
const result = await capturedResult.execute("tc-1", { action: "add", target: "project", content: "Project entry" }, undefined as any, undefined as any, undefined as any);
const parsed = JSON.parse(result.content[0].text);
assert.strictEqual(parsed.target, 'project');
assert.strictEqual(result.details.target, 'project');
assert.deepStrictEqual(addTargets, ['memory']);
const results = getMemories(dbManager, { project: 'project-a', target: 'memory' });
assert.strictEqual(results.length, 1);
assert.strictEqual(results[0].content, 'Project entry');
});
it("resolves the active project store and name for each mutation", async () => {
let capturedResult: any;
const mockPi = {
registerTool: (def: any) => {
if (!capturedResult || def.name === "memory_add") capturedResult = def;
},
} as unknown as ExtensionAPI;
let activeProjectName = "project-a";
const calls: string[] = [];
const stores = new Map<string, MemoryStore>();
const makeProjectStore = (name: string) => ({
add: (target: string, content: string) => {
calls.push(`${name}:${target}:${content}`);
return {
success: true,
target,
entries: [content],
usage: "2% — 20/5000 chars",
entry_count: 1,
message: "Entry added.",
};
},
}) as unknown as MemoryStore;
stores.set("project-a", makeProjectStore("project-a"));
stores.set("project-b", makeProjectStore("project-b"));
registerMemoryTool(
mockPi,
{} as MemoryStore,
() => stores.get(activeProjectName) ?? null,
dbManager,
() => activeProjectName,
);
await capturedResult.execute("tc-1", { target: "project", content: "first" }, undefined as any, undefined as any, undefined as any);
activeProjectName = "project-b";
await capturedResult.execute("tc-2", { target: "project", content: "second" }, undefined as any, undefined as any, undefined as any);
assert.deepStrictEqual(calls, ["project-a:memory:first", "project-b:memory:second"]);
const projectRows = getMemories(dbManager, { project: "project-b", target: "memory" });
assert.deepStrictEqual(projectRows.map((row) => row.content), ["second"]);
});
it("reconciles rebound project-store mutations in the project SQLite scope", async () => {
let activeProjectName = "project-a";
type MutationObserver = (
target: "memory" | "user" | "failure",
entries: string[],
) => Promise<string | null | undefined>;
const observers = new Map<string, MutationObserver>();
const stores = new Map<string, MemoryStore>();
const makeProjectStore = (name: string) => ({
setMutationObserver: (observer: MutationObserver) => {
observers.set(name, observer);
},
}) as unknown as MemoryStore;
stores.set("project-a", makeProjectStore("project-a"));
stores.set("project-b", makeProjectStore("project-b"));
const mockPi = {
registerTool: () => {},
} as unknown as ExtensionAPI;
const configureProjectStore = registerMemoryTool(
mockPi,
{} as MemoryStore,
() => stores.get(activeProjectName) ?? null,
dbManager,
() => activeProjectName,
);
await observers.get("project-a")?.("memory", ["first project entry"]);
activeProjectName = "project-b";
configureProjectStore(stores.get("project-b") ?? null);
await observers.get("project-b")?.("memory", ["second project entry"]);
assert.deepStrictEqual(
getMemories(dbManager, { target: "memory", project: "project-a" }).map((row) => row.content),
["first project entry"],
);
assert.deepStrictEqual(
getMemories(dbManager, { target: "memory", project: "project-b" }).map((row) => row.content),
["second project entry"],
);
assert.deepStrictEqual(getMemories(dbManager, { target: "memory", project: null }), []);
});
it("returns a warning instead of failing when SQLite sync errors", async () => {
let capturedResult: any;
const mockPi = {
registerTool: (def: any) => {
if (!capturedResult || def.name === "memory_add") capturedResult = def;
},
} as unknown as ExtensionAPI;
const mockStore = {
add: () => ({
success: true,
target: "memory",
entries: ["Entry one"],
usage: "5% — 110/5000 chars",
entry_count: 1,
message: "Entry added.",
}),
} as unknown as MemoryStore;
const failingDbManager = {
getDb: () => {
throw new Error('sqlite unavailable');
},
} as unknown as DatabaseManager;
registerMemoryTool(mockPi, mockStore, null, failingDbManager);
const result = await capturedResult.execute("tc-1", { action: "add", target: "memory", content: "Entry one" }, undefined as any, undefined as any, undefined as any);
const parsed = JSON.parse(result.content[0].text);
assert.strictEqual(parsed.success, true);
assert.match(parsed.message, /SQLite search sync failed/);
assert.match(parsed.warning, /sqlite unavailable/);
});
it("does not sync to SQLite when core Markdown add fails", async () => {
let capturedResult: any;
const mockPi = {
registerTool: (def: any) => {
if (!capturedResult || def.name === "memory_add") capturedResult = def;
},
} as unknown as ExtensionAPI;
const mockStore = {
add: () => ({
success: false,
error: "Memory at 5000/5000 chars. Adding this entry would exceed the limit.",
}),
} as unknown as MemoryStore;
registerMemoryTool(mockPi, mockStore, null, dbManager);
const result = await capturedResult.execute(
"tc-1",
{ action: "add", target: "memory", content: "overflow entry" },
undefined as any,
undefined as any,
undefined as any,
);
const parsed = JSON.parse(result.content[0].text);
assert.strictEqual(parsed.success, false);
const rows = getMemories(dbManager, { target: "memory", project: null });
assert.strictEqual(rows.length, 0, "SQLite should stay unchanged when core add fails");
});
it("registers action-specific schemas that reject incomplete mutations", () => {
const registeredTools: Record<string, any> = {};
const mockPi = {
registerTool: (definition: any) => {
registeredTools[definition.name] = definition;
},
} as unknown as ExtensionAPI;
registerMemoryTool(mockPi, {} as MemoryStore, null);
assert.deepStrictEqual(Object.keys(registeredTools).sort(), [
"memory_add",
"memory_remove",
"memory_replace",
]);
assert.strictEqual(
Value.Check(registeredTools.memory_add.parameters, {
action: "add",
target: "failure",
category: "tool-quirk",
failure_reason: "missing content",
}),
false,
"add without content must fail schema validation",
);
assert.strictEqual(
Value.Check(registeredTools.memory_replace.parameters, {
target: "memory",
content: "new",
}),
false,
"replace without old_text must fail schema validation",
);
assert.strictEqual(
Value.Check(registeredTools.memory_remove.parameters, { target: "memory" }),
false,
"remove without old_text must fail schema validation",
);
assert.strictEqual(
Value.Check(registeredTools.memory_add.parameters, {
target: "memory",
content: "durable fact",
}),
true,
);
assert.strictEqual(
Value.Check(registeredTools.memory_replace.parameters, {
target: "memory",
old_text: "old",
content: "new",
}),
true,
);
assert.strictEqual(
Value.Check(registeredTools.memory_remove.parameters, {
target: "memory",
old_text: "old",
}),
true,
);
});
it("execute delegates remove to store.remove", async () => {
let capturedResult: any;
let removeArgs: any;
const mockPi = {
registerTool: (def: any) => {
if (def.name === "memory_remove") capturedResult = def;
},
} as unknown as ExtensionAPI;
const mockStore = {
remove: (...args: any[]) => {
removeArgs = args;
return { success: true, target: "memory", entries: [], usage: "0% — 0/5000 chars", entry_count: 0 };
},
} as unknown as MemoryStore;
registerMemoryTool(mockPi, mockStore, null);
await capturedResult.execute("tc-1", { target: "memory", old_text: "old entry" }, undefined as any, undefined as any, undefined as any);
assert.deepStrictEqual(removeArgs, ["memory", "old entry"], "should pass target, old_text to store.remove");
});
it("execute delegates replace to store.replace", async () => {
let capturedResult: any;
let replaceArgs: any;
const mockPi = {
registerTool: (def: any) => {
if (def.name === "memory_replace") capturedResult = def;
},
} as unknown as ExtensionAPI;
const mockStore = {
replace: (...args: any[]) => {
replaceArgs = args;
return { success: true, target: "memory", entries: ["new"], usage: "5% — 110/5000 chars", entry_count: 1 };
},
} as unknown as MemoryStore;
registerMemoryTool(mockPi, mockStore, null);
await capturedResult.execute("tc-1", { action: "replace", target: "memory", content: "new", old_text: "old" }, undefined as any, undefined as any, undefined as any);
assert.deepStrictEqual(replaceArgs, ["memory", "old", "new"], "should pass target, old_text, content to store.replace");
});
it("binds project identity from execute ctx.cwd instead of a factory snapshot", async () => {
let capturedResult: any;
const boundCwds: string[] = [];
const mockPi = {
registerTool: (def: any) => {
if (!capturedResult || def.name === "memory_add") capturedResult = def;
},
} as unknown as ExtensionAPI;
const mockProjectStore = {
add: () => ({
success: true,
target: "memory",
entries: ["from session cwd"],
usage: "2% — 20/5000 chars",
entry_count: 1,
message: "Entry added.",
}),
} as unknown as MemoryStore;
registerMemoryTool(
mockPi,
{} as MemoryStore,
mockProjectStore,
dbManager,
"factory-project",
(cwd) => {
if (cwd) boundCwds.push(cwd);
},
);
await capturedResult.execute(
"tc-1",
{ target: "project", content: "from session cwd" },
undefined,
undefined,
{ cwd: "/tmp/opened-session-project" },
);
assert.deepStrictEqual(boundCwds, ["/tmp/opened-session-project"]);
});
});
@@ -0,0 +1,345 @@
import { describe, it, afterEach } from "node:test";
import * as assert from "node:assert/strict";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { registerSessionSearchTool } from "../../src/tools/session-search-tool.js";
import { DatabaseManager } from "../../src/store/db.js";
import { indexSession } from "../../src/store/session-indexer.js";
let ROOT_DIR = "";
afterEach(() => {
if (ROOT_DIR) fs.rmSync(ROOT_DIR, { recursive: true, force: true });
ROOT_DIR = "";
});
function makeSessionsDir(): string {
ROOT_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "pi-session-search-tool-test-"));
return ROOT_DIR;
}
describe("registerSessionSearchTool", () => {
it("registers the legacy query schema by default", () => {
let captured: any;
const mockPi = {
registerTool: (def: any) => { captured = def; },
} as any;
registerSessionSearchTool(mockPi, {} as any);
const schema = JSON.stringify(captured.parameters);
assert.strictEqual(captured.name, "session_search");
assert.match(schema, /query/);
assert.doesNotMatch(schema, /markdown/);
assert.match(schema, /"minimum":1/);
assert.match(schema, /"maximum":20/);
});
it("clamps negative and fractional legacy limits before querying", async () => {
let captured: any;
const mockPi = {
registerTool: (def: any) => { captured = def; },
} as any;
const memoryDir = makeSessionsDir();
const dbManager = new DatabaseManager(memoryDir);
try {
indexSession(dbManager, {
id: "bounded-limit-session",
project: "bounded-project",
cwd: "/work/bounded",
startedAt: "2026-07-11T00:00:00.000Z",
endedAt: null,
messages: Array.from({ length: 25 }, (_, index) => ({
id: `bounded-limit-message-${index}`,
role: "assistant",
content: `bounded-limit-needle ${index}`,
timestamp: `2026-07-11T00:${String(index).padStart(2, "0")}:00.000Z`,
})),
});
registerSessionSearchTool(mockPi, dbManager);
const negative = await captured.execute("tc-negative-limit", {
query: "bounded-limit-needle",
limit: -1,
});
const fractional = await captured.execute("tc-fractional-limit", {
query: "bounded-limit-needle",
limit: 2.9,
});
assert.strictEqual(negative.details.count, 1);
assert.strictEqual(fractional.details.count, 2);
assert.ok(negative.content[0].text.length < 2_000);
assert.ok(fractional.content[0].text.length < 4_000);
} finally {
dbManager.close();
}
});
it("clamps non-finite snippetChars values instead of propagating NaN", async () => {
let captured: any;
const mockPi = {
registerTool: (def: any) => { captured = def; },
} as any;
const memoryDir = makeSessionsDir();
const dbManager = new DatabaseManager(memoryDir);
try {
indexSession(dbManager, {
id: "nan-snippet-session",
project: "nan-snippet-project",
cwd: "/work/nan-snippet",
startedAt: "2026-07-11T00:00:00.000Z",
endedAt: null,
messages: [{
id: "nan-snippet-message",
role: "assistant",
content: `needle ${"a".repeat(500)}`,
timestamp: "2026-07-11T00:01:00.000Z",
}],
});
registerSessionSearchTool(mockPi, dbManager);
const nanResult = await captured.execute("tc-nan-snippet", {
query: "needle",
snippetChars: NaN,
});
const infinityResult = await captured.execute("tc-infinity-snippet", {
query: "needle",
snippetChars: Infinity,
});
const largeResult = await captured.execute("tc-large-snippet", {
query: "needle",
snippetChars: 999_999_999,
});
assert.strictEqual(nanResult.details.snippetChars, 1_200);
assert.strictEqual(infinityResult.details.snippetChars, 1_200);
assert.strictEqual(largeResult.details.snippetChars, 4_000);
assert.ok(Number.isFinite(nanResult.details.snippetChars));
assert.match(nanResult.content[0].text, /needle a+/);
} finally {
dbManager.close();
}
});
it("bounds oversized legacy results and reports truncation without duplicating output in details", async () => {
let captured: any;
const mockPi = {
registerTool: (def: any) => { captured = def; },
} as any;
const memoryDir = makeSessionsDir();
const dbManager = new DatabaseManager(memoryDir);
const oversizedContent = `needle ${"x".repeat(6_000_000)}`;
try {
// Seed a pre-cap database row directly. New ingestion paths must cap
// message content, but search output still needs to stay bounded for
// oversized rows written by older extension versions.
const db = dbManager.getDb();
db.prepare(`
INSERT INTO sessions (id, project, cwd, started_at, ended_at, message_count)
VALUES (?, ?, ?, ?, ?, ?)
`).run(
"oversized-session",
"oversized-project",
"/work/oversized",
"2026-07-11T00:00:00.000Z",
null,
1,
);
db.prepare(`
INSERT INTO messages (id, session_id, role, content, timestamp)
VALUES (?, ?, ?, ?, ?)
`).run(
"oversized-message",
"oversized-session",
"assistant",
oversizedContent,
"2026-07-11T00:01:00.000Z",
);
registerSessionSearchTool(mockPi, dbManager);
const result = await captured.execute("tc-oversized", { query: "needle" });
const output = result.content[0].text as string;
assert.ok(output.length <= 50 * 1024, `expected <= 50 KiB, got ${output.length}`);
assert.match(output, /truncated/);
assert.match(output, /6000007 chars total/);
assert.strictEqual(result.details.truncatedCount, 1);
assert.strictEqual(result.details.outputChars, output.length);
assert.strictEqual(result.details.output, undefined);
assert.ok(JSON.stringify(result.details).length < 1_000);
} finally {
dbManager.close();
}
});
it("offers a bounded snippetChars override for legacy searches", async () => {
let captured: any;
const mockPi = {
registerTool: (def: any) => { captured = def; },
} as any;
const memoryDir = makeSessionsDir();
const dbManager = new DatabaseManager(memoryDir);
try {
indexSession(dbManager, {
id: "bounded-override-session",
project: "bounded-project",
cwd: "/work/bounded",
startedAt: "2026-07-11T00:00:00.000Z",
endedAt: null,
messages: [{
id: "bounded-override-message",
role: "assistant",
content: `needle ${"y".repeat(10_000)}`,
timestamp: "2026-07-11T00:01:00.000Z",
}],
});
registerSessionSearchTool(mockPi, dbManager);
assert.match(JSON.stringify(captured.parameters), /snippetChars/);
const result = await captured.execute("tc-bounded-override", {
query: "needle",
snippetChars: 2_000,
});
assert.strictEqual(result.details.snippetChars, 2_000);
assert.strictEqual(result.details.truncatedCount, 1);
assert.match(result.content[0].text, /10007 chars total/);
assert.ok(result.content[0].text.length < 3_000);
} finally {
dbManager.close();
}
});
it("enforces a hard 50 KiB ceiling across many large legacy results", async () => {
let captured: any;
const mockPi = {
registerTool: (def: any) => { captured = def; },
} as any;
const memoryDir = makeSessionsDir();
const dbManager = new DatabaseManager(memoryDir);
try {
indexSession(dbManager, {
id: "aggregate-ceiling-session",
project: "aggregate-project",
cwd: "/work/aggregate",
startedAt: "2026-07-11T00:00:00.000Z",
endedAt: null,
messages: Array.from({ length: 20 }, (_, index) => ({
id: `aggregate-message-${index}`,
role: "assistant",
content: `needle-${index} ${"z".repeat(10_000)}`,
timestamp: `2026-07-11T00:${String(index).padStart(2, "0")}:00.000Z`,
})),
});
registerSessionSearchTool(mockPi, dbManager);
const result = await captured.execute("tc-aggregate-ceiling", {
query: "needle",
limit: 20,
snippetChars: 4_000,
});
const output = result.content[0].text as string;
assert.ok(output.length <= 50 * 1024, `expected <= 50 KiB, got ${output.length}`);
assert.strictEqual(result.details.outputTruncated, true);
assert.match(output, /output truncated/);
assert.match(output, /refine the query or lower the result limit/);
} finally {
dbManager.close();
}
});
it("bounds the zero-result response without echoing an oversized query", async () => {
let captured: any;
const mockPi = {
registerTool: (def: any) => { captured = def; },
} as any;
const memoryDir = makeSessionsDir();
const dbManager = new DatabaseManager(memoryDir);
try {
indexSession(dbManager, {
id: "zero-result-session",
project: "zero-result-project",
cwd: "/work/zero-result",
startedAt: "2026-07-11T00:00:00.000Z",
endedAt: null,
messages: [{
id: "zero-result-message",
role: "assistant",
content: "indexed haystack",
timestamp: "2026-07-11T00:01:00.000Z",
}],
});
registerSessionSearchTool(mockPi, dbManager);
const query = `${" ".repeat(60_000)}missing`;
const result = await captured.execute("tc-zero-result", { query });
const output = result.content[0].text as string;
assert.strictEqual(result.details.count, 0);
assert.ok(output.length <= 50 * 1024, `expected <= 50 KiB, got ${output.length}`);
assert.strictEqual(output.includes(query), false);
assert.ok(JSON.stringify(result.details).length < 1_000);
} finally {
dbManager.close();
}
});
it("registers and executes the anchor markdown-only schema when configured", async () => {
let captured: any;
const mockPi = {
registerTool: (def: any) => { captured = def; },
} as any;
const sessionsDir = makeSessionsDir();
const filePath = path.join(sessionsDir, "session.jsonl");
fs.writeFileSync(filePath, `${JSON.stringify({
type: "message",
timestamp: "2026-05-15T10:00:00.000Z",
sessionId: "session-1",
cwd: "/work/project",
message: { role: "user", content: "needle" },
})}\n`);
registerSessionSearchTool(mockPi, {} as any, { variant: "anchors" }, { sessionsDir });
const schema = JSON.stringify(captured.parameters);
assert.strictEqual(captured.name, "session_search");
assert.match(schema, /markdown/);
assert.doesNotMatch(schema, /query/);
assert.match(captured.description, /all terms must match/);
assert.match(captured.description, /any requires at least one listed term/);
assert.match(captured.description, /exclude removes matching ranges/);
assert.match(captured.description, /Output is plain text: count, optional message/);
assert.match(captured.description, /path:startLine-endLine with a short reason/);
assert.match(captured.description, /Example:\nfrom: 2026-05-14/);
assert.match(captured.promptGuidelines.join("\n"), /Use all for required terms/);
const empty = await captured.execute("tc-1", { markdown: "" });
assert.strictEqual(empty.details.success, false);
assert.strictEqual(empty.details.message, "markdown is required");
const result = await captured.execute("tc-2", { markdown: "any:\n- needle" });
assert.strictEqual(result.details.success, true);
assert.strictEqual(result.details.count, 1);
assert.deepStrictEqual(result.details.ranges.map((range: any) => ({
path: range.path,
startLine: range.startLine,
endLine: range.endLine,
reason: range.reason,
})), [{ path: filePath, startLine: 1, endLine: 1, reason: "matched any: needle" }]);
assert.strictEqual(result.details.output, result.content[0].text);
assert.match(result.content[0].text, /^count: 1\nanchors:\n-/);
assert.match(result.content[0].text, new RegExp(`${filePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}:1-1 — matched any: needle`));
assert.doesNotMatch(result.content[0].text, /"ranges"/);
assert.doesNotMatch(result.content[0].text, /"startLine"/);
assert.doesNotMatch(result.content[0].text, /"sessionId"/);
});
});
@@ -0,0 +1,362 @@
import { describe, it } from "node:test";
import * as assert from "node:assert/strict";
import { initTheme } from "@earendil-works/pi-coding-agent";
import { Box, visibleWidth, type Component } from "@earendil-works/pi-tui";
import {
createSharedToolResultRenderer,
type SharedOutputView,
} from "../../src/tools/shared-output-view.js";
import {
memoryResultView,
searchResultView,
skillResultView,
} from "../../src/tools/tool-result-views.js";
initTheme();
function fakeTheme() {
return {
fg: (_color: string, text: string): string => text,
getBgAnsi: (_color: string): string => "",
} as any;
}
function stripAnsi(text: string): string {
return text.replace(/\x1b\[[0-?]*[ -/]*m/g, "");
}
function renderPlain(component: Component, width = 120): string {
return component.render(width).map((row) => stripAnsi(row).trimEnd()).join("\n");
}
function result(text: string, details: Record<string, unknown> = { success: true }) {
return { content: [{ type: "text", text }], details };
}
function renderToolResult(
toolResult: ReturnType<typeof result>,
expanded: boolean,
width = 120,
): string {
return renderPlain(
createSharedToolResultRenderer()(
toolResult,
{ expanded, isPartial: false },
fakeTheme(),
{ isError: false },
),
width,
);
}
function renderView(view: SharedOutputView, expanded: boolean, width = 120): string {
return renderPlain(
createSharedToolResultRenderer(() => view)(
result(view.expandedText),
{ expanded, isPartial: false },
fakeTheme(),
{ isError: false },
),
width,
);
}
function assertRowKeepsBackground(row: string): void {
let backgroundActive = false;
for (let index = 0; index < row.length;) {
const sgr = row.slice(index).match(/^\x1b\[([0-9;]*)m/);
if (sgr) {
const parameters = sgr[1];
if (parameters === "" || parameters === "0" || parameters === "49") {
backgroundActive = false;
} else if (parameters.split(";")[0] === "48") {
backgroundActive = true;
}
index += sgr[0].length;
continue;
}
assert.equal(backgroundActive, true, `background cleared before ${JSON.stringify(row.slice(index))}`);
index += 1;
}
}
describe("shared tool-result view", () => {
it("collapses to one concise line and expands to the full bounded result", () => {
const toolResult = result("Found 2 memories matching auth:\n\nfirst\nsecond", {
success: true,
count: 2,
});
const collapsed = renderToolResult(toolResult, false);
assert.equal(collapsed.split("\n").length, 1);
assert.match(collapsed, /Found 2 memories matching auth/);
assert.doesNotMatch(collapsed, /second/);
assert.match(collapsed, /expand/);
const expanded = renderToolResult(toolResult, true);
assert.equal(expanded, toolResult.content[0].text);
});
it("sanitizes only the rendered copy of hostile tool text and failure details", () => {
const hostileText = [
"safe 世界 \x1b]52;c;YXR0YWNr\x07 after OSC",
"styled \x1b[31mred\x1b[0m moved \x1b[2J after CSI",
"private \x1bPdanger\x1b\\ after DCS",
"linked \x1b]8;;https://evil.example\x1b\\label\x1b]8;;\x1b\\ after ST",
"carriage\rreturn binary\x00\x08\x7f end",
].join("\n");
const hostileReason = "query \x1b]52;c;YXR0YWNr\x07 is required\x00";
const toolResult = result(hostileText, {
success: false,
message: hostileReason,
nested: { keep: true },
});
const before = structuredClone(toolResult);
const collapsed = renderToolResult(toolResult, false);
const expanded = renderToolResult(toolResult, true);
for (const rendered of [collapsed, expanded]) {
assert.doesNotMatch(rendered, /\x1b|\x07|\x00|\x08|\x7f|\r/);
}
assert.match(collapsed, /query .* is required/);
assert.match(expanded, /safe 世界/);
assert.match(expanded, /styled red moved after CSI/);
assert.match(expanded, /private danger\\ after DCS/);
assert.match(expanded, /linked label after ST/);
assert.match(expanded, /carriagereturn binary end/);
assert.deepEqual(toolResult, before);
const adapterResult = result("safe", {
success: true,
message: "Entry added.",
target: "memory\x1b[2J",
usage: "1 entry\x00",
});
const adapterBefore = structuredClone(adapterResult);
const adapterRendered = renderPlain(createSharedToolResultRenderer(memoryResultView)(
adapterResult,
{ expanded: false, isPartial: false },
fakeTheme(),
{ isError: false },
));
assert.doesNotMatch(adapterRendered, /\x1b|\x00/);
assert.deepEqual(adapterResult, adapterBefore);
});
it("does not mutate model-visible content or details", () => {
const toolResult = result("immutable\nfull output", {
success: true,
nested: { keep: true },
});
const before = structuredClone(toolResult);
renderToolResult(toolResult, false);
renderToolResult(toolResult, true);
assert.deepEqual(toolResult, before);
});
it("keeps textual failure and warning reasons in collapsed output", () => {
const failure = renderToolResult(
result("query is required", { success: false, message: "query is required" }),
false,
);
assert.match(failure, /query is required/);
const warningView = memoryResultView(result(JSON.stringify({
success: true,
message: "Entry added. Warning: SQLite mirror unavailable",
warning: "SQLite mirror unavailable",
warnings: ["SQLite mirror unavailable"],
target: "memory",
entry_count: 1,
}), {
success: true,
message: "Entry added. Warning: SQLite mirror unavailable",
warning: "SQLite mirror unavailable",
warnings: ["SQLite mirror unavailable"],
target: "memory",
entry_count: 1,
}));
assert.match(renderView(warningView, false), /SQLite mirror unavailable/);
});
it("fits ANSI, CJK, and partial rows while preserving the outer tool-card background", () => {
const width = 34;
const backgroundOpen = "\x1b[48;2;40;50;40m";
const ansiTheme = {
fg: (_color: string, text: string): string => `\x1b[38;2;145;148;145m${text}\x1b[39m`,
getBgAnsi: (_color: string): string => backgroundOpen,
} as any;
const box = new Box(1, 1, (text: string) => `${backgroundOpen}${text}\x1b[49m`);
box.addChild(createSharedToolResultRenderer()(
result("处理中 世界世界世界世界 \x1b[1mvery long partial output\x1b[0m"),
{ expanded: false, isPartial: true },
ansiTheme,
{ isError: false },
));
const rows = box.render(width);
assert.equal(rows.length, 3);
assert.equal(visibleWidth(rows[1]), width);
assert.match(stripAnsi(rows[1]), /处理中|In progress/);
assertRowKeepsBackground(rows[1]);
});
it("keeps the tool-card background after sanitizing expanded ANSI and CJK text", () => {
const width = 18;
const backgroundOpen = "\x1b[48;2;40;50;40m";
const ansiTheme = {
fg: (_color: string, text: string): string => text,
getBgAnsi: (_color: string): string => backgroundOpen,
} as any;
const expandedText = "第一行 世界世界世界\x1b[0m继续\n第二行 \x1b[49m背景保留";
const box = new Box(1, 1, (text: string) => `${backgroundOpen}${text}\x1b[49m`);
box.addChild(createSharedToolResultRenderer()(
result(expandedText),
{ expanded: true, isPartial: false },
ansiTheme,
{ isError: false },
));
const rows = box.render(width);
assert.ok(rows.length > 4);
for (const row of rows) {
assert.equal(visibleWidth(row), width);
assertRowKeepsBackground(row);
}
const expandedRows = rows.slice(1, -1).map((row) => stripAnsi(row).trim());
assert.deepEqual(expandedRows, ["第一行 世界世界", "世界继续", "第二行 背景保留"]);
});
it("uses Pi shell state for the card background and logical status for foreground", () => {
const scenarios = [
{ partial: true, isError: false, success: false, background: "toolPendingBg", foreground: "warning" },
{ partial: false, isError: true, success: true, background: "toolErrorBg", foreground: "error" },
{ partial: false, isError: false, success: true, background: "toolSuccessBg", foreground: "toolOutput" },
{ partial: false, isError: false, success: false, background: "toolSuccessBg", foreground: "error" },
] as const;
for (const scenario of scenarios) {
const backgrounds: string[] = [];
const foregrounds: string[] = [];
const theme = {
fg: (color: string, text: string): string => {
foregrounds.push(color);
return text;
},
getBgAnsi: (color: string): string => {
backgrounds.push(color);
return "";
},
} as any;
const message = scenario.success ? "done" : "query is required";
const toolResult = result(message, { success: scenario.success, message });
const renderer = createSharedToolResultRenderer();
const rendered = renderPlain(renderer(
toolResult,
{ expanded: false, isPartial: scenario.partial },
theme,
{ isError: scenario.isError },
));
renderPlain(renderer(
toolResult,
{ expanded: true, isPartial: scenario.partial },
theme,
{ isError: scenario.isError },
));
assert.deepEqual(backgrounds, [scenario.background, scenario.background]);
assert.deepEqual(foregrounds, [scenario.foreground]);
if (!scenario.success) assert.match(rendered, /query is required/);
}
});
});
describe("tool-specific summaries", () => {
it("summarizes memory, search/session, and skill results without changing expanded text", () => {
const memoryText = JSON.stringify({
success: true,
message: "Entry added.",
target: "failure",
category: "tool-quirk",
entry_count: 2,
usage: "120 / 5,000 chars",
});
const memory = memoryResultView(result(memoryText, JSON.parse(memoryText)));
assert.match(memory.summary, /Saved/);
assert.match(memory.summary, /target: failure/);
assert.match(memory.summary, /category: tool-quirk/);
assert.equal(memory.expandedText, memoryText);
const searchText = "Found 3 memories matching auth:\n\nfirst\nsecond\nthird";
const search = searchResultView(result(searchText, { success: true, count: 3 }));
assert.match(search.summary, /3/);
assert.equal(search.expandedText, searchText);
const skillText = JSON.stringify({ success: true, skillId: "global:deploy", name: "deploy" });
const skill = skillResultView(result(skillText, JSON.parse(skillText)));
assert.match(skill.summary, /deploy/i);
assert.equal(skill.expandedText, skillText);
});
it("renders a real skill-tool JSON failure as an actionable failure", () => {
const error = "Skill 'global:missing' not found.";
const skillText = JSON.stringify({ success: false, error });
const toolResult = result(skillText, {});
const skill = skillResultView(toolResult);
assert.equal(skill.status, "failure");
assert.equal(skill.summary, `Error · ${error}`);
assert.equal(skill.expandedText, skillText);
assert.match(renderPlain(
createSharedToolResultRenderer(skillResultView)(
toolResult,
{ expanded: false, isPartial: false },
fakeTheme(),
{ isError: false },
),
), new RegExp(error.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
});
it("summarizes successful failure memory from the real producer result shape", () => {
// MemoryResult has no category field; producers encode it only in message.
const details = {
success: true,
target: "failure",
message: "Failure memory saved: tool-quirk",
entry_count: 16,
usage: "92% — 9271/10000 chars",
};
const fullText = JSON.stringify(details);
const toolResult = result(fullText, details);
const before = structuredClone(toolResult);
const view = memoryResultView(toolResult);
assert.equal(view.status, "success");
assert.equal(
view.summary,
"Saved · target: failure · category: tool-quirk · 16 entries · 92% — 9271/10000 chars",
);
assert.doesNotMatch(view.summary, /^failure:/);
assert.equal(view.expandedText, fullText);
assert.deepEqual(toolResult, before);
const collapsed = renderPlain(
createSharedToolResultRenderer(memoryResultView)(
toolResult,
{ expanded: false, isPartial: false },
fakeTheme(),
{ isError: false },
),
);
assert.match(
collapsed,
/Saved · target: failure · category: tool-quirk · 16 entries · 92% — 9271\/10000 chars/,
);
assert.doesNotMatch(collapsed, /^failure:/);
});
});
@@ -0,0 +1,529 @@
/**
* Unit tests for skill tool registration and execute function.
*/
import { describe, it } from "node:test";
import * as assert from "node:assert/strict";
import { registerSkillTool, SKILL_MANAGE_TOOL_NAME } from "../../src/tools/skill-tool.js";
import { SkillStore } from "../../src/store/skill-store.js";
import * as path from "node:path";
import * as os from "node:os";
import * as fs from "node:fs/promises";
let ROOT_DIR = "";
let GLOBAL_SKILLS_DIR = "";
let PROJECT_SKILLS_DIR = "";
async function makeStore(withProject = true): Promise<SkillStore> {
ROOT_DIR = await fs.mkdtemp(path.join(os.tmpdir(), "pi-skill-tool-test-"));
GLOBAL_SKILLS_DIR = path.join(ROOT_DIR, "global-skills");
PROJECT_SKILLS_DIR = path.join(ROOT_DIR, "project-skills");
await fs.mkdir(GLOBAL_SKILLS_DIR, { recursive: true });
if (withProject) await fs.mkdir(PROJECT_SKILLS_DIR, { recursive: true });
return new SkillStore({
globalSkillsDir: GLOBAL_SKILLS_DIR,
projectSkillsDir: withProject ? PROJECT_SKILLS_DIR : null,
projectName: withProject ? "demo-project" : null,
legacySkillsDir: path.join(ROOT_DIR, "legacy-skills"),
migrationSentinelPath: path.join(ROOT_DIR, ".skill-migration"),
});
}
async function cleanup(): Promise<void> {
try {
await fs.rm(ROOT_DIR, { recursive: true, force: true });
} catch {
// ignore
}
}
describe("registerSkillTool", () => {
it("registers tool with name 'skill_manage'", async () => {
let captured: any;
const mockPi = {
registerTool: (def: any) => { captured = def; },
} as any;
const store = await makeStore();
registerSkillTool(mockPi, store);
await cleanup();
assert.strictEqual(captured.name, SKILL_MANAGE_TOOL_NAME);
assert.strictEqual(captured.label, "Skill Manager");
assert.ok(captured.description.length > 0);
assert.ok(captured.promptSnippet.length > 0);
assert.ok(Array.isArray(captured.promptGuidelines));
assert.ok(captured.parameters);
});
it("create requires name, description, a body or structured fields, and scope", async () => {
let captured: any;
const mockPi = {
registerTool: (def: any) => { captured = def; },
} as any;
const store = await makeStore();
registerSkillTool(mockPi, store);
let result = await captured.execute("tc-1", { action: "create", description: "desc", content: "body" }, undefined, undefined, undefined);
assert.strictEqual(JSON.parse(result.content[0].text).success, false);
result = await captured.execute("tc-1", { action: "create", name: "test", content: "body" }, undefined, undefined, undefined);
assert.strictEqual(JSON.parse(result.content[0].text).success, false);
result = await captured.execute("tc-1", { action: "create", name: "test", description: "desc" }, undefined, undefined, undefined);
const missingBody = JSON.parse(result.content[0].text);
assert.strictEqual(missingBody.success, false);
assert.match(missingBody.error, /Either content or structured fields are required/i);
result = await captured.execute("tc-1", {
action: "create",
name: "test",
description: "desc",
content: "body",
}, undefined, undefined, undefined);
const missingScope = JSON.parse(result.content[0].text);
assert.strictEqual(missingScope.success, false);
assert.match(missingScope.error, /scope is required/i);
await cleanup();
});
it("create succeeds with raw markdown content and returns skill_id", async () => {
let captured: any;
const mockPi = {
registerTool: (def: any) => { captured = def; },
} as any;
const store = await makeStore();
registerSkillTool(mockPi, store);
const result = await captured.execute("tc-1", {
action: "create",
name: "test-skill",
description: "A test skill",
scope: "global",
content: "## Procedure\n1. Do it",
}, undefined, undefined, undefined);
const parsed = JSON.parse(result.content[0].text);
assert.strictEqual(parsed.success, true);
assert.strictEqual(parsed.skillId, "global:test-skill");
assert.strictEqual(parsed.scope, "global");
await cleanup();
});
it("create builds a valid skill body from structured fields", async () => {
let captured: any;
const mockPi = {
registerTool: (def: any) => { captured = def; },
} as any;
const store = await makeStore();
registerSkillTool(mockPi, store);
const result = await captured.execute("tc-1", {
action: "create",
name: "debug-typescript-errors",
description: "Debug TypeScript build failures in this repo",
scope: "project",
when_to_use: "Use when the repo's TypeScript build fails locally or in CI.",
procedure_steps: [
"Run pnpm tsc --noEmit to get the full error list.",
"Fix dependency and config errors before leaf-module errors.",
],
pitfalls: [
"Do not rely on editor-only diagnostics.",
],
verification_steps: [
"pnpm tsc --noEmit exits successfully.",
],
}, undefined, undefined, undefined);
const parsed = JSON.parse(result.content[0].text);
assert.strictEqual(parsed.success, true);
const created = await store.loadSkill(parsed.skillId);
assert.match(created?.body || "", /## When to Use/);
assert.match(created?.body || "", /## Procedure/);
assert.match(created?.body || "", /1\. Run pnpm tsc --noEmit/);
assert.match(created?.body || "", /## Pitfalls/);
assert.match(created?.body || "", /## Verification/);
await cleanup();
});
it("create supports explicit project scope", async () => {
let captured: any;
const mockPi = {
registerTool: (def: any) => { captured = def; },
} as any;
const store = await makeStore();
registerSkillTool(mockPi, store);
const result = await captured.execute("tc-1", {
action: "create",
name: "release-app",
description: "Release this app",
scope: "project",
content: "## Procedure\n1. Run pnpm build",
}, undefined, undefined, undefined);
const parsed = JSON.parse(result.content[0].text);
assert.strictEqual(parsed.success, true);
assert.strictEqual(parsed.skillId, "project:demo-project:release-app");
assert.strictEqual(parsed.scope, "project");
await cleanup();
});
it("view without skill_id lists all skills", async () => {
let captured: any;
const mockPi = {
registerTool: (def: any) => { captured = def; },
} as any;
const store = await makeStore();
await store.create("skill-a", "First", "body a");
await store.create("skill-b", "Second", "body b", "project");
registerSkillTool(mockPi, store);
const result = await captured.execute("tc-1", { action: "view" }, undefined, undefined, undefined);
const parsed = JSON.parse(result.content[0].text);
assert.strictEqual(parsed.success, true);
assert.strictEqual(parsed.skills.length, 2);
await cleanup();
});
it("view with skill_id returns full document", async () => {
let captured: any;
const mockPi = {
registerTool: (def: any) => { captured = def; },
} as any;
const store = await makeStore();
const created = await store.create("my-skill", "A skill", "## Body content here");
registerSkillTool(mockPi, store);
const result = await captured.execute("tc-1", { action: "view", skill_id: created.skillId }, undefined, undefined, undefined);
const parsed = JSON.parse(result.content[0].text);
assert.strictEqual(parsed.success, true);
assert.strictEqual(parsed.name, "my-skill");
assert.ok(parsed.body.includes("## Body content here"));
await cleanup();
});
it("view with invalid skill_id returns error", async () => {
let captured: any;
const mockPi = {
registerTool: (def: any) => { captured = def; },
} as any;
const store = await makeStore();
registerSkillTool(mockPi, store);
const result = await captured.execute("tc-1", { action: "view", skill_id: "global:missing" }, undefined, undefined, undefined);
const parsed = JSON.parse(result.content[0].text);
assert.strictEqual(parsed.success, false);
assert.ok(parsed.error.includes("not found"));
await cleanup();
});
it("patch requires skill_id, section, and content or structured fields", async () => {
let captured: any;
const mockPi = {
registerTool: (def: any) => { captured = def; },
} as any;
const store = await makeStore();
registerSkillTool(mockPi, store);
let result = await captured.execute("tc-1", { action: "patch", section: "Procedure", content: "new" }, undefined, undefined, undefined);
assert.strictEqual(JSON.parse(result.content[0].text).success, false);
result = await captured.execute("tc-1", { action: "patch", skill_id: "global:test", content: "new" }, undefined, undefined, undefined);
assert.strictEqual(JSON.parse(result.content[0].text).success, false);
result = await captured.execute("tc-1", { action: "patch", skill_id: "global:test", section: "Procedure" }, undefined, undefined, undefined);
assert.strictEqual(JSON.parse(result.content[0].text).success, false);
await cleanup();
});
it("patch accepts structured procedure_steps for a section update", async () => {
let captured: any;
const mockPi = {
registerTool: (def: any) => { captured = def; },
} as any;
const store = await makeStore();
const created = await store.create(
"patch-me",
"desc",
"## Procedure\n1. Old way\n\n## Pitfalls\n- Keep me\n\n## Verification\n1. Still works",
);
registerSkillTool(mockPi, store);
const result = await captured.execute("tc-1", {
action: "patch",
skill_id: created.skillId,
section: "Procedure",
procedure_steps: ["Do the new first step", "Do the new second step"],
}, undefined, undefined, undefined);
const parsed = JSON.parse(result.content[0].text);
assert.strictEqual(parsed.success, true);
const doc = await store.loadSkill(created.skillId!);
assert.match(doc!.body, /## Procedure\n1\. Do the new first step\n2\. Do the new second step/);
assert.match(doc!.body, /## Pitfalls\n- Keep me/);
assert.match(doc!.body, /## Verification\n1\. Still works/);
assert.doesNotMatch(doc!.body, /Old way/);
await cleanup();
});
it("patch rejects JSON object content strings", async () => {
let captured: any;
const mockPi = {
registerTool: (def: any) => { captured = def; },
} as any;
const store = await makeStore();
const created = await store.create("patch-me", "desc", "## Procedure\n1. Keep");
registerSkillTool(mockPi, store);
const result = await captured.execute("tc-1", {
action: "patch",
skill_id: created.skillId,
section: "Procedure",
content: '{"steps":["bad"]}',
}, undefined, undefined, undefined);
const parsed = JSON.parse(result.content[0].text);
assert.strictEqual(parsed.success, false);
assert.match(parsed.error, /JSON object/i);
assert.equal((await store.loadSkill(created.skillId!))!.version, 1);
await cleanup();
});
it("patch coerces JSON array content strings into Markdown lists", async () => {
let captured: any;
const mockPi = {
registerTool: (def: any) => { captured = def; },
} as any;
const store = await makeStore();
const created = await store.create("patch-me", "desc", "## Pitfalls\n- old");
registerSkillTool(mockPi, store);
const result = await captured.execute("tc-1", {
action: "patch",
skill_id: created.skillId,
section: "Pitfalls",
content: '["Do not skip verification", "Do not wipe other sections"]',
}, undefined, undefined, undefined);
const parsed = JSON.parse(result.content[0].text);
assert.strictEqual(parsed.success, true);
const doc = await store.loadSkill(created.skillId!);
assert.match(doc!.body, /## Pitfalls\n- Do not skip verification\n- Do not wipe other sections/);
assert.doesNotMatch(doc!.body, /\[\"Do not skip verification\"/);
await cleanup();
});
it("update requires skill_id", async () => {
let captured: any;
const mockPi = {
registerTool: (def: any) => { captured = def; },
} as any;
const store = await makeStore();
registerSkillTool(mockPi, store);
const result = await captured.execute("tc-1", { action: "update", description: "new desc" }, undefined, undefined, undefined);
const parsed = JSON.parse(result.content[0].text);
assert.strictEqual(parsed.success, false);
assert.ok(parsed.error.includes("skill_id"));
await cleanup();
});
it("update aliases to full skill rewrite", async () => {
let captured: any;
const mockPi = {
registerTool: (def: any) => { captured = def; },
} as any;
const store = await makeStore();
const created = await store.create("my-skill", "Old desc", "## Old body", "global");
registerSkillTool(mockPi, store);
const result = await captured.execute("tc-1", {
action: "update",
skill_id: created.skillId,
description: "New desc",
content: "## New body",
}, undefined, undefined, undefined);
const parsed = JSON.parse(result.content[0].text);
assert.strictEqual(parsed.success, true);
const updated = await store.loadSkill(created.skillId!);
assert.strictEqual(updated?.description, "New desc");
assert.match(updated?.body || "", /New body/);
await cleanup();
});
it("update can rebuild the body from structured fields", async () => {
let captured: any;
const mockPi = {
registerTool: (def: any) => { captured = def; },
} as any;
const store = await makeStore();
const created = await store.create("my-skill", "Old desc", "## Old body", "global");
registerSkillTool(mockPi, store);
const result = await captured.execute("tc-1", {
action: "update",
skill_id: created.skillId,
description: "New desc",
when_to_use: "Use when validating the new rewrite path.",
procedure_steps: ["Perform the new sequence."],
verification_steps: ["Confirm the new sequence works."],
}, undefined, undefined, undefined);
const parsed = JSON.parse(result.content[0].text);
assert.strictEqual(parsed.success, true);
const updated = await store.loadSkill(created.skillId!);
assert.match(updated?.body || "", /## When to Use/);
assert.match(updated?.body || "", /Perform the new sequence/);
assert.match(updated?.body || "", /No notable pitfalls recorded yet/);
await cleanup();
});
it("legacy edit alias still rewrites the skill", async () => {
let captured: any;
const mockPi = {
registerTool: (def: any) => { captured = def; },
} as any;
const store = await makeStore();
const created = await store.create("legacy-skill", "Old desc", "## Old body", "global");
registerSkillTool(mockPi, store);
const result = await captured.execute("tc-1", {
action: "edit",
skill_id: created.skillId,
description: "Legacy desc",
content: "## Legacy body",
}, undefined, undefined, undefined);
const parsed = JSON.parse(result.content[0].text);
assert.strictEqual(parsed.success, true);
const updated = await store.loadSkill(created.skillId!);
assert.strictEqual(updated?.description, "Legacy desc");
assert.match(updated?.body || "", /Legacy body/);
await cleanup();
});
it("delete requires skill_id", async () => {
let captured: any;
const mockPi = {
registerTool: (def: any) => { captured = def; },
} as any;
const store = await makeStore();
registerSkillTool(mockPi, store);
const result = await captured.execute("tc-1", { action: "delete" }, undefined, undefined, undefined);
const parsed = JSON.parse(result.content[0].text);
assert.strictEqual(parsed.success, false);
assert.ok(parsed.error.includes("skill_id"));
await cleanup();
});
it("unknown action returns error", async () => {
let captured: any;
const mockPi = {
registerTool: (def: any) => { captured = def; },
} as any;
const store = await makeStore();
registerSkillTool(mockPi, store);
const result = await captured.execute("tc-1", { action: "explode" }, undefined, undefined, undefined);
const parsed = JSON.parse(result.content[0].text);
assert.strictEqual(parsed.success, false);
assert.ok(parsed.error.includes("Unknown action"));
await cleanup();
});
});
describe("skill discovery wording (issue #244)", () => {
async function captureTool(): Promise<any> {
let captured: any;
const mockPi = {
registerTool: (def: any) => { captured = def; },
} as any;
const store = await makeStore();
registerSkillTool(mockPi, store);
return captured;
}
it("description param carries the discovery requirement, when_to_use notes the index boundary", async () => {
const captured = await captureTool();
const props = captured.parameters.properties;
const descriptionHelp: string = props.description.description;
assert.match(descriptionHelp, /Pi indexes skills by this field alone/);
assert.match(descriptionHelp, /trigger signals/);
assert.match(descriptionHelp, /1024/);
const whenToUseHelp: string = props.when_to_use.description;
assert.match(whenToUseHelp, /does not participate in Pi's skill index/);
await cleanup();
});
it("tool description states the mechanism and demonstrates it in the example", async () => {
const captured = await captureTool();
const text: string = captured.description;
assert.match(text, /Pi indexes skills by this field alone/);
assert.match(text, /discoverable trigger signals belong in description/);
assert.match(text, /"description": "Debug TypeScript build failures in this repo: tsc --noEmit errors/);
await cleanup();
});
it("prompt guidelines point trigger signals at description", async () => {
const captured = await captureTool();
const guidelines: string[] = captured.promptGuidelines;
assert.ok(
guidelines.some((g) => /Put the trigger signals a user would type in 'description'/.test(g)),
"expected a guideline directing trigger signals to description",
);
await cleanup();
});
});
@@ -0,0 +1,40 @@
import { describe, it } from "node:test";
import * as assert from "node:assert/strict";
import { registerMemoryTool } from "../../src/tools/memory-tool.js";
import { registerMemorySearchTool } from "../../src/tools/memory-search-tool.js";
import { registerSessionSearchTool } from "../../src/tools/session-search-tool.js";
import { registerSkillTool } from "../../src/tools/skill-tool.js";
function capture(register: (pi: any) => void): any {
let definition: any;
register({ registerTool(value: any) { definition = value; } });
return definition;
}
describe("tool result renderer wiring", () => {
it("registers renderResult on memory and memory_search", () => {
const memory = capture((pi) => registerMemoryTool(pi, {} as any, null));
const memorySearch = capture((pi) => registerMemorySearchTool(pi, {} as any));
assert.equal(typeof memory.renderResult, "function");
assert.equal(typeof memorySearch.renderResult, "function");
});
it("registers renderResult on both session_search variants", () => {
const legacy = capture((pi) => registerSessionSearchTool(pi, {} as any));
const anchors = capture((pi) => registerSessionSearchTool(
pi,
{} as any,
{ variant: "anchors" },
{ sessionsDir: "/path-independent-sessions" },
));
assert.equal(typeof legacy.renderResult, "function");
assert.equal(typeof anchors.renderResult, "function");
});
it("registers renderResult on skill_manage", () => {
const skill = capture((pi) => registerSkillTool(pi, {} as any));
assert.equal(typeof skill.renderResult, "function");
});
});