@optimizely/ocp-cli 1.2.13 → 1.2.14
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/commands/app/Init.js +1 -1
- package/dist/commands/app/Init.js.map +1 -1
- package/dist/oo-cli.manifest.json +1 -1
- package/package.json +10 -6
- package/src/commands/app/Init.ts +1 -1
- package/src/test/e2e/__tests__/accounts/accounts.test.ts +120 -0
- package/src/test/e2e/__tests__/availability/availability.test.ts +156 -0
- package/src/test/e2e/__tests__/directory/directory.test.ts +668 -0
- package/src/test/e2e/__tests__/jobs/jobs.test.ts +487 -0
- package/src/test/e2e/__tests__/review/review.test.ts +355 -0
- package/src/test/e2e/config/fixture-loader.ts +130 -0
- package/src/test/e2e/config/setup.ts +29 -0
- package/src/test/e2e/config/test-data-config.ts +27 -0
- package/src/test/e2e/config/test-data-helpers.ts +23 -0
- package/src/test/e2e/fixtures/baselines/accounts/whoami.txt +11 -0
- package/src/test/e2e/fixtures/baselines/accounts/whois.txt +4 -0
- package/src/test/e2e/fixtures/baselines/directory/info.txt +7 -0
- package/src/test/e2e/fixtures/baselines/directory/list.txt +4 -0
- package/src/test/e2e/fixtures/baselines/jobs/list.txt +4 -0
- package/src/test/e2e/fixtures/baselines/review/list.txt +4 -0
- package/src/test/e2e/lib/base-test.ts +150 -0
- package/src/test/e2e/lib/command-discovery.ts +324 -0
- package/src/test/e2e/utils/baseline-normalizer.ts +79 -0
- package/src/test/e2e/utils/cli-executor.ts +349 -0
- package/src/test/e2e/utils/command-registry.ts +99 -0
- package/src/test/e2e/utils/output-validator.ts +661 -0
- package/src/test/setup.ts +3 -1
- package/src/test/tsconfig.json +17 -0
- package/dist/test/setup.d.ts +0 -0
- package/dist/test/setup.js +0 -4
- package/dist/test/setup.js.map +0 -1
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
import { CLIExecutionResult } from './cli-executor';
|
|
2
|
+
|
|
3
|
+
export enum OutputType {
|
|
4
|
+
JSON = 'json',
|
|
5
|
+
YAML = 'yaml',
|
|
6
|
+
TABLE = 'table',
|
|
7
|
+
PLAIN_TEXT = 'plain_text'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ValidationResult {
|
|
11
|
+
isValid: boolean;
|
|
12
|
+
errors: string[];
|
|
13
|
+
warnings: string[];
|
|
14
|
+
outputType: OutputType;
|
|
15
|
+
metadata?: {
|
|
16
|
+
lineCount?: number;
|
|
17
|
+
columnCount?: number;
|
|
18
|
+
jsonKeys?: string[];
|
|
19
|
+
yamlKeys?: string[];
|
|
20
|
+
tableHeaders?: string[];
|
|
21
|
+
detectedPatterns?: string[];
|
|
22
|
+
exitCode?: number;
|
|
23
|
+
executionTime?: number;
|
|
24
|
+
timedOut?: boolean;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SchemaValidationRule {
|
|
29
|
+
path: string;
|
|
30
|
+
type: 'string' | 'number' | 'boolean' | 'array' | 'object';
|
|
31
|
+
required?: boolean;
|
|
32
|
+
pattern?: RegExp;
|
|
33
|
+
minLength?: number;
|
|
34
|
+
maxLength?: number;
|
|
35
|
+
min?: number;
|
|
36
|
+
max?: number;
|
|
37
|
+
enum?: any[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface TextPattern {
|
|
41
|
+
name: string;
|
|
42
|
+
pattern: RegExp;
|
|
43
|
+
required?: boolean;
|
|
44
|
+
description?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface TableValidationOptions {
|
|
48
|
+
expectedColumns?: string[];
|
|
49
|
+
minRows?: number;
|
|
50
|
+
maxRows?: number;
|
|
51
|
+
columnSeparator?: string | RegExp;
|
|
52
|
+
headerRequired?: boolean;
|
|
53
|
+
allowEmptyRows?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface JsonValidationOptions {
|
|
57
|
+
schema?: SchemaValidationRule[];
|
|
58
|
+
strictMode?: boolean;
|
|
59
|
+
allowAdditionalProperties?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface YamlValidationOptions {
|
|
63
|
+
schema?: SchemaValidationRule[];
|
|
64
|
+
strictMode?: boolean;
|
|
65
|
+
allowMultipleDocuments?: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface TextValidationOptions {
|
|
69
|
+
patterns?: TextPattern[];
|
|
70
|
+
minLines?: number;
|
|
71
|
+
maxLines?: number;
|
|
72
|
+
encoding?: string;
|
|
73
|
+
allowEmptyOutput?: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class OutputValidator {
|
|
77
|
+
/**
|
|
78
|
+
* Detect the type of output with improved accuracy
|
|
79
|
+
*/
|
|
80
|
+
static detectOutputType(output: string): OutputType {
|
|
81
|
+
const trimmed = output.trim();
|
|
82
|
+
|
|
83
|
+
if (!trimmed) {
|
|
84
|
+
return OutputType.PLAIN_TEXT;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check for JSON first (most specific)
|
|
88
|
+
if (this.isValidJson(trimmed)) {
|
|
89
|
+
return OutputType.JSON;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check for YAML (improved detection)
|
|
93
|
+
if (this.looksLikeYaml(trimmed)) {
|
|
94
|
+
return OutputType.YAML;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check for table format (improved detection)
|
|
98
|
+
if (this.looksLikeTable(trimmed)) {
|
|
99
|
+
return OutputType.TABLE;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return OutputType.PLAIN_TEXT;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Comprehensive output validation with detailed results
|
|
107
|
+
*/
|
|
108
|
+
static validateOutput(
|
|
109
|
+
output: string,
|
|
110
|
+
expectedType?: OutputType,
|
|
111
|
+
options?: JsonValidationOptions | YamlValidationOptions | TableValidationOptions | TextValidationOptions
|
|
112
|
+
): ValidationResult {
|
|
113
|
+
const detectedType = this.detectOutputType(output);
|
|
114
|
+
|
|
115
|
+
// Check if detected type matches expected type
|
|
116
|
+
if (expectedType && detectedType !== expectedType) {
|
|
117
|
+
return {
|
|
118
|
+
isValid: false,
|
|
119
|
+
errors: [`Expected ${expectedType} output, but detected ${detectedType}`],
|
|
120
|
+
warnings: [],
|
|
121
|
+
outputType: detectedType
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Validate based on detected type
|
|
126
|
+
switch (detectedType) {
|
|
127
|
+
case OutputType.JSON:
|
|
128
|
+
return this.validateJson(output, options as JsonValidationOptions);
|
|
129
|
+
case OutputType.YAML:
|
|
130
|
+
return this.validateYaml(output, options as YamlValidationOptions);
|
|
131
|
+
case OutputType.TABLE:
|
|
132
|
+
return this.validateTable(output, options as TableValidationOptions);
|
|
133
|
+
case OutputType.PLAIN_TEXT:
|
|
134
|
+
return this.validatePlainText(output, options as TextValidationOptions);
|
|
135
|
+
default:
|
|
136
|
+
return {
|
|
137
|
+
isValid: false,
|
|
138
|
+
errors: [`Unknown output type: ${detectedType}`],
|
|
139
|
+
warnings: [],
|
|
140
|
+
outputType: detectedType
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Enhanced JSON validation with schema support
|
|
147
|
+
*/
|
|
148
|
+
static validateJson(output: string, options: JsonValidationOptions = {}): ValidationResult {
|
|
149
|
+
const errors: string[] = [];
|
|
150
|
+
const warnings: string[] = [];
|
|
151
|
+
const metadata: any = {};
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const parsed = JSON.parse(output);
|
|
155
|
+
|
|
156
|
+
// Extract JSON keys for metadata
|
|
157
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
158
|
+
metadata.jsonKeys = this.extractJsonKeys(parsed);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Schema validation if provided
|
|
162
|
+
if (options.schema) {
|
|
163
|
+
const schemaErrors = this.validateJsonSchema(parsed, options.schema);
|
|
164
|
+
errors.push(...schemaErrors);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Check for additional properties if strict mode
|
|
168
|
+
if (options.strictMode && !options.allowAdditionalProperties && options.schema) {
|
|
169
|
+
const additionalProps = this.findAdditionalProperties(parsed, options.schema);
|
|
170
|
+
if (additionalProps.length > 0) {
|
|
171
|
+
warnings.push(`Additional properties found: ${additionalProps.join(', ')}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
isValid: errors.length === 0,
|
|
177
|
+
errors,
|
|
178
|
+
warnings,
|
|
179
|
+
outputType: OutputType.JSON,
|
|
180
|
+
metadata
|
|
181
|
+
};
|
|
182
|
+
} catch (error) {
|
|
183
|
+
return {
|
|
184
|
+
isValid: false,
|
|
185
|
+
errors: [`Invalid JSON: ${error instanceof Error ? error.message : String(error)}`],
|
|
186
|
+
warnings: [],
|
|
187
|
+
outputType: OutputType.JSON
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* YAML validation with schema support
|
|
194
|
+
*/
|
|
195
|
+
static validateYaml(output: string, options: YamlValidationOptions = {}): ValidationResult {
|
|
196
|
+
const errors: string[] = [];
|
|
197
|
+
const warnings: string[] = [];
|
|
198
|
+
const metadata: any = {};
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
// Basic YAML parsing (simplified - would need yaml library for full support)
|
|
202
|
+
const yamlKeys = this.extractYamlKeys(output);
|
|
203
|
+
metadata.yamlKeys = yamlKeys;
|
|
204
|
+
|
|
205
|
+
// Check for multiple documents
|
|
206
|
+
const documentCount = (output.match(/^---/gm) || []).length;
|
|
207
|
+
if (documentCount > 1 && !options.allowMultipleDocuments) {
|
|
208
|
+
warnings.push(`Multiple YAML documents found (${documentCount})`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Additional YAML-specific warnings could be added here
|
|
212
|
+
if (options.strictMode && yamlKeys.length === 0) {
|
|
213
|
+
warnings.push('No YAML keys detected in strict mode');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Basic structure validation
|
|
217
|
+
if (yamlKeys.length === 0) {
|
|
218
|
+
errors.push('No YAML keys found in output');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
isValid: errors.length === 0,
|
|
223
|
+
errors,
|
|
224
|
+
warnings,
|
|
225
|
+
outputType: OutputType.YAML,
|
|
226
|
+
metadata
|
|
227
|
+
};
|
|
228
|
+
} catch (error) {
|
|
229
|
+
return {
|
|
230
|
+
isValid: false,
|
|
231
|
+
errors: [`Invalid YAML: ${error instanceof Error ? error.message : String(error)}`],
|
|
232
|
+
warnings: [],
|
|
233
|
+
outputType: OutputType.YAML
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Enhanced table validation with comprehensive options
|
|
240
|
+
*/
|
|
241
|
+
static validateTable(output: string, options: TableValidationOptions = {}): ValidationResult {
|
|
242
|
+
const errors: string[] = [];
|
|
243
|
+
const warnings: string[] = [];
|
|
244
|
+
const metadata: any = {};
|
|
245
|
+
|
|
246
|
+
const lines = output.split('\n');
|
|
247
|
+
const nonEmptyLines = lines.filter(line => line.trim());
|
|
248
|
+
|
|
249
|
+
metadata.lineCount = lines.length;
|
|
250
|
+
|
|
251
|
+
if (nonEmptyLines.length === 0) {
|
|
252
|
+
errors.push('Empty table output');
|
|
253
|
+
return {
|
|
254
|
+
isValid: false,
|
|
255
|
+
errors,
|
|
256
|
+
warnings,
|
|
257
|
+
outputType: OutputType.TABLE,
|
|
258
|
+
metadata
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Detect table structure
|
|
263
|
+
const tableInfo = this.analyzeTableStructure(output, options.columnSeparator);
|
|
264
|
+
metadata.columnCount = tableInfo.columnCount;
|
|
265
|
+
metadata.tableHeaders = tableInfo.headers;
|
|
266
|
+
|
|
267
|
+
// Validate row count
|
|
268
|
+
if (options.minRows && tableInfo.dataRows < options.minRows) {
|
|
269
|
+
errors.push(`Expected at least ${options.minRows} data rows, found ${tableInfo.dataRows}`);
|
|
270
|
+
}
|
|
271
|
+
if (options.maxRows && tableInfo.dataRows > options.maxRows) {
|
|
272
|
+
errors.push(`Expected at most ${options.maxRows} data rows, found ${tableInfo.dataRows}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Validate headers
|
|
276
|
+
if (options.headerRequired !== false && !tableInfo.hasHeader) {
|
|
277
|
+
errors.push('Table header is required but not found');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Check for expected columns
|
|
281
|
+
if (options.expectedColumns && tableInfo.headers) {
|
|
282
|
+
const missingColumns = options.expectedColumns.filter(col =>
|
|
283
|
+
!tableInfo.headers!.some(header => header.toLowerCase().includes(col.toLowerCase()))
|
|
284
|
+
);
|
|
285
|
+
if (missingColumns.length > 0) {
|
|
286
|
+
errors.push(`Missing expected columns: ${missingColumns.join(', ')}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Check for empty rows
|
|
291
|
+
if (!options.allowEmptyRows && tableInfo.emptyRows > 0) {
|
|
292
|
+
warnings.push(`Found ${tableInfo.emptyRows} empty rows in table`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
isValid: errors.length === 0,
|
|
297
|
+
errors,
|
|
298
|
+
warnings,
|
|
299
|
+
outputType: OutputType.TABLE,
|
|
300
|
+
metadata
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Plain text validation with pattern matching
|
|
306
|
+
*/
|
|
307
|
+
static validatePlainText(output: string, options: TextValidationOptions = {}): ValidationResult {
|
|
308
|
+
const errors: string[] = [];
|
|
309
|
+
const warnings: string[] = [];
|
|
310
|
+
const metadata: any = {};
|
|
311
|
+
|
|
312
|
+
const lines = output.split('\n');
|
|
313
|
+
metadata.lineCount = lines.length;
|
|
314
|
+
|
|
315
|
+
// Check if empty output is allowed
|
|
316
|
+
if (!output.trim() && !options.allowEmptyOutput) {
|
|
317
|
+
errors.push('Empty output not allowed');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Validate line count
|
|
321
|
+
if (options.minLines && lines.length < options.minLines) {
|
|
322
|
+
errors.push(`Expected at least ${options.minLines} lines, found ${lines.length}`);
|
|
323
|
+
}
|
|
324
|
+
if (options.maxLines && lines.length > options.maxLines) {
|
|
325
|
+
errors.push(`Expected at most ${options.maxLines} lines, found ${lines.length}`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Pattern matching
|
|
329
|
+
if (options.patterns) {
|
|
330
|
+
const detectedPatterns: string[] = [];
|
|
331
|
+
for (const pattern of options.patterns) {
|
|
332
|
+
const matches = output.match(pattern.pattern);
|
|
333
|
+
if (matches) {
|
|
334
|
+
detectedPatterns.push(pattern.name);
|
|
335
|
+
} else if (pattern.required) {
|
|
336
|
+
errors.push(`Required pattern '${pattern.name}' not found: ${pattern.description || pattern.pattern.toString()}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
metadata.detectedPatterns = detectedPatterns;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
isValid: errors.length === 0,
|
|
344
|
+
errors,
|
|
345
|
+
warnings,
|
|
346
|
+
outputType: OutputType.PLAIN_TEXT,
|
|
347
|
+
metadata
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Validate command execution result with comprehensive options
|
|
353
|
+
*/
|
|
354
|
+
static validateResult(
|
|
355
|
+
result: CLIExecutionResult,
|
|
356
|
+
expectedType?: OutputType,
|
|
357
|
+
options?: JsonValidationOptions | YamlValidationOptions | TableValidationOptions | TextValidationOptions
|
|
358
|
+
): ValidationResult {
|
|
359
|
+
// Check for command execution failure
|
|
360
|
+
if (result.exitCode !== 0) {
|
|
361
|
+
return {
|
|
362
|
+
isValid: false,
|
|
363
|
+
errors: [`Command failed with exit code ${result.exitCode}: ${result.stderr}`],
|
|
364
|
+
warnings: [],
|
|
365
|
+
outputType: OutputType.PLAIN_TEXT,
|
|
366
|
+
metadata: {
|
|
367
|
+
exitCode: result.exitCode,
|
|
368
|
+
executionTime: result.executionTime,
|
|
369
|
+
timedOut: result.timedOut
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Validate the output
|
|
375
|
+
return this.validateOutput(result.stdout, expectedType, options);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Create a detailed validation report
|
|
380
|
+
*/
|
|
381
|
+
static createValidationReport(results: ValidationResult[]): string {
|
|
382
|
+
const report: string[] = [];
|
|
383
|
+
|
|
384
|
+
report.push('=== Output Validation Report ===\n');
|
|
385
|
+
|
|
386
|
+
let totalTests = results.length;
|
|
387
|
+
let passedTests = results.filter(r => r.isValid).length;
|
|
388
|
+
let failedTests = totalTests - passedTests;
|
|
389
|
+
|
|
390
|
+
report.push(`Total Tests: ${totalTests}`);
|
|
391
|
+
report.push(`Passed: ${passedTests}`);
|
|
392
|
+
report.push(`Failed: ${failedTests}`);
|
|
393
|
+
report.push(`Success Rate: ${((passedTests / totalTests) * 100).toFixed(1)}%\n`);
|
|
394
|
+
|
|
395
|
+
// Group by output type
|
|
396
|
+
const byType = results.reduce((acc, result, index) => {
|
|
397
|
+
if (!acc[result.outputType]) {
|
|
398
|
+
acc[result.outputType] = [];
|
|
399
|
+
}
|
|
400
|
+
acc[result.outputType].push({ result, index });
|
|
401
|
+
return acc;
|
|
402
|
+
}, {} as Record<OutputType, Array<{ result: ValidationResult; index: number }>>);
|
|
403
|
+
|
|
404
|
+
for (const [type, typeResults] of Object.entries(byType)) {
|
|
405
|
+
report.push(`\n--- ${type.toUpperCase()} Results ---`);
|
|
406
|
+
|
|
407
|
+
typeResults.forEach(({ result, index }) => {
|
|
408
|
+
const status = result.isValid ? '✓ PASS' : '✗ FAIL';
|
|
409
|
+
report.push(`Test ${index + 1}: ${status}`);
|
|
410
|
+
|
|
411
|
+
if (result.errors.length > 0) {
|
|
412
|
+
report.push(` Errors:`);
|
|
413
|
+
result.errors.forEach(error => report.push(` - ${error}`));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (result.warnings.length > 0) {
|
|
417
|
+
report.push(` Warnings:`);
|
|
418
|
+
result.warnings.forEach(warning => report.push(` - ${warning}`));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (result.metadata) {
|
|
422
|
+
report.push(` Metadata: ${JSON.stringify(result.metadata, null, 2)}`);
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return report.join('\n');
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Private helper methods
|
|
431
|
+
private static isValidJson(str: string): boolean {
|
|
432
|
+
try {
|
|
433
|
+
JSON.parse(str);
|
|
434
|
+
return true;
|
|
435
|
+
} catch {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private static looksLikeYaml(str: string): boolean {
|
|
441
|
+
const lines = str.split('\n');
|
|
442
|
+
|
|
443
|
+
// YAML characteristics
|
|
444
|
+
const hasColons = str.includes(':');
|
|
445
|
+
const hasIndentation = lines.some(line => line.match(/^\s+/));
|
|
446
|
+
const hasListItems = lines.some(line => line.trim().startsWith('-'));
|
|
447
|
+
const hasDocumentSeparator = str.includes('---');
|
|
448
|
+
|
|
449
|
+
// Must have colons and either indentation, list items, or document separators
|
|
450
|
+
return hasColons && (hasIndentation || hasListItems || hasDocumentSeparator);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
private static looksLikeTable(str: string): boolean {
|
|
454
|
+
const lines = str.split('\n').filter(line => line.trim());
|
|
455
|
+
|
|
456
|
+
if (lines.length < 2) return false;
|
|
457
|
+
|
|
458
|
+
// Check for common table patterns
|
|
459
|
+
const hasPipes = lines.some(line => line.includes('|'));
|
|
460
|
+
const hasTabs = lines.some(line => line.includes('\t'));
|
|
461
|
+
const hasMultipleSpaces = lines.some(line => /\s{2,}/.test(line));
|
|
462
|
+
const hasConsistentColumns = this.hasConsistentColumnStructure(lines);
|
|
463
|
+
|
|
464
|
+
return hasPipes || hasTabs || (hasMultipleSpaces && hasConsistentColumns);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
private static hasConsistentColumnStructure(lines: string[]): boolean {
|
|
468
|
+
if (lines.length < 2) return false;
|
|
469
|
+
|
|
470
|
+
// Check if lines have similar structure (similar number of words/columns)
|
|
471
|
+
const wordCounts = lines.map(line => line.trim().split(/\s+/).length);
|
|
472
|
+
const avgWordCount = wordCounts.reduce((a, b) => a + b, 0) / wordCounts.length;
|
|
473
|
+
|
|
474
|
+
// Allow some variance but expect consistency
|
|
475
|
+
return wordCounts.every(count => Math.abs(count - avgWordCount) <= 2);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private static extractJsonKeys(obj: any, prefix = ''): string[] {
|
|
479
|
+
const keys: string[] = [];
|
|
480
|
+
|
|
481
|
+
if (typeof obj === 'object' && obj !== null) {
|
|
482
|
+
if (Array.isArray(obj)) {
|
|
483
|
+
obj.forEach((item, index) => {
|
|
484
|
+
const itemKeys = this.extractJsonKeys(item, `${prefix}[${index}]`);
|
|
485
|
+
keys.push(...itemKeys);
|
|
486
|
+
});
|
|
487
|
+
} else {
|
|
488
|
+
Object.keys(obj).forEach(key => {
|
|
489
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
490
|
+
keys.push(fullKey);
|
|
491
|
+
|
|
492
|
+
const nestedKeys = this.extractJsonKeys(obj[key], fullKey);
|
|
493
|
+
keys.push(...nestedKeys);
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return keys;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
private static extractYamlKeys(yamlStr: string): string[] {
|
|
502
|
+
const keys: string[] = [];
|
|
503
|
+
const lines = yamlStr.split('\n');
|
|
504
|
+
|
|
505
|
+
for (const line of lines) {
|
|
506
|
+
const trimmed = line.trim();
|
|
507
|
+
if (trimmed && trimmed.includes(':') && !trimmed.startsWith('#')) {
|
|
508
|
+
const colonIndex = trimmed.indexOf(':');
|
|
509
|
+
const key = trimmed.substring(0, colonIndex).trim();
|
|
510
|
+
if (key && !key.startsWith('-')) {
|
|
511
|
+
keys.push(key);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return keys;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
private static analyzeTableStructure(output: string, separator?: string | RegExp) {
|
|
520
|
+
const lines = output.split('\n');
|
|
521
|
+
const nonEmptyLines = lines.filter(line => line.trim());
|
|
522
|
+
|
|
523
|
+
let headers: string[] | null = null;
|
|
524
|
+
let columnCount = 0;
|
|
525
|
+
let dataRows = 0;
|
|
526
|
+
let emptyRows = 0;
|
|
527
|
+
let hasHeader = false;
|
|
528
|
+
|
|
529
|
+
if (nonEmptyLines.length > 0) {
|
|
530
|
+
// Assume first non-empty line is header
|
|
531
|
+
const firstLine = nonEmptyLines[0];
|
|
532
|
+
|
|
533
|
+
if (separator) {
|
|
534
|
+
headers = firstLine.split(separator).map(h => h.trim());
|
|
535
|
+
} else {
|
|
536
|
+
// Auto-detect separator
|
|
537
|
+
if (firstLine.includes('|')) {
|
|
538
|
+
headers = firstLine.split('|').map(h => h.trim()).filter(h => h);
|
|
539
|
+
} else if (firstLine.includes('\t')) {
|
|
540
|
+
headers = firstLine.split('\t').map(h => h.trim());
|
|
541
|
+
} else {
|
|
542
|
+
headers = firstLine.split(/\s{2,}/).map(h => h.trim());
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
columnCount = headers.length;
|
|
547
|
+
hasHeader = true;
|
|
548
|
+
|
|
549
|
+
// Count actual data rows, excluding header and separator lines
|
|
550
|
+
dataRows = nonEmptyLines.slice(1).filter(line => {
|
|
551
|
+
// Skip markdown-style separator lines like |------|-----|------|
|
|
552
|
+
const trimmed = line.trim();
|
|
553
|
+
return !this.isTableSeparatorLine(trimmed);
|
|
554
|
+
}).length;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
emptyRows = lines.length - nonEmptyLines.length;
|
|
558
|
+
|
|
559
|
+
return {
|
|
560
|
+
headers,
|
|
561
|
+
columnCount,
|
|
562
|
+
dataRows,
|
|
563
|
+
emptyRows,
|
|
564
|
+
hasHeader
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
private static isTableSeparatorLine(line: string): boolean {
|
|
569
|
+
// Check if line is a markdown-style table separator (e.g., |------|-----|------|)
|
|
570
|
+
const trimmed = line.trim();
|
|
571
|
+
if (trimmed.includes('|')) {
|
|
572
|
+
// Remove pipes and check if remaining content is mostly dashes and spaces
|
|
573
|
+
const withoutPipes = trimmed.replace(/\|/g, '');
|
|
574
|
+
return /^[\s\-:]*$/.test(withoutPipes);
|
|
575
|
+
}
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
private static validateJsonSchema(data: any, schema: SchemaValidationRule[]): string[] {
|
|
580
|
+
const errors: string[] = [];
|
|
581
|
+
|
|
582
|
+
for (const rule of schema) {
|
|
583
|
+
const value = this.getValueByPath(data, rule.path);
|
|
584
|
+
|
|
585
|
+
// Check if required field is missing
|
|
586
|
+
if (rule.required && (value === undefined || value === null)) {
|
|
587
|
+
errors.push(`Required field '${rule.path}' is missing`);
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Skip validation if field is not required and missing
|
|
592
|
+
if (!rule.required && (value === undefined || value === null)) {
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Type validation
|
|
597
|
+
if (!this.validateType(value, rule.type)) {
|
|
598
|
+
errors.push(`Field '${rule.path}' expected type ${rule.type}, got ${typeof value}`);
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Additional validations based on type
|
|
603
|
+
if (rule.type === 'string' && typeof value === 'string') {
|
|
604
|
+
if (rule.pattern && !rule.pattern.test(value)) {
|
|
605
|
+
errors.push(`Field '${rule.path}' does not match pattern ${rule.pattern}`);
|
|
606
|
+
}
|
|
607
|
+
if (rule.minLength && value.length < rule.minLength) {
|
|
608
|
+
errors.push(`Field '${rule.path}' length ${value.length} is less than minimum ${rule.minLength}`);
|
|
609
|
+
}
|
|
610
|
+
if (rule.maxLength && value.length > rule.maxLength) {
|
|
611
|
+
errors.push(`Field '${rule.path}' length ${value.length} exceeds maximum ${rule.maxLength}`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (rule.type === 'number' && typeof value === 'number') {
|
|
616
|
+
if (rule.min !== undefined && value < rule.min) {
|
|
617
|
+
errors.push(`Field '${rule.path}' value ${value} is less than minimum ${rule.min}`);
|
|
618
|
+
}
|
|
619
|
+
if (rule.max !== undefined && value > rule.max) {
|
|
620
|
+
errors.push(`Field '${rule.path}' value ${value} exceeds maximum ${rule.max}`);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (rule.enum && !rule.enum.includes(value)) {
|
|
625
|
+
errors.push(`Field '${rule.path}' value '${value}' is not in allowed values: ${rule.enum.join(', ')}`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return errors;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
private static getValueByPath(obj: any, path: string): any {
|
|
633
|
+
return path.split('.').reduce((current, key) => {
|
|
634
|
+
return current && current[key] !== undefined ? current[key] : undefined;
|
|
635
|
+
}, obj);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
private static validateType(value: any, expectedType: string): boolean {
|
|
639
|
+
switch (expectedType) {
|
|
640
|
+
case 'string':
|
|
641
|
+
return typeof value === 'string';
|
|
642
|
+
case 'number':
|
|
643
|
+
return typeof value === 'number' && !isNaN(value);
|
|
644
|
+
case 'boolean':
|
|
645
|
+
return typeof value === 'boolean';
|
|
646
|
+
case 'array':
|
|
647
|
+
return Array.isArray(value);
|
|
648
|
+
case 'object':
|
|
649
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
650
|
+
default:
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
private static findAdditionalProperties(data: any, schema: SchemaValidationRule[]): string[] {
|
|
656
|
+
const definedPaths = new Set(schema.map(rule => rule.path));
|
|
657
|
+
const actualPaths = this.extractJsonKeys(data);
|
|
658
|
+
|
|
659
|
+
return actualPaths.filter(path => !definedPaths.has(path));
|
|
660
|
+
}
|
|
661
|
+
}
|
package/src/test/setup.ts
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"include": [
|
|
4
|
+
"**/*.ts"
|
|
5
|
+
],
|
|
6
|
+
"exclude": [
|
|
7
|
+
"node_modules"
|
|
8
|
+
],
|
|
9
|
+
"compilerOptions": {
|
|
10
|
+
"types": ["vitest/globals", "node"],
|
|
11
|
+
"moduleResolution": "node",
|
|
12
|
+
"allowSyntheticDefaultImports": true,
|
|
13
|
+
"esModuleInterop": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"noEmit": true
|
|
16
|
+
}
|
|
17
|
+
}
|
package/dist/test/setup.d.ts
DELETED
|
File without changes
|
package/dist/test/setup.js
DELETED
package/dist/test/setup.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"setup.js","sourceRoot":"","sources":["../../src/test/setup.ts"],"names":[],"mappings":";AAAA,mCAAmC;AACnC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,kBAAkB,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC"}
|