postgresai 0.15.0-dev.1 → 0.15.0-dev.3
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/bin/postgres-ai.ts +228 -4
- package/dist/bin/postgres-ai.js +704 -4
- package/lib/mcp-server.ts +90 -0
- package/lib/reports.ts +373 -0
- package/package.json +1 -1
- package/test/checkup.test.ts +28 -0
- package/test/mcp-server.test.ts +390 -0
- package/test/reports.cli.test.ts +793 -0
- package/test/reports.test.ts +977 -0
package/test/mcp-server.test.ts
CHANGED
|
@@ -1531,6 +1531,396 @@ describe("MCP Server", () => {
|
|
|
1531
1531
|
});
|
|
1532
1532
|
});
|
|
1533
1533
|
|
|
1534
|
+
describe("list_reports tool", () => {
|
|
1535
|
+
test("successfully returns reports list as JSON", async () => {
|
|
1536
|
+
const mockReports = [
|
|
1537
|
+
{ id: 1, org_id: 1, org_name: "TestOrg", project_id: 10, project_name: "prod-db", status: "completed" },
|
|
1538
|
+
];
|
|
1539
|
+
|
|
1540
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
1541
|
+
apiKey: "test-key",
|
|
1542
|
+
baseUrl: null,
|
|
1543
|
+
orgId: null,
|
|
1544
|
+
defaultProject: null,
|
|
1545
|
+
projectName: null,
|
|
1546
|
+
});
|
|
1547
|
+
|
|
1548
|
+
globalThis.fetch = mock(() =>
|
|
1549
|
+
Promise.resolve(
|
|
1550
|
+
new Response(JSON.stringify(mockReports), {
|
|
1551
|
+
status: 200,
|
|
1552
|
+
headers: { "Content-Type": "application/json" },
|
|
1553
|
+
})
|
|
1554
|
+
)
|
|
1555
|
+
) as unknown as typeof fetch;
|
|
1556
|
+
|
|
1557
|
+
const response = await handleToolCall(createRequest("list_reports"));
|
|
1558
|
+
|
|
1559
|
+
expect(response.isError).toBeUndefined();
|
|
1560
|
+
const parsed = JSON.parse(getResponseText(response));
|
|
1561
|
+
expect(parsed).toHaveLength(1);
|
|
1562
|
+
expect(parsed[0].status).toBe("completed");
|
|
1563
|
+
|
|
1564
|
+
readConfigSpy.mockRestore();
|
|
1565
|
+
});
|
|
1566
|
+
|
|
1567
|
+
test("passes filters to API", async () => {
|
|
1568
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
1569
|
+
apiKey: "test-key",
|
|
1570
|
+
baseUrl: null,
|
|
1571
|
+
orgId: null,
|
|
1572
|
+
defaultProject: null,
|
|
1573
|
+
projectName: null,
|
|
1574
|
+
});
|
|
1575
|
+
|
|
1576
|
+
let capturedUrl: string | undefined;
|
|
1577
|
+
globalThis.fetch = mock((url: string) => {
|
|
1578
|
+
capturedUrl = url;
|
|
1579
|
+
return Promise.resolve(
|
|
1580
|
+
new Response(JSON.stringify([]), {
|
|
1581
|
+
status: 200,
|
|
1582
|
+
headers: { "Content-Type": "application/json" },
|
|
1583
|
+
})
|
|
1584
|
+
);
|
|
1585
|
+
}) as unknown as typeof fetch;
|
|
1586
|
+
|
|
1587
|
+
await handleToolCall(createRequest("list_reports", {
|
|
1588
|
+
project_id: 5,
|
|
1589
|
+
status: "completed",
|
|
1590
|
+
limit: 10,
|
|
1591
|
+
}));
|
|
1592
|
+
|
|
1593
|
+
expect(capturedUrl).toContain("project_id=eq.5");
|
|
1594
|
+
expect(capturedUrl).toContain("status=eq.completed");
|
|
1595
|
+
expect(capturedUrl).toContain("limit=10");
|
|
1596
|
+
|
|
1597
|
+
readConfigSpy.mockRestore();
|
|
1598
|
+
});
|
|
1599
|
+
|
|
1600
|
+
test("handles API errors gracefully", async () => {
|
|
1601
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
1602
|
+
apiKey: "test-key",
|
|
1603
|
+
baseUrl: null,
|
|
1604
|
+
orgId: null,
|
|
1605
|
+
defaultProject: null,
|
|
1606
|
+
projectName: null,
|
|
1607
|
+
});
|
|
1608
|
+
|
|
1609
|
+
globalThis.fetch = mock(() =>
|
|
1610
|
+
Promise.resolve(
|
|
1611
|
+
new Response('{"message": "Unauthorized"}', {
|
|
1612
|
+
status: 401,
|
|
1613
|
+
headers: { "Content-Type": "application/json" },
|
|
1614
|
+
})
|
|
1615
|
+
)
|
|
1616
|
+
) as unknown as typeof fetch;
|
|
1617
|
+
|
|
1618
|
+
const response = await handleToolCall(createRequest("list_reports"));
|
|
1619
|
+
|
|
1620
|
+
expect(response.isError).toBe(true);
|
|
1621
|
+
expect(getResponseText(response)).toContain("401");
|
|
1622
|
+
|
|
1623
|
+
readConfigSpy.mockRestore();
|
|
1624
|
+
});
|
|
1625
|
+
|
|
1626
|
+
test("passes before_date to API as created_at filter", async () => {
|
|
1627
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
1628
|
+
apiKey: "test-key",
|
|
1629
|
+
baseUrl: null,
|
|
1630
|
+
orgId: null,
|
|
1631
|
+
defaultProject: null,
|
|
1632
|
+
projectName: null,
|
|
1633
|
+
});
|
|
1634
|
+
|
|
1635
|
+
let capturedUrl: string | undefined;
|
|
1636
|
+
globalThis.fetch = mock((url: string) => {
|
|
1637
|
+
capturedUrl = url;
|
|
1638
|
+
return Promise.resolve(
|
|
1639
|
+
new Response(JSON.stringify([]), {
|
|
1640
|
+
status: 200,
|
|
1641
|
+
headers: { "Content-Type": "application/json" },
|
|
1642
|
+
})
|
|
1643
|
+
);
|
|
1644
|
+
}) as unknown as typeof fetch;
|
|
1645
|
+
|
|
1646
|
+
await handleToolCall(createRequest("list_reports", {
|
|
1647
|
+
before_date: "2025-01-15",
|
|
1648
|
+
}));
|
|
1649
|
+
|
|
1650
|
+
expect(capturedUrl).toContain("created_at=lt.2025-01-15");
|
|
1651
|
+
|
|
1652
|
+
readConfigSpy.mockRestore();
|
|
1653
|
+
});
|
|
1654
|
+
|
|
1655
|
+
test("fetches all reports when all=true", async () => {
|
|
1656
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
1657
|
+
apiKey: "test-key",
|
|
1658
|
+
baseUrl: null,
|
|
1659
|
+
orgId: null,
|
|
1660
|
+
defaultProject: null,
|
|
1661
|
+
projectName: null,
|
|
1662
|
+
});
|
|
1663
|
+
|
|
1664
|
+
const mockReports = [
|
|
1665
|
+
{ id: 10, org_id: 1, org_name: "O", project_id: 1, project_name: "P", status: "completed" },
|
|
1666
|
+
];
|
|
1667
|
+
|
|
1668
|
+
globalThis.fetch = mock(() =>
|
|
1669
|
+
Promise.resolve(
|
|
1670
|
+
new Response(JSON.stringify(mockReports), {
|
|
1671
|
+
status: 200,
|
|
1672
|
+
headers: { "Content-Type": "application/json" },
|
|
1673
|
+
})
|
|
1674
|
+
)
|
|
1675
|
+
) as unknown as typeof fetch;
|
|
1676
|
+
|
|
1677
|
+
const response = await handleToolCall(createRequest("list_reports", {
|
|
1678
|
+
all: true,
|
|
1679
|
+
}));
|
|
1680
|
+
|
|
1681
|
+
expect(response.isError).toBeUndefined();
|
|
1682
|
+
const parsed = JSON.parse(getResponseText(response));
|
|
1683
|
+
expect(parsed).toHaveLength(1);
|
|
1684
|
+
expect(parsed[0].id).toBe(10);
|
|
1685
|
+
|
|
1686
|
+
readConfigSpy.mockRestore();
|
|
1687
|
+
});
|
|
1688
|
+
});
|
|
1689
|
+
|
|
1690
|
+
describe("list_report_files tool", () => {
|
|
1691
|
+
test("returns error when neither report_id nor check_id is provided", async () => {
|
|
1692
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
1693
|
+
apiKey: "test-key",
|
|
1694
|
+
baseUrl: null,
|
|
1695
|
+
orgId: null,
|
|
1696
|
+
defaultProject: null,
|
|
1697
|
+
projectName: null,
|
|
1698
|
+
});
|
|
1699
|
+
|
|
1700
|
+
const response = await handleToolCall(createRequest("list_report_files", {}));
|
|
1701
|
+
|
|
1702
|
+
expect(response.isError).toBe(true);
|
|
1703
|
+
expect(getResponseText(response)).toContain("Either report_id or check_id is required");
|
|
1704
|
+
|
|
1705
|
+
readConfigSpy.mockRestore();
|
|
1706
|
+
});
|
|
1707
|
+
|
|
1708
|
+
test("works with only check_id (no report_id)", async () => {
|
|
1709
|
+
const mockFiles = [
|
|
1710
|
+
{ id: 100, checkup_report_id: 1, filename: "H002.md", check_id: "H002", type: "md" },
|
|
1711
|
+
];
|
|
1712
|
+
|
|
1713
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
1714
|
+
apiKey: "test-key",
|
|
1715
|
+
baseUrl: null,
|
|
1716
|
+
orgId: null,
|
|
1717
|
+
defaultProject: null,
|
|
1718
|
+
projectName: null,
|
|
1719
|
+
});
|
|
1720
|
+
|
|
1721
|
+
let capturedUrl: string | undefined;
|
|
1722
|
+
globalThis.fetch = mock((url: string) => {
|
|
1723
|
+
capturedUrl = url;
|
|
1724
|
+
return Promise.resolve(
|
|
1725
|
+
new Response(JSON.stringify(mockFiles), {
|
|
1726
|
+
status: 200,
|
|
1727
|
+
headers: { "Content-Type": "application/json" },
|
|
1728
|
+
})
|
|
1729
|
+
);
|
|
1730
|
+
}) as unknown as typeof fetch;
|
|
1731
|
+
|
|
1732
|
+
const response = await handleToolCall(createRequest("list_report_files", {
|
|
1733
|
+
check_id: "H002",
|
|
1734
|
+
}));
|
|
1735
|
+
|
|
1736
|
+
expect(response.isError).toBeUndefined();
|
|
1737
|
+
const parsed = JSON.parse(getResponseText(response));
|
|
1738
|
+
expect(parsed[0].filename).toBe("H002.md");
|
|
1739
|
+
expect(capturedUrl).toContain("check_id=eq.H002");
|
|
1740
|
+
expect(capturedUrl).not.toContain("checkup_report_id");
|
|
1741
|
+
|
|
1742
|
+
readConfigSpy.mockRestore();
|
|
1743
|
+
});
|
|
1744
|
+
|
|
1745
|
+
test("successfully returns report files", async () => {
|
|
1746
|
+
const mockFiles = [
|
|
1747
|
+
{ id: 100, checkup_report_id: 1, filename: "H002.md", check_id: "H002", type: "md" },
|
|
1748
|
+
];
|
|
1749
|
+
|
|
1750
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
1751
|
+
apiKey: "test-key",
|
|
1752
|
+
baseUrl: null,
|
|
1753
|
+
orgId: null,
|
|
1754
|
+
defaultProject: null,
|
|
1755
|
+
projectName: null,
|
|
1756
|
+
});
|
|
1757
|
+
|
|
1758
|
+
let capturedUrl: string | undefined;
|
|
1759
|
+
globalThis.fetch = mock((url: string) => {
|
|
1760
|
+
capturedUrl = url;
|
|
1761
|
+
return Promise.resolve(
|
|
1762
|
+
new Response(JSON.stringify(mockFiles), {
|
|
1763
|
+
status: 200,
|
|
1764
|
+
headers: { "Content-Type": "application/json" },
|
|
1765
|
+
})
|
|
1766
|
+
);
|
|
1767
|
+
}) as unknown as typeof fetch;
|
|
1768
|
+
|
|
1769
|
+
const response = await handleToolCall(createRequest("list_report_files", {
|
|
1770
|
+
report_id: 1,
|
|
1771
|
+
type: "md",
|
|
1772
|
+
check_id: "H002",
|
|
1773
|
+
}));
|
|
1774
|
+
|
|
1775
|
+
expect(response.isError).toBeUndefined();
|
|
1776
|
+
const parsed = JSON.parse(getResponseText(response));
|
|
1777
|
+
expect(parsed[0].filename).toBe("H002.md");
|
|
1778
|
+
expect(capturedUrl).toContain("checkup_report_id=eq.1");
|
|
1779
|
+
expect(capturedUrl).toContain("type=eq.md");
|
|
1780
|
+
expect(capturedUrl).toContain("check_id=eq.H002");
|
|
1781
|
+
|
|
1782
|
+
readConfigSpy.mockRestore();
|
|
1783
|
+
});
|
|
1784
|
+
});
|
|
1785
|
+
|
|
1786
|
+
describe("get_report_data tool", () => {
|
|
1787
|
+
test("returns error when neither report_id nor check_id is provided", async () => {
|
|
1788
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
1789
|
+
apiKey: "test-key",
|
|
1790
|
+
baseUrl: null,
|
|
1791
|
+
orgId: null,
|
|
1792
|
+
defaultProject: null,
|
|
1793
|
+
projectName: null,
|
|
1794
|
+
});
|
|
1795
|
+
|
|
1796
|
+
const response = await handleToolCall(createRequest("get_report_data", {}));
|
|
1797
|
+
|
|
1798
|
+
expect(response.isError).toBe(true);
|
|
1799
|
+
expect(getResponseText(response)).toContain("Either report_id or check_id is required");
|
|
1800
|
+
|
|
1801
|
+
readConfigSpy.mockRestore();
|
|
1802
|
+
});
|
|
1803
|
+
|
|
1804
|
+
test("works with only check_id (no report_id)", async () => {
|
|
1805
|
+
const mockData = [
|
|
1806
|
+
{
|
|
1807
|
+
id: 100,
|
|
1808
|
+
checkup_report_id: 1,
|
|
1809
|
+
filename: "H002.md",
|
|
1810
|
+
check_id: "H002",
|
|
1811
|
+
type: "md",
|
|
1812
|
+
data: "# H002\n\nUnused indexes found.",
|
|
1813
|
+
},
|
|
1814
|
+
];
|
|
1815
|
+
|
|
1816
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
1817
|
+
apiKey: "test-key",
|
|
1818
|
+
baseUrl: null,
|
|
1819
|
+
orgId: null,
|
|
1820
|
+
defaultProject: null,
|
|
1821
|
+
projectName: null,
|
|
1822
|
+
});
|
|
1823
|
+
|
|
1824
|
+
let capturedUrl: string | undefined;
|
|
1825
|
+
globalThis.fetch = mock((url: string) => {
|
|
1826
|
+
capturedUrl = url;
|
|
1827
|
+
return Promise.resolve(
|
|
1828
|
+
new Response(JSON.stringify(mockData), {
|
|
1829
|
+
status: 200,
|
|
1830
|
+
headers: { "Content-Type": "application/json" },
|
|
1831
|
+
})
|
|
1832
|
+
);
|
|
1833
|
+
}) as unknown as typeof fetch;
|
|
1834
|
+
|
|
1835
|
+
const response = await handleToolCall(createRequest("get_report_data", {
|
|
1836
|
+
check_id: "H002",
|
|
1837
|
+
}));
|
|
1838
|
+
|
|
1839
|
+
expect(response.isError).toBeUndefined();
|
|
1840
|
+
const parsed = JSON.parse(getResponseText(response));
|
|
1841
|
+
expect(parsed[0].data).toContain("# H002");
|
|
1842
|
+
expect(capturedUrl).toContain("check_id=eq.H002");
|
|
1843
|
+
expect(capturedUrl).not.toContain("checkup_report_id");
|
|
1844
|
+
|
|
1845
|
+
readConfigSpy.mockRestore();
|
|
1846
|
+
});
|
|
1847
|
+
|
|
1848
|
+
test("successfully returns report data with content", async () => {
|
|
1849
|
+
const mockData = [
|
|
1850
|
+
{
|
|
1851
|
+
id: 100,
|
|
1852
|
+
checkup_report_id: 1,
|
|
1853
|
+
filename: "H002.md",
|
|
1854
|
+
check_id: "H002",
|
|
1855
|
+
type: "md",
|
|
1856
|
+
data: "# H002\n\nUnused indexes found.",
|
|
1857
|
+
},
|
|
1858
|
+
];
|
|
1859
|
+
|
|
1860
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
1861
|
+
apiKey: "test-key",
|
|
1862
|
+
baseUrl: null,
|
|
1863
|
+
orgId: null,
|
|
1864
|
+
defaultProject: null,
|
|
1865
|
+
projectName: null,
|
|
1866
|
+
});
|
|
1867
|
+
|
|
1868
|
+
globalThis.fetch = mock(() =>
|
|
1869
|
+
Promise.resolve(
|
|
1870
|
+
new Response(JSON.stringify(mockData), {
|
|
1871
|
+
status: 200,
|
|
1872
|
+
headers: { "Content-Type": "application/json" },
|
|
1873
|
+
})
|
|
1874
|
+
)
|
|
1875
|
+
) as unknown as typeof fetch;
|
|
1876
|
+
|
|
1877
|
+
const response = await handleToolCall(createRequest("get_report_data", {
|
|
1878
|
+
report_id: 1,
|
|
1879
|
+
type: "md",
|
|
1880
|
+
check_id: "H002",
|
|
1881
|
+
}));
|
|
1882
|
+
|
|
1883
|
+
expect(response.isError).toBeUndefined();
|
|
1884
|
+
const parsed = JSON.parse(getResponseText(response));
|
|
1885
|
+
expect(parsed[0].data).toContain("# H002");
|
|
1886
|
+
|
|
1887
|
+
readConfigSpy.mockRestore();
|
|
1888
|
+
});
|
|
1889
|
+
|
|
1890
|
+
test("passes filters to API", async () => {
|
|
1891
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
1892
|
+
apiKey: "test-key",
|
|
1893
|
+
baseUrl: null,
|
|
1894
|
+
orgId: null,
|
|
1895
|
+
defaultProject: null,
|
|
1896
|
+
projectName: null,
|
|
1897
|
+
});
|
|
1898
|
+
|
|
1899
|
+
let capturedUrl: string | undefined;
|
|
1900
|
+
globalThis.fetch = mock((url: string) => {
|
|
1901
|
+
capturedUrl = url;
|
|
1902
|
+
return Promise.resolve(
|
|
1903
|
+
new Response(JSON.stringify([]), {
|
|
1904
|
+
status: 200,
|
|
1905
|
+
headers: { "Content-Type": "application/json" },
|
|
1906
|
+
})
|
|
1907
|
+
);
|
|
1908
|
+
}) as unknown as typeof fetch;
|
|
1909
|
+
|
|
1910
|
+
await handleToolCall(createRequest("get_report_data", {
|
|
1911
|
+
report_id: 42,
|
|
1912
|
+
type: "json",
|
|
1913
|
+
check_id: "F004",
|
|
1914
|
+
}));
|
|
1915
|
+
|
|
1916
|
+
expect(capturedUrl).toContain("checkup_report_id=eq.42");
|
|
1917
|
+
expect(capturedUrl).toContain("type=eq.json");
|
|
1918
|
+
expect(capturedUrl).toContain("check_id=eq.F004");
|
|
1919
|
+
|
|
1920
|
+
readConfigSpy.mockRestore();
|
|
1921
|
+
});
|
|
1922
|
+
});
|
|
1923
|
+
|
|
1534
1924
|
describe("error propagation", () => {
|
|
1535
1925
|
test("propagates API errors through MCP layer", async () => {
|
|
1536
1926
|
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|