@testchimp/cli 0.1.84 → 0.1.85

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.
@@ -1,6 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
- import { afterEach, describe, it } from "node:test";
3
- import { getBackendUrl, postMcp } from "./client.js";
2
+ import { afterEach, beforeEach, describe, it } from "node:test";
3
+ import { getBackendUrl, isValidBotId, postIngress, postMcp, resetClientWarningsForTests, runWithRequestAuth, } from "./client.js";
4
4
  const originalBackendUrl = process.env.TESTCHIMP_BACKEND_URL;
5
5
  const originalApiKey = process.env.TESTCHIMP_API_KEY;
6
6
  const originalFetch = globalThis.fetch;
@@ -52,3 +52,131 @@ describe("postMcp", () => {
52
52
  assert.equal(requestInit?.redirect, "manual");
53
53
  });
54
54
  });
55
+ describe("request headers", () => {
56
+ const originalBotId = process.env.TESTCHIMP_BOT_ID;
57
+ const originalOauthToken = process.env.TESTCHIMP_OAUTH_TOKEN;
58
+ const originalIngressUrl = process.env.TESTCHIMP_INGRESS_URL;
59
+ const originalConsoleError = console.error;
60
+ beforeEach(() => {
61
+ process.env.TESTCHIMP_BACKEND_URL = "https://featureservice.example.com";
62
+ delete process.env.TESTCHIMP_API_KEY;
63
+ delete process.env.TESTCHIMP_BOT_ID;
64
+ delete process.env.TESTCHIMP_OAUTH_TOKEN;
65
+ delete process.env.TESTCHIMP_INGRESS_URL;
66
+ resetClientWarningsForTests();
67
+ });
68
+ afterEach(() => {
69
+ for (const [key, value] of [
70
+ ["TESTCHIMP_BOT_ID", originalBotId],
71
+ ["TESTCHIMP_OAUTH_TOKEN", originalOauthToken],
72
+ ["TESTCHIMP_INGRESS_URL", originalIngressUrl],
73
+ ]) {
74
+ if (value == null)
75
+ delete process.env[key];
76
+ else
77
+ process.env[key] = value;
78
+ }
79
+ console.error = originalConsoleError;
80
+ });
81
+ function captureFetch() {
82
+ const capture = { calls: [] };
83
+ globalThis.fetch = (async (input, init) => {
84
+ capture.calls.push({ url: String(input), init });
85
+ return new Response("{}", { status: 200 });
86
+ });
87
+ return capture;
88
+ }
89
+ it("sends exactly the legacy header set with an API key and no bot env", async () => {
90
+ process.env.TESTCHIMP_API_KEY = "test-key";
91
+ const capture = captureFetch();
92
+ await postMcp("/api/mcp/example", { a: 1 });
93
+ assert.equal(capture.calls[0].url, "https://featureservice.example.com/api/mcp/example");
94
+ assert.deepEqual(capture.calls[0].init?.headers, {
95
+ "Content-Type": "application/json",
96
+ "TestChimp-Api-Key": "test-key",
97
+ });
98
+ assert.equal(capture.calls[0].init?.body, JSON.stringify({ a: 1 }));
99
+ });
100
+ it("keeps the missing API key error when no credentials are set", async () => {
101
+ captureFetch();
102
+ await assert.rejects(postMcp("/api/mcp/example", {}), /TESTCHIMP_API_KEY is required/);
103
+ });
104
+ it("adds a valid bot-id header", async () => {
105
+ process.env.TESTCHIMP_API_KEY = "test-key";
106
+ process.env.TESTCHIMP_BOT_ID = "grok-bot_01";
107
+ const capture = captureFetch();
108
+ await postMcp("/api/mcp/example", {});
109
+ assert.deepEqual(capture.calls[0].init?.headers, {
110
+ "Content-Type": "application/json",
111
+ "TestChimp-Api-Key": "test-key",
112
+ "bot-id": "grok-bot_01",
113
+ });
114
+ });
115
+ it("drops an invalid bot id and warns once", async () => {
116
+ process.env.TESTCHIMP_API_KEY = "test-key";
117
+ process.env.TESTCHIMP_BOT_ID = "bad bot id";
118
+ const warnings = [];
119
+ console.error = (msg) => warnings.push(String(msg));
120
+ const capture = captureFetch();
121
+ await postMcp("/api/mcp/example", {});
122
+ await postMcp("/api/mcp/example", {});
123
+ assert.deepEqual(capture.calls[0].init?.headers, {
124
+ "Content-Type": "application/json",
125
+ "TestChimp-Api-Key": "test-key",
126
+ });
127
+ assert.equal(warnings.length, 1);
128
+ assert.match(warnings[0], /TESTCHIMP_BOT_ID/);
129
+ assert.equal(isValidBotId("x".repeat(64)), true);
130
+ assert.equal(isValidBotId("x".repeat(65)), false);
131
+ assert.equal(isValidBotId("bot\u0007"), false);
132
+ });
133
+ it("uses the OAuth token without requiring an API key", async () => {
134
+ process.env.TESTCHIMP_OAUTH_TOKEN = "jwt-token";
135
+ const capture = captureFetch();
136
+ await postMcp("/api/mcp/example", {});
137
+ assert.deepEqual(capture.calls[0].init?.headers, {
138
+ "Content-Type": "application/json",
139
+ Authorization: "Bearer jwt-token",
140
+ });
141
+ });
142
+ it("sends both the API key and the bearer when both are set", async () => {
143
+ process.env.TESTCHIMP_API_KEY = "test-key";
144
+ process.env.TESTCHIMP_OAUTH_TOKEN = "jwt-token";
145
+ process.env.TESTCHIMP_BOT_ID = "bot-1";
146
+ const capture = captureFetch();
147
+ await postMcp("/api/mcp/example", {});
148
+ assert.deepEqual(capture.calls[0].init?.headers, {
149
+ "Content-Type": "application/json",
150
+ "TestChimp-Api-Key": "test-key",
151
+ Authorization: "Bearer jwt-token",
152
+ "bot-id": "bot-1",
153
+ });
154
+ });
155
+ it("isolated request auth ignores env credentials", async () => {
156
+ process.env.TESTCHIMP_API_KEY = "server-key";
157
+ process.env.TESTCHIMP_BOT_ID = "server-bot";
158
+ const capture = captureFetch();
159
+ await runWithRequestAuth({ bearerToken: "caller-token", isolated: true }, () => postMcp("/api/mcp/example", {}));
160
+ assert.deepEqual(capture.calls[0].init?.headers, {
161
+ "Content-Type": "application/json",
162
+ Authorization: "Bearer caller-token",
163
+ });
164
+ });
165
+ it("posts to the ingress base or a trusted absolute URL", async () => {
166
+ process.env.TESTCHIMP_API_KEY = "test-key";
167
+ const capture = captureFetch();
168
+ await postIngress("/bot/events/ack", { eventIds: ["e1"] });
169
+ assert.equal(capture.calls[0].url, "https://ingress.testchimp.io/bot/events/ack");
170
+ assert.equal(capture.calls[0].init?.redirect, "manual");
171
+ await postIngress("https://ingress-staging.testchimp.io/bot/events/ack", {});
172
+ assert.equal(capture.calls[1].url, "https://ingress-staging.testchimp.io/bot/events/ack");
173
+ await postIngress("http://localhost:4302/bot/events/ack", {});
174
+ assert.equal(capture.calls[2].url, "http://localhost:4302/bot/events/ack");
175
+ await assert.rejects(postIngress("http://ingress.testchimp.io/bot/events/ack", {}), /must use https/);
176
+ await assert.rejects(postIngress("https://evil.example.com/bot/events/ack", {}), /untrusted ingress host/);
177
+ assert.equal(capture.calls.length, 3);
178
+ process.env.TESTCHIMP_INGRESS_URL = "https://ingress.customer.example/";
179
+ await postIngress("https://ingress.customer.example/bot/events/ack", {});
180
+ assert.equal(capture.calls[3].url, "https://ingress.customer.example/bot/events/ack");
181
+ });
182
+ });
@@ -18,9 +18,9 @@ export declare const listCoverageInput: z.ZodObject<{
18
18
  includeNonCoveredTestScenarios: z.ZodOptional<z.ZodBoolean>;
19
19
  branchName: z.ZodOptional<z.ZodString>;
20
20
  platform: z.ZodOptional<z.ZodEnum<{
21
+ android: "android";
21
22
  web: "web";
22
23
  ios: "ios";
23
- android: "android";
24
24
  }>>;
25
25
  recordTypes: z.ZodOptional<z.ZodArray<z.ZodEnum<{
26
26
  manual: "manual";
@@ -47,9 +47,9 @@ export declare const listExecutionInput: z.ZodObject<{
47
47
  scenarioId: z.ZodOptional<z.ZodString>;
48
48
  testId: z.ZodOptional<z.ZodString>;
49
49
  platform: z.ZodOptional<z.ZodEnum<{
50
+ android: "android";
50
51
  web: "web";
51
52
  ios: "ios";
52
- android: "android";
53
53
  }>>;
54
54
  dimensionFilters: z.ZodOptional<z.ZodArray<z.ZodObject<{
55
55
  dimension: z.ZodString;
@@ -70,9 +70,9 @@ export declare const suiteExecutionStatsInput: z.ZodObject<{
70
70
  scenarioId: z.ZodOptional<z.ZodString>;
71
71
  testId: z.ZodOptional<z.ZodString>;
72
72
  platform: z.ZodOptional<z.ZodEnum<{
73
+ android: "android";
73
74
  web: "web";
74
75
  ios: "ios";
75
- android: "android";
76
76
  }>>;
77
77
  dimensionFilters: z.ZodOptional<z.ZodArray<z.ZodObject<{
78
78
  dimension: z.ZodString;
@@ -828,14 +828,14 @@ export declare const executionScopeSchema: z.ZodObject<{
828
828
  release: z.ZodOptional<z.ZodString>;
829
829
  branchName: z.ZodOptional<z.ZodString>;
830
830
  platform: z.ZodOptional<z.ZodPipe<z.ZodEnum<{
831
+ android: "android";
831
832
  web: "web";
832
833
  ios: "ios";
833
- android: "android";
834
834
  UNKNOWN_EXECUTION_PLATFORM: "UNKNOWN_EXECUTION_PLATFORM";
835
835
  WEB_EXECUTION_PLATFORM: "WEB_EXECUTION_PLATFORM";
836
836
  IOS_EXECUTION_PLATFORM: "IOS_EXECUTION_PLATFORM";
837
837
  ANDROID_EXECUTION_PLATFORM: "ANDROID_EXECUTION_PLATFORM";
838
- }>, z.ZodTransform<"UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM", "web" | "ios" | "android" | "UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM">>>;
838
+ }>, z.ZodTransform<"UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM", "android" | "web" | "ios" | "UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM">>>;
839
839
  automationEmitsOnly: z.ZodOptional<z.ZodBoolean>;
840
840
  metadataFilters: z.ZodOptional<z.ZodArray<z.ZodObject<{
841
841
  key: z.ZodString;
@@ -868,14 +868,14 @@ export declare const listTruecoverageEventsInput: z.ZodObject<{
868
868
  release: z.ZodOptional<z.ZodString>;
869
869
  branchName: z.ZodOptional<z.ZodString>;
870
870
  platform: z.ZodOptional<z.ZodPipe<z.ZodEnum<{
871
+ android: "android";
871
872
  web: "web";
872
873
  ios: "ios";
873
- android: "android";
874
874
  UNKNOWN_EXECUTION_PLATFORM: "UNKNOWN_EXECUTION_PLATFORM";
875
875
  WEB_EXECUTION_PLATFORM: "WEB_EXECUTION_PLATFORM";
876
876
  IOS_EXECUTION_PLATFORM: "IOS_EXECUTION_PLATFORM";
877
877
  ANDROID_EXECUTION_PLATFORM: "ANDROID_EXECUTION_PLATFORM";
878
- }>, z.ZodTransform<"UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM", "web" | "ios" | "android" | "UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM">>>;
878
+ }>, z.ZodTransform<"UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM", "android" | "web" | "ios" | "UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM">>>;
879
879
  automationEmitsOnly: z.ZodOptional<z.ZodBoolean>;
880
880
  metadataFilters: z.ZodOptional<z.ZodArray<z.ZodObject<{
881
881
  key: z.ZodString;
@@ -907,14 +907,14 @@ export declare const listTruecoverageEventsInput: z.ZodObject<{
907
907
  release: z.ZodOptional<z.ZodString>;
908
908
  branchName: z.ZodOptional<z.ZodString>;
909
909
  platform: z.ZodOptional<z.ZodPipe<z.ZodEnum<{
910
+ android: "android";
910
911
  web: "web";
911
912
  ios: "ios";
912
- android: "android";
913
913
  UNKNOWN_EXECUTION_PLATFORM: "UNKNOWN_EXECUTION_PLATFORM";
914
914
  WEB_EXECUTION_PLATFORM: "WEB_EXECUTION_PLATFORM";
915
915
  IOS_EXECUTION_PLATFORM: "IOS_EXECUTION_PLATFORM";
916
916
  ANDROID_EXECUTION_PLATFORM: "ANDROID_EXECUTION_PLATFORM";
917
- }>, z.ZodTransform<"UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM", "web" | "ios" | "android" | "UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM">>>;
917
+ }>, z.ZodTransform<"UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM", "android" | "web" | "ios" | "UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM">>>;
918
918
  automationEmitsOnly: z.ZodOptional<z.ZodBoolean>;
919
919
  metadataFilters: z.ZodOptional<z.ZodArray<z.ZodObject<{
920
920
  key: z.ZodString;
@@ -949,14 +949,14 @@ export declare const getTruecoverageEventDetailsInput: z.ZodObject<{
949
949
  release: z.ZodOptional<z.ZodString>;
950
950
  branchName: z.ZodOptional<z.ZodString>;
951
951
  platform: z.ZodOptional<z.ZodPipe<z.ZodEnum<{
952
+ android: "android";
952
953
  web: "web";
953
954
  ios: "ios";
954
- android: "android";
955
955
  UNKNOWN_EXECUTION_PLATFORM: "UNKNOWN_EXECUTION_PLATFORM";
956
956
  WEB_EXECUTION_PLATFORM: "WEB_EXECUTION_PLATFORM";
957
957
  IOS_EXECUTION_PLATFORM: "IOS_EXECUTION_PLATFORM";
958
958
  ANDROID_EXECUTION_PLATFORM: "ANDROID_EXECUTION_PLATFORM";
959
- }>, z.ZodTransform<"UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM", "web" | "ios" | "android" | "UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM">>>;
959
+ }>, z.ZodTransform<"UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM", "android" | "web" | "ios" | "UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM">>>;
960
960
  automationEmitsOnly: z.ZodOptional<z.ZodBoolean>;
961
961
  metadataFilters: z.ZodOptional<z.ZodArray<z.ZodObject<{
962
962
  key: z.ZodString;
@@ -988,14 +988,14 @@ export declare const getTruecoverageEventDetailsInput: z.ZodObject<{
988
988
  release: z.ZodOptional<z.ZodString>;
989
989
  branchName: z.ZodOptional<z.ZodString>;
990
990
  platform: z.ZodOptional<z.ZodPipe<z.ZodEnum<{
991
+ android: "android";
991
992
  web: "web";
992
993
  ios: "ios";
993
- android: "android";
994
994
  UNKNOWN_EXECUTION_PLATFORM: "UNKNOWN_EXECUTION_PLATFORM";
995
995
  WEB_EXECUTION_PLATFORM: "WEB_EXECUTION_PLATFORM";
996
996
  IOS_EXECUTION_PLATFORM: "IOS_EXECUTION_PLATFORM";
997
997
  ANDROID_EXECUTION_PLATFORM: "ANDROID_EXECUTION_PLATFORM";
998
- }>, z.ZodTransform<"UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM", "web" | "ios" | "android" | "UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM">>>;
998
+ }>, z.ZodTransform<"UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM", "android" | "web" | "ios" | "UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM">>>;
999
999
  automationEmitsOnly: z.ZodOptional<z.ZodBoolean>;
1000
1000
  metadataFilters: z.ZodOptional<z.ZodArray<z.ZodObject<{
1001
1001
  key: z.ZodString;
@@ -1030,14 +1030,14 @@ export declare const listTruecoverageChildEventTreeInput: z.ZodObject<{
1030
1030
  release: z.ZodOptional<z.ZodString>;
1031
1031
  branchName: z.ZodOptional<z.ZodString>;
1032
1032
  platform: z.ZodOptional<z.ZodPipe<z.ZodEnum<{
1033
+ android: "android";
1033
1034
  web: "web";
1034
1035
  ios: "ios";
1035
- android: "android";
1036
1036
  UNKNOWN_EXECUTION_PLATFORM: "UNKNOWN_EXECUTION_PLATFORM";
1037
1037
  WEB_EXECUTION_PLATFORM: "WEB_EXECUTION_PLATFORM";
1038
1038
  IOS_EXECUTION_PLATFORM: "IOS_EXECUTION_PLATFORM";
1039
1039
  ANDROID_EXECUTION_PLATFORM: "ANDROID_EXECUTION_PLATFORM";
1040
- }>, z.ZodTransform<"UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM", "web" | "ios" | "android" | "UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM">>>;
1040
+ }>, z.ZodTransform<"UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM", "android" | "web" | "ios" | "UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM">>>;
1041
1041
  automationEmitsOnly: z.ZodOptional<z.ZodBoolean>;
1042
1042
  metadataFilters: z.ZodOptional<z.ZodArray<z.ZodObject<{
1043
1043
  key: z.ZodString;
@@ -1069,14 +1069,14 @@ export declare const listTruecoverageChildEventTreeInput: z.ZodObject<{
1069
1069
  release: z.ZodOptional<z.ZodString>;
1070
1070
  branchName: z.ZodOptional<z.ZodString>;
1071
1071
  platform: z.ZodOptional<z.ZodPipe<z.ZodEnum<{
1072
+ android: "android";
1072
1073
  web: "web";
1073
1074
  ios: "ios";
1074
- android: "android";
1075
1075
  UNKNOWN_EXECUTION_PLATFORM: "UNKNOWN_EXECUTION_PLATFORM";
1076
1076
  WEB_EXECUTION_PLATFORM: "WEB_EXECUTION_PLATFORM";
1077
1077
  IOS_EXECUTION_PLATFORM: "IOS_EXECUTION_PLATFORM";
1078
1078
  ANDROID_EXECUTION_PLATFORM: "ANDROID_EXECUTION_PLATFORM";
1079
- }>, z.ZodTransform<"UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM", "web" | "ios" | "android" | "UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM">>>;
1079
+ }>, z.ZodTransform<"UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM", "android" | "web" | "ios" | "UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM">>>;
1080
1080
  automationEmitsOnly: z.ZodOptional<z.ZodBoolean>;
1081
1081
  metadataFilters: z.ZodOptional<z.ZodArray<z.ZodObject<{
1082
1082
  key: z.ZodString;
@@ -1112,14 +1112,14 @@ export declare const getTruecoverageEventTransitionInput: z.ZodObject<{
1112
1112
  release: z.ZodOptional<z.ZodString>;
1113
1113
  branchName: z.ZodOptional<z.ZodString>;
1114
1114
  platform: z.ZodOptional<z.ZodPipe<z.ZodEnum<{
1115
+ android: "android";
1115
1116
  web: "web";
1116
1117
  ios: "ios";
1117
- android: "android";
1118
1118
  UNKNOWN_EXECUTION_PLATFORM: "UNKNOWN_EXECUTION_PLATFORM";
1119
1119
  WEB_EXECUTION_PLATFORM: "WEB_EXECUTION_PLATFORM";
1120
1120
  IOS_EXECUTION_PLATFORM: "IOS_EXECUTION_PLATFORM";
1121
1121
  ANDROID_EXECUTION_PLATFORM: "ANDROID_EXECUTION_PLATFORM";
1122
- }>, z.ZodTransform<"UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM", "web" | "ios" | "android" | "UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM">>>;
1122
+ }>, z.ZodTransform<"UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM", "android" | "web" | "ios" | "UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM">>>;
1123
1123
  automationEmitsOnly: z.ZodOptional<z.ZodBoolean>;
1124
1124
  metadataFilters: z.ZodOptional<z.ZodArray<z.ZodObject<{
1125
1125
  key: z.ZodString;
@@ -1151,14 +1151,14 @@ export declare const getTruecoverageEventTransitionInput: z.ZodObject<{
1151
1151
  release: z.ZodOptional<z.ZodString>;
1152
1152
  branchName: z.ZodOptional<z.ZodString>;
1153
1153
  platform: z.ZodOptional<z.ZodPipe<z.ZodEnum<{
1154
+ android: "android";
1154
1155
  web: "web";
1155
1156
  ios: "ios";
1156
- android: "android";
1157
1157
  UNKNOWN_EXECUTION_PLATFORM: "UNKNOWN_EXECUTION_PLATFORM";
1158
1158
  WEB_EXECUTION_PLATFORM: "WEB_EXECUTION_PLATFORM";
1159
1159
  IOS_EXECUTION_PLATFORM: "IOS_EXECUTION_PLATFORM";
1160
1160
  ANDROID_EXECUTION_PLATFORM: "ANDROID_EXECUTION_PLATFORM";
1161
- }>, z.ZodTransform<"UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM", "web" | "ios" | "android" | "UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM">>>;
1161
+ }>, z.ZodTransform<"UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM", "android" | "web" | "ios" | "UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM">>>;
1162
1162
  automationEmitsOnly: z.ZodOptional<z.ZodBoolean>;
1163
1163
  metadataFilters: z.ZodOptional<z.ZodArray<z.ZodObject<{
1164
1164
  key: z.ZodString;
@@ -1205,14 +1205,14 @@ export declare const getTruecoverageEventTimeSeriesInput: z.ZodObject<{
1205
1205
  release: z.ZodOptional<z.ZodString>;
1206
1206
  branchName: z.ZodOptional<z.ZodString>;
1207
1207
  platform: z.ZodOptional<z.ZodPipe<z.ZodEnum<{
1208
+ android: "android";
1208
1209
  web: "web";
1209
1210
  ios: "ios";
1210
- android: "android";
1211
1211
  UNKNOWN_EXECUTION_PLATFORM: "UNKNOWN_EXECUTION_PLATFORM";
1212
1212
  WEB_EXECUTION_PLATFORM: "WEB_EXECUTION_PLATFORM";
1213
1213
  IOS_EXECUTION_PLATFORM: "IOS_EXECUTION_PLATFORM";
1214
1214
  ANDROID_EXECUTION_PLATFORM: "ANDROID_EXECUTION_PLATFORM";
1215
- }>, z.ZodTransform<"UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM", "web" | "ios" | "android" | "UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM">>>;
1215
+ }>, z.ZodTransform<"UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM", "android" | "web" | "ios" | "UNKNOWN_EXECUTION_PLATFORM" | "WEB_EXECUTION_PLATFORM" | "IOS_EXECUTION_PLATFORM" | "ANDROID_EXECUTION_PLATFORM">>>;
1216
1216
  automationEmitsOnly: z.ZodOptional<z.ZodBoolean>;
1217
1217
  metadataFilters: z.ZodOptional<z.ZodArray<z.ZodObject<{
1218
1218
  key: z.ZodString;
@@ -2008,3 +2008,71 @@ export declare const uploadAttachmentInput: z.ZodObject<{
2008
2008
  export declare const getBatchViewUrlInput: z.ZodObject<{
2009
2009
  batchInvocationId: z.ZodString;
2010
2010
  }, z.core.$strip>;
2011
+ export declare const botRoleSchema: z.ZodEnum<{
2012
+ QA_LEAD: "QA_LEAD";
2013
+ PM: "PM";
2014
+ QA_ENGINEER: "QA_ENGINEER";
2015
+ DEVELOPER: "DEVELOPER";
2016
+ }>;
2017
+ export declare const botCapabilitySchema: z.ZodEnum<{
2018
+ REQUIREMENTS_UPDATE: "REQUIREMENTS_UPDATE";
2019
+ E2E_AUTHORING: "E2E_AUTHORING";
2020
+ ISSUE_FIX: "ISSUE_FIX";
2021
+ MANUAL_TEST_COORDINATION: "MANUAL_TEST_COORDINATION";
2022
+ TEST_BATCH_FIX: "TEST_BATCH_FIX";
2023
+ QA_POSTURE: "QA_POSTURE";
2024
+ }>;
2025
+ export declare const botSubscriptionFilterSchema: z.ZodObject<{
2026
+ field: z.ZodString;
2027
+ op: z.ZodDefault<z.ZodLiteral<"eq">>;
2028
+ value: z.ZodString;
2029
+ }, z.core.$strip>;
2030
+ export declare const botSubscriptionSchema: z.ZodObject<{
2031
+ eventType: z.ZodString;
2032
+ filters: z.ZodOptional<z.ZodArray<z.ZodObject<{
2033
+ field: z.ZodString;
2034
+ op: z.ZodDefault<z.ZodLiteral<"eq">>;
2035
+ value: z.ZodString;
2036
+ }, z.core.$strip>>>;
2037
+ }, z.core.$strip>;
2038
+ export declare const registerBotProfileInput: z.ZodObject<{
2039
+ botId: z.ZodOptional<z.ZodString>;
2040
+ role: z.ZodEnum<{
2041
+ QA_LEAD: "QA_LEAD";
2042
+ PM: "PM";
2043
+ QA_ENGINEER: "QA_ENGINEER";
2044
+ DEVELOPER: "DEVELOPER";
2045
+ }>;
2046
+ responsibilities: z.ZodOptional<z.ZodString>;
2047
+ capabilities: z.ZodDefault<z.ZodArray<z.ZodEnum<{
2048
+ REQUIREMENTS_UPDATE: "REQUIREMENTS_UPDATE";
2049
+ E2E_AUTHORING: "E2E_AUTHORING";
2050
+ ISSUE_FIX: "ISSUE_FIX";
2051
+ MANUAL_TEST_COORDINATION: "MANUAL_TEST_COORDINATION";
2052
+ TEST_BATCH_FIX: "TEST_BATCH_FIX";
2053
+ QA_POSTURE: "QA_POSTURE";
2054
+ }>>>;
2055
+ subscriptions: z.ZodDefault<z.ZodArray<z.ZodObject<{
2056
+ eventType: z.ZodString;
2057
+ filters: z.ZodOptional<z.ZodArray<z.ZodObject<{
2058
+ field: z.ZodString;
2059
+ op: z.ZodDefault<z.ZodLiteral<"eq">>;
2060
+ value: z.ZodString;
2061
+ }, z.core.$strip>>>;
2062
+ }, z.core.$strip>>>;
2063
+ }, z.core.$strip>;
2064
+ export declare const getBotProfileInput: z.ZodObject<{
2065
+ botId: z.ZodOptional<z.ZodString>;
2066
+ }, z.core.$strip>;
2067
+ export declare const getMyTasksInput: z.ZodObject<{
2068
+ userId: z.ZodOptional<z.ZodString>;
2069
+ }, z.core.$strip>;
2070
+ export declare const listTestsAwaitingVerificationInput: z.ZodObject<{
2071
+ userId: z.ZodOptional<z.ZodString>;
2072
+ limit: z.ZodOptional<z.ZodNumber>;
2073
+ }, z.core.$strip>;
2074
+ export declare const MAX_BOT_ACK_EVENT_IDS = 100;
2075
+ export declare const ackBotEventsInput: z.ZodObject<{
2076
+ eventIds: z.ZodArray<z.ZodString>;
2077
+ ackUrl: z.ZodOptional<z.ZodString>;
2078
+ }, z.core.$strip>;
@@ -1044,3 +1044,52 @@ export const uploadAttachmentInput = z.object({
1044
1044
  export const getBatchViewUrlInput = z.object({
1045
1045
  batchInvocationId: z.string().min(1),
1046
1046
  });
1047
+ export const botRoleSchema = z.enum(["QA_LEAD", "PM", "QA_ENGINEER", "DEVELOPER"]);
1048
+ export const botCapabilitySchema = z.enum([
1049
+ "REQUIREMENTS_UPDATE",
1050
+ "E2E_AUTHORING",
1051
+ "ISSUE_FIX",
1052
+ "MANUAL_TEST_COORDINATION",
1053
+ "TEST_BATCH_FIX",
1054
+ "QA_POSTURE",
1055
+ ]);
1056
+ export const botSubscriptionFilterSchema = z.object({
1057
+ /** Payload filter field, e.g. author / assignee / adder. */
1058
+ field: z.string().min(1),
1059
+ op: z.literal("eq").default("eq"),
1060
+ /** Literal value, or the token `me` (resolved to the bot's user). */
1061
+ value: z.string().min(1),
1062
+ });
1063
+ export const botSubscriptionSchema = z.object({
1064
+ /** Wire event type, e.g. git-push, issue-assigned, e2e-batch-completed. */
1065
+ eventType: z.string().min(1),
1066
+ filters: z.array(botSubscriptionFilterSchema).optional(),
1067
+ });
1068
+ export const registerBotProfileInput = z.object({
1069
+ /** Defaults to the bot-id header (TESTCHIMP_BOT_ID) or the OAuth token's bot. */
1070
+ botId: z.string().min(1).max(64).optional(),
1071
+ role: botRoleSchema,
1072
+ responsibilities: z.string().optional(),
1073
+ capabilities: z.array(botCapabilitySchema).default([]),
1074
+ subscriptions: z.array(botSubscriptionSchema).default([]),
1075
+ });
1076
+ export const getBotProfileInput = z.object({
1077
+ botId: z.string().min(1).max(64).optional(),
1078
+ });
1079
+ export const getMyTasksInput = z.object({
1080
+ /** Required with an API key; ignored with an OAuth token (the token's user is used). */
1081
+ userId: z.string().min(1).optional(),
1082
+ });
1083
+ export const listTestsAwaitingVerificationInput = z.object({
1084
+ userId: z.string().min(1).optional(),
1085
+ limit: z.number().int().positive().max(500).optional(),
1086
+ });
1087
+ export const MAX_BOT_ACK_EVENT_IDS = 100;
1088
+ export const ackBotEventsInput = z.object({
1089
+ eventIds: z
1090
+ .array(z.string().regex(/^[\x20-\x7e]+$/, "eventIds must be printable ASCII").min(1))
1091
+ .min(1)
1092
+ .max(MAX_BOT_ACK_EVENT_IDS),
1093
+ /** Webhook delivery ackUrl; defaults to ${TESTCHIMP_INGRESS_URL}/bot/events/ack. */
1094
+ ackUrl: z.string().min(1).optional(),
1095
+ });
@@ -1,10 +1,14 @@
1
1
  import { type ZodTypeAny } from "zod";
2
- import type { PostMcpFn } from "./client.js";
2
+ import { type PostIngressFn, type PostMcpFn } from "./client.js";
3
3
  import { type ProgressLog } from "./ephemeralWait.js";
4
4
  export interface ToolContext {
5
5
  postMcp: PostMcpFn;
6
+ /** Ingress poster (bot acks); defaults to the env-configured ingress client. */
7
+ postIngress?: PostIngressFn;
6
8
  onProgress?: ProgressLog;
7
9
  }
10
+ export declare const BOT_ACK_PATH = "/bot/events/ack";
11
+ export declare const BOT_ACK_UNSUPPORTED_MESSAGE: string;
8
12
  export interface ToolDefinition {
9
13
  kebab: string;
10
14
  description: string;
@@ -1,9 +1,13 @@
1
1
  import { normalizeScope } from "./normalize.js";
2
+ import { postIngress, TestChimpHttpError } from "./client.js";
2
3
  import { runProvisionEphemeralEnvironmentAndWait } from "./ephemeralWait.js";
3
4
  import * as S from "./schemas.js";
4
5
  import { resolveGitHeadSha } from "./gitSha.js";
5
6
  import { parseDateBoundToMillis } from "./dates.js";
6
7
  import { buildAgentTraceabilityPayload, resolveToolchainVersions } from "./agentTraceability.js";
8
+ export const BOT_ACK_PATH = "/bot/events/ack";
9
+ export const BOT_ACK_UNSUPPORTED_MESSAGE = "This TestChimp deployment does not support bot acks yet (ingress returned 404 for /bot/events/ack). " +
10
+ "Upgrade the TestChimp platform, or ask your admin to enable QA bots.";
7
11
  function platformToProtoEnum(platform) {
8
12
  switch (platform) {
9
13
  case "ios":
@@ -1618,6 +1622,112 @@ export const TOOL_DEFINITIONS = [
1618
1622
  });
1619
1623
  },
1620
1624
  },
1625
+ {
1626
+ kebab: "get-my-tasks",
1627
+ description: "List a team member's open QA work in this project: manual test scenarios assigned to them in test runs " +
1628
+ "(with testRunUrl), issues assigned to them (status, severity, due date, issueUrl), and SmartTests they " +
1629
+ "authored that still await verification. With an OAuth token the user is the token's user; with an API key " +
1630
+ "pass userId. Use for daily reminders and 'what should I work on' questions.",
1631
+ inputSchema: S.getMyTasksInput,
1632
+ execute: async (args, { postMcp }) => {
1633
+ const a = args;
1634
+ return postMcp("/api/mcp/get_my_tasks", a.userId ? { userId: a.userId.trim() } : {});
1635
+ },
1636
+ },
1637
+ {
1638
+ kebab: "list-tests-awaiting-verification",
1639
+ description: "List SmartTests whose latest executions still need human verification before they earn a verified badge " +
1640
+ "(testId, testName, filePath, scenarioId / scenarioOrdinalId / scenarioTitle, verificationStatus NOT_VERIFIED " +
1641
+ "or VERIFIED_STALE, reportedAtMillis, workflowExecutionId). " +
1642
+ "Optional userId narrows to tests authored for that user (defaults to the OAuth token's user); optional limit. " +
1643
+ "Use after writing E2E tests to prompt the user to verify the executions.",
1644
+ inputSchema: S.listTestsAwaitingVerificationInput,
1645
+ execute: async (args, { postMcp }) => {
1646
+ const a = args;
1647
+ const body = {};
1648
+ if (a.userId)
1649
+ body.userId = a.userId.trim();
1650
+ if (a.limit != null)
1651
+ body.limit = a.limit;
1652
+ return postMcp("/api/mcp/list_tests_awaiting_verification", body);
1653
+ },
1654
+ },
1655
+ {
1656
+ kebab: "get-qa-posture",
1657
+ description: "Project-wide QA posture snapshot: releases (version, lifecycleStatus, due date, passed / failed / blocked / " +
1658
+ "not-attempted counts), issues (active, inProgress, blocked, openBySeverity), activeTestRuns with result " +
1659
+ "counts, and testsAwaitingVerificationCount. Use for weekly " +
1660
+ "QA digests and release-health questions; summarise for the reader's role rather than dumping raw JSON.",
1661
+ inputSchema: S.emptyInput,
1662
+ execute: async (_args, { postMcp }) => postMcp("/api/mcp/get_qa_posture", {}),
1663
+ },
1664
+ {
1665
+ kebab: "get-bot-compat",
1666
+ description: "Return the minimum testchimp skill version, minimum @testchimp/cli version, and the bot event schema " +
1667
+ "version this TestChimp deployment requires from QA bots. Call on bot startup / daily; if the local skill or " +
1668
+ "CLI is older, ask the user to approve an upgrade.",
1669
+ inputSchema: S.emptyInput,
1670
+ execute: async (_args, { postMcp }) => postMcp("/api/mcp/get_bot_compat", {}),
1671
+ },
1672
+ {
1673
+ kebab: "get-bot-profile",
1674
+ description: "Fetch this QA bot's registration: botId, project, the team member it represents (userId), platform, status, " +
1675
+ "role, responsibilities, capabilities, event subscriptions, paused flag, and webhook health. botId defaults to " +
1676
+ "the bot-id header (TESTCHIMP_BOT_ID) or the OAuth token's bot. Use to confirm identity before onboarding and " +
1677
+ "to check paused / capabilities before running routines.",
1678
+ inputSchema: S.getBotProfileInput,
1679
+ execute: async (args, { postMcp }) => {
1680
+ const a = args;
1681
+ return postMcp("/api/mcp/get_bot_profile", a.botId ? { botId: a.botId } : {});
1682
+ },
1683
+ },
1684
+ {
1685
+ kebab: "register-bot-profile",
1686
+ description: "Register or replace this QA bot's profile and event subscriptions atomically (mutating — confirm with the user " +
1687
+ "first). role: QA_LEAD | PM | QA_ENGINEER | DEVELOPER. responsibilities: the user's own words. capabilities: " +
1688
+ "REQUIREMENTS_UPDATE, E2E_AUTHORING, ISSUE_FIX, MANUAL_TEST_COORDINATION, TEST_BATCH_FIX, QA_POSTURE. " +
1689
+ "subscriptions: [{eventType, filters:[{field, op:'eq', value}]}] where value may be 'me' (e.g. git-push " +
1690
+ "author=me, issue-assigned assignee=me, e2e-batch-completed). Derive subscriptions from capabilities per the " +
1691
+ "testchimp skill's bot onboarding guide. botId defaults to the bot-id header / OAuth token.",
1692
+ inputSchema: S.registerBotProfileInput,
1693
+ execute: async (args, { postMcp }) => {
1694
+ const a = args;
1695
+ const body = {
1696
+ role: a.role,
1697
+ responsibilities: a.responsibilities?.trim() ?? "",
1698
+ capabilities: a.capabilities,
1699
+ subscriptions: a.subscriptions.map((s) => ({
1700
+ eventType: s.eventType.trim(),
1701
+ filters: (s.filters ?? []).map((f) => ({ field: f.field.trim(), op: f.op, value: f.value.trim() })),
1702
+ })),
1703
+ };
1704
+ if (a.botId)
1705
+ body.botId = a.botId;
1706
+ return postMcp("/bots/register_profile", body);
1707
+ },
1708
+ },
1709
+ {
1710
+ kebab: "ack-bot-events",
1711
+ description: "Acknowledge QA-bot webhook events by eventId (1-100 per call) so TestChimp stops redelivering them. Ack every " +
1712
+ "event you handled or decided to ignore (including expired / irrelevant ones); re-acking is safe. Pass the " +
1713
+ "delivery's ackUrl when present (defaults to the ingress /bot/events/ack). Returns per-id status: " +
1714
+ "BOT_ACK_ACCEPTED / BOT_ACK_ACKED / BOT_ACK_ALREADY_ACKED / BOT_ACK_EXPIRED_RECORDED are fine; " +
1715
+ "BOT_ACK_UNKNOWN_EVENT / BOT_ACK_NOT_A_TARGET / BOT_ACK_MISSING_BOT_ID indicate a wrong id or bot identity.",
1716
+ inputSchema: S.ackBotEventsInput,
1717
+ execute: async (args, ctx) => {
1718
+ const a = args;
1719
+ const post = ctx.postIngress ?? postIngress;
1720
+ const eventIds = Array.from(new Set(a.eventIds));
1721
+ try {
1722
+ return await post(a.ackUrl?.trim() || BOT_ACK_PATH, { eventIds });
1723
+ }
1724
+ catch (e) {
1725
+ if (e instanceof TestChimpHttpError && e.status === 404)
1726
+ throw new Error(BOT_ACK_UNSUPPORTED_MESSAGE);
1727
+ throw e;
1728
+ }
1729
+ },
1730
+ },
1621
1731
  ];
1622
1732
  const TOOL_BY_KEBAB = new Map(TOOL_DEFINITIONS.map((t) => [t.kebab, t]));
1623
1733
  export function getToolDefinition(kebab) {