@modusensus/dsh-mneme 0.7.10 → 0.7.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +437 -0
- package/README.md +364 -166
- package/bin/cli.mjs +603 -0
- package/{dsh-mneme/cordis.patch.yml → cordis.patch.yml} +1 -0
- package/lib/api-standalone.js +264 -0
- package/{dsh-mneme/lib → lib}/api.js +133 -289
- package/{dsh-mneme/lib → lib}/client.js +1008 -922
- package/{dsh-mneme/lib → lib}/config.js +54 -99
- package/{dsh-mneme/lib → lib}/dream/decisions.js +94 -143
- package/{dsh-mneme/lib → lib}/dream/sleep.js +12 -152
- package/{dsh-mneme/lib → lib}/dream.js +22 -168
- package/{dsh-mneme/src → lib}/hot-memory.js +0 -7
- package/{dsh-mneme/lib → lib}/index.js +45 -42
- package/{dsh-mneme/lib → lib}/inject.js +11 -53
- package/{dsh-mneme/src → lib}/mirror.js +1 -12
- package/{dsh-mneme/lib → lib}/quality-filter.js +1 -3
- package/{dsh-mneme/lib → lib}/reranker.js +1 -4
- package/{dsh-mneme/lib → lib}/service.js +20 -410
- package/{dsh-mneme/src → lib}/settings.js +34 -64
- package/{dsh-mneme/lib → lib}/store.js +38 -450
- package/{dsh-mneme/lib → lib}/summarize.js +10 -10
- package/lib/tools.js +274 -0
- package/package.json +45 -19
- package/{dsh-mneme/scripts → scripts}/e2e-dsh.js +3 -4
- package/{dsh-mneme/scripts → scripts}/sync-lib.js +5 -10
- package/src/api-standalone.js +264 -0
- package/{dsh-mneme/src → src}/api.js +133 -289
- package/{dsh-mneme/src → src}/config.js +54 -99
- package/{dsh-mneme/src → src}/dream/decisions.js +94 -143
- package/{dsh-mneme/src → src}/dream/sleep.js +12 -152
- package/{dsh-mneme/src → src}/dream.js +22 -168
- package/{dsh-mneme/lib → src}/hot-memory.js +0 -7
- package/{dsh-mneme/src → src}/index.js +45 -42
- package/{dsh-mneme/src → src}/inject.js +11 -53
- package/{dsh-mneme/lib → src}/mirror.js +1 -12
- package/{dsh-mneme/src → src}/quality-filter.js +1 -3
- package/{dsh-mneme/src → src}/reranker.js +1 -4
- package/{dsh-mneme/src → src}/service.js +20 -410
- package/{dsh-mneme/lib → src}/settings.js +34 -64
- package/{dsh-mneme/src → src}/store.js +38 -450
- package/{dsh-mneme/src → src}/summarize.js +10 -10
- package/src/tools.js +274 -0
- package/{dsh-mneme/test → test}/api.test.js +45 -156
- package/{dsh-mneme/test → test}/client.test.js +10 -206
- package/{dsh-mneme/test → test}/config.test.js +0 -21
- package/{dsh-mneme/test → test}/dream.test.js +1 -160
- package/{dsh-mneme/test → test}/graph-api.test.js +0 -34
- package/{dsh-mneme/test → test}/hot-memory.test.js +0 -29
- package/test/inject.test.js +120 -0
- package/{dsh-mneme/test → test}/llm-audit.test.js +4 -4
- package/{dsh-mneme/test → test}/reasoning-effort.test.js +0 -27
- package/{dsh-mneme/test → test}/recall-layer.test.js +5 -26
- package/{dsh-mneme/test → test}/reranker.test.js +0 -43
- package/{dsh-mneme/test → test}/service-search.test.js +0 -25
- package/{dsh-mneme/test → test}/service.test.js +0 -106
- package/{dsh-mneme/test → test}/settings.test.js +16 -35
- package/{dsh-mneme/test → test}/sleep.test.js +4 -171
- package/test/standalone-api.test.js +326 -0
- package/{dsh-mneme/test → test}/store.test.js +0 -76
- package/{dsh-mneme/test → test}/summarize.test.js +16 -15
- package/test/tools.test.js +265 -0
- package/.github/workflows/publish.yml +0 -58
- package/.github/workflows/test.yml +0 -32
- package/CHANGELOG.md +0 -91
- package/CONTRIBUTING.md +0 -293
- package/SECURITY.md +0 -718
- package/docs/devlog/2026-08-14-dsh-mneme-dev-log.md +0 -247
- package/docs/devlog/2026-08-15-dsh-mneme-audit-stress-dev-log.md +0 -145
- package/docs/devlog/2026-08-15-dsh-mneme-pipeline-dev-log.md +0 -56
- package/docs/devlog/2026-08-15-dsh-mneme-reflection-dev-log.md +0 -77
- package/docs/devlog/2026-08-15-dsh-mneme-review-fixes-dev-log.md +0 -64
- package/docs/devlog/2026-08-15-dsh-mneme-semantic-dev-log.md +0 -90
- package/dsh-mneme/CHANGELOG.md +0 -419
- package/dsh-mneme/LICENSE +0 -21
- package/dsh-mneme/README.md +0 -501
- package/dsh-mneme/docs/AGENT_MEMORY_RESEARCH.md +0 -183
- package/dsh-mneme/docs/ENTITIES.md +0 -245
- package/dsh-mneme/docs/LOCAL_MODEL.md +0 -141
- package/dsh-mneme/docs/MIGRATION.md +0 -127
- package/dsh-mneme/docs/SEMANTIC.md +0 -256
- package/dsh-mneme/docs/SLEEP.md +0 -163
- package/dsh-mneme/lib/dream/tag-extractor.js +0 -156
- package/dsh-mneme/lib/heat.js +0 -136
- package/dsh-mneme/lib/parser/tag.js +0 -59
- package/dsh-mneme/lib/parser/wiki-link.js +0 -38
- package/dsh-mneme/lib/search/tag-boost.js +0 -61
- package/dsh-mneme/lib/tools.js +0 -458
- package/dsh-mneme/package-lock.json +0 -1936
- package/dsh-mneme/package.json +0 -83
- package/dsh-mneme/scripts/check-sync.js +0 -42
- package/dsh-mneme/src/client.js +0 -2242
- package/dsh-mneme/src/dream/tag-extractor.js +0 -156
- package/dsh-mneme/src/heat.js +0 -136
- package/dsh-mneme/src/parser/tag.js +0 -59
- package/dsh-mneme/src/parser/wiki-link.js +0 -38
- package/dsh-mneme/src/search/tag-boost.js +0 -61
- package/dsh-mneme/src/tools.js +0 -458
- package/dsh-mneme/test/boundary-v0625.test.js +0 -82
- package/dsh-mneme/test/directory.test.js +0 -134
- package/dsh-mneme/test/heat.test.js +0 -148
- package/dsh-mneme/test/inject.test.js +0 -206
- package/dsh-mneme/test/layered-types-stats.test.js +0 -144
- package/dsh-mneme/test/lib-smoke.test.js +0 -109
- package/dsh-mneme/test/normalize-decisions.test.js +0 -120
- package/dsh-mneme/test/provenance.test.js +0 -103
- package/dsh-mneme/test/recall-runs.test.js +0 -93
- package/dsh-mneme/test/sleep-heat.test.js +0 -112
- package/dsh-mneme/test/tag-boost.test.js +0 -125
- package/dsh-mneme/test/tag.test.js +0 -426
- package/dsh-mneme/test/tools.test.js +0 -674
- package/dsh-mneme/test/updated-at-semantics.test.js +0 -113
- package/dsh-mneme/test/wiki-link.test.js +0 -332
- package//346/250/252/345/271/205.png +0 -0
- /package/{dsh-mneme/lib → lib}/commands.js +0 -0
- /package/{dsh-mneme/lib → lib}/dream/clustering.js +0 -0
- /package/{dsh-mneme/lib → lib}/embedding.js +0 -0
- /package/{dsh-mneme/lib → lib}/entities/extractor.js +0 -0
- /package/{dsh-mneme/lib → lib}/local-embedder.js +0 -0
- /package/{dsh-mneme/lib → lib}/search/adaptive.js +0 -0
- /package/{dsh-mneme/lib → lib}/search/bm25.js +0 -0
- /package/{dsh-mneme/lib → lib}/vector-index.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-embed.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-recall.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-rerank.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/stress-dsh.js +0 -0
- /package/{dsh-mneme/src → src}/commands.js +0 -0
- /package/{dsh-mneme/src → src}/dream/clustering.js +0 -0
- /package/{dsh-mneme/src → src}/embedding.js +0 -0
- /package/{dsh-mneme/src → src}/entities/extractor.js +0 -0
- /package/{dsh-mneme/src → src}/local-embedder.js +0 -0
- /package/{dsh-mneme/src → src}/search/adaptive.js +0 -0
- /package/{dsh-mneme/src → src}/search/bm25.js +0 -0
- /package/{dsh-mneme/src → src}/vector-index.js +0 -0
- /package/{dsh-mneme/test → test}/audit.test.js +0 -0
- /package/{dsh-mneme/test → test}/benchmark.test.js +0 -0
- /package/{dsh-mneme/test → test}/clustering.test.js +0 -0
- /package/{dsh-mneme/test → test}/commands.test.js +0 -0
- /package/{dsh-mneme/test → test}/conflict-freeze.test.js +0 -0
- /package/{dsh-mneme/test → test}/entities.test.js +0 -0
- /package/{dsh-mneme/test → test}/epistemic.test.js +0 -0
- /package/{dsh-mneme/test → test}/fnew-0112.test.js +0 -0
- /package/{dsh-mneme/test → test}/fnew-03.test.js +0 -0
- /package/{dsh-mneme/test → test}/helpers/dream-mock.js +0 -0
- /package/{dsh-mneme/test → test}/local-embedder.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-dirty.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-edit-digest.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-generation.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror.test.js +0 -0
- /package/{dsh-mneme/test → test}/peer-blockers.test.js +0 -0
- /package/{dsh-mneme/test → test}/policy-epoch.test.js +0 -0
- /package/{dsh-mneme/test → test}/quality-filter.test.js +0 -0
- /package/{dsh-mneme/test → test}/recall-evals.test.js +0 -0
- /package/{dsh-mneme/test → test}/receipt-chain.test.js +0 -0
- /package/{dsh-mneme/test → test}/reflection.test.js +0 -0
- /package/{dsh-mneme/test → test}/search-fusion.test.js +0 -0
- /package/{dsh-mneme/test → test}/semantic.test.js +0 -0
- /package/{dsh-mneme/test → test}/stress.test.js +0 -0
- /package/{dsh-mneme/test → test}/vector-index.test.js +0 -0
|
@@ -5,9 +5,7 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
|
|
7
7
|
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
-
|
|
9
|
-
// client bundle); lib/ is build output produced by `npm run sync`.
|
|
10
|
-
const clientSource = readFileSync(join(root, "src/client.js"), "utf8");
|
|
8
|
+
const clientSource = readFileSync(join(root, "lib/client.js"), "utf8");
|
|
11
9
|
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
12
10
|
|
|
13
11
|
// The Web client bundle registers itself via __ModuleLoader__.load. DSH
|
|
@@ -20,14 +18,11 @@ test("client bundle registers under the package name", () => {
|
|
|
20
18
|
assert.equal(match[1], pkg.name, "registered id must equal package.json name");
|
|
21
19
|
});
|
|
22
20
|
|
|
23
|
-
// client.js is authored under
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
assert.equal(existsSync(join(root, "src/client.js")), true, "src/client.js must exist");
|
|
21
|
+
// client.js is hand-authored under lib/ only (no src/ counterpart), so the
|
|
22
|
+
// src->lib sync must never prune it.
|
|
23
|
+
test("client bundle is lib-only with no src counterpart", () => {
|
|
24
|
+
assert.equal(existsSync(join(root, "src/client.js")), false, "src/ must not contain client.js");
|
|
28
25
|
assert.equal(existsSync(join(root, "lib/client.js")), true, "lib/client.js must exist");
|
|
29
|
-
const libClient = readFileSync(join(root, "lib/client.js"), "utf8");
|
|
30
|
-
assert.equal(libClient, clientSource, "lib/client.js must equal src/client.js (run `npm run sync`)");
|
|
31
26
|
});
|
|
32
27
|
|
|
33
28
|
// The memory entry lives at the sidebar foot, not in the settings modal: the
|
|
@@ -155,18 +150,18 @@ test("graph toggle uses a node-graph glyph, not the share icon", () => {
|
|
|
155
150
|
});
|
|
156
151
|
|
|
157
152
|
// Every memory feature lives in the main-area library now: the explorer
|
|
158
|
-
// hosts three sub-views (browse /
|
|
153
|
+
// hosts three sub-views (browse / entities / settings) switched by tabs whose
|
|
159
154
|
// labels come from dedicated dictionary keys.
|
|
160
|
-
test("explorer hosts browse,
|
|
161
|
-
for (const key of ["tabMemory", "
|
|
155
|
+
test("explorer hosts browse, entities and settings sub-views", () => {
|
|
156
|
+
for (const key of ["tabMemory", "tabEntities", "tabSettings"]) {
|
|
162
157
|
assert.ok(
|
|
163
158
|
clientSource.includes(`"memory.explorer.${key}"`),
|
|
164
159
|
`sub-view labels must come from memory.explorer.${key}`
|
|
165
160
|
);
|
|
166
161
|
}
|
|
167
162
|
assert.ok(
|
|
168
|
-
clientSource.includes(
|
|
169
|
-
"the
|
|
163
|
+
clientSource.includes("h(EntityPanel, { t, focusEntity: graphFocus, onJumpMemory: jumpToMemory })"),
|
|
164
|
+
"the entity panel must be embedded as a sub-view"
|
|
170
165
|
);
|
|
171
166
|
assert.ok(
|
|
172
167
|
clientSource.includes('h(SettingsContent, { t })'),
|
|
@@ -248,194 +243,3 @@ test("explorer chrome aligns with the host design system", () => {
|
|
|
248
243
|
"pill chips belong to the drawer era and must stay gone"
|
|
249
244
|
);
|
|
250
245
|
});
|
|
251
|
-
|
|
252
|
-
// The v0.6.1 wiki-link feature: the detail pane mounts a BacklinksPanel that
|
|
253
|
-
// fetches the two read-only link endpoints by the selected memory id and
|
|
254
|
-
// renders back/forward rows, each jumping back into the browser.
|
|
255
|
-
test("detail pane mounts a BacklinksPanel backed by the two link endpoints", () => {
|
|
256
|
-
assert.ok(
|
|
257
|
-
clientSource.includes("h(BacklinksPanel, { memory: selected, t, onJump: jumpToMemory })"),
|
|
258
|
-
"the detail pane must mount BacklinksPanel after the actions row"
|
|
259
|
-
);
|
|
260
|
-
assert.ok(
|
|
261
|
-
clientSource.includes("/api/dsh-mneme/wikilinks/backlinks?id="),
|
|
262
|
-
"BacklinksPanel must fetch the backlinks endpoint by memory id"
|
|
263
|
-
);
|
|
264
|
-
assert.ok(
|
|
265
|
-
clientSource.includes("/api/dsh-mneme/wikilinks/forward?id="),
|
|
266
|
-
"BacklinksPanel must fetch the forward-links endpoint by memory id"
|
|
267
|
-
);
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
// Detail content turns [[target]] / [[display|target]] into clickable links:
|
|
271
|
-
// the display text survives, the target title is kept for hover, and a click
|
|
272
|
-
// resolves the title through the resolve endpoint before jumping.
|
|
273
|
-
test("detail content renders wiki-links and resolves them on click", () => {
|
|
274
|
-
assert.ok(
|
|
275
|
-
clientSource.includes('className: "mneme-wikilink"'),
|
|
276
|
-
"inline [[target]] links must use the .mneme-wikilink style"
|
|
277
|
-
);
|
|
278
|
-
assert.ok(
|
|
279
|
-
clientSource.includes("/api/dsh-mneme/wikilinks/resolve?title="),
|
|
280
|
-
"clicking a wiki-link must resolve the title via the resolve endpoint"
|
|
281
|
-
);
|
|
282
|
-
assert.ok(
|
|
283
|
-
clientSource.includes('className: "mneme-backlinks"'),
|
|
284
|
-
"the panel must render inside a .mneme-backlinks block"
|
|
285
|
-
);
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
// v0.6.2 tag system: the detail pane renders tags as clickable chips wired to
|
|
289
|
-
// the memory/tags endpoints, and a tag: query always goes server-side.
|
|
290
|
-
test("detail pane renders editable tag chips backed by the tags endpoint", () => {
|
|
291
|
-
assert.ok(
|
|
292
|
-
clientSource.includes("/api/dsh-mneme/memory/tags?id="),
|
|
293
|
-
"the panel must fetch the selected memory's tags via the GET endpoint"
|
|
294
|
-
);
|
|
295
|
-
assert.ok(
|
|
296
|
-
clientSource.includes('method: "POST"'),
|
|
297
|
-
"adding/removing a tag must POST to the tags endpoint"
|
|
298
|
-
);
|
|
299
|
-
assert.ok(
|
|
300
|
-
clientSource.includes("onClick: () => onTagClick(tag)"),
|
|
301
|
-
"a tag chip click must trigger a tag: search"
|
|
302
|
-
);
|
|
303
|
-
assert.ok(
|
|
304
|
-
clientSource.includes("tagManual"),
|
|
305
|
-
"the panel must read the manualTagEnabled gate to hide editing"
|
|
306
|
-
);
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
test("tag: queries run server-side regardless of the semantic toggle", () => {
|
|
310
|
-
assert.ok(
|
|
311
|
-
clientSource.includes('(!semantic && !q.startsWith("tag:"))'),
|
|
312
|
-
"tag: must bypass the semantic toggle and use the search endpoint"
|
|
313
|
-
);
|
|
314
|
-
assert.ok(
|
|
315
|
-
clientSource.includes('setQuery(`tag:${tag}`)'),
|
|
316
|
-
"a tag chip click must set the query to tag:<tag>"
|
|
317
|
-
);
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
test("tag UI ships localized labels in both dictionaries", () => {
|
|
321
|
-
for (const key of ["tagAdd", "tagRemove", "tagPlaceholder", "tagsEmpty"]) {
|
|
322
|
-
assert.ok(
|
|
323
|
-
clientSource.includes(`"memory.explorer.${key}"`),
|
|
324
|
-
`memory.explorer.${key} key must exist`
|
|
325
|
-
);
|
|
326
|
-
}
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
// v0.6.3 directory view: a tag-folder tree backed by the directory endpoint.
|
|
330
|
-
// Folders accordion via data-expanded controlled by MemoryExplorer-lifted
|
|
331
|
-
// state; a memory entry click jumps back into the browser via onJump.
|
|
332
|
-
test("directory sub-view mounts DirectoryPanel backed by the directory endpoint", () => {
|
|
333
|
-
assert.ok(
|
|
334
|
-
clientSource.includes("h(DirectoryPanel, { t, onJump: jumpToMemory, collapsed: dirCollapsed, setCollapsed: setDirCollapsed })"),
|
|
335
|
-
"the directory panel must be embedded as a sub-view with lifted collapse state"
|
|
336
|
-
);
|
|
337
|
-
assert.ok(
|
|
338
|
-
clientSource.includes("/api/dsh-mneme/directory"),
|
|
339
|
-
"DirectoryPanel must fetch the directory endpoint"
|
|
340
|
-
);
|
|
341
|
-
assert.ok(
|
|
342
|
-
clientSource.includes('className: "mneme-directory"'),
|
|
343
|
-
"the directory panel must render inside the .mneme-directory block"
|
|
344
|
-
);
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
test("directory folders accordion via data-expanded driven by MemoryExplorer state", () => {
|
|
348
|
-
assert.ok(
|
|
349
|
-
clientSource.includes('"data-expanded": String(open)'),
|
|
350
|
-
"folder expansion must be driven by the data-expanded attribute"
|
|
351
|
-
);
|
|
352
|
-
assert.ok(
|
|
353
|
-
clientSource.includes("const [dirCollapsed, setDirCollapsed] = useState({})"),
|
|
354
|
-
"collapse state must live in MemoryExplorer so tab switches keep it"
|
|
355
|
-
);
|
|
356
|
-
assert.ok(
|
|
357
|
-
clientSource.includes('"aria-expanded": String(open)'),
|
|
358
|
-
"folder headers must announce their expanded state"
|
|
359
|
-
);
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
test("directory entry click jumps to the memory detail via onJump", () => {
|
|
363
|
-
assert.ok(
|
|
364
|
-
clientSource.includes("onClick: () => onJump(m)"),
|
|
365
|
-
"clicking a memory entry must call onJump with the memory"
|
|
366
|
-
);
|
|
367
|
-
assert.ok(
|
|
368
|
-
clientSource.includes("formatDate(m.updated_at || m.created_at)"),
|
|
369
|
-
"entries must render a timestamp via formatDate"
|
|
370
|
-
);
|
|
371
|
-
assert.ok(
|
|
372
|
-
clientSource.includes("memory.directory.untagged"),
|
|
373
|
-
"untagged memories must be bucketed under a labeled fallback folder"
|
|
374
|
-
);
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
test("directory ships localized labels in both dictionaries", () => {
|
|
378
|
-
for (const key of ["memory.explorer.tabDirectory", "memory.directory.untagged", "memory.directory.loading", "memory.directory.empty"]) {
|
|
379
|
-
assert.ok(
|
|
380
|
-
clientSource.includes(`"${key}"`),
|
|
381
|
-
`${key} key must exist`
|
|
382
|
-
);
|
|
383
|
-
}
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
// issue #35: the delete confirm step must not depend on the host window.confirm —
|
|
387
|
-
// in the DSH web host the native dialog is unreliable/blocked, so clicks
|
|
388
|
-
// appeared to do nothing. Deletion is now an inline two-step confirm
|
|
389
|
-
// (✕ → 确认? → DELETE), gated per row by confirmingId.
|
|
390
|
-
test("delete flow drops window.confirm for an inline two-step confirm", () => {
|
|
391
|
-
assert.equal(
|
|
392
|
-
/window\.confirm\(/.test(clientSource),
|
|
393
|
-
false,
|
|
394
|
-
"the delete path must not call the host window.confirm"
|
|
395
|
-
);
|
|
396
|
-
assert.ok(
|
|
397
|
-
clientSource.includes("confirmingId"),
|
|
398
|
-
"a per-row confirm state must gate the second click"
|
|
399
|
-
);
|
|
400
|
-
assert.ok(
|
|
401
|
-
clientSource.includes('"directory.deleteConfirmShort"'),
|
|
402
|
-
"the short inline confirm label must be localized"
|
|
403
|
-
);
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
// issue #35: a failed DELETE (network / 401 / 500 / deleted!==true) must be
|
|
407
|
-
// visible — the old code swallowed it with a bare return and .catch(() => {}).
|
|
408
|
-
test("delete failure surfaces visible error feedback", () => {
|
|
409
|
-
assert.ok(
|
|
410
|
-
clientSource.includes("delErrorId"),
|
|
411
|
-
"a per-row error state must track failed deletes"
|
|
412
|
-
);
|
|
413
|
-
assert.ok(
|
|
414
|
-
clientSource.includes('"directory.deleteError"'),
|
|
415
|
-
"the delete error message must be localized"
|
|
416
|
-
);
|
|
417
|
-
assert.ok(
|
|
418
|
-
clientSource.includes(".mneme-dir-error"),
|
|
419
|
-
"a failed row must get a visible error style"
|
|
420
|
-
);
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
// issue #38: the sidebar foot trigger must be disableable so it stops colliding
|
|
424
|
-
// with other plugins that also claim the bottom-left footer slot (dsh-cost-meter
|
|
425
|
-
// et al). The client reads the settings-over-config toggle from the /config
|
|
426
|
-
// endpoint and renders no button when it is off; the 记忆库 conversation tab is
|
|
427
|
-
// unaffected, so nothing is lost by hiding the entry.
|
|
428
|
-
test("sidebar trigger is hidden when showSidebarTrigger is false (issue #38)", () => {
|
|
429
|
-
assert.ok(
|
|
430
|
-
clientSource.includes('apiFetch("/api/dsh-mneme/config")'),
|
|
431
|
-
"trigger must read the effective config from /api/dsh-mneme/config"
|
|
432
|
-
);
|
|
433
|
-
assert.ok(
|
|
434
|
-
/d\.config\?\.showSidebarTrigger !== false/.test(clientSource),
|
|
435
|
-
"trigger must hide when showSidebarTrigger is explicitly false and stay visible by default"
|
|
436
|
-
);
|
|
437
|
-
assert.ok(
|
|
438
|
-
clientSource.includes('"memory.settings.sidebarTriggerTitle"'),
|
|
439
|
-
"settings panel must expose a sidebar-trigger toggle"
|
|
440
|
-
);
|
|
441
|
-
});
|
|
@@ -27,13 +27,6 @@ test("dream sliding window + implicit keep config defaults and bounds (v0.4.4)",
|
|
|
27
27
|
assert.equal(off.dreamImplicitKeep, false, "implicit keep can be disabled");
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
test("injectTimePrefix is opt-in: default off, enabled explicitly (issue #34)", () => {
|
|
31
|
-
const cfg = Config({});
|
|
32
|
-
assert.equal(cfg.injectTimePrefix, false, "time prefix defaults to false (behavior unchanged)");
|
|
33
|
-
const on = Config({ injectTimePrefix: true });
|
|
34
|
-
assert.equal(on.injectTimePrefix, true, "explicit opt-in enables it");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
30
|
test("dream explicit coverage threshold config defaults and bounds (v0.4.4 fix)", () => {
|
|
38
31
|
const cfg = Config({});
|
|
39
32
|
assert.equal(cfg.dreamMinExplicitCoverage, 0.5, "coverage threshold defaults to 0.5");
|
|
@@ -55,17 +48,3 @@ test("startup probe: the reranker module never statically imports transformers/o
|
|
|
55
48
|
);
|
|
56
49
|
assert.match(src, /await import\("@huggingface\/transformers"\)/, "transformers.js loads lazily via dynamic import");
|
|
57
50
|
});
|
|
58
|
-
|
|
59
|
-
test("showSidebarTrigger is on by default, disabled explicitly (issue #38)", () => {
|
|
60
|
-
const cfg = Config({});
|
|
61
|
-
assert.equal(cfg.showSidebarTrigger, true, "sidebar trigger defaults to visible (behavior unchanged)");
|
|
62
|
-
const off = Config({ showSidebarTrigger: false });
|
|
63
|
-
assert.equal(off.showSidebarTrigger, false, "explicit false hides the trigger");
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("escapePromptVariables is on by default, disabled explicitly (issue #40)", () => {
|
|
67
|
-
const cfg = Config({});
|
|
68
|
-
assert.equal(cfg.escapePromptVariables, true, "{{...}} escaping defaults to on (protects interpolate)");
|
|
69
|
-
const off = Config({ escapePromptVariables: false });
|
|
70
|
-
assert.equal(off.escapePromptVariables, false, "explicit false passes {{...}} through verbatim");
|
|
71
|
-
});
|
|
@@ -137,100 +137,6 @@ test("merge across types rejects", () => {
|
|
|
137
137
|
assert.ok(errors.some((e) => e.includes("multiple types")), "multi-type error present");
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
-
// --- Issue #26 (P0): skipInvalid — 单条非法决策跳过,合法子集照常应用 ---------
|
|
141
|
-
|
|
142
|
-
test("validateDecisions skipInvalid: a single invalid decision is skipped, valid subset survives", () => {
|
|
143
|
-
const snap = new Map([
|
|
144
|
-
["p", { id: "p", type: "preference", title: "语言", content: "中文", importance: 3, archived: false, forgotten: false }],
|
|
145
|
-
["j", { id: "j", type: "project", title: "插件", content: "内容", importance: 3, archived: false, forgotten: false }],
|
|
146
|
-
["a", { id: "a", type: "project", title: "旧A", content: "过时A", importance: 3, archived: false, forgotten: false }],
|
|
147
|
-
["b", { id: "b", type: "project", title: "旧B", content: "过时B", importance: 3, archived: false, forgotten: false }]
|
|
148
|
-
]);
|
|
149
|
-
const decisions = [
|
|
150
|
-
{ action: "merge", ids: ["p", "j"], keepSource: "p", title: "跨类型", content: "不应合并", importance: 4 },
|
|
151
|
-
{ action: "archive", ids: ["a"], reason: "stale" },
|
|
152
|
-
{ action: "archive", ids: ["b"], reason: "stale" }
|
|
153
|
-
];
|
|
154
|
-
const { ok, errors, skipped } = validateDecisions(decisions, snap, { skipInvalid: true });
|
|
155
|
-
assert.equal(ok, true, `valid subset should survive, got: ${errors.join("; ")}`);
|
|
156
|
-
assert.equal(skipped.length, 1, "cross-type merge recorded as skipped");
|
|
157
|
-
assert.equal(skipped[0].index, 0, "the skipped one is the cross-type merge");
|
|
158
|
-
assert.match(skipped[0].error, /multiple types/, "skip reason mentions types");
|
|
159
|
-
// invalid merge spliced out of the caller's array; valid archives + implicit
|
|
160
|
-
// keeps for p/j survive (p/j were left unclaimed by the skipped merge)
|
|
161
|
-
assert.deepEqual(decisions.map((d) => d.action), ["archive", "archive", "keep", "keep"]);
|
|
162
|
-
assert.deepEqual(decisions[0].ids, ["a"]);
|
|
163
|
-
assert.ok(decisions.some((d) => d.action === "keep" && d.ids.includes("p")), "p auto-kept");
|
|
164
|
-
assert.ok(decisions.some((d) => d.action === "keep" && d.ids.includes("j")), "j auto-kept");
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test("validateDecisions skipInvalid: an all-invalid batch still rejects (coverage floor guards truncation)", () => {
|
|
168
|
-
const snap = new Map([
|
|
169
|
-
["p", { id: "p", type: "preference", title: "语言", content: "中文", importance: 3, archived: false, forgotten: false }],
|
|
170
|
-
["j", { id: "j", type: "project", title: "插件", content: "内容", importance: 3, archived: false, forgotten: false }],
|
|
171
|
-
["d1", { id: "d1", type: "decision", title: "决定", content: "内容D", importance: 3, archived: false, forgotten: false }],
|
|
172
|
-
["b", { id: "b", type: "project", title: "旧B", content: "过时B", importance: 3, archived: false, forgotten: false }]
|
|
173
|
-
]);
|
|
174
|
-
// both decisions are cross-type merges → both skipped → valid claims = 0
|
|
175
|
-
const decisions = [
|
|
176
|
-
{ action: "merge", ids: ["p", "j"], keepSource: "p", title: "跨类型", content: "不应合并", importance: 4 },
|
|
177
|
-
{ action: "merge", ids: ["d1", "b"], keepSource: "d1", title: "跨类型2", content: "不应合并", importance: 4 }
|
|
178
|
-
];
|
|
179
|
-
const { ok, errors, skipped } = validateDecisions(decisions, snap, { skipInvalid: true });
|
|
180
|
-
assert.equal(ok, false, "no valid decisions left → whole batch rejected");
|
|
181
|
-
assert.equal(skipped.length, 2);
|
|
182
|
-
assert.ok(errors.some((e) => e.includes("coverage")), "coverage error present");
|
|
183
|
-
assert.equal(decisions.length, 2, "rejected batch left untouched (splice only on the success path)");
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
test("validateDecisions skipInvalid: runaway update count still rejects the whole batch (global cap)", () => {
|
|
187
|
-
const snap = new Map([
|
|
188
|
-
["a", { id: "a", type: "project", title: "A", content: "旧A", importance: 3, archived: false, forgotten: false, created_at: "2020-01-01T00:00:00.000Z" }],
|
|
189
|
-
["b", { id: "b", type: "project", title: "B", content: "旧B", importance: 3, archived: false, forgotten: false, created_at: "2020-01-01T00:00:00.000Z" }],
|
|
190
|
-
["c", { id: "c", type: "project", title: "C", content: "旧C", importance: 3, archived: false, forgotten: false, created_at: "2020-01-01T00:00:00.000Z" }]
|
|
191
|
-
]);
|
|
192
|
-
const decisions = [
|
|
193
|
-
{ action: "update", ids: ["a"], content: "新A" },
|
|
194
|
-
{ action: "update", ids: ["b"], content: "新B" },
|
|
195
|
-
{ action: "update", ids: ["c"], content: "新C" }
|
|
196
|
-
];
|
|
197
|
-
const { ok, errors } = validateDecisions(decisions, snap, { skipInvalid: true });
|
|
198
|
-
assert.equal(ok, false, "3 updates > default cap 2 → still rejects");
|
|
199
|
-
assert.ok(errors.some((e) => e.includes("too many update decisions")), "global cap error present");
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
// --- Issue #26 (P1): allowCrossTypeMerge — 显式放宽跨类型合并 -----------------
|
|
203
|
-
|
|
204
|
-
test("validateDecisions allowCrossTypeMerge: cross-type merge is allowed when the flag is on", () => {
|
|
205
|
-
const snap = new Map([
|
|
206
|
-
["p", { id: "p", type: "preference", title: "语言", content: "中文", importance: 3, archived: false, forgotten: false }],
|
|
207
|
-
["j", { id: "j", type: "project", title: "插件", content: "内容", importance: 3, archived: false, forgotten: false }]
|
|
208
|
-
]);
|
|
209
|
-
const decisions = [
|
|
210
|
-
{ action: "merge", ids: ["p", "j"], keepSource: "p", title: "合并", content: "合并内容", importance: 4 }
|
|
211
|
-
];
|
|
212
|
-
const { ok, errors } = validateDecisions(decisions, snap, { allowCrossTypeMerge: true });
|
|
213
|
-
assert.equal(ok, true, `cross-type merge allowed with the flag, got: ${errors.join("; ")}`);
|
|
214
|
-
assert.equal(decisions.length, 1, "no keep appended (both snapshot ids claimed)");
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
test("validateDecisions allowCrossTypeMerge off + skipInvalid: cross-type merge is skipped, not applied", () => {
|
|
218
|
-
const snap = new Map([
|
|
219
|
-
["p", { id: "p", type: "preference", title: "语言", content: "中文", importance: 3, archived: false, forgotten: false }],
|
|
220
|
-
["j", { id: "j", type: "project", title: "插件", content: "内容", importance: 3, archived: false, forgotten: false }],
|
|
221
|
-
["a", { id: "a", type: "project", title: "旧A", content: "过时A", importance: 3, archived: false, forgotten: false }],
|
|
222
|
-
["b", { id: "b", type: "project", title: "旧B", content: "过时B", importance: 3, archived: false, forgotten: false }]
|
|
223
|
-
]);
|
|
224
|
-
const decisions = [
|
|
225
|
-
{ action: "merge", ids: ["p", "j"], keepSource: "p", title: "跨类型", content: "不应合并", importance: 4 },
|
|
226
|
-
{ action: "archive", ids: ["a"], reason: "stale" },
|
|
227
|
-
{ action: "archive", ids: ["b"], reason: "stale" }
|
|
228
|
-
];
|
|
229
|
-
const { ok, skipped } = validateDecisions(decisions, snap, { skipInvalid: true, allowCrossTypeMerge: false });
|
|
230
|
-
assert.equal(ok, true);
|
|
231
|
-
assert.equal(skipped.length, 1, "flag off → cross-type merge still skipped");
|
|
232
|
-
});
|
|
233
|
-
|
|
234
140
|
function dreamSetup() {
|
|
235
141
|
const store = createStore(":memory:");
|
|
236
142
|
const service = createService({ store, mirror: null, config: {} });
|
|
@@ -577,69 +483,6 @@ test("runDream fails safe on invalid decisions", async () => {
|
|
|
577
483
|
store.close();
|
|
578
484
|
});
|
|
579
485
|
|
|
580
|
-
// --- Issue #26: 部分非法决策 → 跳过非法、应用合法子集、run 记为 degraded ----
|
|
581
|
-
|
|
582
|
-
test("runDream applies the valid subset and records degraded when some decisions are invalid (Issue #26)", async () => {
|
|
583
|
-
const { store, service } = dreamSetup();
|
|
584
|
-
const a = service.saveWithDedupe({ type: "project", title: "项目A", content: "内容A", importance: 3 });
|
|
585
|
-
const b = service.saveWithDedupe({ type: "project", title: "项目B", content: "内容B", importance: 3 });
|
|
586
|
-
const c = service.saveWithDedupe({ type: "preference", title: "偏好C", content: "内容C", importance: 3 });
|
|
587
|
-
const d = service.saveWithDedupe({ type: "preference", title: "偏好D", content: "内容D", importance: 3 });
|
|
588
|
-
// 2 个合法同类型 merge + 1 个非法跨类型 merge(a×c,且 a/c 已被合法决策占用)
|
|
589
|
-
const decisions = [
|
|
590
|
-
{ action: "merge", ids: [a.memory.id, b.memory.id], title: "项目总览", content: "AB 合并", importance: 4, keepSource: a.memory.id },
|
|
591
|
-
{ action: "merge", ids: [c.memory.id, d.memory.id], title: "偏好总览", content: "CD 合并", importance: 4, keepSource: c.memory.id },
|
|
592
|
-
{ action: "merge", ids: [a.memory.id, c.memory.id], title: "非法跨类型", content: "不应合并", importance: 4, keepSource: a.memory.id }
|
|
593
|
-
];
|
|
594
|
-
const warnings = [];
|
|
595
|
-
const ctx = mockCtx({ onConsolidation: () => JSON.stringify(decisions) });
|
|
596
|
-
ctx.logger.warn = (m) => warnings.push(m);
|
|
597
|
-
const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
|
|
598
|
-
const result = await dream.runDream(ctx, service, { dreamProvider: "deepseek", dreamModel: "deepseek-chat" });
|
|
599
|
-
assert.equal(result.status, "degraded", "partial-invalid run recorded as degraded");
|
|
600
|
-
assert.equal(result.ok, true, "degraded is ok for the scheduler baseline");
|
|
601
|
-
assert.equal(result.applied, 2, "2 valid merges applied");
|
|
602
|
-
assert.equal(result.skipped.length, 1, "cross-type merge skipped");
|
|
603
|
-
// valid merges landed
|
|
604
|
-
assert.equal(store.getById(a.memory.id).title, "项目总览", "keeper A updated");
|
|
605
|
-
assert.equal(store.getById(b.memory.id).archived, true, "B archived");
|
|
606
|
-
assert.equal(store.getById(c.memory.id).title, "偏好总览", "keeper C updated");
|
|
607
|
-
assert.equal(store.getById(d.memory.id).archived, true, "D archived");
|
|
608
|
-
// audit row records degraded + the skipped decision, never a fake ok
|
|
609
|
-
const run = store.listDreamRuns()[0];
|
|
610
|
-
assert.equal(run.status, "degraded", "audit row records degraded");
|
|
611
|
-
assert.equal(run.applied, 2, "audit row applied count");
|
|
612
|
-
assert.equal(run.outcome.skipped.length, 1, "audit outcome records the skipped decision");
|
|
613
|
-
assert.match(run.error, /skipped 1 invalid decision/, "audit error explains the skip");
|
|
614
|
-
assert.ok(warnings.some((m) => m.includes("skipping invalid decision")), "skip logged");
|
|
615
|
-
store.close();
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
test("runDream with dreamSkipInvalid=false keeps the old whole-batch rejection (Issue #26 opt-out)", async () => {
|
|
619
|
-
const { store, service } = dreamSetup();
|
|
620
|
-
const a = service.saveWithDedupe({ type: "project", title: "项目A", content: "内容A", importance: 3 });
|
|
621
|
-
const b = service.saveWithDedupe({ type: "project", title: "项目B", content: "内容B", importance: 3 });
|
|
622
|
-
const c = service.saveWithDedupe({ type: "preference", title: "偏好C", content: "内容C", importance: 3 });
|
|
623
|
-
const d = service.saveWithDedupe({ type: "preference", title: "偏好D", content: "内容D", importance: 3 });
|
|
624
|
-
const decisions = [
|
|
625
|
-
{ action: "merge", ids: [a.memory.id, c.memory.id], title: "非法跨类型", content: "不应合并", importance: 4, keepSource: a.memory.id },
|
|
626
|
-
{ action: "merge", ids: [b.memory.id, d.memory.id], title: "项目B", content: "合并", importance: 4, keepSource: b.memory.id }
|
|
627
|
-
];
|
|
628
|
-
const warnings = [];
|
|
629
|
-
const ctx = mockCtx({ onConsolidation: () => JSON.stringify(decisions) });
|
|
630
|
-
ctx.logger.warn = (m) => warnings.push(m);
|
|
631
|
-
const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
|
|
632
|
-
const result = await dream.runDream(ctx, service, {
|
|
633
|
-
dreamProvider: "deepseek", dreamModel: "deepseek-chat",
|
|
634
|
-
dreamSkipInvalid: false
|
|
635
|
-
});
|
|
636
|
-
assert.equal(result.ok, false, "opt-out rejects the whole batch");
|
|
637
|
-
assert.equal(store.listDreamRuns()[0].status, "failed", "audited as failed");
|
|
638
|
-
assert.equal(store.getById(a.memory.id).content, "内容A", "nothing changed");
|
|
639
|
-
assert.equal(store.getById(b.memory.id).archived, false, "valid-looking decision also not applied");
|
|
640
|
-
store.close();
|
|
641
|
-
});
|
|
642
|
-
|
|
643
486
|
// --- item ①: CAS guard against concurrent edits ----------------------------
|
|
644
487
|
|
|
645
488
|
test("applyDecisions CAS: merge onto a concurrently-edited target is skipped as a conflict, not applied", () => {
|
|
@@ -1027,9 +870,7 @@ test("Bug8: runDream records llm_audit_logs rows for consolidation and summary",
|
|
|
1027
870
|
assert.deepEqual(summarize.related_memory_ids, [], "summary audit has no related ids");
|
|
1028
871
|
for (const row of rows) {
|
|
1029
872
|
assert.equal(row.status, "success");
|
|
1030
|
-
|
|
1031
|
-
// default — the audit must reflect the actually-used config route.
|
|
1032
|
-
assert.equal(row.model_id, "deepseek:deepseek-chat");
|
|
873
|
+
assert.equal(row.model_id, "mock:stress-model");
|
|
1033
874
|
assert.ok(Number.isInteger(row.duration_ms) && row.duration_ms >= 0, "duration recorded");
|
|
1034
875
|
assert.equal(row.input_tokens, 0);
|
|
1035
876
|
assert.equal(row.output_tokens, 0);
|
|
@@ -173,37 +173,3 @@ test("ego 2-hop over 100+ nodes stays under 50ms (spec §7)", async () => {
|
|
|
173
173
|
assert.ok(data.edges.length > 0);
|
|
174
174
|
assert.ok(ms < 50, `2-hop query took ${ms.toFixed(1)}ms, spec budget is 50ms`);
|
|
175
175
|
});
|
|
176
|
-
|
|
177
|
-
test("ego node heat projects from linked memory heat (v0.7.0)", async () => {
|
|
178
|
-
const { routes, service } = setup();
|
|
179
|
-
const mem = service.saveWithDedupe({ type: "history", title: "test", content: "test entity heat", importance: 3 });
|
|
180
|
-
const entity = service.createEntity({ name: "E", type: "project" });
|
|
181
|
-
service.saveRelation({ from_entity: entity.id, to_entity: entity.id, relation_type: "evidence", memory_id: mem.memory.id });
|
|
182
|
-
const route = routes.find((r) => r.path === "/api/dsh-mneme/semantic/graph/ego");
|
|
183
|
-
assert.ok(route, "ego route exists");
|
|
184
|
-
const res = new FakeRes();
|
|
185
|
-
await route.handler(req("/api/dsh-mneme/semantic/graph/ego?entity=E"), res);
|
|
186
|
-
const data = JSON.parse(res.body);
|
|
187
|
-
const node = data.nodes.find((n) => n.name === "E");
|
|
188
|
-
assert.ok(typeof node.heat === "number" && node.heat >= 0 && node.heat <= 1, "entity node has a heat value");
|
|
189
|
-
assert.ok(Math.abs(node.heat - 1) < 0.01, "fresh memory -> heat ~1.0 -> entity heat ~1.0");
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
test("ego node heat is null when heatEnabled=false (v0.7.0)", async () => {
|
|
193
|
-
const store = createStore(":memory:");
|
|
194
|
-
const service = createService({ store, mirror: null, config: { heatEnabled: false } });
|
|
195
|
-
const settings = createSettings(store.db);
|
|
196
|
-
const commands = { add: () => {}, remove: () => {}, list: () => [] };
|
|
197
|
-
const routes = [];
|
|
198
|
-
const ctx = { webServer: { register(route) { routes.push(route); return () => {}; } } };
|
|
199
|
-
createApi(ctx, service, settings, commands, null, undefined, "");
|
|
200
|
-
const mem = service.saveWithDedupe({ type: "history", title: "关联记忆", content: "实体关联", importance: 3 });
|
|
201
|
-
const entity = service.createEntity({ name: "F", type: "project" });
|
|
202
|
-
service.saveRelation({ from_entity: entity.id, to_entity: entity.id, relation_type: "evidence", memory_id: mem.memory.id });
|
|
203
|
-
const route = routes.find((r) => r.path === "/api/dsh-mneme/semantic/graph/ego");
|
|
204
|
-
const res = new FakeRes();
|
|
205
|
-
await route.handler(req("/api/dsh-mneme/semantic/graph/ego?entity=F"), res);
|
|
206
|
-
const data = JSON.parse(res.body);
|
|
207
|
-
assert.equal(data.nodes.find((n) => n.name === "F").heat, null, "heat disabled → entity heat is null");
|
|
208
|
-
store.close();
|
|
209
|
-
});
|
|
@@ -49,35 +49,6 @@ test("estimateTokens counts CJK heavier than ASCII", () => {
|
|
|
49
49
|
assert.ok(estimateTokens("中文内容") > estimateTokens("abcd"));
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
-
// --- entry defense: non-positive / non-integer bounds fall back to defaults ---
|
|
53
|
-
// Bug: createHotMemory({ maxRounds: -1 }) made the eviction while-loop
|
|
54
|
-
// `while (buffer.length > maxRounds)` unbounded — after the buffer emptied,
|
|
55
|
-
// `0 > -1` stayed true and buffer.shift() on an empty array is a no-op, so
|
|
56
|
-
// every add() spun forever. Non-integer values (1.5, NaN, null) were also
|
|
57
|
-
// silently wrong. The fix clamps them to the 5/2000 defaults at the door.
|
|
58
|
-
|
|
59
|
-
test("hot memory falls back to maxRounds=5 for non-positive/invalid values", () => {
|
|
60
|
-
for (const bad of [0, -1, 1.5, NaN, null]) {
|
|
61
|
-
const hot = createHotMemory({ maxRounds: bad, maxTokens: 10000 });
|
|
62
|
-
for (let i = 0; i < 8; i++) hot.add({ query: `第${i}轮`, response: "x" });
|
|
63
|
-
assert.equal(hot.rounds().length, 5, `maxRounds=${bad} must fall back to 5, no infinite loop`);
|
|
64
|
-
assert.ok(hot.getContext().includes("第7轮"), `maxRounds=${bad}: newest round survives`);
|
|
65
|
-
assert.ok(!hot.getContext().includes("第0轮"), `maxRounds=${bad}: oldest round evicted`);
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test("hot memory falls back to maxTokens=2000 for non-positive/infinite values", () => {
|
|
70
|
-
for (const bad of [0, -1, Infinity]) {
|
|
71
|
-
const hot = createHotMemory({ maxRounds: 50, maxTokens: bad });
|
|
72
|
-
// 50 rounds at ~74 tokens each blow a 2000-token budget; the fallback must
|
|
73
|
-
// evict into (1, 50). A broken budget of 0/-1 would squeeze to 1 round and
|
|
74
|
-
// Infinity would keep all 50 — both are the pre-fix behavior.
|
|
75
|
-
for (let i = 0; i < 50; i++) hot.add({ query: `第${i}轮`, response: "长回答".repeat(40) });
|
|
76
|
-
const n = hot.rounds().length;
|
|
77
|
-
assert.ok(n > 1 && n < 50, `maxTokens=${bad} falls back to 2000 (kept ${n} rounds)`);
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
|
|
81
52
|
// --- service-level: BM25 fusion + semantic dedup + selective injection ---
|
|
82
53
|
|
|
83
54
|
function toyVec(text) {
|