@testchimp/cli 0.1.78 → 0.1.80
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/dist/cli/program.js +32 -0
- package/dist/core/client.js +24 -1
- package/dist/core/client.test.d.ts +1 -0
- package/dist/core/client.test.js +54 -0
- package/dist/core/schemas.d.ts +6 -1
- package/dist/core/schemas.js +7 -0
- package/dist/core/tools.js +30 -3
- package/dist/core/tools.test.d.ts +1 -0
- package/dist/core/tools.test.js +56 -0
- package/package.json +10 -2
package/dist/cli/program.js
CHANGED
|
@@ -434,6 +434,38 @@ export function buildCliProgram() {
|
|
|
434
434
|
const out = await runTool("get-test-scenarios", merged, { postMcp });
|
|
435
435
|
console.log(out);
|
|
436
436
|
});
|
|
437
|
+
program
|
|
438
|
+
.command("list-test-scenarios-for-scope")
|
|
439
|
+
.description(TOOL_DEFINITIONS.find((t) => t.kebab === "list-test-scenarios-for-scope").description)
|
|
440
|
+
.addOption(jsonInputOption())
|
|
441
|
+
.option("--named-test-run-id <id>", "named test run id")
|
|
442
|
+
.option("--release <label>", "release catalog version / label")
|
|
443
|
+
.option("--plans-path <path>", "platform plans folder or .md file (e.g. plans/scenarios/checkout or plans/scenarios/checkout/login.md)")
|
|
444
|
+
.action(async (opts) => {
|
|
445
|
+
const body = {};
|
|
446
|
+
if (opts.namedTestRunId)
|
|
447
|
+
body.namedTestRunId = String(opts.namedTestRunId).trim();
|
|
448
|
+
if (opts.release)
|
|
449
|
+
body.release = String(opts.release).trim();
|
|
450
|
+
if (opts.plansPath)
|
|
451
|
+
body.plansPath = String(opts.plansPath).trim();
|
|
452
|
+
const merged = mergeBodies(body, opts.jsonInput);
|
|
453
|
+
for (const key of ["namedTestRunId", "release", "plansPath"]) {
|
|
454
|
+
const value = merged[key];
|
|
455
|
+
if (typeof value !== "string") {
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
const trimmed = value.trim();
|
|
459
|
+
if (!trimmed) {
|
|
460
|
+
delete merged[key];
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
merged[key] = trimmed;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const out = await runTool("list-test-scenarios-for-scope", merged, { postMcp });
|
|
467
|
+
console.log(out);
|
|
468
|
+
});
|
|
437
469
|
program
|
|
438
470
|
.command("get-manual-session-details")
|
|
439
471
|
.description(TOOL_DEFINITIONS.find((t) => t.kebab === "get-manual-session-details").description)
|
package/dist/core/client.js
CHANGED
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
/** HTTP client for TestChimp MCP proxy APIs. */
|
|
2
2
|
export const DEFAULT_BACKEND = "https://featureservice.testchimp.io";
|
|
3
|
+
function isTestChimpSaasFeatureservice(hostname) {
|
|
4
|
+
return hostname === "featureservice.testchimp.io"
|
|
5
|
+
|| /^featureservice-[a-z0-9-]+\.testchimp\.io$/i.test(hostname);
|
|
6
|
+
}
|
|
3
7
|
export function getBackendUrl() {
|
|
4
8
|
const raw = process.env.TESTCHIMP_BACKEND_URL?.trim();
|
|
5
9
|
if (!raw)
|
|
6
10
|
return DEFAULT_BACKEND;
|
|
7
|
-
|
|
11
|
+
let parsed;
|
|
12
|
+
try {
|
|
13
|
+
parsed = new URL(raw);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
throw new Error(`TESTCHIMP_BACKEND_URL must be an absolute http(s) URL; received ${JSON.stringify(raw)}`);
|
|
17
|
+
}
|
|
18
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
19
|
+
throw new Error(`TESTCHIMP_BACKEND_URL must use http or https; received ${parsed.protocol}`);
|
|
20
|
+
}
|
|
21
|
+
if (parsed.protocol === "http:" && isTestChimpSaasFeatureservice(parsed.hostname)) {
|
|
22
|
+
throw new Error(`TESTCHIMP_BACKEND_URL must use https for TestChimp SaaS; change it to https://${parsed.host}`);
|
|
23
|
+
}
|
|
24
|
+
return raw.replace(/\/+$/, "");
|
|
8
25
|
}
|
|
9
26
|
export function requireApiKey() {
|
|
10
27
|
const k = process.env.TESTCHIMP_API_KEY?.trim();
|
|
@@ -18,12 +35,18 @@ export async function postMcp(path, body) {
|
|
|
18
35
|
const url = `${getBackendUrl()}${path}`;
|
|
19
36
|
const res = await fetch(url, {
|
|
20
37
|
method: "POST",
|
|
38
|
+
redirect: "manual",
|
|
21
39
|
headers: {
|
|
22
40
|
"Content-Type": "application/json",
|
|
23
41
|
"TestChimp-Api-Key": apiKey,
|
|
24
42
|
},
|
|
25
43
|
body: JSON.stringify(body ?? {}),
|
|
26
44
|
});
|
|
45
|
+
if (res.status >= 300 && res.status < 400) {
|
|
46
|
+
const location = res.headers.get("location");
|
|
47
|
+
const destination = location ? ` to ${location}` : "";
|
|
48
|
+
throw new Error(`TestChimp API redirected ${res.status}${destination}. Refusing to follow because an HTTP redirect may rewrite POST to GET; set TESTCHIMP_BACKEND_URL to the final HTTPS featureservice URL.`);
|
|
49
|
+
}
|
|
27
50
|
const text = await res.text();
|
|
28
51
|
if (!res.ok) {
|
|
29
52
|
throw new Error(`TestChimp API ${res.status} ${res.statusText}: ${text}`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { afterEach, describe, it } from "node:test";
|
|
3
|
+
import { getBackendUrl, postMcp } from "./client.js";
|
|
4
|
+
const originalBackendUrl = process.env.TESTCHIMP_BACKEND_URL;
|
|
5
|
+
const originalApiKey = process.env.TESTCHIMP_API_KEY;
|
|
6
|
+
const originalFetch = globalThis.fetch;
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
if (originalBackendUrl == null)
|
|
9
|
+
delete process.env.TESTCHIMP_BACKEND_URL;
|
|
10
|
+
else
|
|
11
|
+
process.env.TESTCHIMP_BACKEND_URL = originalBackendUrl;
|
|
12
|
+
if (originalApiKey == null)
|
|
13
|
+
delete process.env.TESTCHIMP_API_KEY;
|
|
14
|
+
else
|
|
15
|
+
process.env.TESTCHIMP_API_KEY = originalApiKey;
|
|
16
|
+
globalThis.fetch = originalFetch;
|
|
17
|
+
});
|
|
18
|
+
describe("getBackendUrl", () => {
|
|
19
|
+
it("rejects insecure TestChimp SaaS URLs", () => {
|
|
20
|
+
process.env.TESTCHIMP_BACKEND_URL = "http://featureservice.testchimp.io";
|
|
21
|
+
assert.throws(() => getBackendUrl(), /must use https for TestChimp SaaS/);
|
|
22
|
+
process.env.TESTCHIMP_BACKEND_URL = "http://featureservice-staging.testchimp.io/";
|
|
23
|
+
assert.throws(() => getBackendUrl(), /must use https for TestChimp SaaS/);
|
|
24
|
+
});
|
|
25
|
+
it("allows HTTP for local and self-hosted development", () => {
|
|
26
|
+
process.env.TESTCHIMP_BACKEND_URL = "http://localhost:4301/";
|
|
27
|
+
assert.equal(getBackendUrl(), "http://localhost:4301");
|
|
28
|
+
process.env.TESTCHIMP_BACKEND_URL = "http://featureservice.internal/";
|
|
29
|
+
assert.equal(getBackendUrl(), "http://featureservice.internal");
|
|
30
|
+
});
|
|
31
|
+
it("rejects invalid and unsupported URLs", () => {
|
|
32
|
+
process.env.TESTCHIMP_BACKEND_URL = "featureservice.testchimp.io";
|
|
33
|
+
assert.throws(() => getBackendUrl(), /absolute http\(s\) URL/);
|
|
34
|
+
process.env.TESTCHIMP_BACKEND_URL = "ftp://featureservice.internal";
|
|
35
|
+
assert.throws(() => getBackendUrl(), /must use http or https/);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
describe("postMcp", () => {
|
|
39
|
+
it("does not follow redirects that could change POST to GET", async () => {
|
|
40
|
+
process.env.TESTCHIMP_BACKEND_URL = "https://featureservice.example.com";
|
|
41
|
+
process.env.TESTCHIMP_API_KEY = "test-key";
|
|
42
|
+
let requestInit;
|
|
43
|
+
globalThis.fetch = (async (_input, init) => {
|
|
44
|
+
requestInit = init;
|
|
45
|
+
return new Response(null, {
|
|
46
|
+
status: 301,
|
|
47
|
+
headers: { location: "https://featureservice.testchimp.io/api/mcp/example" },
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
await assert.rejects(postMcp("/api/mcp/example", {}), /Refusing to follow because an HTTP redirect may rewrite POST to GET/);
|
|
51
|
+
assert.equal(requestInit?.method, "POST");
|
|
52
|
+
assert.equal(requestInit?.redirect, "manual");
|
|
53
|
+
});
|
|
54
|
+
});
|
package/dist/core/schemas.d.ts
CHANGED
|
@@ -23,8 +23,8 @@ export declare const listCoverageInput: z.ZodObject<{
|
|
|
23
23
|
android: "android";
|
|
24
24
|
}>>;
|
|
25
25
|
recordTypes: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
26
|
-
smart_test: "smart_test";
|
|
27
26
|
manual: "manual";
|
|
27
|
+
smart_test: "smart_test";
|
|
28
28
|
perf_test: "perf_test";
|
|
29
29
|
SMART_TEST: "SMART_TEST";
|
|
30
30
|
MANUAL: "MANUAL";
|
|
@@ -281,6 +281,11 @@ export declare const getTestScenariosInput: z.ZodObject<{
|
|
|
281
281
|
scenarioOrdinalIds: z.ZodOptional<z.ZodArray<z.ZodCoercedNumber<unknown>>>;
|
|
282
282
|
externalIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
283
283
|
}, z.core.$strip>;
|
|
284
|
+
export declare const listTestScenariosForScopeInput: z.ZodObject<{
|
|
285
|
+
namedTestRunId: z.ZodOptional<z.ZodString>;
|
|
286
|
+
release: z.ZodOptional<z.ZodString>;
|
|
287
|
+
plansPath: z.ZodOptional<z.ZodString>;
|
|
288
|
+
}, z.core.$strip>;
|
|
284
289
|
export declare const getManualSessionDetailsInput: z.ZodObject<{
|
|
285
290
|
manualSessionId: z.ZodString;
|
|
286
291
|
}, z.core.$strip>;
|
package/dist/core/schemas.js
CHANGED
|
@@ -157,6 +157,13 @@ export const getTestScenariosInput = z
|
|
|
157
157
|
})
|
|
158
158
|
.refine((v) => (v.scenarioOrdinalIds != null && v.scenarioOrdinalIds.length > 0) ||
|
|
159
159
|
(v.externalIds != null && v.externalIds.length > 0), { message: "Provide scenarioOrdinalIds and/or externalIds (non-empty)" });
|
|
160
|
+
export const listTestScenariosForScopeInput = z
|
|
161
|
+
.object({
|
|
162
|
+
namedTestRunId: z.string().min(1).optional(),
|
|
163
|
+
release: z.string().min(1).optional(),
|
|
164
|
+
plansPath: z.string().min(1).optional(),
|
|
165
|
+
})
|
|
166
|
+
.refine((v) => [v.namedTestRunId, v.release, v.plansPath].filter((x) => x != null && String(x).trim() !== "").length === 1, { message: "Provide exactly one of namedTestRunId, release, or plansPath" });
|
|
160
167
|
export const getManualSessionDetailsInput = z.object({
|
|
161
168
|
manualSessionId: z.string().min(1),
|
|
162
169
|
});
|
package/dist/core/tools.js
CHANGED
|
@@ -350,6 +350,28 @@ export const TOOL_DEFINITIONS = [
|
|
|
350
350
|
return postMcp("/api/mcp/get_test_scenarios", body);
|
|
351
351
|
},
|
|
352
352
|
},
|
|
353
|
+
{
|
|
354
|
+
kebab: "list-test-scenarios-for-scope",
|
|
355
|
+
description: "List in-scope test scenarios (ordinalId + title only) for exactly one locator: " +
|
|
356
|
+
"a named test run, a release label, or a platform plans folder/file path. " +
|
|
357
|
+
"Does not return markdown — use get-test-scenarios for detail by known ordinal. " +
|
|
358
|
+
"Use when executing tests for a plans path, release, or named test run.",
|
|
359
|
+
inputSchema: S.listTestScenariosForScopeInput,
|
|
360
|
+
execute: async (args, { postMcp }) => {
|
|
361
|
+
const a = args;
|
|
362
|
+
const body = {};
|
|
363
|
+
const namedTestRunId = a.namedTestRunId?.trim();
|
|
364
|
+
const release = a.release?.trim();
|
|
365
|
+
const plansPath = a.plansPath?.trim();
|
|
366
|
+
if (namedTestRunId)
|
|
367
|
+
body.namedTestRunId = namedTestRunId;
|
|
368
|
+
if (release)
|
|
369
|
+
body.release = release;
|
|
370
|
+
if (plansPath)
|
|
371
|
+
body.plansPath = plansPath;
|
|
372
|
+
return postMcp("/api/mcp/list_test_scenarios_for_scope", body);
|
|
373
|
+
},
|
|
374
|
+
},
|
|
353
375
|
{
|
|
354
376
|
kebab: "get-manual-session-details",
|
|
355
377
|
description: "Fetch a manual test session by id. Returns project id, title, environment, steps " +
|
|
@@ -1254,8 +1276,11 @@ export const TOOL_DEFINITIONS = [
|
|
|
1254
1276
|
},
|
|
1255
1277
|
{
|
|
1256
1278
|
kebab: "list-api-operations",
|
|
1257
|
-
description: "List API operations for a service resource with covering-test previews
|
|
1258
|
-
"
|
|
1279
|
+
description: "List API operations for a service resource with covering-test previews, coverageSummary scores, " +
|
|
1280
|
+
"observability mapping state, and runtimeObservation from the latest finalized production daily summary " +
|
|
1281
|
+
"(with latest-hour fallback). runtimeObservation may include window bounds, requestCount/rpm, errorCount/errorRate, " +
|
|
1282
|
+
"p50/p95/p99LatencyMs, status-class counts, and syncStatus. Missing observability is unknown/not configured, not zero. " +
|
|
1283
|
+
"Use these signals to prioritize uncovered high-volume/high-error operations; prefer --root-file-path.",
|
|
1259
1284
|
inputSchema: S.listApiOperationsInput,
|
|
1260
1285
|
execute: async (args, { postMcp }) => {
|
|
1261
1286
|
const a = args;
|
|
@@ -1274,7 +1299,9 @@ export const TOOL_DEFINITIONS = [
|
|
|
1274
1299
|
{
|
|
1275
1300
|
kebab: "get-api-operation-detail",
|
|
1276
1301
|
description: "Fetch detailed API operation coverage (request/query/response fields, response codes, covering tests) — " +
|
|
1277
|
-
"
|
|
1302
|
+
"the operation also carries obsMappingState and the latest runtimeObservation daily summary when available. " +
|
|
1303
|
+
"Use request volume/error rate to rank coverage risk and p95/p99 latency to prioritize performance-test work; " +
|
|
1304
|
+
"do not treat absent observations as zero or production latency as a test threshold. Prefer TestChimp operation id (--id ULID); " +
|
|
1278
1305
|
"or rootFilePath + oasOperationId; or rootFilePath + httpMethod + pathTemplate.",
|
|
1279
1306
|
inputSchema: S.getApiOperationDetailInput,
|
|
1280
1307
|
execute: async (args, { postMcp }) => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { getToolDefinition, runTool } from "./tools.js";
|
|
4
|
+
const runtimeObservation = {
|
|
5
|
+
windowStartMillis: "1789497600000",
|
|
6
|
+
windowEndMillis: "1789584000000",
|
|
7
|
+
requestCount: "125000",
|
|
8
|
+
rpm: 86.8,
|
|
9
|
+
errorCount: "2500",
|
|
10
|
+
errorRate: 0.02,
|
|
11
|
+
p50LatencyMs: 42,
|
|
12
|
+
p95LatencyMs: 180,
|
|
13
|
+
p99LatencyMs: 420,
|
|
14
|
+
status2xxCount: "122000",
|
|
15
|
+
status4xxCount: "1000",
|
|
16
|
+
status5xxCount: "2000",
|
|
17
|
+
syncStatus: "OBSERVABILITY_SYNC_SUCCESS",
|
|
18
|
+
};
|
|
19
|
+
describe("API operation observability", () => {
|
|
20
|
+
it("preserves daily observability summaries when listing operations", async () => {
|
|
21
|
+
const response = JSON.stringify({
|
|
22
|
+
operations: [{
|
|
23
|
+
id: "01OBSERVABILITYOPERATION",
|
|
24
|
+
coverageSummary: { coverageScore: 25 },
|
|
25
|
+
obsMappingState: "API_OBS_MAPPED",
|
|
26
|
+
runtimeObservation,
|
|
27
|
+
}],
|
|
28
|
+
});
|
|
29
|
+
let request;
|
|
30
|
+
const result = await runTool("list-api-operations", { rootFilePath: "openapi/service.json" }, {
|
|
31
|
+
postMcp: async (path, body) => {
|
|
32
|
+
request = { path, body };
|
|
33
|
+
return response;
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
assert.deepEqual(request, {
|
|
37
|
+
path: "/api/mcp/list_api_operations",
|
|
38
|
+
body: { rootFilePath: "openapi/service.json" },
|
|
39
|
+
});
|
|
40
|
+
assert.deepEqual(JSON.parse(result), JSON.parse(response));
|
|
41
|
+
});
|
|
42
|
+
it("preserves observability on operation schema coverage detail", async () => {
|
|
43
|
+
const response = JSON.stringify({
|
|
44
|
+
operation: {
|
|
45
|
+
id: "01OBSERVABILITYOPERATION",
|
|
46
|
+
obsMappingState: "API_OBS_MAPPED",
|
|
47
|
+
runtimeObservation,
|
|
48
|
+
},
|
|
49
|
+
requestFields: [{ jsonPointer: "/customerId", coveringTests: [] }],
|
|
50
|
+
responseCodes: [{ responseCode: "500", coveringTests: [] }],
|
|
51
|
+
});
|
|
52
|
+
const result = await runTool("get-api-operation-detail", { id: "01OBSERVABILITYOPERATION" }, { postMcp: async () => response });
|
|
53
|
+
assert.deepEqual(JSON.parse(result), JSON.parse(response));
|
|
54
|
+
assert.match(getToolDefinition("get-api-operation-detail")?.description ?? "", /p95\/p99 latency/);
|
|
55
|
+
});
|
|
56
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testchimp/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.80",
|
|
4
4
|
"description": "TestChimp CLI and MCP server — coverage, plans, EaaS, TrueCoverage, API operations (calls /api/mcp/*)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/bin/testchimp.js",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
15
|
"build": "tsc",
|
|
16
|
-
"test": "npm run build && node --test dist/chimphands/llmRetryProxy.test.js dist/chimphands/midTurnSteering.test.js dist/chimphands/steeringSidecar.test.js dist/chimphands/agentBranch.test.js dist/chimphands/worktreeCommitPaths.test.js",
|
|
16
|
+
"test": "npm run build && node --test dist/core/client.test.js dist/core/tools.test.js dist/chimphands/llmRetryProxy.test.js dist/chimphands/midTurnSteering.test.js dist/chimphands/steeringSidecar.test.js dist/chimphands/agentBranch.test.js dist/chimphands/worktreeCommitPaths.test.js",
|
|
17
17
|
"watch": "tsc --watch",
|
|
18
18
|
"start": "node dist/bin/testchimp.js",
|
|
19
19
|
"prepublishOnly": "npm run build"
|
|
@@ -32,6 +32,14 @@
|
|
|
32
32
|
"@types/ws": "^8.18.1",
|
|
33
33
|
"typescript": "^6.0.2"
|
|
34
34
|
},
|
|
35
|
+
"overrides": {
|
|
36
|
+
"fast-uri": "3.1.7",
|
|
37
|
+
"qs": "6.16.0",
|
|
38
|
+
"ip-address": "10.3.1",
|
|
39
|
+
"hono": "4.12.34",
|
|
40
|
+
"@hono/node-server": "1.19.17",
|
|
41
|
+
"body-parser": "2.3.0"
|
|
42
|
+
},
|
|
35
43
|
"keywords": [
|
|
36
44
|
"testchimp",
|
|
37
45
|
"mcp",
|