@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.
- package/dist/lib/MockRunner.d.ts +2 -0
- package/dist/lib/MockRunner.js +17 -7
- package/dist/lib/configHelper.js +108 -107
- package/dist/lib/validators/code-validator.d.ts +6 -0
- package/dist/lib/validators/code-validator.js +41 -5
- package/dist/test/BrowserRunner.test.js +1 -1
- package/dist/test/CodeValidator.test.js +322 -0
- package/dist/test/MockRunner.test.js +54 -0
- package/package.json +1 -1
package/dist/lib/MockRunner.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/lib/MockRunner.js
CHANGED
|
@@ -162,12 +162,10 @@ class MockRunner {
|
|
|
162
162
|
actionId,
|
|
163
163
|
sessionKeys: Object.keys(sessionData),
|
|
164
164
|
});
|
|
165
|
-
const step = this.
|
|
165
|
+
const step = this.resolveStep(baseActionId);
|
|
166
166
|
if (!step) {
|
|
167
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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;
|
package/dist/lib/configHelper.js
CHANGED
|
@@ -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
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
step.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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.
|
|
141
|
-
|
|
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
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
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("
|
|
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
|
});
|