@modusensus/dsh-mneme 0.7.5 → 0.7.7

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.
@@ -398,6 +398,139 @@ test("memory_delete on missing id returns deleted:false", async () => {
398
398
  assert.equal(result.deleted, false);
399
399
  });
400
400
 
401
+ // --- issue #48: id resolution via unique prefix (delete/update/forget/archive) --
402
+
403
+ test("memory_delete accepts a unique id prefix", async () => {
404
+ const id = "a1b2c3d4-0000-0000-0000-000000000001";
405
+ const { registered, store } = setup();
406
+ store.save({ id, type: "decision", title: "t", content: "c" });
407
+ const del = registered.find((t) => t.name === "memory_delete");
408
+ const result = await del.execute({ id: "a1b2c3d4" });
409
+ assert.equal(result.deleted, true);
410
+ assert.equal(store.getById(id), undefined, "entry is gone");
411
+ assert.equal(store.count(), 0);
412
+ });
413
+
414
+ test("memory_delete rejects an ambiguous id prefix without deleting", async () => {
415
+ const { registered, store } = setup();
416
+ store.save({ id: "deadbeef-0000-0000-0000-000000000001", type: "decision", title: "t1", content: "c" });
417
+ store.save({ id: "deadbeef-0000-0000-0000-000000000002", type: "decision", title: "t2", content: "c" });
418
+ const del = registered.find((t) => t.name === "memory_delete");
419
+ await assert.rejects(() => del.execute({ id: "deadbeef" }), /matches 2 entries.*refusing to guess/s);
420
+ assert.equal(store.count(), 2, "ambiguous prefix removes nothing");
421
+ });
422
+
423
+ test("memory_delete with a non-matching id prefix returns deleted:false and leaves data", async () => {
424
+ const { registered, store } = setup();
425
+ store.save({ id: "a1b2c3d4-0000-0000-0000-000000000001", type: "decision", title: "t", content: "c" });
426
+ const del = registered.find((t) => t.name === "memory_delete");
427
+ const result = await del.execute({ id: "ffffffff" });
428
+ assert.equal(result.deleted, false);
429
+ assert.equal(store.count(), 1, "nothing removed when the prefix matches nothing");
430
+ });
431
+
432
+ test("memory_update accepts a unique id prefix", async () => {
433
+ const { registered, service, store } = setup();
434
+ const { memory } = service.saveWithDedupe({ type: "decision", title: "t", content: "c" });
435
+ const update = registered.find((t) => t.name === "memory_update");
436
+ const result = await update.execute({ id: memory.id.slice(0, 8), content: "updated via prefix" });
437
+ assert.equal(result.memory.id, memory.id, "resolved to the full id");
438
+ assert.equal(store.getById(memory.id).content, "updated via prefix");
439
+ });
440
+
441
+ test("memory_update rejects an ambiguous id prefix", async () => {
442
+ const { registered, store } = setup();
443
+ store.save({ id: "cafe1234-0000-0000-0000-000000000001", type: "decision", title: "t1", content: "c" });
444
+ store.save({ id: "cafe1234-0000-0000-0000-000000000002", type: "decision", title: "t2", content: "c" });
445
+ const update = registered.find((t) => t.name === "memory_update");
446
+ await assert.rejects(() => update.execute({ id: "cafe1234", content: "x" }), /matches 2 entries.*refusing to guess/s);
447
+ assert.equal(store.getById("cafe1234-0000-0000-0000-000000000001").content, "c");
448
+ });
449
+
450
+ test("memory_forget accepts a unique id prefix and restores it", async () => {
451
+ const id = "facade01-0000-0000-0000-000000000001";
452
+ const { registered, store } = setup();
453
+ store.save({ id, type: "project", title: "t", content: "c", importance: 5 });
454
+ const forget = registered.find((t) => t.name === "memory_forget");
455
+ const result = await forget.execute({ id: "facade01" });
456
+ assert.equal(result.memory.id, id, "resolved to the full id");
457
+ assert.equal(result.memory.forgotten, true);
458
+ assert.equal(store.getById(id).forgotten, true);
459
+ const restored = await forget.execute({ id: "facade01", forgotten: false });
460
+ assert.equal(restored.memory.forgotten, false);
461
+ });
462
+
463
+ test("memory_archive accepts a unique id prefix and restores it", async () => {
464
+ const id = "arcade01-0000-0000-0000-000000000001";
465
+ const { registered, service, store } = setup();
466
+ store.save({ id, type: "project", title: "t", content: "c" });
467
+ const archive = registered.find((t) => t.name === "memory_archive");
468
+ const result = await archive.execute({ id: "arcade01" });
469
+ assert.equal(result.memory.id, id, "resolved to the full id");
470
+ assert.equal(result.memory.archived, true);
471
+ assert.ok(!service.list({ type: "project" }).some((x) => x.id === id), "archived hidden from default list");
472
+ const restored = await archive.execute({ id: "arcade01", archived: false });
473
+ assert.equal(restored.memory.archived, false);
474
+ assert.ok(service.list({ type: "project" }).some((x) => x.id === id), "restored entry visible again");
475
+ });
476
+
477
+ // Exact id match must win even when the same string is also a prefix of
478
+ // another (custom/unequal-length ids): a full id is never "ambiguous" with
479
+ // a longer one that merely starts with it.
480
+ test("memory_delete prefers an exact id match over an identical-prefix collision", async () => {
481
+ const { registered, store } = setup();
482
+ store.save({ id: "abc", type: "decision", title: "short", content: "c" });
483
+ store.save({ id: "abc-def-000000000000000000000000001", type: "decision", title: "long", content: "c" });
484
+ const del = registered.find((t) => t.name === "memory_delete");
485
+ const result = await del.execute({ id: "abc" });
486
+ assert.equal(result.deleted, true);
487
+ assert.equal(store.getById("abc"), undefined, "exact-match entry removed");
488
+ assert.equal(store.count(), 1, "the longer id sharing the prefix stays");
489
+ });
490
+
491
+ // A wildcard-only id ("%", "_") strips to an empty prefix; it must never
492
+ // degrade into a `LIKE '%'` table-wide match that deletes a lone memory.
493
+ test("memory_delete with a wildcard-only id matches nothing and deletes nothing", async () => {
494
+ const { registered, store } = setup();
495
+ store.save({ id: "a1b2c3d4-0000-0000-0000-000000000001", type: "decision", title: "t", content: "c" });
496
+ const del = registered.find((t) => t.name === "memory_delete");
497
+ for (const wildcard of ["%", "_", "%_", "%%", "___"]) {
498
+ const result = await del.execute({ id: wildcard });
499
+ assert.equal(result.deleted, false, `id "${wildcard}" must not match the whole table`);
500
+ }
501
+ assert.equal(store.count(), 1, "nothing removed by wildcard-only ids");
502
+ });
503
+
504
+ // Hand-copied ids can carry surrounding whitespace; resolve against the
505
+ // trimmed form so a padded id still reaches the right memory.
506
+ test("memory_delete trims surrounding whitespace from the id", async () => {
507
+ const id = "a1b2c3d4-0000-0000-0000-000000000001";
508
+ const { registered, store } = setup();
509
+ store.save({ id, type: "decision", title: "t", content: "c" });
510
+ const del = registered.find((t) => t.name === "memory_delete");
511
+ const result = await del.execute({ id: ` ${id} ` });
512
+ assert.equal(result.deleted, true);
513
+ assert.equal(store.count(), 0);
514
+ });
515
+
516
+ // issue #48 observability: an unmatched id on memory_delete is logged (warn)
517
+ // instead of silently vanishing — the logger is optional, but when present the
518
+ // service layer records the silent-miss case.
519
+ test("memory_delete logs a warn when the id matches nothing", async () => {
520
+ const warns = [];
521
+ const store = createStore(":memory:");
522
+ const service = createService({ store, mirror: null, config: {}, logger: { warn: (msg) => warns.push(msg) } });
523
+ store.save({ id: "a1b2c3d4-0000-0000-0000-000000000001", type: "decision", title: "t", content: "c" });
524
+ const registered = [];
525
+ const ctx = { tools: { register(def) { registered.push(def); return () => {}; } } };
526
+ createTools(ctx, service, {}, undefined);
527
+ const del = registered.find((t) => t.name === "memory_delete");
528
+ const result = await del.execute({ id: "ffffffff" });
529
+ assert.equal(result.deleted, false);
530
+ assert.equal(warns.length, 1, "one warn for the silent-miss case");
531
+ assert.match(warns[0], /matched nothing/);
532
+ });
533
+
401
534
  test("memory_delete by query deletes the best match (no id round-trip)", async () => {
402
535
  const { registered, service, store } = setup();
403
536
  const { memory } = service.saveWithDedupe({ type: "preference", title: "喜欢 Rust", content: "用户偏好 Rust 优先于 Go", importance: 4 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modusensus/dsh-mneme",
3
- "version": "0.7.5",
3
+ "version": "0.7.7",
4
4
  "description": "Structured memory engine for DeepSeek Harness. Offline semantic search, entity-attribute-timeline, autoDream self-consolidation, and human-editable Markdown storage.",
5
5
  "license": "MIT",
6
6
  "repository": {