@testchimp/cli 0.1.79 → 0.1.81

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.
@@ -480,6 +480,23 @@ export function buildCliProgram() {
480
480
  const out = await runTool("get-manual-session-details", merged, { postMcp });
481
481
  console.log(out);
482
482
  });
483
+ program
484
+ .command("get-meeting-transcript")
485
+ .description(TOOL_DEFINITIONS.find((t) => t.kebab === "get-meeting-transcript").description)
486
+ .addOption(jsonInputOption())
487
+ .option("--meeting-id <id>", "meeting id (calendar event id, or URL hash for ad-hoc; same as Studio folder under ~/.testchimp/data/meetings/)")
488
+ .action(async (opts) => {
489
+ const body = {};
490
+ if (opts.meetingId) {
491
+ body.meetingId = String(opts.meetingId).trim();
492
+ }
493
+ const merged = mergeBodies(body, opts.jsonInput);
494
+ if (!merged.meetingId || String(merged.meetingId).trim() === "") {
495
+ throw new Error("meetingId is required (--meeting-id)");
496
+ }
497
+ const out = await runTool("get-meeting-transcript", { meetingId: String(merged.meetingId).trim() }, { postMcp });
498
+ console.log(out);
499
+ });
483
500
  program
484
501
  .command("get-issue-details")
485
502
  .description(TOOL_DEFINITIONS.find((t) => t.kebab === "get-issue-details").description)
@@ -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
- return raw.replace(/\/$/, "");
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
+ });
@@ -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";
@@ -289,6 +289,9 @@ export declare const listTestScenariosForScopeInput: z.ZodObject<{
289
289
  export declare const getManualSessionDetailsInput: z.ZodObject<{
290
290
  manualSessionId: z.ZodString;
291
291
  }, z.core.$strip>;
292
+ export declare const getMeetingTranscriptInput: z.ZodObject<{
293
+ meetingId: z.ZodString;
294
+ }, z.core.$strip>;
292
295
  export declare const getIssueDetailsInput: z.ZodObject<{
293
296
  issueId: z.ZodString;
294
297
  }, z.core.$strip>;
@@ -167,6 +167,10 @@ export const listTestScenariosForScopeInput = z
167
167
  export const getManualSessionDetailsInput = z.object({
168
168
  manualSessionId: z.string().min(1),
169
169
  });
170
+ export const getMeetingTranscriptInput = z.object({
171
+ /** Calendar event id, or URL hash for ad-hoc meetings */
172
+ meetingId: z.string().min(1),
173
+ });
170
174
  export const getIssueDetailsInput = z.object({
171
175
  /** Accepts #B-123, B-123, #B123, B123, or plain 123 */
172
176
  issueId: z.string().min(1),
@@ -385,6 +385,19 @@ export const TOOL_DEFINITIONS = [
385
385
  });
386
386
  },
387
387
  },
388
+ {
389
+ kebab: "get-meeting-transcript",
390
+ description: "Fetch a cloud-synced Meeting Bots transcript by meeting id (calendar event id, or URL hash " +
391
+ "for ad-hoc meetings). Prefer local ~/.testchimp/data/meetings/<meeting-id>/transcript.md " +
392
+ "when present on Studio / the recording machine.",
393
+ inputSchema: S.getMeetingTranscriptInput,
394
+ execute: async (args, { postMcp }) => {
395
+ const a = args;
396
+ return postMcp("/api/mcp/get_meeting_transcript", {
397
+ meetingId: a.meetingId,
398
+ });
399
+ },
400
+ },
388
401
  {
389
402
  kebab: "get-issue-details",
390
403
  description: "Fetch a TestChimp issue (bug) by ordinal id. Accepts flexible issueId formats: " +
@@ -1276,8 +1289,11 @@ export const TOOL_DEFINITIONS = [
1276
1289
  },
1277
1290
  {
1278
1291
  kebab: "list-api-operations",
1279
- description: "List API operations for a service resource with covering-test previews and coverageSummary scores " +
1280
- "(same payload as the Operations list UI). Prefer --root-file-path (repo-relative OpenAPI root).",
1292
+ description: "List API operations for a service resource with covering-test previews, coverageSummary scores, " +
1293
+ "observability mapping state, and runtimeObservation from the latest finalized production daily summary " +
1294
+ "(with latest-hour fallback). runtimeObservation may include window bounds, requestCount/rpm, errorCount/errorRate, " +
1295
+ "p50/p95/p99LatencyMs, status-class counts, and syncStatus. Missing observability is unknown/not configured, not zero. " +
1296
+ "Use these signals to prioritize uncovered high-volume/high-error operations; prefer --root-file-path.",
1281
1297
  inputSchema: S.listApiOperationsInput,
1282
1298
  execute: async (args, { postMcp }) => {
1283
1299
  const a = args;
@@ -1296,7 +1312,9 @@ export const TOOL_DEFINITIONS = [
1296
1312
  {
1297
1313
  kebab: "get-api-operation-detail",
1298
1314
  description: "Fetch detailed API operation coverage (request/query/response fields, response codes, covering tests) — " +
1299
- "same payload as the Operation detail UI. Prefer TestChimp operation id (--id ULID); " +
1315
+ "the operation also carries obsMappingState and the latest runtimeObservation daily summary when available. " +
1316
+ "Use request volume/error rate to rank coverage risk and p95/p99 latency to prioritize performance-test work; " +
1317
+ "do not treat absent observations as zero or production latency as a test threshold. Prefer TestChimp operation id (--id ULID); " +
1300
1318
  "or rootFilePath + oasOperationId; or rootFilePath + httpMethod + pathTemplate.",
1301
1319
  inputSchema: S.getApiOperationDetailInput,
1302
1320
  execute: async (args, { postMcp }) => {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,76 @@
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
+ });
57
+ describe("get-meeting-transcript", () => {
58
+ it("posts meetingId to get_meeting_transcript", async () => {
59
+ const response = JSON.stringify({
60
+ meetingId: "evt-123",
61
+ transcriptMarkdown: "# Meeting\n\nP1: hello",
62
+ });
63
+ let request;
64
+ const result = await runTool("get-meeting-transcript", { meetingId: "evt-123" }, {
65
+ postMcp: async (path, body) => {
66
+ request = { path, body };
67
+ return response;
68
+ },
69
+ });
70
+ assert.deepEqual(request, {
71
+ path: "/api/mcp/get_meeting_transcript",
72
+ body: { meetingId: "evt-123" },
73
+ });
74
+ assert.deepEqual(JSON.parse(result), JSON.parse(response));
75
+ });
76
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testchimp/cli",
3
- "version": "0.1.79",
3
+ "version": "0.1.81",
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"