@modusensus/dsh-mneme 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -21
- package/docs/CONFIGURATION.md +45 -4
- package/docs/DIRECTORIES.md +114 -0
- package/docs/DISTILL.md +64 -0
- package/docs/DSH-GO.md +97 -0
- package/docs/ENTITIES.md +5 -3
- package/docs/FORGET.md +77 -0
- package/docs/KB.md +110 -0
- package/docs/SLEEP.md +28 -3
- package/docs/STORAGE.md +23 -0
- package/lib/api-standalone.js +4 -1
- package/lib/api.js +744 -26
- package/lib/attachment-text.js +122 -0
- package/lib/choro-command.js +9 -2
- package/lib/client.js +3752 -819
- package/lib/config.js +171 -2
- package/lib/continuity.js +7 -6
- package/lib/diary/auto.js +190 -0
- package/lib/diary/compose.js +440 -0
- package/lib/diary/dir.js +59 -0
- package/lib/diary/export.js +97 -0
- package/lib/diary/index.js +26 -0
- package/lib/diary/library.js +189 -0
- package/lib/diary/qq-inbox.js +142 -0
- package/lib/diary/rollover.js +61 -0
- package/lib/diary/session-provider.js +122 -0
- package/lib/document.js +9 -3
- package/lib/dream/clustering.js +6 -1
- package/lib/dream/decisions.js +56 -5
- package/lib/dream/sleep.js +616 -15
- package/lib/dream.js +45 -13
- package/lib/dsh-go/host.js +162 -0
- package/lib/dsh-go/panel.js +2091 -0
- package/lib/dsh-go/text.js +342 -0
- package/lib/dsh-go/views/common.js +68 -0
- package/lib/dsh-go/views/entity.js +48 -0
- package/lib/dsh-go/views/memory.js +198 -0
- package/lib/dsh-go/views/root.js +30 -0
- package/lib/dsh-go/views/settings.js +398 -0
- package/lib/dsh-go/views/status.js +51 -0
- package/lib/embedding.js +42 -8
- package/lib/entities/extractor.js +36 -7
- package/lib/hot-memory.js +8 -9
- package/lib/index.js +131 -5
- package/lib/inject.js +192 -19
- package/lib/kb/index.js +309 -0
- package/lib/kb/routes.js +220 -0
- package/lib/kb/tools.js +148 -0
- package/lib/lang.js +115 -4
- package/lib/peak-hours.js +57 -2
- package/lib/profile-facts.js +356 -0
- package/lib/semantic.js +347 -60
- package/lib/service.js +302 -70
- package/lib/settings.js +117 -14
- package/lib/store.js +830 -55
- package/lib/summarize.js +38 -11
- package/lib/tools.js +173 -25
- package/lib/user-settings.js +175 -23
- package/lib/vector-index.js +76 -6
- package/lib/wiki/cli-hint.js +90 -0
- package/lib/wiki/queries.js +264 -10
- package/lib/wiki/t1-jobs.js +232 -0
- package/lib/wiki/t1.js +7 -1
- package/package.json +25 -1
- package/scripts/sync-test-badge.mjs +1 -1
- package/scripts/test-count-sync.mjs +9 -5
- package/src/api-standalone.js +4 -1
- package/src/api.js +744 -26
- package/src/attachment-text.js +122 -0
- package/src/choro-command.js +9 -2
- package/src/config.js +171 -2
- package/src/continuity.js +7 -6
- package/src/diary/auto.js +190 -0
- package/src/diary/compose.js +440 -0
- package/src/diary/dir.js +59 -0
- package/src/diary/export.js +97 -0
- package/src/diary/index.js +26 -0
- package/src/diary/library.js +189 -0
- package/src/diary/qq-inbox.js +142 -0
- package/src/diary/rollover.js +61 -0
- package/src/diary/session-provider.js +122 -0
- package/src/document.js +9 -3
- package/src/dream/clustering.js +6 -1
- package/src/dream/decisions.js +56 -5
- package/src/dream/sleep.js +616 -15
- package/src/dream.js +45 -13
- package/src/dsh-go/host.js +162 -0
- package/src/dsh-go/panel.js +2091 -0
- package/src/dsh-go/text.js +342 -0
- package/src/dsh-go/views/common.js +68 -0
- package/src/dsh-go/views/entity.js +48 -0
- package/src/dsh-go/views/memory.js +198 -0
- package/src/dsh-go/views/root.js +30 -0
- package/src/dsh-go/views/settings.js +398 -0
- package/src/dsh-go/views/status.js +51 -0
- package/src/embedding.js +42 -8
- package/src/entities/extractor.js +36 -7
- package/src/hot-memory.js +8 -9
- package/src/index.js +131 -5
- package/src/inject.js +192 -19
- package/src/kb/index.js +309 -0
- package/src/kb/routes.js +220 -0
- package/src/kb/tools.js +148 -0
- package/src/lang.js +115 -4
- package/src/peak-hours.js +57 -2
- package/src/profile-facts.js +356 -0
- package/src/semantic.js +347 -60
- package/src/service.js +302 -70
- package/src/settings.js +117 -14
- package/src/store.js +830 -55
- package/src/summarize.js +38 -11
- package/src/tools.js +173 -25
- package/src/user-settings.js +175 -23
- package/src/vector-index.js +76 -6
- package/src/wiki/cli-hint.js +90 -0
- package/src/wiki/queries.js +264 -10
- package/src/wiki/t1-jobs.js +232 -0
- package/src/wiki/t1.js +7 -1
- package/test/api-routes-gaps.test.js +63 -0
- package/test/api.test.js +377 -4
- package/test/attachment-text.test.js +140 -0
- package/test/bm25-corpus-cache.test.js +145 -0
- package/test/capacity-warning.test.js +262 -0
- package/test/client.test.js +908 -138
- package/test/config.test.js +79 -0
- package/test/conflict-freeze.test.js +45 -1
- package/test/continuity.test.js +8 -1
- package/test/diary-library.test.js +187 -0
- package/test/diary.test.js +576 -0
- package/test/document.test.js +32 -0
- package/test/dream-maxtokens-fallback.test.js +3 -3
- package/test/dream.test.js +62 -1
- package/test/dsh-go-host.test.js +134 -0
- package/test/dsh-go-panel-actions.test.js +854 -0
- package/test/dsh-go-panel.test.js +251 -0
- package/test/embedding-cache-f32.test.js +276 -0
- package/test/embedding-ready.test.js +33 -3
- package/test/entities.test.js +73 -0
- package/test/forget-corrections.test.js +70 -0
- package/test/image-memory.test.js +171 -0
- package/test/inject-pin-explicit.test.js +300 -0
- package/test/inject-pin-vector-crowd.test.js +225 -0
- package/test/issue-617-step1-query-and-fusion.test.js +361 -0
- package/test/kb-api-wiring.test.js +128 -0
- package/test/kb-collections.test.js +272 -0
- package/test/kb-routes.test.js +379 -0
- package/test/mint-source.test.js +26 -0
- package/test/peak-hours-observability.test.js +70 -0
- package/test/persona-api.test.js +3 -3
- package/test/persona-by-session.test.js +568 -0
- package/test/persona-settings.test.js +38 -7
- package/test/profile-facts.test.js +492 -0
- package/test/reflection-update-gate.test.js +129 -0
- package/test/reindex-backfill.test.js +334 -0
- package/test/reindex-scheduler.test.js +369 -0
- package/test/scope-candidates.test.js +249 -0
- package/test/session-gate.test.js +1 -1
- package/test/sleep-action-set.test.js +14 -0
- package/test/sleep-heat.test.js +49 -0
- package/test/sleep.test.js +598 -0
- package/test/standalone-api.test.js +24 -0
- package/test/store.test.js +20 -0
- package/test/summarize.test.js +26 -0
- package/test/test-count-sync.test.js +14 -3
- package/test/tool-state-ignore.test.js +65 -0
- package/test/tools-dto-schema-matrix.test.js +23 -2
- package/test/tools.test.js +144 -7
- package/test/user-settings-segments.test.js +528 -0
- package/test/vector-index.test.js +120 -6
- package/test/wiki-api.test.js +476 -1
- package/test/wiki-cli-hint.test.js +118 -0
- package/test/wiki-queries.test.js +91 -1
- package/test/wiki-t1-jobs.test.js +125 -0
- package/test/wiki-tools.test.js +131 -76
package/test/store.test.js
CHANGED
|
@@ -108,6 +108,26 @@ test("count excludes forgotten by default, includeForgotten opts in", () => {
|
|
|
108
108
|
store.close();
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
+
test("countByType: same filters, no type clause — the distribution sums to count(undefined) (#597)", () => {
|
|
112
|
+
const store = openMemory();
|
|
113
|
+
const a = store.save({ type: "project", title: "t1", content: "c" });
|
|
114
|
+
store.save({ type: "project", title: "t2", content: "c" });
|
|
115
|
+
store.save({ type: "preference", title: "p", content: "c", importance: 5 });
|
|
116
|
+
store.setForget(a.id, true);
|
|
117
|
+
const counts = store.countByType();
|
|
118
|
+
assert.equal(counts.project, 1, "forgotten rows excluded (same口径 as count)");
|
|
119
|
+
assert.equal(counts.preference, 1);
|
|
120
|
+
assert.equal(
|
|
121
|
+
Object.keys(counts).reduce((n, k) => n + counts[k], 0),
|
|
122
|
+
store.count(),
|
|
123
|
+
"distribution sums to count(undefined)"
|
|
124
|
+
);
|
|
125
|
+
// 筛选同口径:带 minImportance 的分布合计 = 带同筛选的 count
|
|
126
|
+
const filtered = store.countByType({ minImportance: 5 });
|
|
127
|
+
assert.deepEqual(filtered, { preference: 1 }, "distribution honors the same filters as count");
|
|
128
|
+
store.close();
|
|
129
|
+
});
|
|
130
|
+
|
|
111
131
|
test("search matches title, content and tags", () => {
|
|
112
132
|
const store = openMemory();
|
|
113
133
|
store.save({ type: "project", title: "记忆插件", content: "c1", tags: [] });
|
package/test/summarize.test.js
CHANGED
|
@@ -386,6 +386,32 @@ test("#239-4 高峰时段解析:合法 / 多段 / 跨零点 / 星期前缀 /
|
|
|
386
386
|
}
|
|
387
387
|
});
|
|
388
388
|
|
|
389
|
+
// #424:中文输入法带出来的全角等价符号曾让整串解析成 null——null 的语义是
|
|
390
|
+
// 「未配置 = 不限制」,错峰闸门于是静默失效(用户写的全角逗号把省钱功能整个
|
|
391
|
+
// 关掉,且没有任何提示)。本用例锁两件事:
|
|
392
|
+
// ① 全角逗号/冒号/数字/波浪号(含 U+301C)与全角星期前缀都归一成半角照常解析,
|
|
393
|
+
// 判定与补跑(isInPeakWindow / nextOffPeakAt)走同一份解析、自动受益;
|
|
394
|
+
// ② 归一化只做等价映射不救错:值本身越界的写法(24:00)无论半角全角仍然非法,
|
|
395
|
+
// 防的是「归一化顺手放宽了数值边界」这类静默语义漂移。
|
|
396
|
+
test("#424 全角等价符号:归一后照常解析,不改变越界判定", () => {
|
|
397
|
+
assert.deepEqual(parsePeakSpec("00:00-03:00,04:00-06:00"), {
|
|
398
|
+
days: null,
|
|
399
|
+
windows: [{ start: 0, end: 180 }, { start: 240, end: 360 }]
|
|
400
|
+
}, "全角逗号切段");
|
|
401
|
+
assert.deepEqual(parsePeakSpec("00:00-03:00").windows, [{ start: 0, end: 180 }], "全角冒号");
|
|
402
|
+
assert.deepEqual(parsePeakSpec("00:00-03:00").windows, [{ start: 0, end: 180 }], "全角数字");
|
|
403
|
+
assert.deepEqual(parsePeakSpec("00:00~03:00").windows, [{ start: 0, end: 180 }], "全角波浪号当连接号");
|
|
404
|
+
assert.deepEqual(parsePeakSpec("00:00〜03:00").windows, [{ start: 0, end: 180 }], "U+301C 波浪号的另一半");
|
|
405
|
+
assert.deepEqual(parsePeakSpec("sat,sun 23:00-06:00").days, [6, 7], "整串全角:星期前缀一起归一");
|
|
406
|
+
// 判定与补跑与解析共用同一入口,全角串在两侧行为一致
|
|
407
|
+
const at = (h, m) => new Date(2026, 10, 10, h, m, 0);
|
|
408
|
+
assert.equal(isInPeakWindow(at(1, 0), "00:00~03:00"), true, "判定侧同一份解析");
|
|
409
|
+
assert.equal(nextOffPeakAt(at(1, 0), "00:00~03:00").getHours(), 3, "补跑时刻同一份解析");
|
|
410
|
+
// 归一化不救错:#424 的举例串本身还踩 24:00 越界,跨零点的正写是 04:00-00:00
|
|
411
|
+
assert.equal(parsePeakSpec("00:00-03:00,04:00-24:00"), null, "24:00 越界不因归一化变合法");
|
|
412
|
+
assert.equal(parsePeakSpec("24:00-06:00"), null, "全角形式不改变越界判定");
|
|
413
|
+
});
|
|
414
|
+
|
|
389
415
|
test("#239-4 高峰判定与补跑时刻(跨零点、右开区间)", () => {
|
|
390
416
|
const at = (h, m) => new Date(2026, 8, 19, h, m, 30);
|
|
391
417
|
assert.equal(isInPeakWindow(at(10, 0), "09:00-18:00"), true);
|
|
@@ -32,8 +32,8 @@ test("双 README 的测试数处处一致(徽章与所有注释同数)", ()
|
|
|
32
32
|
// 自动发版路径因此反倒成了漂移来源。
|
|
33
33
|
test("applyTestCount 覆盖徽章与全部四种注释形状(真实漂移样本)", () => {
|
|
34
34
|
const drifted = [
|
|
35
|
-
'<img src="https://img.shields.io/badge/tests-1431%
|
|
36
|
-
"[](https://github.com/slow-stack/mneme)",
|
|
37
37
|
"npm test # 1431 个测试",
|
|
38
38
|
"npm test # 运行 1431 个测试",
|
|
39
39
|
"test/ # 1431 个 node:test 测试(审计与三轴线压测不变量)",
|
|
@@ -47,7 +47,7 @@ test("applyTestCount 覆盖徽章与全部四种注释形状(真实漂移样
|
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
test("applyTestCount 幂等:同一个数重复施加不再产生变化", () => {
|
|
50
|
-
const once = applyTestCount("tests-9%
|
|
50
|
+
const once = applyTestCount("tests-9%20total-3E63DD\n# 9 个测试\n# 9 tests", 12);
|
|
51
51
|
assert.equal(applyTestCount(once, 12), once);
|
|
52
52
|
});
|
|
53
53
|
|
|
@@ -58,3 +58,14 @@ test("applyTestCount 不碰历史版本表里的「N 测试全绿」", () => {
|
|
|
58
58
|
const history = "| **v0.8.0** | 745 测试全绿 |\n| **v0.7.9** | 790 通过 |\n| v0.2.7 | 263 |";
|
|
59
59
|
assert.equal(applyTestCount(history, 1437), history);
|
|
60
60
|
});
|
|
61
|
+
// 徽章报的是套件总数(`ℹ tests N`),不是通过数:本地 Windows 未开开发者模式时有一条
|
|
62
|
+
// 符号链接用例被条件跳过,pass 数会比总数少 1。徽章字面量若退回 `%20passed`,读者会把它
|
|
63
|
+
// 读成「N 个用例都通过了」,而它其实只表示「跑了 N 个套件」——#599 评审指出这一处措辞
|
|
64
|
+
// 与实际口径不符。这条用例锁住那个词,改回 `passed` 立刻转红。
|
|
65
|
+
test("双 README 的徽章字面量报总数(total),不谎称全部通过(passed)", () => {
|
|
66
|
+
for (const f of READMES) {
|
|
67
|
+
const text = readFileSync(f, "utf8");
|
|
68
|
+
assert.ok(/tests-\d+%20total/.test(text), `${f} 徽章必须写 %20total:${text.match(/tests-\d+%20\w+/)}`);
|
|
69
|
+
assert.ok(!/tests-\d+%20passed/.test(text), `${f} 徽章不得退回 %20passed`);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// 锁「本地工具状态目录不得再进库」这条预防层,以及它配套的那条公开记档。
|
|
2
|
+
//
|
|
3
|
+
// 背景:2026-11-05 分支 feat/choro-panel-run-and-vectors 有一次粗粒度 git add 把 .mimosa/
|
|
4
|
+
// (本地扫描钩子的状态目录,22 个文件)吸进了功能提交。文件两个提交后删掉了,但 blob 留在历史里、
|
|
5
|
+
// 仓库是 PUBLIC,其中一条 hook-status 记着一台机器的绝对路径。删文件不是补偿——删只把暴露从
|
|
6
|
+
// 目录树挪进对象存储(详见 SECURITY.md 的 2026-11-05 条目)。所以真正的防线只有两层:
|
|
7
|
+
//
|
|
8
|
+
// 1. 忽略规则(本文件第 1 组用例)——两个 .gitignore 都必须有。仓库里有两个 .mimosa 前缀
|
|
9
|
+
// (仓库根与 dsh-mneme/),只写一处时另一处照样会被 add 进来;历史重写那次就栽在这里。
|
|
10
|
+
// 2. 忽略规则若被谁「顺手清掉」,本文件必须变红——否则下一次 add -A 又是同一个坑。
|
|
11
|
+
//
|
|
12
|
+
// 第 2 组用例守的是记档本身:SECURITY.md 要中英各一条(两份文档都在同一文件里,缺一边等于
|
|
13
|
+
// 一种语言的读者看不到这件事),且条目里不得出现任何本机绝对路径——一份记录泄露的文档
|
|
14
|
+
// 自己再泄露一次就是二次事故。
|
|
15
|
+
import test from "node:test";
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
|
|
21
|
+
const pkgRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
22
|
+
const repoRoot = path.dirname(pkgRoot);
|
|
23
|
+
|
|
24
|
+
// 两个前缀都必须覆盖:历史上漏的就是「只写了 dsh-mneme/.mimosa」。
|
|
25
|
+
const gitignores = [
|
|
26
|
+
{ label: "仓库根 .gitignore", file: path.join(repoRoot, ".gitignore") },
|
|
27
|
+
{ label: "dsh-mneme/.gitignore", file: path.join(pkgRoot, ".gitignore") },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
for (const { label, file } of gitignores) {
|
|
31
|
+
test(`${label} 忽略 .mimosa/(本地工具状态不进库)`, () => {
|
|
32
|
+
const lines = readFileSync(file, "utf8").split(/\r?\n/).map((l) => l.trim());
|
|
33
|
+
assert.ok(
|
|
34
|
+
lines.includes(".mimosa/"),
|
|
35
|
+
`${label} 缺少 .mimosa/ 忽略规则。少了它,下一次 git add -A 会把钩子状态目录再带进库——` +
|
|
36
|
+
"2026-11-05 已经发生过一次,且 blob 至今仍可从历史取到。两处都要写:本仓有两个 .mimosa 前缀。"
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test("SECURITY.md 有这次暴露的中英双份记档,且记档本身不含本机绝对路径", () => {
|
|
42
|
+
const doc = readFileSync(path.join(repoRoot, "SECURITY.md"), "utf8");
|
|
43
|
+
const headings = doc.split(/\r?\n/).filter((l) => l.startsWith("### 2026-11-05 — "));
|
|
44
|
+
assert.equal(
|
|
45
|
+
headings.length,
|
|
46
|
+
2,
|
|
47
|
+
`SECURITY.md 应有中英各一条「### 2026-11-05 — 」记档,实际 ${headings.length} 条。` +
|
|
48
|
+
"只写一边等于另一种语言的读者看不到这条已知暴露。"
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// 记档里点名的是「本机目录结构」这件事本身,不能反过来把路径抄进去:
|
|
52
|
+
// 一条记录泄露的条目自己再泄露一次,就是把事故重演一遍。
|
|
53
|
+
// 只扫记档那两段(每条从标题到下一个 `### `),别拿整篇文档去撞历史遗留的行。
|
|
54
|
+
const lines = doc.split(/\r?\n/);
|
|
55
|
+
const section = [];
|
|
56
|
+
let inside = false;
|
|
57
|
+
for (const l of lines) {
|
|
58
|
+
if (l.startsWith("### ")) inside = l.startsWith("### 2026-11-05 — ");
|
|
59
|
+
if (inside) section.push(l);
|
|
60
|
+
}
|
|
61
|
+
const leaked = section.filter((l) =>
|
|
62
|
+
/[A-Za-z]:\\Users\\/.test(l) || l.includes("/Users/") || l.includes("AppData")
|
|
63
|
+
);
|
|
64
|
+
assert.deepEqual(leaked, [], `记档不得出现本机绝对路径,实际命中:${leaked.join(" | ")}`);
|
|
65
|
+
});
|
|
@@ -37,6 +37,7 @@ const TOOL_NAMES = [
|
|
|
37
37
|
"memory_delete",
|
|
38
38
|
"memory_forget",
|
|
39
39
|
"memory_archive",
|
|
40
|
+
"memory_pin",
|
|
40
41
|
"memory_register_document",
|
|
41
42
|
"memory_runtime",
|
|
42
43
|
"memory_organize",
|
|
@@ -126,7 +127,7 @@ test("工具矩阵与 TOOL_NAMES 清单一致(新增工具必须补进矩阵
|
|
|
126
127
|
assert.deepEqual(Object.keys(byName).sort(), [...TOOL_NAMES].sort());
|
|
127
128
|
});
|
|
128
129
|
|
|
129
|
-
test("A 实跑:
|
|
130
|
+
test("A 实跑:13 个工具每个可安全触达的分支,真实返回 ⊆ 声明的 output schema", async () => {
|
|
130
131
|
const { store, service, byName } = setup({ documentMemoryEnabled: true, organizeEnabled: true });
|
|
131
132
|
const shapes = seedShapes(store);
|
|
132
133
|
const annotated = shapes[shapes.length - 1];
|
|
@@ -202,6 +203,26 @@ test("A 实跑:12 个工具每个可安全触达的分支,真实返回 ⊆
|
|
|
202
203
|
assert.equal((await drive(byName, "memory_archive", { id: archiveTarget.id, archived: false })).memory.archived, false);
|
|
203
204
|
note("memory_archive");
|
|
204
205
|
|
|
206
|
+
// --- memory_pin(#249 第二批):钉上 / 松开 / 幂等三条分支。
|
|
207
|
+
// 幂等单独驱动是因为回执要报 pinned:false 而不是报错,这条分支最容易在
|
|
208
|
+
// 「本来没钉也算成功」的语义上写错。
|
|
209
|
+
const pinTarget = store.save({ type: "constraint", title: "pin-me", content: "每轮都要在。", source: "test" });
|
|
210
|
+
const pinned = await drive(byName, "memory_pin", { id: pinTarget.id, note: "matrix" });
|
|
211
|
+
assert.equal(pinned.pinned, true);
|
|
212
|
+
assert.equal(pinned.memory_id, pinTarget.id);
|
|
213
|
+
assert.equal(pinned.note, "matrix");
|
|
214
|
+
// #593:pin 池由 pinnedInjectBudget 开关,本矩阵用的默认配置里它是 0(关闭),
|
|
215
|
+
// 所以钉上了也不会注入 —— 回执必须如实报 false。这里断 false 而不是 true,
|
|
216
|
+
// 锁的正是「返回成功不等于生效」那条口径。
|
|
217
|
+
assert.equal(pinned.injectable, false);
|
|
218
|
+
const unpinned = await drive(byName, "memory_pin", { id: pinTarget.id, unpin: true });
|
|
219
|
+
assert.equal(unpinned.pinned, false);
|
|
220
|
+
assert.equal(store.countPinned(), 0);
|
|
221
|
+
// 本来没钉时再松一次:返回 pinned:false,不抛错(幂等)。
|
|
222
|
+
assert.equal((await drive(byName, "memory_pin", { id: pinTarget.id, unpin: true })).pinned, false);
|
|
223
|
+
note("memory_pin");
|
|
224
|
+
|
|
225
|
+
|
|
205
226
|
// --- set_scope_state(#471):两个方向的分支——按下与松开各驱动一次 ---
|
|
206
227
|
const pressed = await drive(byName, "set_scope_state", { dimension: "workspace", scope_value: "proj-alpha", state: "paused" });
|
|
207
228
|
assert.equal(pressed.scope.state, "paused");
|
|
@@ -337,7 +358,7 @@ test("B' search / list / get 三个消费方共用同一份 item schema(#184
|
|
|
337
358
|
|
|
338
359
|
// 同清单比对那条:opt-in 工具在默认配置下不在注册表里,C 层按 TOOL_NAMES 取
|
|
339
360
|
// byName[name],所以必须用暴露闸全开的配置,否则新工具的 schema 结构从未被检查。
|
|
340
|
-
test("C 结构:全部
|
|
361
|
+
test("C 结构:全部 13 个工具的 output schema 闭合、required ⊆ properties、每项带 type", () => {
|
|
341
362
|
const { byName } = setup({ organizeEnabled: true });
|
|
342
363
|
for (const name of TOOL_NAMES) {
|
|
343
364
|
const schema = byName[name].output.schema;
|
package/test/tools.test.js
CHANGED
|
@@ -49,11 +49,148 @@ function walkSchema(node, path, problems) {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
// memory_pin 的 injectable 回执必须跟注入侧同一个出口判定(#249 第二批)。
|
|
53
|
+
//
|
|
54
|
+
// 这条单独立在 tools 侧而不是注入侧:注入侧早已锁了「暂停作用域的行不进块」,
|
|
55
|
+
// 但回执那句话是另一段代码,它可以照样撒谎——描述文案与 README 都写着
|
|
56
|
+
// 「遗忘 / 归档 / 暂停作用域都优先于 pin」,而 execute 里只算了 forgotten/archived
|
|
57
|
+
// 两个标志位,暂停作用域被漏掉。于是同一条行在「真会注入」与「被作用域挡住」
|
|
58
|
+
// 两种完全相反的处境下都报 injectable: true,正是「返回成功不等于生效」那一类。
|
|
59
|
+
//
|
|
60
|
+
// 三档一起锁,是因为 filterPaused 的出口语义是分档的(见 src/scope-state.js:215):
|
|
61
|
+
// enforce 开 → 真正剔掉,回执必须 false;
|
|
62
|
+
// 只开 enabled(观察档)→ 不拦任何东西,回执必须 true(此时报 false 才是假警报);
|
|
63
|
+
// 两个都关 → 同样不拦,回执 true。
|
|
64
|
+
function setupPinReceipt(config) {
|
|
65
|
+
// pin 预算从 config 读(src/service.js:1885),不是 injectCandidates 的入参——
|
|
66
|
+
// 传进调用参数会被静默忽略,那样这几条测试就测不到 pin 池。
|
|
67
|
+
const cfg = { pinnedInjectBudget: 5, ...config };
|
|
68
|
+
const store = createStore(":memory:");
|
|
69
|
+
// 注意 config 必须同时喂 service 与 tools:作用域闸在 service 侧构造,
|
|
70
|
+
// 而 memory_pin 的 execute 走的是 service 暴露的那个出口。
|
|
71
|
+
const service = createService({ store, mirror: null, config: cfg });
|
|
72
|
+
const registered = [];
|
|
73
|
+
const ctx = { tools: { register(def) { registered.push(def); return () => {}; } } };
|
|
74
|
+
createTools(ctx, service, cfg, undefined);
|
|
75
|
+
return { store, service, byName: Object.fromEntries(registered.map((d) => [d.name, d])) };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
test("memory_pin 的 injectable 如实报告暂停作用域(而不是只算遗忘与归档)", async () => {
|
|
79
|
+
const config = { scopeState: { enabled: true, enforce: true } };
|
|
80
|
+
const { store, service, byName } = setupPinReceipt(config);
|
|
81
|
+
const target = store.save({
|
|
82
|
+
type: "preference",
|
|
83
|
+
title: "暂停域里的偏好",
|
|
84
|
+
content: "这一路现在被按下了。",
|
|
85
|
+
workspace_scope: "proj-a",
|
|
86
|
+
importance: 5
|
|
87
|
+
});
|
|
88
|
+
await service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
|
|
89
|
+
|
|
90
|
+
const receipt = await byName.memory_pin.execute({ id: target.id }, undefined);
|
|
91
|
+
// 钉是钉上了(存储确实写了),但它在当前状态下不会被注入——两件事分开报。
|
|
92
|
+
assert.equal(receipt.pinned, true);
|
|
93
|
+
assert.equal(receipt.injectable, false, "暂停作用域里的行必须报 injectable: false");
|
|
94
|
+
|
|
95
|
+
// 回执与行为必须同向:真的注入一次,这条不许出现。
|
|
96
|
+
const pinnedStats = {};
|
|
97
|
+
const injected = service.injectCandidates({
|
|
98
|
+
query: "随便问个问题",
|
|
99
|
+
maxItems: 5,
|
|
100
|
+
pinnedStats
|
|
101
|
+
});
|
|
102
|
+
assert.equal(pinnedStats.source, "explicit", "这一条本身钉住了,口径应留在 explicit");
|
|
103
|
+
assert.ok(
|
|
104
|
+
!injected.some((m) => m.id === target.id),
|
|
105
|
+
"被暂停作用域挡住的行不该出现在注入块里(回执说 false,行为也必须一致)"
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("暂停作用域只开观察档(enforce 关)时,injectable 仍报 true", async () => {
|
|
110
|
+
// 观察档的语义是「记录本应被拦下多少,但一条都不拦」。此时那条行确实会进上下文,
|
|
111
|
+
// 所以回执必须报 true;报 false 会让用户以为它被挡了,比漏掉暂停作用域更难查。
|
|
112
|
+
const config = { scopeState: { enabled: true, enforce: false } };
|
|
113
|
+
const { store, service, byName } = setupPinReceipt(config);
|
|
114
|
+
const target = store.save({
|
|
115
|
+
type: "preference",
|
|
116
|
+
title: "观察档下的偏好",
|
|
117
|
+
content: "按下但未生效。",
|
|
118
|
+
workspace_scope: "proj-a",
|
|
119
|
+
importance: 5
|
|
120
|
+
});
|
|
121
|
+
await service.setScopeState({ dimension: "workspace", scope_value: "proj-a", state: "paused" });
|
|
122
|
+
|
|
123
|
+
const receipt = await byName.memory_pin.execute({ id: target.id }, undefined);
|
|
124
|
+
assert.equal(receipt.injectable, true, "观察档不拦任何东西,回执必须报 true");
|
|
125
|
+
|
|
126
|
+
const pinnedStats = {};
|
|
127
|
+
const injected = service.injectCandidates({
|
|
128
|
+
query: "随便问个问题",
|
|
129
|
+
maxItems: 5,
|
|
130
|
+
pinnedStats
|
|
131
|
+
});
|
|
132
|
+
assert.ok(
|
|
133
|
+
injected.some((m) => m.id === target.id),
|
|
134
|
+
"观察档下这条应当照常注入,回执与行为必须同向"
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("pinnedInjectBudget=0(默认,pin 池整体关闭)时 injectable 必须报 false", async () => {
|
|
139
|
+
// #593 遗留缺口:pin 池由 pinnedInjectBudget 开关(src/service.js:1885 的
|
|
140
|
+
// pinnedBudget > 0 才读表选路),默认 0。上一版只看遗忘/归档/暂停作用域,
|
|
141
|
+
// 预算为 0 的一条照样报 true——钉住成功但一条都不会被注入,正是「返回成功
|
|
142
|
+
// 不等于生效」。这里把默认档(不传任何 config)与显式 0 两档一起锁。
|
|
143
|
+
const { store, service, byName } = setupPinReceipt({ pinnedInjectBudget: 0 });
|
|
144
|
+
const target = store.save({ type: "preference", title: "预算关着", content: "x", importance: 5 });
|
|
145
|
+
const receipt = await byName.memory_pin.execute({ id: target.id }, undefined);
|
|
146
|
+
assert.equal(receipt.pinned, true, "钉的动作确实成功了,两件事分开报");
|
|
147
|
+
assert.equal(receipt.injectable, false, "预算 0 = pin 池关闭,此刻不会经 pin 通道注入,回执必须报 false");
|
|
148
|
+
|
|
149
|
+
// 这里不断言「注入序列里一定没有它」:预算 0 关掉的是 **pin 通道**,不是这条记忆
|
|
150
|
+
// 本身——它仍可能靠自己的重要性走常规候选路进块(既有用例
|
|
151
|
+
// inject-pin-explicit.test.js:176 锁的是「注入序列与改前逐字节一致」,不是「不出现」)。
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("autoInject=false(注入器未注册)时 injectable 必须报 false", async () => {
|
|
155
|
+
// #596 评审指出的第二处:autoInject 是 pin 通道的总闸,关着时 src/index.js:617
|
|
156
|
+
// 根本不注册注入器——pin 段一行都跑不到,回执照样报 true 就是同一个「返回成功
|
|
157
|
+
// 不等于生效」。判据按「只有显式 false 才算关」写,故这里显式传 false。
|
|
158
|
+
const { store, byName } = setupPinReceipt({ autoInject: false });
|
|
159
|
+
const target = store.save({ type: "preference", title: "注入器没挂", content: "x", importance: 5 });
|
|
160
|
+
const receipt = await byName.memory_pin.execute({ id: target.id }, undefined);
|
|
161
|
+
assert.equal(receipt.pinned, true, "钉的动作照样成功,两件事分开报");
|
|
162
|
+
assert.equal(receipt.injectable, false, "autoInject 关 = 注入器不存在,此刻不可能经 pin 通道注入");
|
|
163
|
+
|
|
164
|
+
// 反例:不显式传 autoInject 时不能因为 config 里没这个键就误报 false
|
|
165
|
+
// (createTools 的测试直连场景拿不到已填 defaults 的 config)。
|
|
166
|
+
const on = setupPinReceipt({});
|
|
167
|
+
const t2 = on.store.save({ type: "preference", title: "默认开", content: "y", importance: 5 });
|
|
168
|
+
assert.equal(
|
|
169
|
+
(await on.byName.memory_pin.execute({ id: t2.id }, undefined)).injectable,
|
|
170
|
+
true,
|
|
171
|
+
"未显式关 autoInject 时必须报 true(预算 5 已满足)"
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("遗忘或归档的 pin:injectable 报 false(原有两项不能被这次改动弄丢)", async () => {
|
|
176
|
+
const { store, byName } = setupPinReceipt({});
|
|
177
|
+
const target = store.save({ type: "preference", title: "会被归档", content: "x", importance: 5 });
|
|
178
|
+
assert.equal((await byName.memory_pin.execute({ id: target.id }, undefined)).injectable, true);
|
|
179
|
+
store.setArchived(target.id, true);
|
|
180
|
+
const archived = await byName.memory_pin.execute({ id: target.id }, undefined);
|
|
181
|
+
assert.equal(archived.pinned, true);
|
|
182
|
+
assert.equal(archived.injectable, false);
|
|
183
|
+
store.setArchived(target.id, false);
|
|
184
|
+
store.setForget(target.id, true);
|
|
185
|
+
const forgotten = await byName.memory_pin.execute({ id: target.id }, undefined);
|
|
186
|
+
assert.equal(forgotten.injectable, false);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("registers twelve tools with correct names", () => {
|
|
53
190
|
const { registered } = setup();
|
|
54
191
|
const names = registered.map((t) => t.name).sort();
|
|
55
192
|
// #230 新增 memory_register_document(agent 产长文档的指针行铸造口)。
|
|
56
|
-
assert.deepEqual(names, ["memory_archive", "memory_delete", "memory_forget", "memory_get", "memory_list", "memory_register_document", "memory_runtime", "memory_save", "memory_search", "memory_update", "set_scope_state"]);
|
|
193
|
+
assert.deepEqual(names, ["memory_archive", "memory_delete", "memory_forget", "memory_get", "memory_list", "memory_pin", "memory_register_document", "memory_runtime", "memory_save", "memory_search", "memory_update", "set_scope_state"]);
|
|
57
194
|
});
|
|
58
195
|
|
|
59
196
|
// v0.8.5 tool-exposure gate: disabling a tool hides it from the model entirely
|
|
@@ -62,19 +199,19 @@ test("registers eleven tools with correct names", () => {
|
|
|
62
199
|
test("disableMemorySearch hides only memory_search", () => {
|
|
63
200
|
const { registered } = setup({}, { disableMemorySearch: true });
|
|
64
201
|
const names = registered.map((t) => t.name).sort();
|
|
65
|
-
assert.deepEqual(names, ["memory_archive", "memory_delete", "memory_forget", "memory_get", "memory_list", "memory_register_document", "memory_runtime", "memory_save", "memory_update", "set_scope_state"]);
|
|
202
|
+
assert.deepEqual(names, ["memory_archive", "memory_delete", "memory_forget", "memory_get", "memory_list", "memory_pin", "memory_register_document", "memory_runtime", "memory_save", "memory_update", "set_scope_state"]);
|
|
66
203
|
});
|
|
67
204
|
|
|
68
205
|
test("disableMemoryArchive hides only memory_archive", () => {
|
|
69
206
|
const { registered } = setup({}, { disableMemoryArchive: true });
|
|
70
207
|
const names = registered.map((t) => t.name).sort();
|
|
71
|
-
assert.deepEqual(names, ["memory_delete", "memory_forget", "memory_get", "memory_list", "memory_register_document", "memory_runtime", "memory_save", "memory_search", "memory_update", "set_scope_state"]);
|
|
208
|
+
assert.deepEqual(names, ["memory_delete", "memory_forget", "memory_get", "memory_list", "memory_pin", "memory_register_document", "memory_runtime", "memory_save", "memory_search", "memory_update", "set_scope_state"]);
|
|
72
209
|
});
|
|
73
210
|
|
|
74
211
|
test("both disable flags hide both tools", () => {
|
|
75
212
|
const { registered } = setup({}, { disableMemorySearch: true, disableMemoryArchive: true });
|
|
76
213
|
const names = registered.map((t) => t.name).sort();
|
|
77
|
-
assert.deepEqual(names, ["memory_delete", "memory_forget", "memory_get", "memory_list", "memory_register_document", "memory_runtime", "memory_save", "memory_update", "set_scope_state"]);
|
|
214
|
+
assert.deepEqual(names, ["memory_delete", "memory_forget", "memory_get", "memory_list", "memory_pin", "memory_register_document", "memory_runtime", "memory_save", "memory_update", "set_scope_state"]);
|
|
78
215
|
});
|
|
79
216
|
|
|
80
217
|
// Description discipline: with all tools exposed, the descriptions must steer a
|
|
@@ -90,12 +227,12 @@ test("memory_search/archive descriptions carry tool-use discipline", () => {
|
|
|
90
227
|
|
|
91
228
|
test("compiled schemas pass the enforced DSH subset (defineTool projection)", () => {
|
|
92
229
|
const { registered } = setup();
|
|
93
|
-
assert.equal(registered.length,
|
|
230
|
+
assert.equal(registered.length, 12);
|
|
94
231
|
// opt-in 暴露的工具在默认配置下不注册(#231 的 memory_organize),所以本用例还得
|
|
95
232
|
// 把暴露闸打开再走一遍——否则新工具的编译 schema 从来没被这条护栏看过,
|
|
96
233
|
// 「子集不支持的写法」要到运行时才炸。
|
|
97
234
|
const { registered: exposed } = setup(undefined, { organizeEnabled: true });
|
|
98
|
-
assert.equal(exposed.length,
|
|
235
|
+
assert.equal(exposed.length, 13, "organizeEnabled 只多带一个工具,不许顺手改在场的别的东西");
|
|
99
236
|
for (const tool of [...registered, ...exposed]) {
|
|
100
237
|
assertSupportedJsonSchema(tool.parameters);
|
|
101
238
|
assertSupportedJsonSchema(tool.output.schema);
|