@modusensus/dsh-mneme 0.8.3 → 0.8.5
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 +727 -636
- package/bin/cli.mjs +0 -0
- package/bin/dsh-mneme-mcp.mjs +510 -0
- package/cordis.patch.yml +21 -21
- package/lib/api-standalone.js +506 -316
- package/lib/api.js +1403 -1316
- package/lib/bootstrap.js +211 -0
- package/lib/client.js +4657 -4366
- package/lib/commands.js +95 -95
- package/lib/config.js +615 -510
- package/lib/document.js +221 -0
- package/lib/dream/clustering.js +118 -118
- package/lib/dream/decisions.js +658 -658
- package/lib/dream/narratives.js +44 -0
- package/lib/dream/sleep.js +702 -700
- package/lib/dream.js +147 -16
- package/lib/embedding.js +177 -177
- package/lib/entities/extractor.js +249 -249
- package/lib/guide.js +44 -0
- package/lib/heat.js +144 -136
- package/lib/hot-memory.js +46 -46
- package/lib/index.js +515 -515
- package/lib/inject.js +375 -295
- package/lib/lang.js +208 -179
- package/lib/local-embedder.js +320 -320
- package/lib/mirror.js +198 -198
- package/lib/organize.js +450 -0
- package/lib/quality-filter.js +126 -126
- package/lib/recall-stats.js +125 -0
- 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 -235
- package/lib/runtime/loader.js +252 -249
- 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 +204 -191
- package/lib/scope.js +118 -118
- package/lib/search/adaptive.js +60 -22
- package/lib/search/bm25.js +96 -96
- package/lib/service.js +2324 -2087
- package/lib/settings.js +459 -420
- package/lib/store.js +2549 -2367
- package/lib/summarize.js +820 -449
- package/lib/tools.js +702 -574
- package/lib/vector-index.js +110 -110
- package/lib/version-check.js +122 -122
- package/package.json +109 -108
- package/runtime-manifest.json +526 -596
- package/scripts/benchmark-embed.js +201 -201
- package/scripts/benchmark-recall.js +199 -199
- package/scripts/benchmark-rerank.js +166 -166
- package/scripts/build-runtime-manifest.mjs +224 -217
- 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 +67 -61
- package/src/api-standalone.js +506 -316
- package/src/api.js +1403 -1316
- package/src/bootstrap.js +211 -0
- package/src/commands.js +95 -95
- package/src/config.js +615 -510
- package/src/document.js +221 -0
- package/src/dream/clustering.js +118 -118
- package/src/dream/decisions.js +658 -658
- package/src/dream/narratives.js +44 -0
- package/src/dream/sleep.js +702 -700
- package/src/dream.js +147 -16
- package/src/embedding.js +177 -177
- package/src/entities/extractor.js +249 -249
- package/src/guide.js +44 -0
- package/src/heat.js +144 -136
- package/src/hot-memory.js +46 -46
- package/src/index.js +515 -515
- package/src/inject.js +375 -295
- package/src/lang.js +208 -179
- package/src/local-embedder.js +320 -320
- package/src/mirror.js +198 -198
- package/src/organize.js +450 -0
- package/src/quality-filter.js +126 -126
- package/src/recall-stats.js +125 -0
- 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 -235
- package/src/runtime/loader.js +252 -249
- 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 +204 -191
- package/src/scope.js +118 -118
- package/src/search/adaptive.js +60 -22
- package/src/search/bm25.js +96 -96
- package/src/service.js +2324 -2087
- package/src/settings.js +459 -420
- package/src/store.js +2549 -2367
- package/src/summarize.js +820 -449
- package/src/tools.js +702 -574
- package/src/vector-index.js +110 -110
- package/src/version-check.js +122 -122
- package/test/adaptive-inject-budget.test.js +85 -0
- package/test/api-routes-gaps.test.js +218 -218
- package/test/api.test.js +1640 -1482
- package/test/audit.test.js +448 -448
- package/test/benchmark.test.js +94 -94
- package/test/bootstrap.test.js +220 -0
- package/test/client.test.js +860 -736
- 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/document.test.js +523 -0
- 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.test.js +1308 -1254
- package/test/embedding-ready.test.js +196 -196
- package/test/entities.test.js +547 -547
- package/test/entity-recall.test.js +179 -0
- package/test/epistemic.test.js +298 -298
- package/test/fnew-0112.test.js +311 -311
- package/test/fnew-03.test.js +422 -422
- package/test/graph-api.test.js +175 -175
- package/test/heat.test.js +175 -148
- 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/hot-memory.test.js +145 -145
- package/test/inject-content-cap.test.js +91 -0
- package/test/inject-first-round-bm25.test.js +89 -89
- package/test/inject-heat.test.js +61 -0
- package/test/inject-pools.test.js +170 -0
- package/test/inject-recall.test.js +105 -0
- package/test/inject-rotation.test.js +161 -161
- package/test/inject.test.js +283 -249
- package/test/lang.test.js +87 -87
- package/test/lib-smoke.test.js +96 -96
- package/test/llm-audit.test.js +462 -309
- package/test/llm-message-source.test.js +177 -177
- package/test/local-embedder.test.js +249 -249
- package/test/mcp-stdio.test.js +385 -0
- 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.test.js +249 -249
- package/test/narrative-bars.test.js +162 -0
- package/test/organize.test.js +418 -0
- package/test/peer-blockers.test.js +193 -193
- package/test/policy-epoch.test.js +259 -259
- package/test/quality-filter.test.js +186 -186
- package/test/reasoning-effort.test.js +746 -746
- package/test/recall-evals.test.js +235 -235
- package/test/recall-layer.test.js +326 -326
- package/test/recall-stats.test.js +123 -0
- 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 -0
- 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 -237
- package/test/runtime-manifest.test.js +159 -158
- 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 +279 -214
- package/test/scope-candidates.test.js +239 -239
- package/test/scope-explicit.test.js +452 -452
- package/test/scope-retrieval.test.js +197 -197
- package/test/scope-storage.test.js +255 -255
- package/test/scope-strict.test.js +231 -231
- package/test/search-fusion.test.js +90 -90
- package/test/semantic.test.js +124 -124
- 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 -125
- package/test/sleep-scheduler-rearm.test.js +89 -89
- package/test/sleep.test.js +410 -373
- package/test/standalone-api.test.js +544 -359
- package/test/store-vector-cache.test.js +98 -98
- package/test/store.test.js +396 -383
- package/test/stress.test.js +209 -209
- package/test/summarize.test.js +1288 -575
- package/test/tools-dto-schema-matrix.test.js +325 -0
- package/test/tools-memory-get-schema.test.js +65 -65
- package/test/tools.test.js +374 -341
- 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
|
@@ -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
|
+
});
|