@modusensus/dsh-mneme 0.8.14 → 0.8.15

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.
Files changed (271) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +655 -654
  3. package/assets/icon.svg +3 -3
  4. package/bin/cli.mjs +689 -689
  5. package/bin/dsh-mneme-mcp.mjs +510 -510
  6. package/bin/dsh-mneme-serve.mjs +151 -151
  7. package/cordis.patch.yml +21 -21
  8. package/docs/CONFIGURATION.md +263 -0
  9. package/docs/CONTINUITY.md +124 -0
  10. package/docs/DAEMON.md +46 -0
  11. package/docs/ENTITIES.md +245 -0
  12. package/docs/LOCAL_MODEL.md +222 -0
  13. package/docs/SEMANTIC.md +256 -0
  14. package/docs/SLEEP.md +164 -0
  15. package/docs/STORAGE.md +89 -0
  16. package/lib/api-standalone.js +673 -673
  17. package/lib/api.js +1437 -1433
  18. package/lib/bootstrap.js +211 -211
  19. package/lib/client.js +5444 -5295
  20. package/lib/commands.js +95 -95
  21. package/lib/config.js +880 -843
  22. package/lib/content-hash.js +54 -54
  23. package/lib/continuity.js +220 -220
  24. package/lib/document-index.js +94 -94
  25. package/lib/document.js +303 -303
  26. package/lib/dream/clustering.js +118 -118
  27. package/lib/dream/decisions.js +674 -674
  28. package/lib/dream/narratives.js +44 -44
  29. package/lib/dream/sleep.js +819 -819
  30. package/lib/dream.js +1501 -1501
  31. package/lib/embedding.js +177 -177
  32. package/lib/entities/extractor.js +252 -252
  33. package/lib/graph/anchoring.js +179 -179
  34. package/lib/guide.js +74 -74
  35. package/lib/heat.js +144 -144
  36. package/lib/hot-memory.js +46 -46
  37. package/lib/index.js +535 -503
  38. package/lib/inject.js +464 -450
  39. package/lib/lang.js +248 -229
  40. package/lib/local-embedder.js +358 -358
  41. package/lib/maintenance.js +142 -142
  42. package/lib/mirror.js +330 -330
  43. package/lib/organize.js +450 -450
  44. package/lib/peak-hours.js +117 -117
  45. package/lib/placement.js +13 -0
  46. package/lib/pre-inject-gate.js +340 -0
  47. package/lib/quality-filter.js +126 -126
  48. package/lib/recall-stats.js +175 -175
  49. package/lib/reranker.js +269 -269
  50. package/lib/runtime/adopt.js +176 -176
  51. package/lib/runtime/closure.js +172 -172
  52. package/lib/runtime/download.js +247 -247
  53. package/lib/runtime/layout.js +265 -265
  54. package/lib/runtime/loader.js +252 -252
  55. package/lib/runtime/model-fetch.js +361 -361
  56. package/lib/runtime/provision.js +237 -237
  57. package/lib/runtime/tarball.js +130 -130
  58. package/lib/runtime/verify.js +223 -204
  59. package/lib/scope.js +146 -146
  60. package/lib/search/adaptive.js +60 -60
  61. package/lib/search/bm25.js +96 -96
  62. package/lib/semantic.js +229 -229
  63. package/lib/sensitive-scan.js +209 -209
  64. package/lib/serve.js +215 -215
  65. package/lib/service.js +2663 -2605
  66. package/lib/settings.js +517 -511
  67. package/lib/store.js +3007 -3007
  68. package/lib/summarize.js +891 -856
  69. package/lib/tools.js +743 -737
  70. package/lib/vector-index.js +110 -110
  71. package/lib/version-check.js +122 -122
  72. package/lib/write-admission.js +402 -402
  73. package/locale/en.json +6 -6
  74. package/locale/zh.json +6 -6
  75. package/package.json +118 -118
  76. package/runtime-manifest.json +526 -526
  77. package/scripts/benchmark-embed.js +201 -201
  78. package/scripts/benchmark-recall.js +249 -249
  79. package/scripts/benchmark-rerank.js +166 -166
  80. package/scripts/build-runtime-manifest.mjs +224 -224
  81. package/scripts/changelog-prep.mjs +36 -36
  82. package/scripts/check-sync.js +42 -42
  83. package/scripts/e2e-dsh.js +220 -220
  84. package/scripts/mneme-runtime.mjs +277 -277
  85. package/scripts/stress-dsh.js +255 -255
  86. package/scripts/sync-lib.js +47 -47
  87. package/scripts/sync-test-badge.mjs +69 -69
  88. package/scripts/test-count-sync.mjs +44 -44
  89. package/src/api-standalone.js +673 -673
  90. package/src/api.js +1437 -1433
  91. package/src/bootstrap.js +211 -211
  92. package/src/commands.js +95 -95
  93. package/src/config.js +880 -843
  94. package/src/content-hash.js +54 -54
  95. package/src/continuity.js +220 -220
  96. package/src/document-index.js +94 -94
  97. package/src/document.js +303 -303
  98. package/src/dream/clustering.js +118 -118
  99. package/src/dream/decisions.js +674 -674
  100. package/src/dream/narratives.js +44 -44
  101. package/src/dream/sleep.js +819 -819
  102. package/src/dream.js +1501 -1501
  103. package/src/embedding.js +177 -177
  104. package/src/entities/extractor.js +252 -252
  105. package/src/graph/anchoring.js +179 -179
  106. package/src/guide.js +74 -74
  107. package/src/heat.js +144 -144
  108. package/src/hot-memory.js +46 -46
  109. package/src/index.js +535 -503
  110. package/src/inject.js +464 -450
  111. package/src/lang.js +248 -229
  112. package/src/local-embedder.js +358 -358
  113. package/src/maintenance.js +142 -142
  114. package/src/mirror.js +330 -330
  115. package/src/organize.js +450 -450
  116. package/src/peak-hours.js +117 -117
  117. package/src/placement.js +13 -0
  118. package/src/pre-inject-gate.js +340 -0
  119. package/src/quality-filter.js +126 -126
  120. package/src/recall-stats.js +175 -175
  121. package/src/reranker.js +269 -269
  122. package/src/runtime/adopt.js +176 -176
  123. package/src/runtime/closure.js +172 -172
  124. package/src/runtime/download.js +247 -247
  125. package/src/runtime/layout.js +265 -265
  126. package/src/runtime/loader.js +252 -252
  127. package/src/runtime/model-fetch.js +361 -361
  128. package/src/runtime/provision.js +237 -237
  129. package/src/runtime/tarball.js +130 -130
  130. package/src/runtime/verify.js +223 -204
  131. package/src/scope.js +146 -146
  132. package/src/search/adaptive.js +60 -60
  133. package/src/search/bm25.js +96 -96
  134. package/src/semantic.js +229 -229
  135. package/src/sensitive-scan.js +209 -209
  136. package/src/serve.js +215 -215
  137. package/src/service.js +2663 -2605
  138. package/src/settings.js +517 -511
  139. package/src/store.js +3007 -3007
  140. package/src/summarize.js +891 -856
  141. package/src/tools.js +743 -737
  142. package/src/vector-index.js +110 -110
  143. package/src/version-check.js +122 -122
  144. package/src/write-admission.js +402 -402
  145. package/test/adaptive-inject-budget.test.js +85 -85
  146. package/test/api-routes-gaps.test.js +218 -218
  147. package/test/api.test.js +1702 -1690
  148. package/test/audit.test.js +448 -448
  149. package/test/baseline-persist.test.js +248 -248
  150. package/test/benchmark.test.js +94 -94
  151. package/test/bootstrap.test.js +220 -220
  152. package/test/changelog-prep.test.js +60 -60
  153. package/test/client.test.js +1230 -1169
  154. package/test/clustering.test.js +100 -100
  155. package/test/commands.test.js +106 -106
  156. package/test/config.test.js +66 -66
  157. package/test/conflict-freeze.test.js +300 -300
  158. package/test/content-hash.test.js +204 -204
  159. package/test/continuity.test.js +333 -333
  160. package/test/document-dir.test.js +233 -233
  161. package/test/document.test.js +671 -671
  162. package/test/dream-archive-guardrail.test.js +116 -116
  163. package/test/dream-candidate-hybrid.test.js +281 -281
  164. package/test/dream-coverage-degrade.test.js +86 -86
  165. package/test/dream-failure-backoff.test.js +173 -173
  166. package/test/dream-peak-hours.test.js +194 -194
  167. package/test/dream.test.js +1385 -1383
  168. package/test/embedding-ready.test.js +196 -196
  169. package/test/entities.test.js +627 -547
  170. package/test/entity-recall.test.js +179 -179
  171. package/test/epistemic.test.js +325 -325
  172. package/test/fnew-0112.test.js +311 -311
  173. package/test/fnew-03.test.js +422 -422
  174. package/test/graph-anchoring.test.js +184 -184
  175. package/test/graph-api.test.js +175 -175
  176. package/test/graph-inject-budget.test.js +103 -103
  177. package/test/graph-passive-confirm.test.js +91 -91
  178. package/test/graph-weight-evolution.test.js +104 -104
  179. package/test/heat.test.js +175 -175
  180. package/test/helpers/dream-mock.js +82 -82
  181. package/test/helpers/peer-worker.mjs +25 -25
  182. package/test/helpers/runtime-source.js +60 -60
  183. package/test/helpers/tar-builder.js +80 -80
  184. package/test/helpers/write-admission-samples.js +308 -308
  185. package/test/hot-memory.test.js +145 -145
  186. package/test/inject-content-cap.test.js +91 -91
  187. package/test/inject-first-round-bm25.test.js +89 -89
  188. package/test/inject-heat.test.js +77 -77
  189. package/test/inject-parent-gate.test.js +106 -106
  190. package/test/inject-pools.test.js +194 -194
  191. package/test/inject-recall.test.js +105 -105
  192. package/test/inject-rotation.test.js +161 -161
  193. package/test/inject.test.js +420 -420
  194. package/test/lang.test.js +87 -87
  195. package/test/lastrunat-persist.test.js +101 -101
  196. package/test/lib-smoke.test.js +96 -96
  197. package/test/llm-audit.test.js +598 -598
  198. package/test/llm-message-source.test.js +207 -207
  199. package/test/local-embedder.test.js +292 -292
  200. package/test/maintenance.test.js +391 -391
  201. package/test/mcp-stdio.test.js +385 -385
  202. package/test/mirror-dirty.test.js +424 -424
  203. package/test/mirror-edit-digest.test.js +187 -187
  204. package/test/mirror-generation.test.js +499 -499
  205. package/test/mirror-types.test.js +208 -208
  206. package/test/mirror.test.js +249 -249
  207. package/test/narrative-bars.test.js +162 -162
  208. package/test/organize.test.js +418 -418
  209. package/test/peer-blockers.test.js +193 -193
  210. package/test/placement.test.js +36 -0
  211. package/test/policy-epoch.test.js +259 -259
  212. package/test/pre-inject-gate.test.js +434 -0
  213. package/test/quality-filter.test.js +186 -186
  214. package/test/reasoning-effort.test.js +752 -752
  215. package/test/recall-anti-update.test.js +82 -82
  216. package/test/recall-evals.test.js +253 -253
  217. package/test/recall-layer.test.js +326 -326
  218. package/test/recall-stats.test.js +189 -189
  219. package/test/receipt-chain.test.js +451 -451
  220. package/test/reflection.test.js +226 -226
  221. package/test/reindex-backfill.test.js +62 -62
  222. package/test/reranker-single-logit.test.js +73 -73
  223. package/test/reranker.test.js +197 -197
  224. package/test/resident-status.test.js +83 -83
  225. package/test/runtime-adopt.test.js +206 -206
  226. package/test/runtime-cli.test.js +89 -89
  227. package/test/runtime-closure.test.js +193 -193
  228. package/test/runtime-download.test.js +302 -302
  229. package/test/runtime-layout.test.js +163 -163
  230. package/test/runtime-loader.test.js +255 -255
  231. package/test/runtime-manifest.test.js +159 -159
  232. package/test/runtime-model-fetch.test.js +348 -348
  233. package/test/runtime-provision.test.js +101 -101
  234. package/test/runtime-tarball.test.js +81 -81
  235. package/test/runtime-verify.test.js +319 -279
  236. package/test/scope-candidates.test.js +239 -239
  237. package/test/scope-explicit.test.js +452 -452
  238. package/test/scope-inject-soft.test.js +106 -106
  239. package/test/scope-retrieval.test.js +197 -197
  240. package/test/scope-storage.test.js +255 -255
  241. package/test/scope-strict.test.js +367 -367
  242. package/test/search-fusion.test.js +90 -90
  243. package/test/semantic.test.js +124 -124
  244. package/test/sensitive-scan.test.js +207 -207
  245. package/test/serve-bin.test.js +121 -121
  246. package/test/serve.test.js +196 -196
  247. package/test/service-search.test.js +174 -174
  248. package/test/service.test.js +405 -405
  249. package/test/settings.test.js +261 -261
  250. package/test/skip-invalid-restore.test.js +262 -262
  251. package/test/sleep-action-set.test.js +422 -422
  252. package/test/sleep-heat.test.js +126 -126
  253. package/test/sleep-scheduler-rearm.test.js +89 -89
  254. package/test/sleep.test.js +527 -527
  255. package/test/standalone-api.test.js +751 -751
  256. package/test/store-vector-cache.test.js +98 -98
  257. package/test/store.test.js +396 -396
  258. package/test/stress.test.js +209 -209
  259. package/test/summarize-occurred-at.test.js +127 -0
  260. package/test/summarize-reasoning-effort.test.js +189 -189
  261. package/test/summarize.test.js +1465 -1465
  262. package/test/supersede-state-tracking.test.js +394 -0
  263. package/test/test-count-sync.test.js +60 -60
  264. package/test/tools-dto-schema-matrix.test.js +325 -325
  265. package/test/tools-memory-get-schema.test.js +65 -65
  266. package/test/tools.test.js +428 -374
  267. package/test/updated-at-semantics.test.js +113 -113
  268. package/test/vector-index.test.js +221 -221
  269. package/test/version-check.test.js +152 -152
  270. package/test/webServer-optional.test.js +82 -82
  271. package/test/write-admission.test.js +668 -668
@@ -1,85 +1,85 @@
1
- // Issue #239(第 5 项):注入条数的查询自适应。
2
- // 判据只看查询本身(零成本,不做探针检索——先探针检索等于白付一次 fuseRecall),
3
- // 且只做**单向收缩**:模糊维持上限、确定收缩,绝不越过 maxInjectedItems——注入
4
- // 相关却带偏生成的内容可能比不注入更糟。
5
- import test from "node:test";
6
- import assert from "node:assert/strict";
7
- import { needsBroadRecall, adaptiveInjectBudget } from "../src/search/adaptive.js";
8
- import { createStore } from "../src/store.js";
9
- import { createService } from "../src/service.js";
10
- import { createInjector } from "../src/inject.js";
11
- import { createSettings } from "../src/settings.js";
12
-
13
- function injectorSetup(over = {}) {
14
- const store = createStore(":memory:");
15
- const service = createService({ store, mirror: null, config: {} });
16
- const settings = createSettings(store.db);
17
- const contexts = [];
18
- const ctx = { systemPrompt: { context(def) { contexts.push(def); return () => {}; } } };
19
- createInjector(ctx, service, settings, { maxInjectedItems: 4, importanceThreshold: 3, ...over });
20
- return { store, service, contexts };
21
- }
22
-
23
- // lastUserQuery 读的是 materialized 会话日志(DSH ≥0.1.2-rc 走 snapshotEvents)。
24
- function ctxWithQuery(text) {
25
- return {
26
- agent: {
27
- session: {
28
- events: [{ type: "user/message", data: { source: { kind: "user" }, content: [{ type: "text", text }] } }]
29
- }
30
- }
31
- };
32
- }
33
-
34
- function seed(service, n = 6) {
35
- for (let i = 0; i < n; i++) {
36
- service.saveWithDedupe({ type: "project", title: `项目${i}`, content: `内容${i}`, importance: 5 });
37
- }
38
- }
39
-
40
- const injectedCount = (text) => [0, 1, 2, 3, 4, 5].filter((i) => text.includes(`内容${i}`)).length;
41
-
42
- test("#239-5 宽召回判据:回指/时间线索与极短查询算模糊,其余算确定", () => {
43
- // 模糊:回指或时间线索(中英各有)
44
- for (const q of ["上次那个方案怎么样了", "我们之前讨论过什么", "还记得我提过的偏好吗",
45
- "what did we discuss last time", "do you remember my preference", "remind me"]) {
46
- assert.equal(needsBroadRecall(q), true, `${q} 应判为模糊`);
47
- }
48
- // 模糊:极短(与 adaptiveThreshold 对 <5 字符的判断同源:几乎匹配任何东西)
49
- assert.equal(needsBroadRecall("你好"), true);
50
- assert.equal(needsBroadRecall(""), true, "拿不到查询时不猜,维持现状");
51
- // 确定:具体技术问句
52
- for (const q of ["pnpm MODULE_NOT_FOUND 怎么修", "把 settings.js 里的白名单登记补上", "node:sqlite 的 WAL 配置项在哪"]) {
53
- assert.equal(needsBroadRecall(q), false, `${q} 应判为确定`);
54
- }
55
- });
56
-
57
- test("#239-5 注入预算:确定收缩到一半(下限 1),模糊维持上限,绝不越界", () => {
58
- assert.equal(adaptiveInjectBudget("pnpm MODULE_NOT_FOUND 怎么修", 5), 3, "确定 → 收缩到一半");
59
- assert.equal(adaptiveInjectBudget("上次讨论的方案", 5), 5, "模糊 → 维持上限");
60
- assert.equal(adaptiveInjectBudget("pnpm MODULE_NOT_FOUND 怎么修", 1), 1, "下限 1");
61
- assert.equal(adaptiveInjectBudget("pnpm MODULE_NOT_FOUND 怎么修", 0), 0, "0 = 不注入,flag 打开也不该变 1");
62
- assert.equal(adaptiveInjectBudget("any", undefined), undefined, "非法上限原样返回,不猜");
63
- });
64
-
65
- test("#239-5 默认关:开关未开时注入条数与查询无关(行为逐字节不变)", () => {
66
- const { contexts, service } = injectorSetup();
67
- seed(service);
68
- assert.equal(injectedCount(contexts[0].text(ctxWithQuery("pnpm MODULE_NOT_FOUND 怎么修"))), 4);
69
- assert.equal(injectedCount(contexts[0].text(ctxWithQuery("上次讨论的方案"))), 4);
70
- });
71
-
72
- test("#239-5 开关打开:确定查询少注入,模糊查询保持上限", () => {
73
- const { contexts, service } = injectorSetup({ injectUncertaintyAdaptive: true });
74
- seed(service);
75
- assert.equal(injectedCount(contexts[0].text(ctxWithQuery("pnpm MODULE_NOT_FOUND 怎么修"))), 2,
76
- "确定 → 注入条数减半");
77
- assert.equal(injectedCount(contexts[0].text(ctxWithQuery("上次讨论的方案"))), 4,
78
- "模糊 → 维持 maxInjectedItems");
79
- });
80
-
81
- test("#239-5 拿不到查询(无会话日志)时维持上限,不因开关而减少", () => {
82
- const { contexts, service } = injectorSetup({ injectUncertaintyAdaptive: true });
83
- seed(service);
84
- assert.equal(injectedCount(contexts[0].text({})), 4, "退化路径不猜、不缩");
85
- });
1
+ // Issue #239(第 5 项):注入条数的查询自适应。
2
+ // 判据只看查询本身(零成本,不做探针检索——先探针检索等于白付一次 fuseRecall),
3
+ // 且只做**单向收缩**:模糊维持上限、确定收缩,绝不越过 maxInjectedItems——注入
4
+ // 相关却带偏生成的内容可能比不注入更糟。
5
+ import test from "node:test";
6
+ import assert from "node:assert/strict";
7
+ import { needsBroadRecall, adaptiveInjectBudget } from "../src/search/adaptive.js";
8
+ import { createStore } from "../src/store.js";
9
+ import { createService } from "../src/service.js";
10
+ import { createInjector } from "../src/inject.js";
11
+ import { createSettings } from "../src/settings.js";
12
+
13
+ function injectorSetup(over = {}) {
14
+ const store = createStore(":memory:");
15
+ const service = createService({ store, mirror: null, config: {} });
16
+ const settings = createSettings(store.db);
17
+ const contexts = [];
18
+ const ctx = { systemPrompt: { context(def) { contexts.push(def); return () => {}; } } };
19
+ createInjector(ctx, service, settings, { maxInjectedItems: 4, importanceThreshold: 3, ...over });
20
+ return { store, service, contexts };
21
+ }
22
+
23
+ // lastUserQuery 读的是 materialized 会话日志(DSH ≥0.1.2-rc 走 snapshotEvents)。
24
+ function ctxWithQuery(text) {
25
+ return {
26
+ agent: {
27
+ session: {
28
+ events: [{ type: "user/message", data: { source: { kind: "user" }, content: [{ type: "text", text }] } }]
29
+ }
30
+ }
31
+ };
32
+ }
33
+
34
+ function seed(service, n = 6) {
35
+ for (let i = 0; i < n; i++) {
36
+ service.saveWithDedupe({ type: "project", title: `项目${i}`, content: `内容${i}`, importance: 5 });
37
+ }
38
+ }
39
+
40
+ const injectedCount = (text) => [0, 1, 2, 3, 4, 5].filter((i) => text.includes(`内容${i}`)).length;
41
+
42
+ test("#239-5 宽召回判据:回指/时间线索与极短查询算模糊,其余算确定", () => {
43
+ // 模糊:回指或时间线索(中英各有)
44
+ for (const q of ["上次那个方案怎么样了", "我们之前讨论过什么", "还记得我提过的偏好吗",
45
+ "what did we discuss last time", "do you remember my preference", "remind me"]) {
46
+ assert.equal(needsBroadRecall(q), true, `${q} 应判为模糊`);
47
+ }
48
+ // 模糊:极短(与 adaptiveThreshold 对 <5 字符的判断同源:几乎匹配任何东西)
49
+ assert.equal(needsBroadRecall("你好"), true);
50
+ assert.equal(needsBroadRecall(""), true, "拿不到查询时不猜,维持现状");
51
+ // 确定:具体技术问句
52
+ for (const q of ["pnpm MODULE_NOT_FOUND 怎么修", "把 settings.js 里的白名单登记补上", "node:sqlite 的 WAL 配置项在哪"]) {
53
+ assert.equal(needsBroadRecall(q), false, `${q} 应判为确定`);
54
+ }
55
+ });
56
+
57
+ test("#239-5 注入预算:确定收缩到一半(下限 1),模糊维持上限,绝不越界", () => {
58
+ assert.equal(adaptiveInjectBudget("pnpm MODULE_NOT_FOUND 怎么修", 5), 3, "确定 → 收缩到一半");
59
+ assert.equal(adaptiveInjectBudget("上次讨论的方案", 5), 5, "模糊 → 维持上限");
60
+ assert.equal(adaptiveInjectBudget("pnpm MODULE_NOT_FOUND 怎么修", 1), 1, "下限 1");
61
+ assert.equal(adaptiveInjectBudget("pnpm MODULE_NOT_FOUND 怎么修", 0), 0, "0 = 不注入,flag 打开也不该变 1");
62
+ assert.equal(adaptiveInjectBudget("any", undefined), undefined, "非法上限原样返回,不猜");
63
+ });
64
+
65
+ test("#239-5 默认关:开关未开时注入条数与查询无关(行为逐字节不变)", () => {
66
+ const { contexts, service } = injectorSetup();
67
+ seed(service);
68
+ assert.equal(injectedCount(contexts[0].text(ctxWithQuery("pnpm MODULE_NOT_FOUND 怎么修"))), 4);
69
+ assert.equal(injectedCount(contexts[0].text(ctxWithQuery("上次讨论的方案"))), 4);
70
+ });
71
+
72
+ test("#239-5 开关打开:确定查询少注入,模糊查询保持上限", () => {
73
+ const { contexts, service } = injectorSetup({ injectUncertaintyAdaptive: true });
74
+ seed(service);
75
+ assert.equal(injectedCount(contexts[0].text(ctxWithQuery("pnpm MODULE_NOT_FOUND 怎么修"))), 2,
76
+ "确定 → 注入条数减半");
77
+ assert.equal(injectedCount(contexts[0].text(ctxWithQuery("上次讨论的方案"))), 4,
78
+ "模糊 → 维持 maxInjectedItems");
79
+ });
80
+
81
+ test("#239-5 拿不到查询(无会话日志)时维持上限,不因开关而减少", () => {
82
+ const { contexts, service } = injectorSetup({ injectUncertaintyAdaptive: true });
83
+ seed(service);
84
+ assert.equal(injectedCount(contexts[0].text({})), 4, "退化路径不猜、不缩");
85
+ });
@@ -1,218 +1,218 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import { EventEmitter } from "node:events";
4
- import { createStore } from "../src/store.js";
5
- import { createService } from "../src/service.js";
6
- import { createApi } from "../src/api.js";
7
- import { createSettings } from "../src/settings.js";
8
-
9
- // 覆盖 PR#73 删测后遗留的三个存活 API 空白(v0.7.16 回归补测):
10
- // /api/dsh-mneme/delete 面板删记忆(POST + auth + 存在性 404)
11
- // /api/dsh-mneme/entities 实体目录(只读、limit 夹取)
12
- // /api/dsh-mneme/external-api 独立服务配置(GET 发 token / PUT 校验)
13
- // 这三个路由在 src/api.js 里一直在跑,只是丢了测试。
14
-
15
- class FakeRes extends EventEmitter {
16
- constructor() { super(); this.statusCode = 200; this.body = ""; }
17
- writeHead(code, headers) { this.statusCode = code; this.headers = headers; return this; }
18
- end(text) { this.body = text ?? ""; this.emit("end"); return this; }
19
- }
20
-
21
- function req(path, method = "GET", body = null) {
22
- const r = new EventEmitter();
23
- r.url = path;
24
- r.method = method;
25
- r.headers = {};
26
- if (body !== null) {
27
- process.nextTick(() => {
28
- r.emit("data", Buffer.from(JSON.stringify(body)));
29
- r.emit("end");
30
- });
31
- }
32
- return r;
33
- }
34
-
35
- function setup(apiToken = "") {
36
- const store = createStore(":memory:");
37
- const service = createService({ store, mirror: null, config: {} });
38
- const settings = createSettings(store.db);
39
- const commands = { add: () => {}, remove: () => {}, list: () => [] };
40
- const routes = [];
41
- const ctx = { webServer: { register(route) { routes.push(route); return () => {}; } } };
42
- createApi(ctx, service, settings, commands, null, undefined, apiToken);
43
- return { store, service, routes, settings };
44
- }
45
-
46
- function handler(routes, path) {
47
- return routes.find((r) => r.path === path);
48
- }
49
-
50
- // ---------------------------------------------------------------------------
51
- // /api/dsh-mneme/delete
52
- // ---------------------------------------------------------------------------
53
- test("delete is auth-gated and rejects non-POST", async () => {
54
- const { routes } = setup("secret-token");
55
- const route = handler(routes, "/api/dsh-mneme/delete");
56
-
57
- // 未带 token → 401
58
- let res = new FakeRes();
59
- await route.handler(req("/api/dsh-mneme/delete", "POST", { id: "x" }), res);
60
- assert.equal(res.statusCode, 401);
61
- assert.equal(JSON.parse(res.body).error, "unauthorized");
62
-
63
- // 带 token 但 GET → 404
64
- res = new FakeRes();
65
- const ok = req("/api/dsh-mneme/delete", "GET");
66
- ok.headers = { authorization: "Bearer secret-token" };
67
- await route.handler(ok, res);
68
- assert.equal(res.statusCode, 404);
69
- });
70
-
71
- test("delete validates id and reports missing/unknown 404", async () => {
72
- const { routes, service } = setup("secret-token");
73
- const route = handler(routes, "/api/dsh-mneme/delete");
74
-
75
- // 空 id → 400
76
- let res = new FakeRes();
77
- let ok = req("/api/dsh-mneme/delete", "POST", { id: " " });
78
- ok.headers = { authorization: "Bearer secret-token" };
79
- await route.handler(ok, res);
80
- assert.equal(res.statusCode, 400);
81
- assert.equal(JSON.parse(res.body).error, "missing-id");
82
-
83
- // 不存在的 id → 404(store.remove 静默,靠前置 getById 区分)
84
- res = new FakeRes();
85
- ok = req("/api/dsh-mneme/delete", "POST", { id: "no-such-memory" });
86
- ok.headers = { authorization: "Bearer secret-token" };
87
- await route.handler(ok, res);
88
- assert.equal(res.statusCode, 404);
89
- assert.equal(JSON.parse(res.body).error, "not-found");
90
-
91
- // 确认没被误删
92
- assert.equal(service.count(), 0);
93
- });
94
-
95
- test("delete removes an existing memory", async () => {
96
- const { routes, service } = setup("secret-token");
97
- const route = handler(routes, "/api/dsh-mneme/delete");
98
-
99
- const { memory } = service.saveWithDedupe({ type: "project", title: "要删的", content: "删掉我" });
100
- assert.ok(service.getById(memory.id));
101
-
102
- const res = new FakeRes();
103
- const ok = req("/api/dsh-mneme/delete", "POST", { id: memory.id });
104
- ok.headers = { authorization: "Bearer secret-token" };
105
- await route.handler(ok, res);
106
- assert.equal(res.statusCode, 200);
107
- assert.deepEqual(JSON.parse(res.body), { ok: true });
108
- assert.equal(service.getById(memory.id), undefined);
109
- assert.equal(service.count(), 0);
110
- });
111
-
112
- // ---------------------------------------------------------------------------
113
- // /api/dsh-mneme/entities
114
- // ---------------------------------------------------------------------------
115
- test("entities directory is read-only and returns empty list", async () => {
116
- const { routes } = setup();
117
- const route = handler(routes, "/api/dsh-mneme/entities");
118
- const res = new FakeRes();
119
- await route.handler(req("/api/dsh-mneme/entities"), res);
120
- assert.equal(res.statusCode, 200);
121
- assert.deepEqual(JSON.parse(res.body), { entities: [], total: 0 });
122
- });
123
-
124
- test("entities lists seeded entities without auth", async () => {
125
- const { routes, service } = setup("secret-token"); // 有 token 但读路由不校验
126
- service.createEntity({ name: "SQLite", type: "technology" });
127
- service.createEntity({ name: "记忆", type: "concept" });
128
-
129
- const route = handler(routes, "/api/dsh-mneme/entities");
130
- const res = new FakeRes();
131
- await route.handler(req("/api/dsh-mneme/entities"), res);
132
- assert.equal(res.statusCode, 200);
133
- const data = JSON.parse(res.body);
134
- assert.equal(data.total, 2);
135
- const names = data.entities.map((e) => e.name).sort();
136
- assert.deepEqual(names, ["SQLite", "记忆"]);
137
- for (const e of data.entities) {
138
- assert.ok(e.type, "each entity carries a type");
139
- assert.ok(Number.isInteger(e.mention_count));
140
- }
141
- });
142
-
143
- test("entities clamps limit to [1, 1000]", async () => {
144
- const { routes, service } = setup();
145
- for (let i = 0; i < 5; i++) service.createEntity({ name: `e${i}`, type: "concept" });
146
- const route = handler(routes, "/api/dsh-mneme/entities");
147
-
148
- // 夹取语义:NaN/0 先被 `|| 500` 兜底成 500(0 不再走 max(1,) 下限),
149
- // 负数才被夹到 1,超限(>1000)夹到 1000。断言按真实实现收紧。
150
- for (const [qs, want] of [["limit=abc", 5], ["limit=0", 5], ["limit=-10", 1], ["limit=5000", 5]]) {
151
- const res = new FakeRes();
152
- await route.handler(req(`/api/dsh-mneme/entities?${qs}`), res);
153
- const data = JSON.parse(res.body);
154
- assert.equal(data.entities.length, want, `${qs} → ${want} (got ${data.entities.length})`);
155
- }
156
- });
157
-
158
- // ---------------------------------------------------------------------------
159
- // /api/dsh-mneme/external-api
160
- // ---------------------------------------------------------------------------
161
- test("external-api GET materializes and persists a token", async () => {
162
- const { routes, settings } = setup();
163
- const route = handler(routes, "/api/dsh-mneme/external-api");
164
-
165
- const res = new FakeRes();
166
- await route.handler(req("/api/dsh-mneme/external-api"), res);
167
- assert.equal(res.statusCode, 200);
168
- const cfg = JSON.parse(res.body).config;
169
- assert.ok(typeof cfg.token === "string" && cfg.token.length >= 16, "token is generated");
170
-
171
- // 持久化后二次 GET 返回同一 token
172
- const res2 = new FakeRes();
173
- await route.handler(req("/api/dsh-mneme/external-api"), res2);
174
- assert.equal(JSON.parse(res2.body).config.token, cfg.token);
175
- assert.ok(settings.getExternalApi().token, "token persisted to settings kv");
176
- });
177
-
178
- test("external-api PUT requires auth and validates port/host", async () => {
179
- const { routes } = setup("secret-token");
180
- const route = handler(routes, "/api/dsh-mneme/external-api");
181
-
182
- // 未带 token → 401
183
- let res = new FakeRes();
184
- await route.handler(req("/api/dsh-mneme/external-api", "PUT", { enabled: true }), res);
185
- assert.equal(res.statusCode, 401);
186
-
187
- // 端口越界 → 400
188
- res = new FakeRes();
189
- let ok = req("/api/dsh-mneme/external-api", "PUT", { port: 70000 });
190
- ok.headers = { authorization: "Bearer secret-token" };
191
- await route.handler(ok, res);
192
- assert.equal(res.statusCode, 400);
193
- assert.equal(JSON.parse(res.body).error, "invalid-port");
194
-
195
- // host 带协议 → 400
196
- res = new FakeRes();
197
- ok = req("/api/dsh-mneme/external-api", "PUT", { host: "https://example.com" });
198
- ok.headers = { authorization: "Bearer secret-token" };
199
- await route.handler(ok, res);
200
- assert.equal(res.statusCode, 400);
201
- assert.equal(JSON.parse(res.body).error, "invalid-host");
202
- });
203
-
204
- test("external-api PUT applies valid config and echoes it", async () => {
205
- const { routes } = setup("secret-token");
206
- const route = handler(routes, "/api/dsh-mneme/external-api");
207
-
208
- const res = new FakeRes();
209
- const ok = req("/api/dsh-mneme/external-api", "PUT", { enabled: true, port: 8790, host: "127.0.0.1" });
210
- ok.headers = { authorization: "Bearer secret-token" };
211
- await route.handler(ok, res);
212
- assert.equal(res.statusCode, 200);
213
- const cfg = JSON.parse(res.body).config;
214
- assert.equal(cfg.enabled, true);
215
- assert.equal(cfg.port, 8790);
216
- assert.equal(cfg.host, "127.0.0.1");
217
- assert.ok(typeof cfg.token === "string");
218
- });
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { EventEmitter } from "node:events";
4
+ import { createStore } from "../src/store.js";
5
+ import { createService } from "../src/service.js";
6
+ import { createApi } from "../src/api.js";
7
+ import { createSettings } from "../src/settings.js";
8
+
9
+ // 覆盖 PR#73 删测后遗留的三个存活 API 空白(v0.7.16 回归补测):
10
+ // /api/dsh-mneme/delete 面板删记忆(POST + auth + 存在性 404)
11
+ // /api/dsh-mneme/entities 实体目录(只读、limit 夹取)
12
+ // /api/dsh-mneme/external-api 独立服务配置(GET 发 token / PUT 校验)
13
+ // 这三个路由在 src/api.js 里一直在跑,只是丢了测试。
14
+
15
+ class FakeRes extends EventEmitter {
16
+ constructor() { super(); this.statusCode = 200; this.body = ""; }
17
+ writeHead(code, headers) { this.statusCode = code; this.headers = headers; return this; }
18
+ end(text) { this.body = text ?? ""; this.emit("end"); return this; }
19
+ }
20
+
21
+ function req(path, method = "GET", body = null) {
22
+ const r = new EventEmitter();
23
+ r.url = path;
24
+ r.method = method;
25
+ r.headers = {};
26
+ if (body !== null) {
27
+ process.nextTick(() => {
28
+ r.emit("data", Buffer.from(JSON.stringify(body)));
29
+ r.emit("end");
30
+ });
31
+ }
32
+ return r;
33
+ }
34
+
35
+ function setup(apiToken = "") {
36
+ const store = createStore(":memory:");
37
+ const service = createService({ store, mirror: null, config: {} });
38
+ const settings = createSettings(store.db);
39
+ const commands = { add: () => {}, remove: () => {}, list: () => [] };
40
+ const routes = [];
41
+ const ctx = { webServer: { register(route) { routes.push(route); return () => {}; } } };
42
+ createApi(ctx, service, settings, commands, null, undefined, apiToken);
43
+ return { store, service, routes, settings };
44
+ }
45
+
46
+ function handler(routes, path) {
47
+ return routes.find((r) => r.path === path);
48
+ }
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // /api/dsh-mneme/delete
52
+ // ---------------------------------------------------------------------------
53
+ test("delete is auth-gated and rejects non-POST", async () => {
54
+ const { routes } = setup("secret-token");
55
+ const route = handler(routes, "/api/dsh-mneme/delete");
56
+
57
+ // 未带 token → 401
58
+ let res = new FakeRes();
59
+ await route.handler(req("/api/dsh-mneme/delete", "POST", { id: "x" }), res);
60
+ assert.equal(res.statusCode, 401);
61
+ assert.equal(JSON.parse(res.body).error, "unauthorized");
62
+
63
+ // 带 token 但 GET → 404
64
+ res = new FakeRes();
65
+ const ok = req("/api/dsh-mneme/delete", "GET");
66
+ ok.headers = { authorization: "Bearer secret-token" };
67
+ await route.handler(ok, res);
68
+ assert.equal(res.statusCode, 404);
69
+ });
70
+
71
+ test("delete validates id and reports missing/unknown 404", async () => {
72
+ const { routes, service } = setup("secret-token");
73
+ const route = handler(routes, "/api/dsh-mneme/delete");
74
+
75
+ // 空 id → 400
76
+ let res = new FakeRes();
77
+ let ok = req("/api/dsh-mneme/delete", "POST", { id: " " });
78
+ ok.headers = { authorization: "Bearer secret-token" };
79
+ await route.handler(ok, res);
80
+ assert.equal(res.statusCode, 400);
81
+ assert.equal(JSON.parse(res.body).error, "missing-id");
82
+
83
+ // 不存在的 id → 404(store.remove 静默,靠前置 getById 区分)
84
+ res = new FakeRes();
85
+ ok = req("/api/dsh-mneme/delete", "POST", { id: "no-such-memory" });
86
+ ok.headers = { authorization: "Bearer secret-token" };
87
+ await route.handler(ok, res);
88
+ assert.equal(res.statusCode, 404);
89
+ assert.equal(JSON.parse(res.body).error, "not-found");
90
+
91
+ // 确认没被误删
92
+ assert.equal(service.count(), 0);
93
+ });
94
+
95
+ test("delete removes an existing memory", async () => {
96
+ const { routes, service } = setup("secret-token");
97
+ const route = handler(routes, "/api/dsh-mneme/delete");
98
+
99
+ const { memory } = service.saveWithDedupe({ type: "project", title: "要删的", content: "删掉我" });
100
+ assert.ok(service.getById(memory.id));
101
+
102
+ const res = new FakeRes();
103
+ const ok = req("/api/dsh-mneme/delete", "POST", { id: memory.id });
104
+ ok.headers = { authorization: "Bearer secret-token" };
105
+ await route.handler(ok, res);
106
+ assert.equal(res.statusCode, 200);
107
+ assert.deepEqual(JSON.parse(res.body), { ok: true });
108
+ assert.equal(service.getById(memory.id), undefined);
109
+ assert.equal(service.count(), 0);
110
+ });
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // /api/dsh-mneme/entities
114
+ // ---------------------------------------------------------------------------
115
+ test("entities directory is read-only and returns empty list", async () => {
116
+ const { routes } = setup();
117
+ const route = handler(routes, "/api/dsh-mneme/entities");
118
+ const res = new FakeRes();
119
+ await route.handler(req("/api/dsh-mneme/entities"), res);
120
+ assert.equal(res.statusCode, 200);
121
+ assert.deepEqual(JSON.parse(res.body), { entities: [], total: 0 });
122
+ });
123
+
124
+ test("entities lists seeded entities without auth", async () => {
125
+ const { routes, service } = setup("secret-token"); // 有 token 但读路由不校验
126
+ service.createEntity({ name: "SQLite", type: "technology" });
127
+ service.createEntity({ name: "记忆", type: "concept" });
128
+
129
+ const route = handler(routes, "/api/dsh-mneme/entities");
130
+ const res = new FakeRes();
131
+ await route.handler(req("/api/dsh-mneme/entities"), res);
132
+ assert.equal(res.statusCode, 200);
133
+ const data = JSON.parse(res.body);
134
+ assert.equal(data.total, 2);
135
+ const names = data.entities.map((e) => e.name).sort();
136
+ assert.deepEqual(names, ["SQLite", "记忆"]);
137
+ for (const e of data.entities) {
138
+ assert.ok(e.type, "each entity carries a type");
139
+ assert.ok(Number.isInteger(e.mention_count));
140
+ }
141
+ });
142
+
143
+ test("entities clamps limit to [1, 1000]", async () => {
144
+ const { routes, service } = setup();
145
+ for (let i = 0; i < 5; i++) service.createEntity({ name: `e${i}`, type: "concept" });
146
+ const route = handler(routes, "/api/dsh-mneme/entities");
147
+
148
+ // 夹取语义:NaN/0 先被 `|| 500` 兜底成 500(0 不再走 max(1,) 下限),
149
+ // 负数才被夹到 1,超限(>1000)夹到 1000。断言按真实实现收紧。
150
+ for (const [qs, want] of [["limit=abc", 5], ["limit=0", 5], ["limit=-10", 1], ["limit=5000", 5]]) {
151
+ const res = new FakeRes();
152
+ await route.handler(req(`/api/dsh-mneme/entities?${qs}`), res);
153
+ const data = JSON.parse(res.body);
154
+ assert.equal(data.entities.length, want, `${qs} → ${want} (got ${data.entities.length})`);
155
+ }
156
+ });
157
+
158
+ // ---------------------------------------------------------------------------
159
+ // /api/dsh-mneme/external-api
160
+ // ---------------------------------------------------------------------------
161
+ test("external-api GET materializes and persists a token", async () => {
162
+ const { routes, settings } = setup();
163
+ const route = handler(routes, "/api/dsh-mneme/external-api");
164
+
165
+ const res = new FakeRes();
166
+ await route.handler(req("/api/dsh-mneme/external-api"), res);
167
+ assert.equal(res.statusCode, 200);
168
+ const cfg = JSON.parse(res.body).config;
169
+ assert.ok(typeof cfg.token === "string" && cfg.token.length >= 16, "token is generated");
170
+
171
+ // 持久化后二次 GET 返回同一 token
172
+ const res2 = new FakeRes();
173
+ await route.handler(req("/api/dsh-mneme/external-api"), res2);
174
+ assert.equal(JSON.parse(res2.body).config.token, cfg.token);
175
+ assert.ok(settings.getExternalApi().token, "token persisted to settings kv");
176
+ });
177
+
178
+ test("external-api PUT requires auth and validates port/host", async () => {
179
+ const { routes } = setup("secret-token");
180
+ const route = handler(routes, "/api/dsh-mneme/external-api");
181
+
182
+ // 未带 token → 401
183
+ let res = new FakeRes();
184
+ await route.handler(req("/api/dsh-mneme/external-api", "PUT", { enabled: true }), res);
185
+ assert.equal(res.statusCode, 401);
186
+
187
+ // 端口越界 → 400
188
+ res = new FakeRes();
189
+ let ok = req("/api/dsh-mneme/external-api", "PUT", { port: 70000 });
190
+ ok.headers = { authorization: "Bearer secret-token" };
191
+ await route.handler(ok, res);
192
+ assert.equal(res.statusCode, 400);
193
+ assert.equal(JSON.parse(res.body).error, "invalid-port");
194
+
195
+ // host 带协议 → 400
196
+ res = new FakeRes();
197
+ ok = req("/api/dsh-mneme/external-api", "PUT", { host: "https://example.com" });
198
+ ok.headers = { authorization: "Bearer secret-token" };
199
+ await route.handler(ok, res);
200
+ assert.equal(res.statusCode, 400);
201
+ assert.equal(JSON.parse(res.body).error, "invalid-host");
202
+ });
203
+
204
+ test("external-api PUT applies valid config and echoes it", async () => {
205
+ const { routes } = setup("secret-token");
206
+ const route = handler(routes, "/api/dsh-mneme/external-api");
207
+
208
+ const res = new FakeRes();
209
+ const ok = req("/api/dsh-mneme/external-api", "PUT", { enabled: true, port: 8790, host: "127.0.0.1" });
210
+ ok.headers = { authorization: "Bearer secret-token" };
211
+ await route.handler(ok, res);
212
+ assert.equal(res.statusCode, 200);
213
+ const cfg = JSON.parse(res.body).config;
214
+ assert.equal(cfg.enabled, true);
215
+ assert.equal(cfg.port, 8790);
216
+ assert.equal(cfg.host, "127.0.0.1");
217
+ assert.ok(typeof cfg.token === "string");
218
+ });