@openparachute/vault 0.7.0-rc.4 → 0.7.0-rc.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/src/vault.test.ts CHANGED
@@ -484,7 +484,7 @@ describe("deeper link queries", async () => {
484
484
  describe("MCP tools", async () => {
485
485
  test("generates the consolidated tool set", () => {
486
486
  const tools = generateMcpTools(store);
487
- expect(tools.length).toBe(10);
487
+ expect(tools.length).toBe(13);
488
488
 
489
489
  const names = tools.map((t) => t.name);
490
490
  expect(names).toContain("query-notes");
@@ -498,6 +498,12 @@ describe("MCP tools", async () => {
498
498
  expect(names).toContain("vault-info");
499
499
  // prune-schema (admin) — drops orphaned indexed-field columns.
500
500
  expect(names).toContain("prune-schema");
501
+ // rename-tag / merge-tags (vault#552) — MCP parity with the pre-existing
502
+ // REST engine.
503
+ expect(names).toContain("rename-tag");
504
+ expect(names).toContain("merge-tags");
505
+ // doctor (admin, vault#552) — read-only taxonomy/metadata integrity scan.
506
+ expect(names).toContain("doctor");
501
507
  // Six note-schema MCP tools (list/update/delete-note-schema +
502
508
  // list/set/delete-schema-mapping) retired in v17 — vault#267.
503
509
  expect(names).not.toContain("list-note-schemas");
@@ -973,7 +979,7 @@ describe("scoped MCP wrapper", async () => {
973
979
  closeAllStores();
974
980
  });
975
981
 
976
- test("create-note with schema tag auto-populates defaults", async () => {
982
+ test("create-note with schema tag applies explicit defaults; undeclared fields stay absent (vault#553 Decision B)", async () => {
977
983
  const { generateScopedMcpTools } = await import("./mcp-tools.ts");
978
984
  const { writeVaultConfig } = await import("./config.ts");
979
985
  const { getVaultStore, closeAllStores } = await import("./vault-store.ts");
@@ -989,8 +995,8 @@ describe("scoped MCP wrapper", async () => {
989
995
  await vaultStore.upsertTagSchema("person", {
990
996
  description: "A person",
991
997
  fields: {
992
- first_appeared: { type: "string", description: "When" },
993
- relationship: { type: "string", description: "How" },
998
+ first_appeared: { type: "string", description: "When", default: "unknown" },
999
+ relationship: { type: "string", description: "How" }, // no default — stays absent
994
1000
  },
995
1001
  });
996
1002
 
@@ -998,19 +1004,19 @@ describe("scoped MCP wrapper", async () => {
998
1004
  const createNote = tools.find((t) => t.name === "create-note")!;
999
1005
  const queryNotes = tools.find((t) => t.name === "query-notes")!;
1000
1006
 
1001
- // Create a note tagged person with no metadata — defaults auto-populated
1007
+ // Create a note tagged person with no metadata — the EXPLICIT default
1008
+ // auto-populates; the field with no `default` stays absent.
1002
1009
  const result = await createNote.execute({
1003
1010
  content: "Alice",
1004
1011
  tags: ["person"],
1005
1012
  }) as any;
1006
1013
  expect(result.content).toBe("Alice");
1007
1014
 
1008
- // Verify defaults were written
1009
1015
  const fresh = await queryNotes.execute({ id: result.id }) as any;
1010
- expect(fresh.metadata.first_appeared).toBe("");
1011
- expect(fresh.metadata.relationship).toBe("");
1016
+ expect(fresh.metadata.first_appeared).toBe("unknown");
1017
+ expect(fresh.metadata.relationship).toBeUndefined();
1012
1018
 
1013
- // Create with explicit metadata — preserved
1019
+ // Create with explicit metadata — preserved (overrides the default).
1014
1020
  const result2 = await createNote.execute({
1015
1021
  content: "Bob",
1016
1022
  tags: ["person"],
@@ -1023,7 +1029,7 @@ describe("scoped MCP wrapper", async () => {
1023
1029
  closeAllStores();
1024
1030
  });
1025
1031
 
1026
- test("update-note tags.add with schema auto-populates defaults", async () => {
1032
+ test("update-note tags.add with schema applies explicit defaults; undeclared fields stay absent (vault#553 Decision B)", async () => {
1027
1033
  const { generateScopedMcpTools } = await import("./mcp-tools.ts");
1028
1034
  const { writeVaultConfig } = await import("./config.ts");
1029
1035
  const { getVaultStore, closeAllStores: close } = await import("./vault-store.ts");
@@ -1039,16 +1045,16 @@ describe("scoped MCP wrapper", async () => {
1039
1045
  await vaultStore.upsertTagSchema("person", {
1040
1046
  description: "A person",
1041
1047
  fields: {
1042
- first_appeared: { type: "string", description: "When" },
1043
- relationship: { type: "string", description: "How" },
1048
+ first_appeared: { type: "string", description: "When", default: "unknown" },
1049
+ relationship: { type: "string", description: "How" }, // no default — stays absent
1044
1050
  },
1045
1051
  });
1046
1052
  await vaultStore.upsertTagSchema("project", {
1047
1053
  description: "A project",
1048
1054
  fields: {
1049
- status: { type: "string", enum: ["active", "completed", "abandoned"], description: "Status" },
1050
- active: { type: "boolean", description: "Is active" },
1051
- priority: { type: "integer", description: "Priority level" },
1055
+ status: { type: "string", enum: ["active", "completed", "abandoned"], description: "Status", default: "active" },
1056
+ active: { type: "boolean", description: "Is active", default: false },
1057
+ priority: { type: "integer", description: "Priority level" }, // no default — stays absent
1052
1058
  },
1053
1059
  });
1054
1060
  const tools = generateScopedMcpTools(vaultName);
@@ -1060,10 +1066,11 @@ describe("scoped MCP wrapper", async () => {
1060
1066
  const note = await createNote.execute({ content: "Alice" }) as any;
1061
1067
  await updateNote.execute({ id: note.id, tags: { add: ["person"] }, force: true });
1062
1068
  const after = await queryNotes.execute({ id: note.id }) as any;
1063
- expect(after.metadata.first_appeared).toBe("");
1064
- expect(after.metadata.relationship).toBe("");
1069
+ expect(after.metadata.first_appeared).toBe("unknown");
1070
+ expect(after.metadata.relationship).toBeUndefined();
1065
1071
 
1066
- // Tag note that already has partial metadata — only missing fields populated
1072
+ // Tag note that already has partial metadata — only missing fields WITH a
1073
+ // declared default get populated; the field with no default stays absent.
1067
1074
  const note2 = await createNote.execute({
1068
1075
  content: "Bob",
1069
1076
  metadata: { first_appeared: "2023-11" },
@@ -1071,22 +1078,22 @@ describe("scoped MCP wrapper", async () => {
1071
1078
  await updateNote.execute({ id: note2.id, tags: { add: ["person"] }, force: true });
1072
1079
  const after2 = await queryNotes.execute({ id: note2.id }) as any;
1073
1080
  expect(after2.metadata.first_appeared).toBe("2023-11"); // preserved
1074
- expect(after2.metadata.relationship).toBe(""); // added
1081
+ expect(after2.metadata.relationship).toBeUndefined(); // no default — stays absent
1075
1082
 
1076
- // Tag with #project — enum defaults to first value, boolean to false, integer to 0
1083
+ // Tag with #project — declared defaults land; `priority` (no default) stays absent.
1077
1084
  const note4 = await createNote.execute({ content: "My Project" }) as any;
1078
1085
  await updateNote.execute({ id: note4.id, tags: { add: ["project"] }, force: true });
1079
1086
  const after4 = await queryNotes.execute({ id: note4.id }) as any;
1080
1087
  expect(after4.metadata.status).toBe("active");
1081
1088
  expect(after4.metadata.active).toBe(false);
1082
- expect(after4.metadata.priority).toBe(0);
1089
+ expect(after4.metadata.priority).toBeUndefined();
1083
1090
 
1084
- // Multiple schema tags at once — all defaults merged
1091
+ // Multiple schema tags at once — all EXPLICIT defaults merged.
1085
1092
  const note5 = await createNote.execute({ content: "Multi" }) as any;
1086
1093
  await updateNote.execute({ id: note5.id, tags: { add: ["person", "project"] }, force: true });
1087
1094
  const after5 = await queryNotes.execute({ id: note5.id }) as any;
1088
- expect(after5.metadata.first_appeared).toBe("");
1089
- expect(after5.metadata.relationship).toBe("");
1095
+ expect(after5.metadata.first_appeared).toBe("unknown");
1096
+ expect(after5.metadata.relationship).toBeUndefined();
1090
1097
  expect(after5.metadata.status).toBe("active");
1091
1098
  expect(after5.metadata.active).toBe(false);
1092
1099
 
@@ -1560,9 +1567,12 @@ describe("scoped MCP wrapper", async () => {
1560
1567
  });
1561
1568
 
1562
1569
  const vaultStore = getVaultStore(vaultName);
1570
+ // vault#553 Decision B: backfill is explicit-`default`-only — declare
1571
+ // one so this test still exercises an actual defaults-write (and can
1572
+ // assert it lands without bumping updatedAt).
1563
1573
  await vaultStore.upsertTagSchema("person", {
1564
1574
  description: "A person",
1565
- fields: { name: { type: "string" } },
1575
+ fields: { name: { type: "string", default: "unknown" } },
1566
1576
  });
1567
1577
 
1568
1578
  const tools = generateScopedMcpTools(vaultName);
@@ -1575,7 +1585,7 @@ describe("scoped MCP wrapper", async () => {
1575
1585
  await updateNote.execute({ id: note.id, tags: { add: ["person"] }, force: true });
1576
1586
  const after = await queryNotes.execute({ id: note.id }) as any;
1577
1587
  expect(after.updatedAt).toBe(originalUpdatedAt);
1578
- expect(after.metadata.name).toBe("");
1588
+ expect(after.metadata.name).toBe("unknown");
1579
1589
 
1580
1590
  close();
1581
1591
  });
@@ -2193,8 +2203,10 @@ describe("HTTP /notes", async () => {
2193
2203
  // mapped over the pre-defaults in-memory objects, so default-filled
2194
2204
  // metadata was missing from `POST /api/notes` responses.
2195
2205
  test("POST /notes response reflects post-applySchemaDefaults state (vault#316)", async () => {
2206
+ // vault#553 Decision B: backfill is explicit-`default`-only — declare one
2207
+ // so this test still exercises the post-defaults re-read mechanism.
2196
2208
  await store.upsertTagSchema("task", {
2197
- fields: { priority: { type: "string", enum: ["high", "low"] } },
2209
+ fields: { priority: { type: "string", enum: ["high", "low"], default: "high" } },
2198
2210
  });
2199
2211
 
2200
2212
  // Single: default lands in the returned metadata and agrees with disk.
@@ -2205,7 +2217,7 @@ describe("HTTP /notes", async () => {
2205
2217
  );
2206
2218
  expect(single.status).toBe(201);
2207
2219
  const singleBody = await single.json() as any;
2208
- expect(singleBody.metadata?.priority).toBe("high"); // first enum value
2220
+ expect(singleBody.metadata?.priority).toBe("high"); // explicit schema default
2209
2221
  const onDisk = await store.getNoteByPath("Inbox/task-1");
2210
2222
  expect((onDisk!.metadata as any)?.priority).toBe("high");
2211
2223
 
@@ -5414,8 +5426,11 @@ describe("stateless MCP transport", async () => {
5414
5426
  expect(toolNames).not.toContain("delete-note");
5415
5427
  expect(toolNames).not.toContain("update-tag");
5416
5428
  expect(toolNames).not.toContain("delete-tag");
5429
+ expect(toolNames).not.toContain("rename-tag");
5430
+ expect(toolNames).not.toContain("merge-tags");
5417
5431
  // Admin tools (vault#376) are hidden too
5418
5432
  expect(toolNames).not.toContain("manage-token");
5433
+ expect(toolNames).not.toContain("doctor");
5419
5434
  // Read tier is exactly 4 tools.
5420
5435
  expect(toolNames.length).toBe(4);
5421
5436
 
@@ -5658,7 +5673,7 @@ describe("MCP tools/list scope tiers (vault#376)", () => {
5658
5673
  expect(names.length).toBe(4);
5659
5674
  });
5660
5675
 
5661
- test("vault:read + vault:write sees the 9 read+write tools", async () => {
5676
+ test("vault:read + vault:write sees the 11 read+write tools", async () => {
5662
5677
  const names = await listToolNames(["vault:read", "vault:write"]);
5663
5678
  expect(new Set(names)).toEqual(
5664
5679
  new Set([
@@ -5671,24 +5686,31 @@ describe("MCP tools/list scope tiers (vault#376)", () => {
5671
5686
  "delete-note",
5672
5687
  "update-tag",
5673
5688
  "delete-tag",
5689
+ // vault#552: MCP parity with the pre-existing REST rename/merge engine.
5690
+ "rename-tag",
5691
+ "merge-tags",
5674
5692
  ]),
5675
5693
  );
5676
- expect(names.length).toBe(9);
5694
+ expect(names.length).toBe(11);
5677
5695
  expect(names).not.toContain("manage-token");
5696
+ expect(names).not.toContain("doctor");
5678
5697
  // Aaron 2026-05-27: delete-* are write-tier (same destructive verb as
5679
- // update). Only manage-token is admin-gated.
5698
+ // update). Only manage-token/prune-schema/doctor are admin-gated.
5680
5699
  expect(names).toContain("delete-note");
5681
5700
  expect(names).toContain("delete-tag");
5682
5701
  });
5683
5702
 
5684
- test("vault:admin sees all 11 tools including manage-token + prune-schema", async () => {
5703
+ test("vault:admin sees all 14 tools including manage-token + prune-schema + doctor", async () => {
5685
5704
  const names = await listToolNames(["vault:read", "vault:write", "vault:admin"]);
5686
5705
  expect(names).toContain("manage-token");
5687
5706
  expect(names).toContain("prune-schema");
5688
- expect(names.length).toBe(11);
5707
+ expect(names).toContain("doctor");
5708
+ expect(names).toContain("rename-tag");
5709
+ expect(names).toContain("merge-tags");
5710
+ expect(names.length).toBe(14);
5689
5711
  });
5690
5712
 
5691
- test("legacy-derived full token sees all 11 tools (back-compat)", async () => {
5713
+ test("legacy-derived full token sees all 14 tools (back-compat)", async () => {
5692
5714
  const { handleScopedMcp } = await import("./mcp-http.ts");
5693
5715
  const { writeVaultConfig } = await import("./config.ts");
5694
5716
  const { closeAllStores } = await import("./vault-store.ts");
@@ -5721,9 +5743,10 @@ describe("MCP tools/list scope tiers (vault#376)", () => {
5721
5743
  } as any);
5722
5744
  const body = await res.json() as any;
5723
5745
  const names: string[] = body.result.tools.map((t: any) => t.name);
5724
- expect(names.length).toBe(11);
5746
+ expect(names.length).toBe(14);
5725
5747
  expect(names).toContain("manage-token");
5726
5748
  expect(names).toContain("prune-schema");
5749
+ expect(names).toContain("doctor");
5727
5750
  closeAllStores();
5728
5751
  });
5729
5752