datagrok-tools 6.0.8 → 6.1.1

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.
@@ -4,12 +4,14 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
4
4
  Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
+ exports.matchesFilter = matchesFilter;
7
8
  exports.test = test;
8
9
  var _child_process = require("child_process");
9
10
  var _util = require("util");
10
11
  var _fs = _interopRequireDefault(require("fs"));
11
12
  var _os = _interopRequireDefault(require("os"));
12
13
  var _path = _interopRequireDefault(require("path"));
14
+ var _readline = _interopRequireDefault(require("readline"));
13
15
  var _jsYaml = _interopRequireDefault(require("js-yaml"));
14
16
  var utils = _interopRequireWildcard(require("../utils/utils"));
15
17
  var color = _interopRequireWildcard(require("../utils/color-utils"));
@@ -23,12 +25,85 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
23
25
  const execAsync = (0, _util.promisify)(_child_process.exec);
24
26
  const execFileAsync = (0, _util.promisify)(_child_process.execFile);
25
27
  const testInvocationTimeout = 3600000;
26
- const availableCommandOptions = ['host', 'package', 'csv', 'gui', 'catchUnhandled', 'platform', 'core', 'report', 'skip-build', 'skip-publish', 'path', 'record', 'verbose', 'benchmark', 'category', 'test', 'stress-test', 'link', 'tag', 'ci-cd', 'debug', 'no-retry'];
28
+ const availableCommandOptions = ['host', 'package', 'csv', 'gui', 'catchUnhandled', 'platform', 'core', 'report', 'skip-build', 'skip-publish', 'path', 'record', 'verbose', 'benchmark', 'category', 'test', 'stress-test', 'link', 'tag', 'ci-cd', 'debug', 'no-retry', 'dartium', 'f', 'params', 'logfailed'];
27
29
  const curDir = process.cwd();
30
+
31
+ /** Expands camelCase to space-separated lowercase: "dataManipulation" → "data manipulation" */
32
+ function expandCamelCase(s) {
33
+ return s.replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase();
34
+ }
35
+
36
+ /** Checks if a segment matches a filter segment (case-insensitive substring, with camelCase expansion) */
37
+ function segmentMatches(testSegment, filterSegment) {
38
+ if (filterSegment === '*') return true;
39
+ const t = expandCamelCase(testSegment.trim());
40
+ const f = expandCamelCase(filterSegment.trim());
41
+ return t.includes(f);
42
+ }
43
+
44
+ /**
45
+ * Fluent test name filter. Matches a full test name like "Core: d4 | Viewers | Data Manipulation | Scatter plot".
46
+ *
47
+ * Supports: substring search, `/`-anchored paths, `|`/`/` delimited segments, camelCase expansion, `*` wildcards.
48
+ */
49
+ function matchesFilter(fullName, filter) {
50
+ // Normalize test name: strip "Core: " prefix, split by " | "
51
+ const normalized = fullName.replace(/^Core:\s*/, '');
52
+ const testSegments = normalized.split(/\s*\|\s*/);
53
+
54
+ // Detect anchored mode: ^ prefix
55
+ const anchored = filter.startsWith('^');
56
+ const rawFilter = anchored ? filter.slice(1) : filter;
57
+
58
+ // Split filter by / or | (with optional spaces)
59
+ const filterParts = rawFilter.split(/\s*[/|]\s*/).map(s => s.trim()).filter(s => s.length > 0);
60
+
61
+ // Substring mode: single segment, no delimiters, no anchor
62
+ if (filterParts.length <= 1 && !anchored) {
63
+ const f = expandCamelCase(filterParts[0] || '');
64
+ const full = expandCamelCase(normalized);
65
+ return full.includes(f);
66
+ }
67
+
68
+ // Segment mode: find filterParts in order within testSegments
69
+ if (anchored) {
70
+ // Anchored: filter[0] must match testSegments[0], filter[1] must match testSegments[1], etc.
71
+ if (filterParts.length > testSegments.length) return false;
72
+ for (let i = 0; i < filterParts.length; i++) {
73
+ if (!segmentMatches(testSegments[i], filterParts[i])) return false;
74
+ }
75
+ return true;
76
+ }
77
+
78
+ // Unanchored: find filter segments in order (with possible gaps, unless wildcard forces position)
79
+ let ti = 0;
80
+ for (let fi = 0; fi < filterParts.length; fi++) {
81
+ let found = false;
82
+ while (ti < testSegments.length) {
83
+ if (segmentMatches(testSegments[ti], filterParts[fi])) {
84
+ ti++;
85
+ found = true;
86
+ break;
87
+ }
88
+ // If previous filter was wildcard (exact position), don't skip
89
+ if (fi > 0 && filterParts[fi - 1] === '*') {
90
+ // Wildcard consumed exactly one segment, so this filter must match next
91
+ return false;
92
+ }
93
+ ti++;
94
+ }
95
+ if (!found) return false;
96
+ }
97
+ return true;
98
+ }
28
99
  const grokDir = _path.default.join(_os.default.homedir(), '.grok');
29
100
  const confPath = _path.default.join(grokDir, 'config.yaml');
30
101
  const consoleLogOutputDir = _path.default.join(curDir, 'test-console-output.log');
31
- const csvReportDir = _path.default.join(curDir, 'test-report.csv');
102
+ const defaultCsvReportDir = _path.default.join(curDir, 'test-report.csv');
103
+ function resolveCsvPath(csv) {
104
+ if (typeof csv === 'string' && csv !== 'true' && csv.length > 0) return _path.default.resolve(csv);
105
+ return defaultCsvReportDir;
106
+ }
32
107
 
33
108
  /**
34
109
  * Detects if the current directory is within a Dart library folder (d4, xamgle, ddt, dml)
@@ -57,12 +132,31 @@ function findGitRoot(startDir) {
57
132
  }
58
133
  }
59
134
  async function test(args) {
135
+ if (args['_'][1] === 'list') return await listTests(args);
136
+ if (args.dartium) return await testDartium(args);
60
137
  if (args.recursive) return await testRecursive(process.cwd(), args);
61
138
  const config = _jsYaml.default.load(_fs.default.readFileSync(confPath, {
62
139
  encoding: 'utf-8'
63
140
  }));
64
141
  isArgsValid(args);
65
142
 
143
+ // Resolve fluent filter into category/test for normal mode (best-effort)
144
+ const filter = resolveFilter(args);
145
+ if (filter && !args.category && !args.test) {
146
+ const anchored = filter.startsWith('/');
147
+ const raw = anchored ? filter.slice(1) : filter;
148
+ const parts = raw.split(/\s*[/|]\s*/).map(s => s.trim()).filter(s => s.length > 0);
149
+ if (parts.length > 1) {
150
+ // Multi-segment: use all but last as category, last as test
151
+ args.category = 'Core: ' + parts.slice(0, -1).join(': ');
152
+ args.test = parts[parts.length - 1];
153
+ } else if (parts.length === 1) {
154
+ // Single segment: use as test name filter
155
+ args.test = parts[0];
156
+ }
157
+ color.info(`Filter "${filter}" → category: "${args.category || ''}", test: "${args.test || ''}"`);
158
+ }
159
+
66
160
  // If running from a core Dart library directory, delegate to DevTools package
67
161
  if (!args.package) {
68
162
  const detectedCategory = detectDartLibraryCategory();
@@ -125,9 +219,7 @@ async function test(args) {
125
219
  try {
126
220
  await testUtils.loadPackages(packagesDir, packageName, args.host, args['skip-publish'], args['skip-build'], args.link);
127
221
  } catch (e) {
128
- console.error('\n');
129
- // @ts-ignore
130
- console.error(e.message);
222
+ console.error(e.message || 'Package build/publish failed with no output. Run with --verbose for details.');
131
223
  process.exit(1);
132
224
  }
133
225
  }
@@ -136,7 +228,7 @@ async function test(args) {
136
228
  if (args.csv) {
137
229
  res.csv = (0, _testUtils.addColumnToCsv)(res.csv, 'stress_test', args['stress-test'] ?? false);
138
230
  res.csv = (0, _testUtils.addColumnToCsv)(res.csv, 'benchmark', args.benchmark ?? false);
139
- (0, _testUtils.saveCsvResults)([res.csv], csvReportDir);
231
+ (0, _testUtils.saveCsvResults)([res.csv], resolveCsvPath(args.csv));
140
232
  }
141
233
  (0, _testUtils.printBrowsersResult)(res, args.verbose);
142
234
  if (res.failed) {
@@ -145,9 +237,12 @@ async function test(args) {
145
237
  } else testUtils.exitWithCode(0);
146
238
  return true;
147
239
  }
240
+ function resolveFilter(args) {
241
+ return args.f || (args['_'].length > 1 ? String(args['_'][1]) : undefined);
242
+ }
148
243
  function isArgsValid(args) {
149
244
  const options = Object.keys(args).slice(1);
150
- if (args['_'].length > 1 || options.length > availableCommandOptions.length || options.length > 0 && !options.every(op => availableCommandOptions.includes(op))) return false;
245
+ if (args['_'].length > 2 || options.length > availableCommandOptions.length || options.length > 0 && !options.every(op => availableCommandOptions.includes(op))) return false;
151
246
  return true;
152
247
  }
153
248
 
@@ -208,13 +303,14 @@ async function runTesting(args) {
208
303
  debug: args['debug'] ?? false,
209
304
  skipToCategory: currentSkipToCategory,
210
305
  skipToTest: currentSkipToTest,
211
- keepBrowserOpen: useRetry // Keep browser open if retry is enabled
306
+ keepBrowserOpen: useRetry,
307
+ urlParams: args.params
212
308
  }, browserId, testInvocationTimeout, browserSession);
213
309
 
214
310
  // Store browser session for potential reuse
215
311
  if (r.browserSession) browserSession = r.browserSession;
216
312
  if (r.error) {
217
- console.log(`\nexecution error:`);
313
+ color.error(`\nTest execution failed:`);
218
314
  console.log(r.error);
219
315
  // Close browser on error
220
316
  if (browserSession?.browser) await browserSession.browser.close();
@@ -326,6 +422,443 @@ function readCSVResultData(data) {
326
422
  rows: parsed.data
327
423
  };
328
424
  }
425
+ const DARTIUM_WELL_KNOWN_PATHS = ['C:\\programs\\dartium-win-ia32-stable-1.24.2.0\\chrome.exe', _path.default.join(_os.default.homedir(), 'dartium', 'chrome.exe'), _path.default.join(_os.default.homedir(), 'dartium', 'chrome')];
426
+ const DARTIUM_INACTIVITY_TIMEOUT = 180000; // 3 minutes
427
+
428
+ function resolveDartiumPath(arg) {
429
+ if (typeof arg === 'string' && arg !== 'true') {
430
+ if (!_fs.default.existsSync(arg)) throw new Error(`Dartium not found at: ${arg}`);
431
+ return arg;
432
+ }
433
+ const envPath = process.env.DARTIUM_PATH;
434
+ if (envPath) {
435
+ if (!_fs.default.existsSync(envPath)) throw new Error(`DARTIUM_PATH set but not found: ${envPath}`);
436
+ return envPath;
437
+ }
438
+ for (const p of DARTIUM_WELL_KNOWN_PATHS) {
439
+ if (_fs.default.existsSync(p)) return p;
440
+ }
441
+ throw new Error('Dartium not found. Use --dartium=/path/to/chrome.exe or set DARTIUM_PATH');
442
+ }
443
+ function extractConsoleMessage(line) {
444
+ // Try full format first: "message", source: URL (line)
445
+ const match = line.match(/INFO:CONSOLE\(\d+\)\] "(.*)", source:/);
446
+ if (match) return match[1];
447
+ // Truncated format (long messages): "message" without source suffix
448
+ const truncated = line.match(/INFO:CONSOLE\(\d+\)\] "(.*)"?\s*$/);
449
+ return truncated ? truncated[1] : null;
450
+ }
451
+ function parseAutotestLine(msg) {
452
+ // AUTOTEST_PASS: d4 | Viewers | Inputs (42ms)
453
+ // AUTOTEST_FAIL: d4 | Viewers | Inputs (42ms) - error message
454
+ // AUTOTEST_SKIP: d4 | Viewers | Inputs - reason
455
+ const passMatch = msg.match(/^AUTOTEST_PASS: (.+?) \((\d+)ms\)$/);
456
+ if (passMatch) {
457
+ const parts = passMatch[1].split(' | ');
458
+ return {
459
+ name: passMatch[1],
460
+ category: parts.slice(0, -1).join(' | '),
461
+ testName: parts[parts.length - 1],
462
+ success: true,
463
+ ms: parseInt(passMatch[2]),
464
+ skipped: false,
465
+ error: ''
466
+ };
467
+ }
468
+ const failMatch = msg.match(/^AUTOTEST_FAIL: (.+?) \((\d+)ms\) - (.*)$/);
469
+ if (failMatch) {
470
+ const parts = failMatch[1].split(' | ');
471
+ return {
472
+ name: failMatch[1],
473
+ category: parts.slice(0, -1).join(' | '),
474
+ testName: parts[parts.length - 1],
475
+ success: false,
476
+ ms: parseInt(failMatch[2]),
477
+ skipped: false,
478
+ error: failMatch[3]
479
+ };
480
+ }
481
+ const skipMatch = msg.match(/^AUTOTEST_SKIP: (.+?) - (.*)$/);
482
+ if (skipMatch) {
483
+ const parts = skipMatch[1].split(' | ');
484
+ return {
485
+ name: skipMatch[1],
486
+ category: parts.slice(0, -1).join(' | '),
487
+ testName: parts[parts.length - 1],
488
+ success: true,
489
+ ms: 0,
490
+ skipped: true,
491
+ error: skipMatch[2]
492
+ };
493
+ }
494
+ return null;
495
+ }
496
+ function writeFailedTestsLog(results, args) {
497
+ const failures = results.filter(r => !r.success && !r.skipped);
498
+ if (failures.length === 0) return null;
499
+ let logPath;
500
+ if (typeof args.logfailed === 'string' && args.logfailed !== 'true' && args.logfailed.length > 0) logPath = _path.default.resolve(args.logfailed);else logPath = _path.default.join(_os.default.tmpdir(), `grok-test-failures-${Date.now()}.md`);
501
+ const filter = resolveFilter(args) || '';
502
+ const lines = [];
503
+ lines.push(`# Failed Tests Report`);
504
+ lines.push('');
505
+ lines.push(`Date: ${new Date().toISOString()}`);
506
+ if (filter) lines.push(`Filter: ${filter}`);
507
+ lines.push(`Total: ${results.length}, Failed: ${failures.length}`);
508
+ lines.push('');
509
+ for (const f of failures) {
510
+ lines.push(`## ${f.name}`);
511
+ lines.push('');
512
+ lines.push(`**Error:** ${f.error}`);
513
+ lines.push('');
514
+ if (f.stack && f.stack.length > 0) {
515
+ lines.push('**Stack trace:**');
516
+ lines.push('```');
517
+ for (const sl of f.stack) lines.push(sl);
518
+ lines.push('```');
519
+ lines.push('');
520
+ }
521
+ const escapedName = f.name.replace(/\|/g, '/').replace(/\s+/g, ' ').trim();
522
+ lines.push(`**Reproduce:** \`grok test --dartium "${escapedName}"\``);
523
+ lines.push('');
524
+ lines.push('---');
525
+ lines.push('');
526
+ }
527
+ _fs.default.writeFileSync(logPath, lines.join('\n'), 'utf8');
528
+ return logPath;
529
+ }
530
+ async function listTests(args) {
531
+ const config = _jsYaml.default.load(_fs.default.readFileSync(confPath, {
532
+ encoding: 'utf-8'
533
+ }));
534
+ const dartiumPath = resolveDartiumPath(args.dartium || true);
535
+ const filter = args.f || (args['_'].length > 2 ? String(args['_'][2]) : '');
536
+ const {
537
+ url,
538
+ key
539
+ } = testUtils.getDevKey(args.host ?? '');
540
+ const token = await testUtils.getToken(url, key);
541
+ const webUrl = await testUtils.getWebUrl(url, token);
542
+ const urlParams = new URLSearchParams();
543
+ urlParams.set('token', token);
544
+ urlParams.set('listTests', filter);
545
+ urlParams.set('excludePackages', '');
546
+ const testUrl = `${webUrl}/?${urlParams.toString()}`;
547
+ const userDataDir = _path.default.join(_os.default.tmpdir(), 'dartium-grok-test');
548
+ color.info(`Listing tests${filter ? ` matching "${filter}"` : ''}...`);
549
+ const dartium = (0, _child_process.spawn)(dartiumPath, ['--enable-logging=stderr', '--no-first-run', `--user-data-dir=${userDataDir}`, testUrl], {
550
+ stdio: ['ignore', 'ignore', 'pipe']
551
+ });
552
+ return new Promise(resolve => {
553
+ const rl = _readline.default.createInterface({
554
+ input: dartium.stderr
555
+ });
556
+ const tests = [];
557
+ const inactivityCheck = setInterval(() => {
558
+ color.error('Timed out waiting for test list.');
559
+ dartium.kill();
560
+ }, DARTIUM_INACTIVITY_TIMEOUT);
561
+ rl.on('line', line => {
562
+ const msg = extractConsoleMessage(line);
563
+ if (!msg) return;
564
+ if (msg.startsWith('AUTOTEST_LIST_DONE:')) {
565
+ clearInterval(inactivityCheck);
566
+ console.log('');
567
+ for (const t of tests) console.log(t);
568
+ console.log(`\n${tests.length} test(s)`);
569
+ dartium.kill();
570
+ return;
571
+ }
572
+ if (msg.startsWith('AUTOTEST_LIST: ')) tests.push(msg.replace('AUTOTEST_LIST: ', ''));
573
+ });
574
+ dartium.on('close', () => {
575
+ clearInterval(inactivityCheck);
576
+ rl.close();
577
+ resolve(true);
578
+ });
579
+ process.on('SIGINT', () => {
580
+ dartium.kill();
581
+ });
582
+ });
583
+ }
584
+ /** Runs a single Dartium session with the given URL and collects results. */
585
+ function runDartiumSession(dartiumPath, testUrl, userDataDir, verbose) {
586
+ const dartium = (0, _child_process.spawn)(dartiumPath, ['--enable-logging=stderr', '--no-first-run', `--user-data-dir=${userDataDir}`, testUrl], {
587
+ stdio: ['ignore', 'ignore', 'pipe']
588
+ });
589
+ const results = [];
590
+ let totalExpected = 0;
591
+ let done = false;
592
+ let lastRunningTest = '';
593
+ let lastActivityTime = Date.now();
594
+ return new Promise(resolve => {
595
+ const rl = _readline.default.createInterface({
596
+ input: dartium.stderr
597
+ });
598
+ const inactivityCheck = setInterval(() => {
599
+ if (Date.now() - lastActivityTime > DARTIUM_INACTIVITY_TIMEOUT && !done) {
600
+ process.stdout.write('\r\x1b[K');
601
+ color.error(`\nTest appears stuck (no output for ${DARTIUM_INACTIVITY_TIMEOUT / 1000}s). Killing Dartium.`);
602
+ done = true;
603
+ dartium.kill();
604
+ }
605
+ }, 10000);
606
+ rl.on('line', line => {
607
+ const msg = extractConsoleMessage(line);
608
+ if (!msg || !msg.startsWith('AUTOTEST_')) return;
609
+ lastActivityTime = Date.now();
610
+ if (msg.startsWith('AUTOTEST_START:')) {
611
+ const countMatch = msg.match(/(\d+) test/);
612
+ totalExpected = countMatch ? parseInt(countMatch[1]) : 0;
613
+ return;
614
+ }
615
+ if (msg.startsWith('AUTOTEST_RUN:')) {
616
+ lastRunningTest = msg.replace('AUTOTEST_RUN: ', '');
617
+ process.stdout.write(`\r\x1b[K \x1b[90m\u25B6 ${lastRunningTest}\x1b[0m`);
618
+ return;
619
+ }
620
+ if (msg.startsWith('AUTOTEST_STACK:')) {
621
+ const stackLine = msg.replace('AUTOTEST_STACK: ', '');
622
+ const lastResult = results.length > 0 ? results[results.length - 1] : null;
623
+ if (lastResult && !lastResult.success && !lastResult.skipped) {
624
+ if (!lastResult.stack) lastResult.stack = [];
625
+ lastResult.stack.push(stackLine);
626
+ }
627
+ return;
628
+ }
629
+ if (msg.startsWith('AUTOTEST_DONE:')) {
630
+ process.stdout.write('\r\x1b[K');
631
+ done = true;
632
+ dartium.kill();
633
+ return;
634
+ }
635
+ const result = parseAutotestLine(msg);
636
+ if (result) {
637
+ process.stdout.write('\r\x1b[K');
638
+ results.push(result);
639
+ lastRunningTest = '';
640
+ if (verbose) {
641
+ if (result.skipped) console.log(` \x1b[33m\u25CB ${result.testName} (skipped: ${result.error})\x1b[0m`);else if (result.success) console.log(` \x1b[32m\u2714 ${result.testName} (${result.ms}ms)\x1b[0m`);else console.log(` \x1b[31m\u274C ${result.testName} (${result.ms}ms) - ${result.error}\x1b[0m`);
642
+ }
643
+ }
644
+ });
645
+ dartium.on('close', () => {
646
+ clearInterval(inactivityCheck);
647
+ rl.close();
648
+ resolve({
649
+ results,
650
+ done,
651
+ totalExpected,
652
+ lastRunningTest
653
+ });
654
+ });
655
+ process.on('SIGINT', () => {
656
+ color.warn('\nInterrupted. Killing Dartium...');
657
+ done = true;
658
+ dartium.kill();
659
+ });
660
+ });
661
+ }
662
+
663
+ /** Gets the full list of test names matching a filter via Dartium. */
664
+ function getDartiumTestList(dartiumPath, webUrl, token, filter, extraParams) {
665
+ const urlParams = new URLSearchParams();
666
+ urlParams.set('token', token);
667
+ urlParams.set('listTests', filter);
668
+ urlParams.set('excludePackages', '');
669
+ if (extraParams) for (const pair of extraParams.split('&')) {
670
+ const [k, ...v] = pair.split('=');
671
+ if (k) urlParams.set(k.trim(), v.join('='));
672
+ }
673
+ const testUrl = `${webUrl}/?${urlParams.toString()}`;
674
+ const userDataDir = _path.default.join(_os.default.tmpdir(), 'dartium-grok-test');
675
+ const dartium = (0, _child_process.spawn)(dartiumPath, ['--enable-logging=stderr', '--no-first-run', `--user-data-dir=${userDataDir}`, testUrl], {
676
+ stdio: ['ignore', 'ignore', 'pipe']
677
+ });
678
+ return new Promise(resolve => {
679
+ const rl = _readline.default.createInterface({
680
+ input: dartium.stderr
681
+ });
682
+ const tests = [];
683
+ const timeout = setTimeout(() => {
684
+ dartium.kill();
685
+ }, DARTIUM_INACTIVITY_TIMEOUT);
686
+ rl.on('line', line => {
687
+ const msg = extractConsoleMessage(line);
688
+ if (!msg) return;
689
+ if (msg.startsWith('AUTOTEST_LIST_DONE:')) {
690
+ clearTimeout(timeout);
691
+ dartium.kill();
692
+ return;
693
+ }
694
+ if (msg.startsWith('AUTOTEST_LIST: ')) tests.push(msg.replace('AUTOTEST_LIST: ', ''));
695
+ });
696
+ dartium.on('close', () => {
697
+ clearTimeout(timeout);
698
+ rl.close();
699
+ resolve(tests);
700
+ });
701
+ });
702
+ }
703
+ function buildDartiumUrl(webUrl, token, filter, args) {
704
+ const urlParams = new URLSearchParams();
705
+ urlParams.set('token', token);
706
+ urlParams.set('tests', filter);
707
+ if (!urlParams.has('excludePackages')) urlParams.set('excludePackages', '');
708
+ if (args.params) for (const pair of args.params.split('&')) {
709
+ const [k, ...v] = pair.split('=');
710
+ if (k) urlParams.set(k.trim(), v.join('='));
711
+ }
712
+ return `${webUrl}/?${urlParams.toString()}`;
713
+ }
714
+ function printDartiumSummary(allResults, args) {
715
+ // Category-level summary
716
+ const categoryResults = new Map();
717
+ const categoryFailures = new Map();
718
+ for (const result of allResults) {
719
+ if (!categoryResults.has(result.category)) categoryResults.set(result.category, {
720
+ passed: 0,
721
+ failed: 0,
722
+ skipped: 0
723
+ });
724
+ const catR = categoryResults.get(result.category);
725
+ if (result.skipped) catR.skipped++;else if (result.success) catR.passed++;else {
726
+ catR.failed++;
727
+ if (!categoryFailures.has(result.category)) categoryFailures.set(result.category, []);
728
+ categoryFailures.get(result.category).push({
729
+ testName: result.testName,
730
+ error: result.error,
731
+ stack: result.stack || []
732
+ });
733
+ }
734
+ }
735
+ for (const [cat, r] of categoryResults) {
736
+ const skippedSuffix = r.skipped > 0 ? `, \x1b[33m${r.skipped} skipped\x1b[0m` : '';
737
+ if (r.failed > 0) {
738
+ console.log(`\x1b[31m\u274C ${cat}\x1b[31m (\x1b[32m${r.passed} passed${skippedSuffix}\x1b[31m, ${r.failed} failed)\x1b[0m`);
739
+ const failures = categoryFailures.get(cat) || [];
740
+ for (const f of failures) {
741
+ console.log(` \x1b[31m\u274C ${f.testName}\x1b[0m${f.error ? `: ${f.error}` : ''}`);
742
+ for (const sl of f.stack) console.log(` \x1b[90m${sl}\x1b[0m`);
743
+ }
744
+ } else console.log(`\x1b[32m\u2714 ${cat} (${r.passed} passed${skippedSuffix})\x1b[0m`);
745
+ }
746
+ const passed = allResults.filter(r => r.success && !r.skipped).length;
747
+ const failed = allResults.filter(r => !r.success && !r.skipped).length;
748
+ const skipped = allResults.filter(r => r.skipped).length;
749
+ console.log('');
750
+ if (failed > 0) color.error(`Results: ${passed} passed, ${failed} failed${skipped > 0 ? `, ${skipped} skipped` : ''} (${allResults.length} total)`);else color.success(`Results: ${passed} passed${skipped > 0 ? `, ${skipped} skipped` : ''} (${allResults.length} total)`);
751
+
752
+ // CSV
753
+ if (args.csv && allResults.length > 0) {
754
+ const now = new Date().toISOString();
755
+ const csvRows = allResults.map(r => ({
756
+ date: now,
757
+ category: r.category,
758
+ name: r.testName,
759
+ success: r.success,
760
+ result: r.skipped ? r.error : r.success ? 'OK' : r.error,
761
+ ms: r.ms,
762
+ skipped: r.skipped,
763
+ error: r.success ? '' : r.error
764
+ }));
765
+ const csv = Papa.unparse(csvRows);
766
+ const csvPath = resolveCsvPath(args.csv);
767
+ _fs.default.writeFileSync(csvPath, csv, 'utf8');
768
+ color.info(`Saved ${csvPath}`);
769
+ }
770
+
771
+ // Log failed
772
+ if (failed > 0 || args.logfailed) {
773
+ const logPath = writeFailedTestsLog(allResults, args);
774
+ if (logPath) color.warn(`Failed tests log: ${logPath}`);
775
+ }
776
+ const totalTestMs = allResults.reduce((sum, r) => sum + r.ms, 0);
777
+ console.log(`\nPassed tests: ${passed}`);
778
+ console.log(`Failed tests: ${failed}`);
779
+ console.log(`Skipped tests: ${skipped}`);
780
+ console.log(`Total test time: ${(totalTestMs / 1000).toFixed(1)}s`);
781
+ if (failed > 0) testUtils.exitWithCode(1);else testUtils.exitWithCode(0);
782
+ }
783
+ async function testDartium(args) {
784
+ const dartiumPath = resolveDartiumPath(args.dartium);
785
+ color.info(`Using Dartium: ${dartiumPath}`);
786
+ const {
787
+ url,
788
+ key
789
+ } = testUtils.getDevKey(args.host ?? '');
790
+ const token = await testUtils.getToken(url, key);
791
+ const webUrl = await testUtils.getWebUrl(url, token);
792
+ const filter = resolveFilter(args) || '';
793
+ const userDataDir = _path.default.join(_os.default.tmpdir(), 'dartium-grok-test');
794
+ const testUrl = buildDartiumUrl(webUrl, token, filter, args);
795
+ const useRetry = args['no-retry'] === false;
796
+ color.info(`Opening: ${webUrl} (tests: ${filter || 'all'})`);
797
+
798
+ // Main run
799
+ const session = await runDartiumSession(dartiumPath, testUrl, userDataDir, args.verbose ?? false);
800
+ const allResults = [...session.results];
801
+ if (session.done) {
802
+ color.info(`Running ${session.totalExpected} test(s)...\n`);
803
+ }
804
+
805
+ // If Dartium crashed mid-run and retry is enabled, retry missing tests individually
806
+ if (!session.done && useRetry) {
807
+ const completedNames = new Set(session.results.map(r => r.name));
808
+
809
+ // The test that was running when Dartium crashed
810
+ if (session.lastRunningTest && !completedNames.has(session.lastRunningTest)) {
811
+ allResults.push({
812
+ name: session.lastRunningTest,
813
+ category: session.lastRunningTest.split(/\s*\|\s*/).slice(0, -1).join(' | '),
814
+ testName: session.lastRunningTest.split(/\s*\|\s*/).pop(),
815
+ success: false,
816
+ ms: 0,
817
+ skipped: false,
818
+ error: 'Dartium crashed while running this test',
819
+ stack: []
820
+ });
821
+ completedNames.add(session.lastRunningTest);
822
+ }
823
+
824
+ // Get full test list to find what wasn't run
825
+ color.info('\nDartium crashed. Fetching full test list for retry...');
826
+ const allTests = await getDartiumTestList(dartiumPath, webUrl, token, filter, args.params);
827
+ const missingTests = allTests.filter(t => !completedNames.has(t));
828
+ if (missingTests.length > 0) {
829
+ color.info(`Retrying ${missingTests.length} unexecuted test(s) individually...\n`);
830
+ for (let i = 0; i < missingTests.length; i++) {
831
+ const testName = missingTests[i];
832
+ const shortName = testName.split(' | ').pop();
833
+ process.stdout.write(`\r\x1b[K \x1b[90m[${i + 1}/${missingTests.length}] Retrying: ${shortName}\x1b[0m`);
834
+
835
+ // Build URL for this specific test
836
+ const escapedName = testName.replace(/\|/g, '/').replace(/\s+/g, ' ').trim();
837
+ const retryUrl = buildDartiumUrl(webUrl, token, escapedName, args);
838
+ const retrySession = await runDartiumSession(dartiumPath, retryUrl, userDataDir, args.verbose ?? false);
839
+ if (retrySession.results.length > 0) allResults.push(...retrySession.results);else {
840
+ // Test crashed Dartium again or produced no output
841
+ allResults.push({
842
+ name: testName,
843
+ category: testName.split(/\s*\|\s*/).slice(0, -1).join(' | '),
844
+ testName: testName.split(/\s*\|\s*/).pop(),
845
+ success: false,
846
+ ms: 0,
847
+ skipped: false,
848
+ error: 'Dartium crashed on retry — test consistently crashes the browser',
849
+ stack: []
850
+ });
851
+ }
852
+ }
853
+ process.stdout.write('\r\x1b[K');
854
+ console.log('');
855
+ }
856
+ } else if (!session.done && !useRetry) {
857
+ color.warn(`Dartium exited before tests completed (${allResults.length}/${session.totalExpected || '?'} tests ran). Use --retry to retry missing tests.`);
858
+ }
859
+ printDartiumSummary(allResults, args);
860
+ return allResults.every(r => r.success || r.skipped);
861
+ }
329
862
  async function testRecursive(baseDir, args) {
330
863
  const packages = (0, _build.discoverPackages)(baseDir);
331
864
  if (packages.length === 0) {
@@ -365,9 +898,9 @@ function buildTestArgs(args) {
365
898
  return parts;
366
899
  }
367
900
  function parseTestOutput(stdout) {
368
- const passedMatch = stdout.match(/Passed amount:\s*(\d+)/);
369
- const failedMatch = stdout.match(/Failed amount:\s*(\d+)/);
370
- const skippedMatch = stdout.match(/Skipped amount:\s*(\d+)/);
901
+ const passedMatch = stdout.match(/Passed (?:amount|tests):\s*(\d+)/);
902
+ const failedMatch = stdout.match(/Failed (?:amount|tests):\s*(\d+)/);
903
+ const skippedMatch = stdout.match(/Skipped (?:amount|tests):\s*(\d+)/);
371
904
  if (!passedMatch && !failedMatch) return null;
372
905
  return {
373
906
  passed: passedMatch ? parseInt(passedMatch[1]) : 0,
package/bin/grok.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  const argv = require('minimist')(process.argv.slice(2), {
3
3
  alias: {k: 'key', h: 'help', r: 'recursive', s: 'silent'},
4
+ boolean: ['dartium'],
4
5
  });
5
6
  const help = require('./commands/help').help;
6
7
  const runAllCommand = require('./utils/utils').runAll;
@@ -13,6 +14,7 @@ const commands = {
13
14
  claude: require('./commands/claude').claude,
14
15
  config: require('./commands/config').config,
15
16
  create: require('./commands/create').create,
17
+ 'docker-gen': require('./commands/docker-gen').dockerGen,
16
18
  init: require('./commands/init').init,
17
19
  link: require('./commands/link').link,
18
20
  publish: require('./commands/publish').publish,