@modusensus/dsh-mneme 0.4.5 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/lib/api.js +40 -2
- package/lib/config.js +35 -0
- package/lib/dream.js +86 -6
- package/lib/embedding.js +59 -2
- package/lib/index.js +68 -1
- package/lib/inject.js +79 -4
- package/lib/quality-filter.js +123 -0
- package/lib/service.js +214 -13
- package/lib/store.js +212 -5
- package/lib/summarize.js +65 -7
- package/lib/vector-index.js +12 -2
- package/package.json +1 -1
- package/src/api.js +40 -2
- package/src/config.js +35 -0
- package/src/dream.js +86 -6
- package/src/embedding.js +59 -2
- package/src/index.js +68 -1
- package/src/inject.js +79 -4
- package/src/quality-filter.js +123 -0
- package/src/service.js +214 -13
- package/src/store.js +212 -5
- package/src/summarize.js +65 -7
- package/src/vector-index.js +12 -2
- package/test/api.test.js +84 -0
- package/test/dream.test.js +52 -0
- package/test/inject.test.js +21 -0
- package/test/llm-audit.test.js +279 -0
- package/test/mirror-edit-digest.test.js +3 -1
- package/test/quality-filter.test.js +118 -0
- package/test/service.test.js +133 -2
- package/test/vector-index.test.js +22 -6
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createStore } from "../src/store.js";
|
|
4
|
+
import { createService } from "../src/service.js";
|
|
5
|
+
import {
|
|
6
|
+
evaluateMemoryQuality,
|
|
7
|
+
META_MEMORY_RE,
|
|
8
|
+
textSimilarity,
|
|
9
|
+
dedupRatio
|
|
10
|
+
} from "../src/quality-filter.js";
|
|
11
|
+
|
|
12
|
+
// Bug7: rule-based memory quality filter. Gated on
|
|
13
|
+
// config.memoryQualityFilter.enabled === true — raw configs (`{}`) keep the
|
|
14
|
+
// legacy behavior. Score bands:
|
|
15
|
+
// >= 60 stored normally
|
|
16
|
+
// 30 .. < 60 quality_score persisted, injection ranked by importance × score/100
|
|
17
|
+
// < 30 archived + tagged low_quality (explicit search still recalls it)
|
|
18
|
+
|
|
19
|
+
function setup(over = {}) {
|
|
20
|
+
const store = createStore(":memory:");
|
|
21
|
+
const service = createService({ store, mirror: null, config: { memoryQualityFilter: { enabled: true }, ...over } });
|
|
22
|
+
return { store, service };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test("evaluateMemoryQuality: meta-memory scores below 60 (degraded, not archived)", () => {
|
|
26
|
+
const { score, tags } = evaluateMemoryQuality({
|
|
27
|
+
type: "history",
|
|
28
|
+
title: "对话总结",
|
|
29
|
+
content: "总结一下刚才的对话,需要记住以下要点"
|
|
30
|
+
});
|
|
31
|
+
assert.ok(score >= 30 && score < 60, `meta memory should be degraded (30..60), got ${score}`);
|
|
32
|
+
assert.ok(tags.includes("meta"), "tagged meta");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("evaluateMemoryQuality: short content is archived (< 30)", () => {
|
|
36
|
+
const { score, tags } = evaluateMemoryQuality({ type: "preference", title: "语言", content: "短" });
|
|
37
|
+
assert.ok(score < 30, `short content should be archived, got ${score}`);
|
|
38
|
+
assert.ok(tags.includes("short_content"));
|
|
39
|
+
assert.ok(tags.includes("low_quality"));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("evaluateMemoryQuality: near-duplicate of a recent memory is archived", () => {
|
|
43
|
+
const { score, tags } = evaluateMemoryQuality(
|
|
44
|
+
{ type: "preference", title: "重复", content: "猫咪喜欢在阳台晒太阳并打盹" },
|
|
45
|
+
{ recentContents: ["猫咪喜欢在阳台晒太阳并打盹"] }
|
|
46
|
+
);
|
|
47
|
+
assert.ok(score < 30, `duplicate should be archived, got ${score}`);
|
|
48
|
+
assert.ok(tags.includes("duplicate"));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("saveWithDedupe: meta memory persists quality_score < 60 and is not archived", () => {
|
|
52
|
+
const { store, service } = setup();
|
|
53
|
+
const { memory } = service.saveWithDedupe({
|
|
54
|
+
type: "history",
|
|
55
|
+
title: "对话总结",
|
|
56
|
+
content: "总结一下刚才的对话,需要记住以下要点"
|
|
57
|
+
});
|
|
58
|
+
const got = store.getById(memory.id);
|
|
59
|
+
assert.ok(got.quality_score !== undefined && got.quality_score < 60,
|
|
60
|
+
`meta memory should store quality_score < 60, got ${got.quality_score}`);
|
|
61
|
+
assert.equal(got.archived, false, "degraded memory stays un-archived");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("saveWithDedupe: short text is archived + tagged low_quality", () => {
|
|
65
|
+
const { store, service } = setup();
|
|
66
|
+
const { memory } = service.saveWithDedupe({
|
|
67
|
+
type: "preference",
|
|
68
|
+
title: "语言",
|
|
69
|
+
content: "短"
|
|
70
|
+
});
|
|
71
|
+
const got = store.getById(memory.id);
|
|
72
|
+
assert.equal(got.archived, true, "short text archived");
|
|
73
|
+
assert.ok(got.tags.includes("low_quality"), "tagged low_quality");
|
|
74
|
+
assert.ok(got.quality_score < 30, `score below archive threshold, got ${got.quality_score}`);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("saveWithDedupe: near-duplicate content is archived", () => {
|
|
78
|
+
const { store, service } = setup();
|
|
79
|
+
service.saveWithDedupe({ type: "preference", title: "习惯一", content: "猫咪喜欢在阳台晒太阳并打盹" });
|
|
80
|
+
const { memory: dup } = service.saveWithDedupe({ type: "preference", title: "习惯二", content: "猫咪喜欢在阳台晒太阳并打盹" });
|
|
81
|
+
const got = store.getById(dup.id);
|
|
82
|
+
assert.equal(got.archived, true, "near-duplicate archived");
|
|
83
|
+
assert.ok(got.tags.includes("low_quality"));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("saveWithDedupe: filter disabled (raw config) skips scoring entirely", () => {
|
|
87
|
+
const store = createStore(":memory:");
|
|
88
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
89
|
+
const { memory } = service.saveWithDedupe({ type: "preference", title: "语言", content: "短" });
|
|
90
|
+
const got = store.getById(memory.id);
|
|
91
|
+
assert.equal(got.archived, false, "nothing archived when filter off");
|
|
92
|
+
assert.equal(got.quality_score, undefined, "no quality_score when filter off");
|
|
93
|
+
assert.ok(!got.tags.includes("low_quality"));
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("injectCandidates: degraded memory ranks below a healthy preference", () => {
|
|
97
|
+
const { service } = setup();
|
|
98
|
+
// Healthy preference: score 100 → weight 1.0 → 3 * 1.0 = 3.0.
|
|
99
|
+
service.saveWithDedupe({ type: "preference", title: "健康偏好", content: "用户平时习惯用中文交流", importance: 3 });
|
|
100
|
+
// Degraded (meta-memory) preference: score ~55 → weight 0.55 → 5 * 0.55 = 2.75.
|
|
101
|
+
service.saveWithDedupe({ type: "preference", title: "元记忆偏好", content: "总结一下刚才的对话内容吧", importance: 5 });
|
|
102
|
+
const candidates = service.injectCandidates({ maxItems: 5, threshold: 3 });
|
|
103
|
+
const titles = candidates.map((c) => c.title);
|
|
104
|
+
assert.ok(titles.includes("健康偏好"), "healthy preference present");
|
|
105
|
+
assert.ok(titles.includes("元记忆偏好"), "degraded preference present");
|
|
106
|
+
assert.ok(titles.indexOf("健康偏好") < titles.indexOf("元记忆偏好"),
|
|
107
|
+
"degraded memory is demoted below the healthy one");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("exported helpers behave (meta regex, similarity, dedup ratio)", () => {
|
|
111
|
+
assert.ok(META_MEMORY_RE.test("总结一下刚才的对话"));
|
|
112
|
+
assert.ok(META_MEMORY_RE.test("作为AI助手,我需要记住"));
|
|
113
|
+
assert.ok(!META_MEMORY_RE.test("用户喜欢喝咖啡"));
|
|
114
|
+
assert.equal(textSimilarity("猫咪喜欢晒太阳", "猫咪喜欢晒太阳"), 1);
|
|
115
|
+
assert.ok(textSimilarity("猫咪喜欢晒太阳", "完全不同的内容") < 0.3);
|
|
116
|
+
assert.ok(dedupRatio("哈哈哈哈哈哈") < 0.3, "repetitive filler has low dedup ratio");
|
|
117
|
+
assert.ok(dedupRatio("一句信息量足够的话") > 0.3);
|
|
118
|
+
});
|
package/test/service.test.js
CHANGED
|
@@ -32,8 +32,15 @@ test("saveWithDedupe merges into existing on exact title match", () => {
|
|
|
32
32
|
assert.equal(result.action, "merged");
|
|
33
33
|
assert.equal(store.count(), 1);
|
|
34
34
|
const all = store.all();
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
// Bug5: same-title merge appends under a timestamped `---` separator instead
|
|
36
|
+
// of overwriting; the previous content is archived into content_history.
|
|
37
|
+
assert.ok(all[0].content.includes("旧内容"), "old content preserved in appended body");
|
|
38
|
+
assert.ok(all[0].content.includes("新内容"), "new content appended");
|
|
39
|
+
assert.ok(all[0].content.includes("---"), "timestamped separator present");
|
|
40
|
+
assert.equal(all[0].importance, 5, "importance takes the max of both");
|
|
41
|
+
assert.equal(all[0].content_history.length, 1, "old content archived to history");
|
|
42
|
+
assert.equal(all[0].content_history[0].content, "旧内容");
|
|
43
|
+
assert.equal(all[0].content_history[0].source, "auto_merge");
|
|
37
44
|
});
|
|
38
45
|
|
|
39
46
|
test("injectCandidates returns preferences + high-importance items within limit", () => {
|
|
@@ -196,3 +203,127 @@ test("service.compareAndUpdate rejects a stale version token (no lost update)",
|
|
|
196
203
|
assert.equal(service.getById(m.id).content, "count=1", "increment not lost");
|
|
197
204
|
store.close();
|
|
198
205
|
});
|
|
206
|
+
|
|
207
|
+
// --- Bug4: injectCandidates semantic-first recall (query) --------------------
|
|
208
|
+
|
|
209
|
+
test("Bug4: injectCandidates with an empty query keeps the legacy rule-based selection", () => {
|
|
210
|
+
const { service } = setup();
|
|
211
|
+
service.saveWithDedupe({ type: "preference", title: "p0", content: "偏好" });
|
|
212
|
+
service.saveWithDedupe({ type: "preference", title: "p1", content: "偏好" });
|
|
213
|
+
service.saveWithDedupe({ type: "project", title: "low", content: "低", importance: 2 });
|
|
214
|
+
service.saveWithDedupe({ type: "project", title: "high", content: "高", importance: 5 });
|
|
215
|
+
service.saveWithDedupe({ type: "history", title: "h", content: "历史", importance: 5 });
|
|
216
|
+
service.saveWithDedupe({ type: "summary", title: "记忆库总览", content: "总览", importance: 5 });
|
|
217
|
+
const legacy = service.injectCandidates({ maxItems: 5, threshold: 4 });
|
|
218
|
+
const noQuery = service.injectCandidates({ maxItems: 5, threshold: 4, query: "" });
|
|
219
|
+
const blankQuery = service.injectCandidates({ maxItems: 5, threshold: 4, query: " " });
|
|
220
|
+
assert.deepEqual(noQuery.map((c) => c.id), legacy.map((c) => c.id), "empty query behaves identically to the old logic");
|
|
221
|
+
assert.deepEqual(blankQuery.map((c) => c.id), legacy.map((c) => c.id), "whitespace-only query degrades too");
|
|
222
|
+
assert.equal(legacy[0].type, "summary", "summary still leads");
|
|
223
|
+
assert.ok(!legacy.some((c) => c.type === "history"), "history still excluded");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("Bug4: injectCandidates ignores the query when hybridInject is disabled", () => {
|
|
227
|
+
const { store } = setup();
|
|
228
|
+
const svc = createService({ store, mirror: null, config: { hybridInject: false } });
|
|
229
|
+
svc.saveWithDedupe({ type: "preference", title: "p0", content: "偏好" });
|
|
230
|
+
svc.saveWithDedupe({ type: "summary", title: "记忆库总览", content: "总览", importance: 5 });
|
|
231
|
+
const noQuery = svc.injectCandidates({ maxItems: 5, threshold: 3 });
|
|
232
|
+
const withQuery = svc.injectCandidates({ maxItems: 5, threshold: 3, query: "中文" });
|
|
233
|
+
assert.deepEqual(withQuery.map((c) => c.id), noQuery.map((c) => c.id), "hybridInject off keeps rule-based ordering");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// --- Bug5: content_history FIFO + human-edited direct overwrite --------------
|
|
237
|
+
|
|
238
|
+
test("Bug5: content_history grows FIFO across repeated same-title merges (capped at 20)", () => {
|
|
239
|
+
const { store, service } = setup();
|
|
240
|
+
service.saveWithDedupe({ type: "project", title: "流水", content: "v0" });
|
|
241
|
+
for (let i = 1; i <= 25; i++) {
|
|
242
|
+
service.saveWithDedupe({ type: "project", title: "流水", content: `v${i}` });
|
|
243
|
+
}
|
|
244
|
+
const row = store.all()[0];
|
|
245
|
+
assert.ok(Array.isArray(row.content_history), "content_history is an array");
|
|
246
|
+
assert.equal(row.content_history.length, 20, "FIFO cap of 20 respected");
|
|
247
|
+
assert.ok(row.content_history[0].content.includes("v24"), "newest archived body kept at index 0");
|
|
248
|
+
assert.ok(row.content_history[19].content.includes("v5"), "oldest kept entry");
|
|
249
|
+
assert.ok(!row.content_history[19].content.includes("v6"), "entries older than the cap dropped");
|
|
250
|
+
assert.ok(row.content.includes("v25"), "latest merge still appended to the live body");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("Bug5: _humanEdited overwrite replaces content directly and archives the old version", () => {
|
|
254
|
+
const { store, service } = setup();
|
|
255
|
+
service.saveWithDedupe({ type: "preference", title: "语言", content: "机器内容" });
|
|
256
|
+
const result = service.saveWithDedupe({ type: "preference", title: "语言", content: "人工修正", _humanEdited: true });
|
|
257
|
+
const row = store.getById(result.memory.id);
|
|
258
|
+
assert.equal(row.content, "人工修正", "direct overwrite, no appended --- block");
|
|
259
|
+
assert.ok(!row.content.includes("机器内容"), "old content not in the live body");
|
|
260
|
+
assert.equal(row.content_history.length, 1);
|
|
261
|
+
assert.equal(row.content_history[0].content, "机器内容");
|
|
262
|
+
assert.equal(row.content_history[0].source, "human_override");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// --- Bug7: memory quality filter ---------------------------------------------
|
|
266
|
+
|
|
267
|
+
function qualitySetup(config = {}) {
|
|
268
|
+
const store = createStore(":memory:");
|
|
269
|
+
const service = createService({ store, mirror: null, config: { memoryQualityFilter: { enabled: true, ...config } } });
|
|
270
|
+
return { store, service };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
test("Bug7: meta-memory is scored below the degrade threshold but still stored", () => {
|
|
274
|
+
const { store, service } = qualitySetup();
|
|
275
|
+
const result = service.saveWithDedupe({ type: "project", title: "记忆系统规则", content: "我需要记住用户喜欢中文阅读长篇小说和散文" });
|
|
276
|
+
assert.equal(result.action, "created");
|
|
277
|
+
assert.ok(result.memory.quality_score < 60, `meta-memory degraded, got ${result.memory.quality_score}`);
|
|
278
|
+
assert.ok(result.memory.quality_score >= 30, "still above the archive threshold");
|
|
279
|
+
assert.equal(result.memory.archived, false, "not archived");
|
|
280
|
+
assert.equal(store.count(), 1, "still stored and searchable");
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("Bug7: short content is archived and tagged low_quality", () => {
|
|
284
|
+
const { store, service } = qualitySetup();
|
|
285
|
+
const result = service.saveWithDedupe({ type: "project", title: "备忘", content: "x" });
|
|
286
|
+
const row = store.getById(result.memory.id);
|
|
287
|
+
assert.equal(row.archived, true, "short content archived");
|
|
288
|
+
assert.ok(row.tags.includes("low_quality"), "low_quality tag added");
|
|
289
|
+
assert.ok(row.quality_score < 30, `score below archive threshold: ${row.quality_score}`);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("Bug7: near-duplicate content is archived and tagged low_quality", () => {
|
|
293
|
+
const { store, service } = qualitySetup();
|
|
294
|
+
const base = "今天下午去操场跑步三圈然后回来洗澡";
|
|
295
|
+
service.saveWithDedupe({ type: "history", title: "记录A", content: base });
|
|
296
|
+
const result = service.saveWithDedupe({ type: "history", title: "记录B", content: `${base}${base}${base}` });
|
|
297
|
+
const row = store.getById(result.memory.id);
|
|
298
|
+
assert.equal(row.archived, true, "near-duplicate archived");
|
|
299
|
+
assert.ok(row.tags.includes("low_quality"), "low_quality tag added");
|
|
300
|
+
assert.ok(row.quality_score < 30, `score below archive threshold: ${row.quality_score}`);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("Bug7: repetitive (dedup<0.3) content is scored into the degraded band", () => {
|
|
304
|
+
const { store, service } = qualitySetup();
|
|
305
|
+
const result = service.saveWithDedupe({ type: "history", title: "唠叨", content: "好好好好好好好好好好好好好好好好好好" });
|
|
306
|
+
const row = store.getById(result.memory.id);
|
|
307
|
+
assert.ok(row.quality_score >= 30 && row.quality_score < 60, `repetitive-only degraded, got ${row.quality_score}`);
|
|
308
|
+
assert.equal(row.archived, false, "not archived (only a single non-meta signal)");
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("Bug7: memoryQualityFilter.enabled=false skips scoring entirely", () => {
|
|
312
|
+
const store = createStore(":memory:");
|
|
313
|
+
const service = createService({ store, mirror: null, config: { memoryQualityFilter: { enabled: false } } });
|
|
314
|
+
const result = service.saveWithDedupe({ type: "project", title: "记忆系统规则", content: "我需要记住用户喜欢中文阅读长篇小说和散文" });
|
|
315
|
+
const row = store.getById(result.memory.id);
|
|
316
|
+
assert.ok(row.quality_score == null, "no quality_score written");
|
|
317
|
+
assert.equal(row.archived, false, "not archived");
|
|
318
|
+
assert.ok(!(row.tags ?? []).includes("low_quality"), "no low_quality tag");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("Bug7: injection ranking re-weights by quality_score (degraded memories rank lower)", () => {
|
|
322
|
+
const { store } = setup();
|
|
323
|
+
const svc = createService({ store, mirror: null, config: { memoryQualityFilter: { enabled: true } } });
|
|
324
|
+
svc.saveWithDedupe({ type: "preference", title: "高质量", content: "用户喜欢读科幻小说和散文" });
|
|
325
|
+
svc.saveWithDedupe({ type: "preference", title: "元记忆", content: "我需要记住用户偏好的完整清单防止忘记" });
|
|
326
|
+
const candidates = svc.injectCandidates({ maxItems: 5, threshold: 3 });
|
|
327
|
+
assert.equal(candidates[0].title, "高质量", "100-quality preference leads the degraded one");
|
|
328
|
+
assert.equal(candidates[1].title, "元记忆");
|
|
329
|
+
});
|
|
@@ -21,9 +21,9 @@ function addMemory(store, { title, content, type = "preference", vector }) {
|
|
|
21
21
|
return memory;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
// Embedder mock for rebuildIndex.
|
|
25
|
-
//
|
|
26
|
-
//
|
|
24
|
+
// Embedder mock for rebuildIndex. The rebuild loop prefers embedSingle and
|
|
25
|
+
// falls back to `embed` for embed-only OpenAI-compatible clients, so a mock
|
|
26
|
+
// that provides embedSingle exercises the primary path.
|
|
27
27
|
function rebuildEmbedder({ vector = [1, 0, 0], modelHash = "mock#rebuild", dimension = 3 } = {}) {
|
|
28
28
|
return {
|
|
29
29
|
embed: async (texts) => texts.map(() => vector),
|
|
@@ -137,15 +137,31 @@ test("rebuildIndex handles a throwing embedder without failing the batch", async
|
|
|
137
137
|
assert.equal(vectorIndex.getStats().embeddedCount, 1);
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
-
test("rebuildIndex guard: embedder
|
|
140
|
+
test("rebuildIndex guard: embedder with no embed method at all is ignored", async () => {
|
|
141
141
|
const { store, vectorIndex } = setup();
|
|
142
142
|
addMemory(store, { title: "缺向量", content: "A" });
|
|
143
|
-
const bare = {
|
|
143
|
+
const bare = { modelHash: "mock#bare", dimension: 3 };
|
|
144
144
|
const result = await vectorIndex.rebuildIndex(bare, { limit: 100 });
|
|
145
|
-
assert.deepEqual(result, { indexed: 0, skipped: 0 }, "guard returns early when embedSingle
|
|
145
|
+
assert.deepEqual(result, { indexed: 0, skipped: 0 }, "guard returns early when neither embed nor embedSingle is available");
|
|
146
146
|
assert.equal(vectorIndex.modelHash(), undefined, "no model marked");
|
|
147
147
|
});
|
|
148
148
|
|
|
149
|
+
test("rebuildIndex supports an embed-only OpenAI-compatible embedder (issue #10)", async () => {
|
|
150
|
+
const { store, vectorIndex } = setup();
|
|
151
|
+
addMemory(store, { title: "缺向量", content: "A" });
|
|
152
|
+
const embedOnly = {
|
|
153
|
+
embed: async (text) => [1, 0, 0], // OpenAI-compatible single-text embed
|
|
154
|
+
modelHash: "text-embedding#abc",
|
|
155
|
+
dimension: 3
|
|
156
|
+
};
|
|
157
|
+
const result = await vectorIndex.rebuildIndex(embedOnly, { limit: 100 });
|
|
158
|
+
assert.equal(result.indexed, 1, "embed-only embedder indexes the missing row");
|
|
159
|
+
assert.equal(result.skipped, 0);
|
|
160
|
+
assert.equal(vectorIndex.modelHash(), "text-embedding#abc", "model fingerprint recorded");
|
|
161
|
+
assert.equal(vectorIndex.dimension(), 3, "dimension recorded");
|
|
162
|
+
assert.equal(vectorIndex.getStats().embeddedCount, 1, "embedding persisted");
|
|
163
|
+
});
|
|
164
|
+
|
|
149
165
|
test("rebuildIndex works with embedSingle-only embedder", async () => {
|
|
150
166
|
const { store, vectorIndex } = setup();
|
|
151
167
|
addMemory(store, { title: "缺向量", content: "A" });
|