@modusensus/dsh-mneme 0.7.31 → 0.7.32
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 -8
- package/lib/api.js +79 -6
- package/lib/client.js +217 -30
- package/lib/commands.js +6 -2
- package/lib/config.js +80 -14
- package/lib/dream/decisions.js +147 -14
- package/lib/dream/sleep.js +84 -34
- package/lib/dream.js +250 -60
- package/lib/embedding.js +22 -2
- package/lib/index.js +68 -37
- package/lib/inject.js +16 -9
- package/lib/lang.js +156 -0
- package/lib/local-embedder.js +23 -9
- package/lib/mirror.js +17 -14
- package/lib/reranker.js +23 -8
- package/lib/runtime/adopt.js +176 -0
- package/lib/runtime/closure.js +172 -0
- package/lib/runtime/download.js +247 -0
- package/lib/runtime/layout.js +235 -0
- package/lib/runtime/loader.js +249 -0
- package/lib/runtime/provision.js +237 -0
- package/lib/runtime/tarball.js +130 -0
- package/lib/runtime/verify.js +191 -0
- package/lib/service.js +102 -8
- package/lib/settings.js +36 -2
- package/lib/store.js +26 -8
- package/lib/summarize.js +108 -35
- package/lib/tools.js +119 -0
- package/package.json +7 -2
- package/runtime-manifest.json +596 -0
- package/scripts/build-runtime-manifest.mjs +217 -0
- package/scripts/mneme-runtime.mjs +277 -0
- package/src/api.js +79 -6
- package/src/commands.js +6 -2
- package/src/config.js +80 -14
- package/src/dream/decisions.js +147 -14
- package/src/dream/sleep.js +84 -34
- package/src/dream.js +250 -60
- package/src/embedding.js +22 -2
- package/src/index.js +68 -37
- package/src/inject.js +16 -9
- package/src/lang.js +156 -0
- package/src/local-embedder.js +23 -9
- package/src/mirror.js +17 -14
- package/src/reranker.js +23 -8
- package/src/runtime/adopt.js +176 -0
- package/src/runtime/closure.js +172 -0
- package/src/runtime/download.js +247 -0
- package/src/runtime/layout.js +235 -0
- package/src/runtime/loader.js +249 -0
- package/src/runtime/provision.js +237 -0
- package/src/runtime/tarball.js +130 -0
- package/src/runtime/verify.js +191 -0
- package/src/service.js +102 -8
- package/src/settings.js +36 -2
- package/src/store.js +26 -8
- package/src/summarize.js +108 -35
- package/src/tools.js +119 -0
- package/test/api.test.js +89 -9
- package/test/client.test.js +94 -10
- package/test/config.test.js +17 -1
- package/test/dream-candidate-hybrid.test.js +281 -0
- package/test/dream.test.js +172 -0
- package/test/embedding-ready.test.js +196 -0
- package/test/helpers/runtime-source.js +60 -0
- package/test/helpers/tar-builder.js +80 -0
- package/test/inject.test.js +37 -1
- package/test/lang.test.js +87 -0
- package/test/quality-filter.test.js +68 -0
- package/test/reasoning-effort.test.js +106 -15
- package/test/reindex-backfill.test.js +62 -0
- package/test/runtime-adopt.test.js +206 -0
- package/test/runtime-cli.test.js +89 -0
- package/test/runtime-closure.test.js +193 -0
- package/test/runtime-download.test.js +302 -0
- package/test/runtime-layout.test.js +163 -0
- package/test/runtime-loader.test.js +237 -0
- package/test/runtime-manifest.test.js +158 -0
- package/test/runtime-provision.test.js +101 -0
- package/test/runtime-tarball.test.js +82 -0
- package/test/runtime-verify.test.js +214 -0
- package/test/sleep-action-set.test.js +422 -0
- package/test/store.test.js +14 -0
- package/test/summarize.test.js +224 -0
- package/test/tools.test.js +30 -5
- package/test/webServer-optional.test.js +34 -1
package/src/tools.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
2
|
+
import { describeLocalRuntime, resolveRuntimeEntry } from "./runtime/loader.js";
|
|
3
|
+
import { defaultRuntimeDir } from "./runtime/layout.js";
|
|
4
|
+
import { hostModulesDir, loadRuntimeManifest, provisionRuntime } from "./runtime/provision.js";
|
|
5
|
+
import { matchesPlatform } from "./runtime/closure.js";
|
|
6
|
+
import { verifyPayload } from "./runtime/verify.js";
|
|
2
7
|
|
|
3
8
|
const TEXT_OUTPUT = (text) => [{ type: "text", text }];
|
|
4
9
|
// Per-registry tool-name registry: guards against duplicate registration on
|
|
@@ -331,6 +336,120 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
331
336
|
const memory = service.setArchived(args.id, args.archived ?? true);
|
|
332
337
|
return { memory: { id: memory.id, archived: memory.archived } };
|
|
333
338
|
}
|
|
339
|
+
}),
|
|
340
|
+
|
|
341
|
+
// 运行时口子(issue #131 / PR-C)。
|
|
342
|
+
//
|
|
343
|
+
// 为什么要有这个工具:mneme 的取向是「本体保持完善但轻量;重的功能存在、默认不开、不被依赖」。
|
|
344
|
+
// 本地向量化就是那个重功能 —— 它需要额外一份运行时(解包后数百 MB)。面板上有按钮,但用户
|
|
345
|
+
// 可能根本不看面板;这时 agent 需要一个入口,能先查状态、把代价告诉用户,再按需取回。
|
|
346
|
+
//
|
|
347
|
+
// 关于「要不要直接 provision」:它会联网、可能传几十到上百 MB、耗时数分钟。所以工具描述里
|
|
348
|
+
// 明确要求先告知用户;只有在用户已经表示要本地向量化时才直接动手。
|
|
349
|
+
defineTool({
|
|
350
|
+
name: "memory_runtime",
|
|
351
|
+
description:
|
|
352
|
+
"Inspect or provision the LOCAL inference runtime that dsh-mneme's local (offline) embedding and rerank need: " +
|
|
353
|
+
"the transformers + onnxruntime dependency closure, hundreds of MB unpacked. It is deliberately NOT a default " +
|
|
354
|
+
"dependency — mneme's core stays lightweight and local vectorization is opt-in — so this tool is the hook that " +
|
|
355
|
+
"makes the heavy capability available on demand. Use action=status first: it is read-only and reports whether " +
|
|
356
|
+
"the runtime is ready plus what provisioning would cost. action=provision tries, in order: (1) adopting an " +
|
|
357
|
+
"existing copy from this profile's node_modules (no network; hardlinked when on the same volume, so no extra " +
|
|
358
|
+
"disk), (2) a configured local .tgz directory, (3) downloading from the npm registry against a pinned manifest, " +
|
|
359
|
+
"verifying every tarball's sha512. Provisioning can transfer tens to hundreds of MB and take minutes: unless " +
|
|
360
|
+
"the user has already asked for local vectorization, tell them that cost before calling it. action=verify loads " +
|
|
361
|
+
"the runtime and runs one real inference (strictly offline, so the model must already be cached).",
|
|
362
|
+
parameters: {
|
|
363
|
+
action: { type: "string", enum: ["status", "provision", "verify"], required: true, description: "status = read-only; provision = fetch/adopt the runtime; verify = run one real inference" },
|
|
364
|
+
overwrite: { type: "boolean", description: "provision only: replace an existing payload (use when repairing a broken one)" }
|
|
365
|
+
},
|
|
366
|
+
output: {
|
|
367
|
+
schema: {
|
|
368
|
+
type: "object",
|
|
369
|
+
additionalProperties: false,
|
|
370
|
+
properties: {
|
|
371
|
+
summary: { type: "string", required: true },
|
|
372
|
+
status: { type: "string", required: true },
|
|
373
|
+
cost: { type: "string", required: true },
|
|
374
|
+
strategy: { type: "string" },
|
|
375
|
+
payloadId: { type: "string" },
|
|
376
|
+
packages: { type: "integer" },
|
|
377
|
+
files: { type: "integer" },
|
|
378
|
+
bytes: { type: "integer" },
|
|
379
|
+
dimension: { type: "integer" },
|
|
380
|
+
reason: { type: "string" }
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
render: (_args, value) => TEXT_OUTPUT(value.summary)
|
|
384
|
+
},
|
|
385
|
+
async execute(args) {
|
|
386
|
+
const runtimeDir = config?.runtimeDir ?? "";
|
|
387
|
+
const manifest = loadRuntimeManifest();
|
|
388
|
+
const forPlatform = Array.isArray(manifest?.packages) ? manifest.packages.filter((pkg) => matchesPlatform(pkg, process.platform, process.arch)) : null;
|
|
389
|
+
const cost =
|
|
390
|
+
`Local vectorization needs an extra local inference runtime (transformers + onnxruntime closure, ` +
|
|
391
|
+
`hundreds of MB unpacked${forPlatform ? `, ${forPlatform.length} packages for this platform` : ""}). ` +
|
|
392
|
+
`It is not a default dependency: provisioning adopts an existing copy when possible, otherwise downloads it.`;
|
|
393
|
+
|
|
394
|
+
if (args.action === "provision") {
|
|
395
|
+
const result = await provisionRuntime({
|
|
396
|
+
hostModulesDir: hostModulesDir(import.meta.url),
|
|
397
|
+
runtimeDir,
|
|
398
|
+
localTarballDir: config?.runtimeTarballDir ?? "",
|
|
399
|
+
mirror: config?.runtimeMirror ?? "",
|
|
400
|
+
overwrite: args.overwrite === true,
|
|
401
|
+
manifest
|
|
402
|
+
});
|
|
403
|
+
return {
|
|
404
|
+
summary: result.ok
|
|
405
|
+
? `Runtime ${result.strategy === "download" ? "downloaded from npm" : "adopted from this machine"}: ` +
|
|
406
|
+
`${result.packages} packages / ${result.files} files. Local embedding can be used after a DSH reload.`
|
|
407
|
+
: `Could not provision the runtime: ${result.reason}`,
|
|
408
|
+
status: result.ok ? "available" : "failed",
|
|
409
|
+
cost,
|
|
410
|
+
strategy: result.strategy,
|
|
411
|
+
...(result.payloadId ? { payloadId: result.payloadId } : {}),
|
|
412
|
+
...(result.packages === undefined ? {} : { packages: result.packages }),
|
|
413
|
+
...(result.files === undefined ? {} : { files: result.files }),
|
|
414
|
+
...(result.bytes === undefined ? {} : { bytes: result.bytes }),
|
|
415
|
+
...(result.reason ? { reason: result.reason } : {})
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (args.action === "verify") {
|
|
420
|
+
const candidate = resolveRuntimeEntry({ runtimeDir: runtimeDir || undefined });
|
|
421
|
+
if (candidate === null) {
|
|
422
|
+
return { summary: "No usable runtime payload to verify.", status: "missing", cost };
|
|
423
|
+
}
|
|
424
|
+
const report = await verifyPayload(candidate.dir, { cacheDir: config?.embedModelCacheDir || undefined });
|
|
425
|
+
return {
|
|
426
|
+
summary: report.ok
|
|
427
|
+
? `Runtime verified: real inference succeeded (dim ${report.functional.dim}, ${report.functional.rows} rows, ` +
|
|
428
|
+
`${report.functional.elapsedMs}ms, offline). Integrity: ${report.integrity.status}.`
|
|
429
|
+
: `Runtime verification failed — structural: ${report.structural.ok}, functional: ${report.functional.ok} (${report.functional.reason}).`,
|
|
430
|
+
status: report.ok ? "verified" : "failed",
|
|
431
|
+
cost,
|
|
432
|
+
payloadId: candidate.payloadId,
|
|
433
|
+
...(report.functional.dim ? { dimension: report.functional.dim } : {}),
|
|
434
|
+
...(report.ok ? {} : { reason: report.functional.reason })
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// status:只读,且刻意不加载模型(那要几百毫秒且碰缓存)。报告要给到 agent 足够的信息去
|
|
439
|
+
// 决定「要不要打扰用户」。
|
|
440
|
+
const report = describeLocalRuntime({ runtimeDir: runtimeDir || defaultRuntimeDir() });
|
|
441
|
+
return {
|
|
442
|
+
summary:
|
|
443
|
+
`Local inference runtime: ${report.status}` +
|
|
444
|
+
(report.payloadId ? ` (${report.payloadId}, ${report.procedure ?? "?"}, ${report.materialize ?? "?"})` : "") +
|
|
445
|
+
`. ${report.status === "available" ? "Local embedding/rerank can be used." : "Local embedding is unavailable — " + (report.hint ?? "")}`,
|
|
446
|
+
status: report.status,
|
|
447
|
+
cost,
|
|
448
|
+
...(report.payloadId ? { payloadId: report.payloadId } : {}),
|
|
449
|
+
...(forPlatform ? { packages: forPlatform.length } : {}),
|
|
450
|
+
...(report.reason ? { reason: report.reason } : {})
|
|
451
|
+
};
|
|
452
|
+
}
|
|
334
453
|
})
|
|
335
454
|
];
|
|
336
455
|
|
package/test/api.test.js
CHANGED
|
@@ -33,7 +33,7 @@ function req(path, method = "GET", body = null) {
|
|
|
33
33
|
return r;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function setup(embedder, apiToken = "", config = null) {
|
|
36
|
+
function setup(embedder, apiToken = "", config = null, semantic = undefined) {
|
|
37
37
|
const store = createStore(":memory:");
|
|
38
38
|
const service = createService({ store, mirror: null, config: {} });
|
|
39
39
|
const settings = createSettings(store.db);
|
|
@@ -51,7 +51,7 @@ function setup(embedder, apiToken = "", config = null) {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
};
|
|
54
|
-
const api = createApi(ctx, service, settings, commands, embedder,
|
|
54
|
+
const api = createApi(ctx, service, settings, commands, embedder, semantic, apiToken, config);
|
|
55
55
|
return { store, service, routes, api, settings, apiToken };
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -470,6 +470,73 @@ test("GET /api/dsh-mneme/semantic reports ready state per embedder", async () =>
|
|
|
470
470
|
assert.equal(named.ready, true);
|
|
471
471
|
});
|
|
472
472
|
|
|
473
|
+
// --- #135: /semantic must tell "not configured" apart from "degraded" ---------
|
|
474
|
+
|
|
475
|
+
test("GET /api/dsh-mneme/semantic reports configured/coverage/degraded", async () => {
|
|
476
|
+
const fetchSem = async (embedder, semantic) => {
|
|
477
|
+
const { routes } = setup(embedder, "", null, semantic);
|
|
478
|
+
const sem = routes.find((r) => r.path === "/api/dsh-mneme/semantic");
|
|
479
|
+
const res = new FakeRes();
|
|
480
|
+
await sem.handler(req("/api/dsh-mneme/semantic"), res);
|
|
481
|
+
assert.equal(res.statusCode, 200);
|
|
482
|
+
return JSON.parse(res.body);
|
|
483
|
+
};
|
|
484
|
+
// Stats double: `total` is the memory count, `embedded` the indexed count.
|
|
485
|
+
const index = (embeddedCount, totalCount) => ({
|
|
486
|
+
vectorIndex: { getStats: () => ({ embeddedCount, totalCount }) }
|
|
487
|
+
});
|
|
488
|
+
const openai = (ready, configured) => ({
|
|
489
|
+
name: "OpenAI",
|
|
490
|
+
get ready() { return ready; },
|
|
491
|
+
get configured() { return configured; },
|
|
492
|
+
embed() {}
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// No embedder at all → everything reports absence, not a false green.
|
|
496
|
+
const none = await fetchSem(undefined, index(0, 0));
|
|
497
|
+
assert.equal(none.configured, null);
|
|
498
|
+
assert.equal(none.reason, "no-embedder");
|
|
499
|
+
assert.equal(none.degraded, false);
|
|
500
|
+
assert.deepEqual(none.coverage, { embedded: 0, total: 0, ratio: null },
|
|
501
|
+
"empty store → ratio null (0 would read as 'nothing embedded')");
|
|
502
|
+
|
|
503
|
+
// Configured embedder still initializing: unreachable ≠ unconfigured.
|
|
504
|
+
assert.equal((await fetchSem(openai(false, true), index(0, 346))).reason, "initializing");
|
|
505
|
+
|
|
506
|
+
// Never configured: reported as such even though the index has memories.
|
|
507
|
+
const unconfigured = await fetchSem(openai(false, false), index(0, 346));
|
|
508
|
+
assert.equal(unconfigured.ready, false);
|
|
509
|
+
assert.equal(unconfigured.configured, false);
|
|
510
|
+
assert.equal(unconfigured.reason, "not-configured");
|
|
511
|
+
assert.equal(unconfigured.degraded, false, "unconfigured was never meant to index");
|
|
512
|
+
assert.equal(unconfigured.coverage.ratio, 0);
|
|
513
|
+
|
|
514
|
+
// Configured, memories present, zero vectors — the state this machine sat in
|
|
515
|
+
// for weeks while the card showed a healthy provider.
|
|
516
|
+
const degraded = await fetchSem(openai(true, true), index(0, 346));
|
|
517
|
+
assert.equal(degraded.reason, null, "a complete config has no abnormal reason");
|
|
518
|
+
assert.equal(degraded.degraded, true);
|
|
519
|
+
assert.equal(degraded.coverage.ratio, 0);
|
|
520
|
+
|
|
521
|
+
// Healthy index.
|
|
522
|
+
const ok = await fetchSem(openai(true, true), index(173, 346));
|
|
523
|
+
assert.equal(ok.degraded, false);
|
|
524
|
+
assert.equal(ok.coverage.ratio, 0.5);
|
|
525
|
+
|
|
526
|
+
// Adapters without a `configured` field (third-party/test doubles) keep the
|
|
527
|
+
// old contract: assumed configured, `ready` decides.
|
|
528
|
+
const legacy = await fetchSem({ embed() {} }, index(5, 10));
|
|
529
|
+
assert.equal(legacy.configured, true);
|
|
530
|
+
assert.equal(legacy.reason, null);
|
|
531
|
+
assert.equal(legacy.ready, true);
|
|
532
|
+
|
|
533
|
+
// No stats provider (no vector index) must not throw — coverage falls back
|
|
534
|
+
// to zeros and an empty store is never "degraded".
|
|
535
|
+
const noIndex = await fetchSem(openai(true, true), null);
|
|
536
|
+
assert.deepEqual(noIndex.coverage, { embedded: 0, total: 0, ratio: null });
|
|
537
|
+
assert.equal(noIndex.degraded, false);
|
|
538
|
+
});
|
|
539
|
+
|
|
473
540
|
// --- Bug8: llm-audit API (pagination + stats) --------------------------------
|
|
474
541
|
|
|
475
542
|
test("GET /api/dsh-mneme/semantic/llm-audit returns paginated rows", async () => {
|
|
@@ -577,19 +644,20 @@ test("GET /api/dsh-mneme/features returns empty overrides and effective config d
|
|
|
577
644
|
assert.equal(res.statusCode, 200);
|
|
578
645
|
const data = JSON.parse(res.body);
|
|
579
646
|
assert.deepEqual(data.overrides, {});
|
|
580
|
-
// effective 覆盖全部
|
|
647
|
+
// effective 覆盖全部 51 个白名单键(含 v0.7.20 heatEnabled、Issue #89 新增
|
|
581
648
|
// dreamSkipInvalid/allowCrossTypeMerge/dreamMinIntervalMinutes、面板可调的
|
|
582
649
|
// dreamMaxTokens、睡眠路由 sleepProvider/sleepModel、PR1 新增的
|
|
583
|
-
// recallFusion/signalTransparency
|
|
584
|
-
// entityExtractionProvider/entityExtractionModel/entityExtractionReasoning
|
|
585
|
-
//
|
|
650
|
+
// recallFusion/signalTransparency、issue #109 新增的实体抽取路由
|
|
651
|
+
// entityExtractionProvider/entityExtractionModel/entityExtractionReasoning、
|
|
652
|
+
// issue #127 新增的 summarize 五键、issue #126 新增的 sleepActionSet,以及
|
|
653
|
+
// issue #125 新增的候选集三键),未覆盖时取 bundle 配置的解析默认值;
|
|
586
654
|
// dreamProvider/dreamModel 无 schema 默认值(Config({}) 解析为 undefined),
|
|
587
|
-
// 不编造给前端 →
|
|
588
|
-
assert.equal(Object.keys(data.effective).length,
|
|
655
|
+
// 不编造给前端 → 51 - 2 = 49
|
|
656
|
+
assert.equal(Object.keys(data.effective).length, 49);
|
|
589
657
|
assert.equal(data.effective.dreamSkipInvalid, true);
|
|
590
658
|
assert.equal(data.effective.allowCrossTypeMerge, false);
|
|
591
659
|
assert.equal(data.effective.dreamMinIntervalMinutes, 0);
|
|
592
|
-
assert.equal(data.effective.dreamMaxTokens,
|
|
660
|
+
assert.equal(data.effective.dreamMaxTokens, 131072);
|
|
593
661
|
assert.equal(data.effective.sleepProvider, "");
|
|
594
662
|
assert.equal(data.effective.sleepModel, "");
|
|
595
663
|
assert.equal(data.effective.autoInject, true);
|
|
@@ -615,6 +683,18 @@ test("GET /api/dsh-mneme/features returns empty overrides and effective config d
|
|
|
615
683
|
assert.equal(data.effective.entityExtractionProvider, "");
|
|
616
684
|
assert.equal(data.effective.entityExtractionModel, "");
|
|
617
685
|
assert.equal(data.effective.entityExtractionReasoning, "none");
|
|
686
|
+
// issue #127:summarize 节流五键(均有默认值,故计入 effective 计数)
|
|
687
|
+
assert.equal(data.effective.summarizeMinIntervalMinutes, 0);
|
|
688
|
+
assert.equal(data.effective.summarizeMaxEntriesPerRun, 0);
|
|
689
|
+
assert.equal(data.effective.summarizeDedupeMode, "off");
|
|
690
|
+
assert.equal(data.effective.summarizeDedupeMinSim, 0.92);
|
|
691
|
+
assert.equal(data.effective.summarizeDedupeWindowHours, 24);
|
|
692
|
+
// issue #126:sleep 动作集(默认 conflict = 现状;面板可切 full)
|
|
693
|
+
assert.equal(data.effective.sleepActionSet, "conflict");
|
|
694
|
+
// issue #125:候选集三键(默认 window / 0 / 0.85)
|
|
695
|
+
assert.equal(data.effective.dreamCandidateMode, "window");
|
|
696
|
+
assert.equal(data.effective.dreamCandidateMax, 0);
|
|
697
|
+
assert.equal(data.effective.dreamCandidateMinSim, 0.85);
|
|
618
698
|
});
|
|
619
699
|
|
|
620
700
|
test("PUT /api/dsh-mneme/features round-trips, overrides effective and persists", async () => {
|
package/test/client.test.js
CHANGED
|
@@ -121,10 +121,10 @@ test("trigger renders a wide row or a rail icon from the wide flag", () => {
|
|
|
121
121
|
);
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
-
// The entry lives
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
// the React anchor and the in-place fallback when the host markup changes.
|
|
124
|
+
// The entry lives at the top of the plugin-entry group: the real button is
|
|
125
|
+
// portalled right after the host's New-Session row (above the entries other
|
|
126
|
+
// plugins inject there), while the sidebar.footer.action registration remains
|
|
127
|
+
// as the React anchor and the in-place fallback when the host markup changes.
|
|
128
128
|
test("sidebar entry portals above the workspaces region with footer fallback", () => {
|
|
129
129
|
assert.ok(
|
|
130
130
|
clientSource.includes('ctx.slots.inject("sidebar.footer.action"'),
|
|
@@ -139,8 +139,8 @@ test("sidebar entry portals above the workspaces region with footer fallback", (
|
|
|
139
139
|
"the portal must anchor at the host's regionArea container"
|
|
140
140
|
);
|
|
141
141
|
assert.ok(
|
|
142
|
-
clientSource.includes("insertBefore(created,
|
|
143
|
-
"the entry must
|
|
142
|
+
clientSource.includes("insertBefore(created, target)"),
|
|
143
|
+
"the entry must be placed through the idempotent place() helper"
|
|
144
144
|
);
|
|
145
145
|
assert.ok(
|
|
146
146
|
clientSource.includes("if (!host) return fallback;"),
|
|
@@ -160,13 +160,40 @@ test("sidebar entry portals above the workspaces region with footer fallback", (
|
|
|
160
160
|
);
|
|
161
161
|
});
|
|
162
162
|
|
|
163
|
+
// Issue #130: ecosystem plugins (task board, skill explorer, …) inject their
|
|
164
|
+
// own entries right after the New-Session row, and late inserters land above
|
|
165
|
+
// earlier ones — a fixed "before regionArea" slot degrades into the tail of
|
|
166
|
+
// that group, detaching the memory button from its familiar top spot. The
|
|
167
|
+
// entry therefore (a) resolves the same anchor as the ecosystem's shared
|
|
168
|
+
// sidebar-entry-core (logoRow row, legacy direct-child button), (b) re-asserts
|
|
169
|
+
// its slot from the existing observer, and (c) stays idempotent — the anchor
|
|
170
|
+
// check short-circuits before any DOM write, so observers never ping-pong.
|
|
171
|
+
test("entry re-asserts the New-Session-adjacent slot against ecosystem entries", () => {
|
|
172
|
+
assert.ok(
|
|
173
|
+
clientSource.includes(`btn.closest('[class*="logoRow"]')`),
|
|
174
|
+
"the anchor must resolve the New-Session logo row like the ecosystem core"
|
|
175
|
+
);
|
|
176
|
+
assert.ok(
|
|
177
|
+
clientSource.includes("const target = anchor ? anchor.nextSibling : region;"),
|
|
178
|
+
"the entry must sit right after the New-Session row, falling back before regionArea"
|
|
179
|
+
);
|
|
180
|
+
assert.ok(
|
|
181
|
+
clientSource.includes("if (created.parentElement === parent && created.nextSibling === target) return;"),
|
|
182
|
+
"placement must be idempotent so the observer never writes on steady state"
|
|
183
|
+
);
|
|
184
|
+
assert.ok(
|
|
185
|
+
/place\(cur\);\s*\n\s*const cls = findNative\(cur\)/.test(clientSource),
|
|
186
|
+
"the observer must re-place the entry on childList churn, not just re-sync the class"
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
|
|
163
190
|
// Three alignment/softness guarantees born from field feedback: (a) the
|
|
164
191
|
// portalled entry keeps tracking the live New-Session class (host and skin
|
|
165
192
|
// rewrite it asynchronously, so a mount-time snapshot goes stale), (b) it
|
|
166
|
-
//
|
|
193
|
+
// inherits that button's width and margins along with its native centering,
|
|
167
194
|
// (c) the toolbar dropdown escapes the transform stacking trap — the
|
|
168
195
|
// container needs the z-index because transform creates the context.
|
|
169
|
-
test("entry tracks native class,
|
|
196
|
+
test("entry tracks native class, inherits native sizing, and lifts the toolbar dropdown", () => {
|
|
170
197
|
assert.ok(
|
|
171
198
|
clientSource.includes("new MutationObserver"),
|
|
172
199
|
"the entry must re-sync the copied class via MutationObserver"
|
|
@@ -175,9 +202,14 @@ test("entry tracks native class, fills native width, and lifts the toolbar dropd
|
|
|
175
202
|
clientSource.includes('attributeFilter: ["class"]'),
|
|
176
203
|
"the observer must watch class attribute changes"
|
|
177
204
|
);
|
|
205
|
+
assert.doesNotMatch(
|
|
206
|
+
clientSource,
|
|
207
|
+
/\.mneme-topentry-native\{(?:[^}]*;)?width:/,
|
|
208
|
+
"the entry must use the native button width, which reserves its horizontal margins"
|
|
209
|
+
);
|
|
178
210
|
assert.ok(
|
|
179
|
-
|
|
180
|
-
"the entry
|
|
211
|
+
clientSource.includes(".mneme-topentry{display:flex;flex-direction:column}"),
|
|
212
|
+
"the entry wrapper must stretch the native button within a column flex layout"
|
|
181
213
|
);
|
|
182
214
|
assert.equal(
|
|
183
215
|
clientSource.includes(".mneme-topentry-native .mneme-topentry-label{flex:1;min-width:0;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}"),
|
|
@@ -593,3 +625,55 @@ test("settings feedback card: prefilled issue + mailto + browse, version from /i
|
|
|
593
625
|
assert.ok(occurrences >= 2, `i18n key memory.settings.${key} must exist in both zh and en (got ${occurrences})`);
|
|
594
626
|
}
|
|
595
627
|
});
|
|
628
|
+
|
|
629
|
+
// UI 文案必须双语齐全(中英各一条)。用户明确要求所有 UI 文字都做 i18n,
|
|
630
|
+
// 这条守卫用「每个键恰好出现两次」兜住:出现奇数次就是漏译,>2 次说明重复。
|
|
631
|
+
test("i18n keys are bilingual: every key appears exactly twice (zh + en)", () => {
|
|
632
|
+
const counts = new Map();
|
|
633
|
+
for (const match of clientSource.matchAll(/^\s*"([a-zA-Z0-9_.]+)"\s*:\s*"/gm)) {
|
|
634
|
+
counts.set(match[1], (counts.get(match[1]) ?? 0) + 1);
|
|
635
|
+
}
|
|
636
|
+
const odd = [...counts.entries()].filter(([, n]) => n !== 2).map(([key, n]) => `${key}×${n}`);
|
|
637
|
+
assert.deepEqual(odd, [], `这些 i18n 键没有做到中英各一条:${odd.join(", ")}`);
|
|
638
|
+
assert.ok(counts.size > 100, `解析到的键太少(${counts.size}),正则可能没匹配到 i18n 表`);
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
// issue #135:向量状态卡必须区分「没配」与「配了却一条都没嵌上」。此前卡片只看
|
|
642
|
+
// ready,而未配置的 legacy embedder(ready 恒 true)一路显示成正常 provider。
|
|
643
|
+
test("vector status card surfaces the configured/degraded split", () => {
|
|
644
|
+
// 1. 读后端新字段,缺失(旧服务端)时退化成旧行为而不是误报未配置
|
|
645
|
+
assert.ok(
|
|
646
|
+
clientSource.includes('typeof j?.configured === "boolean" ? j.configured : null'),
|
|
647
|
+
"the card must read `configured` and tolerate older servers that omit it"
|
|
648
|
+
);
|
|
649
|
+
assert.ok(
|
|
650
|
+
clientSource.includes("j?.degraded === true"),
|
|
651
|
+
"the card must read `degraded`"
|
|
652
|
+
);
|
|
653
|
+
// 2. 三态互斥:未配置优先于初始化中,降级只在既非未配置也非初始化时成立
|
|
654
|
+
assert.ok(
|
|
655
|
+
clientSource.includes("const unconfigured = !off && state.configured === false;"),
|
|
656
|
+
"unconfigured must be its own state, not folded into ready"
|
|
657
|
+
);
|
|
658
|
+
assert.ok(
|
|
659
|
+
clientSource.includes("const pending = !off && !unconfigured && state.ready !== true;"),
|
|
660
|
+
"an unconfigured embedder must not be reported as initializing"
|
|
661
|
+
);
|
|
662
|
+
assert.ok(
|
|
663
|
+
clientSource.includes("const degraded = !off && !unconfigured && !pending && state.degraded === true;"),
|
|
664
|
+
"degraded must only apply to a configured, settled embedder"
|
|
665
|
+
);
|
|
666
|
+
// 3. 三个状态各自的标签/说明走 i18n,双语齐备
|
|
667
|
+
for (const key of ["vectorUnconfigured", "vectorUnconfiguredHint", "vectorDegradedHint"]) {
|
|
668
|
+
const occurrences = clientSource.split(`"memory.status.${key}"`).length - 1;
|
|
669
|
+
assert.ok(occurrences >= 2, `i18n key memory.status.${key} must exist in both zh and en (got ${occurrences})`);
|
|
670
|
+
}
|
|
671
|
+
assert.ok(
|
|
672
|
+
clientSource.includes('t("memory.status.vectorUnconfigured")'),
|
|
673
|
+
"the unconfigured state must render its own label"
|
|
674
|
+
);
|
|
675
|
+
assert.ok(
|
|
676
|
+
clientSource.includes('t("memory.status.vectorDegradedHint")'),
|
|
677
|
+
"the degraded state must render its own caption (0 of {m} indexed)"
|
|
678
|
+
);
|
|
679
|
+
});
|
package/test/config.test.js
CHANGED
|
@@ -46,5 +46,21 @@ test("startup probe: the reranker module never statically imports transformers/o
|
|
|
46
46
|
!src.includes('from "@huggingface/transformers"'),
|
|
47
47
|
"no static transformers.js import in reranker.js"
|
|
48
48
|
);
|
|
49
|
-
|
|
49
|
+
// issue #131 之后,懒加载的落点从 reranker 内部搬到了 src/runtime/loader.js
|
|
50
|
+
// (三层解析:自管 payload → 宿主裸 specifier)。判据随之改成两条,意图不变:
|
|
51
|
+
// reranker 不得静态导入,且必须经由 loader 间接加载;动态 import 只允许出现在
|
|
52
|
+
// loader 里(它是唯一知道「运行时从哪来」的模块)。
|
|
53
|
+
assert.match(src, /from "\.\/runtime\/loader\.js"/, "reranker 经 runtime/loader.js 取运行时");
|
|
54
|
+
assert.match(src, /loadTransformers\(/, "reranker 必须走三层解析,而不是自己拼 import");
|
|
55
|
+
assert.ok(
|
|
56
|
+
!/^\s*import[^\n]*@huggingface\/transformers/m.test(src),
|
|
57
|
+
"reranker 不允许任何形式的静态 transformers 导入"
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const loaderSrc = readFileSync(new URL("../src/runtime/loader.js", import.meta.url), "utf8");
|
|
61
|
+
assert.match(loaderSrc, /import\(specifier\)/, "loader 用动态 import 保持懒加载");
|
|
62
|
+
assert.ok(
|
|
63
|
+
!/^\s*import[^\n]*@huggingface\/transformers/m.test(loaderSrc),
|
|
64
|
+
"loader 不得静态导入 transformers,否则惰性加载就失效了"
|
|
65
|
+
);
|
|
50
66
|
});
|