@openparachute/vault 0.4.4-rc.14 → 0.4.5

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
@@ -1702,6 +1702,162 @@ describe("HTTP /notes", async () => {
1702
1702
  expect(body.createdAt).toBe("2025-01-01T00:00:00.000Z");
1703
1703
  });
1704
1704
 
1705
+ // ---- Extension field (vault#328) ----
1706
+
1707
+ test("POST /notes accepts extension and persists it", async () => {
1708
+ const res = await handleNotes(
1709
+ mkReq("POST", "/notes", {
1710
+ content: "month,total\n2026-01,9000",
1711
+ path: "Tabular/budget",
1712
+ extension: "csv",
1713
+ }),
1714
+ store,
1715
+ "",
1716
+ );
1717
+ expect(res.status).toBe(201);
1718
+ const body = await res.json() as any;
1719
+ expect(body.extension).toBe("csv");
1720
+ const fetched = await store.getNote(body.id);
1721
+ expect(fetched!.extension).toBe("csv");
1722
+ });
1723
+
1724
+ test("POST /notes defaults extension to 'md' when omitted", async () => {
1725
+ const res = await handleNotes(
1726
+ mkReq("POST", "/notes", { content: "plain", path: "p" }),
1727
+ store,
1728
+ "",
1729
+ );
1730
+ const body = await res.json() as any;
1731
+ expect(body.extension).toBe("md");
1732
+ });
1733
+
1734
+ test("POST /notes rejects invalid extension with 400 invalid_extension", async () => {
1735
+ const res = await handleNotes(
1736
+ mkReq("POST", "/notes", { content: "x", extension: "CSV" }),
1737
+ store,
1738
+ "",
1739
+ );
1740
+ expect(res.status).toBe(400);
1741
+ const body = await res.json() as any;
1742
+ expect(body.error_type).toBe("invalid_extension");
1743
+ expect(body.extension).toBe("CSV");
1744
+ });
1745
+
1746
+ test("POST /notes rejects reserved 'parachute' prefix with 400", async () => {
1747
+ const res = await handleNotes(
1748
+ mkReq("POST", "/notes", { content: "x", extension: "parachute" }),
1749
+ store,
1750
+ "",
1751
+ );
1752
+ expect(res.status).toBe(400);
1753
+ const body = await res.json() as any;
1754
+ expect(body.error_type).toBe("invalid_extension");
1755
+ expect(body.reason).toMatch(/reserved/);
1756
+ });
1757
+
1758
+ test("PATCH /notes/:id changes extension", async () => {
1759
+ const note = await store.createNote("hi", { id: "ext-patch", path: "p" });
1760
+ const res = await handleNotes(
1761
+ mkReq("PATCH", "/notes/ext-patch", {
1762
+ extension: "mdx",
1763
+ if_updated_at: note.updatedAt,
1764
+ }),
1765
+ store,
1766
+ "/ext-patch",
1767
+ );
1768
+ expect(res.status).toBe(200);
1769
+ const fetched = await store.getNote("ext-patch");
1770
+ expect(fetched!.extension).toBe("mdx");
1771
+ });
1772
+
1773
+ test("PATCH /notes/:id rejects invalid extension with 400", async () => {
1774
+ const note = await store.createNote("hi", { id: "ext-bad", path: "p" });
1775
+ const res = await handleNotes(
1776
+ mkReq("PATCH", "/notes/ext-bad", {
1777
+ extension: "foo.bar",
1778
+ if_updated_at: note.updatedAt,
1779
+ }),
1780
+ store,
1781
+ "/ext-bad",
1782
+ );
1783
+ expect(res.status).toBe(400);
1784
+ const body = await res.json() as any;
1785
+ expect(body.error_type).toBe("invalid_extension");
1786
+ });
1787
+
1788
+ test("GET /notes?extension=csv filters by extension", async () => {
1789
+ await store.createNote("md note", { path: "a" });
1790
+ await store.createNote("csv note", { path: "b", extension: "csv" });
1791
+ await store.createNote("yaml note", { path: "c", extension: "yaml" });
1792
+ const res = await handleNotes(
1793
+ mkReq("GET", "/notes?extension=csv&include_content=true"),
1794
+ store,
1795
+ "",
1796
+ );
1797
+ const body = await res.json() as any[];
1798
+ expect(body).toHaveLength(1);
1799
+ expect(body[0].path).toBe("b");
1800
+ });
1801
+
1802
+ test("GET /notes?extension=csv&extension=yaml filters by array of extensions", async () => {
1803
+ await store.createNote("md note", { path: "a" });
1804
+ await store.createNote("csv note", { path: "b", extension: "csv" });
1805
+ await store.createNote("yaml note", { path: "c", extension: "yaml" });
1806
+ const res = await handleNotes(
1807
+ mkReq("GET", "/notes?extension=csv&extension=yaml&include_content=true"),
1808
+ store,
1809
+ "",
1810
+ );
1811
+ const body = await res.json() as any[];
1812
+ expect(body).toHaveLength(2);
1813
+ expect(body.map((n) => n.path).sort()).toEqual(["b", "c"]);
1814
+ });
1815
+
1816
+ test("PATCH /notes/:id if_missing=create honors extension", async () => {
1817
+ const idOrPath = encodeURIComponent("Tabular/new-budget");
1818
+ const res = await handleNotes(
1819
+ mkReq("PATCH", `/notes/${idOrPath}`, {
1820
+ content: "month,total\n2026-02,1000",
1821
+ extension: "csv",
1822
+ if_missing: "create",
1823
+ }),
1824
+ store,
1825
+ `/${idOrPath}`,
1826
+ );
1827
+ expect(res.status).toBe(200);
1828
+ const body = await res.json() as any;
1829
+ expect(body.created).toBe(true);
1830
+ expect(body.extension).toBe("csv");
1831
+ });
1832
+
1833
+ test("GET /notes?id=<path> returns 409 ambiguous_path when path matches multiple extensions (vault#330 S1)", async () => {
1834
+ await store.createNote("# md", { path: "Foo", id: "foo-md" });
1835
+ await store.createNote("a,b\n1,2", { path: "Foo", extension: "csv", id: "foo-csv" });
1836
+ const res = await handleNotes(
1837
+ mkReq("GET", "/notes?id=Foo"),
1838
+ store,
1839
+ "",
1840
+ );
1841
+ expect(res.status).toBe(409);
1842
+ const body = await res.json() as any;
1843
+ expect(body.error_type).toBe("ambiguous_path");
1844
+ expect(body.path).toBe("Foo");
1845
+ expect(body.candidates).toHaveLength(2);
1846
+ });
1847
+
1848
+ test("GET /notes?id=Foo.csv resolves the explicit-extension form (vault#330 S1)", async () => {
1849
+ await store.createNote("# md", { path: "Foo", id: "foo-md" });
1850
+ await store.createNote("a,b\n1,2", { path: "Foo", extension: "csv", id: "foo-csv" });
1851
+ const res = await handleNotes(
1852
+ mkReq("GET", "/notes?id=Foo.csv"),
1853
+ store,
1854
+ "",
1855
+ );
1856
+ expect(res.status).toBe(200);
1857
+ const body = await res.json() as any;
1858
+ expect(body.id).toBe("foo-csv");
1859
+ });
1860
+
1705
1861
  test("POST /notes/:id/attachments accepts mimeType (camelCase) in body", async () => {
1706
1862
  const n = await store.createNote("x", { id: "x" });
1707
1863
  const res = await handleNotes(
@@ -3279,6 +3435,25 @@ describe("HTTP /find-path", async () => {
3279
3435
  const res = await handleFindPath(mkReq("GET", "/find-path?source=a"), store);
3280
3436
  expect(res.status).toBe(400);
3281
3437
  });
3438
+
3439
+ test("returns 409 ambiguous_path when source path is ambiguous (vault#331 N1)", async () => {
3440
+ // Two notes share path "Foo" differing by extension. handleFindPath's
3441
+ // resolveNote(source) would otherwise non-deterministically pick
3442
+ // one; post-#331 it throws AmbiguousPathError and the handler's
3443
+ // catch surfaces a structured 409 (same shape as handleNotes).
3444
+ await store.createNote("# md", { id: "foo-md", path: "Foo" });
3445
+ await store.createNote("a,b\n1,2", { id: "foo-csv", path: "Foo", extension: "csv" });
3446
+ await store.createNote("target", { id: "target" });
3447
+ const res = await handleFindPath(
3448
+ mkReq("GET", "/find-path?source=Foo&target=target"),
3449
+ store,
3450
+ );
3451
+ expect(res.status).toBe(409);
3452
+ const body = await res.json() as any;
3453
+ expect(body.error_type).toBe("ambiguous_path");
3454
+ expect(body.path).toBe("Foo");
3455
+ expect(body.candidates).toHaveLength(2);
3456
+ });
3282
3457
  });
3283
3458
 
3284
3459
  describe("stateless MCP transport", async () => {