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.
- package/dist/cli.js +57397 -74669
- package/dist/guides/golden-smoke.md +3 -3
- package/esbuild.mjs +2 -0
- package/package.json +1 -1
- package/src/mockRunner.ts +25 -116
- package/src/pkg-entry.cjs +36 -6
|
@@ -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
package/package.json
CHANGED
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
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
54
|
+
let processedContent = rawContent;
|
|
120
55
|
try {
|
|
121
56
|
const processor = (mmtcore as any).dataImportProcessor;
|
|
122
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
221
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
186
|
+
let formatHttpTraceRequest = (req) => {
|
|
187
187
|
const reqSummary = req ? `${(req.method || 'GET').toUpperCase()} ${req.url || ''}` : 'unknown';
|
|
188
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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',
|
|
224
|
+
lg('trace', formatHttpTraceResponse({
|
|
225
|
+
error: err && err.message ? err.message : String(err),
|
|
226
|
+
}));
|
|
197
227
|
throw err;
|
|
198
228
|
}
|
|
199
229
|
} : networkCore.send;
|