test(e2e): retry a crashed Windows spec in a fresh VS Code session - #2132
Conversation
*Why*: * On the slow Windows e2e shards, a VS Code window reload (e.g. opening a freshly initialized bundle in bundle_init) can drop the wdio<->VS Code websocket with "Connection closed. Code: 1006". That kills the whole session, so every remaining test in the spec cascades into "target window already closed" / "web view not found". * This whole-session crash is unrecoverable from inside the test: no added wait or try/catch helps once the session is dead. The only recovery is a fresh session, which is exactly what a spec-file retry starts. * wdio's specFileRetries was 0, so this class of infra flake was an instant hard failure with no recovery. *What:* * Set wdio specFileRetries to re-run a whole spec once, Windows-only, via a small specFileRetriesForPlatform(platform) helper. Linux/macOS stay at 0 so a real regression there fails fast instead of being masked, and CI time is not doubled on the stable platforms. *Verification:* * yarn test:unit — 845 passing, 0 failing (includes the new helper tests). * yarn run eslint + prettier -c clean on the changed files. Co-authored-by: Isaac
|
🤖 Integration tests ✅ all 41 test jobs passed for |
*Why*: * Code-conventions review flagged two nits: the new local import preceded an external one (Section 7 wants external-first), and the helper's doc-comment was longer than the code it guards (Section 4b). *What:* * Moved the ../retry.ts import after node:util, and trimmed the specFileRetriesForPlatform comment to the load-bearing rationale. *Verification:* * prettier -c + eslint clean; retry unit tests 13 passing. Co-authored-by: Isaac
|
🤖 Integration tests ✅ all 41 test jobs passed for |
*Why*: * A multi-source review converged on a real gap: the Windows specFileRetries fires on ANY whole-spec failure, not just the WS-1006 session crash it targets, so a *flaky* Windows-only regression could fail once, pass on retry, and leave CI silently green. wdio has no retry-on-specific-error hook, so spec-file retry stays (it's the only layer that recovers a dead session), but the masking must not be silent. *What:* * Added SpecRetryTracker, wired into wdio onWorkerEnd/onComplete, which prints a "PASSED ONLY ON RETRY" summary for any spec that failed an attempt then passed — turning a would-be-silent flake into a visible CI signal. * Corrected the specFileRetriesForPlatform comment to state the retry is blanket-scoped, not 1006-specific. *Verification:* * yarn test:unit — 850 passing, 0 failing. * eslint + prettier clean; e2e project typechecks (the new hooks included). Co-authored-by: Isaac
|
🤖 Integration tests triggered for |
*Why*: * Review noted the "passed only on retry" signal was a bare console.log at the end of a large, usually-unread green log — weak visibility for the very flakes the tracker exists to surface. A retry-recovered flake turns CI green, so the signal has to appear where a green run is still seen. *What:* * Under GitHub Actions, onComplete now emits a `::warning::` workflow command per recovered spec (surfaces as a checks-UI annotation on green runs), in addition to the human-readable summary; formatting extracted into a tested formatRecoveredSpecsReport helper. * Pinned the one-spec-per-worker assumption behind onWorkerEnd's per-spec attribution, and noted the fail-pass-fail test guards a path unreachable at the current retry count. *Verification:* * yarn test:unit — 853 passing, 0 failing. * eslint + prettier clean; e2e project typechecks. Co-authored-by: Isaac
|
🤖 Integration tests triggered for |
anton-107
left a comment
There was a problem hiding this comment.
Approving — the diagnosis holds up. A WS-1006 session death really is unrecoverable in-test, so this is a different class from the per-waitUntil hardening in #2056/#2123, and Windows-only + a single retry + surfacing the recovery instead of swallowing it is the right shape. SpecRetryTracker keying on the spec string is correct: _endHandler re-pushes the same specs array on requeue, so the key is stable across attempts.
One thing worth a follow-up, because it partly defeats the tracker's own purpose.
The retry truncates the crashed attempt's logs
wdio deliberately reuses the failing runner's cid for a retry — @wdio/cli/build/launcher.js:329-331, "Retried tests receive the cid of the failing test as rid". Every per-worker artifact path is therefore byte-identical across attempts, and all of them are opened truncating:
WDIO_LOG_PATH = logs/wdio-<cid>.log(@wdio/local-runner/build/worker.js:91), opened asfs.createWriteStream(path)with defaultflags: 'w'(@wdio/logger/build/node.js:63)logs/wdio-<worker-id>-chromedriver.log— explicit{flags: 'w'}(@wdio/utils/build/node/startWebDriver.js:126-129)afterSession(wdio.conf.ts:606) copies extension logs intologs/vscode-logs-<spec-basename>/<basename>.json; both name parts are deterministic (LoggerManager.getLogFile→${prefix}-logs.json), socpSyncoverwrites
So on exactly the outcome this PR exists to produce — attempt 1 crashes, attempt 2 goes green — the wdio log holding the Connection closed. Code: 1006 and the extension logs from the crashed session are gone by the time the run finishes. The ::warning:: says bundle_init.e2e.ts flaked, but the artifact no longer contains the evidence to act on it. Videos survive (timestamped filenames, and saveAllVideos: false keeps the failed one), so it isn't a total loss — but the logs are the part you'd actually want.
Quick fix
Cheap, and onWorkerEnd is already the right place — note the hook currently drops the 4th param, which is precisely the signal needed (retries > 0 means wdio is about to requeue this spec and reopen those paths):
onWorkerEnd: function (cid, exitCode, specs, retries) {
for (const spec of specs) {
specRetryTracker.record(spec, exitCode === 0);
}
// A retry reuses this cid, so wdio reopens logs/wdio-<cid>.log with
// flags:'w' and truncates the very crash we retried for. Park it.
if (exitCode !== 0 && retries > 0) {
const from = path.join("logs", `wdio-${cid}.log`);
const to = path.join("logs", `wdio-${cid}-failed-attempt.log`);
try {
renameSync(from, to);
} catch (e) {
console.error(`Could not preserve ${from}:`, e);
}
}
},Same treatment for wdio-<cid>-chromedriver.log. For the extension logs, folding an attempt marker into the vscode-logs-* directory name in afterSession covers it — a module-level Map<spec, attempt> incremented in onWorkerEnd won't reach the worker (different process), so the simplest version is a suffix derived from something already per-attempt, or just letting the parked wdio log carry the diagnosis.
Smaller notes, non-blocking
specFileRetriesDelay: 0stays at zero on the one platform where that's riskiest. The retry relaunches VS Code immediately after a session died mid-window-reload, while the old Electron and chromedriver are still exiting, and its first act (beforeSession) iscode --install-extension --forceagainst the sharedEXTENSIONS_DIR— the same Windows file-lock territoryisTransientFileLockErrorin this very file exists to paper over. ~5–10s via the same per-platform helper costs nothing against a multi-minute spec.- The
::warning::probably doesn't reach the PR author.integration-tests.ymldispatches the isolated workflow in a separate repo and reports back only through an API-created check run, so the annotation attaches to that run rather than the "Integration Tests" check on the PR.GITHUB_STEP_SUMMARY, or feeding the check-run output, would land where people look — or at minimum a comment noting where the annotation actually surfaces. - Shard timeout headroom. A retry roughly doubles the shard's wall clock, and
afterSuitecompounds it: it runsbundle destroyover every subfolder ofWORKSPACE_PATH, andonWorkerStartmints a freshtestRootper attempt, so the retry's cleanup also destroys the orphaned one. Worth confirming the Windows shard has ~2x headroom — otherwise this turns a clean failure into a timeout with no report, which is strictly worse. - Convention nit:
SpecRetryTrackeris a class in a camelCase file. CODE_CONVENTIONS §1 wantsPascalCase.tsnamed for the class export, and theretry.tsheader rationale (live outsidee2e/so the tsconfig exclude doesn't hide the unit test) applies just as well to a siblingsrc/test/SpecRetryTracker.ts. formatRecoveredSpecsReportprints whateveronWorkerEndpasses, which is afile:///…absolute URL (FileSystemPathService.ensureAbsolutePathreturnspathToFileURL(p).href). Basenames read better —afterSessionalready does that split.
*Why*: * Review (Anton) found the retry defeats part of its own purpose: wdio reuses the failed worker's cid, so on the fail→pass outcome the tracker exists to flag, wdio reopens logs/wdio-<cid>*.log with flags:"w" and truncates the very "Connection closed. Code: 1006" evidence. Verified in @wdio/local-runner and @wdio/utils (both open with flags:"w", keyed on the cid). * The retry also relaunched VS Code and reinstalled the extension into the shared dir with zero delay, racing the crashed Electron/chromedriver still exiting — the Windows file-lock territory isTransientFileLockError guards. *What:* * onWorkerEnd now parks logs/wdio-<cid>.log and the chromedriver log to *-failed-attempt names before a retry truncates them (only when a failed attempt will be retried), via tested shouldPreserveFailedAttemptLogs + failedAttemptLogRenames helpers. * Added a Windows-only specFileRetriesDelay (specFileRetriesDelayForPlatform) so the retry waits for the old processes to release locks. * Moved SpecRetryTracker to its own PascalCase file (CODE_CONVENTIONS §1) with a co-located test; the recovered-flake report now prints spec basenames. *Verification:* * yarn test:unit — 860 passing, 0 failing. * eslint + prettier clean; e2e project typechecks (no new errors). Co-authored-by: Isaac
|
🤖 Integration tests triggered for |
*Why*:
* Review (Codex) caught that the log-preservation fix targeted the wrong file:
@wdio/local-runner names the runner log `${specBaseName}-<cid>.log` when a
spec is present (local-runner build/index.js:261-266), not `wdio-<cid>.log`.
So the rename hit ENOENT and the actual crash log was still truncated by the
retry — the fix didn't preserve the log it existed to save.
*What:*
* failedAttemptLogRenames now takes the spec and derives the runner-log name
from its basename (stripping only the final extension, matching wdio); the
chromedriver name was already correct. onWorkerEnd passes specs[0].
*Verification:*
* yarn test:unit — 861 passing, 0 failing (tests updated to the real filename,
incl. a file:// URL case).
* eslint + prettier clean; e2e project typechecks.
Co-authored-by: Isaac
|
🤖 Integration tests ✅ all 41 test jobs passed for |
|
Thanks @anton-107 for the detailed review — every point verified against the wdio 9.29 source. Here's what was addressed: Addressed
Deferred (tracked as a separate follow-up)
Noted, not changed
Unit tests: 861 passing; eslint/prettier/e2e-typecheck clean. |
|
If integration tests don't run automatically, an authorized user can run them manually by following the instructions below: Trigger: Inputs:
Checks will be approved automatically on success. |
Why
On the slow Windows e2e shards, a VS Code window reload — e.g. opening a freshly initialized bundle in
bundle_init.e2e.ts— can drop the wdio↔VS Code websocket withConnection closed. Code: 1006. That kills the whole session, so every remaining test in the spec cascades intotarget window already closed/web view not found. It was the failure on PR #2130's IT run and recurs across nightlies as a bystander flake.This whole-session crash is unrecoverable from inside the test: no added wait or
try/catchhelps once the session is dead (the earlier per-waitUntilhardening in #2056/#2123 addresses a different class — transient element races within a live session). The only recovery is a fresh session, which is exactly what a spec-file retry starts.wdio'sspecFileRetrieswas0, so this class of infra flake was an instant hard failure with no recovery.What
specFileRetriesto re-run a whole spec once, Windows-only, via a smallspecFileRetriesForPlatform(platform)helper insrc/test/retry.ts.0so a real regression there fails fast instead of being masked, and CI time isn't doubled on the stable platforms.Tradeoff
Blanket spec retry can mask a reproducible-once real failure. Scoping to Windows + a single retry keeps that risk small while covering the observed crash class.
Verification
yarn test:unit— 845 passing, 0 failing (includes the new helper tests).eslint+prettier -cclean on the changed files.This pull request and its description were written by Isaac.