fix: report skipped and hook-aborted tests in the CLI flow#55
fix: report skipped and hook-aborted tests in the CLI flow#55kamal-kaur04 wants to merge 2 commits into
Conversation
…he CLI flow In the CLI (gRPC) pipeline, tests that never reach beforeTest/afterTest emitted no events at all: static it.skip, this.skip() inside before/beforeEach hooks, and suites aborted by a failed before hook. The legacy Listener -> api/v1/batch path these flowed through previously is not functional in the CLI pipeline, so such tests were invisible on the dashboard and their Automate sessions were never linked to the build. - add cli/skipReporter: routes skipped tests through the TestFramework tracker using the same INIT_TEST -> TEST PRE -> LOG_REPORT POST -> TEST POST sequence afterTest uses (LOG_REPORT/POST is what loads the result), serialized through a single chain since wdio does not await reporter hooks - reporter.onTestSkip: CLI branch reporting the skipped test with a resolved spec file (events without a location are rejected downstream) and the suite chain passed via ctx for hierarchy extraction - service.afterHook: on a non-passed BEFORE_ALL/BEFORE_EACH/AFTER_EACH hook, report the suite's undetermined tests as skipped (port of the legacy insights-handler cascade); service.beforeTest marks started tests so runtime this.skip() is never double-reported - wdioMochaTestFramework: hook results read `.status`, which does not exist on Frameworks.TestResult, leaving hook_result at 'pending' (coerced to 'passed' downstream) — failed before-hooks produced green builds while CI exited 1. Map passed/skipped/failed from the actual result fields. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
xxshubhamxx
left a comment
There was a problem hiding this comment.
Short answer: the overall direction of the PR is solid and consistent with the rest of the repo, but I would ask for a few concrete changes before merging, mainly around state lifecycle, identifiers, and a small control-flow edge case. Functionally the fix does what it claims.
Below I’ll walk through the main areas.
Core behavior changes
The PR introduces a new CLI-only skip reporter, hooks it into reporter.ts and service.ts, and fixes hook result mapping in WdioMochaTestFramework.
Conceptually:
- Skipped tests that never reach
beforeTest/afterTest(staticit.skip,this.skip()inbefore*, suites dropped after a failed hook) now emit events over the gRPC tracker and show up in Observability, with Automate sessions linked. - Hook results now read
passed/skippedfromFrameworks.TestResultinstead of a non-existentstatusfield, so failing before-hooks no longer show as “pending→passed”. - The CLI path mirrors the legacy
insights-handlersemantics for hook cascades and for theINIT_TEST → TEST PRE → LOG_REPORT POST → TEST POSTsequence.
That all lines up logically, and is consistent with how the rest of the service treats mocha and the CLI.
New skipReporter module
State & lifecycle
skipReporter.ts maintains:
startedTests: Set<string>reportedSkips: Set<string>reportChain: Promise<void>to serialize trackEvent calls.
Issues / suggestions:
-
State never reset across worker lifetime
The sets and
reportChainare module-level and never cleared. In a long-lived worker that runs multiple specs sequentially, any identifiers added in the first spec will keep suppressing later reports with the same identifier (for example, if another file defines the same “<suite> - <title>” combination).This is subtle, but given BrowserStack often runs multiple specs per worker, it’s safer to expose and use a reset:
// skipReporter.ts export function resetSkipReporter() { startedTests.clear() reportedSkips.clear() reportChain = Promise.resolve() }
And call
resetSkipReporter()from a suitable lifecycle (e.g.onWorkerEndor at least in the unit testbeforeEach). -
Tests don’t reset this state
skipReporter.test.tsonly callsvi.clearAllMocks()inbeforeEach, so the sets andreportChainleak between test cases. That makes some expectations brittle and could hide issues if you reorder/add tests. Once you addresetSkipReporter, it should be used there as well. -
Serialization strategy
The
reportChainpattern to serializetrackEventcalls is appropriate, givenwdiodoes not await reporter hooks and the CLI tracker instance is single and mutable. The test that asserts ordering of concurrent reports exercises this nicely.
Identifier choice
Both reportSuiteSkipped and the CLI onTestSkip branch use:
const identifier = `${parentTitle} - ${t.title}`or the equivalent in reporter.ts.
- This matches the legacy pattern in
insights-handlerwhere the same string is used to key_testswhen sending skipped tests for hook cascades. - However,
skipReporteruses this string inreportedSkipsas a global dedup key.
Risk: if two distinct tests in the same run share the same parentTitle and title (e.g., same nested describe structure across two files), the second will never be reported as skipped in the CLI path because reportedSkips already contains that identifier.
I’d recommend making the key more specific, e.g.:
const identifier = `${stats.file ?? ''}:${parentTitle} - ${stats.title}`and mirroring that in reportSuiteSkipped (including file) so dedup is per-file+suite+title rather than just suite+title.
resolveSpecFile and path handling
resolveSpecFile does:
if (runnerSpec) {
return runnerSpec.startsWith('file://')
? runnerSpec.replace(/^file:\/\//, '')
: path.resolve(runnerSpec)
}Concerns:
- On Unix,
file:///runner/spec.js→/runner/spec.js, which is fine. - On Windows,
file:///C:/spec.js→/C:/spec.js, which is not a valid native path and won’t match whatpath.relativeor the binary expects.
You already import node:path; consider importing fileURLToPath from node:url and doing:
import { fileURLToPath } from 'node:url'
if (runnerSpec?.startsWith('file://')) {
return fileURLToPath(runnerSpec)
}This will be correct cross-platform and consistent with how reporter.ts already uses url.fileURLToPath for Jasmine.
Reporter integration (reporter.ts)
The new onTestSkip branch for CLI is:
- Guarded by
needToSendData('test', 'skip')(true for mocha). - Uses
BrowserstackCLI.getInstance().isRunning()to decide between CLI and legacy paths. - Constructs
identifierandctxhierarchy, resolves the spec file viaresolveSpecFile, then callsreportSkippedTest.
This is structurally sound, but there are two nuances.
Control flow if CLI is “running” but framework is null
You have:
if (BrowserstackCLI.getInstance().isRunning()) {
const framework = BrowserstackCLI.getInstance().getTestFramework()
if (framework) {
// ... await reportSkippedTest(...)
}
return
}- The
returnis inside theisRunning()block but outside theif (framework)guard. - In normal operation,
isRunning()should only be true when a test framework is registered, butgetTestFramework()can benullduring startup races or for non-mocha frameworks.
If that ever happens, the code returns without emitting a CLI event and without falling back to the legacy path, silently dropping the skip. A safer pattern:
if (BrowserstackCLI.getInstance().isRunning()) {
const framework = BrowserstackCLI.getInstance().getTestFramework()
if (framework) {
// normal CLI handling
await reportSkippedTest(...)
return
}
// fall through to legacy path if framework is unexpectedly null
}That preserves behavior in the expected case but avoids a hard swallow on unusual CLI states.
Hierarchy via _suites
The way you reconstruct parentChain from _suites and pass it via ctx is consistent with how insights-handler builds hierarchy for mocha tests. That should give the CLI the same describe-chain as the legacy path.
Service integration (service.ts)
Two main changes:
-
beforeTest: mark the test as “started” for dedup.if (this._config.framework === 'mocha' && uuid) { this._cliTestUuids.set(getUniqueIdentifier(test, this._config.framework), uuid as string) } markTestStarted(getUniqueIdentifier(test, this._config.framework))
- Using
getUniqueIdentifier(test, framework)keeps this aligned with how the legacy path keys tests ininsights-handler. - This ensures runtime
this.skip()tests (which do go throughbeforeTest) aren’t double-reported via theonTestSkipbranch — good.
- Using
-
afterHook: cascade skip for undetermined tests after failing hooks.const hookType = getHookType((test as Frameworks.Test).title) const suite = (test as Frameworks.Test).ctx?.test?.parent if (result && !result.passed && ['BEFORE_ALL', 'BEFORE_EACH', 'AFTER_EACH'].includes(hookType) && suite) { await reportSuiteSkipped(framework, suite) }
- This is a direct CLI port of the legacy
insights-handler.afterHookbehavior, includingAFTER_EACH. - Semantically, it means: if any of
beforeAll,beforeEach, orafterEachfails, remaining undetermined tests in the suite hierarchy are reported as skipped, matching the non-CLI path.
- This is a direct CLI port of the legacy
This is consistent with the existing behavior; if you ever decide that cascading on AFTER_EACH is not desired, you should change both insights-handler and skipReporter together. For the purpose of this PR, mirroring the legacy logic is correct.
Also nice: after() now clears _cliTestUuids after sweeping unfinished tests, ensuring snapshots don’t leak across workers. skipReporter’s sets are the only state left not tied into teardown.
Hook result mapping (wdioMochaTestFramework.ts)
Previously, hook results read testResult.status, which doesn’t exist on Frameworks.TestResult, leaving hooks in the default "pending" state and causing the binary to treat them as passed.
The new mapping:
const result = testResult
? (testResult.passed ? 'passed' : (testResult.skipped ? 'skipped' : 'failed'))
: TestFrameworkConstants.DEFAULT_HOOK_RESULTThis is the right thing to do:
- It correctly distinguishes
passed,skipped, andfailedbased on the actual WDIO result flags. - The comment about
this.skip()arriving as{ passed: false, skipped: true }matches WDIO’s behavior for skipped hooks. - The default remains “pending” when no result exists, which is reasonable for hooks that genuinely never ran.
Tests and coverage
The new skipReporter.test.ts suite covers several key behaviors:
- Correct INIT/TEST/LOG_REPORT/TEST sequence and result shape.
- Deduplication of repeated identifiers.
- Respecting
markTestStarted(no double-report for tests that hitbeforeTest). - Serialization of concurrent reports (ensuring order).
- Walking nested suites and skipping already-determined tests.
resolveSpecFilebehavior.
Gaps to consider filling:
- A test that simulates two distinct skipped tests with the same
parentTitle+titlein different suites/files, to make the dedup behavior explicit (either as expected or as a regression test after tightening identifiers). - A test that exercises the
onTestSkipCLI branch end-to-end via a small harness (even if partially mocked), to catch any future regressions in theframeworknull/fall-through logic.
Repo-wide fit
Looking at the broader repo structure and existing patterns:
- Logging: use of
BStackLoggerinskipReportermatches the rest of the service. - “Legacy Listener vs CLI” split: the new code mirrors existing guards (
BrowserstackCLI.getInstance().isRunning()) and keeps the legacyListener → api/v1/batchpath intact for non-CLI flows. getHookType,getUniqueIdentifier, and hierarchy-building are reused consistently fromutil.tsandinsights-handler.ts.- The comments are long, but in this repo that’s the established style (detailed behavioral notes and references to upstream Mocha/CLI behavior).
I don’t see any obvious violations of existing architectural conventions.
Concrete changes I’d request before approval
- Add a reset entry point to
skipReporterand use it in tests; consider also calling it at worker teardown to avoid state bleeding between runs. - Make the skip identifier more specific (include
file), to avoid cross-file collisions when different tests share the same suite/title string. - Adjust the
onTestSkipcontrol flow to onlyreturnearly whenframeworkis non-null, letting unexpectedgetTestFramework() === nullfall back to the legacy path. - Use
fileURLToPathforfile://URLs inresolveSpecFilefor cross-platform correctness.
Everything else looks consistent and, once these points are addressed, I’d be comfortable with this landing.
- batchAndPostEvents: read the response as text and check response.ok — error responses (401/5xx) and empty bodies are not JSON, and the blind response.json() surfaced every failure as a misleading "Unexpected end of JSON input". Failures now carry the HTTP status and a body snippet. - listener: include the failure reason when a batch is marked failed instead of only the event count. - trackHookEvents: also treat mocha's sync-skip error shape as a skipped hook (wdio v8 delivers this.skip() hooks without a `skipped` flag; harmless on v9, keeps both lines aligned). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Addendum (commit
|
Why
In the CLI (gRPC) pipeline, tests that never reach
beforeTest/afterTestemit no events at all: staticit.skip,this.skip()insidebefore/beforeEachhooks, and suites aborted by a failedbeforehook. The legacyListener → api/v1/batchpath these flowed through previously is not functional in the CLI pipeline (its own usage stats reportskipped: { triggeredCount: N, sentCount: 0, failedCount: N }), so such tests are invisible on the Test Observability dashboard and their Automate sessions are never linked to the build (surfacing as session-linking noise in build-stability metrics).Separately, a failed
beforehook produced a green build while CI exited 1: hook results readtestResult.status, which does not exist onFrameworks.TestResult, leavinghook_resultat'pending'— which downstream coerces to'passed'for any finished hook.What
cli/skipReporter.ts(new): routes skipped tests through the TestFramework tracker using the sameINIT_TEST → TEST PRE → LOG_REPORT POST → TEST POSTsequenceafterTestuses (LOG_REPORT/POSTis what loads the result). Reports are serialized through a single chain — wdio does not await reporter hooks, and unserialized sequences interleave on the tracker's single per-worker instance. Started/reported dedup sets prevent double-reporting runtimethis.skip()(which already reports viaafterTest).reporter.ts:onTestSkipCLI branch — reports the skipped test with a resolved spec file (events without a file location are rejected downstream) and the suite chain passed viactxfor hierarchy extraction (parentstays a string; the Automate session name interpolates it).service.ts:afterHookCLI branch — on a non-passedBEFORE_ALL/BEFORE_EACH/AFTER_EACHhook, reports the suite's undetermined tests as skipped (port of the legacy insights-handler cascade);beforeTestmarks started tests for the dedup.cli/frameworks/wdioMochaTestFramework.ts: hook results mapped from the actualFrameworks.TestResultfields —passed/skipped: true/ else →passed/skipped/failed, so genuine hook failures reach the dashboard while deliberatethis.skip()hooks stay non-failing.Validation
Live-validated against BrowserStack with a 6-spec matrix (2 normal specs,
it.skip, runtimethis.skip(),this.skip()inbeforeandbeforeEach, and a throwingbeforehook), verified via the Observability API:passed, stats{passed: 3}or{passed: 4}— skipped/aborted suites entirely absent from the hierarchy, their sessions unlinked.{passed: 4, failed: 1, skipped: 8}— all 12 tests present exactly once, every test attributed to its Automate session, build statusfaileddriven only by the genuinely-thrown hook (conditional-skip suites stay green), hook results on the wire: 1failed, 2skipped, 5passed.npm run build(buf generate + esbuild + tsc) passes. New unit tests forskipReporter(6, all passing); pre-existing fetch-mock test failures onmainare unchanged by this PR.🤖 Generated with Claude Code