@ondc/automation-mock-runner 1.3.53 → 1.3.55

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.
@@ -123,5 +123,7 @@ export declare class MockRunner {
123
123
  static encodeBase64(input: string): string;
124
124
  static decodeBase64(encoded: string): string;
125
125
  private static resolveBaseActionId;
126
+ private resolveStep;
127
+ private allActionIds;
126
128
  private static getIdFromSession;
127
129
  }
@@ -162,12 +162,10 @@ class MockRunner {
162
162
  actionId,
163
163
  sessionKeys: Object.keys(sessionData),
164
164
  });
165
- const step = this.config.steps.find((s) => s.action_id === baseActionId);
165
+ const step = this.resolveStep(baseActionId);
166
166
  if (!step) {
167
- const availableActions = this.config.steps.map((s) => s.action_id);
168
- throw new errors_1.ActionNotFoundError(actionId, availableActions);
167
+ throw new errors_1.ActionNotFoundError(actionId, this.allActionIds());
169
168
  }
170
- const index = this.config.steps.findIndex((s) => s.action_id === baseActionId);
171
169
  // Deep clone to avoid mutations
172
170
  const defaultPayload = JSON.parse(JSON.stringify(step.mock.defaultPayload));
173
171
  const context = this.generateContext(step.action_id, step.api, sessionData);
@@ -248,7 +246,7 @@ class MockRunner {
248
246
  async runValidatePayloadWithSession(actionId, targetPayload, sessionData) {
249
247
  try {
250
248
  const baseActionId = MockRunner.resolveBaseActionId(actionId);
251
- const step = this.config.steps.find((s) => s.action_id === baseActionId);
249
+ const step = this.resolveStep(baseActionId);
252
250
  if (!step) {
253
251
  throw new Error(`Action step with ID ${actionId} not found.`);
254
252
  }
@@ -300,7 +298,7 @@ class MockRunner {
300
298
  async runMeetRequirementsWithSession(actionId, sessionData) {
301
299
  try {
302
300
  const baseActionId = MockRunner.resolveBaseActionId(actionId);
303
- const step = this.config.steps.find((s) => s.action_id === baseActionId);
301
+ const step = this.resolveStep(baseActionId);
304
302
  if (!step) {
305
303
  throw new Error(`Action step with ID ${actionId} not found.`);
306
304
  }
@@ -423,7 +421,7 @@ class MockRunner {
423
421
  // GENERATED#1#on_search_full_page_gcr
424
422
  // get the last by splitting on # and taking the last part
425
423
  const baseActionId = MockRunner.resolveBaseActionId(actionId);
426
- const step = this.config.steps.find((s) => s.action_id === baseActionId);
424
+ const step = this.resolveStep(baseActionId);
427
425
  // Determine the message_id based on responseFor logic
428
426
  let messageId = (0, uuid_1.v4)();
429
427
  if (step?.responseFor) {
@@ -635,6 +633,18 @@ class MockRunner {
635
633
  static resolveBaseActionId(actionId) {
636
634
  return actionId.split("#").slice(-1)[0];
637
635
  }
636
+ // Resolve a step by base action id across main steps then extra steps.
637
+ resolveStep(baseActionId) {
638
+ return (this.config.steps.find((s) => s.action_id === baseActionId) ??
639
+ this.config.extra_steps?.steps.find((s) => s.action_id === baseActionId));
640
+ }
641
+ // All known action ids (main + extra), for error messages.
642
+ allActionIds() {
643
+ return [
644
+ ...this.config.steps.map((s) => s.action_id),
645
+ ...(this.config.extra_steps?.steps.map((s) => s.action_id) ?? []),
646
+ ];
647
+ }
638
648
  static getIdFromSession(sessionData, key) {
639
649
  if (sessionData === undefined) {
640
650
  return undefined;
@@ -40,117 +40,118 @@ function createInitialMockConfig(domain, version, flowId) {
40
40
  helperLib: MockRunner_1.MockRunner.encodeBase64(helpers_1.DEFAULT_HELPER_LIB),
41
41
  };
42
42
  }
43
- function convertToFlowConfig(config) {
44
- const flowConfig = {};
45
- flowConfig.id = config.meta.flowId;
46
- flowConfig.description = "";
47
- flowConfig.sequence = [];
48
- let index = 0;
49
- for (const step of config.steps) {
50
- const pair = config.steps.find((s) => s.responseFor === step.action_id)?.action_id ||
51
- null;
52
- let flowStep = {};
53
- const isFormStep = [
54
- "HTML_FORM",
55
- "DYNAMIC_FORM",
56
- "HTML_FORM_MULTI",
57
- "dynamic_form",
58
- "html_form",
59
- ];
60
- // Check if previous step was a form step
61
- const previousStep = index > 0 ? config.steps[index - 1] : null;
62
- const isPreviousStepForm = previousStep !== null && isFormStep.includes(previousStep.api);
63
- // Check if current step has no inputs
64
- const hasNoInputs = step.mock.inputs === undefined ||
65
- step.mock.inputs === null ||
66
- Object.keys(step.mock.inputs).length === 0;
67
- if (step.api === "dynamic_form") {
68
- flowStep = {
69
- key: step.action_id,
70
- type: "DYNAMIC_FORM",
71
- owner: step.owner,
72
- description: step.description || "",
73
- label: step.description || "FORM",
74
- unsolicited: step.unsolicited,
75
- pair: pair,
76
- repeat: step.repeatCount || 1,
77
- input: [
78
- {
79
- name: "form_submission_id",
80
- label: "Enter form submission ID",
81
- type: "DYNAMIC_FORM",
82
- payloadField: "form_submission_id",
83
- reference: `$.reference_data.${step.action_id}`,
84
- },
85
- ],
86
- };
87
- }
88
- else if (step.api === "HTML_FORM" || step.api === "html_form") {
89
- flowStep = {
90
- key: step.action_id,
91
- type: "HTML_FORM",
92
- owner: step.owner,
93
- description: step.description || "",
94
- label: step.description || "FORM",
95
- unsolicited: step.unsolicited,
96
- pair: pair,
97
- repeat: step.repeatCount || 1,
98
- input: [
99
- {
100
- name: "form_submission_id",
101
- label: "Enter form submission ID",
102
- type: "HTML_FORM",
103
- payloadField: "form_submission_id",
104
- reference: `$.reference_data.${step.action_id}`,
105
- },
106
- ],
107
- };
43
+ function buildFlowStep(step, index, steps) {
44
+ const pair = steps.find((s) => s.responseFor === step.action_id)?.action_id || null;
45
+ let flowStep = {};
46
+ const isFormStep = [
47
+ "HTML_FORM",
48
+ "DYNAMIC_FORM",
49
+ "HTML_FORM_MULTI",
50
+ "dynamic_form",
51
+ "html_form",
52
+ ];
53
+ // Check if previous step was a form step
54
+ const previousStep = index > 0 ? steps[index - 1] : null;
55
+ const isPreviousStepForm = previousStep !== null && isFormStep.includes(previousStep.api);
56
+ // Check if current step has no inputs
57
+ const hasNoInputs = step.mock.inputs === undefined ||
58
+ step.mock.inputs === null ||
59
+ Object.keys(step.mock.inputs).length === 0;
60
+ if (step.api === "dynamic_form") {
61
+ flowStep = {
62
+ key: step.action_id,
63
+ type: "DYNAMIC_FORM",
64
+ owner: step.owner,
65
+ description: step.description || "",
66
+ label: step.description || "FORM",
67
+ unsolicited: step.unsolicited,
68
+ pair: pair,
69
+ repeat: step.repeatCount || 1,
70
+ input: [
71
+ {
72
+ name: "form_submission_id",
73
+ label: "Enter form submission ID",
74
+ type: "DYNAMIC_FORM",
75
+ payloadField: "form_submission_id",
76
+ reference: `$.reference_data.${step.action_id}`,
77
+ },
78
+ ],
79
+ };
80
+ }
81
+ else if (step.api === "HTML_FORM" || step.api === "html_form") {
82
+ flowStep = {
83
+ key: step.action_id,
84
+ type: "HTML_FORM",
85
+ owner: step.owner,
86
+ description: step.description || "",
87
+ label: step.description || "FORM",
88
+ unsolicited: step.unsolicited,
89
+ pair: pair,
90
+ repeat: step.repeatCount || 1,
91
+ input: [
92
+ {
93
+ name: "form_submission_id",
94
+ label: "Enter form submission ID",
95
+ type: "HTML_FORM",
96
+ payloadField: "form_submission_id",
97
+ reference: `$.reference_data.${step.action_id}`,
98
+ },
99
+ ],
100
+ };
101
+ }
102
+ else {
103
+ flowStep = {
104
+ key: step.action_id,
105
+ type: step.api,
106
+ owner: step.owner,
107
+ description: step.description || "",
108
+ expect: index === 0 ? true : false,
109
+ unsolicited: step.unsolicited,
110
+ pair: pair,
111
+ repeat: step.repeatCount || 1,
112
+ };
113
+ }
114
+ if (step.mock.inputs !== undefined &&
115
+ step.mock.inputs !== null &&
116
+ Object.keys(step.mock.inputs).length > 0) {
117
+ if (step.mock.inputs.id == "finvu_verification") {
118
+ flowStep.input = [
119
+ {
120
+ name: "finvu_verification",
121
+ label: "Complete Account Aggregator Verification",
122
+ type: "FINVU_REDIRECT",
123
+ payloadField: "$.context.aa_consent_verified",
124
+ },
125
+ ];
108
126
  }
109
127
  else {
110
- flowStep = {
111
- key: step.action_id,
112
- type: step.api,
113
- owner: step.owner,
114
- description: step.description || "",
115
- expect: index === 0 ? true : false,
116
- unsolicited: step.unsolicited,
117
- pair: pair,
118
- repeat: step.repeatCount || 1,
119
- };
120
- }
121
- if (step.mock.inputs !== undefined &&
122
- step.mock.inputs !== null &&
123
- Object.keys(step.mock.inputs).length > 0) {
124
- if (step.mock.inputs.id == "finvu_verification") {
125
- flowStep.input = [
126
- {
127
- name: "finvu_verification",
128
- label: "Complete Account Aggregator Verification",
129
- type: "FINVU_REDIRECT",
130
- payloadField: "$.context.aa_consent_verified",
131
- },
132
- ];
133
- }
134
- else {
135
- flowStep.input = [
136
- {
137
- name: step.mock.inputs.id,
138
- type: step.mock.inputs.id,
139
- schema: step.mock.inputs.jsonSchema,
140
- },
141
- ];
142
- }
143
- }
144
- if (step.mock.inputs?.oldInputs) {
145
- flowStep.input = step.mock.inputs.oldInputs;
128
+ flowStep.input = [
129
+ {
130
+ name: step.mock.inputs.id,
131
+ type: step.mock.inputs.id,
132
+ schema: step.mock.inputs.jsonSchema,
133
+ },
134
+ ];
146
135
  }
147
- // Add force_proceed if previous step was a form and current step has no inputs
148
- if (isPreviousStepForm && hasNoInputs) {
149
- flowStep.force_proceed = true;
150
- }
151
- flowConfig.sequence.push(flowStep);
152
- index++;
153
136
  }
137
+ if (step.mock.inputs?.oldInputs) {
138
+ flowStep.input = step.mock.inputs.oldInputs;
139
+ }
140
+ // Add force_proceed if previous step was a form and current step has no inputs
141
+ if (isPreviousStepForm && hasNoInputs) {
142
+ flowStep.force_proceed = true;
143
+ }
144
+ return flowStep;
145
+ }
146
+ function buildFlowSequence(steps) {
147
+ return steps.map((step, index) => buildFlowStep(step, index, steps));
148
+ }
149
+ function convertToFlowConfig(config) {
150
+ const flowConfig = {};
151
+ flowConfig.id = config.meta.flowId;
152
+ flowConfig.description = "";
153
+ flowConfig.sequence = buildFlowSequence(config.steps);
154
+ flowConfig.extraSequence = buildFlowSequence(config.extra_steps?.steps || []);
154
155
  return flowConfig;
155
156
  }
156
157
  async function createOptimizedMockConfig(config) {
@@ -22,6 +22,12 @@ export declare class CodeValidator {
22
22
  * functions/arrows are also skipped (they aren't the contract).
23
23
  * Falls back to the whole AST when no named match is found.
24
24
  */
25
+ /**
26
+ * Returns the function name the schema's template wraps user code with
27
+ * (e.g. "generate", "validate", "meetsRequirements"). Returns undefined for
28
+ * raw-code schemas like `getSave` whose template is the identity function.
29
+ */
30
+ private static getExpectedDeclarationName;
25
31
  private static collectTopLevelReturns;
26
32
  private static validateReturnStructure;
27
33
  /**
@@ -137,14 +137,29 @@ class CodeValidator {
137
137
  dangerousPatterns.forEach((pattern) => {
138
138
  errors.push(`[Line ${pattern.line}] ${pattern.message}`);
139
139
  });
140
- // 5. Validate return type structure (for validate and meetsRequirements)
141
- if (schema.returnType.properties) {
140
+ // 5. Require the top-level function declaration the schema wraps with
141
+ // (skipped for raw-code schemas like `getSave` whose template doesn't
142
+ // define a named function). Without this, a typo'd name silently
143
+ // degrades to a misleading "Function should return an object..." warning.
144
+ const expectedName = this.getExpectedDeclarationName(schema);
145
+ const hasNamedDecl = expectedName
146
+ ? (ast.body || []).some((n) => n.type === "FunctionDeclaration" && n.id?.name === expectedName)
147
+ : true;
148
+ if (expectedName && !hasNamedDecl) {
149
+ errors.push(`Expected a top-level function declaration named '${expectedName}' (check for typos or a missing/wrapping function)`);
150
+ }
151
+ // 6. Validate return type structure (for validate and meetsRequirements).
152
+ // Skipped when the named declaration is missing — the message would
153
+ // duplicate the clearer error above.
154
+ if (schema.returnType.properties && hasNamedDecl) {
142
155
  const returnValidation = this.validateReturnStructure(ast, schema.returnType.properties, schema.name);
143
156
  errors.push(...returnValidation);
144
157
  }
145
- // 6. Check for best practices
146
- const practiceWarnings = this.checkBestPractices(ast, schema);
147
- warnings.push(...practiceWarnings);
158
+ // 7. Check for best practices (skipped when declaration is missing)
159
+ if (hasNamedDecl) {
160
+ const practiceWarnings = this.checkBestPractices(ast, schema);
161
+ warnings.push(...practiceWarnings);
162
+ }
148
163
  }
149
164
  catch (e) {
150
165
  // Syntax error during parsing
@@ -179,6 +194,27 @@ class CodeValidator {
179
194
  * functions/arrows are also skipped (they aren't the contract).
180
195
  * Falls back to the whole AST when no named match is found.
181
196
  */
197
+ /**
198
+ * Returns the function name the schema's template wraps user code with
199
+ * (e.g. "generate", "validate", "meetsRequirements"). Returns undefined for
200
+ * raw-code schemas like `getSave` whose template is the identity function.
201
+ */
202
+ static getExpectedDeclarationName(schema) {
203
+ try {
204
+ const ast = acorn.parse(schema.template(""), {
205
+ ecmaVersion: 2020,
206
+ sourceType: "script",
207
+ });
208
+ const body = Array.isArray(ast.body)
209
+ ? ast.body
210
+ : [];
211
+ const fn = body.find((n) => n.type === "FunctionDeclaration" && n.id?.name === schema.name);
212
+ return fn ? schema.name : undefined;
213
+ }
214
+ catch {
215
+ return undefined;
216
+ }
217
+ }
182
218
  static collectTopLevelReturns(ast, targetFnName) {
183
219
  const returns = [];
184
220
  const programBody = Array.isArray(ast.body)
@@ -117,7 +117,7 @@ describe("BrowserRunner", () => {
117
117
  const result = await browserRunner.execute(wrongFunctionCode, schema, args);
118
118
  expect(result.success).toBe(false);
119
119
  expect(result.error).toBeDefined();
120
- expect(result.error.message).toContain("generate is not defined");
120
+ expect(result.error.message).toContain("Expected a top-level function declaration named 'generate'");
121
121
  });
122
122
  it("should handle runtime errors in functions", async () => {
123
123
  const errorCode = `
@@ -124,3 +124,325 @@ describe("CodeValidator.validate — return structure", () => {
124
124
  expect(result.warnings.some((w) => w.includes("should return a value"))).toBe(true);
125
125
  });
126
126
  });
127
+ describe("CodeValidator.validate — meetsRequirements return structure", () => {
128
+ it("accepts an outer return with the full expected shape", () => {
129
+ const code = `
130
+ function meetsRequirements(sessionData) {
131
+ return { valid: true, code: 200, description: "Requirements met" };
132
+ }
133
+ `;
134
+ const result = code_validator_1.CodeValidator.validate(code, meetsRequirementsSchema);
135
+ expect(result.isValid).toBe(true);
136
+ expect(result.errors).toEqual([]);
137
+ });
138
+ it("ignores nested arrow helper returns inside meetsRequirements", () => {
139
+ const code = `
140
+ function meetsRequirements(sessionData) {
141
+ const ok = (x) => { return x.length > 0; };
142
+ const ids = (sessionData.items || []).filter(i => { return i.id; });
143
+ return { valid: ok("hi"), code: 200, description: "Requirements met" };
144
+ }
145
+ `;
146
+ const result = code_validator_1.CodeValidator.validate(code, meetsRequirementsSchema);
147
+ expect(result.isValid).toBe(true);
148
+ expect(result.errors).toEqual([]);
149
+ });
150
+ it("ignores nested function declaration returns inside meetsRequirements", () => {
151
+ const code = `
152
+ function meetsRequirements(sessionData) {
153
+ function buildMsg(x) { return "req: " + x; }
154
+ return { valid: false, code: 400, description: buildMsg("nope") };
155
+ }
156
+ `;
157
+ const result = code_validator_1.CodeValidator.validate(code, meetsRequirementsSchema);
158
+ expect(result.isValid).toBe(true);
159
+ expect(result.errors).toEqual([]);
160
+ });
161
+ it("flags missing 'description' on the outer return", () => {
162
+ const code = `
163
+ function meetsRequirements(sessionData) {
164
+ return { valid: true, code: 200 };
165
+ }
166
+ `;
167
+ const result = code_validator_1.CodeValidator.validate(code, meetsRequirementsSchema);
168
+ expect(result.isValid).toBe(false);
169
+ expect(result.errors.some((e) => e.includes("description"))).toBe(true);
170
+ });
171
+ it("flags missing 'valid' on the outer return", () => {
172
+ const code = `
173
+ function meetsRequirements(sessionData) {
174
+ return { code: 200, description: "ok" };
175
+ }
176
+ `;
177
+ const result = code_validator_1.CodeValidator.validate(code, meetsRequirementsSchema);
178
+ expect(result.isValid).toBe(false);
179
+ expect(result.errors.some((e) => e.includes("valid"))).toBe(true);
180
+ });
181
+ it("flags missing 'code' on the outer return", () => {
182
+ const code = `
183
+ function meetsRequirements(sessionData) {
184
+ return { valid: true, description: "ok" };
185
+ }
186
+ `;
187
+ const result = code_validator_1.CodeValidator.validate(code, meetsRequirementsSchema);
188
+ expect(result.isValid).toBe(false);
189
+ expect(result.errors.some((e) => e.includes("code"))).toBe(true);
190
+ });
191
+ it("flags an outer return that is not an object literal (boolean)", () => {
192
+ const code = `
193
+ function meetsRequirements(sessionData) {
194
+ return true;
195
+ }
196
+ `;
197
+ const result = code_validator_1.CodeValidator.validate(code, meetsRequirementsSchema);
198
+ expect(result.isValid).toBe(false);
199
+ expect(result.errors.some((e) => e.includes("Function should return an object literal"))).toBe(true);
200
+ });
201
+ it("flags an outer return that is not an object literal (string)", () => {
202
+ const code = `
203
+ function meetsRequirements(sessionData) {
204
+ return "ok";
205
+ }
206
+ `;
207
+ const result = code_validator_1.CodeValidator.validate(code, meetsRequirementsSchema);
208
+ expect(result.isValid).toBe(false);
209
+ expect(result.errors.some((e) => e.includes("Function should return an object literal"))).toBe(true);
210
+ });
211
+ it("accepts a minified conditional return: return c ? {...} : {...}", () => {
212
+ const code = `function meetsRequirements(s){return s.ok?{valid:!0,code:200,description:"Requirements met"}:{valid:!1,code:400,description:"not met"}}`;
213
+ const result = code_validator_1.CodeValidator.validate(code, meetsRequirementsSchema);
214
+ expect(result.errors).toEqual([]);
215
+ expect(result.isValid).toBe(true);
216
+ });
217
+ it("accepts multiple return paths inside if/else, both objects", () => {
218
+ const code = `
219
+ function meetsRequirements(sessionData) {
220
+ if (!sessionData) {
221
+ return { valid: false, code: 400, description: "no session" };
222
+ }
223
+ if (sessionData.bad) {
224
+ return { valid: false, code: 401, description: "bad" };
225
+ }
226
+ return { valid: true, code: 200, description: "Requirements met" };
227
+ }
228
+ `;
229
+ const result = code_validator_1.CodeValidator.validate(code, meetsRequirementsSchema);
230
+ expect(result.errors).toEqual([]);
231
+ expect(result.isValid).toBe(true);
232
+ });
233
+ it("flags when one of multiple return paths is missing a property", () => {
234
+ const code = `
235
+ function meetsRequirements(sessionData) {
236
+ if (!sessionData) {
237
+ return { valid: false, code: 400 };
238
+ }
239
+ return { valid: true, code: 200, description: "Requirements met" };
240
+ }
241
+ `;
242
+ const result = code_validator_1.CodeValidator.validate(code, meetsRequirementsSchema);
243
+ expect(result.isValid).toBe(false);
244
+ expect(result.errors.some((e) => e.includes("description"))).toBe(true);
245
+ });
246
+ it("flags when one of multiple return paths is not an object", () => {
247
+ const code = `
248
+ function meetsRequirements(sessionData) {
249
+ if (!sessionData) {
250
+ return false;
251
+ }
252
+ return { valid: true, code: 200, description: "Requirements met" };
253
+ }
254
+ `;
255
+ const result = code_validator_1.CodeValidator.validate(code, meetsRequirementsSchema);
256
+ expect(result.isValid).toBe(false);
257
+ expect(result.errors.some((e) => e.includes("Function should return an object literal"))).toBe(true);
258
+ });
259
+ it("flags unexpected/extra properties on the outer return", () => {
260
+ const code = `
261
+ function meetsRequirements(sessionData) {
262
+ return { valid: true, code: 200, description: "ok", extra: "nope" };
263
+ }
264
+ `;
265
+ const result = code_validator_1.CodeValidator.validate(code, meetsRequirementsSchema);
266
+ expect(result.isValid).toBe(false);
267
+ expect(result.errors.some((e) => e.includes("unexpected property"))).toBe(true);
268
+ });
269
+ it("does not warn 'should return a value' when meetsRequirements has a return", () => {
270
+ const code = `
271
+ function meetsRequirements(sessionData) {
272
+ return { valid: true, code: 200, description: "Requirements met" };
273
+ }
274
+ `;
275
+ const result = code_validator_1.CodeValidator.validate(code, meetsRequirementsSchema);
276
+ expect(result.warnings.some((w) => w.includes("should return a value"))).toBe(false);
277
+ });
278
+ it("warns when only a nested helper returns and meetsRequirements has no return", () => {
279
+ const code = `
280
+ function meetsRequirements(sessionData) {
281
+ function helper() { return 42; }
282
+ helper();
283
+ }
284
+ `;
285
+ const result = code_validator_1.CodeValidator.validate(code, meetsRequirementsSchema);
286
+ expect(result.warnings.some((w) => w.includes("should return a value"))).toBe(true);
287
+ });
288
+ it("ignores sibling top-level helper returns even when they return non-objects", () => {
289
+ const code = `
290
+ function helper() { return 123; }
291
+ function anotherHelper() { return "string"; }
292
+ function meetsRequirements(sessionData) {
293
+ const a = helper();
294
+ const b = anotherHelper();
295
+ return { valid: a > 0 && !!b, code: 200, description: "Requirements met" };
296
+ }
297
+ `;
298
+ const result = code_validator_1.CodeValidator.validate(code, meetsRequirementsSchema);
299
+ expect(result.errors).toEqual([]);
300
+ expect(result.warnings).toEqual([]);
301
+ expect(result.isValid).toBe(true);
302
+ });
303
+ it("ignores returns inside try/catch helpers nested in meetsRequirements", () => {
304
+ const code = `
305
+ function meetsRequirements(sessionData) {
306
+ const safe = (fn) => {
307
+ try { return fn(); } catch (e) { return null; }
308
+ };
309
+ const v = safe(() => sessionData.x);
310
+ return { valid: !!v, code: 200, description: "Requirements met" };
311
+ }
312
+ `;
313
+ const result = code_validator_1.CodeValidator.validate(code, meetsRequirementsSchema);
314
+ expect(result.isValid).toBe(true);
315
+ expect(result.errors).toEqual([]);
316
+ });
317
+ it("flags forbidden global usage inside meetsRequirements", () => {
318
+ const code = `
319
+ function meetsRequirements(sessionData) {
320
+ eval("1+1");
321
+ return { valid: true, code: 200, description: "Requirements met" };
322
+ }
323
+ `;
324
+ const result = code_validator_1.CodeValidator.validate(code, meetsRequirementsSchema);
325
+ expect(result.isValid).toBe(false);
326
+ expect(result.errors.some((e) => e.includes("eval"))).toBe(true);
327
+ });
328
+ it("flags infinite loops inside meetsRequirements", () => {
329
+ const code = `
330
+ function meetsRequirements(sessionData) {
331
+ while (true) { break; }
332
+ return { valid: true, code: 200, description: "Requirements met" };
333
+ }
334
+ `;
335
+ const result = code_validator_1.CodeValidator.validate(code, meetsRequirementsSchema);
336
+ expect(result.isValid).toBe(false);
337
+ expect(result.errors.some((e) => e.includes("infinite loop"))).toBe(true);
338
+ });
339
+ it("accepts return inside switch branches when all branches return objects", () => {
340
+ const code = `
341
+ function meetsRequirements(sessionData) {
342
+ switch (sessionData.kind) {
343
+ case "a":
344
+ return { valid: true, code: 200, description: "Requirements met" };
345
+ case "b":
346
+ return { valid: false, code: 400, description: "bad b" };
347
+ default:
348
+ return { valid: false, code: 400, description: "unknown" };
349
+ }
350
+ }
351
+ `;
352
+ const result = code_validator_1.CodeValidator.validate(code, meetsRequirementsSchema);
353
+ expect(result.errors).toEqual([]);
354
+ expect(result.isValid).toBe(true);
355
+ });
356
+ it("errors when the function name is misspelled (meetRequirements vs meetsRequirements)", () => {
357
+ const code = `
358
+ function meetRequirements(sessionData) {
359
+ if (!sessionData.selected_items) {
360
+ return { valid: false, code: "MISSING", description: "no items" };
361
+ }
362
+ return { valid: true, code: "200", description: "ok" };
363
+ }
364
+ `;
365
+ const result = code_validator_1.CodeValidator.validate(code, meetsRequirementsSchema);
366
+ expect(result.isValid).toBe(false);
367
+ expect(result.errors.some((e) => e.includes("Expected a top-level function declaration named 'meetsRequirements'"))).toBe(true);
368
+ // Suppresses the older misleading "should return an object with properties" message
369
+ expect(result.errors.some((e) => e.includes("Function should return an object with properties"))).toBe(false);
370
+ // Suppresses the "should return a value" warning too
371
+ expect(result.warnings.some((w) => w.includes("should return a value"))).toBe(false);
372
+ });
373
+ it("accepts return inside try/finally in meetsRequirements", () => {
374
+ const code = `
375
+ function meetsRequirements(sessionData) {
376
+ try {
377
+ return { valid: true, code: 200, description: "Requirements met" };
378
+ } catch (e) {
379
+ return { valid: false, code: 500, description: String(e) };
380
+ }
381
+ }
382
+ `;
383
+ const result = code_validator_1.CodeValidator.validate(code, meetsRequirementsSchema);
384
+ expect(result.errors).toEqual([]);
385
+ expect(result.isValid).toBe(true);
386
+ });
387
+ });
388
+ describe("CodeValidator.validate — required top-level declaration name", () => {
389
+ const getSaveSchema = (0, function_registry_1.getFunctionSchema)("getSave");
390
+ it("errors when 'validate' is misspelled (validatee)", () => {
391
+ const code = `
392
+ function validatee(targetPayload, sessionData) {
393
+ return { valid: true, code: 200, description: "Valid request" };
394
+ }
395
+ `;
396
+ const result = code_validator_1.CodeValidator.validate(code, validateSchema);
397
+ expect(result.isValid).toBe(false);
398
+ expect(result.errors.some((e) => e.includes("Expected a top-level function declaration named 'validate'"))).toBe(true);
399
+ });
400
+ it("errors when 'generate' is misspelled (genrate)", () => {
401
+ const code = `
402
+ async function genrate(defaultPayload, sessionData) {
403
+ return defaultPayload;
404
+ }
405
+ `;
406
+ const result = code_validator_1.CodeValidator.validate(code, generateSchema);
407
+ expect(result.isValid).toBe(false);
408
+ expect(result.errors.some((e) => e.includes("Expected a top-level function declaration named 'generate'"))).toBe(true);
409
+ });
410
+ it("accepts async generate (async function declaration counts)", () => {
411
+ const code = `
412
+ async function generate(defaultPayload, sessionData) {
413
+ return defaultPayload;
414
+ }
415
+ `;
416
+ const result = code_validator_1.CodeValidator.validate(code, generateSchema);
417
+ expect(result.errors).toEqual([]);
418
+ expect(result.isValid).toBe(true);
419
+ });
420
+ it("errors when function is defined as const arrow instead of declaration", () => {
421
+ const code = `
422
+ const validate = (targetPayload, sessionData) => {
423
+ return { valid: true, code: 200, description: "Valid request" };
424
+ };
425
+ `;
426
+ const result = code_validator_1.CodeValidator.validate(code, validateSchema);
427
+ expect(result.isValid).toBe(false);
428
+ expect(result.errors.some((e) => e.includes("Expected a top-level function declaration named 'validate'"))).toBe(true);
429
+ });
430
+ it("errors when only helpers are defined and the target function is missing entirely", () => {
431
+ const code = `
432
+ function helper() { return 1; }
433
+ `;
434
+ const result = code_validator_1.CodeValidator.validate(code, meetsRequirementsSchema);
435
+ expect(result.isValid).toBe(false);
436
+ expect(result.errors.some((e) => e.includes("Expected a top-level function declaration named 'meetsRequirements'"))).toBe(true);
437
+ });
438
+ it("does NOT require a function declaration for getSave (raw code allowed)", () => {
439
+ const code = `return payload.context.transaction_id;`;
440
+ const result = code_validator_1.CodeValidator.validate(code, getSaveSchema);
441
+ expect(result.errors.some((e) => e.includes("Expected a top-level function declaration"))).toBe(false);
442
+ });
443
+ it("accepts getSave wrapped in an IIFE (no named declaration needed)", () => {
444
+ const code = `const id = payload.context.transaction_id; return id;`;
445
+ const result = code_validator_1.CodeValidator.validate(code, getSaveSchema);
446
+ expect(result.errors.some((e) => e.includes("Expected a top-level function declaration"))).toBe(false);
447
+ });
448
+ });
@@ -719,4 +719,58 @@ describe("MockRunner", () => {
719
719
  expect(result.success).toBe(true);
720
720
  });
721
721
  });
722
+ describe("extra steps support in *WithSession methods", () => {
723
+ let runner;
724
+ beforeEach(async () => {
725
+ const baseConfig = {
726
+ meta: {
727
+ domain: "ONDC:TRV14",
728
+ version: "2.0.0",
729
+ flowId: "extra-steps-test",
730
+ },
731
+ transaction_data: {
732
+ transaction_id: "extra-steps-txn-id",
733
+ latest_timestamp: "1970-01-01T00:00:00.000Z",
734
+ },
735
+ steps: [],
736
+ transaction_history: [],
737
+ validationLib: "",
738
+ helperLib: "",
739
+ };
740
+ const base = new MockRunner_1.MockRunner(baseConfig, true);
741
+ base.getConfig().steps.push(base.getDefaultStep("search", "search_0"));
742
+ const optimized = await (0, configHelper_1.createOptimizedMockConfig)(base.getConfig());
743
+ // createOptimizedMockConfig drops extra_steps, so attach an extra step
744
+ // (absent from main steps) after optimizing the main steps.
745
+ optimized.extra_steps = {
746
+ steps: [base.getDefaultStep("on_status", "on_status_0")],
747
+ };
748
+ runner = new MockRunner_1.MockRunner(optimized, true);
749
+ });
750
+ it("runGeneratePayloadWithSession resolves a step from extra_steps", async () => {
751
+ const result = await runner.runGeneratePayloadWithSession("on_status_0", {
752
+ transaction_id: "some-txn",
753
+ });
754
+ expect(result.success).toBe(true);
755
+ });
756
+ it("runValidatePayloadWithSession resolves a step from extra_steps", async () => {
757
+ const result = await runner.runValidatePayloadWithSession("on_status_0", { context: {}, message: {} }, {});
758
+ expect(result.success).toBe(true);
759
+ });
760
+ it("runMeetRequirementsWithSession resolves a step from extra_steps", async () => {
761
+ const result = await runner.runMeetRequirementsWithSession("on_status_0", {});
762
+ expect(result.success).toBe(true);
763
+ });
764
+ it("main steps still resolve (main takes precedence over extras)", async () => {
765
+ const result = await runner.runGeneratePayloadWithSession("search_0", {
766
+ transaction_id: "some-txn",
767
+ });
768
+ expect(result.success).toBe(true);
769
+ });
770
+ it("an action id in neither main nor extra returns failure", async () => {
771
+ const result = await runner.runGeneratePayloadWithSession("not_a_real_action", {});
772
+ expect(result.success).toBe(false);
773
+ expect(result.error?.message).toContain("not_a_real_action");
774
+ });
775
+ });
722
776
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ondc/automation-mock-runner",
3
- "version": "1.3.53",
3
+ "version": "1.3.55",
4
4
  "description": "A TypeScript library for ONDC automation mock runner",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",