postgresai 0.14.0-dev.70 → 0.14.0-dev.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/postgres-ai.ts +403 -95
- package/dist/bin/postgres-ai.js +1126 -158
- package/lib/init.ts +76 -19
- package/lib/issues.ts +453 -7
- package/lib/mcp-server.ts +180 -3
- package/lib/metrics-embedded.ts +1 -1
- package/lib/supabase.ts +52 -0
- package/package.json +1 -1
- package/test/config-consistency.test.ts +36 -0
- package/test/init.integration.test.ts +78 -70
- package/test/init.test.ts +155 -0
- package/test/issues.cli.test.ts +224 -0
- package/test/mcp-server.test.ts +551 -12
package/test/mcp-server.test.ts
CHANGED
|
@@ -321,7 +321,7 @@ describe("MCP Server", () => {
|
|
|
321
321
|
);
|
|
322
322
|
});
|
|
323
323
|
|
|
324
|
-
const response = await handleToolCall(createRequest("view_issue", { issue_id: "
|
|
324
|
+
const response = await handleToolCall(createRequest("view_issue", { issue_id: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" }));
|
|
325
325
|
|
|
326
326
|
expect(response.isError).toBeUndefined();
|
|
327
327
|
const parsed = JSON.parse(getResponseText(response));
|
|
@@ -360,7 +360,7 @@ describe("MCP Server", () => {
|
|
|
360
360
|
});
|
|
361
361
|
|
|
362
362
|
const response = await handleToolCall(
|
|
363
|
-
createRequest("post_issue_comment", { issue_id: "
|
|
363
|
+
createRequest("post_issue_comment", { issue_id: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", content: "" })
|
|
364
364
|
);
|
|
365
365
|
|
|
366
366
|
expect(response.isError).toBe(true);
|
|
@@ -390,7 +390,7 @@ describe("MCP Server", () => {
|
|
|
390
390
|
|
|
391
391
|
await handleToolCall(
|
|
392
392
|
createRequest("post_issue_comment", {
|
|
393
|
-
issue_id: "
|
|
393
|
+
issue_id: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
|
|
394
394
|
content: "line1\\nline2\\ttab",
|
|
395
395
|
})
|
|
396
396
|
);
|
|
@@ -423,7 +423,7 @@ describe("MCP Server", () => {
|
|
|
423
423
|
|
|
424
424
|
const response = await handleToolCall(
|
|
425
425
|
createRequest("post_issue_comment", {
|
|
426
|
-
issue_id: "
|
|
426
|
+
issue_id: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
|
|
427
427
|
content: "Reply content",
|
|
428
428
|
parent_comment_id: "parent-1",
|
|
429
429
|
})
|
|
@@ -618,7 +618,7 @@ describe("MCP Server", () => {
|
|
|
618
618
|
defaultProject: null,
|
|
619
619
|
});
|
|
620
620
|
|
|
621
|
-
const response = await handleToolCall(createRequest("update_issue", { issue_id: "
|
|
621
|
+
const response = await handleToolCall(createRequest("update_issue", { issue_id: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" }));
|
|
622
622
|
|
|
623
623
|
expect(response.isError).toBe(true);
|
|
624
624
|
expect(getResponseText(response)).toContain("At least one field to update is required");
|
|
@@ -635,7 +635,7 @@ describe("MCP Server", () => {
|
|
|
635
635
|
});
|
|
636
636
|
|
|
637
637
|
const response = await handleToolCall(
|
|
638
|
-
createRequest("update_issue", { issue_id: "
|
|
638
|
+
createRequest("update_issue", { issue_id: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", status: 2 })
|
|
639
639
|
);
|
|
640
640
|
|
|
641
641
|
expect(response.isError).toBe(true);
|
|
@@ -653,7 +653,7 @@ describe("MCP Server", () => {
|
|
|
653
653
|
});
|
|
654
654
|
|
|
655
655
|
const response = await handleToolCall(
|
|
656
|
-
createRequest("update_issue", { issue_id: "
|
|
656
|
+
createRequest("update_issue", { issue_id: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", status: -1 })
|
|
657
657
|
);
|
|
658
658
|
|
|
659
659
|
expect(response.isError).toBe(true);
|
|
@@ -683,7 +683,7 @@ describe("MCP Server", () => {
|
|
|
683
683
|
|
|
684
684
|
await handleToolCall(
|
|
685
685
|
createRequest("update_issue", {
|
|
686
|
-
issue_id: "
|
|
686
|
+
issue_id: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
|
|
687
687
|
title: "Updated\\nTitle",
|
|
688
688
|
description: "Updated\\tDescription",
|
|
689
689
|
})
|
|
@@ -715,7 +715,7 @@ describe("MCP Server", () => {
|
|
|
715
715
|
);
|
|
716
716
|
|
|
717
717
|
const response = await handleToolCall(
|
|
718
|
-
createRequest("update_issue", { issue_id: "
|
|
718
|
+
createRequest("update_issue", { issue_id: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", title: "New Title" })
|
|
719
719
|
);
|
|
720
720
|
|
|
721
721
|
expect(response.isError).toBeUndefined();
|
|
@@ -743,7 +743,7 @@ describe("MCP Server", () => {
|
|
|
743
743
|
});
|
|
744
744
|
|
|
745
745
|
const response = await handleToolCall(
|
|
746
|
-
createRequest("update_issue", { issue_id: "
|
|
746
|
+
createRequest("update_issue", { issue_id: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", status: 1 })
|
|
747
747
|
);
|
|
748
748
|
|
|
749
749
|
expect(response.isError).toBeUndefined();
|
|
@@ -774,7 +774,7 @@ describe("MCP Server", () => {
|
|
|
774
774
|
});
|
|
775
775
|
|
|
776
776
|
const response = await handleToolCall(
|
|
777
|
-
createRequest("update_issue", { issue_id: "
|
|
777
|
+
createRequest("update_issue", { issue_id: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", labels: ["new-label"] })
|
|
778
778
|
);
|
|
779
779
|
|
|
780
780
|
expect(response.isError).toBeUndefined();
|
|
@@ -805,7 +805,7 @@ describe("MCP Server", () => {
|
|
|
805
805
|
});
|
|
806
806
|
|
|
807
807
|
const response = await handleToolCall(
|
|
808
|
-
createRequest("update_issue", { issue_id: "
|
|
808
|
+
createRequest("update_issue", { issue_id: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", status: 0 })
|
|
809
809
|
);
|
|
810
810
|
|
|
811
811
|
expect(response.isError).toBeUndefined();
|
|
@@ -919,6 +919,545 @@ describe("MCP Server", () => {
|
|
|
919
919
|
});
|
|
920
920
|
});
|
|
921
921
|
|
|
922
|
+
describe("view_action_item tool", () => {
|
|
923
|
+
test("returns error when no IDs provided", async () => {
|
|
924
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
925
|
+
apiKey: "test-key",
|
|
926
|
+
baseUrl: null,
|
|
927
|
+
orgId: null,
|
|
928
|
+
defaultProject: null,
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
const response = await handleToolCall(createRequest("view_action_item", {}));
|
|
932
|
+
|
|
933
|
+
expect(response.isError).toBe(true);
|
|
934
|
+
expect(getResponseText(response)).toBe("action_item_id or action_item_ids is required");
|
|
935
|
+
|
|
936
|
+
readConfigSpy.mockRestore();
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
test("returns error when action_item_id is empty", async () => {
|
|
940
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
941
|
+
apiKey: "test-key",
|
|
942
|
+
baseUrl: null,
|
|
943
|
+
orgId: null,
|
|
944
|
+
defaultProject: null,
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
const response = await handleToolCall(createRequest("view_action_item", { action_item_id: "" }));
|
|
948
|
+
|
|
949
|
+
expect(response.isError).toBe(true);
|
|
950
|
+
expect(getResponseText(response)).toBe("action_item_id or action_item_ids is required");
|
|
951
|
+
|
|
952
|
+
readConfigSpy.mockRestore();
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
test("returns error when action_item_ids is empty array", async () => {
|
|
956
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
957
|
+
apiKey: "test-key",
|
|
958
|
+
baseUrl: null,
|
|
959
|
+
orgId: null,
|
|
960
|
+
defaultProject: null,
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
const response = await handleToolCall(createRequest("view_action_item", { action_item_ids: [] }));
|
|
964
|
+
|
|
965
|
+
expect(response.isError).toBe(true);
|
|
966
|
+
expect(getResponseText(response)).toBe("action_item_id or action_item_ids is required");
|
|
967
|
+
|
|
968
|
+
readConfigSpy.mockRestore();
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
test("returns error when action_item_id is not a valid UUID", async () => {
|
|
972
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
973
|
+
apiKey: "test-key",
|
|
974
|
+
baseUrl: null,
|
|
975
|
+
orgId: null,
|
|
976
|
+
defaultProject: null,
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
const response = await handleToolCall(createRequest("view_action_item", { action_item_id: "invalid-id-format" }));
|
|
980
|
+
|
|
981
|
+
expect(response.isError).toBe(true);
|
|
982
|
+
expect(getResponseText(response)).toBe("actionItemId is required and must be a valid UUID");
|
|
983
|
+
|
|
984
|
+
readConfigSpy.mockRestore();
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
test("returns error when action item not found", async () => {
|
|
988
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
989
|
+
apiKey: "test-key",
|
|
990
|
+
baseUrl: null,
|
|
991
|
+
orgId: null,
|
|
992
|
+
defaultProject: null,
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
globalThis.fetch = mock(() =>
|
|
996
|
+
Promise.resolve(
|
|
997
|
+
new Response("[]", {
|
|
998
|
+
status: 200,
|
|
999
|
+
headers: { "Content-Type": "application/json" },
|
|
1000
|
+
})
|
|
1001
|
+
)
|
|
1002
|
+
);
|
|
1003
|
+
|
|
1004
|
+
const response = await handleToolCall(createRequest("view_action_item", { action_item_id: "00000000-0000-0000-0000-000000000000" }));
|
|
1005
|
+
|
|
1006
|
+
expect(response.isError).toBe(true);
|
|
1007
|
+
expect(getResponseText(response)).toBe("Action item(s) not found");
|
|
1008
|
+
|
|
1009
|
+
readConfigSpy.mockRestore();
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
test("successfully returns single action item details", async () => {
|
|
1013
|
+
const mockActionItem = {
|
|
1014
|
+
id: "11111111-1111-1111-1111-111111111111",
|
|
1015
|
+
issue_id: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
|
|
1016
|
+
title: "Fix index",
|
|
1017
|
+
description: "Drop unused index",
|
|
1018
|
+
severity: 3,
|
|
1019
|
+
is_done: false,
|
|
1020
|
+
status: "waiting_for_approval",
|
|
1021
|
+
sql_action: "DROP INDEX CONCURRENTLY idx_unused;",
|
|
1022
|
+
configs: [{ parameter: "work_mem", value: "256MB" }],
|
|
1023
|
+
};
|
|
1024
|
+
|
|
1025
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
1026
|
+
apiKey: "test-key",
|
|
1027
|
+
baseUrl: null,
|
|
1028
|
+
orgId: null,
|
|
1029
|
+
defaultProject: null,
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
globalThis.fetch = mock(() =>
|
|
1033
|
+
Promise.resolve(
|
|
1034
|
+
new Response(JSON.stringify([mockActionItem]), {
|
|
1035
|
+
status: 200,
|
|
1036
|
+
headers: { "Content-Type": "application/json" },
|
|
1037
|
+
})
|
|
1038
|
+
)
|
|
1039
|
+
);
|
|
1040
|
+
|
|
1041
|
+
const response = await handleToolCall(createRequest("view_action_item", { action_item_id: "11111111-1111-1111-1111-111111111111" }));
|
|
1042
|
+
|
|
1043
|
+
expect(response.isError).toBeUndefined();
|
|
1044
|
+
const parsed = JSON.parse(getResponseText(response));
|
|
1045
|
+
expect(Array.isArray(parsed)).toBe(true);
|
|
1046
|
+
expect(parsed[0].title).toBe("Fix index");
|
|
1047
|
+
expect(parsed[0].sql_action).toBe("DROP INDEX CONCURRENTLY idx_unused;");
|
|
1048
|
+
expect(parsed[0].configs).toEqual([{ parameter: "work_mem", value: "256MB" }]);
|
|
1049
|
+
|
|
1050
|
+
readConfigSpy.mockRestore();
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
test("successfully returns multiple action items", async () => {
|
|
1054
|
+
const mockActionItems = [
|
|
1055
|
+
{ id: "11111111-1111-1111-1111-111111111111", title: "Fix index", severity: 3 },
|
|
1056
|
+
{ id: "22222222-2222-2222-2222-222222222222", title: "Update config", severity: 2 },
|
|
1057
|
+
];
|
|
1058
|
+
|
|
1059
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
1060
|
+
apiKey: "test-key",
|
|
1061
|
+
baseUrl: null,
|
|
1062
|
+
orgId: null,
|
|
1063
|
+
defaultProject: null,
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
let capturedUrl: string | undefined;
|
|
1067
|
+
globalThis.fetch = mock((url: string) => {
|
|
1068
|
+
capturedUrl = url;
|
|
1069
|
+
return Promise.resolve(
|
|
1070
|
+
new Response(JSON.stringify(mockActionItems), {
|
|
1071
|
+
status: 200,
|
|
1072
|
+
headers: { "Content-Type": "application/json" },
|
|
1073
|
+
})
|
|
1074
|
+
);
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
const response = await handleToolCall(createRequest("view_action_item", { action_item_ids: ["11111111-1111-1111-1111-111111111111", "22222222-2222-2222-2222-222222222222"] }));
|
|
1078
|
+
|
|
1079
|
+
expect(response.isError).toBeUndefined();
|
|
1080
|
+
const parsed = JSON.parse(getResponseText(response));
|
|
1081
|
+
expect(parsed).toHaveLength(2);
|
|
1082
|
+
expect(parsed[0].title).toBe("Fix index");
|
|
1083
|
+
expect(parsed[1].title).toBe("Update config");
|
|
1084
|
+
// Verify the URL uses in.() syntax
|
|
1085
|
+
expect(capturedUrl).toContain("id=in.");
|
|
1086
|
+
|
|
1087
|
+
readConfigSpy.mockRestore();
|
|
1088
|
+
});
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
describe("list_action_items tool", () => {
|
|
1092
|
+
test("returns error when issue_id is empty", async () => {
|
|
1093
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
1094
|
+
apiKey: "test-key",
|
|
1095
|
+
baseUrl: null,
|
|
1096
|
+
orgId: null,
|
|
1097
|
+
defaultProject: null,
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
const response = await handleToolCall(createRequest("list_action_items", { issue_id: "" }));
|
|
1101
|
+
|
|
1102
|
+
expect(response.isError).toBe(true);
|
|
1103
|
+
expect(getResponseText(response)).toBe("issue_id is required");
|
|
1104
|
+
|
|
1105
|
+
readConfigSpy.mockRestore();
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
test("returns error when issue_id is whitespace only", async () => {
|
|
1109
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
1110
|
+
apiKey: "test-key",
|
|
1111
|
+
baseUrl: null,
|
|
1112
|
+
orgId: null,
|
|
1113
|
+
defaultProject: null,
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
const response = await handleToolCall(createRequest("list_action_items", { issue_id: " " }));
|
|
1117
|
+
|
|
1118
|
+
expect(response.isError).toBe(true);
|
|
1119
|
+
expect(getResponseText(response)).toBe("issue_id is required");
|
|
1120
|
+
|
|
1121
|
+
readConfigSpy.mockRestore();
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
test("successfully returns action items list as JSON", async () => {
|
|
1125
|
+
const mockActionItems = [
|
|
1126
|
+
{ id: "action-1", title: "First Action", severity: 1 },
|
|
1127
|
+
{ id: "action-2", title: "Second Action", severity: 2 },
|
|
1128
|
+
];
|
|
1129
|
+
|
|
1130
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
1131
|
+
apiKey: "test-key",
|
|
1132
|
+
baseUrl: null,
|
|
1133
|
+
orgId: null,
|
|
1134
|
+
defaultProject: null,
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
globalThis.fetch = mock(() =>
|
|
1138
|
+
Promise.resolve(
|
|
1139
|
+
new Response(JSON.stringify(mockActionItems), {
|
|
1140
|
+
status: 200,
|
|
1141
|
+
headers: { "Content-Type": "application/json" },
|
|
1142
|
+
})
|
|
1143
|
+
)
|
|
1144
|
+
);
|
|
1145
|
+
|
|
1146
|
+
const response = await handleToolCall(createRequest("list_action_items", { issue_id: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" }));
|
|
1147
|
+
|
|
1148
|
+
expect(response.isError).toBeUndefined();
|
|
1149
|
+
const parsed = JSON.parse(getResponseText(response));
|
|
1150
|
+
expect(parsed).toHaveLength(2);
|
|
1151
|
+
expect(parsed[0].title).toBe("First Action");
|
|
1152
|
+
|
|
1153
|
+
readConfigSpy.mockRestore();
|
|
1154
|
+
});
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
describe("create_action_item tool", () => {
|
|
1158
|
+
test("returns error when issue_id is empty", async () => {
|
|
1159
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
1160
|
+
apiKey: "test-key",
|
|
1161
|
+
baseUrl: null,
|
|
1162
|
+
orgId: null,
|
|
1163
|
+
defaultProject: null,
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
const response = await handleToolCall(
|
|
1167
|
+
createRequest("create_action_item", { issue_id: "", title: "Test" })
|
|
1168
|
+
);
|
|
1169
|
+
|
|
1170
|
+
expect(response.isError).toBe(true);
|
|
1171
|
+
expect(getResponseText(response)).toBe("issue_id is required");
|
|
1172
|
+
|
|
1173
|
+
readConfigSpy.mockRestore();
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
test("returns error when title is empty", async () => {
|
|
1177
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
1178
|
+
apiKey: "test-key",
|
|
1179
|
+
baseUrl: null,
|
|
1180
|
+
orgId: null,
|
|
1181
|
+
defaultProject: null,
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1184
|
+
const response = await handleToolCall(
|
|
1185
|
+
createRequest("create_action_item", { issue_id: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", title: "" })
|
|
1186
|
+
);
|
|
1187
|
+
|
|
1188
|
+
expect(response.isError).toBe(true);
|
|
1189
|
+
expect(getResponseText(response)).toBe("title is required");
|
|
1190
|
+
|
|
1191
|
+
readConfigSpy.mockRestore();
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
test("successfully creates action item with minimal params", async () => {
|
|
1195
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
1196
|
+
apiKey: "test-key",
|
|
1197
|
+
baseUrl: null,
|
|
1198
|
+
orgId: null,
|
|
1199
|
+
defaultProject: null,
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
let capturedBody: string | undefined;
|
|
1203
|
+
globalThis.fetch = mock((url: string, options?: RequestInit) => {
|
|
1204
|
+
capturedBody = options?.body as string;
|
|
1205
|
+
return Promise.resolve(
|
|
1206
|
+
new Response(JSON.stringify("new-action-item-id"), {
|
|
1207
|
+
status: 200,
|
|
1208
|
+
headers: { "Content-Type": "application/json" },
|
|
1209
|
+
})
|
|
1210
|
+
);
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
const response = await handleToolCall(
|
|
1214
|
+
createRequest("create_action_item", {
|
|
1215
|
+
issue_id: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
|
|
1216
|
+
title: "Fix the index",
|
|
1217
|
+
})
|
|
1218
|
+
);
|
|
1219
|
+
|
|
1220
|
+
expect(response.isError).toBeUndefined();
|
|
1221
|
+
expect(capturedBody).toBeDefined();
|
|
1222
|
+
const parsed = JSON.parse(capturedBody!);
|
|
1223
|
+
expect(parsed.issue_id).toBe("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
|
|
1224
|
+
expect(parsed.title).toBe("Fix the index");
|
|
1225
|
+
|
|
1226
|
+
readConfigSpy.mockRestore();
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
test("successfully creates action item with all params", async () => {
|
|
1230
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
1231
|
+
apiKey: "test-key",
|
|
1232
|
+
baseUrl: null,
|
|
1233
|
+
orgId: null,
|
|
1234
|
+
defaultProject: null,
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
let capturedBody: string | undefined;
|
|
1238
|
+
globalThis.fetch = mock((url: string, options?: RequestInit) => {
|
|
1239
|
+
capturedBody = options?.body as string;
|
|
1240
|
+
return Promise.resolve(
|
|
1241
|
+
new Response(JSON.stringify("new-action-item-id"), {
|
|
1242
|
+
status: 200,
|
|
1243
|
+
headers: { "Content-Type": "application/json" },
|
|
1244
|
+
})
|
|
1245
|
+
);
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
const response = await handleToolCall(
|
|
1249
|
+
createRequest("create_action_item", {
|
|
1250
|
+
issue_id: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
|
|
1251
|
+
title: "Fix the index",
|
|
1252
|
+
description: "Drop the unused index to improve performance",
|
|
1253
|
+
sql_action: "DROP INDEX CONCURRENTLY idx_unused;",
|
|
1254
|
+
configs: [{ parameter: "work_mem", value: "256MB" }],
|
|
1255
|
+
})
|
|
1256
|
+
);
|
|
1257
|
+
|
|
1258
|
+
expect(response.isError).toBeUndefined();
|
|
1259
|
+
expect(capturedBody).toBeDefined();
|
|
1260
|
+
const parsed = JSON.parse(capturedBody!);
|
|
1261
|
+
expect(parsed.issue_id).toBe("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
|
|
1262
|
+
expect(parsed.title).toBe("Fix the index");
|
|
1263
|
+
expect(parsed.description).toBe("Drop the unused index to improve performance");
|
|
1264
|
+
expect(parsed.sql_action).toBe("DROP INDEX CONCURRENTLY idx_unused;");
|
|
1265
|
+
expect(parsed.configs).toEqual([{ parameter: "work_mem", value: "256MB" }]);
|
|
1266
|
+
|
|
1267
|
+
readConfigSpy.mockRestore();
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
test("interprets escape sequences in title and description", async () => {
|
|
1271
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
1272
|
+
apiKey: "test-key",
|
|
1273
|
+
baseUrl: null,
|
|
1274
|
+
orgId: null,
|
|
1275
|
+
defaultProject: null,
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
let capturedBody: string | undefined;
|
|
1279
|
+
globalThis.fetch = mock((url: string, options?: RequestInit) => {
|
|
1280
|
+
capturedBody = options?.body as string;
|
|
1281
|
+
return Promise.resolve(
|
|
1282
|
+
new Response(JSON.stringify("new-action-item-id"), {
|
|
1283
|
+
status: 200,
|
|
1284
|
+
headers: { "Content-Type": "application/json" },
|
|
1285
|
+
})
|
|
1286
|
+
);
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
await handleToolCall(
|
|
1290
|
+
createRequest("create_action_item", {
|
|
1291
|
+
issue_id: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
|
|
1292
|
+
title: "Title\\nwith newline",
|
|
1293
|
+
description: "Desc\\twith tab",
|
|
1294
|
+
})
|
|
1295
|
+
);
|
|
1296
|
+
|
|
1297
|
+
expect(capturedBody).toBeDefined();
|
|
1298
|
+
const parsed = JSON.parse(capturedBody!);
|
|
1299
|
+
expect(parsed.title).toBe("Title\nwith newline");
|
|
1300
|
+
expect(parsed.description).toBe("Desc\twith tab");
|
|
1301
|
+
|
|
1302
|
+
readConfigSpy.mockRestore();
|
|
1303
|
+
});
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
describe("update_action_item tool", () => {
|
|
1307
|
+
test("returns error when action_item_id is empty", async () => {
|
|
1308
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
1309
|
+
apiKey: "test-key",
|
|
1310
|
+
baseUrl: null,
|
|
1311
|
+
orgId: null,
|
|
1312
|
+
defaultProject: null,
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
const response = await handleToolCall(
|
|
1316
|
+
createRequest("update_action_item", { action_item_id: "", title: "New Title" })
|
|
1317
|
+
);
|
|
1318
|
+
|
|
1319
|
+
expect(response.isError).toBe(true);
|
|
1320
|
+
expect(getResponseText(response)).toBe("action_item_id is required");
|
|
1321
|
+
|
|
1322
|
+
readConfigSpy.mockRestore();
|
|
1323
|
+
});
|
|
1324
|
+
|
|
1325
|
+
test("returns error when no update fields provided", async () => {
|
|
1326
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
1327
|
+
apiKey: "test-key",
|
|
1328
|
+
baseUrl: null,
|
|
1329
|
+
orgId: null,
|
|
1330
|
+
defaultProject: null,
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
const response = await handleToolCall(
|
|
1334
|
+
createRequest("update_action_item", { action_item_id: "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" })
|
|
1335
|
+
);
|
|
1336
|
+
|
|
1337
|
+
expect(response.isError).toBe(true);
|
|
1338
|
+
expect(getResponseText(response)).toContain("At least one field to update is required");
|
|
1339
|
+
|
|
1340
|
+
readConfigSpy.mockRestore();
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
test("returns error when status is invalid", async () => {
|
|
1344
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
1345
|
+
apiKey: "test-key",
|
|
1346
|
+
baseUrl: null,
|
|
1347
|
+
orgId: null,
|
|
1348
|
+
defaultProject: null,
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
const response = await handleToolCall(
|
|
1352
|
+
createRequest("update_action_item", { action_item_id: "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", status: "invalid_status" })
|
|
1353
|
+
);
|
|
1354
|
+
|
|
1355
|
+
expect(response.isError).toBe(true);
|
|
1356
|
+
expect(getResponseText(response)).toContain("status must be");
|
|
1357
|
+
|
|
1358
|
+
readConfigSpy.mockRestore();
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1361
|
+
test("successfully updates with only title", async () => {
|
|
1362
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
1363
|
+
apiKey: "test-key",
|
|
1364
|
+
baseUrl: null,
|
|
1365
|
+
orgId: null,
|
|
1366
|
+
defaultProject: null,
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
let capturedBody: string | undefined;
|
|
1370
|
+
globalThis.fetch = mock((url: string, options?: RequestInit) => {
|
|
1371
|
+
capturedBody = options?.body as string;
|
|
1372
|
+
return Promise.resolve(
|
|
1373
|
+
new Response("", {
|
|
1374
|
+
status: 200,
|
|
1375
|
+
headers: { "Content-Type": "application/json" },
|
|
1376
|
+
})
|
|
1377
|
+
);
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
const response = await handleToolCall(
|
|
1381
|
+
createRequest("update_action_item", { action_item_id: "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", title: "New Title" })
|
|
1382
|
+
);
|
|
1383
|
+
|
|
1384
|
+
expect(response.isError).toBeUndefined();
|
|
1385
|
+
expect(capturedBody).toBeDefined();
|
|
1386
|
+
const parsed = JSON.parse(capturedBody!);
|
|
1387
|
+
expect(parsed.action_item_id).toBe("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb");
|
|
1388
|
+
expect(parsed.title).toBe("New Title");
|
|
1389
|
+
|
|
1390
|
+
readConfigSpy.mockRestore();
|
|
1391
|
+
});
|
|
1392
|
+
|
|
1393
|
+
test("successfully updates is_done", async () => {
|
|
1394
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
1395
|
+
apiKey: "test-key",
|
|
1396
|
+
baseUrl: null,
|
|
1397
|
+
orgId: null,
|
|
1398
|
+
defaultProject: null,
|
|
1399
|
+
});
|
|
1400
|
+
|
|
1401
|
+
let capturedBody: string | undefined;
|
|
1402
|
+
globalThis.fetch = mock((url: string, options?: RequestInit) => {
|
|
1403
|
+
capturedBody = options?.body as string;
|
|
1404
|
+
return Promise.resolve(
|
|
1405
|
+
new Response("", {
|
|
1406
|
+
status: 200,
|
|
1407
|
+
headers: { "Content-Type": "application/json" },
|
|
1408
|
+
})
|
|
1409
|
+
);
|
|
1410
|
+
});
|
|
1411
|
+
|
|
1412
|
+
const response = await handleToolCall(
|
|
1413
|
+
createRequest("update_action_item", { action_item_id: "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", is_done: true })
|
|
1414
|
+
);
|
|
1415
|
+
|
|
1416
|
+
expect(response.isError).toBeUndefined();
|
|
1417
|
+
expect(capturedBody).toBeDefined();
|
|
1418
|
+
const parsed = JSON.parse(capturedBody!);
|
|
1419
|
+
expect(parsed.is_done).toBe(true);
|
|
1420
|
+
|
|
1421
|
+
readConfigSpy.mockRestore();
|
|
1422
|
+
});
|
|
1423
|
+
|
|
1424
|
+
test("successfully updates status with status_reason", async () => {
|
|
1425
|
+
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|
|
1426
|
+
apiKey: "test-key",
|
|
1427
|
+
baseUrl: null,
|
|
1428
|
+
orgId: null,
|
|
1429
|
+
defaultProject: null,
|
|
1430
|
+
});
|
|
1431
|
+
|
|
1432
|
+
let capturedBody: string | undefined;
|
|
1433
|
+
globalThis.fetch = mock((url: string, options?: RequestInit) => {
|
|
1434
|
+
capturedBody = options?.body as string;
|
|
1435
|
+
return Promise.resolve(
|
|
1436
|
+
new Response("", {
|
|
1437
|
+
status: 200,
|
|
1438
|
+
headers: { "Content-Type": "application/json" },
|
|
1439
|
+
})
|
|
1440
|
+
);
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1443
|
+
const response = await handleToolCall(
|
|
1444
|
+
createRequest("update_action_item", {
|
|
1445
|
+
action_item_id: "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
|
|
1446
|
+
status: "approved",
|
|
1447
|
+
status_reason: "Looks good to me",
|
|
1448
|
+
})
|
|
1449
|
+
);
|
|
1450
|
+
|
|
1451
|
+
expect(response.isError).toBeUndefined();
|
|
1452
|
+
expect(capturedBody).toBeDefined();
|
|
1453
|
+
const parsed = JSON.parse(capturedBody!);
|
|
1454
|
+
expect(parsed.status).toBe("approved");
|
|
1455
|
+
expect(parsed.status_reason).toBe("Looks good to me");
|
|
1456
|
+
|
|
1457
|
+
readConfigSpy.mockRestore();
|
|
1458
|
+
});
|
|
1459
|
+
});
|
|
1460
|
+
|
|
922
1461
|
describe("unknown tool handling", () => {
|
|
923
1462
|
test("returns error for unknown tool name", async () => {
|
|
924
1463
|
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
|