@ondc/automation-mock-runner 1.3.46 → 1.3.48
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/README.md
CHANGED
|
@@ -1,48 +1,23 @@
|
|
|
1
1
|
# @ondc/automation-mock-runner
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A TypeScript library for driving ONDC (Open Network for Digital Commerce) transaction flows end-to-end. It turns a **base64-encoded, sandboxed function config** into an executable multi-step flow, handling payload generation, response validation, session state, and (optionally) outbound HTTP — in either Node (worker_threads) or the browser (Web Workers).
|
|
4
4
|
|
|
5
|
-
## What
|
|
5
|
+
## What it does
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **Maintain session state** across the entire transaction flow
|
|
13
|
-
- **Execute code securely** using sandboxed Worker Threads with base64-encoded functions
|
|
14
|
-
|
|
15
|
-
The core concept is simple: define your transaction flow with base64-encoded functions, then let the runner handle payload generation, validation, and state management automatically in a secure environment.
|
|
7
|
+
- **Generate payloads** for every step of a flow, with `context` (domain, version, ids, timestamps, bap/bpp) produced automatically.
|
|
8
|
+
- **Validate responses** against custom per-step logic.
|
|
9
|
+
- **Check prerequisites** before a step runs.
|
|
10
|
+
- **Carry session state** across steps via JSONPath extraction of prior payloads.
|
|
11
|
+
- **Sandbox every function** so user-authored JS runs with a whitelisted set of globals and per-function timeouts, and cannot touch the host filesystem, network, or module system — except where the installing service has explicitly allowlisted outbound URLs for `generate`.
|
|
16
12
|
|
|
17
13
|
## Key Features
|
|
18
14
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
### 🔒 Secure Code Execution
|
|
26
|
-
|
|
27
|
-
- **Base64-encoded functions** for secure storage and transmission
|
|
28
|
-
- **Sandboxed Worker Threads** with isolated VM contexts
|
|
29
|
-
- **Complete function declarations** required (not just function bodies)
|
|
30
|
-
- Built-in timeout protection and error isolation
|
|
31
|
-
- Memory limits and resource monitoring
|
|
32
|
-
|
|
33
|
-
### ✅ Schema Validation
|
|
34
|
-
|
|
35
|
-
- Zod-based configuration validation with base64 string validation
|
|
36
|
-
- Runtime type checking for all inputs and outputs
|
|
37
|
-
- Detailed error reporting with line-by-line feedback
|
|
38
|
-
- JSON Schema validation for user inputs
|
|
39
|
-
|
|
40
|
-
### 🎯 ONDC-Specific Features
|
|
41
|
-
|
|
42
|
-
- Built-in support for BAP (Buyer App) and BPP (Seller App) roles
|
|
43
|
-
- Automatic message ID correlation for request-response pairs
|
|
44
|
-
- Version-aware context generation (supports ONDC v1.x and v2.x)
|
|
45
|
-
- Domain-specific helper utilities
|
|
15
|
+
- 🔄 Multi-step flow management with automatic `context` building and request/response correlation via `responseFor`.
|
|
16
|
+
- 🔒 Worker-thread (Node) / Web Worker (browser) sandbox. Whitelisted globals, per-function timeouts, workers recycled after 100 executions or 10 min.
|
|
17
|
+
- 🌐 Opt-in outbound `fetch` from `generate` with a per-installer origin+path allowlist. Redirects blocked (`redirect: "error"`).
|
|
18
|
+
- 📚 Built-in **default helper library** (`uuidv4`, `currentTimestamp`, `isoDurToSec`, `setCityFromInputs`, `createFormURL`, `generate6DigitId`, `getSubscriberUrl`, `generateConsentHandler`) prepended to every `generate`.
|
|
19
|
+
- ✅ Zod-based config validation, JSON Schema for user inputs.
|
|
20
|
+
- 🎯 ONDC-aware: version-aware context (v1.x flat `city`, v2.x nested `location.city.code`), BAP/BPP roles, form steps (`dynamic_form`, `html_form`, `HTML_FORM_MULTI`), dynamic action IDs (`GENERATED#n#action_id`).
|
|
46
21
|
|
|
47
22
|
## Installation
|
|
48
23
|
|
|
@@ -50,125 +25,89 @@ The core concept is simple: define your transaction flow with base64-encoded fun
|
|
|
50
25
|
npm install @ondc/automation-mock-runner
|
|
51
26
|
```
|
|
52
27
|
|
|
53
|
-
|
|
28
|
+
**Requires Node ≥ 18** (the sandbox uses native `fetch` and `AbortController`).
|
|
54
29
|
|
|
55
|
-
|
|
30
|
+
## Quick Start
|
|
56
31
|
|
|
57
32
|
```typescript
|
|
58
|
-
import {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
33
|
+
import {
|
|
34
|
+
MockRunner,
|
|
35
|
+
createInitialMockConfig,
|
|
36
|
+
} from "@ondc/automation-mock-runner";
|
|
37
|
+
|
|
38
|
+
// 1. Boot the shared runner once at service startup.
|
|
39
|
+
// Only needed if any `generate` function calls fetch().
|
|
40
|
+
MockRunner.initSharedRunner({
|
|
41
|
+
allowedFetchBaseUrls: ["https://dev-automation.ondc.org/finvu"],
|
|
42
|
+
});
|
|
65
43
|
|
|
66
|
-
//
|
|
67
|
-
const config:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
owner: "BAP",
|
|
86
|
-
responseFor: null,
|
|
87
|
-
unsolicited: false,
|
|
88
|
-
description: "Search for products in electronics category",
|
|
89
|
-
mock: {
|
|
90
|
-
generate: encodeFunction(`
|
|
91
|
-
async function generate(defaultPayload, sessionData) {
|
|
92
|
-
// Add search intent to the payload
|
|
93
|
-
defaultPayload.message = {
|
|
94
|
-
intent: {
|
|
95
|
-
category: { descriptor: { name: "Electronics" } },
|
|
96
|
-
location: { country: { code: "IND" }, city: { code: "std:080" } }
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
return defaultPayload;
|
|
100
|
-
}
|
|
101
|
-
`),
|
|
102
|
-
validate: encodeFunction(`
|
|
103
|
-
function validate(targetPayload, sessionData) {
|
|
104
|
-
if (!targetPayload.message?.catalog?.providers?.length) {
|
|
105
|
-
return { valid: false, code: 400, description: "No providers found" };
|
|
106
|
-
}
|
|
107
|
-
return { valid: true, code: 200, description: "Valid catalog response" };
|
|
108
|
-
}
|
|
109
|
-
`),
|
|
110
|
-
requirements: encodeFunction(`
|
|
111
|
-
function meetsRequirements(sessionData) {
|
|
112
|
-
return { valid: true, code: 200, description: "Ready to search" };
|
|
113
|
-
}
|
|
114
|
-
`),
|
|
115
|
-
defaultPayload: { context: {}, message: {} },
|
|
116
|
-
saveData: {
|
|
117
|
-
providers: "$.message.catalog.providers",
|
|
118
|
-
},
|
|
119
|
-
inputs: {
|
|
120
|
-
id: "search_inputs",
|
|
121
|
-
jsonSchema: {
|
|
122
|
-
type: "object",
|
|
123
|
-
properties: {
|
|
124
|
-
category: { type: "string", default: "Electronics" },
|
|
125
|
-
},
|
|
44
|
+
// 2. Scaffold a config (auto-fills `helperLib` with DEFAULT_HELPER_LIB).
|
|
45
|
+
const config = createInitialMockConfig("ONDC:RET11", "2.0.0", "search-flow");
|
|
46
|
+
|
|
47
|
+
// 3. Add a step. Every helper (uuidv4, currentTimestamp, setCityFromInputs, …)
|
|
48
|
+
// is already in scope inside `generate`.
|
|
49
|
+
config.steps.push({
|
|
50
|
+
api: "search",
|
|
51
|
+
action_id: "search_0",
|
|
52
|
+
owner: "BAP",
|
|
53
|
+
responseFor: null,
|
|
54
|
+
unsolicited: false,
|
|
55
|
+
description: "Search for electronics",
|
|
56
|
+
mock: {
|
|
57
|
+
generate: MockRunner.encodeBase64(`
|
|
58
|
+
async function generate(defaultPayload, sessionData) {
|
|
59
|
+
setCityFromInputs(defaultPayload, sessionData.user_inputs);
|
|
60
|
+
defaultPayload.message = {
|
|
61
|
+
intent: {
|
|
62
|
+
category: { descriptor: { name: "Electronics" } },
|
|
126
63
|
},
|
|
127
|
-
}
|
|
64
|
+
};
|
|
65
|
+
return defaultPayload;
|
|
66
|
+
}
|
|
67
|
+
`),
|
|
68
|
+
validate: MockRunner.encodeBase64(`
|
|
69
|
+
function validate(targetPayload, sessionData) {
|
|
70
|
+
if (!targetPayload.message?.catalog?.providers?.length) {
|
|
71
|
+
return { valid: false, code: 400, description: "No providers" };
|
|
72
|
+
}
|
|
73
|
+
return { valid: true, code: 200, description: "ok" };
|
|
74
|
+
}
|
|
75
|
+
`),
|
|
76
|
+
requirements: MockRunner.encodeBase64(`
|
|
77
|
+
function meetsRequirements(sessionData) {
|
|
78
|
+
return { valid: true, code: 200, description: "ready" };
|
|
79
|
+
}
|
|
80
|
+
`),
|
|
81
|
+
defaultPayload: { context: {}, message: {} },
|
|
82
|
+
saveData: { providers: "$.message.catalog.providers" },
|
|
83
|
+
inputs: {
|
|
84
|
+
id: "search_inputs",
|
|
85
|
+
jsonSchema: {
|
|
86
|
+
type: "object",
|
|
87
|
+
properties: { city_code: { type: "string" } },
|
|
88
|
+
required: ["city_code"],
|
|
128
89
|
},
|
|
129
90
|
},
|
|
130
|
-
|
|
131
|
-
transaction_history: [],
|
|
132
|
-
validationLib: encodeFunction(`
|
|
133
|
-
// Shared validation utilities
|
|
134
|
-
function validateONDCContext(context) {
|
|
135
|
-
return context && context.domain && context.action && context.message_id;
|
|
136
|
-
}
|
|
137
|
-
`),
|
|
138
|
-
helperLib: encodeFunction(`
|
|
139
|
-
// Shared helper functions
|
|
140
|
-
function generateMessageId() {
|
|
141
|
-
return crypto.randomUUID();
|
|
142
|
-
}
|
|
143
|
-
`),
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
// Initialize the runner
|
|
147
|
-
const runner = new MockRunner(config);
|
|
148
|
-
|
|
149
|
-
// Generate a search payload
|
|
150
|
-
const searchResult = await runner.runGeneratePayload("search_001", {
|
|
151
|
-
category: "Electronics",
|
|
91
|
+
},
|
|
152
92
|
});
|
|
153
|
-
console.log("Generated search payload:", searchResult.result);
|
|
154
93
|
|
|
155
|
-
//
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
);
|
|
160
|
-
console.log(
|
|
94
|
+
// 4. Run.
|
|
95
|
+
const runner = new MockRunner(config);
|
|
96
|
+
const out = await runner.runGeneratePayload("search_0", {
|
|
97
|
+
city_code: "std:080",
|
|
98
|
+
});
|
|
99
|
+
console.log(out.result);
|
|
161
100
|
```
|
|
162
101
|
|
|
163
102
|
## Configuration Structure
|
|
164
103
|
|
|
165
|
-
###
|
|
104
|
+
### Meta
|
|
166
105
|
|
|
167
106
|
```typescript
|
|
168
107
|
meta: {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
108
|
+
domain: string; // e.g. "ONDC:RET11"
|
|
109
|
+
version: string; // e.g. "1.2.0" or "2.0.0" — drives context shape
|
|
110
|
+
flowId: string;
|
|
172
111
|
}
|
|
173
112
|
```
|
|
174
113
|
|
|
@@ -176,360 +115,296 @@ meta: {
|
|
|
176
115
|
|
|
177
116
|
```typescript
|
|
178
117
|
transaction_data: {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
118
|
+
transaction_id: string;
|
|
119
|
+
latest_timestamp: string;
|
|
120
|
+
bap_id?: string;
|
|
121
|
+
bap_uri?: string;
|
|
122
|
+
bpp_id?: string;
|
|
123
|
+
bpp_uri?: string;
|
|
185
124
|
}
|
|
186
125
|
```
|
|
187
126
|
|
|
188
|
-
### Action
|
|
189
|
-
|
|
190
|
-
Each step represents one API call in your transaction flow:
|
|
127
|
+
### Action Step
|
|
191
128
|
|
|
192
129
|
```typescript
|
|
193
130
|
{
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
131
|
+
api: "search" | "select" | "init" | "confirm"
|
|
132
|
+
| "on_search" | "on_select" | /* … */
|
|
133
|
+
| "dynamic_form" | "html_form" | "HTML_FORM_MULTI",
|
|
134
|
+
action_id: string, // unique within flow
|
|
135
|
+
owner: "BAP" | "BPP",
|
|
136
|
+
responseFor: string | null, // pair this step with a request action_id
|
|
137
|
+
unsolicited: boolean,
|
|
138
|
+
description: string,
|
|
139
|
+
repeatCount?: number,
|
|
140
|
+
force_proceed?: boolean, // skip the "waiting for input" gate; see Form Steps
|
|
141
|
+
mock: {
|
|
142
|
+
generate: string, // base64 function
|
|
143
|
+
validate: string, // base64 function
|
|
144
|
+
requirements: string, // base64 function
|
|
145
|
+
defaultPayload: object,
|
|
146
|
+
saveData: Record<string, string>, // JSONPath map; supports APPEND# / EVAL# prefixes
|
|
147
|
+
inputs: object | {},
|
|
148
|
+
formHtml?: string, // base64 HTML for form steps
|
|
149
|
+
},
|
|
208
150
|
}
|
|
209
151
|
```
|
|
210
152
|
|
|
211
|
-
##
|
|
212
|
-
|
|
213
|
-
**IMPORTANT**: All mock functions must be:
|
|
153
|
+
## Base64 Function Requirements
|
|
214
154
|
|
|
215
|
-
|
|
216
|
-
- `generate` functions: `async function generate(defaultPayload, sessionData) { ... }`
|
|
217
|
-
- `validate` functions: `function validate(targetPayload, sessionData) { ... }`
|
|
218
|
-
- `requirements` functions: `function meetsRequirements(sessionData) { ... }`
|
|
155
|
+
All three user functions must be **complete declarations**:
|
|
219
156
|
|
|
220
|
-
|
|
157
|
+
```js
|
|
158
|
+
async function generate(defaultPayload, sessionData) {
|
|
159
|
+
/* … */ return defaultPayload;
|
|
160
|
+
}
|
|
161
|
+
function validate(targetPayload, sessionData) {
|
|
162
|
+
/* … */ return { valid, code, description };
|
|
163
|
+
}
|
|
164
|
+
function meetsRequirements(sessionData) {
|
|
165
|
+
/* … */ return { valid, code, description };
|
|
166
|
+
}
|
|
167
|
+
```
|
|
221
168
|
|
|
222
|
-
|
|
169
|
+
Encode with `MockRunner.encodeBase64(src)`. The runner decodes, prepends `DEFAULT_HELPER_LIB` (for `generate`), and executes inside the sandbox.
|
|
223
170
|
|
|
224
|
-
|
|
171
|
+
## API Reference
|
|
225
172
|
|
|
226
|
-
|
|
173
|
+
### `MockRunner.initSharedRunner(options?)`
|
|
227
174
|
|
|
228
|
-
|
|
175
|
+
Static. Configure the process-wide shared runner at boot. Replaces any existing runner (terminates the old one). Call **once before** constructing any `MockRunner`.
|
|
229
176
|
|
|
230
177
|
```typescript
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
return defaultPayload;
|
|
238
|
-
}
|
|
178
|
+
MockRunner.initSharedRunner({
|
|
179
|
+
allowedFetchBaseUrls: [
|
|
180
|
+
"https://aa.example.com/finvu-aa",
|
|
181
|
+
"https://api.example.com/v1",
|
|
182
|
+
],
|
|
183
|
+
});
|
|
239
184
|
```
|
|
240
185
|
|
|
241
|
-
|
|
186
|
+
Empty / omitted `allowedFetchBaseUrls` means `fetch` is not injected into the sandbox at all.
|
|
187
|
+
|
|
188
|
+
### `new MockRunner(config, skipValidation?)`
|
|
189
|
+
|
|
190
|
+
Validates the config (Zod) on construction unless `skipValidation: true`.
|
|
191
|
+
|
|
192
|
+
### `runGeneratePayload(actionId, inputs?, extraSessionData?)`
|
|
242
193
|
|
|
243
194
|
```typescript
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
// Must return validation result object
|
|
250
|
-
return {
|
|
251
|
-
valid: true,
|
|
252
|
-
code: 200,
|
|
253
|
-
description: "Validation passed",
|
|
254
|
-
};
|
|
255
|
-
}
|
|
195
|
+
await runner.runGeneratePayload(
|
|
196
|
+
"search_0",
|
|
197
|
+
{ city_code: "std:080" }, // → sessionData.user_inputs
|
|
198
|
+
{ finvuUrl: "https://aa.example.com" }, // shallow-merged into sessionData
|
|
199
|
+
);
|
|
256
200
|
```
|
|
257
201
|
|
|
258
|
-
|
|
202
|
+
### `runValidatePayload(actionId, targetPayload, extraSessionData?)`
|
|
259
203
|
|
|
260
204
|
```typescript
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
// Must return requirement check result
|
|
266
|
-
return {
|
|
267
|
-
valid: true,
|
|
268
|
-
code: 200,
|
|
269
|
-
description: "Requirements met",
|
|
270
|
-
};
|
|
271
|
-
}
|
|
205
|
+
await runner.runValidatePayload("on_search_0", incomingPayload, {
|
|
206
|
+
finvuUrl: "https://aa.example.com",
|
|
207
|
+
});
|
|
272
208
|
```
|
|
273
209
|
|
|
274
|
-
###
|
|
210
|
+
### `runMeetRequirements(actionId)`
|
|
275
211
|
|
|
276
212
|
```typescript
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
async function generate(defaultPayload, sessionData) {
|
|
280
|
-
// Your logic here
|
|
281
|
-
defaultPayload.message = {
|
|
282
|
-
intent: { category: { descriptor: { name: "Electronics" } } }
|
|
283
|
-
};
|
|
284
|
-
return defaultPayload;
|
|
285
|
-
}
|
|
286
|
-
`;
|
|
213
|
+
await runner.runMeetRequirements("select_0");
|
|
214
|
+
```
|
|
287
215
|
|
|
288
|
-
|
|
289
|
-
const encodedFunction = MockRunner.encodeBase64(generateFunction);
|
|
216
|
+
### With-session variants
|
|
290
217
|
|
|
291
|
-
|
|
292
|
-
const step = {
|
|
293
|
-
// ...other properties...
|
|
294
|
-
mock: {
|
|
295
|
-
generate: encodedFunction,
|
|
296
|
-
// ...other mock properties...
|
|
297
|
-
},
|
|
298
|
-
};
|
|
299
|
-
```
|
|
218
|
+
Skip the history-based session build and use a caller-supplied object:
|
|
300
219
|
|
|
301
|
-
|
|
220
|
+
- `runGeneratePayloadWithSession(actionId, sessionData)`
|
|
221
|
+
- `runValidatePayloadWithSession(actionId, targetPayload, sessionData)`
|
|
222
|
+
- `runMeetRequirementsWithSession(actionId, sessionData)`
|
|
302
223
|
|
|
303
|
-
###
|
|
224
|
+
### `getDefaultStep(api, actionId, formType?)`
|
|
304
225
|
|
|
305
|
-
|
|
306
|
-
const steps = [
|
|
307
|
-
{
|
|
308
|
-
api: "search",
|
|
309
|
-
action_id: "search_001",
|
|
310
|
-
// ... search configuration
|
|
311
|
-
mock: {
|
|
312
|
-
saveData: {
|
|
313
|
-
selectedProvider: "$.message.catalog.providers[0]",
|
|
314
|
-
},
|
|
315
|
-
// ...
|
|
316
|
-
},
|
|
317
|
-
},
|
|
318
|
-
{
|
|
319
|
-
api: "select",
|
|
320
|
-
action_id: "select_001",
|
|
321
|
-
// ... select configuration
|
|
322
|
-
mock: {
|
|
323
|
-
generate: `
|
|
324
|
-
// Use data from previous search step
|
|
325
|
-
const provider = sessionData.selectedProvider;
|
|
326
|
-
defaultPayload.message = {
|
|
327
|
-
order: {
|
|
328
|
-
provider: { id: provider.id },
|
|
329
|
-
items: [{ id: provider.items[0].id, quantity: { count: 1 } }]
|
|
330
|
-
}
|
|
331
|
-
};
|
|
332
|
-
return defaultPayload;
|
|
333
|
-
`,
|
|
334
|
-
// ...
|
|
335
|
-
},
|
|
336
|
-
},
|
|
337
|
-
];
|
|
338
|
-
```
|
|
226
|
+
Returns a scaffolded step with template functions already base64-encoded. Pass `formType: "dynamic_form" | "html_form"` for form scaffolds.
|
|
339
227
|
|
|
340
|
-
###
|
|
228
|
+
### `validateConfig()`
|
|
341
229
|
|
|
342
|
-
|
|
343
|
-
// Create complete validation function
|
|
344
|
-
const validateFunction = `
|
|
345
|
-
function validate(targetPayload, sessionData) {
|
|
346
|
-
// Check if order total matches expected amount
|
|
347
|
-
const expectedTotal = sessionData.calculatedTotal;
|
|
348
|
-
const actualTotal = targetPayload.message.order.quote.total;
|
|
349
|
-
|
|
350
|
-
if (Math.abs(expectedTotal - actualTotal) > 0.01) {
|
|
351
|
-
return {
|
|
352
|
-
valid: false,
|
|
353
|
-
code: 400,
|
|
354
|
-
description: \`Total mismatch: expected \${expectedTotal}, got \${actualTotal}\`
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
return { valid: true, code: 200, description: "Order total validated" };
|
|
359
|
-
}
|
|
360
|
-
`;
|
|
230
|
+
Re-validates the stored config; returns `{ success, errors? }`.
|
|
361
231
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
232
|
+
### Static utilities
|
|
233
|
+
|
|
234
|
+
- `MockRunner.encodeBase64(src)` / `decodeBase64(b64)` — work in both Node and browser (use `TextEncoder`/`TextDecoder`, not `Buffer`).
|
|
235
|
+
|
|
236
|
+
## Default Helpers
|
|
237
|
+
|
|
238
|
+
Every `generate` call is prefixed with `DEFAULT_HELPER_LIB` — these are always in scope:
|
|
239
|
+
|
|
240
|
+
| Helper | Purpose |
|
|
241
|
+
| ------------------------------------------------------ | ------------------------------------------------------------------------------- |
|
|
242
|
+
| `uuidv4()` | RFC 4122 v4 UUID. |
|
|
243
|
+
| `generate6DigitId()` | 6-digit numeric string in `[100000, 999999]`. |
|
|
244
|
+
| `currentTimestamp()` | ISO-8601 UTC timestamp. |
|
|
245
|
+
| `isoDurToSec(duration)` | ISO 8601 duration → seconds (0 on unparseable input). |
|
|
246
|
+
| `setCityFromInputs(payload, inputs)` | Writes `inputs.city_code` into `payload.context` (v1 flat / v2 nested). |
|
|
247
|
+
| `createFormURL(domain, formId, sessionData)` | Build a `/forms/<domain>/<formId>/?...` submission URL from session data. |
|
|
248
|
+
| `getSubscriberUrl(sessionData, type)` | `"bpp"` → `sessionData.bppUri`; anything else → `bapUri`. |
|
|
249
|
+
| `generateConsentHandler(sessionData, { custId, ... })` | POSTs to Finvu AA; 10s `AbortController` timeout. Needs `finvuUrl` + allowlist. |
|
|
250
|
+
|
|
251
|
+
Source: `src/lib/helpers/default-helpers.js`. Edit that file and run `npm run helpers:gen` to refresh the shipped bundle (also regenerated automatically by `npm run build` and `npm test`).
|
|
252
|
+
|
|
253
|
+
Helpers that need request-scope data (`getSubscriberUrl`, `createFormURL`, `generateConsentHandler`) take `sessionData` as an **explicit first parameter**. Free-variable references do not resolve inside the sandbox — helpers run at script scope, `sessionData` is only a parameter of `generate()`.
|
|
365
254
|
|
|
366
|
-
|
|
255
|
+
## 3rd-party HTTP from `generate`
|
|
256
|
+
|
|
257
|
+
Outbound HTTP is **opt-in and scoped**:
|
|
258
|
+
|
|
259
|
+
1. Only `generate` gets `fetch` (validate / meetsRequirements / getSave stay pure).
|
|
260
|
+
2. The installing service provides the allowlist at boot:
|
|
261
|
+
```typescript
|
|
262
|
+
MockRunner.initSharedRunner({
|
|
263
|
+
allowedFetchBaseUrls: ["https://finvu.example.com/aa"],
|
|
264
|
+
});
|
|
265
|
+
```
|
|
266
|
+
3. Matching rule: request `origin` must equal an entry's origin **and** the request path must be a strict segment-prefix of the entry's path. `/v1` matches `/v1` and `/v1/foo` but **not** `/v10/foo`.
|
|
267
|
+
4. Redirects are blocked (`redirect: "error"`) — call final URLs, don't rely on 3xx hops.
|
|
268
|
+
5. `AbortController` + `AbortSignal` are in the sandbox; use them for per-request timeouts.
|
|
269
|
+
|
|
270
|
+
### Worked example — Finvu consent via `generateConsentHandler`
|
|
367
271
|
|
|
368
272
|
```typescript
|
|
369
|
-
//
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
}
|
|
273
|
+
// boot
|
|
274
|
+
MockRunner.initSharedRunner({
|
|
275
|
+
allowedFetchBaseUrls: ["https://dev-automation.ondc.org/finvu"],
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// step's generate (base64-encoded)
|
|
279
|
+
const src = `
|
|
280
|
+
async function generate(defaultPayload, sessionData) {
|
|
281
|
+
const handle = await generateConsentHandler(sessionData, { custId: "1234" });
|
|
282
|
+
defaultPayload.message.consentHandle = handle;
|
|
283
|
+
return defaultPayload;
|
|
284
|
+
}
|
|
382
285
|
`;
|
|
383
286
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
inputs: {
|
|
389
|
-
id: "user_details",
|
|
390
|
-
jsonSchema: {
|
|
391
|
-
type: "object",
|
|
392
|
-
properties: {
|
|
393
|
-
email: { type: "string", format: "email" },
|
|
394
|
-
deliveryAddress: { type: "string", minLength: 10 },
|
|
395
|
-
},
|
|
396
|
-
required: ["email", "deliveryAddress"],
|
|
397
|
-
},
|
|
398
|
-
},
|
|
399
|
-
},
|
|
400
|
-
};
|
|
287
|
+
// caller passes finvuUrl through extraSessionData
|
|
288
|
+
await runner.runGeneratePayload("consent_0", inputs, {
|
|
289
|
+
finvuUrl: "https://dev-automation.ondc.org/finvu",
|
|
290
|
+
});
|
|
401
291
|
```
|
|
402
292
|
|
|
403
|
-
##
|
|
293
|
+
## Sandbox globals & limits
|
|
404
294
|
|
|
405
|
-
|
|
295
|
+
**Always available:** `Array, Boolean, Date, Error, JSON, Math, Number, Object, Promise, RegExp, String, Symbol, Map, Set, WeakMap, WeakSet, parseInt, parseFloat, isNaN, isFinite, encodeURI(Component), decodeURI(Component), setTimeout, clearTimeout, AbortController, AbortSignal, console.{log,error,warn,info}`.
|
|
406
296
|
|
|
407
|
-
|
|
297
|
+
**Added for `generate` only** (and only when an allowlist is configured): `fetch, URL, URLSearchParams, Headers, Request, Response`.
|
|
408
298
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
299
|
+
**Explicitly denied:** `require, process, global, globalThis, Buffer, __dirname, __filename, module, exports, eval, Function`.
|
|
300
|
+
|
|
301
|
+
**Timeouts** (from `src/lib/constants/function-registry.ts`):
|
|
302
|
+
|
|
303
|
+
| Function kind | Timeout |
|
|
304
|
+
| ------------------- | ------- |
|
|
305
|
+
| `generate` | 45 s |
|
|
306
|
+
| `validate` | 5 s |
|
|
307
|
+
| `meetsRequirements` | 3 s |
|
|
308
|
+
| `getSave` | 3 s |
|
|
309
|
+
|
|
310
|
+
`setTimeout` inside the sandbox is clamped to 1–45000 ms.
|
|
311
|
+
|
|
312
|
+
## Dynamic action IDs
|
|
412
313
|
|
|
413
|
-
|
|
314
|
+
Any `actionId` containing `#` is resolved by taking the last `#`-separated segment. So `"GENERATED#1#search_0"` and `"GENERATED#42#search_0"` both resolve to the step with `action_id: "search_0"`. Applies to all `run*` methods — useful when the same step repeats inside a flow.
|
|
414
315
|
|
|
415
|
-
|
|
416
|
-
|
|
316
|
+
## Session data extraction
|
|
317
|
+
|
|
318
|
+
Each step declares a `saveData` map of JSONPath expressions applied to the prior response payload. The compiled values land on `sessionData` for subsequent steps.
|
|
417
319
|
|
|
418
320
|
```typescript
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
321
|
+
saveData: {
|
|
322
|
+
providerId: "$.message.catalog.providers[0].id",
|
|
323
|
+
"APPEND#providerIds": "$.message.catalog.providers[*].id", // concat into array
|
|
324
|
+
customValue: "EVAL#<base64 of extractor>", // custom extractor
|
|
422
325
|
}
|
|
423
326
|
```
|
|
424
327
|
|
|
425
|
-
|
|
426
|
-
|
|
328
|
+
- `APPEND#key` — concatenates the JSONPath result into an existing array under `key` instead of overwriting.
|
|
329
|
+
- `EVAL#<base64>` — runs a sandboxed `getSave(payload)` function and stores its return value.
|
|
330
|
+
- Form steps (`dynamic_form`, `html_form`) auto-save under `sessionData.formData[action_id]` and also set `sessionData[action_id]` to the submission ID.
|
|
427
331
|
|
|
428
|
-
|
|
429
|
-
const result = await runner.runGeneratePayload("search_001", {
|
|
430
|
-
category: "books",
|
|
431
|
-
});
|
|
432
|
-
```
|
|
332
|
+
## Form steps
|
|
433
333
|
|
|
434
|
-
|
|
435
|
-
Validates an incoming payload against the specified action step.
|
|
334
|
+
Supported `api` values for forms: `dynamic_form`, `html_form`, `HTML_FORM_MULTI`, `FORM`.
|
|
436
335
|
|
|
437
|
-
|
|
438
|
-
const result = await runner.runValidatePayload(
|
|
439
|
-
"on_search_001",
|
|
440
|
-
responsePayload,
|
|
441
|
-
);
|
|
442
|
-
```
|
|
336
|
+
`force_proceed: true` on a step means "don't wait for user input". `convertToFlowConfig` sets this automatically when the previous step is a form step and the current step has no inputs.
|
|
443
337
|
|
|
444
|
-
|
|
445
|
-
Checks if prerequisites are met before proceeding with an action.
|
|
338
|
+
## Config builders
|
|
446
339
|
|
|
447
|
-
|
|
448
|
-
const result = await runner.runMeetRequirements("select_001", {});
|
|
449
|
-
```
|
|
340
|
+
From `@ondc/automation-mock-runner` (via `configHelper.ts`):
|
|
450
341
|
|
|
451
|
-
|
|
452
|
-
|
|
342
|
+
- `createInitialMockConfig(domain, version, flowId)` — scaffold with `DEFAULT_HELPER_LIB` pre-installed as `helperLib`.
|
|
343
|
+
- `generatePlaygroundConfigFromFlowConfig(payloads, flowConfig)` — reverse: seed a playground config from real ONDC traffic.
|
|
344
|
+
- `generatePlaygroundConfigFromFlowConfigWithMeta(payloads, flowConfig, domain, version)` — same, with explicit meta (useful when payloads are empty).
|
|
345
|
+
- `convertToFlowConfig(config)` — export a playground config to a Flow sequence.
|
|
346
|
+
- `createOptimizedMockConfig(config)` — Terser-minify each step's `generate` / `validate` / `requirements`.
|
|
347
|
+
- `validateConfigForDeployment(config)` — stricter pre-publish check (throws on problems).
|
|
453
348
|
|
|
454
|
-
|
|
455
|
-
const newStep = runner.getDefaultStep("search", "search_002");
|
|
456
|
-
// Returns a step with properly encoded template functions
|
|
457
|
-
```
|
|
349
|
+
## Error handling
|
|
458
350
|
|
|
459
|
-
|
|
460
|
-
Static utility to encode functions as base64.
|
|
351
|
+
Every `run*` method returns an `ExecutionResult`:
|
|
461
352
|
|
|
462
353
|
```typescript
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
354
|
+
const res = await runner.runGeneratePayload("search_0");
|
|
355
|
+
if (!res.success) {
|
|
356
|
+
console.log(res.error.name, res.error.message);
|
|
357
|
+
console.log(res.logs); // captured console output
|
|
358
|
+
console.log(res.executionTime, "ms");
|
|
359
|
+
}
|
|
469
360
|
```
|
|
470
361
|
|
|
471
|
-
|
|
472
|
-
Static utility to decode base64-encoded functions (used internally).
|
|
362
|
+
Common error names: `ActionNotFoundError`, `SessionDataError`, `ConfigurationError`, `PayloadGenerationError`, `PayloadValidationError`, `MeetRequirementsError`.
|
|
473
363
|
|
|
474
|
-
|
|
475
|
-
const decodedFunction = MockRunner.decodeBase64(encodedFunction);
|
|
476
|
-
```
|
|
364
|
+
## FAQ / common gotchas
|
|
477
365
|
|
|
478
|
-
|
|
366
|
+
**`DataCloneError: #<Promise> could not be cloned`** — your `generate` returned a payload containing an un-awaited Promise. Make `generate` `async` and `await` any async helper (including `generateConsentHandler`) before returning. Nested Promises inside the payload are not auto-flattened.
|
|
479
367
|
|
|
480
|
-
|
|
368
|
+
**`fetch blocked: <url> is not in the configured allowlist`** — add the origin+path to `MockRunner.initSharedRunner({ allowedFetchBaseUrls: [...] })`.
|
|
481
369
|
|
|
482
|
-
|
|
483
|
-
const result = await runner.runGeneratePayload("invalid_step", {});
|
|
370
|
+
**`fetch is not defined`** — you're calling it from `validate`, `meetsRequirements`, or `getSave`. Only `generate` gets `fetch`.
|
|
484
371
|
|
|
485
|
-
|
|
486
|
-
console.log("Error:", result.error.message);
|
|
487
|
-
console.log("Logs:", result.logs);
|
|
488
|
-
console.log("Execution time:", result.executionTime);
|
|
489
|
-
}
|
|
490
|
-
```
|
|
372
|
+
**Helper references `sessionData` but throws `ReferenceError`** — take `sessionData` as an explicit first parameter. Helpers don't share scope with `generate`.
|
|
491
373
|
|
|
492
|
-
|
|
374
|
+
**Edited `default-helpers.js` but the bundle didn't change** — run `npm run helpers:gen` (or `npm test` / `npm run build` — both regen automatically).
|
|
493
375
|
|
|
494
|
-
|
|
495
|
-
- **PayloadGenerationError**: Error in payload generation code
|
|
496
|
-
- **PayloadValidationError**: Error in validation code
|
|
497
|
-
- **MeetRequirementsError**: Error in requirements check code
|
|
498
|
-
- **TimeoutError**: Code execution exceeded timeout limit
|
|
376
|
+
**`validationLib` is not injected** — the field exists in the schema but is not currently prepended to any function at execution time. Treat as reserved.
|
|
499
377
|
|
|
500
|
-
|
|
378
|
+
**Execution timed out** — see the timeout table above. `generate` has the largest window (45 s) specifically for delayed-response mocking.
|
|
501
379
|
|
|
502
|
-
|
|
380
|
+
## Testing
|
|
503
381
|
|
|
504
382
|
```bash
|
|
505
|
-
npm test
|
|
506
|
-
npm run test:watch
|
|
507
|
-
npm run test:coverage
|
|
383
|
+
npm test # full suite (regens helpers via pretest)
|
|
384
|
+
npm run test:watch
|
|
385
|
+
npm run test:coverage
|
|
386
|
+
npm run test:browser-mock # BrowserRunner / CrossEnvironment tests only
|
|
508
387
|
```
|
|
509
388
|
|
|
510
|
-
## Security
|
|
389
|
+
## Security notes
|
|
511
390
|
|
|
512
|
-
-
|
|
513
|
-
-
|
|
514
|
-
-
|
|
515
|
-
-
|
|
516
|
-
-
|
|
517
|
-
- **Memory limits** prevent resource exhaustion
|
|
518
|
-
- **Input validation** using Zod schemas for all configuration data
|
|
391
|
+
- Base64 encoding prevents casual injection via config files.
|
|
392
|
+
- The sandbox blocks `eval`, `Function`, `require`, `process`, `Buffer`, filesystem, and (by default) network.
|
|
393
|
+
- Network access is per-installer opt-in and path-scoped.
|
|
394
|
+
- Redirects are refused to prevent allowlist bypass.
|
|
395
|
+
- Workers are recycled after 100 executions / 10 min to limit memory creep in the V8 isolate.
|
|
519
396
|
|
|
520
397
|
## Contributing
|
|
521
398
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
3. Add tests for new features
|
|
527
|
-
4. Update documentation for API changes
|
|
399
|
+
1. `npm test` — all tests green.
|
|
400
|
+
2. `npm run lint` / `npm run format` — code style.
|
|
401
|
+
3. `npm run type-check` — no TypeScript errors.
|
|
402
|
+
4. Add tests for new features; update README when public API changes.
|
|
528
403
|
|
|
529
404
|
## License
|
|
530
405
|
|
|
531
|
-
ISC
|
|
406
|
+
ISC — see LICENSE.
|
|
532
407
|
|
|
533
408
|
## Support
|
|
534
409
|
|
|
535
|
-
|
|
410
|
+
ONDC-specific questions: [ondc.org](https://ondc.org/). Library issues: file on the repository.
|
|
@@ -14,6 +14,12 @@ export declare class CodeValidator {
|
|
|
14
14
|
/**
|
|
15
15
|
* Validate that return statements match expected structure
|
|
16
16
|
*/
|
|
17
|
+
/**
|
|
18
|
+
* Collect ReturnStatement arguments belonging only to the outer (top-level)
|
|
19
|
+
* function. Nested function/arrow bodies are skipped — their returns are
|
|
20
|
+
* not the function's contract and would otherwise produce false positives.
|
|
21
|
+
*/
|
|
22
|
+
private static collectTopLevelReturns;
|
|
17
23
|
private static validateReturnStructure;
|
|
18
24
|
/**
|
|
19
25
|
* Security analysis - checks for forbidden functions and properties
|
|
@@ -171,16 +171,35 @@ class CodeValidator {
|
|
|
171
171
|
/**
|
|
172
172
|
* Validate that return statements match expected structure
|
|
173
173
|
*/
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
174
|
+
/**
|
|
175
|
+
* Collect ReturnStatement arguments belonging only to the outer (top-level)
|
|
176
|
+
* function. Nested function/arrow bodies are skipped — their returns are
|
|
177
|
+
* not the function's contract and would otherwise produce false positives.
|
|
178
|
+
*/
|
|
179
|
+
static collectTopLevelReturns(ast) {
|
|
180
|
+
const returns = [];
|
|
181
|
+
let depth = 0;
|
|
182
|
+
const enterFn = (node, _st, c) => {
|
|
183
|
+
if (depth === 0) {
|
|
184
|
+
depth++;
|
|
185
|
+
c(node.body, null);
|
|
186
|
+
depth--;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
walk.recursive(ast, null, {
|
|
190
|
+
FunctionDeclaration: enterFn,
|
|
191
|
+
FunctionExpression: enterFn,
|
|
192
|
+
ArrowFunctionExpression: enterFn,
|
|
178
193
|
ReturnStatement(node) {
|
|
179
|
-
if (node.argument)
|
|
180
|
-
|
|
181
|
-
}
|
|
194
|
+
if (node.argument)
|
|
195
|
+
returns.push(node.argument);
|
|
182
196
|
},
|
|
183
197
|
});
|
|
198
|
+
return returns;
|
|
199
|
+
}
|
|
200
|
+
static validateReturnStructure(ast, expectedProperties) {
|
|
201
|
+
const warnings = [];
|
|
202
|
+
const foundReturns = this.collectTopLevelReturns(ast);
|
|
184
203
|
// Check if we have return statements
|
|
185
204
|
if (foundReturns.length === 0) {
|
|
186
205
|
warnings.push(`Function should return an object with properties: ${Object.keys(expectedProperties).join(", ")}`);
|
|
@@ -295,12 +314,7 @@ class CodeValidator {
|
|
|
295
314
|
*/
|
|
296
315
|
static checkBestPractices(ast, schema) {
|
|
297
316
|
const warnings = [];
|
|
298
|
-
|
|
299
|
-
walk.simple(ast, {
|
|
300
|
-
ReturnStatement() {
|
|
301
|
-
hasReturn = true;
|
|
302
|
-
},
|
|
303
|
-
});
|
|
317
|
+
const hasReturn = this.collectTopLevelReturns(ast).length > 0;
|
|
304
318
|
if (!hasReturn) {
|
|
305
319
|
warnings.push(`Function should return a value (expected: ${schema.returnType.description})`);
|
|
306
320
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const code_validator_1 = require("../lib/validators/code-validator");
|
|
4
|
+
const function_registry_1 = require("../lib/constants/function-registry");
|
|
5
|
+
const validateSchema = (0, function_registry_1.getFunctionSchema)("validate");
|
|
6
|
+
describe("CodeValidator.validate — return structure", () => {
|
|
7
|
+
it("accepts an outer return with the full expected shape", () => {
|
|
8
|
+
const code = `
|
|
9
|
+
function validate(targetPayload, sessionData) {
|
|
10
|
+
return { valid: false, code: 200, description: "Valid request" };
|
|
11
|
+
}
|
|
12
|
+
`;
|
|
13
|
+
const result = code_validator_1.CodeValidator.validate(code, validateSchema);
|
|
14
|
+
expect(result.isValid).toBe(true);
|
|
15
|
+
expect(result.errors).toEqual([]);
|
|
16
|
+
});
|
|
17
|
+
it("ignores nested arrow helper returns (regression: false positive on nested non-object returns)", () => {
|
|
18
|
+
const code = `
|
|
19
|
+
function validate(targetPayload, sessionData) {
|
|
20
|
+
const ok = (x) => { return x.length > 0; };
|
|
21
|
+
const items = (targetPayload.items || []).filter(i => { return i.id; });
|
|
22
|
+
return { valid: ok("hi"), code: 200, description: "Valid request" };
|
|
23
|
+
}
|
|
24
|
+
`;
|
|
25
|
+
const result = code_validator_1.CodeValidator.validate(code, validateSchema);
|
|
26
|
+
expect(result.isValid).toBe(true);
|
|
27
|
+
expect(result.errors).toEqual([]);
|
|
28
|
+
});
|
|
29
|
+
it("ignores nested function declaration returns", () => {
|
|
30
|
+
const code = `
|
|
31
|
+
function validate(targetPayload, sessionData) {
|
|
32
|
+
function getMsg(x) { return "msg: " + x; }
|
|
33
|
+
return { valid: false, code: 200, description: getMsg("ok") };
|
|
34
|
+
}
|
|
35
|
+
`;
|
|
36
|
+
const result = code_validator_1.CodeValidator.validate(code, validateSchema);
|
|
37
|
+
expect(result.isValid).toBe(true);
|
|
38
|
+
expect(result.errors).toEqual([]);
|
|
39
|
+
});
|
|
40
|
+
it("flags missing properties on the outer return", () => {
|
|
41
|
+
const code = `
|
|
42
|
+
function validate(targetPayload, sessionData) {
|
|
43
|
+
return { valid: true, code: 200 };
|
|
44
|
+
}
|
|
45
|
+
`;
|
|
46
|
+
const result = code_validator_1.CodeValidator.validate(code, validateSchema);
|
|
47
|
+
expect(result.isValid).toBe(false);
|
|
48
|
+
expect(result.errors.some((e) => e.includes("description"))).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
it("flags an outer return that is not an object literal", () => {
|
|
51
|
+
const code = `
|
|
52
|
+
function validate(targetPayload, sessionData) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
`;
|
|
56
|
+
const result = code_validator_1.CodeValidator.validate(code, validateSchema);
|
|
57
|
+
expect(result.isValid).toBe(false);
|
|
58
|
+
expect(result.errors.some((e) => e.includes("Function should return an object literal"))).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
it("warns when only a nested helper returns and the outer function has no return", () => {
|
|
61
|
+
const code = `
|
|
62
|
+
function validate(targetPayload, sessionData) {
|
|
63
|
+
function helper() { return 42; }
|
|
64
|
+
helper();
|
|
65
|
+
}
|
|
66
|
+
`;
|
|
67
|
+
const result = code_validator_1.CodeValidator.validate(code, validateSchema);
|
|
68
|
+
expect(result.warnings.some((w) => w.includes("should return a value"))).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
});
|