@ondc/automation-mock-runner 1.3.52 → 1.3.54
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.js +10 -2
- 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/package.json +1 -1
package/dist/lib/MockRunner.js
CHANGED
|
@@ -617,11 +617,19 @@ class MockRunner {
|
|
|
617
617
|
}
|
|
618
618
|
static encodeBase64(input) {
|
|
619
619
|
const bytes = new TextEncoder().encode(input);
|
|
620
|
-
|
|
620
|
+
const CHUNK = 0x8000;
|
|
621
|
+
let binary = "";
|
|
622
|
+
for (let i = 0; i < bytes.length; i += CHUNK) {
|
|
623
|
+
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + CHUNK));
|
|
624
|
+
}
|
|
625
|
+
return btoa(binary);
|
|
621
626
|
}
|
|
622
627
|
static decodeBase64(encoded) {
|
|
623
628
|
const binaryString = atob(encoded);
|
|
624
|
-
const bytes = new Uint8Array(
|
|
629
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
630
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
631
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
632
|
+
}
|
|
625
633
|
return new TextDecoder().decode(bytes);
|
|
626
634
|
}
|
|
627
635
|
static resolveBaseActionId(actionId) {
|
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
|
+
});
|