@openparachute/vault 0.4.3 → 0.4.4-rc.11

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
@@ -2783,6 +2783,147 @@ describe("HTTP PATCH /notes/:idOrPath (update)", async () => {
2783
2783
  expect(body.id).toBe("big");
2784
2784
  expect(body.path).toBe("big");
2785
2785
  });
2786
+
2787
+ // vault#287 — HTTP must match MCP on validation_status attachment.
2788
+ // Pre-#287 fix: MCP `update-note` attached validation_status; HTTP
2789
+ // PATCH didn't. HTTP consumers using schema-validated vaults had no
2790
+ // way to see schema warnings without re-reading + replaying validation
2791
+ // client-side. These tests pin the symmetry on both response shapes
2792
+ // (`include_content: true` and `false`) and confirm the no-schema
2793
+ // case still returns no validation_status (advisory only — never
2794
+ // forced onto vaults that don't declare fields).
2795
+
2796
+ test("PATCH attaches validation_status with enum_mismatch warning when tag schema is violated", async () => {
2797
+ await store.upsertTagSchema("task287patch", {
2798
+ fields: { priority: { type: "string", enum: ["high", "low"] } },
2799
+ });
2800
+ const note = await store.createNote("body", {
2801
+ id: "p287a",
2802
+ tags: ["task287patch"],
2803
+ metadata: { priority: "high" },
2804
+ });
2805
+ const res = await handleNotes(
2806
+ mkReq("PATCH", "/notes/p287a", {
2807
+ metadata: { priority: "ULTRA" },
2808
+ if_updated_at: note.updatedAt,
2809
+ }),
2810
+ store,
2811
+ "/p287a",
2812
+ );
2813
+ expect(res.status).toBe(200);
2814
+ const body = await res.json() as any;
2815
+ // The write still lands — validation is advisory.
2816
+ expect(body.metadata.priority).toBe("ULTRA");
2817
+ // …but the response carries the warning so the HTTP caller knows.
2818
+ expect(body.validation_status).toBeTruthy();
2819
+ expect(body.validation_status.schemas).toContain("task287patch");
2820
+ expect(body.validation_status.warnings.length).toBeGreaterThan(0);
2821
+ expect(body.validation_status.warnings[0].reason).toBe("enum_mismatch");
2822
+ expect(body.validation_status.warnings[0].field).toBe("priority");
2823
+ });
2824
+
2825
+ test("PATCH preserves validation_status on the lean (include_content: false) response", async () => {
2826
+ await store.upsertTagSchema("task287lean", {
2827
+ fields: { priority: { type: "string", enum: ["high", "low"] } },
2828
+ });
2829
+ const note = await store.createNote("body", {
2830
+ id: "p287b",
2831
+ tags: ["task287lean"],
2832
+ metadata: { priority: "high" },
2833
+ });
2834
+ const res = await handleNotes(
2835
+ mkReq("PATCH", "/notes/p287b", {
2836
+ metadata: { priority: "ULTRA" },
2837
+ include_content: false,
2838
+ if_updated_at: note.updatedAt,
2839
+ }),
2840
+ store,
2841
+ "/p287b",
2842
+ );
2843
+ expect(res.status).toBe(200);
2844
+ const body = await res.json() as any;
2845
+ // Lean shape: no `content`, has `byteSize` + `preview`.
2846
+ expect(body.content).toBeUndefined();
2847
+ expect(typeof body.byteSize).toBe("number");
2848
+ // …and validation_status survives the lean conversion.
2849
+ expect(body.validation_status).toBeTruthy();
2850
+ expect(body.validation_status.warnings[0].reason).toBe("enum_mismatch");
2851
+ });
2852
+
2853
+ test("PATCH omits validation_status when no tag on the note declares fields", async () => {
2854
+ // No tag schemas configured for this note — the response should look
2855
+ // exactly like the pre-#287 shape (no validation_status). The behavior-
2856
+ // unchanged guarantee for callers that don't use tag schemas.
2857
+ await store.createNote("body", { id: "p287c", tags: ["plain"] });
2858
+ const res = await handleNotes(
2859
+ mkReq("PATCH", "/notes/p287c", { content: "updated", force: true }),
2860
+ store,
2861
+ "/p287c",
2862
+ );
2863
+ expect(res.status).toBe(200);
2864
+ const body = await res.json() as any;
2865
+ expect(body.content).toBe("updated");
2866
+ expect(body.validation_status).toBeUndefined();
2867
+ });
2868
+ });
2869
+
2870
+ describe("HTTP POST /notes — validation_status attachment (vault#287)", async () => {
2871
+ // Mirror of the PATCH cases for create. The MCP create-note path
2872
+ // attaches validation_status; HTTP POST must match (vault#287).
2873
+
2874
+ test("POST attaches validation_status with type_mismatch warning", async () => {
2875
+ await store.upsertTagSchema("task287post", {
2876
+ fields: { done: { type: "boolean" } },
2877
+ });
2878
+ const res = await handleNotes(
2879
+ mkReq("POST", "/notes", {
2880
+ content: "x",
2881
+ tags: ["task287post"],
2882
+ metadata: { done: "yes" }, // wrong type
2883
+ }),
2884
+ store,
2885
+ "",
2886
+ );
2887
+ expect(res.status).toBe(201);
2888
+ const body = await res.json() as any;
2889
+ expect(body.id).toBeTruthy();
2890
+ expect(body.validation_status).toBeTruthy();
2891
+ expect(body.validation_status.warnings[0].reason).toBe("type_mismatch");
2892
+ expect(body.validation_status.warnings[0].field).toBe("done");
2893
+ });
2894
+
2895
+ test("POST batch attaches validation_status per-note", async () => {
2896
+ await store.upsertTagSchema("task287batch", {
2897
+ fields: { priority: { type: "string", enum: ["high", "low"] } },
2898
+ });
2899
+ const res = await handleNotes(
2900
+ mkReq("POST", "/notes", {
2901
+ notes: [
2902
+ { content: "good", tags: ["task287batch"], metadata: { priority: "high" } },
2903
+ { content: "bad", tags: ["task287batch"], metadata: { priority: "ULTRA" } },
2904
+ ],
2905
+ }),
2906
+ store,
2907
+ "",
2908
+ );
2909
+ expect(res.status).toBe(201);
2910
+ const body = await res.json() as any[];
2911
+ expect(body).toHaveLength(2);
2912
+ expect(body[0].validation_status.warnings).toEqual([]);
2913
+ expect(body[1].validation_status.warnings[0].reason).toBe("enum_mismatch");
2914
+ });
2915
+
2916
+ test("POST omits validation_status when no tag declares fields (back-compat)", async () => {
2917
+ const res = await handleNotes(
2918
+ mkReq("POST", "/notes", { content: "no schema here", tags: ["plain287"] }),
2919
+ store,
2920
+ "",
2921
+ );
2922
+ expect(res.status).toBe(201);
2923
+ const body = await res.json() as any;
2924
+ expect(body.id).toBeTruthy();
2925
+ expect(body.validation_status).toBeUndefined();
2926
+ });
2786
2927
  });
2787
2928
 
2788
2929
  describe("HTTP /tags", async () => {