fix(coverage): abort when the test process dies during collection

CI on the 3.9 matrix showed the fail-fast guard was incomplete: on that
SDK `dart run` reports a VM service URI even when the test script could
not be loaded, so the service-URI check never fired and the run hung in
`collect_coverage --wait-paused` waiting for an isolate that would never
pause.

The test process is started with --pause-isolates-on-exit, so collection
is what allows it to finish. If it exits with an error before collection
completes, it never reached that pause and waiting is futile. Race the
collection against the process exit and propagate the exit code.

Widen the regression test to accept either failure message, since which
guard fires depends on whether the SDK brings up the service first.
diff --git a/pkgs/coverage/bin/test_with_coverage.dart b/pkgs/coverage/bin/test_with_coverage.dart
index 488f60c..fc6687f 100644
--- a/pkgs/coverage/bin/test_with_coverage.dart
+++ b/pkgs/coverage/bin/test_with_coverage.dart
@@ -330,7 +330,7 @@
     final scopes = flags.scopeOutput.isEmpty
         ? getAllWorkspaceNames(flags.packageDir)
         : flags.scopeOutput;
-    await collect_coverage.main([
+    final collection = collect_coverage.main([
       '--wait-paused',
       '--resume-isolates',
       '--uri=$serviceUri',
@@ -340,6 +340,24 @@
       '-o',
       outJson,
     ]);
+
+    // The test process is paused on exit, so collection is what lets it
+    // finish. If it exits with an error first, it never reached that pause
+    // (for example, the test script failed to load) and `--wait-paused` would
+    // wait forever. Some SDKs report a service URI even in that case, so the
+    // check above is not enough on its own.
+    final collected = await Future.any<Object?>([
+      collection.then((_) => null),
+      process.exitCode,
+    ]);
+    if (collected is int && collected != 0) {
+      stderr.writeln(
+        'Test process exited with code $collected before coverage collection '
+        'finished.',
+      );
+      exit(collected);
+    }
+    await collection;
     exitCode = await process.exitCode.timeout(
       const Duration(seconds: 3),
       onTimeout: () {
diff --git a/pkgs/coverage/test/test_with_coverage_test.dart b/pkgs/coverage/test/test_with_coverage_test.dart
index 504e8e5..a43b946 100644
--- a/pkgs/coverage/test/test_with_coverage_test.dart
+++ b/pkgs/coverage/test/test_with_coverage_test.dart
@@ -332,8 +332,10 @@
     await process.shouldExit(1);
   });
 
-  // A test process that dies before the VM service is ready must fail fast
-  // rather than waiting forever for a service URI that will never arrive.
+  // A test process that dies without pausing must fail fast rather than
+  // waiting forever. Which guard fires depends on the SDK: some report a VM
+  // service URI even when the test script could not be loaded, so the failure
+  // is only detectable once collection is under way.
   test('dart run bin/test_with_coverage.dart fails fast when the test process '
       'cannot start', () async {
     final process = await _run([
@@ -344,7 +346,12 @@
     ]);
     await expectLater(
       process.stderr,
-      emitsThrough(contains('exited before the VM service was ready')),
+      emitsThrough(
+        anyOf(
+          contains('exited before the VM service was ready'),
+          contains('before coverage collection finished'),
+        ),
+      ),
     );
     await process.shouldExit(isNot(0));
   });