A 27-Second Jest Crash Traced to a 1993 Prank

During a pairing session on a React feature for our internal Backstage instance, everything worked locally. Then came the first commit, which triggered yarn test โ€” and everything derailed. The test runner hung, and after a long pause, failed with a cryptic [Error].

Determining test suites to run...

  โ— Test suite failed to run

thrown: [Error]

error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
๐ŸŒˆ  backstage  โšก

The error message was monumentally unhelpful, and web searches returned an ocean of false positives. Attempts to work around the problem with targeted test runs and various Jest flags all ended the same way:

PASS   @cloudflare/backstage-components  plugins/backstage-components/src/components/Cards/TeamMembersListCard/TeamMembersListCard.test.tsx (6.787 s)
PASS   @cloudflare/backstage-components  plugins/backstage-components/src/components/Cards/ClusterDependencyCard/ClusterDependencyCard.test.tsx
PASS   @internal/plugin-software-excellence-dashboard  plugins/software-excellence-dashboard/src/components/AppDetail/AppDetail.test.tsx
PASS   @cloudflare/backstage-entities  plugins/backstage-entities/src/AccessLinkPolicy.test.ts

  โ— Test suite failed to run

thrown: [Error]

Spotting the Timeout Pattern

The breakthrough came when I noticed that Jest would crash even while idling in its interactive menu. Timing the runs revealed a consistent pattern: regardless of what tests were selected or re-run, the crash always happened after almost exactly 27 wallclock seconds.

Determining test suites to run...

  โ— Test suite failed to run

thrown: [Error]

error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
yarn test  2.05s user 0.71s system 10% cpu 27.094 total

Searching for "jest timeout 27 seconds" turned up nothing useful. My colleague confirmed his macOS system was unaffected while my Linux box crashed reproducibly, even on a clean clone of the Backstage repo. Under heavy load, the crash time stretched to 28-29 seconds, suggesting the issue was processing-driven rather than a hard timer.

Tracing the Syscalls

Using strace on the simpler case of the idling Jest menu revealed that the process was waiting on epoll_wait until the 27-second mark, then getting interrupted by SIGCHLD from a child process. A follow-up trace with --follow-forks produced an enormous log โ€” 500,000 lines for the original case, with a smaller version available for the clean Backstage instance.

Filtering the trace for execve calls and counting unique executables revealed the culprit:

๐ŸŒˆ  ~  zgrep -oP '(?<=execve\(")[^"]+' trace.log.gz | xargs -L1 basename | sort | uniq -c | sort -nr
    576 watchman
    576 hg
    368 sl
    358 git
     16 sl.actual
     14 node
      2 sh
      1 yarn
      1 backstage-cli

The offending binary was sl โ€” a joke program from 1993 that displays an animated steam locomotive when users mistype ls. Renaming sl immediately fixed the problem.

An Unfortunate Naming Collision

The root cause was a clash between sl the Steam Locomotive and sl the Sapling CLI, a source control tool. Jest's jest-changed-files module invokes sl to detect changed files, but it picked up the prank binary instead. The Jest developers have acknowledged the issue and prepared a fix that was still unreleased at the time.

Why 27 Seconds?

The train animation's duration depends on terminal width. Since Jest invokes sl via execa, which assumes a default width of 80 columns, each run takes about 6.7 seconds. Intercepting sl execution showed it was being called in four waves of five concurrent workers each, totaling roughly 26.35 seconds of execution โ€” just about the observed 27-second crash point.

The five workers were executing sl root, corresponding to getRoot() in jest-change-files/sl.ts. The Backstage configuration includes 16 rootDirs, requiring four round trips at five workers per wave. The final crash occurred when a command failed with ENAMETOOLONG, as the joke sl program choked on the full repository path arguments that the Sapling CLI would have accepted.

The whole episode was a reminder that even the most well-trodden development tools can be thrown off by an unexpected binary sitting in PATH โ€” especially one with a sense of humor dating back three decades.