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.
- package/README.md +82 -9
- package/bin/postgres-ai.ts +813 -233
- package/bun.lock +4 -4
- package/dist/bin/postgres-ai.js +6193 -1059
- package/instances.demo.yml +14 -0
- package/lib/checkup-api.ts +25 -6
- package/lib/checkup-dictionary.ts +0 -11
- package/lib/checkup.ts +255 -24
- package/lib/config.ts +3 -0
- package/lib/init.ts +197 -5
- package/lib/instances.ts +245 -0
- package/lib/issues.ts +72 -72
- package/lib/mcp-server.ts +229 -18
- package/lib/metrics-loader.ts +6 -4
- package/lib/reports.ts +373 -0
- package/lib/storage.ts +367 -0
- package/lib/supabase.ts +8 -1
- package/lib/util.ts +7 -1
- package/package.json +4 -4
- package/scripts/embed-checkup-dictionary.ts +9 -0
- package/scripts/embed-metrics.ts +2 -0
- package/test/PERMISSION_CHECK_TEST_SUMMARY.md +139 -0
- package/test/checkup.test.ts +1316 -2
- package/test/compose-cmd.test.ts +120 -0
- package/test/config-consistency.test.ts +321 -5
- package/test/init.integration.test.ts +27 -28
- package/test/init.test.ts +534 -6
- package/test/issues.cli.test.ts +625 -1
- package/test/mcp-server.test.ts +944 -2
- package/test/monitoring.test.ts +355 -0
- package/test/permission-check-sql.test.ts +116 -0
- package/test/reports.cli.test.ts +793 -0
- package/test/reports.test.ts +977 -0
- package/test/schema-validation.test.ts +81 -0
- package/test/storage.test.ts +935 -0
- package/test/test-utils.ts +51 -2
- package/test/upgrade.test.ts +422 -0
package/test/mcp-server.test.ts
CHANGED
|
@@ -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
|
-
|
|
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("");
|
|
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");
|
|
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("");
|
|
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"
|
|
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"
|
|
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");
|
|
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("");
|
|
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("");
|
|
2523
|
+
cfgSpy.mockRestore();
|
|
2524
|
+
});
|
|
2525
|
+
});
|
|
1584
2526
|
});
|