norn-cli 1.6.0 → 1.6.2

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.
Files changed (40) hide show
  1. package/AGENTS.md +9 -1
  2. package/CHANGELOG.md +23 -0
  3. package/dist/cli.js +246 -80
  4. package/package.json +1 -1
  5. package/out/assertionRunner.js +0 -537
  6. package/out/chatParticipant.js +0 -722
  7. package/out/cli/colors.js +0 -129
  8. package/out/cli/formatters/assertion.js +0 -75
  9. package/out/cli/formatters/index.js +0 -23
  10. package/out/cli/formatters/response.js +0 -106
  11. package/out/cli/formatters/summary.js +0 -187
  12. package/out/cli/redaction.js +0 -237
  13. package/out/cli/reporters/html.js +0 -634
  14. package/out/cli/reporters/index.js +0 -22
  15. package/out/cli/reporters/junit.js +0 -211
  16. package/out/cli.js +0 -989
  17. package/out/codeLensProvider.js +0 -248
  18. package/out/compareContentProvider.js +0 -85
  19. package/out/completionProvider.js +0 -2404
  20. package/out/contractDecorationProvider.js +0 -243
  21. package/out/coverageCalculator.js +0 -837
  22. package/out/coveragePanel.js +0 -545
  23. package/out/diagnosticProvider.js +0 -1113
  24. package/out/environmentProvider.js +0 -442
  25. package/out/extension.js +0 -1114
  26. package/out/httpClient.js +0 -269
  27. package/out/jsonFileReader.js +0 -320
  28. package/out/nornPrompt.js +0 -580
  29. package/out/nornapiParser.js +0 -326
  30. package/out/parser.js +0 -725
  31. package/out/responsePanel.js +0 -4674
  32. package/out/schemaGenerator.js +0 -393
  33. package/out/scriptRunner.js +0 -419
  34. package/out/sequenceRunner.js +0 -3046
  35. package/out/swaggerBodyIntellisenseCache.js +0 -147
  36. package/out/swaggerParser.js +0 -419
  37. package/out/test/coverageCalculator.test.js +0 -100
  38. package/out/test/extension.test.js +0 -48
  39. package/out/testProvider.js +0 -658
  40. package/out/validationCache.js +0 -245
package/out/cli.js DELETED
@@ -1,989 +0,0 @@
1
- "use strict";
2
- /**
3
- * Norn CLI - Command-line interface for running .norn files
4
- *
5
- * Features:
6
- * - Human-readable output with color coding
7
- * - Verbose mode for detailed request/response info
8
- * - Automatic sensitive data redaction
9
- * - JUnit XML output for CI/CD
10
- * - HTML report generation
11
- */
12
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
- if (k2 === undefined) k2 = k;
14
- var desc = Object.getOwnPropertyDescriptor(m, k);
15
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
- desc = { enumerable: true, get: function() { return m[k]; } };
17
- }
18
- Object.defineProperty(o, k2, desc);
19
- }) : (function(o, m, k, k2) {
20
- if (k2 === undefined) k2 = k;
21
- o[k2] = m[k];
22
- }));
23
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
- Object.defineProperty(o, "default", { enumerable: true, value: v });
25
- }) : function(o, v) {
26
- o["default"] = v;
27
- });
28
- var __importStar = (this && this.__importStar) || (function () {
29
- var ownKeys = function(o) {
30
- ownKeys = Object.getOwnPropertyNames || function (o) {
31
- var ar = [];
32
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
33
- return ar;
34
- };
35
- return ownKeys(o);
36
- };
37
- return function (mod) {
38
- if (mod && mod.__esModule) return mod;
39
- var result = {};
40
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
41
- __setModuleDefault(result, mod);
42
- return result;
43
- };
44
- })();
45
- Object.defineProperty(exports, "__esModule", { value: true });
46
- const fs = __importStar(require("fs"));
47
- const fsPromises = __importStar(require("fs/promises"));
48
- const path = __importStar(require("path"));
49
- const parser_1 = require("./parser");
50
- const httpClient_1 = require("./httpClient");
51
- const sequenceRunner_1 = require("./sequenceRunner");
52
- const nornapiParser_1 = require("./nornapiParser");
53
- // CLI modules
54
- const colors_1 = require("./cli/colors");
55
- const redaction_1 = require("./cli/redaction");
56
- const summary_1 = require("./cli/formatters/summary");
57
- const response_1 = require("./cli/formatters/response");
58
- const junit_1 = require("./cli/reporters/junit");
59
- const html_1 = require("./cli/reporters/html");
60
- const ENV_FILENAME = '.nornenv';
61
- function getEnvSearchStartDirectory(targetPath) {
62
- const resolvedPath = path.resolve(targetPath);
63
- try {
64
- const stats = fs.statSync(resolvedPath);
65
- return stats.isDirectory() ? resolvedPath : path.dirname(resolvedPath);
66
- }
67
- catch {
68
- return path.dirname(resolvedPath);
69
- }
70
- }
71
- /**
72
- * Finds .nornenv file relative to a specific file path (for CLI usage)
73
- */
74
- function findEnvFileFromPath(filePath) {
75
- let dir = getEnvSearchStartDirectory(filePath);
76
- while (true) {
77
- const envPath = path.join(dir, ENV_FILENAME);
78
- if (fs.existsSync(envPath)) {
79
- return envPath;
80
- }
81
- const parentDir = path.dirname(dir);
82
- if (parentDir === dir) {
83
- break;
84
- }
85
- dir = parentDir;
86
- }
87
- return undefined;
88
- }
89
- /**
90
- * Parses the .nornenv file content (with secret support)
91
- */
92
- function parseEnvFile(content) {
93
- const lines = content.split('\n');
94
- const config = {
95
- common: {},
96
- environments: [],
97
- secretNames: new Set(),
98
- secretValues: new Map()
99
- };
100
- let currentEnv = null;
101
- const envRegex = /^\[env:([a-zA-Z_][a-zA-Z0-9_-]*)\]$/;
102
- const varRegex = /^var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$/;
103
- const secretRegex = /^secret\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$/;
104
- for (const line of lines) {
105
- const trimmed = line.trim();
106
- // Skip empty lines and comments
107
- if (!trimmed || trimmed.startsWith('#')) {
108
- continue;
109
- }
110
- // Check for environment section
111
- const envMatch = trimmed.match(envRegex);
112
- if (envMatch) {
113
- currentEnv = {
114
- name: envMatch[1],
115
- variables: {}
116
- };
117
- config.environments.push(currentEnv);
118
- continue;
119
- }
120
- // Check for secret declaration
121
- const secretMatch = trimmed.match(secretRegex);
122
- if (secretMatch) {
123
- const varName = secretMatch[1];
124
- const varValue = secretMatch[2].trim();
125
- config.secretNames.add(varName);
126
- config.secretValues.set(varName, varValue);
127
- if (currentEnv) {
128
- currentEnv.variables[varName] = varValue;
129
- }
130
- else {
131
- config.common[varName] = varValue;
132
- }
133
- continue;
134
- }
135
- // Check for variable declaration
136
- const varMatch = trimmed.match(varRegex);
137
- if (varMatch) {
138
- const varName = varMatch[1];
139
- const varValue = varMatch[2].trim();
140
- if (currentEnv) {
141
- currentEnv.variables[varName] = varValue;
142
- }
143
- else {
144
- config.common[varName] = varValue;
145
- }
146
- }
147
- }
148
- return config;
149
- }
150
- function resolveEnvironmentForPath(targetPath, selectedEnv) {
151
- const envFilePath = findEnvFileFromPath(targetPath);
152
- if (!envFilePath) {
153
- return {
154
- variables: {},
155
- secretNames: new Set(),
156
- secretValues: new Map(),
157
- availableEnvironments: []
158
- };
159
- }
160
- const envContent = fs.readFileSync(envFilePath, 'utf-8');
161
- const envConfig = parseEnvFile(envContent);
162
- const variables = { ...envConfig.common };
163
- const secretNames = new Set(envConfig.secretNames);
164
- const secretValues = new Map(envConfig.secretValues);
165
- const availableEnvironments = envConfig.environments.map(e => e.name);
166
- if (selectedEnv) {
167
- const targetEnv = envConfig.environments.find(e => e.name === selectedEnv);
168
- if (!targetEnv) {
169
- return {
170
- envFilePath,
171
- variables,
172
- secretNames,
173
- secretValues,
174
- availableEnvironments,
175
- envNotFound: selectedEnv
176
- };
177
- }
178
- Object.assign(variables, targetEnv.variables);
179
- for (const [name, value] of Object.entries(targetEnv.variables)) {
180
- if (secretNames.has(name)) {
181
- secretValues.set(name, value);
182
- }
183
- }
184
- }
185
- return {
186
- envFilePath,
187
- variables,
188
- secretNames,
189
- secretValues,
190
- availableEnvironments
191
- };
192
- }
193
- function mergeSecrets(targetNames, targetValues, sourceNames, sourceValues) {
194
- for (const name of sourceNames) {
195
- targetNames.add(name);
196
- }
197
- for (const [name, value] of sourceValues) {
198
- targetValues.set(name, value);
199
- }
200
- }
201
- /**
202
- * Generate a timestamp string for report filenames
203
- * Format: YYYY-MM-DD-HHmmss
204
- */
205
- function generateTimestamp() {
206
- const now = new Date();
207
- const year = now.getFullYear();
208
- const month = String(now.getMonth() + 1).padStart(2, '0');
209
- const day = String(now.getDate()).padStart(2, '0');
210
- const hours = String(now.getHours()).padStart(2, '0');
211
- const minutes = String(now.getMinutes()).padStart(2, '0');
212
- const seconds = String(now.getSeconds()).padStart(2, '0');
213
- return `${year}-${month}-${day}-${hours}${minutes}${seconds}`;
214
- }
215
- /**
216
- * Generate output paths for reports when using --output-dir
217
- */
218
- function generateReportPaths(outputDir, inputFile, timestamp) {
219
- const baseName = path.basename(inputFile, path.extname(inputFile));
220
- return {
221
- junitPath: path.join(outputDir, `${baseName}-${timestamp}-results.xml`),
222
- htmlPath: path.join(outputDir, `${baseName}-${timestamp}-report.html`)
223
- };
224
- }
225
- function parseArgs(args) {
226
- const options = {
227
- file: '',
228
- output: 'pretty',
229
- verbose: false,
230
- failOnError: true,
231
- noRedact: false,
232
- tagFilters: [],
233
- tagsFilter: [],
234
- };
235
- for (let i = 0; i < args.length; i++) {
236
- const arg = args[i];
237
- if (arg === '--json' || arg === '-j') {
238
- options.output = 'json';
239
- }
240
- else if (arg === '--verbose' || arg === '-v') {
241
- options.verbose = true;
242
- }
243
- else if (arg === '--sequence' || arg === '-s') {
244
- options.sequence = args[++i];
245
- }
246
- else if (arg === '--request' || arg === '-r') {
247
- options.request = args[++i];
248
- }
249
- else if (arg === '--env' || arg === '-e') {
250
- options.env = args[++i];
251
- }
252
- else if (arg === '--timeout' || arg === '-t') {
253
- options.timeout = parseInt(args[++i], 10) * 1000;
254
- }
255
- else if (arg === '--no-fail') {
256
- options.failOnError = false;
257
- }
258
- else if (arg === '--no-redact') {
259
- options.noRedact = true;
260
- }
261
- else if (arg === '--junit') {
262
- options.junitOutput = args[++i];
263
- }
264
- else if (arg === '--html') {
265
- options.htmlOutput = args[++i];
266
- }
267
- else if (arg === '--output-dir' || arg === '-o') {
268
- options.outputDir = args[++i];
269
- }
270
- else if (arg === '--tag') {
271
- const tagStr = args[++i];
272
- if (tagStr) {
273
- options.tagFilters.push((0, sequenceRunner_1.parseTagFilter)(tagStr));
274
- }
275
- }
276
- else if (arg === '--tags') {
277
- const tagsStr = args[++i];
278
- if (tagsStr) {
279
- const tags = tagsStr.split(',').map(t => t.trim()).filter(t => t);
280
- options.tagsFilter.push(...tags.map(t => (0, sequenceRunner_1.parseTagFilter)(t)));
281
- }
282
- }
283
- else if (arg === '--help' || arg === '-h') {
284
- printHelp();
285
- process.exit(0);
286
- }
287
- else if (!arg.startsWith('-')) {
288
- options.file = arg;
289
- }
290
- }
291
- return options;
292
- }
293
- function printHelp() {
294
- console.log(`
295
- Norn CLI - Run HTTP requests and test sequences from .norn files
296
-
297
- Usage: norn <file.norn|directory> [options]
298
-
299
- When given a directory, recursively discovers and runs all test sequences
300
- from .norn files within that directory and subdirectories.
301
-
302
- Options:
303
- -s, --sequence <name> Run a specific sequence by name (single file only)
304
- -r, --request <name> Run a specific named request (single file only)
305
- -e, --env <name> Use environment from .nornenv (e.g., dev, prod)
306
- -t, --timeout <sec> Request timeout in seconds (default: no timeout)
307
- -j, --json Output results as JSON (for CI/CD)
308
- -v, --verbose Show detailed output (headers, request/response bodies)
309
- --no-fail Don't exit with error code on failed requests
310
- --no-redact Disable automatic redaction of sensitive data
311
- --junit <file> Generate JUnit XML report (explicit path)
312
- --html <file> Generate HTML report (explicit path)
313
- -o, --output-dir <dir> Output directory for reports (auto-generates timestamped files)
314
- --tag <filter> Filter sequences by tag (AND logic, can be repeated)
315
- --tags <filters> Filter sequences by tags (OR logic, comma-separated)
316
- -h, --help Show this help message
317
-
318
- Report Generation:
319
- Use --output-dir for auto-generated timestamped filenames:
320
- norn tests.norn --output-dir ./reports
321
- # Creates: ./reports/tests-2026-02-01-120000-results.xml
322
- # ./reports/tests-2026-02-01-120000-report.html
323
-
324
- Or use explicit paths with --junit and --html:
325
- norn tests.norn --junit results.xml --html report.html
326
-
327
- Output Behavior:
328
- By default, only errors and failures show full details.
329
- Use -v/--verbose to see all request/response details.
330
-
331
- Sensitive Data:
332
- Authorization headers, tokens, and secrets are automatically redacted.
333
- Use 'secret' keyword in .nornenv to mark variables for redaction:
334
- secret apiKey = my-secret-key
335
- Use --no-redact to disable redaction for debugging.
336
-
337
- Tag Filtering:
338
- Tags are decorators on sequences: @smoke, @team(CustomerExp)
339
-
340
- --tag smoke Run sequences with @smoke tag
341
- --tag smoke --tag auth Run sequences with BOTH @smoke AND @auth tags
342
- --tags smoke,auth Run sequences with @smoke OR @auth tag
343
- --tag team(CustomerExp) Run sequences with @team(CustomerExp) exact match
344
-
345
- Examples:
346
- norn api-tests.norn # Run all test sequences in file
347
- norn tests/ # Run all tests in directory (recursive)
348
- norn tests/Regression/ -e prelive # Run regression suite with environment
349
- norn api-tests.norn -s auth # Run 'auth' sequence by name
350
- norn api-tests.norn -r LoginRequest # Run 'LoginRequest' named request
351
- norn api-tests.norn -e production # Use 'production' environment
352
- norn api-tests.norn -j # JSON output for CI/CD
353
- norn api-tests.norn -v # Verbose output with all details
354
- norn tests/ -o ./reports # Generate reports for all tests
355
- norn api-tests.norn --junit results.xml # Generate JUnit report (explicit)
356
- norn api-tests.norn --html report.html # Generate HTML report (explicit)
357
- norn api-tests.norn --no-redact # Show all data (no redaction)
358
- `);
359
- }
360
- async function runSingleRequest(fileContent, variables, cookieJar, apiDefinitions) {
361
- const lines = fileContent.split('\n');
362
- const requestLines = [];
363
- let foundRequestLine = false;
364
- for (const line of lines) {
365
- const trimmed = line.trim();
366
- if (!foundRequestLine) {
367
- if (!trimmed || trimmed.startsWith('import ') || trimmed.startsWith('#')) {
368
- continue;
369
- }
370
- }
371
- if (!foundRequestLine && /^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+/i.test(trimmed)) {
372
- foundRequestLine = true;
373
- }
374
- if (foundRequestLine) {
375
- requestLines.push(line);
376
- }
377
- }
378
- const requestContent = requestLines.join('\n').trim();
379
- if (apiDefinitions &&
380
- apiDefinitions.endpoints.length > 0 &&
381
- (0, nornapiParser_1.isApiRequestLine)(requestContent, apiDefinitions.endpoints)) {
382
- const apiRequest = (0, nornapiParser_1.parseApiRequest)(requestContent, apiDefinitions.endpoints, apiDefinitions.headerGroups);
383
- if (apiRequest) {
384
- const endpoint = (0, nornapiParser_1.getEndpoint)({ headerGroups: apiDefinitions.headerGroups, endpoints: apiDefinitions.endpoints }, apiRequest.endpointName);
385
- if (endpoint) {
386
- let resolvedPath = endpoint.path;
387
- for (const [key, value] of Object.entries(variables)) {
388
- resolvedPath = resolvedPath.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), String(value));
389
- }
390
- for (const [paramName, paramValue] of Object.entries(apiRequest.params)) {
391
- let resolvedValue = paramValue;
392
- if (variables[paramValue] !== undefined) {
393
- resolvedValue = String(variables[paramValue]);
394
- }
395
- else if (paramValue.startsWith('{{') && paramValue.endsWith('}}')) {
396
- const varName = paramValue.slice(2, -2);
397
- if (variables[varName] !== undefined) {
398
- resolvedValue = String(variables[varName]);
399
- }
400
- }
401
- resolvedPath = resolvedPath.replace(`{${paramName}}`, resolvedValue);
402
- }
403
- const combinedHeaders = {};
404
- for (const groupName of apiRequest.headerGroupNames) {
405
- const group = apiDefinitions.headerGroups.find(hg => hg.name === groupName);
406
- if (group) {
407
- const resolvedHeaders = (0, nornapiParser_1.resolveHeaderValues)(group, variables);
408
- Object.assign(combinedHeaders, resolvedHeaders);
409
- }
410
- }
411
- // Add inline headers (these take precedence over header groups)
412
- for (const [headerName, headerValue] of Object.entries(apiRequest.inlineHeaders)) {
413
- // Resolve variable references in header values
414
- let resolved = headerValue;
415
- resolved = resolved.replace(/\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g, (_, varName) => {
416
- return variables[varName] !== undefined ? String(variables[varName]) : `{{${varName}}}`;
417
- });
418
- combinedHeaders[headerName] = resolved;
419
- }
420
- const parsed = {
421
- method: apiRequest.method,
422
- url: resolvedPath,
423
- headers: combinedHeaders,
424
- body: apiRequest.body
425
- };
426
- return await (0, httpClient_1.sendRequestWithJar)(parsed, cookieJar);
427
- }
428
- }
429
- }
430
- // Regular HTTP request - parse and apply header groups
431
- let parsed = (0, parser_1.parserHttpRequest)(requestContent, variables);
432
- // Check for header group names in the request text
433
- if (apiDefinitions && apiDefinitions.headerGroups.length > 0) {
434
- const headerGroupNames = apiDefinitions.headerGroups.map(hg => hg.name);
435
- const foundGroups = [];
436
- for (const line of requestLines) {
437
- const trimmed = line.trim();
438
- if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('var ')) {
439
- continue;
440
- }
441
- // Check if this is the HTTP method line - look for header groups at the end
442
- const methodMatch = trimmed.match(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(.+)$/i);
443
- if (methodMatch) {
444
- const afterMethod = methodMatch[2];
445
- const tokens = afterMethod.split(/\s+/);
446
- // Scan from the end to find header groups
447
- for (let i = tokens.length - 1; i >= 0; i--) {
448
- if (headerGroupNames.includes(tokens[i])) {
449
- foundGroups.push(tokens[i]);
450
- }
451
- else {
452
- break;
453
- }
454
- }
455
- continue;
456
- }
457
- // Skip lines that look like headers (Name: Value)
458
- if (/^[A-Za-z0-9\-_]+\s*:\s*.+$/.test(trimmed)) {
459
- continue;
460
- }
461
- // Check if this line contains header group names (can be multiple space-separated)
462
- const potentialGroups = trimmed.split(/\s+/);
463
- for (const groupName of potentialGroups) {
464
- if (headerGroupNames.includes(groupName)) {
465
- foundGroups.push(groupName);
466
- }
467
- }
468
- }
469
- // Collect headers from all found groups and strip from URL
470
- for (const groupName of foundGroups) {
471
- const group = apiDefinitions.headerGroups.find(hg => hg.name === groupName);
472
- if (group) {
473
- const resolvedHeaders = (0, nornapiParser_1.resolveHeaderValues)(group, variables);
474
- parsed.headers = { ...resolvedHeaders, ...parsed.headers };
475
- }
476
- // Strip from URL
477
- const endPattern = new RegExp(`\\s+${groupName}$`);
478
- parsed.url = parsed.url.replace(endPattern, '');
479
- }
480
- parsed.url = parsed.url.trim();
481
- }
482
- return await (0, httpClient_1.sendRequestWithJar)(parsed, cookieJar);
483
- }
484
- /**
485
- * Recursively discover all .norn files in a directory
486
- */
487
- function discoverNornFiles(dirPath) {
488
- const files = [];
489
- function walkDir(currentPath) {
490
- const entries = fs.readdirSync(currentPath, { withFileTypes: true });
491
- for (const entry of entries) {
492
- const fullPath = path.join(currentPath, entry.name);
493
- if (entry.isDirectory()) {
494
- // Skip common non-test directories
495
- if (!entry.name.startsWith('.') && entry.name !== 'node_modules') {
496
- walkDir(fullPath);
497
- }
498
- }
499
- else if (entry.isFile() && entry.name.endsWith('.norn')) {
500
- files.push(fullPath);
501
- }
502
- }
503
- }
504
- walkDir(dirPath);
505
- return files.sort(); // Sort for consistent ordering
506
- }
507
- /**
508
- * Count test sequences in a file without running them
509
- */
510
- function countTestSequences(fileContent, tagFilterOptions) {
511
- const allSequences = (0, sequenceRunner_1.extractSequences)(fileContent);
512
- const testSequences = allSequences.filter(seq => seq.isTest);
513
- const filtered = tagFilterOptions && tagFilterOptions.filters.length > 0
514
- ? testSequences.filter(seq => (0, sequenceRunner_1.sequenceMatchesTags)(seq, tagFilterOptions)).length
515
- : testSequences.length;
516
- return { total: testSequences.length, filtered };
517
- }
518
- /**
519
- * Loads theory data from an external JSON file
520
- */
521
- async function loadTheoryFile(theoryPath, workingDir) {
522
- const absolutePath = path.resolve(workingDir, theoryPath);
523
- const content = await fsPromises.readFile(absolutePath, 'utf-8');
524
- return JSON.parse(content);
525
- }
526
- /**
527
- * Formats a test case label from parameter names and values
528
- */
529
- function formatCaseLabel(params) {
530
- const parts = Object.entries(params).map(([key, value]) => {
531
- if (typeof value === 'string') {
532
- return `${key}="${value}"`;
533
- }
534
- return `${key}=${value}`;
535
- });
536
- return `[${parts.join(', ')}]`;
537
- }
538
- async function runAllSequences(fileContent, variables, cookieJar, workingDir, apiDefinitions, tagFilterOptions, sequenceSources) {
539
- const allSequences = (0, sequenceRunner_1.extractSequences)(fileContent);
540
- const results = [];
541
- // First filter to only test sequences (isTest === true)
542
- const testSequences = allSequences.filter(seq => seq.isTest);
543
- // Then apply tag filters if present
544
- const sequences = tagFilterOptions && tagFilterOptions.filters.length > 0
545
- ? testSequences.filter(seq => (0, sequenceRunner_1.sequenceMatchesTags)(seq, tagFilterOptions))
546
- : testSequences;
547
- for (const seq of sequences) {
548
- // Check if this is a parameterized test
549
- let theoryData = seq.theoryData;
550
- // If we have a @theory source file, load it
551
- if (theoryData?.source && theoryData.cases.length === 0) {
552
- try {
553
- const cases = await loadTheoryFile(theoryData.source, workingDir);
554
- theoryData = { ...theoryData, cases };
555
- }
556
- catch (error) {
557
- // Report error as a failed result
558
- const errorResult = {
559
- name: seq.name,
560
- success: false,
561
- responses: [],
562
- scriptResults: [],
563
- assertionResults: [],
564
- steps: [],
565
- errors: [`Failed to load theory file '${theoryData.source}': ${error instanceof Error ? error.message : String(error)}`],
566
- duration: 0
567
- };
568
- results.push(errorResult);
569
- continue;
570
- }
571
- }
572
- // If this has theory data with cases, run each case separately
573
- if (theoryData && theoryData.cases.length > 0) {
574
- for (let caseIdx = 0; caseIdx < theoryData.cases.length; caseIdx++) {
575
- const caseParams = theoryData.cases[caseIdx];
576
- const caseLabel = formatCaseLabel(caseParams);
577
- // Convert case params to string args
578
- const caseArgs = {};
579
- for (const [key, value] of Object.entries(caseParams)) {
580
- caseArgs[key] = String(value);
581
- }
582
- const result = await (0, sequenceRunner_1.runSequenceWithJar)(seq.content, variables, cookieJar, workingDir, fileContent, undefined, undefined, caseArgs, apiDefinitions, tagFilterOptions, sequenceSources);
583
- result.name = `${seq.name}${caseLabel}`;
584
- results.push(result);
585
- }
586
- }
587
- else {
588
- // Regular test sequence (no parameterization)
589
- const defaultArgs = {};
590
- if (seq.parameters) {
591
- for (const param of seq.parameters) {
592
- if (param.defaultValue !== undefined) {
593
- defaultArgs[param.name] = param.defaultValue;
594
- }
595
- }
596
- }
597
- const result = await (0, sequenceRunner_1.runSequenceWithJar)(seq.content, variables, cookieJar, workingDir, fileContent, undefined, undefined, defaultArgs, apiDefinitions, tagFilterOptions, sequenceSources);
598
- result.name = seq.name;
599
- results.push(result);
600
- }
601
- }
602
- return results;
603
- }
604
- async function main() {
605
- const args = process.argv.slice(2);
606
- if (args.length === 0) {
607
- printHelp();
608
- process.exit(1);
609
- }
610
- const options = parseArgs(args);
611
- if (!options.file) {
612
- console.error('Error: No input file or directory specified');
613
- process.exit(1);
614
- }
615
- // Initialize colors (async due to chalk v5 ESM)
616
- const colors = await (0, colors_1.initColors)();
617
- const inputPath = path.resolve(options.file);
618
- if (!fs.existsSync(inputPath)) {
619
- console.error(`Error: Path not found: ${inputPath}`);
620
- process.exit(1);
621
- }
622
- const isDirectory = fs.statSync(inputPath).isDirectory();
623
- // Discover files to run
624
- let filesToRun;
625
- if (isDirectory) {
626
- filesToRun = discoverNornFiles(inputPath);
627
- if (filesToRun.length === 0) {
628
- console.log(colors.info(`No .norn files found in ${inputPath}`));
629
- process.exit(0);
630
- }
631
- if (options.verbose) {
632
- console.log(colors.info(`Discovered ${filesToRun.length} .norn file(s) in ${inputPath}`));
633
- }
634
- }
635
- else {
636
- filesToRun = [inputPath];
637
- }
638
- // For directory mode, we aggregate results across all files
639
- const allResults = [];
640
- const allErrors = [];
641
- let overallSuccess = true;
642
- const startTime = Date.now();
643
- const combinedSecretNames = new Set();
644
- const combinedSecretValues = new Map();
645
- let tagFilterOptions = undefined;
646
- if (options.tagFilters.length > 0) {
647
- tagFilterOptions = {
648
- filters: options.tagFilters,
649
- mode: 'and'
650
- };
651
- }
652
- else if (options.tagsFilter.length > 0) {
653
- tagFilterOptions = {
654
- filters: options.tagsFilter,
655
- mode: 'or'
656
- };
657
- }
658
- // If running a specific sequence or request, only works with single file
659
- if ((options.sequence || options.request) && isDirectory) {
660
- console.error('Error: --sequence and --request flags require a specific file, not a directory');
661
- process.exit(1);
662
- }
663
- // Single file mode with specific sequence/request
664
- if (options.sequence || options.request) {
665
- const filePath = filesToRun[0];
666
- const resolvedEnv = resolveEnvironmentForPath(filePath, options.env);
667
- if (resolvedEnv.envNotFound) {
668
- console.error(`Error: Environment '${resolvedEnv.envNotFound}' not found in .nornenv`);
669
- console.error(`Available environments: ${resolvedEnv.availableEnvironments.join(', ') || 'none'}`);
670
- process.exit(1);
671
- }
672
- if (!resolvedEnv.envFilePath && options.env) {
673
- console.error(colors.warning(`Warning: --env specified but no .nornenv file found`));
674
- }
675
- else if (resolvedEnv.envFilePath && options.env && options.verbose) {
676
- console.log(colors.info(`Using environment: ${options.env}`));
677
- }
678
- mergeSecrets(combinedSecretNames, combinedSecretValues, resolvedEnv.secretNames, resolvedEnv.secretValues);
679
- const redaction = (0, redaction_1.createRedactionOptions)(combinedSecretNames, combinedSecretValues, !options.noRedact);
680
- const fileContent = fs.readFileSync(filePath, 'utf-8');
681
- const fileVariables = (0, parser_1.extractFileLevelVariables)(fileContent);
682
- const variables = { ...resolvedEnv.variables, ...fileVariables };
683
- const cookieJar = (0, httpClient_1.createCookieJar)();
684
- const workingDir = path.dirname(filePath);
685
- const importResult = await (0, parser_1.resolveImports)(fileContent, workingDir, async (importPath) => fsPromises.readFile(importPath, 'utf-8'));
686
- // Check for duplicate definition errors which should block execution
687
- const duplicateErrors = importResult.errors.filter(err => err.error.includes('Duplicate header group') ||
688
- err.error.includes('Duplicate endpoint') ||
689
- err.error.includes('Duplicate named request') ||
690
- err.error.includes('Duplicate sequence'));
691
- const otherErrors = importResult.errors.filter(err => !err.error.includes('Duplicate header group') &&
692
- !err.error.includes('Duplicate endpoint') &&
693
- !err.error.includes('Duplicate named request') &&
694
- !err.error.includes('Duplicate sequence'));
695
- // Show non-duplicate errors as warnings
696
- for (const err of otherErrors) {
697
- console.error(colors.warning(`Import warning: ${err.path} - ${err.error}`));
698
- }
699
- // Duplicate errors block execution
700
- if (duplicateErrors.length > 0) {
701
- console.error(colors.error('Cannot execute - duplicate definitions found:'));
702
- for (const err of duplicateErrors) {
703
- console.error(colors.error(` ${err.path}: ${err.error}`));
704
- }
705
- process.exit(1);
706
- }
707
- const fileContentWithImports = importResult.importedContent
708
- ? `${importResult.importedContent}\n\n${fileContent}`
709
- : fileContent;
710
- const apiDefinitions = (importResult.headerGroups?.length || 0) > 0 || (importResult.endpoints?.length || 0) > 0
711
- ? { headerGroups: importResult.headerGroups || [], endpoints: importResult.endpoints || [] }
712
- : undefined;
713
- if (options.request) {
714
- const namedRequests = (0, parser_1.extractNamedRequests)(fileContentWithImports);
715
- const targetReq = namedRequests.find(r => r.name === options.request);
716
- if (!targetReq) {
717
- console.error(`Error: Named request '${options.request}' not found`);
718
- console.error(`Available requests: ${namedRequests.map(r => r.name).join(', ') || 'none'}`);
719
- process.exit(1);
720
- }
721
- if (options.verbose) {
722
- console.log(colors.info(`Running request: ${options.request}`));
723
- }
724
- const response = await runSingleRequest(targetReq.content, variables, cookieJar, apiDefinitions);
725
- // Format and output single request result
726
- if (options.output === 'json') {
727
- console.log(JSON.stringify({ success: response.status >= 200 && response.status < 400, results: [response] }, null, 2));
728
- }
729
- else {
730
- const isSuccess = response.status >= 200 && response.status < 300;
731
- const lines = (0, response_1.formatResponse)(response, { colors, verbose: options.verbose, showDetails: !isSuccess || options.verbose, redaction });
732
- for (const line of lines) {
733
- console.log(line);
734
- }
735
- }
736
- process.exit(response.status >= 200 && response.status < 400 ? 0 : 1);
737
- }
738
- if (options.sequence) {
739
- const sequences = (0, sequenceRunner_1.extractSequences)(fileContentWithImports);
740
- const targetSeq = sequences.find(s => s.name === options.sequence);
741
- if (!targetSeq) {
742
- console.error(`Error: Sequence '${options.sequence}' not found`);
743
- console.error(`Available sequences: ${sequences.map(s => s.name).join(', ') || 'none'}`);
744
- process.exit(1);
745
- }
746
- if (options.verbose) {
747
- console.log(colors.info(`Running sequence: ${options.sequence}`));
748
- }
749
- const defaultArgs = {};
750
- if (targetSeq.parameters) {
751
- for (const param of targetSeq.parameters) {
752
- if (param.defaultValue !== undefined) {
753
- defaultArgs[param.name] = param.defaultValue;
754
- }
755
- }
756
- }
757
- const seqResult = await (0, sequenceRunner_1.runSequenceWithJar)(targetSeq.content, variables, cookieJar, workingDir, fileContentWithImports, undefined, undefined, defaultArgs, apiDefinitions, tagFilterOptions, importResult.sequenceSources);
758
- seqResult.name = targetSeq.name;
759
- // Format and output single sequence result
760
- if (options.output === 'json') {
761
- console.log(JSON.stringify({ success: seqResult.success, results: [seqResult] }, null, 2));
762
- }
763
- else {
764
- const lines = (0, summary_1.formatSequenceResult)(seqResult, { colors, verbose: options.verbose, redaction });
765
- for (const line of lines) {
766
- console.log(line);
767
- }
768
- const summaryLines = (0, summary_1.formatRunSummary)([seqResult], Date.now() - startTime, colors);
769
- for (const line of summaryLines) {
770
- console.log(line);
771
- }
772
- }
773
- process.exit(seqResult.success ? 0 : 1);
774
- }
775
- }
776
- // Count total tests before running
777
- let totalTestCount = 0;
778
- let filteredTestCount = 0;
779
- for (const filePath of filesToRun) {
780
- const fileContent = fs.readFileSync(filePath, 'utf-8');
781
- const counts = countTestSequences(fileContent, tagFilterOptions);
782
- totalTestCount += counts.total;
783
- filteredTestCount += counts.filtered;
784
- }
785
- if (filteredTestCount === 0) {
786
- if (totalTestCount > 0 && tagFilterOptions) {
787
- console.log(colors.info(`No test sequences match the tag filter (${totalTestCount} total test sequences found)`));
788
- }
789
- else if (totalTestCount === 0) {
790
- console.log(colors.info('No test sequences found. Use "test sequence" to mark sequences for CLI execution.'));
791
- }
792
- process.exit(0);
793
- }
794
- if (options.verbose && isDirectory) {
795
- const tagInfo = tagFilterOptions ? ` (${filteredTestCount} matching tag filter)` : '';
796
- console.log(colors.info(`Running ${filteredTestCount} test sequence(s) from ${filesToRun.length} file(s)${tagInfo}`));
797
- console.log('');
798
- }
799
- // Run tests from each file
800
- for (const filePath of filesToRun) {
801
- const resolvedEnv = resolveEnvironmentForPath(filePath, options.env);
802
- if (resolvedEnv.envNotFound) {
803
- console.error(`Error: Environment '${resolvedEnv.envNotFound}' not found in .nornenv`);
804
- console.error(`Available environments: ${resolvedEnv.availableEnvironments.join(', ') || 'none'}`);
805
- process.exit(1);
806
- }
807
- if (!resolvedEnv.envFilePath && options.env) {
808
- const relPath = isDirectory ? path.relative(inputPath, filePath) : path.basename(filePath);
809
- console.error(colors.warning(`Warning: --env specified but no .nornenv file found for ${relPath}`));
810
- }
811
- mergeSecrets(combinedSecretNames, combinedSecretValues, resolvedEnv.secretNames, resolvedEnv.secretValues);
812
- const redaction = (0, redaction_1.createRedactionOptions)(combinedSecretNames, combinedSecretValues, !options.noRedact);
813
- const fileContent = fs.readFileSync(filePath, 'utf-8');
814
- const fileVariables = (0, parser_1.extractFileLevelVariables)(fileContent);
815
- const variables = { ...resolvedEnv.variables, ...fileVariables };
816
- const cookieJar = (0, httpClient_1.createCookieJar)();
817
- const workingDir = path.dirname(filePath);
818
- const importResult = await (0, parser_1.resolveImports)(fileContent, workingDir, async (importPath) => fsPromises.readFile(importPath, 'utf-8'));
819
- // Check for duplicate definition errors which should block execution
820
- const duplicateErrors2 = importResult.errors.filter(err => err.error.includes('Duplicate header group') ||
821
- err.error.includes('Duplicate endpoint') ||
822
- err.error.includes('Duplicate named request') ||
823
- err.error.includes('Duplicate sequence'));
824
- const otherErrors2 = importResult.errors.filter(err => !err.error.includes('Duplicate header group') &&
825
- !err.error.includes('Duplicate endpoint') &&
826
- !err.error.includes('Duplicate named request') &&
827
- !err.error.includes('Duplicate sequence'));
828
- // Show non-duplicate errors as warnings
829
- for (const err of otherErrors2) {
830
- console.error(colors.warning(`Import warning: ${err.path} - ${err.error}`));
831
- }
832
- // Duplicate errors block execution
833
- if (duplicateErrors2.length > 0) {
834
- console.error(colors.error('Cannot execute - duplicate definitions found:'));
835
- for (const err of duplicateErrors2) {
836
- console.error(colors.error(` ${err.path}: ${err.error}`));
837
- }
838
- process.exit(1);
839
- }
840
- const fileContentWithImports = importResult.importedContent
841
- ? `${importResult.importedContent}\n\n${fileContent}`
842
- : fileContent;
843
- // Check if file has test sequences
844
- const allSequences = (0, sequenceRunner_1.extractSequences)(fileContent);
845
- const testSequences = allSequences.filter(seq => seq.isTest);
846
- const sequences = tagFilterOptions && tagFilterOptions.filters.length > 0
847
- ? testSequences.filter(seq => (0, sequenceRunner_1.sequenceMatchesTags)(seq, tagFilterOptions))
848
- : testSequences;
849
- if (sequences.length === 0) {
850
- // No test sequences in this file, skip
851
- continue;
852
- }
853
- // Show file header in directory mode
854
- if (isDirectory && options.output !== 'json') {
855
- const relPath = path.relative(inputPath, filePath);
856
- console.log(colors.info(`\n━━━ ${relPath} ━━━`));
857
- }
858
- const apiDefinitions = (importResult.headerGroups?.length || 0) > 0 || (importResult.endpoints?.length || 0) > 0
859
- ? { headerGroups: importResult.headerGroups || [], endpoints: importResult.endpoints || [] }
860
- : undefined;
861
- const seqResults = await runAllSequences(fileContentWithImports, variables, cookieJar, workingDir, apiDefinitions, tagFilterOptions, importResult.sequenceSources);
862
- // Tag results with source file for reporting
863
- for (const result of seqResults) {
864
- result.sourceFile = filePath;
865
- allResults.push(result);
866
- if (!result.success) {
867
- overallSuccess = false;
868
- }
869
- allErrors.push(...result.errors);
870
- }
871
- // Output per-file results in pretty mode
872
- if (options.output !== 'json') {
873
- for (const seqResult of seqResults) {
874
- const lines = (0, summary_1.formatSequenceResult)(seqResult, { colors, verbose: options.verbose, redaction });
875
- for (const line of lines) {
876
- console.log(line);
877
- }
878
- }
879
- }
880
- }
881
- const totalDuration = Date.now() - startTime;
882
- const redaction = (0, redaction_1.createRedactionOptions)(combinedSecretNames, combinedSecretValues, !options.noRedact);
883
- // Build result object for reporting
884
- const result = {
885
- success: overallSuccess,
886
- type: 'all-sequences',
887
- results: allResults,
888
- errors: allErrors,
889
- };
890
- // Determine report paths
891
- let junitOutputPath = options.junitOutput;
892
- let htmlOutputPath = options.htmlOutput;
893
- // If --output-dir is specified, auto-generate timestamped filenames
894
- if (options.outputDir) {
895
- const timestamp = generateTimestamp();
896
- // Use input path name for report naming
897
- const baseName = isDirectory ? path.basename(inputPath) : path.basename(inputPath, path.extname(inputPath));
898
- const generatedPaths = generateReportPaths(options.outputDir, baseName + '.norn', timestamp);
899
- // Only use generated paths if explicit paths weren't provided
900
- if (!junitOutputPath) {
901
- junitOutputPath = generatedPaths.junitPath;
902
- }
903
- if (!htmlOutputPath) {
904
- htmlOutputPath = generatedPaths.htmlPath;
905
- }
906
- // Ensure output directory exists
907
- if (!fs.existsSync(options.outputDir)) {
908
- fs.mkdirSync(options.outputDir, { recursive: true });
909
- }
910
- }
911
- // Generate reports if requested
912
- if (junitOutputPath) {
913
- const suiteName = isDirectory ? path.basename(inputPath) : path.basename(inputPath, path.extname(inputPath));
914
- if (result.type === 'request') {
915
- (0, junit_1.generateJUnitReportFromResponse)(result.results[0], options.request || suiteName, { outputPath: junitOutputPath, redaction, suiteName });
916
- }
917
- else {
918
- (0, junit_1.generateJUnitReport)(result.results, { outputPath: junitOutputPath, redaction, suiteName });
919
- }
920
- console.log(colors.info(`JUnit report written to: ${junitOutputPath}`));
921
- }
922
- if (htmlOutputPath) {
923
- const title = `Norn Test Report - ${path.basename(inputPath)}`;
924
- if (result.type === 'request') {
925
- (0, html_1.generateHtmlReportFromResponse)(result.results[0], options.request || path.basename(inputPath), { outputPath: htmlOutputPath, redaction, title });
926
- }
927
- else {
928
- (0, html_1.generateHtmlReport)(result.results, { outputPath: htmlOutputPath, redaction, title });
929
- }
930
- console.log(colors.info(`HTML report written to: ${htmlOutputPath}`));
931
- }
932
- // Output results
933
- if (options.output === 'json') {
934
- // Redact JSON output
935
- const redactedResult = {
936
- ...result,
937
- results: result.results.map(r => {
938
- if ('responses' in r) {
939
- // SequenceResult
940
- return {
941
- ...r,
942
- responses: r.responses.map(resp => ({
943
- ...resp,
944
- body: (0, redaction_1.redactBody)(resp.body, redaction)
945
- })),
946
- steps: r.steps.map(step => ({
947
- ...step,
948
- response: step.response ? {
949
- ...step.response,
950
- body: (0, redaction_1.redactBody)(step.response.body, redaction)
951
- } : undefined
952
- }))
953
- };
954
- }
955
- else {
956
- // HttpResponse
957
- return {
958
- ...r,
959
- body: (0, redaction_1.redactBody)(r.body, redaction)
960
- };
961
- }
962
- })
963
- };
964
- console.log(JSON.stringify(redactedResult, null, 2));
965
- }
966
- else {
967
- // Pretty output - summary only (individual results already printed per-file)
968
- if (result.type !== 'request' && allResults.length > 0) {
969
- // Show summary
970
- const summaryLines = (0, summary_1.formatRunSummary)(allResults, totalDuration, colors);
971
- for (const line of summaryLines) {
972
- console.log(line);
973
- }
974
- }
975
- if (result.errors.length > 0 && result.type === 'request') {
976
- console.error(`\n${colors.error('Errors:')}`);
977
- result.errors.forEach(err => console.error(` ${colors.bullet} ${err}`));
978
- }
979
- }
980
- // Exit with appropriate code
981
- if (options.failOnError && !overallSuccess) {
982
- process.exit(1);
983
- }
984
- }
985
- main().catch((error) => {
986
- console.error('Fatal error:', error.message);
987
- process.exit(1);
988
- });
989
- //# sourceMappingURL=cli.js.map