Files
my-pi/pi-hermes-memory/tests/store/session-anchor-search.test.ts
云服务部-叶林立 312bb2ba5a 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。
2026-10-13 23:19:35 -07:00

211 lines
8.7 KiB
TypeScript

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 { searchSessionAnchors } from "../../src/store/session-anchor-search.js";
let ROOT_DIR = "";
function makeSessionsDir(): string {
ROOT_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "pi-session-anchor-search-test-"));
return ROOT_DIR;
}
function writeJsonl(relativePath: string, events: unknown[]): string {
const filePath = path.join(ROOT_DIR, relativePath);
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, events.map((event) => JSON.stringify(event)).join("\n") + "\n");
return filePath;
}
function message(timestamp: string, text: string, extra: Record<string, unknown> = {}): unknown {
return {
type: "message",
timestamp,
sessionId: "session-1",
cwd: "/work/project",
message: { role: "user", content: text },
...extra,
};
}
afterEach(() => {
if (ROOT_DIR) fs.rmSync(ROOT_DIR, { recursive: true, force: true });
ROOT_DIR = "";
});
describe("searchSessionAnchors", () => {
it("accepts a minimal time window and caps limit", () => {
const sessionsDir = makeSessionsDir();
const events = Array.from({ length: 210 }, (_, index) => (
index % 2 === 0
? message(`2026-05-15T12:${String(index % 60).padStart(2, "0")}:00.000Z`, `event ${index}`)
: message(`2026-05-14T12:${String(index % 60).padStart(2, "0")}:00.000Z`, `outside ${index}`)
));
writeJsonl("session.jsonl", events);
const result = searchSessionAnchors("from: 2026-05-15\nto: 2026-05-15\nlimit: 200", { sessionsDir });
assert.strictEqual(result.success, true);
assert.strictEqual(result.ranges.length, 100);
assert.strictEqual(result.ranges[0].startLine, 1);
assert.strictEqual(result.ranges[0].reason, "time window");
});
it("returns diagnostics for duplicate, unknown, empty, and unconstrained requests", () => {
const sessionsDir = makeSessionsDir();
assert.match(searchSessionAnchors("", { sessionsDir }).message ?? "", /markdown is required/);
assert.match(searchSessionAnchors("since: 2026-05-15", { sessionsDir }).message ?? "", /Invalid field 'since'/);
assert.match(searchSessionAnchors("from: 2026-05-15\nfrom: 2026-05-16", { sessionsDir }).message ?? "", /Duplicate field 'from'/);
assert.match(searchSessionAnchors("limit: 0", { sessionsDir }).message ?? "", /Invalid limit/);
assert.match(searchSessionAnchors("all:\n- ", { sessionsDir }).message ?? "", /Invalid markdown line|Empty term/);
assert.match(searchSessionAnchors("limit: 10", { sessionsDir }).message ?? "", /needs at least one constraint/);
});
it("measures broadness with scan caps instead of rejecting request shape", () => {
const sessionsDir = makeSessionsDir();
writeJsonl("one.jsonl", [
message("2026-05-15T10:00:00.000Z", "a"),
message("2026-05-15T11:00:00.000Z", "b"),
]);
writeJsonl("two.jsonl", [message("2026-05-15T12:00:00.000Z", "a")]);
const shortTerm = searchSessionAnchors("any:\n- a", { sessionsDir });
assert.strictEqual(shortTerm.success, true);
assert.strictEqual(shortTerm.ranges.length, 2);
const fileCap = searchSessionAnchors("any:\n- a", { sessionsDir, maxFiles: 1 });
assert.strictEqual(fileCap.success, false);
assert.match(fileCap.message ?? "", /2 session files exceed/);
const lineCap = searchSessionAnchors("any:\n- a", { sessionsDir, maxLines: 1 });
assert.strictEqual(lineCap.success, false);
assert.match(lineCap.message ?? "", /scan cap/);
});
it("allows cwd-only requests as bounded source anchors", () => {
const sessionsDir = makeSessionsDir();
writeJsonl("session.jsonl", [
message("2026-05-15T10:00:00.000Z", "inside cwd"),
message("2026-05-15T11:00:00.000Z", "outside cwd", { cwd: "/other/project" }),
]);
const result = searchSessionAnchors("cwd: /work/project", { sessionsDir });
assert.strictEqual(result.success, true);
assert.strictEqual(result.ranges.length, 1);
assert.strictEqual(result.ranges[0].startLine, 1);
assert.strictEqual(result.ranges[0].reason, "cwd");
});
it("carries the real session event id onto matching message ranges", () => {
const sessionsDir = makeSessionsDir();
writeJsonl("session.jsonl", [
{ type: "session", version: 1, id: "session-real", timestamp: "2026-05-15T09:00:00.000Z", cwd: "/work/project" },
{ type: "message", id: "message-1", parentId: "session-real", timestamp: "2026-05-15T10:00:00.000Z", message: { role: "user", content: "needle" } },
]);
const result = searchSessionAnchors("any:\n- needle", { sessionsDir });
assert.strictEqual(result.success, true);
assert.strictEqual(result.ranges.length, 1);
assert.strictEqual(result.ranges[0].sessionId, "session-real");
assert.strictEqual(result.ranges[0].startLine, 2);
});
it("does not match metadata fields as text", () => {
const sessionsDir = makeSessionsDir();
writeJsonl("session.jsonl", [
message("2026-05-15T10:00:00.000Z", "actual content"),
]);
const result = searchSessionAnchors("any:\n- /work/project", { sessionsDir });
assert.strictEqual(result.success, true);
assert.strictEqual(result.ranges.length, 0);
});
it("returns contiguous timestamped ranges for time-window-only queries", () => {
const sessionsDir = makeSessionsDir();
const filePath = writeJsonl("session.jsonl", [
message("2026-05-14T12:00:00.000Z", "before"),
message("2026-05-15T10:00:00.000Z", "inside one"),
message("2026-05-15T11:00:00.000Z", "inside two"),
message("2026-05-16T12:00:00.000Z", "after"),
]);
const result = searchSessionAnchors("from: 2026-05-15\nto: 2026-05-15", { sessionsDir });
assert.strictEqual(result.success, true);
assert.deepStrictEqual(result.ranges.map((range) => ({ path: range.path, startLine: range.startLine, endLine: range.endLine })), [
{ path: filePath, startLine: 2, endLine: 3 },
]);
assert.strictEqual(result.ranges[0].startTime, "2026-05-15T10:00:00.000Z");
assert.strictEqual(result.ranges[0].endTime, "2026-05-15T11:00:00.000Z");
});
it("matches all and any terms as case-insensitive literal substrings", () => {
const sessionsDir = makeSessionsDir();
writeJsonl("session.jsonl", [
message("2026-05-15T10:00:00.000Z", "Alpha beta phrase"),
message("2026-05-15T11:00:00.000Z", "Contains GAMMA only"),
message("2026-05-15T12:00:00.000Z", "nothing useful"),
]);
const allResult = searchSessionAnchors("all:\n- alpha beta", { sessionsDir });
assert.strictEqual(allResult.success, true);
assert.strictEqual(allResult.ranges.length, 1);
assert.strictEqual(allResult.ranges[0].startLine, 1);
assert.strictEqual(allResult.ranges[0].reason, "matched all: alpha beta");
const anyResult = searchSessionAnchors("any:\n- gamma\n- delta", { sessionsDir });
assert.strictEqual(anyResult.success, true);
assert.strictEqual(anyResult.ranges.length, 1);
assert.strictEqual(anyResult.ranges[0].startLine, 2);
assert.strictEqual(anyResult.ranges[0].reason, "matched any: gamma");
});
it("removes ranges containing exclude terms", () => {
const sessionsDir = makeSessionsDir();
writeJsonl("session.jsonl", [
message("2026-05-15T10:00:00.000Z", "alpha keep"),
message("2026-05-15T10:30:00.000Z", "separator"),
message("2026-05-15T11:00:00.000Z", "alpha secret drop"),
]);
const result = searchSessionAnchors("any:\n- alpha\nexclude:\n- secret", { sessionsDir });
assert.strictEqual(result.success, true);
assert.strictEqual(result.ranges.length, 1);
assert.strictEqual(result.ranges[0].startLine, 1);
});
it("returns single-line hits as startLine equal to endLine", () => {
const sessionsDir = makeSessionsDir();
writeJsonl("nested/session.jsonl", [
message("2026-05-15T10:00:00.000Z", "first"),
message("2026-05-15T11:00:00.000Z", "needle"),
]);
const result = searchSessionAnchors("any:\n- needle", { sessionsDir });
assert.strictEqual(result.success, true);
assert.strictEqual(result.ranges.length, 1);
assert.strictEqual(result.ranges[0].startLine, 2);
assert.strictEqual(result.ranges[0].endLine, 2);
});
it("fails on invalid JSON lines with path and line", () => {
const sessionsDir = makeSessionsDir();
const filePath = path.join(sessionsDir, "bad.jsonl");
fs.writeFileSync(filePath, `${JSON.stringify(message("2026-05-15T10:00:00.000Z", "ok"))}\n{bad json}\n`);
const result = searchSessionAnchors("from: 2026-05-15", { sessionsDir });
assert.strictEqual(result.success, false);
assert.match(result.message ?? "", new RegExp(`${filePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}:2`));
});
});