mmt-testlight 0.4.5 → 1.40.0

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.
@@ -8,13 +8,13 @@ Canonical low-token example. Mirror this shape; prefer `scaffold_test` over inve
8
8
  type: api
9
9
  title: Echo message
10
10
  description: Golden smoke API for AI agents — minimal POST with outputs
11
- method: post
12
- url: https://test.mmt.dev/echo
13
- format: json
14
11
  inputs:
15
12
  message: hello
16
13
  outputs:
17
14
  echoed: body.message
15
+ url: https://test.mmt.dev/echo
16
+ method: post
17
+ format: json
18
18
  body:
19
19
  message: i:message
20
20
  ```
package/esbuild.mjs CHANGED
@@ -61,6 +61,8 @@ await esbuild.build({
61
61
  'js-yaml',
62
62
  'yaml',
63
63
  'xml-js',
64
+ '@grpc/grpc-js',
65
+ '@grpc/proto-loader',
64
66
  ],
65
67
  logOverride: {
66
68
  'unsupported-dynamic-import': 'silent',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mmt-testlight",
3
- "version": "0.4.5",
3
+ "version": "1.40.0",
4
4
  "description": "Multimeter CLI test runner",
5
5
  "license": "Apache-2.0",
6
6
  "private": false,
package/src/mockRunner.ts CHANGED
@@ -6,65 +6,15 @@ import fs from 'fs';
6
6
  import http from 'http';
7
7
  import https from 'https';
8
8
  import path from 'path';
9
- import yaml from 'js-yaml';
10
9
  import * as mmtcore from 'mmt-core';
11
10
  import {findProjectRootSync, resolveCertFilePath} from 'mmt-core/fileHelper';
11
+ import {dispatchMockHttpRequest} from 'mmt-core/mockDispatch';
12
+ import {buildMockHttpsOptions} from 'mmt-core/mockTlsMaterial';
12
13
 
13
14
  const {mockParsePack, mockServer, variableReplacer} = mmtcore;
14
15
 
15
- type GeneratedTlsMaterial = {
16
- cert: string;
17
- key: string;
18
- };
19
-
20
16
  /** Track active servers so we can clean them all up at exit. */
21
17
  const activeServers = new Map<string, {server: http.Server | https.Server; port: number; dispose: () => void}>();
22
- let generatedDefaultTlsMaterial: GeneratedTlsMaterial | undefined;
23
-
24
- function getDefaultMockTlsMaterial(): GeneratedTlsMaterial {
25
- if (generatedDefaultTlsMaterial) {
26
- return generatedDefaultTlsMaterial;
27
- }
28
-
29
- // Generate a localhost-only self-signed cert at runtime so the CLI
30
- // does not embed or distribute a static private key.
31
- const forge = require('node-forge');
32
- const keys = forge.pki.rsa.generateKeyPair(2048);
33
- const certificate = forge.pki.createCertificate();
34
- const now = new Date();
35
- const expiresAt = new Date(now);
36
- expiresAt.setFullYear(expiresAt.getFullYear() + 10);
37
-
38
- certificate.publicKey = keys.publicKey;
39
- certificate.serialNumber = Math.max(Date.now(), 1).toString(16);
40
- certificate.validity.notBefore = now;
41
- certificate.validity.notAfter = expiresAt;
42
-
43
- const subject = [{name: 'commonName', value: 'localhost'}];
44
- certificate.setSubject(subject);
45
- certificate.setIssuer(subject);
46
- certificate.setExtensions([
47
- {name: 'basicConstraints', cA: false},
48
- {name: 'keyUsage', digitalSignature: true, keyEncipherment: true},
49
- {name: 'extKeyUsage', serverAuth: true},
50
- {
51
- name: 'subjectAltName',
52
- altNames: [
53
- {type: 2, value: 'localhost'},
54
- {type: 7, ip: '127.0.0.1'},
55
- {type: 7, ip: '::1'},
56
- ],
57
- },
58
- ]);
59
- certificate.sign(keys.privateKey, forge.md.sha256.create());
60
-
61
- generatedDefaultTlsMaterial = {
62
- cert: forge.pki.certificateToPem(certificate),
63
- key: forge.pki.privateKeyToPem(keys.privateKey),
64
- };
65
-
66
- return generatedDefaultTlsMaterial;
67
- }
68
18
 
69
19
  function resolveFilePath(relative: string, basePath: string): string {
70
20
  return resolveCertFilePath(relative, {baseFilePath: basePath});
@@ -78,26 +28,11 @@ function createHttpsMockServer(
78
28
  data: any,
79
29
  filePath: string,
80
30
  requestHandler: http.RequestListener): https.Server {
81
- const connection = data.connection || {};
82
- const hasCustomCert = !!connection.cert || !!connection.key;
83
- if (hasCustomCert && (!connection.cert || !connection.key)) {
84
- throw new Error('connection.cert and connection.key must be provided together');
85
- }
86
- const defaultTlsMaterial = hasCustomCert ? undefined : getDefaultMockTlsMaterial();
87
- const tlsOptions: https.ServerOptions = {
88
- cert: connection.cert ? fs.readFileSync(resolveFilePath(connection.cert, filePath)) : defaultTlsMaterial!.cert,
89
- key: connection.key ? fs.readFileSync(resolveFilePath(connection.key, filePath)) : defaultTlsMaterial!.key,
90
- };
91
- if (connection.client_ca) {
92
- tlsOptions.ca = fs.readFileSync(resolveFilePath(connection.client_ca, filePath));
93
- }
94
- if (connection.mode === 'mtls') {
95
- if (!connection.client_ca) {
96
- throw new Error('connection.client_ca is required when connection.mode is mtls');
97
- }
98
- tlsOptions.requestCert = true;
99
- tlsOptions.rejectUnauthorized = true;
100
- }
31
+ const tlsOptions = buildMockHttpsOptions(
32
+ data.connection,
33
+ (abs) => fs.readFileSync(abs),
34
+ (rel) => resolveFilePath(rel, filePath),
35
+ );
101
36
  return https.createServer(tlsOptions, requestHandler);
102
37
  }
103
38
 
@@ -116,10 +51,10 @@ export async function startMockServerFromPath(
116
51
  }
117
52
 
118
53
  const rawContent = fs.readFileSync(filePath, 'utf-8');
119
- let parsed: any;
54
+ let processedContent = rawContent;
120
55
  try {
121
56
  const processor = (mmtcore as any).dataImportProcessor;
122
- const processedContent = processor?.processDataImportsInYaml ?
57
+ processedContent = processor?.processDataImportsInYaml ?
123
58
  await processor.processDataImportsInYaml({
124
59
  rawText: rawContent,
125
60
  filePath,
@@ -127,14 +62,13 @@ export async function startMockServerFromPath(
127
62
  fileLoader: async (p: string) => fs.readFileSync(p, 'utf-8'),
128
63
  }) :
129
64
  rawContent;
130
- parsed = yaml.load(processedContent);
131
65
  } catch (err: any) {
132
66
  throw new Error(`Mock server: YAML parse error in ${path.basename(filePath)}: ${err.message}`);
133
67
  }
134
68
 
135
- const {data, errors} = mockParsePack.parseMockData(parsed);
69
+ const {data, errors} = mockParsePack.loadMockFromYaml(processedContent);
136
70
  if (errors.length > 0 || !data) {
137
- const msg = errors.map((e: any) => e.message).join('; ');
71
+ const msg = errors.map((e: any) => e.message).join('; ') || 'Invalid mock server file';
138
72
  throw new Error(`Mock server validation errors in ${path.basename(filePath)}: ${msg}`);
139
73
  }
140
74
 
@@ -189,55 +123,30 @@ export async function startMockServerFromPath(
189
123
  let body = '';
190
124
  req.on('data', (chunk: Buffer) => { body += chunk; });
191
125
  req.on('end', async () => {
192
- let pathname = urlStr;
193
- const queryObj: Record<string, string> = {};
194
- const qIdx = urlStr.indexOf('?');
195
- if (qIdx >= 0) {
196
- pathname = urlStr.slice(0, qIdx);
197
- const searchParams = new URLSearchParams(urlStr.slice(qIdx + 1));
198
- searchParams.forEach((v, k) => { queryObj[k] = v; });
199
- }
200
-
201
- const parsedBody = mockServer.parseRequestBody(body, (req.headers || {}) as Record<string, string>);
202
-
203
- const mockReq = {
204
- method,
205
- path: pathname,
206
- headers: (req.headers || {}) as Record<string, string>,
207
- query: queryObj,
208
- body: parsedBody,
209
- };
210
-
211
- let mockRes: ReturnType<typeof router>;
126
+ let result: ReturnType<typeof dispatchMockHttpRequest>;
212
127
  try {
213
- mockRes = router(mockReq);
128
+ result = dispatchMockHttpRequest(router, {
129
+ method,
130
+ url: urlStr,
131
+ headers: (req.headers || {}) as Record<string, string>,
132
+ rawBody: body,
133
+ resolveHeaderToken: (v) => String(variableReplacer.resolveEmbeddedTokens(v, envVars)),
134
+ });
214
135
  } catch (err: any) {
215
136
  res.statusCode = 500;
216
137
  res.end(JSON.stringify({error: 'Mock router error', message: err.message}));
217
138
  return;
218
139
  }
219
140
 
220
- // Apply delay
221
- if (mockRes.delay && mockRes.delay > 0) {
222
- await new Promise<void>(resolve => setTimeout(resolve, mockRes.delay));
141
+ if (result.delay > 0) {
142
+ await new Promise<void>(resolve => setTimeout(resolve, result.delay));
223
143
  }
224
144
 
225
- // Resolve tokens in response headers per-request
226
- if (mockRes.headers) {
227
- for (const [k, v] of Object.entries(mockRes.headers)) {
228
- if (typeof v === 'string') {
229
- res.setHeader(k, String(variableReplacer.resolveEmbeddedTokens(v, envVars)));
230
- } else {
231
- res.setHeader(k, v);
232
- }
233
- }
145
+ res.statusCode = result.status;
146
+ for (const [k, v] of Object.entries(result.headers)) {
147
+ res.setHeader(k, v);
234
148
  }
235
-
236
- res.statusCode = mockRes.status;
237
- const responseBody = mockRes.body !== undefined ? (
238
- typeof mockRes.body === 'string' ? mockRes.body : JSON.stringify(mockRes.body)
239
- ) : '';
240
- res.end(responseBody);
149
+ res.end(result.body);
241
150
  });
242
151
  };
243
152
 
package/src/pkg-entry.cjs CHANGED
@@ -183,17 +183,47 @@ function createPkgJsRunner() {
183
183
  );
184
184
 
185
185
  // Wrap send_ with trace-level logging when requested
186
- const sendFn = traceSend ? async (req) => {
186
+ let formatHttpTraceRequest = (req) => {
187
187
  const reqSummary = req ? `${(req.method || 'GET').toUpperCase()} ${req.url || ''}` : 'unknown';
188
- lg('trace', `Request: ${reqSummary}`);
188
+ return `Request: ${reqSummary}`;
189
+ };
190
+ let formatHttpTraceResponse = (args) => {
191
+ if (args.error) {
192
+ return `Response: error - ${args.error}`;
193
+ }
194
+ const status = args.status ?? '?';
195
+ const duration = args.durationMs != null ? ` (${args.durationMs}ms)` : '';
196
+ return `Response: ${status}${duration}`;
197
+ };
198
+ try {
199
+ // eslint-disable-next-line global-require
200
+ const httpTraceLog = require('../../core/dist/httpTraceLog.js');
201
+ if (httpTraceLog && httpTraceLog.formatHttpTraceRequest) {
202
+ formatHttpTraceRequest = httpTraceLog.formatHttpTraceRequest;
203
+ formatHttpTraceResponse = httpTraceLog.formatHttpTraceResponse;
204
+ }
205
+ } catch { /* optional */ }
206
+ const sendFn = traceSend ? async (req) => {
207
+ lg('trace', formatHttpTraceRequest({
208
+ method: req && req.method,
209
+ url: req && req.url,
210
+ headers: req && req.headers,
211
+ query: req && req.query,
212
+ body: req && req.body,
213
+ }));
189
214
  try {
190
215
  const res = await networkCore.send(req);
191
- const status = res && typeof res.status === 'number' ? res.status : '?';
192
- const duration = res && typeof res.duration === 'number' ? ` (${res.duration}ms)` : '';
193
- lg('trace', `Response: ${status}${duration}`);
216
+ lg('trace', formatHttpTraceResponse({
217
+ status: res && typeof res.status === 'number' ? res.status : '?',
218
+ durationMs: res && typeof res.duration === 'number' ? res.duration : undefined,
219
+ headers: res && res.headers,
220
+ body: res && res.body,
221
+ }));
194
222
  return res;
195
223
  } catch (err) {
196
- lg('trace', `Response: error - ${err?.message || String(err)}`);
224
+ lg('trace', formatHttpTraceResponse({
225
+ error: err && err.message ? err.message : String(err),
226
+ }));
197
227
  throw err;
198
228
  }
199
229
  } : networkCore.send;