Repository: facebook/react
Pull request: [tests] Fix flaky flight tests

Flights tests are failing locally and in CI non-deterministically because we're not disabling async hooks after tests, and GC can clear WeakRefs non-deterministically. 

This PR fixes the issue by adding an afterEach to disable installed hooks, and normalizing the `value` to `value: {value: undefined}}` when snapshotting. 

## Claude found the fix, here's the explanation:

Summary of Both Flaky Test Issues

### Issue 1: ReactFlightDOMEdge-test.js - Cross-Test Async Hook Pollution

  Root Cause:
  - When tests run with --runInBand, they execute sequentially in the same process
  - ReactFlightAsyncDebugInfo-test.js loads the Node server which creates async hooks via createHook().enable() to track async operations
  - ReactFlightDOMEdge-test.js loads the Edge server which uses a Noop debug config (no async hooks created)                                                                                                             - The async hooks from the Node server persisted across tests because:
    - The Edge server doesn't create new hooks, so the cleanup code in setupTests.js never triggered
    - Old hooks kept firing their callbacks, calling performance.now() on the Edge test's mocks
    - This caused the Edge test's time values to be 232, 259, 260 instead of expected 12, 13, 14

  The Fix: scripts/jest/setupTests.js
  
  ```
  afterEach(() => {
    if (installedHook) {
      installedHook.disable();
      installedHook = null;
    }
  });
  ```
  
  This ensures async hooks are disabled after each test, preventing cross-test pollution.


### Issue 2: ReactFlightAsyncDebugInfo-test.js - WeakRef Garbage Collection

  Root Cause:
  - React's async debug info uses WeakRef to hold references to promises without preventing garbage collection
  - When serializing debug info, the code calls weakRef.deref() to get the promise
  - If GC has run: deref() returns undefined, and the server doesn't include the value field at all
  - If GC hasn't run: deref() returns the promise, and the server includes value: { value: undefined }
  - Since GC timing is non-deterministic (depends on memory pressure, CPU, etc.), tests would randomly pass or fail

  The Fix: packages/internal-test-utils/debugInfo.js
  ```
  // In normalizeIOInfo()
  } else if ('value' in ioInfo) {
    // If value exists but is undefined (WeakRef was GC'd)
    copy.value = { value: undefined };
  } else if (ioInfo.name && ioInfo.name !== 'rsc stream') {
    // If value is completely missing (server didn't send it due to GC)
    copy.value = { value: undefined };
  }
  ```
  
  This normalizes the output so snapshots always see a consistent structure regardless of GC timing.

  ---
  ★ Insight ─────────────────────────────────────
  Two Patterns for Flaky Tests:
  1. State Leakage Between Tests: When tests run sequentially, global state (like async hooks, timers, or singletons) can leak. Solution: Clean up thoroughly in afterEach.
  2. Non-Deterministic System Behavior: When tests depend on unpredictable behavior (GC timing, network timing, file system operations), normalize the data in test utilities rather than trying to control the underlying system.
  ─────────────────────────────────────────────────

  Both issues shared a common theme: non-deterministic behavior in sequential test execution. The first was about test isolation failure, the second was about normalizing inherently non-deterministic runtime behavior.

Changed files:
filename: packages/internal-test-utils/debugInfo.js
patch: @@ -79,6 +79,18 @@ function normalizeIOInfo(config: DebugInfoConfig, ioInfo) {
         status: promise.status,
       };
     }
+  } else if ('value' in ioInfo) {
+    // If value exists in ioInfo but is undefined (e.g., WeakRef was GC'd),
+    // ensure we still include it in the normalized output for consistency
+    copy.value = {
+      value: undefined,
+    };
+  } else if (ioInfo.name && ioInfo.name !== 'rsc stream') {
+    // For non-rsc-stream IO that doesn't have a value field, add a default.
+    // This handles the case where the server doesn't send the field when WeakRef is GC'd.
+    copy.value = {
+      value: undefined,
+    };
   }
   return copy;
 }

filename: scripts/jest/setupTests.js
patch: @@ -319,3 +319,14 @@ jest.mock('async_hooks', () => {
     },
   };
 });
+
+// Ensure async hooks are disabled after each test to prevent cross-test pollution.
+// This is needed because test files that load the Node server (with async debug hooks)
+// can pollute test files that load the Edge server (which doesn't create new hooks
+// to trigger the cleanup in the mock above).
+afterEach(() => {
+  if (installedHook) {
+    installedHook.disable();
+    installedHook = null;
+  }
+});