@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.
- package/LICENSE +21 -21
- package/README.md +655 -654
- package/assets/icon.svg +3 -3
- package/bin/cli.mjs +689 -689
- package/bin/dsh-mneme-mcp.mjs +510 -510
- package/bin/dsh-mneme-serve.mjs +151 -151
- package/cordis.patch.yml +21 -21
- package/docs/CONFIGURATION.md +263 -0
- package/docs/CONTINUITY.md +124 -0
- package/docs/DAEMON.md +46 -0
- package/docs/ENTITIES.md +245 -0
- package/docs/LOCAL_MODEL.md +222 -0
- package/docs/SEMANTIC.md +256 -0
- package/docs/SLEEP.md +164 -0
- package/docs/STORAGE.md +89 -0
- package/lib/api-standalone.js +673 -673
- package/lib/api.js +1437 -1433
- package/lib/bootstrap.js +211 -211
- package/lib/client.js +5444 -5295
- package/lib/commands.js +95 -95
- package/lib/config.js +880 -843
- package/lib/content-hash.js +54 -54
- package/lib/continuity.js +220 -220
- package/lib/document-index.js +94 -94
- package/lib/document.js +303 -303
- package/lib/dream/clustering.js +118 -118
- package/lib/dream/decisions.js +674 -674
- package/lib/dream/narratives.js +44 -44
- package/lib/dream/sleep.js +819 -819
- package/lib/dream.js +1501 -1501
- package/lib/embedding.js +177 -177
- package/lib/entities/extractor.js +252 -252
- package/lib/graph/anchoring.js +179 -179
- package/lib/guide.js +74 -74
- package/lib/heat.js +144 -144
- package/lib/hot-memory.js +46 -46
- package/lib/index.js +535 -503
- package/lib/inject.js +464 -450
- package/lib/lang.js +248 -229
- package/lib/local-embedder.js +358 -358
- package/lib/maintenance.js +142 -142
- package/lib/mirror.js +330 -330
- package/lib/organize.js +450 -450
- package/lib/peak-hours.js +117 -117
- package/lib/placement.js +13 -0
- package/lib/pre-inject-gate.js +340 -0
- package/lib/quality-filter.js +126 -126
- package/lib/recall-stats.js +175 -175
- package/lib/reranker.js +269 -269
- package/lib/runtime/adopt.js +176 -176
- package/lib/runtime/closure.js +172 -172
- package/lib/runtime/download.js +247 -247
- package/lib/runtime/layout.js +265 -265
- package/lib/runtime/loader.js +252 -252
- package/lib/runtime/model-fetch.js +361 -361
- package/lib/runtime/provision.js +237 -237
- package/lib/runtime/tarball.js +130 -130
- package/lib/runtime/verify.js +223 -204
- package/lib/scope.js +146 -146
- package/lib/search/adaptive.js +60 -60
- package/lib/search/bm25.js +96 -96
- package/lib/semantic.js +229 -229
- package/lib/sensitive-scan.js +209 -209
- package/lib/serve.js +215 -215
- package/lib/service.js +2663 -2605
- package/lib/settings.js +517 -511
- package/lib/store.js +3007 -3007
- package/lib/summarize.js +891 -856
- package/lib/tools.js +743 -737
- package/lib/vector-index.js +110 -110
- package/lib/version-check.js +122 -122
- package/lib/write-admission.js +402 -402
- package/locale/en.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +118 -118
- package/runtime-manifest.json +526 -526
- package/scripts/benchmark-embed.js +201 -201
- package/scripts/benchmark-recall.js +249 -249
- package/scripts/benchmark-rerank.js +166 -166
- package/scripts/build-runtime-manifest.mjs +224 -224
- package/scripts/changelog-prep.mjs +36 -36
- package/scripts/check-sync.js +42 -42
- package/scripts/e2e-dsh.js +220 -220
- package/scripts/mneme-runtime.mjs +277 -277
- package/scripts/stress-dsh.js +255 -255
- package/scripts/sync-lib.js +47 -47
- package/scripts/sync-test-badge.mjs +69 -69
- package/scripts/test-count-sync.mjs +44 -44
- package/src/api-standalone.js +673 -673
- package/src/api.js +1437 -1433
- package/src/bootstrap.js +211 -211
- package/src/commands.js +95 -95
- package/src/config.js +880 -843
- package/src/content-hash.js +54 -54
- package/src/continuity.js +220 -220
- package/src/document-index.js +94 -94
- package/src/document.js +303 -303
- package/src/dream/clustering.js +118 -118
- package/src/dream/decisions.js +674 -674
- package/src/dream/narratives.js +44 -44
- package/src/dream/sleep.js +819 -819
- package/src/dream.js +1501 -1501
- package/src/embedding.js +177 -177
- package/src/entities/extractor.js +252 -252
- package/src/graph/anchoring.js +179 -179
- package/src/guide.js +74 -74
- package/src/heat.js +144 -144
- package/src/hot-memory.js +46 -46
- package/src/index.js +535 -503
- package/src/inject.js +464 -450
- package/src/lang.js +248 -229
- package/src/local-embedder.js +358 -358
- package/src/maintenance.js +142 -142
- package/src/mirror.js +330 -330
- package/src/organize.js +450 -450
- package/src/peak-hours.js +117 -117
- package/src/placement.js +13 -0
- package/src/pre-inject-gate.js +340 -0
- package/src/quality-filter.js +126 -126
- package/src/recall-stats.js +175 -175
- package/src/reranker.js +269 -269
- package/src/runtime/adopt.js +176 -176
- package/src/runtime/closure.js +172 -172
- package/src/runtime/download.js +247 -247
- package/src/runtime/layout.js +265 -265
- package/src/runtime/loader.js +252 -252
- package/src/runtime/model-fetch.js +361 -361
- package/src/runtime/provision.js +237 -237
- package/src/runtime/tarball.js +130 -130
- package/src/runtime/verify.js +223 -204
- package/src/scope.js +146 -146
- package/src/search/adaptive.js +60 -60
- package/src/search/bm25.js +96 -96
- package/src/semantic.js +229 -229
- package/src/sensitive-scan.js +209 -209
- package/src/serve.js +215 -215
- package/src/service.js +2663 -2605
- package/src/settings.js +517 -511
- package/src/store.js +3007 -3007
- package/src/summarize.js +891 -856
- package/src/tools.js +743 -737
- package/src/vector-index.js +110 -110
- package/src/version-check.js +122 -122
- package/src/write-admission.js +402 -402
- package/test/adaptive-inject-budget.test.js +85 -85
- package/test/api-routes-gaps.test.js +218 -218
- package/test/api.test.js +1702 -1690
- package/test/audit.test.js +448 -448
- package/test/baseline-persist.test.js +248 -248
- package/test/benchmark.test.js +94 -94
- package/test/bootstrap.test.js +220 -220
- package/test/changelog-prep.test.js +60 -60
- package/test/client.test.js +1230 -1169
- package/test/clustering.test.js +100 -100
- package/test/commands.test.js +106 -106
- package/test/config.test.js +66 -66
- package/test/conflict-freeze.test.js +300 -300
- package/test/content-hash.test.js +204 -204
- package/test/continuity.test.js +333 -333
- package/test/document-dir.test.js +233 -233
- package/test/document.test.js +671 -671
- package/test/dream-archive-guardrail.test.js +116 -116
- package/test/dream-candidate-hybrid.test.js +281 -281
- package/test/dream-coverage-degrade.test.js +86 -86
- package/test/dream-failure-backoff.test.js +173 -173
- package/test/dream-peak-hours.test.js +194 -194
- package/test/dream.test.js +1385 -1383
- package/test/embedding-ready.test.js +196 -196
- package/test/entities.test.js +627 -547
- package/test/entity-recall.test.js +179 -179
- package/test/epistemic.test.js +325 -325
- package/test/fnew-0112.test.js +311 -311
- package/test/fnew-03.test.js +422 -422
- package/test/graph-anchoring.test.js +184 -184
- package/test/graph-api.test.js +175 -175
- package/test/graph-inject-budget.test.js +103 -103
- package/test/graph-passive-confirm.test.js +91 -91
- package/test/graph-weight-evolution.test.js +104 -104
- package/test/heat.test.js +175 -175
- package/test/helpers/dream-mock.js +82 -82
- package/test/helpers/peer-worker.mjs +25 -25
- package/test/helpers/runtime-source.js +60 -60
- package/test/helpers/tar-builder.js +80 -80
- package/test/helpers/write-admission-samples.js +308 -308
- package/test/hot-memory.test.js +145 -145
- package/test/inject-content-cap.test.js +91 -91
- package/test/inject-first-round-bm25.test.js +89 -89
- package/test/inject-heat.test.js +77 -77
- package/test/inject-parent-gate.test.js +106 -106
- package/test/inject-pools.test.js +194 -194
- package/test/inject-recall.test.js +105 -105
- package/test/inject-rotation.test.js +161 -161
- package/test/inject.test.js +420 -420
- package/test/lang.test.js +87 -87
- package/test/lastrunat-persist.test.js +101 -101
- package/test/lib-smoke.test.js +96 -96
- package/test/llm-audit.test.js +598 -598
- package/test/llm-message-source.test.js +207 -207
- package/test/local-embedder.test.js +292 -292
- package/test/maintenance.test.js +391 -391
- package/test/mcp-stdio.test.js +385 -385
- package/test/mirror-dirty.test.js +424 -424
- package/test/mirror-edit-digest.test.js +187 -187
- package/test/mirror-generation.test.js +499 -499
- package/test/mirror-types.test.js +208 -208
- package/test/mirror.test.js +249 -249
- package/test/narrative-bars.test.js +162 -162
- package/test/organize.test.js +418 -418
- package/test/peer-blockers.test.js +193 -193
- package/test/placement.test.js +36 -0
- package/test/policy-epoch.test.js +259 -259
- package/test/pre-inject-gate.test.js +434 -0
- package/test/quality-filter.test.js +186 -186
- package/test/reasoning-effort.test.js +752 -752
- package/test/recall-anti-update.test.js +82 -82
- package/test/recall-evals.test.js +253 -253
- package/test/recall-layer.test.js +326 -326
- package/test/recall-stats.test.js +189 -189
- package/test/receipt-chain.test.js +451 -451
- package/test/reflection.test.js +226 -226
- package/test/reindex-backfill.test.js +62 -62
- package/test/reranker-single-logit.test.js +73 -73
- package/test/reranker.test.js +197 -197
- package/test/resident-status.test.js +83 -83
- package/test/runtime-adopt.test.js +206 -206
- package/test/runtime-cli.test.js +89 -89
- package/test/runtime-closure.test.js +193 -193
- package/test/runtime-download.test.js +302 -302
- package/test/runtime-layout.test.js +163 -163
- package/test/runtime-loader.test.js +255 -255
- package/test/runtime-manifest.test.js +159 -159
- package/test/runtime-model-fetch.test.js +348 -348
- package/test/runtime-provision.test.js +101 -101
- package/test/runtime-tarball.test.js +81 -81
- package/test/runtime-verify.test.js +319 -279
- package/test/scope-candidates.test.js +239 -239
- package/test/scope-explicit.test.js +452 -452
- package/test/scope-inject-soft.test.js +106 -106
- package/test/scope-retrieval.test.js +197 -197
- package/test/scope-storage.test.js +255 -255
- package/test/scope-strict.test.js +367 -367
- package/test/search-fusion.test.js +90 -90
- package/test/semantic.test.js +124 -124
- package/test/sensitive-scan.test.js +207 -207
- package/test/serve-bin.test.js +121 -121
- package/test/serve.test.js +196 -196
- package/test/service-search.test.js +174 -174
- package/test/service.test.js +405 -405
- package/test/settings.test.js +261 -261
- package/test/skip-invalid-restore.test.js +262 -262
- package/test/sleep-action-set.test.js +422 -422
- package/test/sleep-heat.test.js +126 -126
- package/test/sleep-scheduler-rearm.test.js +89 -89
- package/test/sleep.test.js +527 -527
- package/test/standalone-api.test.js +751 -751
- package/test/store-vector-cache.test.js +98 -98
- package/test/store.test.js +396 -396
- package/test/stress.test.js +209 -209
- package/test/summarize-occurred-at.test.js +127 -0
- package/test/summarize-reasoning-effort.test.js +189 -189
- package/test/summarize.test.js +1465 -1465
- package/test/supersede-state-tracking.test.js +394 -0
- package/test/test-count-sync.test.js +60 -60
- package/test/tools-dto-schema-matrix.test.js +325 -325
- package/test/tools-memory-get-schema.test.js +65 -65
- package/test/tools.test.js +428 -374
- package/test/updated-at-semantics.test.js +113 -113
- package/test/vector-index.test.js +221 -221
- package/test/version-check.test.js +152 -152
- package/test/webServer-optional.test.js +82 -82
- 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
|
+
});
|