@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/core/src/core.test.ts +221 -0
- package/core/src/mcp.ts +56 -6
- package/core/src/notes.ts +185 -15
- package/core/src/portable-md.test.ts +531 -0
- package/core/src/portable-md.ts +508 -19
- package/core/src/schema.ts +61 -3
- package/core/src/store.ts +5 -4
- package/core/src/types.ts +27 -3
- package/core/src/wikilinks.ts +74 -14
- package/package.json +1 -1
- package/src/published.test.ts +17 -0
- package/src/routes.ts +121 -3
- package/src/vault.test.ts +175 -0
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 () => {
|