@modusensus/dsh-mneme 0.7.13 → 0.7.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/test/api.test.js CHANGED
@@ -6,6 +6,11 @@ import { createService } from "../src/service.js";
6
6
  import { createApi } from "../src/api.js";
7
7
  import { createSettings } from "../src/settings.js";
8
8
  import { createVectorIndex } from "../src/vector-index.js";
9
+ import { Config } from "../src/config.js";
10
+ import { parseHumanEdits } from "../src/mirror.js";
11
+
12
+ // 解析后的 schema 默认值,作为 /features effective 的 bundle 配置侧样本。
13
+ const FLAGS_CFG = Config({});
9
14
 
10
15
  class FakeRes extends EventEmitter {
11
16
  constructor() { super(); this.statusCode = 200; this.body = ""; }
@@ -27,7 +32,7 @@ function req(path, method = "GET", body = null) {
27
32
  return r;
28
33
  }
29
34
 
30
- function setup(embedder, apiToken = "") {
35
+ function setup(embedder, apiToken = "", config = null) {
31
36
  const store = createStore(":memory:");
32
37
  const service = createService({ store, mirror: null, config: {} });
33
38
  const settings = createSettings(store.db);
@@ -45,7 +50,7 @@ function setup(embedder, apiToken = "") {
45
50
  }
46
51
  }
47
52
  };
48
- const api = createApi(ctx, service, settings, commands, embedder, undefined, apiToken);
53
+ const api = createApi(ctx, service, settings, commands, embedder, undefined, apiToken, config);
49
54
  return { store, service, routes, api, settings, apiToken };
50
55
  }
51
56
 
@@ -511,3 +516,481 @@ test("Bug10: vector-reindex with an embed-only OpenAI-compatible embedder return
511
516
  assert.equal(vectorIndex.dimension(), 3, "dimension written to vector_meta");
512
517
  assert.equal(vectorIndex.getEmbedding(service.all()[0].id).length, 3, "embedding persisted");
513
518
  });
519
+
520
+ // --- feature flags(/features:overrides + effective)------------------------
521
+
522
+ test("GET /api/dsh-mneme/features returns empty overrides and effective config defaults", async () => {
523
+ const { routes } = setup(undefined, "", FLAGS_CFG);
524
+ const route = routes.find((r) => r.path === "/api/dsh-mneme/features");
525
+ const res = new FakeRes();
526
+ await route.handler(req("/api/dsh-mneme/features"), res);
527
+ assert.equal(res.statusCode, 200);
528
+ const data = JSON.parse(res.body);
529
+ assert.deepEqual(data.overrides, {});
530
+ // effective 覆盖全部 30 个白名单键,未覆盖时取 bundle 配置的解析默认值;
531
+ // dreamProvider/dreamModel 无 schema 默认值(Config({}) 解析为 undefined),
532
+ // 不编造给前端 → 30 - 2 = 28
533
+ assert.equal(Object.keys(data.effective).length, 28);
534
+ assert.equal(data.effective.autoInject, true);
535
+ assert.equal(data.effective.codingRetrospect, false);
536
+ assert.equal(data.effective.distillMaxChars, 24000);
537
+ assert.equal(data.effective.codingBoostFactor, 2);
538
+ // 新增布尔键(含嵌套点号键)从 bundle 配置的对象子字段/顶层取默认值
539
+ assert.equal(data.effective.bm25SearchEnabled, true);
540
+ assert.equal(data.effective.conflictFreezeEnabled, false);
541
+ assert.equal(data.effective.trustEpistemicWeighting, false);
542
+ assert.equal(data.effective.reflectionFailureTracking, true);
543
+ assert.equal(data.effective["memoryQualityFilter.enabled"], true);
544
+ assert.equal(data.effective["llmAudit.enabled"], true);
545
+ // 新增字符串 / URL / 枚举键
546
+ assert.equal(data.effective.localEmbedModel, "Xenova/bge-small-zh-v1.5");
547
+ assert.equal(data.effective.ollamaBaseUrl, "http://localhost:11434");
548
+ assert.equal(data.effective.ollamaModel, "nomic-embed-text");
549
+ assert.equal(data.effective.embedProvider, "openai");
550
+ });
551
+
552
+ test("PUT /api/dsh-mneme/features round-trips, overrides effective and persists", async () => {
553
+ const { routes, settings } = setup(undefined, "", FLAGS_CFG);
554
+ const route = routes.find((r) => r.path === "/api/dsh-mneme/features");
555
+ const res = new FakeRes();
556
+ await route.handler(req("/api/dsh-mneme/features", "PUT", { autoDream: false, distillMaxChars: 48000 }), res);
557
+ assert.equal(res.statusCode, 200);
558
+ const data = JSON.parse(res.body);
559
+ assert.deepEqual(data.overrides, { autoDream: false, distillMaxChars: 48000 });
560
+ assert.equal(data.effective.autoDream, false, "override wins over config default");
561
+ assert.equal(data.effective.distillMaxChars, 48000);
562
+ assert.equal(data.effective.autoInject, true, "keys not in the patch still report config");
563
+ assert.deepEqual(settings.getFeatureFlags(), { autoDream: false, distillMaxChars: 48000 });
564
+ });
565
+
566
+ test("PUT /api/dsh-mneme/features round-trips nested, string, url and enum keys", async () => {
567
+ const { routes, settings } = setup(undefined, "", FLAGS_CFG);
568
+ const route = routes.find((r) => r.path === "/api/dsh-mneme/features");
569
+ const patch = {
570
+ "memoryQualityFilter.enabled": false,
571
+ "llmAudit.enabled": false,
572
+ embedProvider: "local",
573
+ ollamaBaseUrl: "http://127.0.0.1:11434",
574
+ dreamProvider: " siliconflow ",
575
+ dreamModel: ""
576
+ };
577
+ const res = new FakeRes();
578
+ await route.handler(req("/api/dsh-mneme/features", "PUT", patch), res);
579
+ assert.equal(res.statusCode, 200);
580
+ const data = JSON.parse(res.body);
581
+ assert.deepEqual(data.overrides, {
582
+ "memoryQualityFilter.enabled": false,
583
+ "llmAudit.enabled": false,
584
+ embedProvider: "local",
585
+ ollamaBaseUrl: "http://127.0.0.1:11434",
586
+ dreamProvider: "siliconflow",
587
+ dreamModel: ""
588
+ }, "nested keys persist flat, free strings are trimmed");
589
+ // 嵌套键的覆盖值压过 bundle 配置的对象子字段
590
+ assert.equal(data.effective["memoryQualityFilter.enabled"], false);
591
+ assert.equal(data.effective["llmAudit.enabled"], false);
592
+ assert.equal(data.effective.embedProvider, "local");
593
+ assert.equal(data.effective.ollamaBaseUrl, "http://127.0.0.1:11434");
594
+ assert.equal(data.effective.dreamProvider, "siliconflow");
595
+ assert.equal(data.effective.dreamModel, "", "empty string is a legal override");
596
+ assert.deepEqual(settings.getFeatureFlags(), data.overrides, "overrides persisted");
597
+
598
+ // 非法枚举 / 非 http 协议的 URL → 400 且不落库
599
+ const badEnum = new FakeRes();
600
+ await route.handler(req("/api/dsh-mneme/features", "PUT", { embedProvider: "bogus" }), badEnum);
601
+ assert.equal(badEnum.statusCode, 400);
602
+ assert.match(JSON.parse(badEnum.body).error, /embedProvider/);
603
+
604
+ const badUrl = new FakeRes();
605
+ await route.handler(req("/api/dsh-mneme/features", "PUT", { ollamaBaseUrl: "ftp://localhost:11434" }), badUrl);
606
+ assert.equal(badUrl.statusCode, 400);
607
+ assert.match(JSON.parse(badUrl.body).error, /ollamaBaseUrl/);
608
+
609
+ assert.deepEqual(settings.getFeatureFlags(), data.overrides, "rejected writes persist nothing");
610
+ });
611
+
612
+ test("PUT /api/dsh-mneme/features rejects unknown keys, bad types and bad ranges with 400", async () => {
613
+ const { routes, settings } = setup(undefined, "", FLAGS_CFG);
614
+ const route = routes.find((r) => r.path === "/api/dsh-mneme/features");
615
+
616
+ const unknown = new FakeRes();
617
+ await route.handler(req("/api/dsh-mneme/features", "PUT", { noSuchFlag: true }), unknown);
618
+ assert.equal(unknown.statusCode, 400);
619
+ assert.match(JSON.parse(unknown.body).error, /noSuchFlag/);
620
+
621
+ const badType = new FakeRes();
622
+ await route.handler(req("/api/dsh-mneme/features", "PUT", { autoInject: "yes" }), badType);
623
+ assert.equal(badType.statusCode, 400);
624
+ assert.match(JSON.parse(badType.body).error, /autoInject/);
625
+
626
+ const badRange = new FakeRes();
627
+ await route.handler(req("/api/dsh-mneme/features", "PUT", { codingBoostFactor: 9 }), badRange);
628
+ assert.equal(badRange.statusCode, 400);
629
+ assert.match(JSON.parse(badRange.body).error, /codingBoostFactor/);
630
+
631
+ // 空 patch 不携带任何意图,直接 400
632
+ const empty = new FakeRes();
633
+ await route.handler(req("/api/dsh-mneme/features", "PUT", {}), empty);
634
+ assert.equal(empty.statusCode, 400);
635
+
636
+ assert.deepEqual(settings.getFeatureFlags(), {}, "failed writes persist nothing");
637
+ });
638
+
639
+ test("PUT /api/dsh-mneme/features is token-gated; GET stays open", async () => {
640
+ const { routes } = setup(undefined, "secret-token", FLAGS_CFG);
641
+ const route = routes.find((r) => r.path === "/api/dsh-mneme/features");
642
+
643
+ const get = new FakeRes();
644
+ await route.handler(req("/api/dsh-mneme/features"), get);
645
+ assert.equal(get.statusCode, 200, "read stays open without token");
646
+
647
+ const put = new FakeRes();
648
+ await route.handler(req("/api/dsh-mneme/features", "PUT", { autoDream: false }), put);
649
+ assert.equal(put.statusCode, 401);
650
+
651
+ const ok = req("/api/dsh-mneme/features", "PUT", { autoDream: false });
652
+ ok.headers = { authorization: "Bearer secret-token" };
653
+ const okRes = new FakeRes();
654
+ await route.handler(ok, okRes);
655
+ assert.equal(okRes.statusCode, 200);
656
+ });
657
+
658
+ // --- 交互式记忆库面板:日期过滤 / update / memories/entities / export / import / dream-status ---
659
+
660
+ test("GET /api/dsh-mneme/list supports updatedFrom/updatedTo and count matches", async () => {
661
+ const { routes, service, store } = setup();
662
+ service.saveWithDedupe({ type: "preference", title: "旧", content: "1" });
663
+ service.saveWithDedupe({ type: "preference", title: "中", content: "2" });
664
+ service.saveWithDedupe({ type: "preference", title: "新", content: "3" });
665
+ const setAt = (title, at) => store.db.prepare("UPDATE memories SET updated_at = ? WHERE title = ?").run(at, title);
666
+ setAt("旧", "2026-08-01T00:00:00.000Z");
667
+ setAt("中", "2026-09-01T12:00:00.000Z");
668
+ setAt("新", "2026-09-15T23:00:00.000Z");
669
+ const route = routes.find((r) => r.path === "/api/dsh-mneme/list");
670
+
671
+ const from = new FakeRes();
672
+ await route.handler(req("/api/dsh-mneme/list?updatedFrom=2026-09-01"), from);
673
+ const fromData = JSON.parse(from.body);
674
+ assert.equal(fromData.items.length, 2);
675
+ assert.equal(fromData.total, 2, "count filter matches list filter");
676
+ assert.deepEqual(fromData.items.map((m) => m.title).sort(), ["中", "新"]);
677
+
678
+ // date-only updatedTo 闭区间含当天全天(中 09-01T12:00 命中)
679
+ const range = new FakeRes();
680
+ await route.handler(req("/api/dsh-mneme/list?updatedFrom=2026-08-01&updatedTo=2026-09-01"), range);
681
+ const rangeData = JSON.parse(range.body);
682
+ assert.equal(rangeData.items.length, 2);
683
+ assert.equal(rangeData.total, 2);
684
+
685
+ const to = new FakeRes();
686
+ await route.handler(req("/api/dsh-mneme/list?updatedTo=2026-08-31"), to);
687
+ const toData = JSON.parse(to.body);
688
+ assert.equal(toData.items.length, 1);
689
+ assert.equal(toData.items[0].title, "旧");
690
+
691
+ // 非法日期值 → 忽略(不报错、不过滤)
692
+ const bad = new FakeRes();
693
+ await route.handler(req("/api/dsh-mneme/list?updatedFrom=not-a-date&updatedTo=%E4%B9%B1"), bad);
694
+ const badData = JSON.parse(bad.body);
695
+ assert.equal(badData.items.length, 3);
696
+ assert.equal(badData.total, 3);
697
+
698
+ // 完整时间戳同样生效
699
+ const ts = new FakeRes();
700
+ await route.handler(req("/api/dsh-mneme/list?updatedFrom=2026-09-02T00:00:00.000Z"), ts);
701
+ const tsData = JSON.parse(ts.body);
702
+ assert.equal(tsData.items.length, 1);
703
+ assert.equal(tsData.items[0].title, "新");
704
+ });
705
+
706
+ test("POST /api/dsh-mneme/update edits title/importance and archives", async () => {
707
+ const { routes, service } = setup();
708
+ const created = service.saveWithDedupe({ type: "preference", title: "原始", content: "原始内容" });
709
+ const id = created.memory.id;
710
+ const route = routes.find((r) => r.path === "/api/dsh-mneme/update");
711
+
712
+ const edit = new FakeRes();
713
+ await route.handler(req("/api/dsh-mneme/update", "POST", { id, title: "改名", importance: 5 }), edit);
714
+ assert.equal(edit.statusCode, 200);
715
+ const row = JSON.parse(edit.body).memory;
716
+ assert.equal(row.title, "改名");
717
+ assert.equal(row.importance, 5);
718
+ assert.equal(row.archived, false, "archived booleanized in the response row");
719
+ assert.notEqual(row.updated_at, created.memory.updated_at, "updated_at advances");
720
+
721
+ const archive = new FakeRes();
722
+ await route.handler(req("/api/dsh-mneme/update", "POST", { id, archived: true }), archive);
723
+ assert.equal(archive.statusCode, 200);
724
+ assert.equal(JSON.parse(archive.body).memory.archived, true);
725
+ assert.equal(service.getById(id).archived, true);
726
+ });
727
+
728
+ test("POST /api/dsh-mneme/update archives replaced content into content_history", async () => {
729
+ const { routes, service } = setup();
730
+ const created = service.saveWithDedupe({ type: "preference", title: "历史", content: "第一版" });
731
+ const route = routes.find((r) => r.path === "/api/dsh-mneme/update");
732
+ const res = new FakeRes();
733
+ await route.handler(req("/api/dsh-mneme/update", "POST", { id: created.memory.id, content: "第二版" }), res);
734
+ assert.equal(res.statusCode, 200);
735
+ const row = service.getById(created.memory.id);
736
+ assert.equal(row.content, "第二版");
737
+ assert.equal(row.content_history[0].content, "第一版");
738
+ assert.equal(row.content_history[0].source, "human_override");
739
+ });
740
+
741
+ test("POST /api/dsh-mneme/update returns 400/404 for bad requests", async () => {
742
+ const { routes, service } = setup();
743
+ const created = service.saveWithDedupe({ type: "preference", title: "存在", content: "x" });
744
+ const id = created.memory.id;
745
+ const route = routes.find((r) => r.path === "/api/dsh-mneme/update");
746
+
747
+ const missing = new FakeRes();
748
+ await route.handler(req("/api/dsh-mneme/update", "POST", { title: "无 id" }), missing);
749
+ assert.equal(missing.statusCode, 400);
750
+
751
+ const noFields = new FakeRes();
752
+ await route.handler(req("/api/dsh-mneme/update", "POST", { id }), noFields);
753
+ assert.equal(noFields.statusCode, 400);
754
+
755
+ const emptyTitle = new FakeRes();
756
+ await route.handler(req("/api/dsh-mneme/update", "POST", { id, title: " " }), emptyTitle);
757
+ assert.equal(emptyTitle.statusCode, 400);
758
+
759
+ const emptyContent = new FakeRes();
760
+ await route.handler(req("/api/dsh-mneme/update", "POST", { id, content: "" }), emptyContent);
761
+ assert.equal(emptyContent.statusCode, 400);
762
+
763
+ const badImportance = new FakeRes();
764
+ await route.handler(req("/api/dsh-mneme/update", "POST", { id, importance: 9 }), badImportance);
765
+ assert.equal(badImportance.statusCode, 400);
766
+
767
+ const badTags = new FakeRes();
768
+ await route.handler(req("/api/dsh-mneme/update", "POST", { id, tags: "x" }), badTags);
769
+ assert.equal(badTags.statusCode, 400);
770
+
771
+ const badArchived = new FakeRes();
772
+ await route.handler(req("/api/dsh-mneme/update", "POST", { id, archived: "yes" }), badArchived);
773
+ assert.equal(badArchived.statusCode, 400);
774
+
775
+ const gone = new FakeRes();
776
+ await route.handler(req("/api/dsh-mneme/update", "POST", { id: "no-such-id", title: "x" }), gone);
777
+ assert.equal(gone.statusCode, 404);
778
+ assert.deepEqual(JSON.parse(gone.body), { error: "not-found" });
779
+
780
+ assert.equal(service.getById(id).title, "存在", "failed writes persist nothing");
781
+ });
782
+
783
+ test("POST /api/dsh-mneme/update is token-gated", async () => {
784
+ const { routes } = setup(undefined, "secret-token");
785
+ const route = routes.find((r) => r.path === "/api/dsh-mneme/update");
786
+ const res = new FakeRes();
787
+ await route.handler(req("/api/dsh-mneme/update", "POST", { id: "x", title: "y" }), res);
788
+ assert.equal(res.statusCode, 401);
789
+ });
790
+
791
+ test("GET /api/dsh-mneme/memories/entities returns entities linked to a memory", async () => {
792
+ const { routes, service } = setup();
793
+ const a = service.saveWithDedupe({ type: "project", title: "A", content: "提到甲" });
794
+ const b = service.saveWithDedupe({ type: "project", title: "B", content: "无关" });
795
+ const entity = service.createEntity({ name: "甲", type: "person" });
796
+ // 同一记忆两次提及同一实体(两条 attr)→ 去重后只出现一次
797
+ service.saveAttr({ entity_id: entity.id, attr_key: "角色", attr_value: "负责人", memory_id: a.memory.id });
798
+ service.saveAttr({ entity_id: entity.id, attr_key: "团队", attr_value: "前端", memory_id: a.memory.id });
799
+ const route = routes.find((r) => r.path === "/api/dsh-mneme/memories/entities");
800
+
801
+ const res = new FakeRes();
802
+ await route.handler(req(`/api/dsh-mneme/memories/entities?memoryId=${a.memory.id}`), res);
803
+ assert.equal(res.statusCode, 200);
804
+ const data = JSON.parse(res.body);
805
+ assert.equal(data.entities.length, 1, "deduped by mention");
806
+ assert.equal(data.entities[0].name, "甲");
807
+ assert.equal(data.entities[0].type, "person");
808
+
809
+ const none = new FakeRes();
810
+ await route.handler(req(`/api/dsh-mneme/memories/entities?memoryId=${b.memory.id}`), none);
811
+ assert.deepEqual(JSON.parse(none.body), { entities: [] });
812
+
813
+ const missing = new FakeRes();
814
+ await route.handler(req("/api/dsh-mneme/memories/entities"), missing);
815
+ assert.equal(missing.statusCode, 400);
816
+ });
817
+
818
+ test("GET /api/dsh-mneme/export json carries full rows and version", async () => {
819
+ const { routes, service } = setup();
820
+ service.saveWithDedupe({ type: "preference", title: "甲", content: "内容甲" });
821
+ const hidden = service.saveWithDedupe({ type: "preference", title: "乙", content: "内容乙" });
822
+ service.setForget(hidden.memory.id, true);
823
+ const route = routes.find((r) => r.path === "/api/dsh-mneme/export");
824
+ const res = new FakeRes();
825
+ await route.handler(req("/api/dsh-mneme/export"), res);
826
+ assert.equal(res.statusCode, 200);
827
+ const data = JSON.parse(res.body);
828
+ assert.equal(data.count, 2, "export includes forgotten rows");
829
+ assert.equal(data.memories.length, 2);
830
+ assert.equal(typeof data.version, "string", "version comes from package.json");
831
+ assert.ok(data.exported_at);
832
+ assert.equal(data.memories.every((m) => typeof m.archived === "boolean" && typeof m.forgotten === "boolean"), true);
833
+ const times = data.memories.map((m) => m.updated_at);
834
+ assert.deepEqual(times, [...times].sort().reverse(), "updated_at DESC");
835
+ assert.match(res.headers["Content-Disposition"], /attachment; filename="dsh-mneme-export-\d{8}\.json"/);
836
+ });
837
+
838
+ test("GET /api/dsh-mneme/export markdown feeds straight back through import", async () => {
839
+ const { routes, service } = setup();
840
+ const a = service.saveWithDedupe({ type: "preference", title: "语言", content: "中文优先" });
841
+ const b = service.saveWithDedupe({ type: "preference", title: "风格", content: "简洁" });
842
+ const exportRoute = routes.find((r) => r.path === "/api/dsh-mneme/export");
843
+ const importRoute = routes.find((r) => r.path === "/api/dsh-mneme/import");
844
+
845
+ const res = new FakeRes();
846
+ await exportRoute.handler(req("/api/dsh-mneme/export?format=markdown"), res);
847
+ assert.equal(res.statusCode, 200);
848
+ assert.match(res.headers["Content-Type"], /text\/markdown/);
849
+ assert.match(res.headers["Content-Disposition"], /attachment; filename="dsh-mneme-export-\d{8}\.md"/);
850
+ const md = res.body;
851
+ assert.ok(md.includes("- **ID**: `" + a.memory.id + "`"), "anchor line present (readHumanEdits-compatible)");
852
+
853
+ // 黄金用例:导出文本原样导入 → 解析出全部条目,且字段无漂移
854
+ const back = new FakeRes();
855
+ await importRoute.handler(req("/api/dsh-mneme/import", "POST", { type: "preference", markdown: md }), back);
856
+ assert.equal(back.statusCode, 200);
857
+ const data = JSON.parse(back.body);
858
+ assert.equal(data.type, "preference");
859
+ assert.ok(data.merged >= 2, "both memories parsed back");
860
+ assert.equal(service.getById(a.memory.id).title, "语言");
861
+ assert.equal(service.getById(a.memory.id).content, "中文优先");
862
+ assert.equal(service.getById(b.memory.id).title, "风格");
863
+ assert.equal(service.getById(b.memory.id).content, "简洁");
864
+ });
865
+
866
+ test("POST /api/dsh-mneme/import validates type/markdown and tolerates zero edits", async () => {
867
+ const { routes } = setup();
868
+ const route = routes.find((r) => r.path === "/api/dsh-mneme/import");
869
+
870
+ const badType = new FakeRes();
871
+ await route.handler(req("/api/dsh-mneme/import", "POST", { type: "preferences", markdown: "x" }), badType);
872
+ assert.equal(badType.statusCode, 400, "TYPE_FILE keys are singular, plural is rejected");
873
+
874
+ const badMd = new FakeRes();
875
+ await route.handler(req("/api/dsh-mneme/import", "POST", { type: "preference", markdown: "" }), badMd);
876
+ assert.equal(badMd.statusCode, 400);
877
+
878
+ const zero = new FakeRes();
879
+ await route.handler(req("/api/dsh-mneme/import", "POST", { type: "preference", markdown: "# 空镜像" }), zero);
880
+ assert.equal(zero.statusCode, 200);
881
+ assert.deepEqual(JSON.parse(zero.body), { merged: 0, type: "preference" });
882
+ });
883
+
884
+ test("POST /api/dsh-mneme/import is token-gated", async () => {
885
+ const { routes } = setup(undefined, "secret-token");
886
+ const route = routes.find((r) => r.path === "/api/dsh-mneme/import");
887
+ const res = new FakeRes();
888
+ await route.handler(req("/api/dsh-mneme/import", "POST", { type: "preference", markdown: "x" }), res);
889
+ assert.equal(res.statusCode, 401);
890
+ });
891
+
892
+ test("GET /api/dsh-mneme/dream-status returns runs and pending conflict ids", async () => {
893
+ const { routes, service } = setup();
894
+ service.saveDreamRun({ created_at: "2026-01-01T00:00:00.000Z", status: "ok", provider: "ollama", model: "qwen", snapshot_hash: "h1", input_count: 2, receipt: "r1" });
895
+ service.saveDreamRun({ created_at: "2026-01-02T00:00:00.000Z", status: "failed", error: "boom", snapshot_hash: "h2", input_count: 0, receipt: "r2" });
896
+ const pending = service.saveConflictPending({ memory_a: "aaaa", memory_b: "bbbb", reason: "矛盾" });
897
+
898
+ const route = routes.find((r) => r.path === "/api/dsh-mneme/dream-status");
899
+ const res = new FakeRes();
900
+ await route.handler(req("/api/dsh-mneme/dream-status"), res);
901
+ assert.equal(res.statusCode, 200);
902
+ const data = JSON.parse(res.body);
903
+ assert.equal(data.runs.length, 2);
904
+ assert.equal(data.runs[0].created_at, "2026-01-02T00:00:00.000Z", "created_at DESC");
905
+ assert.deepEqual(data.lastRun, data.runs[0]);
906
+ assert.deepEqual(Object.keys(data.lastRun).sort(), ["created_at", "error", "model", "provider", "status"]);
907
+ assert.equal(data.runs[0].error, "boom");
908
+ assert.equal(data.runs[1].provider, "ollama");
909
+ assert.equal(data.pendingConflicts, 1);
910
+ assert.deepEqual(data.pendingMemoryIds.sort(), ["aaaa", "bbbb"]);
911
+
912
+ // 解决后队列清空
913
+ service.resolveConflictPending(pending.id, { winner: "aaaa" });
914
+ const after = new FakeRes();
915
+ await route.handler(req("/api/dsh-mneme/dream-status"), after);
916
+ const afterData = JSON.parse(after.body);
917
+ assert.equal(afterData.pendingConflicts, 0);
918
+ assert.deepEqual(afterData.pendingMemoryIds, []);
919
+ });
920
+
921
+ test("parseHumanEdits is the pure core of readHumanEdits (CRLF tolerant)", () => {
922
+ const digest = "0".repeat(64);
923
+ const text = [
924
+ "# x\r\n",
925
+ "\r\n",
926
+ "## 标题\r\n",
927
+ "\r\n",
928
+ "- **ID**: `m1`\n",
929
+ "- **类型**: preference\n",
930
+ "- **重要性**: 3\n",
931
+ "- **标签**: \n",
932
+ "- **更新时间**: 2026-01-01T00:00:00.000Z\n",
933
+ "\n",
934
+ `<!-- mirror-digest: ${digest} -->\n`,
935
+ "正文一段\n",
936
+ "\n",
937
+ "---\n",
938
+ "\n",
939
+ "## 另一条\n",
940
+ "\r\n",
941
+ "- **ID**: `m2`\n",
942
+ "- **类型**: preference\n",
943
+ "- **重要性**: 4\n",
944
+ "- **标签**: \n",
945
+ "- **更新时间**: 2026-01-02T00:00:00.000Z\n",
946
+ "\n",
947
+ "内容二\n",
948
+ "\n",
949
+ "---\n"
950
+ ].join("");
951
+ const edits = parseHumanEdits(text);
952
+ assert.equal(edits.length, 2);
953
+ assert.equal(edits[0].id, "m1");
954
+ assert.equal(edits[0].title, "标题");
955
+ assert.equal(edits[0].content, "正文一段");
956
+ assert.equal(edits[1].id, "m2");
957
+ assert.equal(edits[1].content, "内容二");
958
+ });
959
+
960
+ test("GET /api/dsh-mneme/list?archived=only lists just archived rows; default list hides them", async () => {
961
+ const { routes, service } = setup();
962
+ service.saveWithDedupe({ type: "preference", title: "在用", content: "keep" });
963
+ service.saveWithDedupe({ type: "project", title: "已归档", content: "gone" });
964
+ const route = routes.find((r) => r.path === "/api/dsh-mneme/list");
965
+ const upd = routes.find((r) => r.path === "/api/dsh-mneme/update");
966
+
967
+ const def0 = new FakeRes();
968
+ await route.handler(req("/api/dsh-mneme/list"), def0);
969
+ const target = JSON.parse(def0.body).items.find((m) => m.title === "已归档");
970
+
971
+ const arch = new FakeRes();
972
+ await upd.handler(req("/api/dsh-mneme/update", "POST", { id: target.id, archived: true }), arch);
973
+ assert.equal(arch.statusCode, 200);
974
+
975
+ const def = new FakeRes();
976
+ await route.handler(req("/api/dsh-mneme/list"), def);
977
+ assert.equal(JSON.parse(def.body).total, 1, "default list hides archived rows");
978
+
979
+ const only = new FakeRes();
980
+ await route.handler(req("/api/dsh-mneme/list?archived=only"), only);
981
+ const onlyData = JSON.parse(only.body);
982
+ assert.equal(onlyData.total, 1, "archived-only count matches rows");
983
+ assert.deepEqual(onlyData.items.map((m) => m.title), ["已归档"]);
984
+ assert.equal(onlyData.items[0].archived, true);
985
+
986
+ // 恢复(unarchive)后归档视图清空、默认列表重新可见
987
+ const restore = new FakeRes();
988
+ await upd.handler(req("/api/dsh-mneme/update", "POST", { id: target.id, archived: false }), restore);
989
+ assert.equal(restore.statusCode, 200);
990
+ const empty = new FakeRes();
991
+ await route.handler(req("/api/dsh-mneme/list?archived=only"), empty);
992
+ assert.equal(JSON.parse(empty.body).total, 0);
993
+ const back = new FakeRes();
994
+ await route.handler(req("/api/dsh-mneme/list"), back);
995
+ assert.equal(JSON.parse(back.body).total, 2);
996
+ });