@glubean/cli 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/contracts.d.ts +21 -1
- package/dist/commands/contracts.d.ts.map +1 -1
- package/dist/commands/contracts.js +104 -43
- package/dist/commands/contracts.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +34 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +471 -379
- package/dist/commands/run.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +3 -1
- package/dist/main.js.map +1 -1
- package/package.json +7 -6
- package/templates/README.md +0 -7
- package/dist/lib/env.d.ts +0 -29
- package/dist/lib/env.d.ts.map +0 -1
- package/dist/lib/env.js +0 -59
- package/dist/lib/env.js.map +0 -1
package/dist/commands/run.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { bootstrap, evaluateThresholds, MetricCollector, ProjectRunner, buildRunContext, } from "@glubean/runner";
|
|
2
2
|
import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
|
|
3
3
|
import { stat, readdir, readFile, writeFile, mkdir, rm } from "node:fs/promises";
|
|
4
|
-
import { pathToFileURL } from "node:url";
|
|
5
4
|
import { glob } from "node:fs/promises";
|
|
6
5
|
import { loadConfig, mergeRunOptions, toSharedRunConfig } from "../lib/config.js";
|
|
7
|
-
import {
|
|
6
|
+
import { loadProjectEnv } from "@glubean/runner";
|
|
8
7
|
import { resolveEnvFileName } from "../lib/active_env.js";
|
|
9
8
|
import { shouldSkipTest } from "../lib/skip.js";
|
|
10
9
|
import { CLI_VERSION } from "../version.js";
|
|
@@ -308,7 +307,13 @@ export async function runCommand(target, options = {}) {
|
|
|
308
307
|
process.exit(1);
|
|
309
308
|
}
|
|
310
309
|
}
|
|
311
|
-
|
|
310
|
+
// Canonical env loading: reads both .env and .env.secrets, expands
|
|
311
|
+
// `${NAME}` references (same file forward refs, cross-file refs, and
|
|
312
|
+
// process.env fallback), splits back into {vars, secrets} with secrets
|
|
313
|
+
// winning on collision. See @glubean/runner:loadProjectEnv.
|
|
314
|
+
const { vars: envVars, secrets } = await loadProjectEnv(rootDir, envFileName);
|
|
315
|
+
// Warn separately on the missing-secrets case so users get a visual
|
|
316
|
+
// signal — loadProjectEnv itself treats missing files as silent empties.
|
|
312
317
|
const secretsPath = resolve(rootDir, `${envFileName}.secrets`);
|
|
313
318
|
let secretsExist = true;
|
|
314
319
|
try {
|
|
@@ -317,7 +322,6 @@ export async function runCommand(target, options = {}) {
|
|
|
317
322
|
catch {
|
|
318
323
|
secretsExist = false;
|
|
319
324
|
}
|
|
320
|
-
const secrets = secretsExist ? await loadEnvFile(secretsPath) : {};
|
|
321
325
|
if (!secretsExist && Object.keys(envVars).length > 0) {
|
|
322
326
|
console.warn(`${colors.yellow}Warning: secrets file '${envFileName}.secrets' not found in ${rootDir}${colors.reset}`);
|
|
323
327
|
}
|
|
@@ -369,6 +373,17 @@ export async function runCommand(target, options = {}) {
|
|
|
369
373
|
process.exit(1);
|
|
370
374
|
}
|
|
371
375
|
}
|
|
376
|
+
// ── Bootstrap plugins BEFORE discovery ─────────────────────────────────
|
|
377
|
+
// CLI's `discoverTests` dynamically imports each .contract.ts / .test.ts
|
|
378
|
+
// in this process. If the file uses plugin-registered names like
|
|
379
|
+
// `contract.graphql.with(...)` before the plugin's manifest is installed,
|
|
380
|
+
// the import throws ("Cannot read properties of undefined (reading
|
|
381
|
+
// 'with')"). ProjectRunner calls bootstrap() too, but that happens AFTER
|
|
382
|
+
// our discovery — too late. Matching MCP's `glubean_openapi` pattern here:
|
|
383
|
+
// explicit bootstrap before any parent-process contract file import. The
|
|
384
|
+
// call is idempotent (bootstrap tracks loadState internally), so
|
|
385
|
+
// ProjectRunner's internal call is a no-op second visit.
|
|
386
|
+
await bootstrap(rootDir);
|
|
372
387
|
// ── Discover tests across all files ─────────────────────────────────────
|
|
373
388
|
console.log(`${colors.dim}Discovering tests...${colors.reset}`);
|
|
374
389
|
const allFileTests = [];
|
|
@@ -460,10 +475,9 @@ export async function runCommand(target, options = {}) {
|
|
|
460
475
|
delete process.env.GLUBEAN_PICK;
|
|
461
476
|
}
|
|
462
477
|
const shared = toSharedRunConfig(effectiveRun);
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
});
|
|
478
|
+
// Note: TestExecutor construction is delegated to ProjectRunner below
|
|
479
|
+
// (it builds one via TestExecutor.fromSharedConfig with identical cwd +
|
|
480
|
+
// inspectBrk params when no executor option is passed).
|
|
467
481
|
let passed = 0;
|
|
468
482
|
let failed = 0;
|
|
469
483
|
let skipped = 0;
|
|
@@ -490,40 +504,15 @@ export async function runCommand(target, options = {}) {
|
|
|
490
504
|
group.push(entry);
|
|
491
505
|
fileGroups.set(entry.filePath, group);
|
|
492
506
|
}
|
|
493
|
-
// ── Session
|
|
507
|
+
// ── Session + execution + teardown via ProjectRunner ─────────────────────
|
|
508
|
+
//
|
|
509
|
+
// Replaces the prior inline RunOrchestrator + per-file TestExecutor loop
|
|
510
|
+
// (~540 lines) with a single event-stream consumer. Per-event presentation
|
|
511
|
+
// handlers (trace / assertion / step / etc.) are byte-for-byte unchanged;
|
|
512
|
+
// only the outer wiring swaps from direct executor.run(...) to the facade.
|
|
513
|
+
//
|
|
514
|
+
// See internal/30-execution/2026-04-23-rf-1b-cli-migration/execution-log.md.
|
|
494
515
|
const sessionState = {};
|
|
495
|
-
const sessionFile = options.noSession
|
|
496
|
-
? undefined
|
|
497
|
-
: discoverSessionFile(startDir, rootDir);
|
|
498
|
-
const orchestrator = new RunOrchestrator(executor);
|
|
499
|
-
if (sessionFile) {
|
|
500
|
-
console.log(`${colors.dim}Session: ${relative(process.cwd(), sessionFile)}${colors.reset}`);
|
|
501
|
-
let sessionFailed = false;
|
|
502
|
-
for await (const event of orchestrator.runSessionSetup(sessionFile, { vars: envVars, secrets, interactive }, toSingleExecutionOptions(shared))) {
|
|
503
|
-
if (event.type === "session:set") {
|
|
504
|
-
sessionState[event.key] = event.value;
|
|
505
|
-
}
|
|
506
|
-
else if (event.type === "status" && event.status === "failed") {
|
|
507
|
-
sessionFailed = true;
|
|
508
|
-
console.log(` ${colors.red}✗ Session setup failed${event.error ? `: ${event.error}` : ""}${colors.reset}`);
|
|
509
|
-
}
|
|
510
|
-
else if (event.type === "log") {
|
|
511
|
-
console.log(` ${colors.dim}[session] ${event.message}${colors.reset}`);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
if (sessionFailed) {
|
|
515
|
-
// Best-effort teardown before exiting
|
|
516
|
-
for await (const _event of orchestrator.runSessionTeardown(sessionFile, { vars: envVars, secrets }, sessionState, toSingleExecutionOptions(shared))) {
|
|
517
|
-
// Silently consume teardown events
|
|
518
|
-
}
|
|
519
|
-
console.log(`\n${colors.red}Session setup failed. All tests skipped.${colors.reset}`);
|
|
520
|
-
process.exit(1);
|
|
521
|
-
}
|
|
522
|
-
const keyCount = Object.keys(sessionState).length;
|
|
523
|
-
if (keyCount > 0) {
|
|
524
|
-
console.log(`${colors.dim} ${keyCount} session value${keyCount > 1 ? "s" : ""} set${colors.reset}`);
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
516
|
const compactUrl = (url) => {
|
|
528
517
|
try {
|
|
529
518
|
const u = new URL(url);
|
|
@@ -543,372 +532,475 @@ export async function runCommand(target, options = {}) {
|
|
|
543
532
|
return `${colors.dim}${status}${colors.reset}`;
|
|
544
533
|
return `${colors.green}${status}${colors.reset}`;
|
|
545
534
|
};
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
535
|
+
// Per-test state, scoped across file:event boundaries. Reset on each
|
|
536
|
+
// "start" event inside file:event handlers.
|
|
537
|
+
let currentGroupFilePath = "";
|
|
538
|
+
let currentTestMap;
|
|
539
|
+
let testId = "";
|
|
540
|
+
let testName = "";
|
|
541
|
+
let testItem = null;
|
|
542
|
+
let startTime = Date.now();
|
|
543
|
+
let testEvents = [];
|
|
544
|
+
let assertions = [];
|
|
545
|
+
let success = false;
|
|
546
|
+
let errorMsg;
|
|
547
|
+
let peakMemoryMB;
|
|
548
|
+
let stepAssertionCount = 0;
|
|
549
|
+
let stepTraceLines = [];
|
|
550
|
+
let testStarted = false;
|
|
551
|
+
const addLogEntry = (type, message, data) => {
|
|
552
|
+
if (effectiveRun.logFile) {
|
|
553
|
+
logEntries.push({
|
|
554
|
+
timestamp: new Date().toISOString(),
|
|
555
|
+
testId,
|
|
556
|
+
testName,
|
|
557
|
+
type,
|
|
558
|
+
message,
|
|
559
|
+
data,
|
|
560
|
+
});
|
|
550
561
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
562
|
+
};
|
|
563
|
+
const finalizeTest = () => {
|
|
564
|
+
if (!testStarted)
|
|
565
|
+
return;
|
|
566
|
+
testStarted = false;
|
|
567
|
+
const duration = Date.now() - startTime;
|
|
568
|
+
const allAssertionsPassed = assertions.every((a) => a.passed);
|
|
569
|
+
const finalSuccess = success && allAssertionsPassed;
|
|
570
|
+
collectedRuns.push({
|
|
571
|
+
testId,
|
|
572
|
+
testName,
|
|
573
|
+
tags: testItem?.meta.tags,
|
|
574
|
+
filePath: currentGroupFilePath,
|
|
575
|
+
events: testEvents,
|
|
576
|
+
success: finalSuccess,
|
|
577
|
+
durationMs: duration,
|
|
578
|
+
groupId: testItem?.meta.groupId,
|
|
579
|
+
});
|
|
580
|
+
addLogEntry("result", finalSuccess ? "PASSED" : "FAILED", {
|
|
581
|
+
duration,
|
|
582
|
+
success: finalSuccess,
|
|
583
|
+
peakMemoryMB,
|
|
584
|
+
});
|
|
585
|
+
const peakMB = peakMemoryMB ? parseFloat(peakMemoryMB) : 0;
|
|
586
|
+
if (peakMB > overallPeakMemoryMB) {
|
|
587
|
+
overallPeakMemoryMB = peakMB;
|
|
558
588
|
}
|
|
559
|
-
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
filePath: groupFilePath,
|
|
573
|
-
events: [{ type: "status", status: "skipped", reason: skipReason }],
|
|
574
|
-
success: true,
|
|
575
|
-
durationMs: 0,
|
|
576
|
-
groupId: ft.test.meta.groupId,
|
|
577
|
-
});
|
|
578
|
-
continue;
|
|
579
|
-
}
|
|
580
|
-
runnableTests.push(ft);
|
|
589
|
+
const testHttpCalls = testEvents.filter((e) => e.type === "trace").length;
|
|
590
|
+
const testSteps = testEvents.filter((e) => e.type === "step_end").length;
|
|
591
|
+
const miniStats = [];
|
|
592
|
+
miniStats.push(`${duration}ms`);
|
|
593
|
+
if (testHttpCalls > 0)
|
|
594
|
+
miniStats.push(`${testHttpCalls} calls`);
|
|
595
|
+
if (assertions.length > 0)
|
|
596
|
+
miniStats.push(`${assertions.length} checks`);
|
|
597
|
+
if (testSteps > 0)
|
|
598
|
+
miniStats.push(`${testSteps} steps`);
|
|
599
|
+
if (finalSuccess) {
|
|
600
|
+
console.log(` ${colors.green}✓ PASSED${colors.reset} ${colors.dim}(${miniStats.join(", ")})${colors.reset}`);
|
|
601
|
+
passed++;
|
|
581
602
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const exportNames = {};
|
|
586
|
-
for (const ft of runnableTests) {
|
|
587
|
-
exportNames[ft.test.meta.id] = ft.exportName;
|
|
603
|
+
else {
|
|
604
|
+
console.log(` ${colors.red}✗ FAILED${colors.reset} ${colors.dim}(${miniStats.join(", ")})${colors.reset}`);
|
|
605
|
+
failed++;
|
|
588
606
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
return sum +
|
|
593
|
-
(normalizePositiveTimeoutMs(ft.test.meta.timeout) ??
|
|
594
|
-
shared.perTestTimeoutMs ?? 30_000);
|
|
595
|
-
}, 0);
|
|
596
|
-
let testId = "";
|
|
597
|
-
let testName = "";
|
|
598
|
-
let testItem = null;
|
|
599
|
-
let startTime = Date.now();
|
|
600
|
-
let testEvents = [];
|
|
601
|
-
let assertions = [];
|
|
602
|
-
let success = false;
|
|
603
|
-
let errorMsg;
|
|
604
|
-
let peakMemoryMB;
|
|
605
|
-
let stepAssertionCount = 0;
|
|
606
|
-
let stepTraceLines = [];
|
|
607
|
-
let testStarted = false;
|
|
608
|
-
const addLogEntry = (type, message, data) => {
|
|
609
|
-
if (effectiveRun.logFile) {
|
|
610
|
-
logEntries.push({
|
|
611
|
-
timestamp: new Date().toISOString(),
|
|
612
|
-
testId,
|
|
613
|
-
testName,
|
|
614
|
-
type,
|
|
615
|
-
message,
|
|
616
|
-
data,
|
|
617
|
-
});
|
|
607
|
+
if (peakMB > MEMORY_WARNING_THRESHOLD_MB) {
|
|
608
|
+
if (peakMB > CLOUD_MEMORY_LIMITS.free) {
|
|
609
|
+
console.log(` ${colors.yellow}⚠ Memory (${peakMemoryMB} MB) exceeds Free cloud runner limit (${CLOUD_MEMORY_LIMITS.free} MB).${colors.reset}`);
|
|
618
610
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
if (!testStarted)
|
|
622
|
-
return;
|
|
623
|
-
testStarted = false;
|
|
624
|
-
const duration = Date.now() - startTime;
|
|
625
|
-
const allAssertionsPassed = assertions.every((a) => a.passed);
|
|
626
|
-
const finalSuccess = success && allAssertionsPassed;
|
|
627
|
-
collectedRuns.push({
|
|
628
|
-
testId,
|
|
629
|
-
testName,
|
|
630
|
-
tags: testItem?.meta.tags,
|
|
631
|
-
filePath: groupFilePath,
|
|
632
|
-
events: testEvents,
|
|
633
|
-
success: finalSuccess,
|
|
634
|
-
durationMs: duration,
|
|
635
|
-
groupId: testItem?.meta.groupId,
|
|
636
|
-
});
|
|
637
|
-
addLogEntry("result", finalSuccess ? "PASSED" : "FAILED", {
|
|
638
|
-
duration,
|
|
639
|
-
success: finalSuccess,
|
|
640
|
-
peakMemoryMB,
|
|
641
|
-
});
|
|
642
|
-
const peakMB = peakMemoryMB ? parseFloat(peakMemoryMB) : 0;
|
|
643
|
-
if (peakMB > overallPeakMemoryMB) {
|
|
644
|
-
overallPeakMemoryMB = peakMB;
|
|
611
|
+
else {
|
|
612
|
+
console.log(` ${colors.yellow}⚠ Memory (${peakMemoryMB} MB) is approaching Free cloud runner limit (${CLOUD_MEMORY_LIMITS.free} MB).${colors.reset}`);
|
|
645
613
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
614
|
+
}
|
|
615
|
+
for (const assertion of assertions) {
|
|
616
|
+
if (!assertion.passed) {
|
|
617
|
+
console.log(` ${colors.red}✗ ${assertion.message}${colors.reset}`);
|
|
618
|
+
if (assertion.expected !== undefined || assertion.actual !== undefined) {
|
|
619
|
+
if (assertion.expected !== undefined) {
|
|
620
|
+
console.log(` ${colors.dim}Expected: ${JSON.stringify(assertion.expected)}${colors.reset}`);
|
|
621
|
+
}
|
|
622
|
+
if (assertion.actual !== undefined) {
|
|
623
|
+
console.log(` ${colors.dim}Actual: ${JSON.stringify(assertion.actual)}${colors.reset}`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (errorMsg) {
|
|
629
|
+
console.log(` ${colors.red}Error: ${errorMsg}${colors.reset}`);
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
// Pre-filter tests by capability profile so file:start can emit the
|
|
633
|
+
// ⊘ lines inline (preserves the pre-migration output layout where these
|
|
634
|
+
// lines appear between the file header and the first runnable test of
|
|
635
|
+
// the file). `runnableByFile` is what actually feeds ProjectRunner.
|
|
636
|
+
const fileCapabilitySkips = new Map();
|
|
637
|
+
const runnableByFile = new Map();
|
|
638
|
+
for (const [filePath, fileTests] of fileGroups) {
|
|
639
|
+
const skips = [];
|
|
640
|
+
const runnable = [];
|
|
641
|
+
for (const ft of fileTests) {
|
|
642
|
+
const reason = shouldSkipTest(ft.test.meta, capabilityProfile);
|
|
643
|
+
if (reason) {
|
|
644
|
+
skips.push({ ft, reason });
|
|
659
645
|
}
|
|
660
646
|
else {
|
|
661
|
-
|
|
662
|
-
failed++;
|
|
647
|
+
runnable.push(ft);
|
|
663
648
|
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
649
|
+
}
|
|
650
|
+
if (skips.length > 0)
|
|
651
|
+
fileCapabilitySkips.set(filePath, skips);
|
|
652
|
+
if (runnable.length > 0)
|
|
653
|
+
runnableByFile.set(filePath, runnable);
|
|
654
|
+
}
|
|
655
|
+
// Flatten in fileGroups insertion order so ProjectRunner processes files
|
|
656
|
+
// in the same order the old inline loop did.
|
|
657
|
+
const runnableTests = [];
|
|
658
|
+
for (const filePath of fileGroups.keys()) {
|
|
659
|
+
const runnable = runnableByFile.get(filePath);
|
|
660
|
+
if (runnable)
|
|
661
|
+
runnableTests.push(...runnable);
|
|
662
|
+
}
|
|
663
|
+
// Files ProjectRunner actually started. Any fileGroups entry that never
|
|
664
|
+
// gets file:start is a fail-fast skip — handled post run:complete.
|
|
665
|
+
const startedFiles = new Set();
|
|
666
|
+
const runner = new ProjectRunner({
|
|
667
|
+
rootDir,
|
|
668
|
+
sharedConfig: shared,
|
|
669
|
+
sessionStartDir: startDir,
|
|
670
|
+
vars: envVars,
|
|
671
|
+
secrets,
|
|
672
|
+
// Cast — CLI's DiscoveredTestMeta.requires is a plain `string | undefined`
|
|
673
|
+
// (scanner output, openly typed). ProjectRunnerTest narrows it to the
|
|
674
|
+
// CaseRequires literal union. Widening happens upstream at scanner.
|
|
675
|
+
tests: runnableTests.map((t) => ({
|
|
676
|
+
filePath: t.filePath,
|
|
677
|
+
exportName: t.exportName,
|
|
678
|
+
meta: t.test.meta,
|
|
679
|
+
})),
|
|
680
|
+
noSession: !!options.noSession,
|
|
681
|
+
interactive,
|
|
682
|
+
...(options.inspectBrk !== undefined && { inspectBrk: options.inspectBrk }),
|
|
683
|
+
metricCollector,
|
|
684
|
+
});
|
|
685
|
+
for await (const ev of runner.run()) {
|
|
686
|
+
switch (ev.type) {
|
|
687
|
+
case "bootstrap:start":
|
|
688
|
+
case "bootstrap:done":
|
|
689
|
+
case "discovery:done":
|
|
690
|
+
case "session:setup:start":
|
|
691
|
+
case "session:teardown:start":
|
|
692
|
+
case "session:teardown:done":
|
|
693
|
+
// Silent — either internal plumbing, or already covered by a more
|
|
694
|
+
// specific event (e.g. session:discovered already printed the
|
|
695
|
+
// "Session: <path>" header before setup:start arrived).
|
|
696
|
+
break;
|
|
697
|
+
case "bootstrap:failed":
|
|
698
|
+
console.error(`\n${colors.red}Bootstrap failed: ${ev.error.message}${colors.reset}`);
|
|
699
|
+
process.exit(1);
|
|
700
|
+
break;
|
|
701
|
+
case "session:discovered":
|
|
702
|
+
if (ev.sessionFile) {
|
|
703
|
+
console.log(`${colors.dim}Session: ${relative(process.cwd(), ev.sessionFile)}${colors.reset}`);
|
|
704
|
+
}
|
|
705
|
+
break;
|
|
706
|
+
case "session:setup:event": {
|
|
707
|
+
const se = ev.event;
|
|
708
|
+
if (se.type === "session:set") {
|
|
709
|
+
sessionState[se.key] = se.value;
|
|
667
710
|
}
|
|
668
|
-
else {
|
|
669
|
-
console.log(`
|
|
711
|
+
else if (se.type === "status" && se.status === "failed") {
|
|
712
|
+
console.log(` ${colors.red}✗ Session setup failed${se.error ? `: ${se.error}` : ""}${colors.reset}`);
|
|
670
713
|
}
|
|
714
|
+
else if (se.type === "log") {
|
|
715
|
+
console.log(` ${colors.dim}[session] ${se.message}${colors.reset}`);
|
|
716
|
+
}
|
|
717
|
+
break;
|
|
671
718
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
if (assertion.expected !== undefined) {
|
|
677
|
-
console.log(` ${colors.dim}Expected: ${JSON.stringify(assertion.expected)}${colors.reset}`);
|
|
678
|
-
}
|
|
679
|
-
if (assertion.actual !== undefined) {
|
|
680
|
-
console.log(` ${colors.dim}Actual: ${JSON.stringify(assertion.actual)}${colors.reset}`);
|
|
681
|
-
}
|
|
682
|
-
}
|
|
719
|
+
case "session:setup:done": {
|
|
720
|
+
const count = ev.stateKeys.length;
|
|
721
|
+
if (count > 0) {
|
|
722
|
+
console.log(`${colors.dim} ${count} session value${count > 1 ? "s" : ""} set${colors.reset}`);
|
|
683
723
|
}
|
|
724
|
+
break;
|
|
684
725
|
}
|
|
685
|
-
|
|
686
|
-
console.log(
|
|
726
|
+
case "session:setup:failed":
|
|
727
|
+
console.log(`\n${colors.red}Session setup failed. All tests skipped.${colors.reset}`);
|
|
728
|
+
process.exit(1);
|
|
729
|
+
break;
|
|
730
|
+
case "session:teardown:event": {
|
|
731
|
+
const te = ev.event;
|
|
732
|
+
if (te.type === "log") {
|
|
733
|
+
console.log(` ${colors.dim}[session] ${te.message}${colors.reset}`);
|
|
734
|
+
}
|
|
735
|
+
else if (te.type === "status" && te.status === "failed") {
|
|
736
|
+
console.log(` ${colors.yellow}⚠ Session teardown failed${te.error ? `: ${te.error}` : ""}${colors.reset}`);
|
|
737
|
+
}
|
|
738
|
+
break;
|
|
687
739
|
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
stepAssertionCount = 0;
|
|
717
|
-
stepTraceLines = [];
|
|
718
|
-
testStarted = true;
|
|
719
|
-
const tags = testItem?.meta.tags?.length
|
|
720
|
-
? ` ${colors.dim}[${testItem.meta.tags.join(", ")}]${colors.reset}`
|
|
721
|
-
: "";
|
|
722
|
-
console.log(` ${colors.cyan}●${colors.reset} ${testName}${tags}`);
|
|
723
|
-
if (testItem?.meta.description) {
|
|
724
|
-
console.log(` ${colors.dim}${testItem.meta.description}${colors.reset}`);
|
|
740
|
+
case "file:start": {
|
|
741
|
+
currentGroupFilePath = ev.filePath;
|
|
742
|
+
startedFiles.add(ev.filePath);
|
|
743
|
+
const runnable = runnableByFile.get(ev.filePath) ?? [];
|
|
744
|
+
currentTestMap = new Map(runnable.map((ft) => [ft.test.meta.id, ft]));
|
|
745
|
+
if (isMultiFile) {
|
|
746
|
+
const relPath = relative(process.cwd(), ev.filePath);
|
|
747
|
+
console.log(`${colors.bold}📁 ${relPath}${colors.reset}`);
|
|
748
|
+
}
|
|
749
|
+
// Inline capability-skip display — preserves pre-migration layout
|
|
750
|
+
// where ⊘ lines sit between the file header and the first runnable
|
|
751
|
+
// test of the file.
|
|
752
|
+
const skips = fileCapabilitySkips.get(ev.filePath);
|
|
753
|
+
if (skips) {
|
|
754
|
+
for (const { ft, reason } of skips) {
|
|
755
|
+
skipped++;
|
|
756
|
+
const name = ft.test.meta.name || ft.test.meta.id;
|
|
757
|
+
console.log(` ${colors.yellow}⊘${colors.reset} ${name} ${colors.dim}— skipped (${reason})${colors.reset}`);
|
|
758
|
+
collectedRuns.push({
|
|
759
|
+
testId: ft.test.meta.id,
|
|
760
|
+
testName: name,
|
|
761
|
+
tags: ft.test.meta.tags,
|
|
762
|
+
filePath: ev.filePath,
|
|
763
|
+
events: [{ type: "status", status: "skipped", reason }],
|
|
764
|
+
success: true,
|
|
765
|
+
durationMs: 0,
|
|
766
|
+
groupId: ft.test.meta.groupId,
|
|
767
|
+
});
|
|
725
768
|
}
|
|
726
|
-
break;
|
|
727
769
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
770
|
+
break;
|
|
771
|
+
}
|
|
772
|
+
case "file:event": {
|
|
773
|
+
const event = ev.event;
|
|
774
|
+
switch (event.type) {
|
|
775
|
+
case "start": {
|
|
776
|
+
const entry = currentTestMap?.get(event.id);
|
|
777
|
+
testId = event.id;
|
|
778
|
+
testName = entry?.test.meta.name || event.name || event.id;
|
|
779
|
+
testItem = entry?.test || null;
|
|
780
|
+
startTime = Date.now();
|
|
781
|
+
testEvents = [];
|
|
782
|
+
assertions = [];
|
|
783
|
+
success = false;
|
|
784
|
+
errorMsg = undefined;
|
|
785
|
+
peakMemoryMB = undefined;
|
|
786
|
+
stepAssertionCount = 0;
|
|
787
|
+
stepTraceLines = [];
|
|
788
|
+
testStarted = true;
|
|
789
|
+
const tags = testItem?.meta.tags?.length
|
|
790
|
+
? ` ${colors.dim}[${testItem.meta.tags.join(", ")}]${colors.reset}`
|
|
791
|
+
: "";
|
|
792
|
+
console.log(` ${colors.cyan}●${colors.reset} ${testName}${tags}`);
|
|
793
|
+
if (testItem?.meta.description) {
|
|
794
|
+
console.log(` ${colors.dim}${testItem.meta.description}${colors.reset}`);
|
|
795
|
+
}
|
|
796
|
+
break;
|
|
733
797
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
798
|
+
case "status":
|
|
799
|
+
success = event.status === "completed";
|
|
800
|
+
if (event.error) {
|
|
801
|
+
errorMsg = event.error;
|
|
802
|
+
addLogEntry("error", event.error);
|
|
803
|
+
}
|
|
804
|
+
if (event.peakMemoryMB)
|
|
805
|
+
peakMemoryMB = event.peakMemoryMB;
|
|
806
|
+
finalizeTest();
|
|
807
|
+
break;
|
|
808
|
+
case "error":
|
|
809
|
+
success = false;
|
|
810
|
+
if (!errorMsg)
|
|
811
|
+
errorMsg = event.message;
|
|
812
|
+
addLogEntry("error", event.message);
|
|
813
|
+
break;
|
|
814
|
+
case "log":
|
|
815
|
+
addLogEntry("log", event.message);
|
|
816
|
+
if (event.message.startsWith("Loading test module:"))
|
|
817
|
+
break;
|
|
818
|
+
console.log(` ${colors.dim}${event.message}${colors.reset}`);
|
|
819
|
+
break;
|
|
820
|
+
case "assertion":
|
|
821
|
+
assertions.push({
|
|
822
|
+
passed: event.passed,
|
|
823
|
+
message: event.message,
|
|
824
|
+
actual: event.actual,
|
|
825
|
+
expected: event.expected,
|
|
826
|
+
});
|
|
827
|
+
stepAssertionCount++;
|
|
828
|
+
addLogEntry("assertion", event.message, {
|
|
829
|
+
passed: event.passed,
|
|
830
|
+
actual: event.actual,
|
|
831
|
+
expected: event.expected,
|
|
832
|
+
});
|
|
833
|
+
if (effectiveRun.verbose) {
|
|
834
|
+
const icon = event.passed ? `${colors.green}✓${colors.reset}` : `${colors.red}✗${colors.reset}`;
|
|
835
|
+
console.log(` ${icon} ${colors.dim}${event.message}${colors.reset}`);
|
|
836
|
+
}
|
|
837
|
+
break;
|
|
838
|
+
case "trace": {
|
|
839
|
+
const traceTarget = event.data.target ?? `${event.data.method ?? "?"} ${event.data.url ?? "?"}`;
|
|
840
|
+
const traceDuration = event.data.durationMs ?? event.data.duration ?? 0;
|
|
841
|
+
const traceProtocol = event.data.protocol ?? "http";
|
|
842
|
+
const traceMsg = `${traceTarget} → ${event.data.status} (${traceDuration}ms)`;
|
|
843
|
+
addLogEntry("trace", traceMsg, event.data);
|
|
844
|
+
traceCollector.push({
|
|
845
|
+
testId,
|
|
846
|
+
protocol: traceProtocol,
|
|
847
|
+
target: traceTarget,
|
|
848
|
+
method: event.data.method,
|
|
849
|
+
url: event.data.url,
|
|
850
|
+
status: event.data.status,
|
|
851
|
+
});
|
|
852
|
+
const displayTarget = event.data.method && event.data.url
|
|
853
|
+
? `${colors.dim}${event.data.method}${colors.reset} ${compactUrl(event.data.url)}`
|
|
854
|
+
: `${colors.dim}${traceTarget}${colors.reset}`;
|
|
855
|
+
const compactTrace = `${displayTarget} ${colors.dim}→${colors.reset} ${colorStatus(event.data.status)} ${colors.dim}${traceDuration}ms${colors.reset}`;
|
|
856
|
+
stepTraceLines.push(compactTrace);
|
|
857
|
+
console.log(` ${colors.dim}↳${colors.reset} ${compactTrace}`);
|
|
858
|
+
if (effectiveRun.verbose && event.data.requestBody) {
|
|
859
|
+
console.log(` ${colors.dim}req: ${JSON.stringify(event.data.requestBody).slice(0, 120)}${colors.reset}`);
|
|
860
|
+
}
|
|
861
|
+
if (effectiveRun.verbose && event.data.responseBody) {
|
|
862
|
+
const body = JSON.stringify(event.data.responseBody);
|
|
863
|
+
console.log(` ${colors.dim}res: ${body.slice(0, 120)}${body.length > 120 ? "…" : ""}${colors.reset}`);
|
|
864
|
+
}
|
|
747
865
|
break;
|
|
748
|
-
console.log(` ${colors.dim}${event.message}${colors.reset}`);
|
|
749
|
-
break;
|
|
750
|
-
case "assertion":
|
|
751
|
-
assertions.push({
|
|
752
|
-
passed: event.passed,
|
|
753
|
-
message: event.message,
|
|
754
|
-
actual: event.actual,
|
|
755
|
-
expected: event.expected,
|
|
756
|
-
});
|
|
757
|
-
stepAssertionCount++;
|
|
758
|
-
addLogEntry("assertion", event.message, {
|
|
759
|
-
passed: event.passed,
|
|
760
|
-
actual: event.actual,
|
|
761
|
-
expected: event.expected,
|
|
762
|
-
});
|
|
763
|
-
if (effectiveRun.verbose) {
|
|
764
|
-
const icon = event.passed ? `${colors.green}✓${colors.reset}` : `${colors.red}✗${colors.reset}`;
|
|
765
|
-
console.log(` ${icon} ${colors.dim}${event.message}${colors.reset}`);
|
|
766
866
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
protocol: traceProtocol,
|
|
777
|
-
target: traceTarget,
|
|
778
|
-
method: event.data.method,
|
|
779
|
-
url: event.data.url,
|
|
780
|
-
status: event.data.status,
|
|
781
|
-
});
|
|
782
|
-
const displayTarget = event.data.method && event.data.url
|
|
783
|
-
? `${colors.dim}${event.data.method}${colors.reset} ${compactUrl(event.data.url)}`
|
|
784
|
-
: `${colors.dim}${traceTarget}${colors.reset}`;
|
|
785
|
-
const compactTrace = `${displayTarget} ${colors.dim}→${colors.reset} ${colorStatus(event.data.status)} ${colors.dim}${traceDuration}ms${colors.reset}`;
|
|
786
|
-
stepTraceLines.push(compactTrace);
|
|
787
|
-
console.log(` ${colors.dim}↳${colors.reset} ${compactTrace}`);
|
|
788
|
-
if (effectiveRun.verbose && event.data.requestBody) {
|
|
789
|
-
console.log(` ${colors.dim}req: ${JSON.stringify(event.data.requestBody).slice(0, 120)}${colors.reset}`);
|
|
867
|
+
case "action": {
|
|
868
|
+
const a = event.data;
|
|
869
|
+
if (a.category === "http:request")
|
|
870
|
+
break;
|
|
871
|
+
const statusColor = a.status === "ok" ? colors.green : a.status === "error" ? colors.red : colors.yellow;
|
|
872
|
+
const statusIcon = a.status === "ok" ? "✓" : a.status === "error" ? "✗" : "⏱";
|
|
873
|
+
addLogEntry("action", `[${a.category}] ${a.target} ${a.duration}ms ${a.status}`, a);
|
|
874
|
+
console.log(` ${colors.dim}↳${colors.reset} ${colors.cyan}${a.category}${colors.reset} ${a.target} ${colors.dim}${a.duration}ms${colors.reset} ${statusColor}${statusIcon}${colors.reset}`);
|
|
875
|
+
break;
|
|
790
876
|
}
|
|
791
|
-
|
|
792
|
-
const
|
|
793
|
-
|
|
877
|
+
case "event": {
|
|
878
|
+
const evData = event.data;
|
|
879
|
+
addLogEntry("event", `[${evData.type}]`, evData);
|
|
880
|
+
if (effectiveRun.verbose) {
|
|
881
|
+
const summary = JSON.stringify(evData.data).slice(0, 80);
|
|
882
|
+
console.log(` ${colors.dim}[${evData.type}] ${summary}${colors.reset}`);
|
|
883
|
+
}
|
|
884
|
+
break;
|
|
794
885
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
886
|
+
case "metric": {
|
|
887
|
+
// ProjectRunner already accumulates into metricCollector (passed
|
|
888
|
+
// in above). CLI only handles verbose display + log entry.
|
|
889
|
+
const unit = event.unit ? ` ${event.unit}` : "";
|
|
890
|
+
const tagStr = event.tags
|
|
891
|
+
? ` ${colors.dim}{${Object.entries(event.tags)
|
|
892
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
893
|
+
.join(", ")}}${colors.reset}`
|
|
894
|
+
: "";
|
|
895
|
+
const metricMsg = `${event.name} = ${event.value}${unit}`;
|
|
896
|
+
addLogEntry("metric", metricMsg, {
|
|
897
|
+
name: event.name,
|
|
898
|
+
value: event.value,
|
|
899
|
+
unit: event.unit,
|
|
900
|
+
tags: event.tags,
|
|
901
|
+
});
|
|
902
|
+
if (effectiveRun.verbose) {
|
|
903
|
+
console.log(` ${colors.blue}📊 ${metricMsg}${colors.reset}${tagStr}`);
|
|
904
|
+
}
|
|
800
905
|
break;
|
|
801
|
-
const statusColor = a.status === "ok" ? colors.green : a.status === "error" ? colors.red : colors.yellow;
|
|
802
|
-
const statusIcon = a.status === "ok" ? "✓" : a.status === "error" ? "✗" : "⏱";
|
|
803
|
-
addLogEntry("action", `[${a.category}] ${a.target} ${a.duration}ms ${a.status}`, a);
|
|
804
|
-
console.log(` ${colors.dim}↳${colors.reset} ${colors.cyan}${a.category}${colors.reset} ${a.target} ${colors.dim}${a.duration}ms${colors.reset} ${statusColor}${statusIcon}${colors.reset}`);
|
|
805
|
-
break;
|
|
806
|
-
}
|
|
807
|
-
case "event": {
|
|
808
|
-
const ev = event.data;
|
|
809
|
-
addLogEntry("event", `[${ev.type}]`, ev);
|
|
810
|
-
if (effectiveRun.verbose) {
|
|
811
|
-
const summary = JSON.stringify(ev.data).slice(0, 80);
|
|
812
|
-
console.log(` ${colors.dim}[${ev.type}] ${summary}${colors.reset}`);
|
|
813
906
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
.
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
907
|
+
case "step_start":
|
|
908
|
+
stepAssertionCount = 0;
|
|
909
|
+
stepTraceLines = [];
|
|
910
|
+
console.log(` ${colors.cyan}┌${colors.reset} ${colors.dim}step ${event.index + 1}/${event.total}${colors.reset} ${colors.bold}${event.name}${colors.reset}`);
|
|
911
|
+
break;
|
|
912
|
+
case "step_end": {
|
|
913
|
+
const stepIcon = event.status === "passed"
|
|
914
|
+
? `${colors.green}✓${colors.reset}`
|
|
915
|
+
: event.status === "failed"
|
|
916
|
+
? `${colors.red}✗${colors.reset}`
|
|
917
|
+
: `${colors.yellow}○${colors.reset}`;
|
|
918
|
+
const stepParts = [];
|
|
919
|
+
if (event.durationMs !== undefined)
|
|
920
|
+
stepParts.push(`${event.durationMs}ms`);
|
|
921
|
+
if (event.assertions > 0)
|
|
922
|
+
stepParts.push(`${event.assertions} assertions`);
|
|
923
|
+
const httpInStep = stepTraceLines.length;
|
|
924
|
+
if (httpInStep > 0)
|
|
925
|
+
stepParts.push(`${httpInStep} API call${httpInStep > 1 ? "s" : ""}`);
|
|
926
|
+
console.log(` ${colors.cyan}└${colors.reset} ${stepIcon} ${colors.dim}${stepParts.join(" · ")}${colors.reset}`);
|
|
927
|
+
if (event.error) {
|
|
928
|
+
console.log(` ${colors.red}${event.error}${colors.reset}`);
|
|
929
|
+
}
|
|
930
|
+
break;
|
|
833
931
|
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
stepParts.push(`${event.durationMs}ms`);
|
|
850
|
-
if (event.assertions > 0)
|
|
851
|
-
stepParts.push(`${event.assertions} assertions`);
|
|
852
|
-
const httpInStep = stepTraceLines.length;
|
|
853
|
-
if (httpInStep > 0)
|
|
854
|
-
stepParts.push(`${httpInStep} API call${httpInStep > 1 ? "s" : ""}`);
|
|
855
|
-
console.log(` ${colors.cyan}└${colors.reset} ${stepIcon} ${colors.dim}${stepParts.join(" · ")}${colors.reset}`);
|
|
856
|
-
if (event.error) {
|
|
857
|
-
console.log(` ${colors.red}${event.error}${colors.reset}`);
|
|
932
|
+
case "summary":
|
|
933
|
+
runStats.httpRequestTotal += event.data.httpRequestTotal;
|
|
934
|
+
runStats.httpErrorTotal += event.data.httpErrorTotal;
|
|
935
|
+
runStats.assertionTotal += event.data.assertionTotal;
|
|
936
|
+
runStats.assertionFailed += event.data.assertionFailed;
|
|
937
|
+
runStats.warningTotal += event.data.warningTotal;
|
|
938
|
+
runStats.warningTriggered += event.data.warningTriggered;
|
|
939
|
+
runStats.stepTotal += event.data.stepTotal;
|
|
940
|
+
runStats.stepPassed += event.data.stepPassed;
|
|
941
|
+
runStats.stepFailed += event.data.stepFailed;
|
|
942
|
+
break;
|
|
943
|
+
case "warning": {
|
|
944
|
+
const warnIcon = event.condition ? `${colors.green}✓${colors.reset}` : `${colors.yellow}⚠${colors.reset}`;
|
|
945
|
+
console.log(` ${warnIcon} ${colors.yellow}${event.message}${colors.reset}`);
|
|
946
|
+
break;
|
|
858
947
|
}
|
|
859
|
-
|
|
948
|
+
case "schema_validation":
|
|
949
|
+
if (effectiveRun.verbose) {
|
|
950
|
+
const icon = event.success ? `${colors.green}✓${colors.reset}` : `${colors.red}✗${colors.reset}`;
|
|
951
|
+
console.log(` ${icon} ${colors.dim}schema: ${event.label}${colors.reset}`);
|
|
952
|
+
}
|
|
953
|
+
break;
|
|
954
|
+
case "session:set":
|
|
955
|
+
// ProjectRunner accumulates internally for cross-file forwarding;
|
|
956
|
+
// CLI keeps its copy only for symmetry with pre-migration code
|
|
957
|
+
// paths (useful e.g. for debug logging).
|
|
958
|
+
sessionState[event.key] = event.value;
|
|
959
|
+
continue;
|
|
860
960
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
961
|
+
if (testStarted)
|
|
962
|
+
testEvents.push(event);
|
|
963
|
+
break;
|
|
964
|
+
}
|
|
965
|
+
case "file:complete":
|
|
966
|
+
// Mirror the old inline loop's tail cleanup: if the harness died
|
|
967
|
+
// mid-test or emitted no start event, promote the leftover state
|
|
968
|
+
// to a visible failure row.
|
|
969
|
+
if (!testStarted && errorMsg) {
|
|
970
|
+
console.log(` ${colors.red}✗ ${errorMsg}${colors.reset}`);
|
|
971
|
+
failed++;
|
|
972
|
+
}
|
|
973
|
+
if (testStarted) {
|
|
974
|
+
if (!errorMsg)
|
|
975
|
+
errorMsg = "Process exited before test completed";
|
|
976
|
+
finalizeTest();
|
|
876
977
|
}
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
978
|
+
break;
|
|
979
|
+
case "run:complete":
|
|
980
|
+
// Fail-fast skip display: any file ProjectRunner never started
|
|
981
|
+
// (because the failure limit kicked in between file groups) gets
|
|
982
|
+
// the old "○ (skipped — fail-fast)" lines here, preserving the
|
|
983
|
+
// pre-migration output layout.
|
|
984
|
+
if (failureLimit !== undefined && ev.failedCount >= failureLimit) {
|
|
985
|
+
for (const [filePath, fileTests] of fileGroups) {
|
|
986
|
+
if (startedFiles.has(filePath))
|
|
987
|
+
continue;
|
|
988
|
+
if (isMultiFile) {
|
|
989
|
+
const relPath = relative(process.cwd(), filePath);
|
|
990
|
+
console.log(`${colors.bold}📁 ${relPath}${colors.reset}`);
|
|
991
|
+
}
|
|
992
|
+
for (const { test } of fileTests) {
|
|
993
|
+
skipped++;
|
|
994
|
+
const name = test.meta.name || test.meta.id;
|
|
995
|
+
console.log(` ${colors.yellow}○${colors.reset} ${name} ${colors.dim}(skipped — fail-fast)${colors.reset}`);
|
|
996
|
+
}
|
|
881
997
|
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
}
|
|
889
|
-
if (testStarted)
|
|
890
|
-
testEvents.push(event);
|
|
891
|
-
}
|
|
892
|
-
if (!testStarted && errorMsg) {
|
|
893
|
-
// Harness failed before emitting any test start (e.g. module load error)
|
|
894
|
-
console.log(` ${colors.red}✗ ${errorMsg}${colors.reset}`);
|
|
895
|
-
failed++;
|
|
896
|
-
}
|
|
897
|
-
if (testStarted) {
|
|
898
|
-
if (!errorMsg)
|
|
899
|
-
errorMsg = "Process exited before test completed";
|
|
900
|
-
finalizeTest();
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
// ── Session teardown ───────────────────────────────────────────────────
|
|
904
|
-
if (sessionFile) {
|
|
905
|
-
for await (const event of orchestrator.runSessionTeardown(sessionFile, { vars: envVars, secrets }, sessionState, toSingleExecutionOptions(shared))) {
|
|
906
|
-
if (event.type === "log") {
|
|
907
|
-
console.log(` ${colors.dim}[session] ${event.message}${colors.reset}`);
|
|
908
|
-
}
|
|
909
|
-
else if (event.type === "status" && event.status === "failed") {
|
|
910
|
-
console.log(` ${colors.yellow}⚠ Session teardown failed${event.error ? `: ${event.error}` : ""}${colors.reset}`);
|
|
911
|
-
}
|
|
998
|
+
}
|
|
999
|
+
break;
|
|
1000
|
+
case "run:failed":
|
|
1001
|
+
// Terminal failure — actual exit already happened in
|
|
1002
|
+
// bootstrap:failed / session:setup:failed above.
|
|
1003
|
+
break;
|
|
912
1004
|
}
|
|
913
1005
|
}
|
|
914
1006
|
const totalDurationMs = Date.now() - totalStartTime;
|