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。
585 lines
26 KiB
TypeScript
585 lines
26 KiB
TypeScript
import { describe, it, afterEach } from "node:test";
|
|
import assert from "node:assert";
|
|
import * as fs from "node:fs";
|
|
import * as path from "node:path";
|
|
import * as os from "node:os";
|
|
import { loadConfig } from "../src/config.js";
|
|
import { AGENT_ROOT } from "../src/paths.js";
|
|
|
|
const TEST_CONFIG_PATH = path.join(os.tmpdir(), `hermes-memory-config-test-${process.pid}.json`);
|
|
|
|
afterEach(() => {
|
|
fs.rmSync(TEST_CONFIG_PATH, { force: true });
|
|
});
|
|
|
|
describe("loadConfig", () => {
|
|
it("returns defaults when no config file exists", () => {
|
|
const config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.memoryMode, "policy-only");
|
|
assert.strictEqual(config.lazyInitialization, false);
|
|
assert.strictEqual(config.memoryPolicyStyle, "full");
|
|
assert.strictEqual(config.memoryPolicyCustomText, undefined);
|
|
assert.strictEqual(config.memoryCharLimit, 5000);
|
|
assert.strictEqual(config.userCharLimit, 5000);
|
|
assert.strictEqual(config.nudgeInterval, 10);
|
|
assert.strictEqual(config.reviewRecentMessages, 0);
|
|
assert.strictEqual(config.reviewEnabled, true);
|
|
assert.strictEqual(config.reviewTransport, "direct");
|
|
assert.strictEqual(config.flushOnCompact, true);
|
|
assert.strictEqual(config.flushOnShutdown, true);
|
|
assert.strictEqual(config.flushMinTurns, 6);
|
|
assert.strictEqual(config.flushRecentMessages, 0);
|
|
assert.strictEqual(config.flushCompactTimeoutMs, 60000);
|
|
assert.strictEqual(config.memoryOverflowStrategy, "auto-consolidate");
|
|
assert.strictEqual(config.autoConsolidate, true);
|
|
assert.strictEqual(config.consolidationTimeoutMs, 180000);
|
|
assert.strictEqual(config.overflowGraceMs, 180000);
|
|
assert.strictEqual(config.autoConsolidationWarnOnFailure, true);
|
|
assert.strictEqual(config.failureInjectionEnabled, true);
|
|
assert.strictEqual(config.failureInjectionMaxAgeDays, 7);
|
|
assert.strictEqual(config.failureInjectionMaxEntries, 5);
|
|
assert.strictEqual(config.projectsMemoryDir, "projects-memory");
|
|
assert.deepStrictEqual(config.sessionSearch, { variant: "legacy" });
|
|
assert.strictEqual(config.llmModelOverride, undefined);
|
|
assert.strictEqual(config.llmThinkingOverride, undefined);
|
|
assert.strictEqual(config.standingInstructionsEnabled, true);
|
|
// Retention is disabled by default so existing history is never silently
|
|
// deleted; a positive value opts in, 0/omitted disables.
|
|
assert.strictEqual(config.sessionRetentionDays, 0);
|
|
assert.strictEqual(config.quickCheckOnOpen, true);
|
|
});
|
|
|
|
it("honors consolidationChunkChars", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({ consolidationChunkChars: 6000 }));
|
|
assert.strictEqual(loadConfig(TEST_CONFIG_PATH).consolidationChunkChars, 6000);
|
|
});
|
|
|
|
it("ignores out-of-range consolidationChunkChars values", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({ consolidationChunkChars: 100 }));
|
|
assert.strictEqual(
|
|
loadConfig(TEST_CONFIG_PATH).consolidationChunkChars,
|
|
4000,
|
|
"below the 500-char floor falls back to the default",
|
|
);
|
|
});
|
|
|
|
it("honors a configured consolidationTimeoutMs, warning only when it is below the default", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
const warnings: string[] = [];
|
|
const originalWarn = console.warn;
|
|
console.warn = (message?: unknown) => { warnings.push(String(message)); };
|
|
|
|
try {
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({ consolidationTimeoutMs: 300000 }));
|
|
assert.strictEqual(loadConfig(TEST_CONFIG_PATH).consolidationTimeoutMs, 300000);
|
|
assert.deepStrictEqual(warnings, [], "a value above the default should not warn");
|
|
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({ consolidationTimeoutMs: 60000 }));
|
|
assert.strictEqual(
|
|
loadConfig(TEST_CONFIG_PATH).consolidationTimeoutMs,
|
|
60000,
|
|
"a lower configured value must be honored, not clamped",
|
|
);
|
|
assert.strictEqual(warnings.length, 1, "a sub-default value should warn once");
|
|
assert.match(warnings[0], /60000ms.*below the 180000ms default/);
|
|
} finally {
|
|
console.warn = originalWarn;
|
|
}
|
|
});
|
|
|
|
it("honors a configured flushCompactTimeoutMs, warning only when it is below the default", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
const warnings: string[] = [];
|
|
const originalWarn = console.warn;
|
|
console.warn = (message?: unknown) => { warnings.push(String(message)); };
|
|
|
|
try {
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({ flushCompactTimeoutMs: 90000 }));
|
|
assert.strictEqual(loadConfig(TEST_CONFIG_PATH).flushCompactTimeoutMs, 90000);
|
|
assert.deepStrictEqual(warnings, [], "a value above the default should not warn");
|
|
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({ flushCompactTimeoutMs: 30000 }));
|
|
assert.strictEqual(
|
|
loadConfig(TEST_CONFIG_PATH).flushCompactTimeoutMs,
|
|
30000,
|
|
"a lower configured value must be honored, not clamped",
|
|
);
|
|
assert.strictEqual(warnings.length, 1, "a sub-default value should warn once");
|
|
assert.match(warnings[0], /30000ms.*below the 60000ms default/);
|
|
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({ flushCompactTimeoutMs: 0 }));
|
|
assert.strictEqual(
|
|
loadConfig(TEST_CONFIG_PATH).flushCompactTimeoutMs,
|
|
0,
|
|
"the disable sentinel must be honored",
|
|
);
|
|
assert.strictEqual(warnings.length, 1, "disabling the flush is not a too-low timeout");
|
|
} finally {
|
|
console.warn = originalWarn;
|
|
}
|
|
});
|
|
|
|
it("ignores non-finite flushCompactTimeoutMs values and keeps the default", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
const warnings: string[] = [];
|
|
const originalWarn = console.warn;
|
|
console.warn = (message?: unknown) => { warnings.push(String(message)); };
|
|
|
|
try {
|
|
// Written raw: JSON.stringify turns Infinity into null, so an object
|
|
// literal would test the string branch instead of the finite guard.
|
|
fs.writeFileSync(TEST_CONFIG_PATH, '{"flushCompactTimeoutMs": 1e999}');
|
|
assert.strictEqual(loadConfig(TEST_CONFIG_PATH).flushCompactTimeoutMs, 60000);
|
|
assert.deepStrictEqual(warnings, [], "a non-finite value is treated as absent, not warned");
|
|
} finally {
|
|
console.warn = originalWarn;
|
|
}
|
|
});
|
|
|
|
|
|
it("overrides defaults when config file exists", () => {
|
|
// Write a config file
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({
|
|
memoryCharLimit: 3000,
|
|
memoryMode: "legacy-inject",
|
|
memoryPolicyStyle: "custom",
|
|
memoryPolicyCustomText: "<memory-policy>Custom</memory-policy>",
|
|
nudgeInterval: 15,
|
|
reviewRecentMessages: 25,
|
|
flushRecentMessages: 40,
|
|
failureInjectionEnabled: false,
|
|
failureInjectionMaxAgeDays: 30,
|
|
failureInjectionMaxEntries: 2,
|
|
projectsMemoryDir: "my-memory",
|
|
llmModelOverride: " openrouter/deepseek/deepseek-v4-flash ",
|
|
llmThinkingOverride: "minimal",
|
|
autoConsolidationWarnOnFailure: false,
|
|
quickCheckOnOpen: false,
|
|
}));
|
|
const config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.memoryMode, "legacy-inject");
|
|
assert.strictEqual(config.memoryPolicyStyle, "custom");
|
|
assert.strictEqual(config.memoryPolicyCustomText, "<memory-policy>Custom</memory-policy>");
|
|
assert.strictEqual(config.memoryCharLimit, 3000);
|
|
assert.strictEqual(config.nudgeInterval, 15);
|
|
assert.strictEqual(config.reviewRecentMessages, 25);
|
|
assert.strictEqual(config.flushRecentMessages, 40);
|
|
assert.strictEqual(config.failureInjectionEnabled, false);
|
|
assert.strictEqual(config.autoConsolidationWarnOnFailure, false);
|
|
assert.strictEqual(config.failureInjectionMaxAgeDays, 30);
|
|
assert.strictEqual(config.failureInjectionMaxEntries, 2);
|
|
assert.strictEqual(config.projectsMemoryDir, "my-memory");
|
|
assert.strictEqual(config.llmModelOverride, "openrouter/deepseek/deepseek-v4-flash");
|
|
assert.strictEqual(config.llmThinkingOverride, "minimal");
|
|
assert.strictEqual(config.quickCheckOnOpen, false);
|
|
// Unset values use defaults
|
|
assert.strictEqual(config.userCharLimit, 5000);
|
|
assert.strictEqual(config.reviewEnabled, true);
|
|
});
|
|
|
|
it("only accepts boolean lazyInitialization overrides", () => {
|
|
for (const value of [true, false, "true", 1, null]) {
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({ lazyInitialization: value }));
|
|
assert.strictEqual(loadConfig(TEST_CONFIG_PATH).lazyInitialization, value === true);
|
|
}
|
|
});
|
|
|
|
it("only accepts boolean quickCheckOnOpen overrides", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({ quickCheckOnOpen: "false" }));
|
|
assert.strictEqual(loadConfig(TEST_CONFIG_PATH).quickCheckOnOpen, true);
|
|
});
|
|
|
|
it("merges array-form override tails with explicit llmFallbackModels", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({
|
|
llmModelOverride: ["p1/m1", "p2/m2"],
|
|
llmFallbackModels: ["p3/m3"],
|
|
}));
|
|
const config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.llmModelOverride, "p1/m1");
|
|
assert.deepStrictEqual(config.llmFallbackModels, ["p2/m2", "p3/m3"]);
|
|
});
|
|
|
|
|
|
it("parses sessionRetentionDays as opt-in, accepting explicit 0 to disable", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
|
|
// Positive value opts in to retention pruning.
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({ sessionRetentionDays: 45 }));
|
|
assert.strictEqual(loadConfig(TEST_CONFIG_PATH).sessionRetentionDays, 45);
|
|
|
|
// Explicit 0 disables retention (even after a prior positive value).
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({ sessionRetentionDays: 0 }));
|
|
assert.strictEqual(loadConfig(TEST_CONFIG_PATH).sessionRetentionDays, 0);
|
|
|
|
// Negative / non-numeric values are ignored, keeping the (disabled) default.
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({ sessionRetentionDays: -5 }));
|
|
assert.strictEqual(loadConfig(TEST_CONFIG_PATH).sessionRetentionDays, 0);
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({ sessionRetentionDays: "30" }));
|
|
assert.strictEqual(loadConfig(TEST_CONFIG_PATH).sessionRetentionDays, 0);
|
|
});
|
|
|
|
it("handles partial config (missing keys use defaults)", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({ reviewEnabled: false }));
|
|
const config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.reviewEnabled, false);
|
|
assert.strictEqual(config.memoryMode, "policy-only");
|
|
assert.strictEqual(config.memoryPolicyStyle, "full");
|
|
assert.strictEqual(config.memoryCharLimit, 5000); // default
|
|
assert.strictEqual(config.reviewRecentMessages, 0);
|
|
assert.strictEqual(config.flushRecentMessages, 0);
|
|
assert.strictEqual(config.failureInjectionEnabled, true);
|
|
assert.strictEqual(config.failureInjectionMaxAgeDays, 7);
|
|
assert.strictEqual(config.failureInjectionMaxEntries, 5);
|
|
assert.strictEqual(config.projectsMemoryDir, "projects-memory");
|
|
assert.strictEqual(config.llmModelOverride, undefined);
|
|
assert.strictEqual(config.llmThinkingOverride, undefined);
|
|
});
|
|
|
|
it("loads, trims, and deduplicates child extension paths", () => {
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({
|
|
childExtensionPaths: [" /tmp/auth-adapter.ts ", "/tmp/auth-adapter.ts", "/tmp/other-adapter.ts"],
|
|
}));
|
|
|
|
const config = loadConfig(TEST_CONFIG_PATH);
|
|
|
|
assert.deepStrictEqual(config.childExtensionPaths, ["/tmp/auth-adapter.ts", "/tmp/other-adapter.ts"]);
|
|
});
|
|
|
|
it("ignores invalid child extension path configuration", () => {
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({ childExtensionPaths: ["/tmp/auth.ts", 42] }));
|
|
assert.strictEqual(loadConfig(TEST_CONFIG_PATH).childExtensionPaths, undefined);
|
|
});
|
|
|
|
it("expands ~/ memoryDir into an absolute home path", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({
|
|
memoryDir: "~/.pi/agent/pi-hermes-memory",
|
|
}));
|
|
|
|
const config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.memoryDir, path.join(os.homedir(), ".pi", "agent", "pi-hermes-memory"));
|
|
});
|
|
|
|
it("resolves relative memoryDir values against the agent root instead of cwd", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({
|
|
memoryDir: "custom-memory-root",
|
|
}));
|
|
|
|
const config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.memoryDir, path.join(AGENT_ROOT, "custom-memory-root"));
|
|
});
|
|
|
|
it("normalizes projectsMemoryDir inside the agent root and ignores unsafe values", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({
|
|
projectsMemoryDir: ` ${path.join(AGENT_ROOT, "team-projects")}/ `,
|
|
}));
|
|
let config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.projectsMemoryDir, "team-projects");
|
|
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({
|
|
projectsMemoryDir: "../escape",
|
|
}));
|
|
config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.projectsMemoryDir, "projects-memory");
|
|
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({
|
|
projectsMemoryDir: "team/projects-memory",
|
|
}));
|
|
config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.projectsMemoryDir, "projects-memory");
|
|
});
|
|
|
|
it("handles partial config with all boolean overrides", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({
|
|
reviewEnabled: false,
|
|
flushOnCompact: false,
|
|
flushOnShutdown: false,
|
|
flushMinTurns: 20,
|
|
}));
|
|
const config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.reviewEnabled, false);
|
|
assert.strictEqual(config.flushOnCompact, false);
|
|
assert.strictEqual(config.flushOnShutdown, false);
|
|
assert.strictEqual(config.flushMinTurns, 20);
|
|
assert.strictEqual(config.memoryCharLimit, 5000);
|
|
assert.strictEqual(config.userCharLimit, 5000);
|
|
assert.strictEqual(config.nudgeInterval, 10);
|
|
});
|
|
|
|
it("accepts review and flush recent-message limits independently", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({
|
|
reviewRecentMessages: 12,
|
|
flushRecentMessages: 34,
|
|
}));
|
|
const config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.reviewRecentMessages, 12);
|
|
assert.strictEqual(config.flushRecentMessages, 34);
|
|
});
|
|
|
|
it("ignores invalid recent-message limits", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({
|
|
reviewRecentMessages: -1,
|
|
flushRecentMessages: "5",
|
|
}));
|
|
const config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.reviewRecentMessages, 0);
|
|
assert.strictEqual(config.flushRecentMessages, 0);
|
|
});
|
|
|
|
it("handles empty file gracefully (falls back to defaults)", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
fs.writeFileSync(TEST_CONFIG_PATH, "");
|
|
const config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.reviewEnabled, true);
|
|
});
|
|
|
|
it("handles malformed JSON (falls back to defaults)", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
fs.writeFileSync(TEST_CONFIG_PATH, "{ bad json }");
|
|
const config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.memoryCharLimit, 5000);
|
|
assert.strictEqual(config.reviewEnabled, true);
|
|
});
|
|
|
|
it("ignores unknown keys in config file", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({
|
|
unknownKey: "value",
|
|
anotherKey: 123,
|
|
memoryCharLimit: 1000,
|
|
}));
|
|
const config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.memoryCharLimit, 1000);
|
|
assert.strictEqual(config.memoryMode, "policy-only");
|
|
assert.strictEqual(config.reviewEnabled, true);
|
|
});
|
|
|
|
it("ignores invalid memoryMode values", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({
|
|
memoryMode: "invalid",
|
|
}));
|
|
const config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.memoryMode, "policy-only");
|
|
});
|
|
|
|
it("accepts valid memoryPolicyStyle values", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
|
|
for (const style of ["full", "compact", "custom", "none"] as const) {
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({ memoryPolicyStyle: style }));
|
|
const config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.memoryPolicyStyle, style);
|
|
}
|
|
});
|
|
|
|
it("ignores invalid memoryPolicyStyle values", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({
|
|
memoryPolicyStyle: "invalid",
|
|
}));
|
|
const config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.memoryPolicyStyle, "full");
|
|
});
|
|
|
|
it("accepts string memoryPolicyCustomText and ignores non-string values", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({
|
|
memoryPolicyCustomText: "custom policy",
|
|
}));
|
|
let config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.memoryPolicyCustomText, "custom policy");
|
|
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({
|
|
memoryPolicyCustomText: 123,
|
|
}));
|
|
config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.memoryPolicyCustomText, undefined);
|
|
});
|
|
|
|
it("accepts valid memoryOverflowStrategy values", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
|
|
for (const policy of ["auto-consolidate", "reject", "fifo-evict"] as const) {
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({ memoryOverflowStrategy: policy }));
|
|
const config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.memoryOverflowStrategy, policy);
|
|
assert.strictEqual(config.autoConsolidate, policy === "auto-consolidate");
|
|
}
|
|
});
|
|
|
|
it("accepts valid sessionSearch variants", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
|
|
for (const variant of ["legacy", "anchors"] as const) {
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({ sessionSearch: { variant } }));
|
|
const config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.deepStrictEqual(config.sessionSearch, { variant });
|
|
}
|
|
});
|
|
|
|
it("accepts valid reviewTransport values and ignores invalid ones", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
|
|
for (const transport of ["direct", "subprocess"] as const) {
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({ reviewTransport: transport }));
|
|
const config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.reviewTransport, transport);
|
|
}
|
|
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({ reviewTransport: "branch" }));
|
|
const invalid = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(invalid.reviewTransport, "direct");
|
|
});
|
|
|
|
it("accepts valid llmThinkingOverride values and ignores invalid ones", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
|
|
for (const level of ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const) {
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({ llmThinkingOverride: level }));
|
|
const config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.llmThinkingOverride, level);
|
|
}
|
|
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({ llmThinkingOverride: "ultra" }));
|
|
const invalid = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(invalid.llmThinkingOverride, undefined);
|
|
});
|
|
|
|
it("ignores blank llmModelOverride values", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({ llmModelOverride: " " }));
|
|
const config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.llmModelOverride, undefined);
|
|
});
|
|
|
|
it("ignores invalid sessionSearch variants", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({
|
|
sessionSearch: { variant: "invalid" },
|
|
}));
|
|
const config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.deepStrictEqual(config.sessionSearch, { variant: "legacy" });
|
|
});
|
|
|
|
it("ignores invalid memoryOverflowStrategy values", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({
|
|
memoryOverflowStrategy: "invalid",
|
|
}));
|
|
const config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.memoryOverflowStrategy, "auto-consolidate");
|
|
assert.strictEqual(config.autoConsolidate, true);
|
|
});
|
|
|
|
it("maps legacy autoConsolidate boolean to memoryOverflowStrategy when strategy is absent", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({ autoConsolidate: false }));
|
|
let config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.autoConsolidate, false);
|
|
assert.strictEqual(config.memoryOverflowStrategy, "reject");
|
|
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({ autoConsolidate: true }));
|
|
config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.autoConsolidate, true);
|
|
assert.strictEqual(config.memoryOverflowStrategy, "auto-consolidate");
|
|
});
|
|
|
|
it("lets explicit memoryOverflowStrategy override legacy autoConsolidate", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({
|
|
autoConsolidate: true,
|
|
memoryOverflowStrategy: "fifo-evict",
|
|
}));
|
|
let config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.memoryOverflowStrategy, "fifo-evict");
|
|
assert.strictEqual(config.autoConsolidate, false);
|
|
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({
|
|
autoConsolidate: false,
|
|
memoryOverflowStrategy: "auto-consolidate",
|
|
}));
|
|
config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.memoryOverflowStrategy, "auto-consolidate");
|
|
assert.strictEqual(config.autoConsolidate, true);
|
|
});
|
|
|
|
it("accepts correction pattern string arrays including empty arrays", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({
|
|
correctionStrongPatterns: ["^custom strong"],
|
|
correctionWeakPatterns: [],
|
|
correctionNegativePatterns: ["^custom negative"],
|
|
correctionDirectiveWords: ["shipit"],
|
|
}));
|
|
|
|
const config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.deepStrictEqual(config.correctionStrongPatterns, ["^custom strong"]);
|
|
assert.deepStrictEqual(config.correctionWeakPatterns, []);
|
|
assert.deepStrictEqual(config.correctionNegativePatterns, ["^custom negative"]);
|
|
assert.deepStrictEqual(config.correctionDirectiveWords, ["shipit"]);
|
|
});
|
|
|
|
it("ignores invalid correction pattern array values", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({
|
|
correctionStrongPatterns: "^custom strong",
|
|
correctionWeakPatterns: ["^custom weak", 123],
|
|
correctionNegativePatterns: [false],
|
|
correctionDirectiveWords: { word: "shipit" },
|
|
}));
|
|
|
|
const config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.correctionStrongPatterns, undefined);
|
|
assert.strictEqual(config.correctionWeakPatterns, undefined);
|
|
assert.strictEqual(config.correctionNegativePatterns, undefined);
|
|
assert.strictEqual(config.correctionDirectiveWords, undefined);
|
|
});
|
|
|
|
it("defaults usage-hit tracking and consolidation usage signals to enabled", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({}));
|
|
|
|
const config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.usageHitTrackingEnabled, true);
|
|
assert.strictEqual(config.consolidationUsageSignals, true);
|
|
});
|
|
|
|
it("accepts usageHitTrackingEnabled and consolidationUsageSignals kill-switches", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({
|
|
usageHitTrackingEnabled: false,
|
|
consolidationUsageSignals: false,
|
|
}));
|
|
|
|
const config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.usageHitTrackingEnabled, false);
|
|
assert.strictEqual(config.consolidationUsageSignals, false);
|
|
});
|
|
|
|
it("ignores non-boolean usage tracking values", () => {
|
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({
|
|
usageHitTrackingEnabled: "yes",
|
|
consolidationUsageSignals: 1,
|
|
}));
|
|
|
|
const config = loadConfig(TEST_CONFIG_PATH);
|
|
assert.strictEqual(config.usageHitTrackingEnabled, true);
|
|
assert.strictEqual(config.consolidationUsageSignals, true);
|
|
});
|
|
});
|