norn-cli 2.3.0 → 2.4.0

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 (92) hide show
  1. package/.claude/skills/norn-social-campaign/SKILL.md +70 -0
  2. package/CHANGELOG.md +6 -0
  3. package/demos/nornenv-region-refactor/README.md +64 -0
  4. package/dist/cli.js +360 -1
  5. package/out/apiResponseIntellisenseCache.js +394 -0
  6. package/out/assertionRunner.js +567 -0
  7. package/out/cacheDir.js +136 -0
  8. package/out/chatParticipant.js +763 -0
  9. package/out/cli/colors.js +127 -0
  10. package/out/cli/formatters/assertion.js +102 -0
  11. package/out/cli/formatters/index.js +23 -0
  12. package/out/cli/formatters/response.js +106 -0
  13. package/out/cli/formatters/summary.js +246 -0
  14. package/out/cli/redaction.js +237 -0
  15. package/out/cli/reporters/html.js +689 -0
  16. package/out/cli/reporters/index.js +22 -0
  17. package/out/cli/reporters/junit.js +226 -0
  18. package/out/codeLensProvider.js +351 -0
  19. package/out/compareContentProvider.js +85 -0
  20. package/out/completionProvider.js +3739 -0
  21. package/out/contractAssertionSummary.js +225 -0
  22. package/out/contractDecorationProvider.js +243 -0
  23. package/out/coverageCalculator.js +879 -0
  24. package/out/coveragePanel.js +597 -0
  25. package/out/debug/breakpointResolver.js +84 -0
  26. package/out/debug/breakpoints.js +52 -0
  27. package/out/debug/nornDebugAdapter.js +166 -0
  28. package/out/debug/nornDebugSession.js +613 -0
  29. package/out/debug/sequenceLocationIndex.js +77 -0
  30. package/out/debug/types.js +3 -0
  31. package/out/deepClone.js +21 -0
  32. package/out/diagnosticProvider.js +2554 -0
  33. package/out/environmentParser.js +736 -0
  34. package/out/environmentProvider.js +544 -0
  35. package/out/environmentTemplates.js +146 -0
  36. package/out/errors/formatError.js +113 -0
  37. package/out/errors/nornError.js +29 -0
  38. package/out/formUrlEncoded.js +89 -0
  39. package/out/httpClient.js +348 -0
  40. package/out/httpRuntimeOptions.js +16 -0
  41. package/out/importErrors.js +31 -0
  42. package/out/inlayHintResolver.js +70 -0
  43. package/out/jsonFileReader.js +323 -0
  44. package/out/mcpClient.js +193 -0
  45. package/out/mcpConfig.js +184 -0
  46. package/out/mcpToolIntellisenseCache.js +96 -0
  47. package/out/mcpToolSchema.js +50 -0
  48. package/out/nornConfig.js +132 -0
  49. package/out/nornHoverProvider.js +124 -0
  50. package/out/nornInlayHintsProvider.js +191 -0
  51. package/out/nornPrompt.js +755 -0
  52. package/out/nornSqlParser.js +286 -0
  53. package/out/nornapiHoverProvider.js +135 -0
  54. package/out/nornapiInlayHintsProvider.js +94 -0
  55. package/out/nornapiParser.js +324 -0
  56. package/out/nornenvCodeActionProvider.js +101 -0
  57. package/out/nornenvDecorationProvider.js +239 -0
  58. package/out/nornenvFoldingProvider.js +63 -0
  59. package/out/nornenvHoverProvider.js +114 -0
  60. package/out/nornenvInlayHintsProvider.js +99 -0
  61. package/out/nornenvLanguageModel.js +187 -0
  62. package/out/nornenvRegionRefactor.js +267 -0
  63. package/out/nornsqlHoverProvider.js +95 -0
  64. package/out/nornsqlInlayHintsProvider.js +114 -0
  65. package/out/parser.js +839 -0
  66. package/out/pathAccess.js +28 -0
  67. package/out/postmanImportPanel.js +732 -0
  68. package/out/postmanImportPlanner.js +1155 -0
  69. package/out/postmanImportSidebarView.js +532 -0
  70. package/out/quotedString.js +35 -0
  71. package/out/requestPreparation.js +179 -0
  72. package/out/requestValidation.js +146 -0
  73. package/out/responsePanel.js +7754 -0
  74. package/out/schemaGenerator.js +562 -0
  75. package/out/scriptRunner.js +419 -0
  76. package/out/secrets/cliSecrets.js +415 -0
  77. package/out/secrets/crypto.js +105 -0
  78. package/out/secrets/envFileSecrets.js +177 -0
  79. package/out/secrets/keyStore.js +259 -0
  80. package/out/sequenceDeclaration.js +15 -0
  81. package/out/sequenceRunner.js +3590 -0
  82. package/out/sqlAdapterRunner.js +122 -0
  83. package/out/sqlBuiltInAdapters.js +604 -0
  84. package/out/sqlConfig.js +184 -0
  85. package/out/starterCatalog.js +554 -0
  86. package/out/stringUtils.js +25 -0
  87. package/out/swaggerBodyIntellisenseCache.js +114 -0
  88. package/out/swaggerParser.js +464 -0
  89. package/out/testProvider.js +767 -0
  90. package/out/theoryCaseLoader.js +113 -0
  91. package/out/validationCache.js +211 -0
  92. package/package.json +6 -1
@@ -0,0 +1,767 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.NornTestController = void 0;
37
+ const vscode = __importStar(require("vscode"));
38
+ const path = __importStar(require("path"));
39
+ const fs = __importStar(require("fs/promises"));
40
+ const sequenceRunner_1 = require("./sequenceRunner");
41
+ const parser_1 = require("./parser");
42
+ const environmentProvider_1 = require("./environmentProvider");
43
+ const httpClient_1 = require("./httpClient");
44
+ const coverageCalculator_1 = require("./coverageCalculator");
45
+ const extension_1 = require("./extension");
46
+ const theoryCaseLoader_1 = require("./theoryCaseLoader");
47
+ const importErrors_1 = require("./importErrors");
48
+ const environmentParser_1 = require("./environmentParser");
49
+ const sequenceDeclaration_1 = require("./sequenceDeclaration");
50
+ const breakpoints_1 = require("./debug/breakpoints");
51
+ const contractAssertionSummary_1 = require("./contractAssertionSummary");
52
+ // ANSI color codes for Test Explorer output
53
+ const ansi = {
54
+ reset: '\x1b[0m',
55
+ bold: '\x1b[1m',
56
+ dim: '\x1b[2m',
57
+ // Colors
58
+ green: '\x1b[32m',
59
+ red: '\x1b[31m',
60
+ yellow: '\x1b[33m',
61
+ blue: '\x1b[34m',
62
+ cyan: '\x1b[36m',
63
+ magenta: '\x1b[35m',
64
+ gray: '\x1b[90m',
65
+ // Bright versions
66
+ brightGreen: '\x1b[92m',
67
+ brightRed: '\x1b[91m',
68
+ brightYellow: '\x1b[93m',
69
+ brightCyan: '\x1b[96m',
70
+ };
71
+ /**
72
+ * Formats tags for display in test description.
73
+ * e.g., "@smoke @team(CustomerExp)"
74
+ */
75
+ function formatTagsDisplay(tags) {
76
+ if (tags.length === 0) {
77
+ return '';
78
+ }
79
+ return tags.map(t => t.value ? `@${t.name}(${t.value})` : `@${t.name}`).join(' ');
80
+ }
81
+ /**
82
+ * Gets the primary tag for grouping (first simple tag, or "Untagged").
83
+ */
84
+ function getPrimaryTagGroup(tags) {
85
+ // Look for simple tags (no value) first - these are typically categories like @smoke, @regression
86
+ const simpleTag = tags.find(t => !t.value);
87
+ if (simpleTag) {
88
+ return `@${simpleTag.name}`;
89
+ }
90
+ // Fall back to first tag with value
91
+ if (tags.length > 0) {
92
+ const t = tags[0];
93
+ return t.value ? `@${t.name}(${t.value})` : `@${t.name}`;
94
+ }
95
+ return 'Untagged';
96
+ }
97
+ /**
98
+ * Creates TestTag objects from sequence tags.
99
+ */
100
+ function createTestTags(controller, tags) {
101
+ return tags.map(tag => {
102
+ const tagId = tag.value ? `${tag.name}:${tag.value}` : tag.name;
103
+ return new vscode.TestTag(tagId);
104
+ });
105
+ }
106
+ /**
107
+ * Norn Test Controller - provides Test Explorer integration for .norn files.
108
+ *
109
+ * Features:
110
+ * - Discovers test sequences from .norn files
111
+ * - Supports parameterized tests via @data and @theory annotations
112
+ * - Streams test progress during execution
113
+ * - Maps sequence tags to Test Explorer tags for filtering
114
+ */
115
+ class NornTestController {
116
+ controller;
117
+ disposables = [];
118
+ // Map from test item ID to sequence info
119
+ testData = new WeakMap();
120
+ constructor() {
121
+ this.controller = vscode.tests.createTestController('nornTests', 'Norn Tests');
122
+ // Set up resolve handler for lazy loading
123
+ this.controller.resolveHandler = async (item) => {
124
+ if (!item) {
125
+ // Initial request: discover all test files
126
+ await this.discoverAllTests();
127
+ }
128
+ else {
129
+ // Resolve a specific item (file or sequence with theory data)
130
+ await this.resolveTestItem(item);
131
+ }
132
+ };
133
+ // Create run profile
134
+ this.controller.createRunProfile('Run Tests', vscode.TestRunProfileKind.Run, (request, token) => this.runTests(request, token), true // isDefault
135
+ );
136
+ this.controller.createRunProfile('Debug Tests', vscode.TestRunProfileKind.Debug, (request, token) => this.debugTests(request, token), true);
137
+ // Watch for file changes
138
+ const fileWatcher = vscode.workspace.createFileSystemWatcher('**/*.norn');
139
+ fileWatcher.onDidCreate(uri => this.onFileCreated(uri));
140
+ fileWatcher.onDidChange(uri => this.onFileChanged(uri));
141
+ fileWatcher.onDidDelete(uri => this.onFileDeleted(uri));
142
+ this.disposables.push(fileWatcher);
143
+ // Watch for document changes (unsaved edits)
144
+ this.disposables.push(vscode.workspace.onDidChangeTextDocument(e => {
145
+ if (e.document.languageId === 'norn') {
146
+ this.updateTestsInDocument(e.document);
147
+ }
148
+ }));
149
+ // Initial discovery for open documents
150
+ vscode.workspace.textDocuments.forEach(doc => {
151
+ if (doc.languageId === 'norn') {
152
+ this.updateTestsInDocument(doc);
153
+ }
154
+ });
155
+ }
156
+ /**
157
+ * Discovers all .norn test files in the workspace.
158
+ */
159
+ async discoverAllTests() {
160
+ const files = await vscode.workspace.findFiles('**/*.norn', '**/node_modules/**');
161
+ for (const uri of files) {
162
+ await this.parseTestFile(uri);
163
+ }
164
+ }
165
+ /**
166
+ * Parses a .norn file and creates test items for test sequences.
167
+ */
168
+ async parseTestFile(uri) {
169
+ try {
170
+ const content = await fs.readFile(uri.fsPath, 'utf-8');
171
+ const lines = content.split('\n');
172
+ const sequences = (0, sequenceRunner_1.extractSequences)(content);
173
+ // Filter to only test sequences (isTest: true)
174
+ const testSequences = sequences.filter(seq => seq.isTest);
175
+ if (testSequences.length === 0) {
176
+ // Remove file from test explorer if it had tests before
177
+ this.controller.items.delete(uri.toString());
178
+ return;
179
+ }
180
+ // Create or update file item
181
+ const fileName = path.basename(uri.fsPath);
182
+ let fileItem = this.controller.items.get(uri.toString());
183
+ if (!fileItem) {
184
+ fileItem = this.controller.createTestItem(uri.toString(), fileName, uri);
185
+ this.controller.items.add(fileItem);
186
+ }
187
+ this.rebuildFileItemTests(fileItem, uri, lines, testSequences);
188
+ }
189
+ catch (error) {
190
+ console.error(`Failed to parse test file ${uri.fsPath}:`, error);
191
+ }
192
+ }
193
+ rebuildFileItemTests(fileItem, uri, lines, testSequences) {
194
+ fileItem.children.replace([]);
195
+ const tagGroups = new Map();
196
+ for (const seq of testSequences) {
197
+ const groupName = getPrimaryTagGroup(seq.tags);
198
+ if (!tagGroups.has(groupName)) {
199
+ tagGroups.set(groupName, []);
200
+ }
201
+ tagGroups.get(groupName).push(seq);
202
+ }
203
+ const sortedGroups = Array.from(tagGroups.keys()).sort((a, b) => {
204
+ if (a === 'Untagged') {
205
+ return 1;
206
+ }
207
+ if (b === 'Untagged') {
208
+ return -1;
209
+ }
210
+ return a.localeCompare(b);
211
+ });
212
+ for (const groupName of sortedGroups) {
213
+ const seqs = tagGroups.get(groupName);
214
+ if (sortedGroups.length === 1) {
215
+ for (const seq of seqs) {
216
+ const declarationLine = (0, sequenceDeclaration_1.findSequenceDeclarationLine)(lines, seq.startLine, seq.endLine, { testOnly: true });
217
+ const testItem = this.createTestItemForSequence(uri, seq, fileItem, declarationLine);
218
+ fileItem.children.add(testItem);
219
+ }
220
+ continue;
221
+ }
222
+ const groupId = `${uri.toString()}::group::${groupName}`;
223
+ const groupItem = this.controller.createTestItem(groupId, groupName, uri);
224
+ groupItem.description = `${seqs.length} test${seqs.length > 1 ? 's' : ''}`;
225
+ for (const seq of seqs) {
226
+ const declarationLine = (0, sequenceDeclaration_1.findSequenceDeclarationLine)(lines, seq.startLine, seq.endLine, { testOnly: true });
227
+ const testItem = this.createTestItemForSequence(uri, seq, groupItem, declarationLine);
228
+ groupItem.children.add(testItem);
229
+ }
230
+ fileItem.children.add(groupItem);
231
+ }
232
+ }
233
+ /**
234
+ * Creates a test item for a sequence, including child items for theory cases.
235
+ */
236
+ createTestItemForSequence(uri, seq, parent, declarationLine) {
237
+ const testId = `${uri.toString()}::${seq.name}`;
238
+ const testItem = this.controller.createTestItem(testId, seq.name, uri);
239
+ // Set range for navigation
240
+ testItem.range = new vscode.Range(declarationLine, 0, seq.endLine, 0);
241
+ // Add tags
242
+ testItem.tags = createTestTags(this.controller, seq.tags);
243
+ // Build description showing tags and param info
244
+ const descParts = [];
245
+ const tagsDisplay = formatTagsDisplay(seq.tags);
246
+ if (tagsDisplay) {
247
+ descParts.push(tagsDisplay);
248
+ }
249
+ if (seq.theoryData && seq.theoryData.cases.length > 0) {
250
+ descParts.push(`${seq.theoryData.cases.length} cases`);
251
+ }
252
+ else if (seq.parameters.length > 0) {
253
+ const paramNames = seq.parameters.map(p => p.name).join(', ');
254
+ descParts.push(`(${paramNames})`);
255
+ }
256
+ if (descParts.length > 0) {
257
+ testItem.description = descParts.join(' · ');
258
+ }
259
+ // Store sequence data
260
+ this.testData.set(testItem, { sequence: seq, uri });
261
+ // If this sequence has theory data (parameterized), create child items
262
+ if (seq.theoryData && seq.theoryData.cases.length > 0) {
263
+ testItem.canResolveChildren = true;
264
+ // Eagerly create children for known cases
265
+ this.createTheoryCaseItems(testItem, seq, uri);
266
+ }
267
+ return testItem;
268
+ }
269
+ /**
270
+ * Creates child test items for each theory case.
271
+ */
272
+ createTheoryCaseItems(parentItem, seq, uri) {
273
+ if (!seq.theoryData) {
274
+ return;
275
+ }
276
+ for (let i = 0; i < seq.theoryData.cases.length; i++) {
277
+ const caseParams = seq.theoryData.cases[i];
278
+ const caseLabel = (0, theoryCaseLoader_1.formatTheoryCaseLabel)(caseParams);
279
+ const caseId = `${parentItem.id}::case${i}`;
280
+ const caseItem = this.controller.createTestItem(caseId, caseLabel, uri);
281
+ caseItem.range = parentItem.range; // Same range as parent
282
+ caseItem.tags = parentItem.tags; // Inherit tags
283
+ this.testData.set(caseItem, {
284
+ sequence: seq,
285
+ uri,
286
+ caseIndex: i,
287
+ caseParams
288
+ });
289
+ parentItem.children.add(caseItem);
290
+ }
291
+ }
292
+ /**
293
+ * Resolves children of a test item (for lazy loading theory cases from @theory files).
294
+ */
295
+ async resolveTestItem(item) {
296
+ const data = this.testData.get(item);
297
+ if (!data) {
298
+ return;
299
+ }
300
+ // If this has a @theory source file, load it now
301
+ if (data.sequence.theoryData?.source && data.sequence.theoryData.cases.length === 0) {
302
+ await this.loadTheoryFile(item, data.sequence, data.uri);
303
+ }
304
+ }
305
+ /**
306
+ * Loads theory data from an external JSON file.
307
+ */
308
+ async loadTheoryFile(parentItem, seq, uri) {
309
+ if (!seq.theoryData?.source) {
310
+ return;
311
+ }
312
+ try {
313
+ const cases = await (0, theoryCaseLoader_1.loadTheoryCasesFromSource)(seq.theoryData.source, path.dirname(uri.fsPath), seq.parameters.map(param => param.name));
314
+ // Update the theory data
315
+ seq.theoryData.cases = cases;
316
+ // Create child items
317
+ this.createTheoryCaseItems(parentItem, seq, uri);
318
+ }
319
+ catch (error) {
320
+ parentItem.error = `Failed to load theory file: ${seq.theoryData.source}`;
321
+ console.error(`Failed to load theory file for ${seq.name}:`, error);
322
+ }
323
+ }
324
+ /**
325
+ * Runs the requested tests.
326
+ */
327
+ async runTests(request, token) {
328
+ const run = this.controller.createTestRun(request);
329
+ const queue = [];
330
+ // Collect tests to run
331
+ if (request.include) {
332
+ request.include.forEach(test => queue.push(test));
333
+ }
334
+ else {
335
+ this.controller.items.forEach(test => queue.push(test));
336
+ }
337
+ // Process queue
338
+ while (queue.length > 0 && !token.isCancellationRequested) {
339
+ const test = queue.shift();
340
+ // Skip excluded tests
341
+ if (request.exclude?.includes(test)) {
342
+ continue;
343
+ }
344
+ const data = this.testData.get(test);
345
+ if (!data) {
346
+ // This is a file item - queue its children
347
+ test.children.forEach(child => queue.push(child));
348
+ continue;
349
+ }
350
+ // Check if this is a parameterized test with children
351
+ if (test.children.size > 0 && data.caseIndex === undefined) {
352
+ // Queue children instead of running parent
353
+ test.children.forEach(child => queue.push(child));
354
+ continue;
355
+ }
356
+ // Run the test
357
+ await this.runSingleTest(run, test, data, token);
358
+ }
359
+ run.end();
360
+ // Recalculate coverage after test run completes (uses cached swagger specs)
361
+ (0, coverageCalculator_1.recalculateCoverageAfterExecution)().catch(() => {
362
+ // Silently ignore coverage calculation errors
363
+ });
364
+ }
365
+ collectDebugTargets(item, exclude, targets, seenIds) {
366
+ if (exclude?.includes(item)) {
367
+ return;
368
+ }
369
+ const data = this.testData.get(item);
370
+ if (data) {
371
+ const hasChildCases = item.children.size > 0 && data.caseIndex === undefined;
372
+ if (hasChildCases) {
373
+ item.children.forEach(child => this.collectDebugTargets(child, exclude, targets, seenIds));
374
+ return;
375
+ }
376
+ if (!seenIds.has(item.id)) {
377
+ targets.push({ item, data });
378
+ seenIds.add(item.id);
379
+ }
380
+ return;
381
+ }
382
+ item.children.forEach(child => this.collectDebugTargets(child, exclude, targets, seenIds));
383
+ }
384
+ getDebugTargets(request) {
385
+ const roots = request.include
386
+ ? Array.from(request.include)
387
+ : [];
388
+ if (!request.include) {
389
+ this.controller.items.forEach(item => roots.push(item));
390
+ }
391
+ const targets = [];
392
+ const seenIds = new Set();
393
+ for (const root of roots) {
394
+ this.collectDebugTargets(root, request.exclude, targets, seenIds);
395
+ }
396
+ return targets;
397
+ }
398
+ async pickSingleDebugTarget(targets) {
399
+ if (targets.length === 0) {
400
+ return undefined;
401
+ }
402
+ if (targets.length === 1) {
403
+ return targets[0];
404
+ }
405
+ const items = targets.map(target => ({
406
+ label: target.item.label,
407
+ description: `${path.basename(target.data.uri.fsPath)} · sequence ${target.data.sequence.name}`,
408
+ target
409
+ }));
410
+ const picked = await vscode.window.showQuickPick(items, {
411
+ title: 'Select one test to debug',
412
+ placeHolder: 'Debug profile supports a single test/case per session in v1',
413
+ ignoreFocusOut: true
414
+ });
415
+ return picked?.target;
416
+ }
417
+ async debugTests(request, token) {
418
+ const run = this.controller.createTestRun(request);
419
+ try {
420
+ const targets = this.getDebugTargets(request);
421
+ if (targets.length === 0) {
422
+ run.appendOutput('\r\nNo debuggable test target selected.\r\n');
423
+ run.end();
424
+ return;
425
+ }
426
+ const selected = await this.pickSingleDebugTarget(targets);
427
+ if (!selected) {
428
+ run.appendOutput('\r\nDebug canceled: no single target selected.\r\n');
429
+ run.end();
430
+ return;
431
+ }
432
+ if (token.isCancellationRequested) {
433
+ run.skipped(selected.item);
434
+ run.end();
435
+ return;
436
+ }
437
+ run.started(selected.item);
438
+ const args = selected.data.caseParams
439
+ ? Object.fromEntries(Object.entries(selected.data.caseParams).map(([key, value]) => [key, String(value)]))
440
+ : undefined;
441
+ const config = {
442
+ type: 'norn',
443
+ request: 'launch',
444
+ name: `Debug ${selected.data.sequence.name}`,
445
+ file: selected.data.uri.fsPath,
446
+ sequence: selected.data.sequence.name,
447
+ stopOnEntry: !(0, breakpoints_1.hasEnabledNornBreakpoints)(),
448
+ args
449
+ };
450
+ const folder = vscode.workspace.getWorkspaceFolder(selected.data.uri);
451
+ const started = await vscode.debug.startDebugging(folder, config, { testRun: run });
452
+ if (!started) {
453
+ run.errored(selected.item, new vscode.TestMessage('Failed to start debug session.'));
454
+ run.end();
455
+ return;
456
+ }
457
+ run.end();
458
+ }
459
+ catch (error) {
460
+ const message = error instanceof Error ? error.message : String(error);
461
+ run.appendOutput(`\r\nDebug launch error: ${message}\r\n`);
462
+ run.end();
463
+ }
464
+ }
465
+ /**
466
+ * Runs a single test (or single theory case).
467
+ */
468
+ async runSingleTest(run, test, data, token) {
469
+ run.started(test);
470
+ if (token.isCancellationRequested) {
471
+ run.skipped(test);
472
+ return;
473
+ }
474
+ try {
475
+ const startTime = Date.now();
476
+ // Load file content and variables
477
+ const content = await fs.readFile(data.uri.fsPath, 'utf-8');
478
+ const fileVariables = (0, parser_1.extractVariables)(content);
479
+ const envErrors = (0, environmentProvider_1.getEnvironmentImportErrorDetails)(data.uri.fsPath);
480
+ if (envErrors.length > 0) {
481
+ const firstError = envErrors[0];
482
+ const location = (0, environmentParser_1.formatNornenvErrorLocationForSource)(data.uri.fsPath, firstError.filePath, firstError.line);
483
+ const moreErrors = envErrors.length > 1
484
+ ? `\n(+${envErrors.length - 1} more .nornenv error${envErrors.length > 2 ? 's' : ''})`
485
+ : '';
486
+ run.failed(test, new vscode.TestMessage(`Cannot run while .nornenv has errors (${location}): ${firstError.message}${moreErrors}`), Date.now() - startTime);
487
+ return;
488
+ }
489
+ const unlocked = await (0, environmentProvider_1.promptForMissingEnvironmentSecretKeys)(data.uri.fsPath);
490
+ if (!unlocked) {
491
+ run.failed(test, new vscode.TestMessage('Cannot run while .nornenv secrets are locked. Provide the shared key to continue.'), Date.now() - startTime);
492
+ return;
493
+ }
494
+ const secretErrors = (0, environmentProvider_1.getEnvironmentSecretErrorDetails)(data.uri.fsPath);
495
+ if (secretErrors.length > 0) {
496
+ const firstError = secretErrors[0];
497
+ const location = (0, environmentParser_1.formatNornenvErrorLocationForSource)(data.uri.fsPath, firstError.filePath, firstError.line);
498
+ const moreErrors = secretErrors.length > 1
499
+ ? `\n(+${secretErrors.length - 1} more secret error${secretErrors.length > 2 ? 's' : ''})`
500
+ : '';
501
+ run.failed(test, new vscode.TestMessage(`Cannot run while .nornenv secrets are invalid (${location}): ${firstError.message}${moreErrors}`), Date.now() - startTime);
502
+ return;
503
+ }
504
+ // Get environment variables
505
+ const envVars = (0, environmentProvider_1.getEnvironmentVariables)(data.uri.fsPath);
506
+ // Merge variables (env vars take precedence)
507
+ const mergedVariables = (0, parser_1.attachEnvironmentScope)({ ...fileVariables, ...envVars }, envVars);
508
+ // If this is a theory case, add case params
509
+ if (data.caseParams) {
510
+ for (const [key, value] of Object.entries(data.caseParams)) {
511
+ mergedVariables[key] = String(value);
512
+ }
513
+ }
514
+ // Helper to output with colors
515
+ const output = (text) => {
516
+ run.appendOutput(text, undefined, test);
517
+ };
518
+ // Output test header
519
+ const testName = data.caseParams
520
+ ? `${data.sequence.name}${(0, theoryCaseLoader_1.formatTheoryCaseLabel)(data.caseParams)}`
521
+ : data.sequence.name;
522
+ output(`\r\n${ansi.bold}${ansi.brightCyan}▸ ${testName}${ansi.reset}\r\n`);
523
+ // Create progress callback for streaming output with colors
524
+ const progressCallback = (info) => {
525
+ const prefix = ' '.repeat(info.nestingDepth || 0);
526
+ const stepNum = `${ansi.gray}[${info.currentStep}/${info.totalSteps}]${ansi.reset}`;
527
+ let icon = '';
528
+ let desc = info.stepDescription;
529
+ // Color based on step type and result
530
+ switch (info.stepType) {
531
+ case 'request':
532
+ case 'namedRequest':
533
+ // HTTP method coloring
534
+ desc = desc.replace(/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+/, `${ansi.cyan}$1${ansi.reset} `);
535
+ // Add status code coloring if present
536
+ desc = desc.replace(/\s+(\d{3})\s*$/, (_, code) => {
537
+ const status = parseInt(code);
538
+ if (status >= 200 && status < 300) {
539
+ return ` ${ansi.green}${code}${ansi.reset}`;
540
+ }
541
+ if (status >= 400) {
542
+ return ` ${ansi.red}${code}${ansi.reset}`;
543
+ }
544
+ return ` ${ansi.yellow}${code}${ansi.reset}`;
545
+ });
546
+ icon = `${ansi.blue}→${ansi.reset}`;
547
+ break;
548
+ case 'assertion':
549
+ if (desc.includes('✓')) {
550
+ icon = `${ansi.green}✓${ansi.reset}`;
551
+ desc = desc.replace('✓ ', '');
552
+ }
553
+ else if (desc.includes('✗')) {
554
+ icon = `${ansi.red}✗${ansi.reset}`;
555
+ desc = desc.replace('✗ ', '');
556
+ }
557
+ break;
558
+ case 'print':
559
+ icon = `${ansi.magenta}◆${ansi.reset}`;
560
+ desc = desc.replace(/^print:\s*/, '');
561
+ desc = `${ansi.dim}${desc}${ansi.reset}`;
562
+ break;
563
+ case 'script':
564
+ icon = `${ansi.yellow}⚡${ansi.reset}`;
565
+ break;
566
+ case 'sql':
567
+ icon = `${ansi.blue}▣${ansi.reset}`;
568
+ break;
569
+ case 'wait':
570
+ icon = `${ansi.gray}⏱${ansi.reset}`;
571
+ break;
572
+ case 'runSequence':
573
+ case 'sequenceStart':
574
+ icon = `${ansi.brightCyan}▶${ansi.reset}`;
575
+ break;
576
+ case 'sequenceEnd':
577
+ icon = `${ansi.brightCyan}■${ansi.reset}`;
578
+ break;
579
+ default:
580
+ icon = ' ';
581
+ }
582
+ const contractDisplay = info.stepResult?.assertion
583
+ ? (0, contractAssertionSummary_1.buildContractAssertionDisplay)(info.stepResult.assertion)
584
+ : undefined;
585
+ if (contractDisplay) {
586
+ desc = contractDisplay.titleText;
587
+ }
588
+ output(`${prefix}${stepNum} ${icon} ${desc}\r\n`);
589
+ if (contractDisplay && info.stepResult?.assertion) {
590
+ if (!info.stepResult.assertion.passed) {
591
+ for (const issue of contractDisplay.issueTexts) {
592
+ output(`${prefix} ${ansi.red}✗ ${issue}${ansi.reset}\r\n`);
593
+ }
594
+ if (contractDisplay.remainingIssueCount > 0) {
595
+ output(`${prefix} ${ansi.dim}+${contractDisplay.remainingIssueCount} more${ansi.reset}\r\n`);
596
+ }
597
+ }
598
+ }
599
+ };
600
+ // Resolve imports
601
+ const workingDir = path.dirname(data.uri.fsPath);
602
+ const importResult = await (0, parser_1.resolveImports)(content, workingDir, async (p) => fs.readFile(p, 'utf-8'), undefined, undefined, data.uri.fsPath);
603
+ const duplicateErrors = (0, importErrors_1.getBlockingImportErrors)(importResult.errors);
604
+ if (duplicateErrors.length > 0) {
605
+ const errorMessage = duplicateErrors.map(err => `${err.path}: ${err.error}`).join('\n');
606
+ run.failed(test, new vscode.TestMessage(`Import error: duplicate definitions found\n${errorMessage}`), Date.now() - startTime);
607
+ return;
608
+ }
609
+ // Combine content with imports
610
+ const fullContent = importResult.importedContent
611
+ ? importResult.importedContent + '\n\n' + content
612
+ : content;
613
+ // Create a fresh cookie jar for this test run
614
+ const cookieJar = (0, httpClient_1.createCookieJar)();
615
+ // Run the sequence
616
+ const result = await (0, sequenceRunner_1.runSequenceWithJar)(data.sequence.content, mergedVariables, cookieJar, workingDir, fullContent, progressCallback, undefined, // callStack
617
+ data.caseParams ? Object.fromEntries(Object.entries(data.caseParams).map(([k, v]) => [k, String(v)])) : undefined, { headerGroups: importResult.headerGroups, endpoints: importResult.endpoints }, undefined, // tagFilterOptions
618
+ importResult.sequenceSources, // For resolving script paths relative to imported files
619
+ importResult.sqlOperationsBySource, {
620
+ filePath: data.uri.fsPath,
621
+ sequenceName: data.sequence.name,
622
+ sequenceStartLine: data.sequence.startLine
623
+ });
624
+ const duration = Date.now() - startTime;
625
+ // Save schema validation results to cache for gutter icons
626
+ (0, extension_1.saveSchemaValidationResults)(result, data.uri.fsPath, data.sequence.startLine + 1);
627
+ // Output summary line - just show pass/fail with duration
628
+ if (result.success) {
629
+ output(`${ansi.green}${ansi.bold}✓ PASSED${ansi.reset} ${ansi.gray}${duration}ms${ansi.reset}\r\n`);
630
+ run.passed(test, duration);
631
+ }
632
+ else {
633
+ // Output detailed failure info
634
+ output(`${ansi.gray}${'─'.repeat(40)}${ansi.reset}\r\n`);
635
+ // Create failure messages from assertion results
636
+ const messages = [];
637
+ for (const assertion of result.assertionResults) {
638
+ if (!assertion.passed) {
639
+ // Build display expression with friendly name when available
640
+ let displayExpr = assertion.expression;
641
+ if (assertion.friendlyName && assertion.responseIndex) {
642
+ displayExpr = displayExpr.replace('$' + assertion.responseIndex, assertion.friendlyName);
643
+ }
644
+ const contractDisplay = (0, contractAssertionSummary_1.buildContractAssertionDisplay)(assertion);
645
+ if (contractDisplay) {
646
+ output(`${ansi.red}✗ ${contractDisplay.titleText}${ansi.reset}\r\n`);
647
+ if (contractDisplay.issueTexts.length > 0) {
648
+ output(` ${ansi.dim}Issues:${ansi.reset}\r\n`);
649
+ for (const issue of contractDisplay.issueTexts) {
650
+ output(` ${ansi.red}✗ ${issue}${ansi.reset}\r\n`);
651
+ }
652
+ if (contractDisplay.remainingIssueCount > 0) {
653
+ output(` ${ansi.dim}+${contractDisplay.remainingIssueCount} more${ansi.reset}\r\n`);
654
+ }
655
+ }
656
+ else if (assertion.error) {
657
+ output(` ${ansi.red}${assertion.error}${ansi.reset}\r\n`);
658
+ }
659
+ if (assertion.message) {
660
+ output(` ${ansi.yellow}${assertion.message}${ansi.reset}\r\n`);
661
+ }
662
+ output(`\r\n`);
663
+ messages.push(new vscode.TestMessage(assertion.message || `Contract assertion failed: ${contractDisplay.summaryText}`));
664
+ continue;
665
+ }
666
+ // Output to test results
667
+ output(`${ansi.red}✗ ${displayExpr}${ansi.reset}\r\n`);
668
+ if (assertion.leftValue !== undefined && assertion.rightValue !== undefined) {
669
+ output(` ${ansi.dim}Expected:${ansi.reset} ${ansi.green}${JSON.stringify(assertion.rightValue)}${ansi.reset}\r\n`);
670
+ output(` ${ansi.dim}Actual:${ansi.reset} ${ansi.red}${JSON.stringify(assertion.leftValue)}${ansi.reset}\r\n`);
671
+ }
672
+ if (assertion.message) {
673
+ output(` ${ansi.yellow}${assertion.message}${ansi.reset}\r\n`);
674
+ }
675
+ output(`\r\n`);
676
+ // Create VS Code diff message
677
+ const msg = vscode.TestMessage.diff(assertion.message || `Assertion failed: ${displayExpr}`, String(assertion.rightValue ?? ''), // expected
678
+ String(assertion.leftValue ?? '') // actual
679
+ );
680
+ messages.push(msg);
681
+ }
682
+ }
683
+ // Add any errors with details
684
+ for (const error of result.errors) {
685
+ // Split error into lines and output each properly
686
+ const errorLines = error.split('\n');
687
+ for (const line of errorLines) {
688
+ if (line.trim()) {
689
+ output(`${ansi.red}${line}${ansi.reset}\r\n`);
690
+ }
691
+ }
692
+ messages.push(new vscode.TestMessage(error));
693
+ }
694
+ // Add request/response info for failed requests (use steps for URL info)
695
+ for (const step of result.steps) {
696
+ if (step.type === 'request' && step.response && step.response.status >= 400) {
697
+ output(`\r\n${ansi.yellow}Request Details:${ansi.reset}\r\n`);
698
+ if (step.requestUrl) {
699
+ output(` ${ansi.dim}URL:${ansi.reset} ${step.requestMethod || 'GET'} ${step.requestUrl}\r\n`);
700
+ }
701
+ output(` ${ansi.dim}Status:${ansi.reset} ${ansi.red}${step.response.status} ${step.response.statusText}${ansi.reset}\r\n`);
702
+ if (step.response.body) {
703
+ const bodyPreview = typeof step.response.body === 'string'
704
+ ? step.response.body.slice(0, 500)
705
+ : JSON.stringify(step.response.body, null, 2).slice(0, 500);
706
+ output(` ${ansi.dim}Response:${ansi.reset}\r\n`);
707
+ // Indent each line of the body for proper formatting
708
+ const indentedBody = bodyPreview.split('\n').map(line => ` ${line}`).join('\r\n');
709
+ output(`${ansi.gray}${indentedBody}${bodyPreview.length >= 500 ? '...' : ''}${ansi.reset}\r\n`);
710
+ }
711
+ }
712
+ }
713
+ output(`${ansi.gray}${'─'.repeat(50)}${ansi.reset}\r\n`);
714
+ output(`${ansi.red}${ansi.bold}✗ FAILED${ansi.reset} ${ansi.gray}${duration}ms${ansi.reset}\r\n`);
715
+ if (messages.length === 0) {
716
+ messages.push(new vscode.TestMessage('Test failed'));
717
+ }
718
+ run.failed(test, messages, duration);
719
+ }
720
+ }
721
+ catch (error) {
722
+ const message = error instanceof Error ? error.message : String(error);
723
+ run.appendOutput(`\r\n${ansi.red}${ansi.bold}ERROR${ansi.reset}\r\n${ansi.red}${message}${ansi.reset}\r\n`, undefined, test);
724
+ run.errored(test, new vscode.TestMessage(message));
725
+ }
726
+ }
727
+ /**
728
+ * Updates tests when a document changes.
729
+ */
730
+ async updateTestsInDocument(document) {
731
+ if (document.uri.scheme !== 'file') {
732
+ return;
733
+ }
734
+ const content = document.getText();
735
+ const lines = content.split('\n');
736
+ const sequences = (0, sequenceRunner_1.extractSequences)(content);
737
+ const testSequences = sequences.filter(seq => seq.isTest);
738
+ if (testSequences.length === 0) {
739
+ this.controller.items.delete(document.uri.toString());
740
+ return;
741
+ }
742
+ // Create or update file item
743
+ const fileName = path.basename(document.uri.fsPath);
744
+ const uri = document.uri;
745
+ let fileItem = this.controller.items.get(uri.toString());
746
+ if (!fileItem) {
747
+ fileItem = this.controller.createTestItem(uri.toString(), fileName, uri);
748
+ this.controller.items.add(fileItem);
749
+ }
750
+ this.rebuildFileItemTests(fileItem, uri, lines, testSequences);
751
+ }
752
+ async onFileCreated(uri) {
753
+ await this.parseTestFile(uri);
754
+ }
755
+ async onFileChanged(uri) {
756
+ await this.parseTestFile(uri);
757
+ }
758
+ onFileDeleted(uri) {
759
+ this.controller.items.delete(uri.toString());
760
+ }
761
+ dispose() {
762
+ this.controller.dispose();
763
+ this.disposables.forEach(d => d.dispose());
764
+ }
765
+ }
766
+ exports.NornTestController = NornTestController;
767
+ //# sourceMappingURL=testProvider.js.map