postgresai 0.15.0-dev.1 → 0.15.0-dev.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.
@@ -75,6 +75,7 @@ describe("MCP Server", () => {
75
75
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
76
76
  apiKey: null,
77
77
  baseUrl: null,
78
+ storageBaseUrl: null,
78
79
  orgId: null,
79
80
  defaultProject: null,
80
81
  projectName: null,
@@ -92,6 +93,7 @@ describe("MCP Server", () => {
92
93
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
93
94
  apiKey: null,
94
95
  baseUrl: null,
96
+ storageBaseUrl: null,
95
97
  orgId: null,
96
98
  defaultProject: null,
97
99
  projectName: null,
@@ -121,6 +123,7 @@ describe("MCP Server", () => {
121
123
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
122
124
  apiKey: "config-api-key",
123
125
  baseUrl: null,
126
+ storageBaseUrl: null,
124
127
  orgId: null,
125
128
  defaultProject: null,
126
129
  projectName: null,
@@ -151,6 +154,7 @@ describe("MCP Server", () => {
151
154
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
152
155
  apiKey: null,
153
156
  baseUrl: null,
157
+ storageBaseUrl: null,
154
158
  orgId: null,
155
159
  defaultProject: null,
156
160
  projectName: null,
@@ -186,6 +190,7 @@ describe("MCP Server", () => {
186
190
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
187
191
  apiKey: "test-key",
188
192
  baseUrl: null,
193
+ storageBaseUrl: null,
189
194
  orgId: null,
190
195
  defaultProject: null,
191
196
  projectName: null,
@@ -214,6 +219,7 @@ describe("MCP Server", () => {
214
219
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
215
220
  apiKey: "test-key",
216
221
  baseUrl: null,
222
+ storageBaseUrl: null,
217
223
  orgId: null,
218
224
  defaultProject: null,
219
225
  projectName: null,
@@ -242,6 +248,7 @@ describe("MCP Server", () => {
242
248
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
243
249
  apiKey: "test-key",
244
250
  baseUrl: null,
251
+ storageBaseUrl: null,
245
252
  orgId: null,
246
253
  defaultProject: null,
247
254
  projectName: null,
@@ -259,6 +266,7 @@ describe("MCP Server", () => {
259
266
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
260
267
  apiKey: "test-key",
261
268
  baseUrl: null,
269
+ storageBaseUrl: null,
262
270
  orgId: null,
263
271
  defaultProject: null,
264
272
  projectName: null,
@@ -276,6 +284,7 @@ describe("MCP Server", () => {
276
284
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
277
285
  apiKey: "test-key",
278
286
  baseUrl: null,
287
+ storageBaseUrl: null,
279
288
  orgId: null,
280
289
  defaultProject: null,
281
290
  projectName: null,
@@ -306,6 +315,7 @@ describe("MCP Server", () => {
306
315
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
307
316
  apiKey: "test-key",
308
317
  baseUrl: null,
318
+ storageBaseUrl: null,
309
319
  orgId: null,
310
320
  defaultProject: null,
311
321
  projectName: null,
@@ -347,6 +357,7 @@ describe("MCP Server", () => {
347
357
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
348
358
  apiKey: "test-key",
349
359
  baseUrl: null,
360
+ storageBaseUrl: null,
350
361
  orgId: null,
351
362
  defaultProject: null,
352
363
  projectName: null,
@@ -366,6 +377,7 @@ describe("MCP Server", () => {
366
377
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
367
378
  apiKey: "test-key",
368
379
  baseUrl: null,
380
+ storageBaseUrl: null,
369
381
  orgId: null,
370
382
  defaultProject: null,
371
383
  projectName: null,
@@ -376,7 +388,8 @@ describe("MCP Server", () => {
376
388
  );
377
389
 
378
390
  expect(response.isError).toBe(true);
379
- expect(getResponseText(response)).toBe("content is required");
391
+ // Error message updated to reflect that attachments alone are also valid input.
392
+ expect(getResponseText(response)).toBe("content or attachments is required");
380
393
 
381
394
  readConfigSpy.mockRestore();
382
395
  });
@@ -385,6 +398,7 @@ describe("MCP Server", () => {
385
398
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
386
399
  apiKey: "test-key",
387
400
  baseUrl: null,
401
+ storageBaseUrl: null,
388
402
  orgId: null,
389
403
  defaultProject: null,
390
404
  projectName: null,
@@ -419,6 +433,7 @@ describe("MCP Server", () => {
419
433
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
420
434
  apiKey: "test-key",
421
435
  baseUrl: null,
436
+ storageBaseUrl: null,
422
437
  orgId: null,
423
438
  defaultProject: null,
424
439
  projectName: null,
@@ -457,6 +472,7 @@ describe("MCP Server", () => {
457
472
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
458
473
  apiKey: "test-key",
459
474
  baseUrl: null,
475
+ storageBaseUrl: null,
460
476
  orgId: 1,
461
477
  defaultProject: null,
462
478
  projectName: null,
@@ -474,6 +490,7 @@ describe("MCP Server", () => {
474
490
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
475
491
  apiKey: "test-key",
476
492
  baseUrl: null,
493
+ storageBaseUrl: null,
477
494
  orgId: 1,
478
495
  defaultProject: null,
479
496
  projectName: null,
@@ -491,6 +508,7 @@ describe("MCP Server", () => {
491
508
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
492
509
  apiKey: "test-key",
493
510
  baseUrl: null,
511
+ storageBaseUrl: null,
494
512
  orgId: null,
495
513
  defaultProject: null,
496
514
  projectName: null,
@@ -508,6 +526,7 @@ describe("MCP Server", () => {
508
526
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
509
527
  apiKey: "test-key",
510
528
  baseUrl: null,
529
+ storageBaseUrl: null,
511
530
  orgId: 42,
512
531
  defaultProject: null,
513
532
  projectName: null,
@@ -537,6 +556,7 @@ describe("MCP Server", () => {
537
556
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
538
557
  apiKey: "test-key",
539
558
  baseUrl: null,
559
+ storageBaseUrl: null,
540
560
  orgId: 1,
541
561
  defaultProject: null,
542
562
  projectName: null,
@@ -572,6 +592,7 @@ describe("MCP Server", () => {
572
592
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
573
593
  apiKey: "test-key",
574
594
  baseUrl: null,
595
+ storageBaseUrl: null,
575
596
  orgId: null,
576
597
  defaultProject: null,
577
598
  projectName: null,
@@ -616,6 +637,7 @@ describe("MCP Server", () => {
616
637
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
617
638
  apiKey: "test-key",
618
639
  baseUrl: null,
640
+ storageBaseUrl: null,
619
641
  orgId: null,
620
642
  defaultProject: null,
621
643
  projectName: null,
@@ -635,6 +657,7 @@ describe("MCP Server", () => {
635
657
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
636
658
  apiKey: "test-key",
637
659
  baseUrl: null,
660
+ storageBaseUrl: null,
638
661
  orgId: null,
639
662
  defaultProject: null,
640
663
  projectName: null,
@@ -652,6 +675,7 @@ describe("MCP Server", () => {
652
675
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
653
676
  apiKey: "test-key",
654
677
  baseUrl: null,
678
+ storageBaseUrl: null,
655
679
  orgId: null,
656
680
  defaultProject: null,
657
681
  projectName: null,
@@ -671,6 +695,7 @@ describe("MCP Server", () => {
671
695
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
672
696
  apiKey: "test-key",
673
697
  baseUrl: null,
698
+ storageBaseUrl: null,
674
699
  orgId: null,
675
700
  defaultProject: null,
676
701
  projectName: null,
@@ -690,6 +715,7 @@ describe("MCP Server", () => {
690
715
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
691
716
  apiKey: "test-key",
692
717
  baseUrl: null,
718
+ storageBaseUrl: null,
693
719
  orgId: null,
694
720
  defaultProject: null,
695
721
  projectName: null,
@@ -726,6 +752,7 @@ describe("MCP Server", () => {
726
752
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
727
753
  apiKey: "test-key",
728
754
  baseUrl: null,
755
+ storageBaseUrl: null,
729
756
  orgId: null,
730
757
  defaultProject: null,
731
758
  projectName: null,
@@ -753,6 +780,7 @@ describe("MCP Server", () => {
753
780
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
754
781
  apiKey: "test-key",
755
782
  baseUrl: null,
783
+ storageBaseUrl: null,
756
784
  orgId: null,
757
785
  defaultProject: null,
758
786
  projectName: null,
@@ -785,6 +813,7 @@ describe("MCP Server", () => {
785
813
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
786
814
  apiKey: "test-key",
787
815
  baseUrl: null,
816
+ storageBaseUrl: null,
788
817
  orgId: null,
789
818
  defaultProject: null,
790
819
  projectName: null,
@@ -817,6 +846,7 @@ describe("MCP Server", () => {
817
846
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
818
847
  apiKey: "test-key",
819
848
  baseUrl: null,
849
+ storageBaseUrl: null,
820
850
  orgId: null,
821
851
  defaultProject: null,
822
852
  projectName: null,
@@ -851,6 +881,7 @@ describe("MCP Server", () => {
851
881
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
852
882
  apiKey: "test-key",
853
883
  baseUrl: null,
884
+ storageBaseUrl: null,
854
885
  orgId: null,
855
886
  defaultProject: null,
856
887
  projectName: null,
@@ -870,6 +901,7 @@ describe("MCP Server", () => {
870
901
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
871
902
  apiKey: "test-key",
872
903
  baseUrl: null,
904
+ storageBaseUrl: null,
873
905
  orgId: null,
874
906
  defaultProject: null,
875
907
  projectName: null,
@@ -880,7 +912,7 @@ describe("MCP Server", () => {
880
912
  );
881
913
 
882
914
  expect(response.isError).toBe(true);
883
- expect(getResponseText(response)).toBe("content is required");
915
+ expect(getResponseText(response)).toBe("content or attachments is required");
884
916
 
885
917
  readConfigSpy.mockRestore();
886
918
  });
@@ -889,6 +921,7 @@ describe("MCP Server", () => {
889
921
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
890
922
  apiKey: "test-key",
891
923
  baseUrl: null,
924
+ storageBaseUrl: null,
892
925
  orgId: null,
893
926
  defaultProject: null,
894
927
  projectName: null,
@@ -923,6 +956,7 @@ describe("MCP Server", () => {
923
956
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
924
957
  apiKey: "test-key",
925
958
  baseUrl: null,
959
+ storageBaseUrl: null,
926
960
  orgId: null,
927
961
  defaultProject: null,
928
962
  projectName: null,
@@ -957,6 +991,7 @@ describe("MCP Server", () => {
957
991
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
958
992
  apiKey: "test-key",
959
993
  baseUrl: null,
994
+ storageBaseUrl: null,
960
995
  orgId: null,
961
996
  defaultProject: null,
962
997
  projectName: null,
@@ -974,6 +1009,7 @@ describe("MCP Server", () => {
974
1009
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
975
1010
  apiKey: "test-key",
976
1011
  baseUrl: null,
1012
+ storageBaseUrl: null,
977
1013
  orgId: null,
978
1014
  defaultProject: null,
979
1015
  projectName: null,
@@ -991,6 +1027,7 @@ describe("MCP Server", () => {
991
1027
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
992
1028
  apiKey: "test-key",
993
1029
  baseUrl: null,
1030
+ storageBaseUrl: null,
994
1031
  orgId: null,
995
1032
  defaultProject: null,
996
1033
  projectName: null,
@@ -1008,6 +1045,7 @@ describe("MCP Server", () => {
1008
1045
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1009
1046
  apiKey: "test-key",
1010
1047
  baseUrl: null,
1048
+ storageBaseUrl: null,
1011
1049
  orgId: null,
1012
1050
  defaultProject: null,
1013
1051
  projectName: null,
@@ -1025,6 +1063,7 @@ describe("MCP Server", () => {
1025
1063
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1026
1064
  apiKey: "test-key",
1027
1065
  baseUrl: null,
1066
+ storageBaseUrl: null,
1028
1067
  orgId: null,
1029
1068
  defaultProject: null,
1030
1069
  projectName: null,
@@ -1063,6 +1102,7 @@ describe("MCP Server", () => {
1063
1102
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1064
1103
  apiKey: "test-key",
1065
1104
  baseUrl: null,
1105
+ storageBaseUrl: null,
1066
1106
  orgId: null,
1067
1107
  defaultProject: null,
1068
1108
  projectName: null,
@@ -1098,6 +1138,7 @@ describe("MCP Server", () => {
1098
1138
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1099
1139
  apiKey: "test-key",
1100
1140
  baseUrl: null,
1141
+ storageBaseUrl: null,
1101
1142
  orgId: null,
1102
1143
  defaultProject: null,
1103
1144
  projectName: null,
@@ -1133,6 +1174,7 @@ describe("MCP Server", () => {
1133
1174
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1134
1175
  apiKey: "test-key",
1135
1176
  baseUrl: null,
1177
+ storageBaseUrl: null,
1136
1178
  orgId: null,
1137
1179
  defaultProject: null,
1138
1180
  projectName: null,
@@ -1150,6 +1192,7 @@ describe("MCP Server", () => {
1150
1192
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1151
1193
  apiKey: "test-key",
1152
1194
  baseUrl: null,
1195
+ storageBaseUrl: null,
1153
1196
  orgId: null,
1154
1197
  defaultProject: null,
1155
1198
  projectName: null,
@@ -1172,6 +1215,7 @@ describe("MCP Server", () => {
1172
1215
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1173
1216
  apiKey: "test-key",
1174
1217
  baseUrl: null,
1218
+ storageBaseUrl: null,
1175
1219
  orgId: null,
1176
1220
  defaultProject: null,
1177
1221
  projectName: null,
@@ -1202,6 +1246,7 @@ describe("MCP Server", () => {
1202
1246
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1203
1247
  apiKey: "test-key",
1204
1248
  baseUrl: null,
1249
+ storageBaseUrl: null,
1205
1250
  orgId: null,
1206
1251
  defaultProject: null,
1207
1252
  projectName: null,
@@ -1221,6 +1266,7 @@ describe("MCP Server", () => {
1221
1266
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1222
1267
  apiKey: "test-key",
1223
1268
  baseUrl: null,
1269
+ storageBaseUrl: null,
1224
1270
  orgId: null,
1225
1271
  defaultProject: null,
1226
1272
  projectName: null,
@@ -1240,6 +1286,7 @@ describe("MCP Server", () => {
1240
1286
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1241
1287
  apiKey: "test-key",
1242
1288
  baseUrl: null,
1289
+ storageBaseUrl: null,
1243
1290
  orgId: null,
1244
1291
  defaultProject: null,
1245
1292
  projectName: null,
@@ -1276,6 +1323,7 @@ describe("MCP Server", () => {
1276
1323
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1277
1324
  apiKey: "test-key",
1278
1325
  baseUrl: null,
1326
+ storageBaseUrl: null,
1279
1327
  orgId: null,
1280
1328
  defaultProject: null,
1281
1329
  projectName: null,
@@ -1318,6 +1366,7 @@ describe("MCP Server", () => {
1318
1366
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1319
1367
  apiKey: "test-key",
1320
1368
  baseUrl: null,
1369
+ storageBaseUrl: null,
1321
1370
  orgId: null,
1322
1371
  defaultProject: null,
1323
1372
  projectName: null,
@@ -1356,6 +1405,7 @@ describe("MCP Server", () => {
1356
1405
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1357
1406
  apiKey: "test-key",
1358
1407
  baseUrl: null,
1408
+ storageBaseUrl: null,
1359
1409
  orgId: null,
1360
1410
  defaultProject: null,
1361
1411
  projectName: null,
@@ -1375,6 +1425,7 @@ describe("MCP Server", () => {
1375
1425
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1376
1426
  apiKey: "test-key",
1377
1427
  baseUrl: null,
1428
+ storageBaseUrl: null,
1378
1429
  orgId: null,
1379
1430
  defaultProject: null,
1380
1431
  projectName: null,
@@ -1394,6 +1445,7 @@ describe("MCP Server", () => {
1394
1445
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1395
1446
  apiKey: "test-key",
1396
1447
  baseUrl: null,
1448
+ storageBaseUrl: null,
1397
1449
  orgId: null,
1398
1450
  defaultProject: null,
1399
1451
  projectName: null,
@@ -1413,6 +1465,7 @@ describe("MCP Server", () => {
1413
1465
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1414
1466
  apiKey: "test-key",
1415
1467
  baseUrl: null,
1468
+ storageBaseUrl: null,
1416
1469
  orgId: null,
1417
1470
  defaultProject: null,
1418
1471
  projectName: null,
@@ -1446,6 +1499,7 @@ describe("MCP Server", () => {
1446
1499
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1447
1500
  apiKey: "test-key",
1448
1501
  baseUrl: null,
1502
+ storageBaseUrl: null,
1449
1503
  orgId: null,
1450
1504
  defaultProject: null,
1451
1505
  projectName: null,
@@ -1478,6 +1532,7 @@ describe("MCP Server", () => {
1478
1532
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1479
1533
  apiKey: "test-key",
1480
1534
  baseUrl: null,
1535
+ storageBaseUrl: null,
1481
1536
  orgId: null,
1482
1537
  defaultProject: null,
1483
1538
  projectName: null,
@@ -1517,6 +1572,7 @@ describe("MCP Server", () => {
1517
1572
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1518
1573
  apiKey: "test-key",
1519
1574
  baseUrl: null,
1575
+ storageBaseUrl: null,
1520
1576
  orgId: null,
1521
1577
  defaultProject: null,
1522
1578
  projectName: null,
@@ -1531,11 +1587,414 @@ describe("MCP Server", () => {
1531
1587
  });
1532
1588
  });
1533
1589
 
1590
+ describe("list_reports tool", () => {
1591
+ test("successfully returns reports list as JSON", async () => {
1592
+ const mockReports = [
1593
+ { id: 1, org_id: 1, org_name: "TestOrg", project_id: 10, project_name: "prod-db", status: "completed" },
1594
+ ];
1595
+
1596
+ const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1597
+ apiKey: "test-key",
1598
+ baseUrl: null,
1599
+ storageBaseUrl: null,
1600
+ orgId: null,
1601
+ defaultProject: null,
1602
+ projectName: null,
1603
+ });
1604
+
1605
+ globalThis.fetch = mock(() =>
1606
+ Promise.resolve(
1607
+ new Response(JSON.stringify(mockReports), {
1608
+ status: 200,
1609
+ headers: { "Content-Type": "application/json" },
1610
+ })
1611
+ )
1612
+ ) as unknown as typeof fetch;
1613
+
1614
+ const response = await handleToolCall(createRequest("list_reports"));
1615
+
1616
+ expect(response.isError).toBeUndefined();
1617
+ const parsed = JSON.parse(getResponseText(response));
1618
+ expect(parsed).toHaveLength(1);
1619
+ expect(parsed[0].status).toBe("completed");
1620
+
1621
+ readConfigSpy.mockRestore();
1622
+ });
1623
+
1624
+ test("passes filters to API", async () => {
1625
+ const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1626
+ apiKey: "test-key",
1627
+ baseUrl: null,
1628
+ storageBaseUrl: null,
1629
+ orgId: null,
1630
+ defaultProject: null,
1631
+ projectName: null,
1632
+ });
1633
+
1634
+ let capturedUrl: string | undefined;
1635
+ globalThis.fetch = mock((url: string) => {
1636
+ capturedUrl = url;
1637
+ return Promise.resolve(
1638
+ new Response(JSON.stringify([]), {
1639
+ status: 200,
1640
+ headers: { "Content-Type": "application/json" },
1641
+ })
1642
+ );
1643
+ }) as unknown as typeof fetch;
1644
+
1645
+ await handleToolCall(createRequest("list_reports", {
1646
+ project_id: 5,
1647
+ status: "completed",
1648
+ limit: 10,
1649
+ }));
1650
+
1651
+ expect(capturedUrl).toContain("project_id=eq.5");
1652
+ expect(capturedUrl).toContain("status=eq.completed");
1653
+ expect(capturedUrl).toContain("limit=10");
1654
+
1655
+ readConfigSpy.mockRestore();
1656
+ });
1657
+
1658
+ test("handles API errors gracefully", async () => {
1659
+ const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1660
+ apiKey: "test-key",
1661
+ baseUrl: null,
1662
+ storageBaseUrl: null,
1663
+ orgId: null,
1664
+ defaultProject: null,
1665
+ projectName: null,
1666
+ });
1667
+
1668
+ globalThis.fetch = mock(() =>
1669
+ Promise.resolve(
1670
+ new Response('{"message": "Unauthorized"}', {
1671
+ status: 401,
1672
+ headers: { "Content-Type": "application/json" },
1673
+ })
1674
+ )
1675
+ ) as unknown as typeof fetch;
1676
+
1677
+ const response = await handleToolCall(createRequest("list_reports"));
1678
+
1679
+ expect(response.isError).toBe(true);
1680
+ expect(getResponseText(response)).toContain("401");
1681
+
1682
+ readConfigSpy.mockRestore();
1683
+ });
1684
+
1685
+ test("passes before_date to API as created_at filter", async () => {
1686
+ const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1687
+ apiKey: "test-key",
1688
+ baseUrl: null,
1689
+ storageBaseUrl: null,
1690
+ orgId: null,
1691
+ defaultProject: null,
1692
+ projectName: null,
1693
+ });
1694
+
1695
+ let capturedUrl: string | undefined;
1696
+ globalThis.fetch = mock((url: string) => {
1697
+ capturedUrl = url;
1698
+ return Promise.resolve(
1699
+ new Response(JSON.stringify([]), {
1700
+ status: 200,
1701
+ headers: { "Content-Type": "application/json" },
1702
+ })
1703
+ );
1704
+ }) as unknown as typeof fetch;
1705
+
1706
+ await handleToolCall(createRequest("list_reports", {
1707
+ before_date: "2025-01-15",
1708
+ }));
1709
+
1710
+ expect(capturedUrl).toContain("created_at=lt.2025-01-15");
1711
+
1712
+ readConfigSpy.mockRestore();
1713
+ });
1714
+
1715
+ test("fetches all reports when all=true", async () => {
1716
+ const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1717
+ apiKey: "test-key",
1718
+ baseUrl: null,
1719
+ storageBaseUrl: null,
1720
+ orgId: null,
1721
+ defaultProject: null,
1722
+ projectName: null,
1723
+ });
1724
+
1725
+ const mockReports = [
1726
+ { id: 10, org_id: 1, org_name: "O", project_id: 1, project_name: "P", status: "completed" },
1727
+ ];
1728
+
1729
+ globalThis.fetch = mock(() =>
1730
+ Promise.resolve(
1731
+ new Response(JSON.stringify(mockReports), {
1732
+ status: 200,
1733
+ headers: { "Content-Type": "application/json" },
1734
+ })
1735
+ )
1736
+ ) as unknown as typeof fetch;
1737
+
1738
+ const response = await handleToolCall(createRequest("list_reports", {
1739
+ all: true,
1740
+ }));
1741
+
1742
+ expect(response.isError).toBeUndefined();
1743
+ const parsed = JSON.parse(getResponseText(response));
1744
+ expect(parsed).toHaveLength(1);
1745
+ expect(parsed[0].id).toBe(10);
1746
+
1747
+ readConfigSpy.mockRestore();
1748
+ });
1749
+ });
1750
+
1751
+ describe("list_report_files tool", () => {
1752
+ test("returns error when neither report_id nor check_id is provided", async () => {
1753
+ const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1754
+ apiKey: "test-key",
1755
+ baseUrl: null,
1756
+ storageBaseUrl: null,
1757
+ orgId: null,
1758
+ defaultProject: null,
1759
+ projectName: null,
1760
+ });
1761
+
1762
+ const response = await handleToolCall(createRequest("list_report_files", {}));
1763
+
1764
+ expect(response.isError).toBe(true);
1765
+ expect(getResponseText(response)).toContain("Either report_id or check_id is required");
1766
+
1767
+ readConfigSpy.mockRestore();
1768
+ });
1769
+
1770
+ test("works with only check_id (no report_id)", async () => {
1771
+ const mockFiles = [
1772
+ { id: 100, checkup_report_id: 1, filename: "H002.md", check_id: "H002", type: "md" },
1773
+ ];
1774
+
1775
+ const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1776
+ apiKey: "test-key",
1777
+ baseUrl: null,
1778
+ storageBaseUrl: null,
1779
+ orgId: null,
1780
+ defaultProject: null,
1781
+ projectName: null,
1782
+ });
1783
+
1784
+ let capturedUrl: string | undefined;
1785
+ globalThis.fetch = mock((url: string) => {
1786
+ capturedUrl = url;
1787
+ return Promise.resolve(
1788
+ new Response(JSON.stringify(mockFiles), {
1789
+ status: 200,
1790
+ headers: { "Content-Type": "application/json" },
1791
+ })
1792
+ );
1793
+ }) as unknown as typeof fetch;
1794
+
1795
+ const response = await handleToolCall(createRequest("list_report_files", {
1796
+ check_id: "H002",
1797
+ }));
1798
+
1799
+ expect(response.isError).toBeUndefined();
1800
+ const parsed = JSON.parse(getResponseText(response));
1801
+ expect(parsed[0].filename).toBe("H002.md");
1802
+ expect(capturedUrl).toContain("check_id=eq.H002");
1803
+ expect(capturedUrl).not.toContain("checkup_report_id");
1804
+
1805
+ readConfigSpy.mockRestore();
1806
+ });
1807
+
1808
+ test("successfully returns report files", async () => {
1809
+ const mockFiles = [
1810
+ { id: 100, checkup_report_id: 1, filename: "H002.md", check_id: "H002", type: "md" },
1811
+ ];
1812
+
1813
+ const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1814
+ apiKey: "test-key",
1815
+ baseUrl: null,
1816
+ storageBaseUrl: null,
1817
+ orgId: null,
1818
+ defaultProject: null,
1819
+ projectName: null,
1820
+ });
1821
+
1822
+ let capturedUrl: string | undefined;
1823
+ globalThis.fetch = mock((url: string) => {
1824
+ capturedUrl = url;
1825
+ return Promise.resolve(
1826
+ new Response(JSON.stringify(mockFiles), {
1827
+ status: 200,
1828
+ headers: { "Content-Type": "application/json" },
1829
+ })
1830
+ );
1831
+ }) as unknown as typeof fetch;
1832
+
1833
+ const response = await handleToolCall(createRequest("list_report_files", {
1834
+ report_id: 1,
1835
+ type: "md",
1836
+ check_id: "H002",
1837
+ }));
1838
+
1839
+ expect(response.isError).toBeUndefined();
1840
+ const parsed = JSON.parse(getResponseText(response));
1841
+ expect(parsed[0].filename).toBe("H002.md");
1842
+ expect(capturedUrl).toContain("checkup_report_id=eq.1");
1843
+ expect(capturedUrl).toContain("type=eq.md");
1844
+ expect(capturedUrl).toContain("check_id=eq.H002");
1845
+
1846
+ readConfigSpy.mockRestore();
1847
+ });
1848
+ });
1849
+
1850
+ describe("get_report_data tool", () => {
1851
+ test("returns error when neither report_id nor check_id is provided", async () => {
1852
+ const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1853
+ apiKey: "test-key",
1854
+ baseUrl: null,
1855
+ storageBaseUrl: null,
1856
+ orgId: null,
1857
+ defaultProject: null,
1858
+ projectName: null,
1859
+ });
1860
+
1861
+ const response = await handleToolCall(createRequest("get_report_data", {}));
1862
+
1863
+ expect(response.isError).toBe(true);
1864
+ expect(getResponseText(response)).toContain("Either report_id or check_id is required");
1865
+
1866
+ readConfigSpy.mockRestore();
1867
+ });
1868
+
1869
+ test("works with only check_id (no report_id)", async () => {
1870
+ const mockData = [
1871
+ {
1872
+ id: 100,
1873
+ checkup_report_id: 1,
1874
+ filename: "H002.md",
1875
+ check_id: "H002",
1876
+ type: "md",
1877
+ data: "# H002\n\nUnused indexes found.",
1878
+ },
1879
+ ];
1880
+
1881
+ const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1882
+ apiKey: "test-key",
1883
+ baseUrl: null,
1884
+ storageBaseUrl: null,
1885
+ orgId: null,
1886
+ defaultProject: null,
1887
+ projectName: null,
1888
+ });
1889
+
1890
+ let capturedUrl: string | undefined;
1891
+ globalThis.fetch = mock((url: string) => {
1892
+ capturedUrl = url;
1893
+ return Promise.resolve(
1894
+ new Response(JSON.stringify(mockData), {
1895
+ status: 200,
1896
+ headers: { "Content-Type": "application/json" },
1897
+ })
1898
+ );
1899
+ }) as unknown as typeof fetch;
1900
+
1901
+ const response = await handleToolCall(createRequest("get_report_data", {
1902
+ check_id: "H002",
1903
+ }));
1904
+
1905
+ expect(response.isError).toBeUndefined();
1906
+ const parsed = JSON.parse(getResponseText(response));
1907
+ expect(parsed[0].data).toContain("# H002");
1908
+ expect(capturedUrl).toContain("check_id=eq.H002");
1909
+ expect(capturedUrl).not.toContain("checkup_report_id");
1910
+
1911
+ readConfigSpy.mockRestore();
1912
+ });
1913
+
1914
+ test("successfully returns report data with content", async () => {
1915
+ const mockData = [
1916
+ {
1917
+ id: 100,
1918
+ checkup_report_id: 1,
1919
+ filename: "H002.md",
1920
+ check_id: "H002",
1921
+ type: "md",
1922
+ data: "# H002\n\nUnused indexes found.",
1923
+ },
1924
+ ];
1925
+
1926
+ const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1927
+ apiKey: "test-key",
1928
+ baseUrl: null,
1929
+ storageBaseUrl: null,
1930
+ orgId: null,
1931
+ defaultProject: null,
1932
+ projectName: null,
1933
+ });
1934
+
1935
+ globalThis.fetch = mock(() =>
1936
+ Promise.resolve(
1937
+ new Response(JSON.stringify(mockData), {
1938
+ status: 200,
1939
+ headers: { "Content-Type": "application/json" },
1940
+ })
1941
+ )
1942
+ ) as unknown as typeof fetch;
1943
+
1944
+ const response = await handleToolCall(createRequest("get_report_data", {
1945
+ report_id: 1,
1946
+ type: "md",
1947
+ check_id: "H002",
1948
+ }));
1949
+
1950
+ expect(response.isError).toBeUndefined();
1951
+ const parsed = JSON.parse(getResponseText(response));
1952
+ expect(parsed[0].data).toContain("# H002");
1953
+
1954
+ readConfigSpy.mockRestore();
1955
+ });
1956
+
1957
+ test("passes filters to API", async () => {
1958
+ const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1959
+ apiKey: "test-key",
1960
+ baseUrl: null,
1961
+ storageBaseUrl: null,
1962
+ orgId: null,
1963
+ defaultProject: null,
1964
+ projectName: null,
1965
+ });
1966
+
1967
+ let capturedUrl: string | undefined;
1968
+ globalThis.fetch = mock((url: string) => {
1969
+ capturedUrl = url;
1970
+ return Promise.resolve(
1971
+ new Response(JSON.stringify([]), {
1972
+ status: 200,
1973
+ headers: { "Content-Type": "application/json" },
1974
+ })
1975
+ );
1976
+ }) as unknown as typeof fetch;
1977
+
1978
+ await handleToolCall(createRequest("get_report_data", {
1979
+ report_id: 42,
1980
+ type: "json",
1981
+ check_id: "F004",
1982
+ }));
1983
+
1984
+ expect(capturedUrl).toContain("checkup_report_id=eq.42");
1985
+ expect(capturedUrl).toContain("type=eq.json");
1986
+ expect(capturedUrl).toContain("check_id=eq.F004");
1987
+
1988
+ readConfigSpy.mockRestore();
1989
+ });
1990
+ });
1991
+
1534
1992
  describe("error propagation", () => {
1535
1993
  test("propagates API errors through MCP layer", async () => {
1536
1994
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1537
1995
  apiKey: "test-key",
1538
1996
  baseUrl: null,
1997
+ storageBaseUrl: null,
1539
1998
  orgId: 1,
1540
1999
  defaultProject: null,
1541
2000
  projectName: null,
@@ -1564,6 +2023,7 @@ describe("MCP Server", () => {
1564
2023
  const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
1565
2024
  apiKey: "test-key",
1566
2025
  baseUrl: null,
2026
+ storageBaseUrl: null,
1567
2027
  orgId: 1,
1568
2028
  defaultProject: null,
1569
2029
  projectName: null,
@@ -1581,4 +2041,486 @@ describe("MCP Server", () => {
1581
2041
  readConfigSpy.mockRestore();
1582
2042
  });
1583
2043
  });
2044
+
2045
+ describe("attachments parameter & file tools", () => {
2046
+ // Real-file approach (rather than fs mocking) — ESM module caching means
2047
+ // monkey-patching fs after `import * as fs from "fs"` does not affect the
2048
+ // already-resolved binding inside storage.ts. Real tmp files are simpler
2049
+ // and match how the existing storage tests work.
2050
+ const fs = require("fs") as typeof import("fs");
2051
+ const path = require("path") as typeof import("path");
2052
+ const os = require("os") as typeof import("os");
2053
+
2054
+ const createdDirs: string[] = [];
2055
+
2056
+ function mockTinyFile(name: string, body = "FAKE"): string {
2057
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pgai-mcp-attach-"));
2058
+ createdDirs.push(dir);
2059
+ const p = path.join(dir, name);
2060
+ fs.writeFileSync(p, body);
2061
+ return p;
2062
+ }
2063
+
2064
+ afterEach(() => {
2065
+ while (createdDirs.length > 0) {
2066
+ const d = createdDirs.pop();
2067
+ if (d) fs.rmSync(d, { recursive: true, force: true });
2068
+ }
2069
+ });
2070
+
2071
+ function configWithKey() {
2072
+ return spyOn(config, "readConfig").mockReturnValue({
2073
+ apiKey: "test-key",
2074
+ baseUrl: null,
2075
+ storageBaseUrl: null,
2076
+ orgId: 1,
2077
+ defaultProject: null,
2078
+ projectName: null,
2079
+ });
2080
+ }
2081
+
2082
+ test("upload_file tool returns url + ready-to-paste markdown link", async () => {
2083
+ const cfgSpy = configWithKey();
2084
+ const fakePath = mockTinyFile("shot.png");
2085
+
2086
+ globalThis.fetch = mock((_url: string, _init?: RequestInit) =>
2087
+ Promise.resolve(
2088
+ new Response(
2089
+ JSON.stringify({
2090
+ success: true,
2091
+ url: "/files/9/abc.png",
2092
+ metadata: { originalName: "shot.png", size: 4, mimeType: "image/png", uploadedAt: "", duration: 0 },
2093
+ requestId: "r1",
2094
+ }),
2095
+ { status: 200, headers: { "Content-Type": "application/json" } }
2096
+ )
2097
+ )
2098
+ ) as unknown as typeof fetch;
2099
+
2100
+ const response = await handleToolCall(
2101
+ createRequest("upload_file", { path: fakePath }),
2102
+ { apiBaseUrl: "https://api.example.com", storageBaseUrl: "https://storage.example.com" }
2103
+ );
2104
+
2105
+ expect(response.isError).toBeFalsy();
2106
+ const obj = JSON.parse(getResponseText(response));
2107
+ expect(obj.success).toBe(true);
2108
+ expect(obj.url).toBe("/files/9/abc.png");
2109
+ // Image extension renders inline.
2110
+ expect(obj.markdown).toBe("![shot.png](https://storage.example.com/files/9/abc.png)");
2111
+
2112
+ cfgSpy.mockRestore();
2113
+ });
2114
+
2115
+ test("upload_file requires path", async () => {
2116
+ const cfgSpy = configWithKey();
2117
+ const r = await handleToolCall(createRequest("upload_file", {}));
2118
+ expect(r.isError).toBe(true);
2119
+ expect(getResponseText(r)).toBe("path is required");
2120
+ cfgSpy.mockRestore();
2121
+ });
2122
+
2123
+ test("download_file tool requires url", async () => {
2124
+ const cfgSpy = configWithKey();
2125
+ const r = await handleToolCall(createRequest("download_file", {}));
2126
+ expect(r.isError).toBe(true);
2127
+ expect(getResponseText(r)).toBe("url is required");
2128
+ cfgSpy.mockRestore();
2129
+ });
2130
+
2131
+ test("post_issue_comment with attachments uploads and appends link", async () => {
2132
+ const cfgSpy = configWithKey();
2133
+ const fakePath = mockTinyFile("debug.png");
2134
+
2135
+ const calls: Array<{ url: string; method?: string; bodyJson?: unknown }> = [];
2136
+ globalThis.fetch = mock(async (url: string | URL, init?: RequestInit) => {
2137
+ const u = String(url);
2138
+ calls.push({
2139
+ url: u,
2140
+ method: init?.method,
2141
+ bodyJson: typeof init?.body === "string" ? JSON.parse(init.body as string) : undefined,
2142
+ });
2143
+ if (u.endsWith("/upload")) {
2144
+ return new Response(
2145
+ JSON.stringify({
2146
+ success: true,
2147
+ url: "/files/9/dbg.png",
2148
+ metadata: { originalName: "debug.png", size: 4, mimeType: "image/png", uploadedAt: "", duration: 0 },
2149
+ requestId: "r1",
2150
+ }),
2151
+ { status: 200, headers: { "Content-Type": "application/json" } }
2152
+ );
2153
+ }
2154
+ if (u.endsWith("/rpc/issue_comment_create")) {
2155
+ return new Response(
2156
+ JSON.stringify({
2157
+ id: "c1",
2158
+ issue_id: "i1",
2159
+ author_id: 1,
2160
+ parent_comment_id: null,
2161
+ content: "ignored",
2162
+ created_at: "",
2163
+ updated_at: "",
2164
+ data: null,
2165
+ }),
2166
+ { status: 200, headers: { "Content-Type": "application/json" } }
2167
+ );
2168
+ }
2169
+ return new Response("nope", { status: 404 });
2170
+ }) as unknown as typeof fetch;
2171
+
2172
+ const response = await handleToolCall(
2173
+ createRequest("post_issue_comment", {
2174
+ issue_id: "11111111-1111-1111-1111-111111111111",
2175
+ content: "see screenshot",
2176
+ attachments: [fakePath],
2177
+ }),
2178
+ { apiBaseUrl: "https://api.example.com", storageBaseUrl: "https://storage.example.com" }
2179
+ );
2180
+
2181
+ expect(response.isError).toBeFalsy();
2182
+
2183
+ // Upload happened first, then comment-create with augmented content.
2184
+ expect(calls[0].url).toContain("/upload");
2185
+ const commentCall = calls.find((c) => c.url.endsWith("/rpc/issue_comment_create"));
2186
+ expect(commentCall).toBeTruthy();
2187
+ const body = commentCall!.bodyJson as { content: string };
2188
+ expect(body.content).toBe("see screenshot\n\n![debug.png](https://storage.example.com/files/9/dbg.png)");
2189
+
2190
+ cfgSpy.mockRestore();
2191
+ });
2192
+
2193
+ test("post_issue_comment with only attachments (no content) is allowed", async () => {
2194
+ const cfgSpy = configWithKey();
2195
+ const fakePath = mockTinyFile("only.png");
2196
+
2197
+ const commentBodies: Array<{ content: string }> = [];
2198
+ globalThis.fetch = mock(async (url: string | URL, init?: RequestInit) => {
2199
+ const u = String(url);
2200
+ if (u.endsWith("/upload")) {
2201
+ return new Response(
2202
+ JSON.stringify({
2203
+ success: true,
2204
+ url: "/files/9/only.png",
2205
+ metadata: { originalName: "only.png", size: 4, mimeType: "image/png", uploadedAt: "", duration: 0 },
2206
+ requestId: "r",
2207
+ }),
2208
+ { status: 200, headers: { "Content-Type": "application/json" } }
2209
+ );
2210
+ }
2211
+ if (u.endsWith("/rpc/issue_comment_create")) {
2212
+ commentBodies.push(JSON.parse(String(init?.body)));
2213
+ return new Response(
2214
+ JSON.stringify({ id: "c1", issue_id: "i1", author_id: 1, parent_comment_id: null, content: "", created_at: "", updated_at: "", data: null }),
2215
+ { status: 200, headers: { "Content-Type": "application/json" } }
2216
+ );
2217
+ }
2218
+ return new Response("nope", { status: 404 });
2219
+ }) as unknown as typeof fetch;
2220
+
2221
+ const response = await handleToolCall(
2222
+ createRequest("post_issue_comment", {
2223
+ issue_id: "11111111-1111-1111-1111-111111111111",
2224
+ content: "",
2225
+ attachments: [fakePath],
2226
+ }),
2227
+ { apiBaseUrl: "https://api.example.com", storageBaseUrl: "https://storage.example.com" }
2228
+ );
2229
+
2230
+ expect(response.isError).toBeFalsy();
2231
+ expect(commentBodies[0].content).toBe("![only.png](https://storage.example.com/files/9/only.png)");
2232
+
2233
+ cfgSpy.mockRestore();
2234
+ });
2235
+
2236
+ test("post_issue_comment with no content and no attachments is rejected", async () => {
2237
+ const cfgSpy = configWithKey();
2238
+ const r = await handleToolCall(
2239
+ createRequest("post_issue_comment", {
2240
+ issue_id: "11111111-1111-1111-1111-111111111111",
2241
+ content: "",
2242
+ })
2243
+ );
2244
+ expect(r.isError).toBe(true);
2245
+ expect(getResponseText(r)).toBe("content or attachments is required");
2246
+ cfgSpy.mockRestore();
2247
+ });
2248
+
2249
+ test("update_issue with attachments and no description fetches existing then appends", async () => {
2250
+ const cfgSpy = configWithKey();
2251
+ const fakePath = mockTinyFile("evidence.png");
2252
+
2253
+ const calls: Array<{ url: string; method?: string; body?: unknown }> = [];
2254
+ globalThis.fetch = mock(async (url: string | URL, init?: RequestInit) => {
2255
+ const u = String(url);
2256
+ calls.push({
2257
+ url: u,
2258
+ method: init?.method,
2259
+ body: typeof init?.body === "string" ? JSON.parse(init.body as string) : undefined,
2260
+ });
2261
+ if (u.includes("/issues?") && (init?.method ?? "GET") === "GET") {
2262
+ return new Response(
2263
+ JSON.stringify([
2264
+ { id: "i1", title: "T", description: "Existing description", status: 0, created_at: "", action_items: [] },
2265
+ ]),
2266
+ { status: 200, headers: { "Content-Type": "application/json" } }
2267
+ );
2268
+ }
2269
+ if (u.endsWith("/upload")) {
2270
+ return new Response(
2271
+ JSON.stringify({
2272
+ success: true,
2273
+ url: "/files/9/ev.png",
2274
+ metadata: { originalName: "evidence.png", size: 4, mimeType: "image/png", uploadedAt: "", duration: 0 },
2275
+ requestId: "r",
2276
+ }),
2277
+ { status: 200, headers: { "Content-Type": "application/json" } }
2278
+ );
2279
+ }
2280
+ if (u.endsWith("/rpc/issue_update")) {
2281
+ return new Response(
2282
+ JSON.stringify({ id: "i1", title: "T", description: "ignored", status: 0, updated_at: "" }),
2283
+ { status: 200, headers: { "Content-Type": "application/json" } }
2284
+ );
2285
+ }
2286
+ return new Response("nope", { status: 404 });
2287
+ }) as unknown as typeof fetch;
2288
+
2289
+ const response = await handleToolCall(
2290
+ createRequest("update_issue", {
2291
+ issue_id: "i1",
2292
+ attachments: [fakePath],
2293
+ }),
2294
+ { apiBaseUrl: "https://api.example.com", storageBaseUrl: "https://storage.example.com" }
2295
+ );
2296
+
2297
+ expect(response.isError).toBeFalsy();
2298
+ const updateCall = calls.find((c) => c.url.endsWith("/rpc/issue_update"));
2299
+ expect(updateCall).toBeTruthy();
2300
+ // Order: GET issues -> POST upload -> POST update.
2301
+ const fetchIdx = calls.findIndex((c) => c.url.includes("/issues?"));
2302
+ const uploadIdx = calls.findIndex((c) => c.url.endsWith("/upload"));
2303
+ const updateIdx = calls.findIndex((c) => c.url.endsWith("/rpc/issue_update"));
2304
+ expect(fetchIdx).toBeGreaterThanOrEqual(0);
2305
+ expect(uploadIdx).toBeGreaterThan(fetchIdx);
2306
+ expect(updateIdx).toBeGreaterThan(uploadIdx);
2307
+
2308
+ const body = updateCall!.body as { p_description: string };
2309
+ expect(body.p_description).toBe(
2310
+ "Existing description\n\n![evidence.png](https://storage.example.com/files/9/ev.png)"
2311
+ );
2312
+
2313
+ cfgSpy.mockRestore();
2314
+ });
2315
+
2316
+ test("update_issue with only attachments is treated as a valid update", async () => {
2317
+ const cfgSpy = configWithKey();
2318
+ const fakePath = mockTinyFile("ok.png");
2319
+
2320
+ globalThis.fetch = mock(async (url: string | URL, init?: RequestInit) => {
2321
+ const u = String(url);
2322
+ if (u.includes("/issues?")) {
2323
+ return new Response(JSON.stringify([{ id: "i1", title: "T", description: "old", status: 0 }]), {
2324
+ status: 200, headers: { "Content-Type": "application/json" },
2325
+ });
2326
+ }
2327
+ if (u.endsWith("/upload")) {
2328
+ return new Response(
2329
+ JSON.stringify({
2330
+ success: true, url: "/files/9/ok.png",
2331
+ metadata: { originalName: "ok.png", size: 4, mimeType: "image/png", uploadedAt: "", duration: 0 },
2332
+ requestId: "r",
2333
+ }),
2334
+ { status: 200, headers: { "Content-Type": "application/json" } }
2335
+ );
2336
+ }
2337
+ if (u.endsWith("/rpc/issue_update")) {
2338
+ return new Response(JSON.stringify({ id: "i1", title: "T", description: "ignored", status: 0, updated_at: "" }), {
2339
+ status: 200, headers: { "Content-Type": "application/json" },
2340
+ });
2341
+ }
2342
+ return new Response("nope", { status: 404 });
2343
+ }) as unknown as typeof fetch;
2344
+
2345
+ const response = await handleToolCall(
2346
+ createRequest("update_issue", { issue_id: "i1", attachments: [fakePath] }),
2347
+ { apiBaseUrl: "https://api.example.com", storageBaseUrl: "https://storage.example.com" }
2348
+ );
2349
+ expect(response.isError).toBeFalsy();
2350
+ cfgSpy.mockRestore();
2351
+ });
2352
+
2353
+ test("update_issue with no fields including no attachments is rejected", async () => {
2354
+ const cfgSpy = configWithKey();
2355
+ const r = await handleToolCall(createRequest("update_issue", { issue_id: "i1" }));
2356
+ expect(r.isError).toBe(true);
2357
+ expect(getResponseText(r)).toContain("At least one field to update is required");
2358
+ // The error message now mentions attachments as a valid update field.
2359
+ expect(getResponseText(r)).toContain("attachments");
2360
+ cfgSpy.mockRestore();
2361
+ });
2362
+
2363
+ test("create_issue with attachments appends link to provided description", async () => {
2364
+ const cfgSpy = configWithKey();
2365
+ const fakePath = mockTinyFile("design.png");
2366
+
2367
+ const createBodies: Array<{ description?: string }> = [];
2368
+ globalThis.fetch = mock(async (url: string | URL, init?: RequestInit) => {
2369
+ const u = String(url);
2370
+ if (u.endsWith("/upload")) {
2371
+ return new Response(
2372
+ JSON.stringify({
2373
+ success: true, url: "/files/9/dz.png",
2374
+ metadata: { originalName: "design.png", size: 4, mimeType: "image/png", uploadedAt: "", duration: 0 },
2375
+ requestId: "r",
2376
+ }),
2377
+ { status: 200, headers: { "Content-Type": "application/json" } }
2378
+ );
2379
+ }
2380
+ if (u.endsWith("/rpc/issue_create")) {
2381
+ createBodies.push(JSON.parse(String(init?.body)));
2382
+ return new Response(
2383
+ JSON.stringify({ id: "i1", title: "T", description: "ignored", created_at: "", status: 0 }),
2384
+ { status: 200, headers: { "Content-Type": "application/json" } }
2385
+ );
2386
+ }
2387
+ return new Response("nope", { status: 404 });
2388
+ }) as unknown as typeof fetch;
2389
+
2390
+ const response = await handleToolCall(
2391
+ createRequest("create_issue", {
2392
+ title: "Dx",
2393
+ description: "see design",
2394
+ attachments: [fakePath],
2395
+ }),
2396
+ { apiBaseUrl: "https://api.example.com", storageBaseUrl: "https://storage.example.com" }
2397
+ );
2398
+ expect(response.isError).toBeFalsy();
2399
+ expect(createBodies[0].description).toBe(
2400
+ "see design\n\n![design.png](https://storage.example.com/files/9/dz.png)"
2401
+ );
2402
+ cfgSpy.mockRestore();
2403
+ });
2404
+
2405
+ test("update_issue_comment with attachments appends to content", async () => {
2406
+ const cfgSpy = configWithKey();
2407
+ const fakePath = mockTinyFile("after.png");
2408
+
2409
+ const bodies: Array<{ p_content?: string }> = [];
2410
+ globalThis.fetch = mock(async (url: string | URL, init?: RequestInit) => {
2411
+ const u = String(url);
2412
+ if (u.endsWith("/upload")) {
2413
+ return new Response(
2414
+ JSON.stringify({
2415
+ success: true, url: "/files/9/aft.png",
2416
+ metadata: { originalName: "after.png", size: 4, mimeType: "image/png", uploadedAt: "", duration: 0 },
2417
+ requestId: "r",
2418
+ }),
2419
+ { status: 200, headers: { "Content-Type": "application/json" } }
2420
+ );
2421
+ }
2422
+ if (u.endsWith("/rpc/issue_comment_update")) {
2423
+ bodies.push(JSON.parse(String(init?.body)));
2424
+ return new Response(JSON.stringify({ id: "c1", issue_id: "i1", content: "ignored", updated_at: "" }), {
2425
+ status: 200, headers: { "Content-Type": "application/json" },
2426
+ });
2427
+ }
2428
+ return new Response("nope", { status: 404 });
2429
+ }) as unknown as typeof fetch;
2430
+
2431
+ const response = await handleToolCall(
2432
+ createRequest("update_issue_comment", { comment_id: "c1", content: "now updated", attachments: [fakePath] }),
2433
+ { apiBaseUrl: "https://api.example.com", storageBaseUrl: "https://storage.example.com" }
2434
+ );
2435
+ expect(response.isError).toBeFalsy();
2436
+ expect(bodies[0].p_content).toBe("now updated\n\n![after.png](https://storage.example.com/files/9/aft.png)");
2437
+ cfgSpy.mockRestore();
2438
+ });
2439
+
2440
+ test("update_issue_comment with attachments-only (no content) sends just the markdown link", async () => {
2441
+ const cfgSpy = configWithKey();
2442
+ const fakePath = mockTinyFile("only.png");
2443
+
2444
+ const bodies: Array<{ p_content?: string }> = [];
2445
+ globalThis.fetch = mock(async (url: string | URL, init?: RequestInit) => {
2446
+ const u = String(url);
2447
+ if (u.endsWith("/upload")) {
2448
+ return new Response(
2449
+ JSON.stringify({
2450
+ success: true, url: "/files/9/only.png",
2451
+ metadata: { originalName: "only.png", size: 4, mimeType: "image/png", uploadedAt: "", duration: 0 },
2452
+ requestId: "r",
2453
+ }),
2454
+ { status: 200, headers: { "Content-Type": "application/json" } }
2455
+ );
2456
+ }
2457
+ if (u.endsWith("/rpc/issue_comment_update")) {
2458
+ bodies.push(JSON.parse(String(init?.body)));
2459
+ return new Response(JSON.stringify({ id: "c1", issue_id: "i1", content: "ignored", updated_at: "" }), {
2460
+ status: 200, headers: { "Content-Type": "application/json" },
2461
+ });
2462
+ }
2463
+ return new Response("nope", { status: 404 });
2464
+ }) as unknown as typeof fetch;
2465
+
2466
+ const response = await handleToolCall(
2467
+ createRequest("update_issue_comment", { comment_id: "c1", attachments: [fakePath] }),
2468
+ { apiBaseUrl: "https://api.example.com", storageBaseUrl: "https://storage.example.com" }
2469
+ );
2470
+ expect(response.isError).toBeFalsy();
2471
+ expect(bodies).toHaveLength(1);
2472
+ expect(bodies[0].p_content).toBe("![only.png](https://storage.example.com/files/9/only.png)");
2473
+ cfgSpy.mockRestore();
2474
+ });
2475
+
2476
+ test("update_issue_comment without content and without attachments is rejected", async () => {
2477
+ const cfgSpy = configWithKey();
2478
+ const fetchSpy = mock(() => {
2479
+ throw new Error("should not be called");
2480
+ });
2481
+ globalThis.fetch = fetchSpy as unknown as typeof fetch;
2482
+
2483
+ const response = await handleToolCall(createRequest("update_issue_comment", { comment_id: "c1" }));
2484
+ expect(response.isError).toBe(true);
2485
+ expect(getResponseText(response)).toBe("content or attachments is required");
2486
+ expect(fetchSpy).not.toHaveBeenCalled();
2487
+ cfgSpy.mockRestore();
2488
+ });
2489
+
2490
+ test("create_issue with attachments and no description sets description to just the link", async () => {
2491
+ const cfgSpy = configWithKey();
2492
+ const fakePath = mockTinyFile("plan.png");
2493
+
2494
+ const createBodies: Array<{ description?: string }> = [];
2495
+ globalThis.fetch = mock(async (url: string | URL, init?: RequestInit) => {
2496
+ const u = String(url);
2497
+ if (u.endsWith("/upload")) {
2498
+ return new Response(
2499
+ JSON.stringify({
2500
+ success: true, url: "/files/9/plan.png",
2501
+ metadata: { originalName: "plan.png", size: 4, mimeType: "image/png", uploadedAt: "", duration: 0 },
2502
+ requestId: "r",
2503
+ }),
2504
+ { status: 200, headers: { "Content-Type": "application/json" } }
2505
+ );
2506
+ }
2507
+ if (u.endsWith("/rpc/issue_create")) {
2508
+ createBodies.push(JSON.parse(String(init?.body)));
2509
+ return new Response(
2510
+ JSON.stringify({ id: "i1", title: "T", description: "ignored", created_at: "", status: 0 }),
2511
+ { status: 200, headers: { "Content-Type": "application/json" } }
2512
+ );
2513
+ }
2514
+ return new Response("nope", { status: 404 });
2515
+ }) as unknown as typeof fetch;
2516
+
2517
+ const response = await handleToolCall(
2518
+ createRequest("create_issue", { title: "Dx", attachments: [fakePath] }),
2519
+ { apiBaseUrl: "https://api.example.com", storageBaseUrl: "https://storage.example.com" }
2520
+ );
2521
+ expect(response.isError).toBeFalsy();
2522
+ expect(createBodies[0].description).toBe("![plan.png](https://storage.example.com/files/9/plan.png)");
2523
+ cfgSpy.mockRestore();
2524
+ });
2525
+ });
1584
2526
  });