$npx -y skills add jackspace/ClaudeSkillz --skill condition-based-waiting_obraUse when tests have race conditions, timing dependencies, or inconsistent pass/fail behavior - replaces arbitrary timeouts with condition polling to wait for actual state changes, eliminating flaky tests from timing guesses
| 1 | # Condition-Based Waiting |
| 2 | |
| 3 | ## Overview |
| 4 | |
| 5 | Flaky tests often guess at timing with arbitrary delays. This creates race conditions where tests pass on fast machines but fail under load or in CI. |
| 6 | |
| 7 | **Core principle:** Wait for the actual condition you care about, not a guess about how long it takes. |
| 8 | |
| 9 | ## When to Use |
| 10 | |
| 11 | ```dot |
| 12 | digraph when_to_use { |
| 13 | "Test uses setTimeout/sleep?" [shape=diamond]; |
| 14 | "Testing timing behavior?" [shape=diamond]; |
| 15 | "Document WHY timeout needed" [shape=box]; |
| 16 | "Use condition-based waiting" [shape=box]; |
| 17 | |
| 18 | "Test uses setTimeout/sleep?" -> "Testing timing behavior?" [label="yes"]; |
| 19 | "Testing timing behavior?" -> "Document WHY timeout needed" [label="yes"]; |
| 20 | "Testing timing behavior?" -> "Use condition-based waiting" [label="no"]; |
| 21 | } |
| 22 | ``` |
| 23 | |
| 24 | **Use when:** |
| 25 | - Tests have arbitrary delays (`setTimeout`, `sleep`, `time.sleep()`) |
| 26 | - Tests are flaky (pass sometimes, fail under load) |
| 27 | - Tests timeout when run in parallel |
| 28 | - Waiting for async operations to complete |
| 29 | |
| 30 | **Don't use when:** |
| 31 | - Testing actual timing behavior (debounce, throttle intervals) |
| 32 | - Always document WHY if using arbitrary timeout |
| 33 | |
| 34 | ## Core Pattern |
| 35 | |
| 36 | ```typescript |
| 37 | // ❌ BEFORE: Guessing at timing |
| 38 | await new Promise(r => setTimeout(r, 50)); |
| 39 | const result = getResult(); |
| 40 | expect(result).toBeDefined(); |
| 41 | |
| 42 | // ✅ AFTER: Waiting for condition |
| 43 | await waitFor(() => getResult() !== undefined); |
| 44 | const result = getResult(); |
| 45 | expect(result).toBeDefined(); |
| 46 | ``` |
| 47 | |
| 48 | ## Quick Patterns |
| 49 | |
| 50 | | Scenario | Pattern | |
| 51 | |----------|---------| |
| 52 | | Wait for event | `waitFor(() => events.find(e => e.type === 'DONE'))` | |
| 53 | | Wait for state | `waitFor(() => machine.state === 'ready')` | |
| 54 | | Wait for count | `waitFor(() => items.length >= 5)` | |
| 55 | | Wait for file | `waitFor(() => fs.existsSync(path))` | |
| 56 | | Complex condition | `waitFor(() => obj.ready && obj.value > 10)` | |
| 57 | |
| 58 | ## Implementation |
| 59 | |
| 60 | Generic polling function: |
| 61 | ```typescript |
| 62 | async function waitFor<T>( |
| 63 | condition: () => T | undefined | null | false, |
| 64 | description: string, |
| 65 | timeoutMs = 5000 |
| 66 | ): Promise<T> { |
| 67 | const startTime = Date.now(); |
| 68 | |
| 69 | while (true) { |
| 70 | const result = condition(); |
| 71 | if (result) return result; |
| 72 | |
| 73 | if (Date.now() - startTime > timeoutMs) { |
| 74 | throw new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`); |
| 75 | } |
| 76 | |
| 77 | await new Promise(r => setTimeout(r, 10)); // Poll every 10ms |
| 78 | } |
| 79 | } |
| 80 | ``` |
| 81 | |
| 82 | See @example.ts for complete implementation with domain-specific helpers (`waitForEvent`, `waitForEventCount`, `waitForEventMatch`) from actual debugging session. |
| 83 | |
| 84 | ## Common Mistakes |
| 85 | |
| 86 | **❌ Polling too fast:** `setTimeout(check, 1)` - wastes CPU |
| 87 | **✅ Fix:** Poll every 10ms |
| 88 | |
| 89 | **❌ No timeout:** Loop forever if condition never met |
| 90 | **✅ Fix:** Always include timeout with clear error |
| 91 | |
| 92 | **❌ Stale data:** Cache state before loop |
| 93 | **✅ Fix:** Call getter inside loop for fresh data |
| 94 | |
| 95 | ## When Arbitrary Timeout IS Correct |
| 96 | |
| 97 | ```typescript |
| 98 | // Tool ticks every 100ms - need 2 ticks to verify partial output |
| 99 | await waitForEvent(manager, 'TOOL_STARTED'); // First: wait for condition |
| 100 | await new Promise(r => setTimeout(r, 200)); // Then: wait for timed behavior |
| 101 | // 200ms = 2 ticks at 100ms intervals - documented and justified |
| 102 | ``` |
| 103 | |
| 104 | **Requirements:** |
| 105 | 1. First wait for triggering condition |
| 106 | 2. Based on known timing (not guessing) |
| 107 | 3. Comment explaining WHY |
| 108 | |
| 109 | ## Real-World Impact |
| 110 | |
| 111 | From debugging session (2025-10-03): |
| 112 | - Fixed 15 flaky tests across 3 files |
| 113 | - Pass rate: 60% → 100% |
| 114 | - Execution time: 40% faster |
| 115 | - No more race conditions |