
Condition Based Waiting
- 36 installs
- 16 repo stars
- Updated November 20, 2025
- jackspace/claudeskillz
Replace arbitrary test timeouts and sleeps with condition polling to eliminate race conditions and flaky pass/fail behavior.
About
Fixes flaky tests by replacing arbitrary delays with polling for the actual condition under test. A developer uses it when tests have race conditions or timing dependencies that pass locally but fail in CI.
- Waits for real state changes instead of guessed timeouts
- Includes a decision flow for when a timeout is genuinely needed
Condition Based Waiting by the numbers
- 36 all-time installs (skills.sh)
- Ranked #1,310 of 2,155 Testing & QA skills by installs in the Skillselion catalog
- Data as of Jul 30, 2026 (Skillselion catalog sync)
npx skills add https://github.com/jackspace/claudeskillz --skill condition-based-waitingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 36 |
|---|---|
| repo stars | ★ 16 |
| Last updated | November 20, 2025 |
| Repository | jackspace/claudeskillz ↗ |
What it does
Replace arbitrary test timeouts and sleeps with condition polling to eliminate race conditions and flaky pass/fail behavior.
Files
Condition-Based Waiting
Overview
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.
Core principle: Wait for the actual condition you care about, not a guess about how long it takes.
When to Use
digraph when_to_use {
"Test uses setTimeout/sleep?" [shape=diamond];
"Testing timing behavior?" [shape=diamond];
"Document WHY timeout needed" [shape=box];
"Use condition-based waiting" [shape=box];
"Test uses setTimeout/sleep?" -> "Testing timing behavior?" [label="yes"];
"Testing timing behavior?" -> "Document WHY timeout needed" [label="yes"];
"Testing timing behavior?" -> "Use condition-based waiting" [label="no"];
}Use when:
- Tests have arbitrary delays (
setTimeout,sleep,time.sleep()) - Tests are flaky (pass sometimes, fail under load)
- Tests timeout when run in parallel
- Waiting for async operations to complete
Don't use when:
- Testing actual timing behavior (debounce, throttle intervals)
- Always document WHY if using arbitrary timeout
Core Pattern
// ❌ BEFORE: Guessing at timing
await new Promise(r => setTimeout(r, 50));
const result = getResult();
expect(result).toBeDefined();
// ✅ AFTER: Waiting for condition
await waitFor(() => getResult() !== undefined);
const result = getResult();
expect(result).toBeDefined();Quick Patterns
| Scenario | Pattern |
|---|---|
| Wait for event | waitFor(() => events.find(e => e.type === 'DONE')) |
| Wait for state | waitFor(() => machine.state === 'ready') |
| Wait for count | waitFor(() => items.length >= 5) |
| Wait for file | waitFor(() => fs.existsSync(path)) |
| Complex condition | waitFor(() => obj.ready && obj.value > 10) |
Implementation
Generic polling function:
async function waitFor<T>(
condition: () => T | undefined | null | false,
description: string,
timeoutMs = 5000
): Promise<T> {
const startTime = Date.now();
while (true) {
const result = condition();
if (result) return result;
if (Date.now() - startTime > timeoutMs) {
throw new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`);
}
await new Promise(r => setTimeout(r, 10)); // Poll every 10ms
}
}See @example.ts for complete implementation with domain-specific helpers (waitForEvent, waitForEventCount, waitForEventMatch) from actual debugging session.
Common Mistakes
❌ Polling too fast: setTimeout(check, 1) - wastes CPU ✅ Fix: Poll every 10ms
❌ No timeout: Loop forever if condition never met ✅ Fix: Always include timeout with clear error
❌ Stale data: Cache state before loop ✅ Fix: Call getter inside loop for fresh data
When Arbitrary Timeout IS Correct
// Tool ticks every 100ms - need 2 ticks to verify partial output
await waitForEvent(manager, 'TOOL_STARTED'); // First: wait for condition
await new Promise(r => setTimeout(r, 200)); // Then: wait for timed behavior
// 200ms = 2 ticks at 100ms intervals - documented and justifiedRequirements: 1. First wait for triggering condition 2. Based on known timing (not guessing) 3. Comment explaining WHY
Real-World Impact
From debugging session (2025-10-03):
- Fixed 15 flaky tests across 3 files
- Pass rate: 60% → 100%
- Execution time: 40% faster
- No more race conditions
// Complete implementation of condition-based waiting utilities
// From: Lace test infrastructure improvements (2025-10-03)
// Context: Fixed 15 flaky tests by replacing arbitrary timeouts
import type { ThreadManager } from '~/threads/thread-manager';
import type { LaceEvent, LaceEventType } from '~/threads/types';
/**
* Wait for a specific event type to appear in thread
*
* @param threadManager - The thread manager to query
* @param threadId - Thread to check for events
* @param eventType - Type of event to wait for
* @param timeoutMs - Maximum time to wait (default 5000ms)
* @returns Promise resolving to the first matching event
*
* Example:
* await waitForEvent(threadManager, agentThreadId, 'TOOL_RESULT');
*/
export function waitForEvent(
threadManager: ThreadManager,
threadId: string,
eventType: LaceEventType,
timeoutMs = 5000
): Promise<LaceEvent> {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const check = () => {
const events = threadManager.getEvents(threadId);
const event = events.find((e) => e.type === eventType);
if (event) {
resolve(event);
} else if (Date.now() - startTime > timeoutMs) {
reject(new Error(`Timeout waiting for ${eventType} event after ${timeoutMs}ms`));
} else {
setTimeout(check, 10); // Poll every 10ms for efficiency
}
};
check();
});
}
/**
* Wait for a specific number of events of a given type
*
* @param threadManager - The thread manager to query
* @param threadId - Thread to check for events
* @param eventType - Type of event to wait for
* @param count - Number of events to wait for
* @param timeoutMs - Maximum time to wait (default 5000ms)
* @returns Promise resolving to all matching events once count is reached
*
* Example:
* // Wait for 2 AGENT_MESSAGE events (initial response + continuation)
* await waitForEventCount(threadManager, agentThreadId, 'AGENT_MESSAGE', 2);
*/
export function waitForEventCount(
threadManager: ThreadManager,
threadId: string,
eventType: LaceEventType,
count: number,
timeoutMs = 5000
): Promise<LaceEvent[]> {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const check = () => {
const events = threadManager.getEvents(threadId);
const matchingEvents = events.filter((e) => e.type === eventType);
if (matchingEvents.length >= count) {
resolve(matchingEvents);
} else if (Date.now() - startTime > timeoutMs) {
reject(
new Error(
`Timeout waiting for ${count} ${eventType} events after ${timeoutMs}ms (got ${matchingEvents.length})`
)
);
} else {
setTimeout(check, 10);
}
};
check();
});
}
/**
* Wait for an event matching a custom predicate
* Useful when you need to check event data, not just type
*
* @param threadManager - The thread manager to query
* @param threadId - Thread to check for events
* @param predicate - Function that returns true when event matches
* @param description - Human-readable description for error messages
* @param timeoutMs - Maximum time to wait (default 5000ms)
* @returns Promise resolving to the first matching event
*
* Example:
* // Wait for TOOL_RESULT with specific ID
* await waitForEventMatch(
* threadManager,
* agentThreadId,
* (e) => e.type === 'TOOL_RESULT' && e.data.id === 'call_123',
* 'TOOL_RESULT with id=call_123'
* );
*/
export function waitForEventMatch(
threadManager: ThreadManager,
threadId: string,
predicate: (event: LaceEvent) => boolean,
description: string,
timeoutMs = 5000
): Promise<LaceEvent> {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const check = () => {
const events = threadManager.getEvents(threadId);
const event = events.find(predicate);
if (event) {
resolve(event);
} else if (Date.now() - startTime > timeoutMs) {
reject(new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`));
} else {
setTimeout(check, 10);
}
};
check();
});
}
// Usage example from actual debugging session:
//
// BEFORE (flaky):
// ---------------
// const messagePromise = agent.sendMessage('Execute tools');
// await new Promise(r => setTimeout(r, 300)); // Hope tools start in 300ms
// agent.abort();
// await messagePromise;
// await new Promise(r => setTimeout(r, 50)); // Hope results arrive in 50ms
// expect(toolResults.length).toBe(2); // Fails randomly
//
// AFTER (reliable):
// ----------------
// const messagePromise = agent.sendMessage('Execute tools');
// await waitForEventCount(threadManager, threadId, 'TOOL_CALL', 2); // Wait for tools to start
// agent.abort();
// await messagePromise;
// await waitForEventCount(threadManager, threadId, 'TOOL_RESULT', 2); // Wait for results
// expect(toolResults.length).toBe(2); // Always succeeds
//
// Result: 60% pass rate → 100%, 40% faster execution
{
"sections": {
"When Arbitrary Timeout IS Correct": "```typescript\r\n// Tool ticks every 100ms - need 2 ticks to verify partial output\r\nawait waitForEvent(manager, 'TOOL_STARTED'); // First: wait for condition\r\nawait new Promise(r => setTimeout(r, 200)); // Then: wait for timed behavior\r\n// 200ms = 2 ticks at 100ms intervals - documented and justified\r\n```\r\n\r\n**Requirements:**\r\n1. First wait for triggering condition\r\n2. Based on known timing (not guessing)\r\n3. Comment explaining WHY",
"When to Use": "```dot\r\ndigraph when_to_use {\r\n \"Test uses setTimeout/sleep?\" [shape=diamond];\r\n \"Testing timing behavior?\" [shape=diamond];\r\n \"Document WHY timeout needed\" [shape=box];\r\n \"Use condition-based waiting\" [shape=box];\r\n\r\n \"Test uses setTimeout/sleep?\" -> \"Testing timing behavior?\" [label=\"yes\"];\r\n \"Testing timing behavior?\" -> \"Document WHY timeout needed\" [label=\"yes\"];\r\n \"Testing timing behavior?\" -> \"Use condition-based waiting\" [label=\"no\"];\r\n}\r\n```\r\n\r\n**Use when:**\r\n- Tests have arbitrary delays (`setTimeout`, `sleep`, `time.sleep()`)\r\n- Tests are flaky (pass sometimes, fail under load)\r\n- Tests timeout when run in parallel\r\n- Waiting for async operations to complete\r\n\r\n**Don't use when:**\r\n- Testing actual timing behavior (debounce, throttle intervals)\r\n- Always document WHY if using arbitrary timeout",
"Common Mistakes": "**❌ Polling too fast:** `setTimeout(check, 1)` - wastes CPU\r\n**✅ Fix:** Poll every 10ms\r\n\r\n**❌ No timeout:** Loop forever if condition never met\r\n**✅ Fix:** Always include timeout with clear error\r\n\r\n**❌ Stale data:** Cache state before loop\r\n**✅ Fix:** Call getter inside loop for fresh data",
"Overview": "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.\r\n\r\n**Core principle:** Wait for the actual condition you care about, not a guess about how long it takes.",
"Implementation": "Generic polling function:\r\n```typescript\r\nasync function waitFor<T>(\r\n condition: () => T | undefined | null | false,\r\n description: string,\r\n timeoutMs = 5000\r\n): Promise<T> {\r\n const startTime = Date.now();\r\n\r\n while (true) {\r\n const result = condition();\r\n if (result) return result;\r\n\r\n if (Date.now() - startTime > timeoutMs) {\r\n throw new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`);\r\n }\r\n\r\n await new Promise(r => setTimeout(r, 10)); // Poll every 10ms\r\n }\r\n}\r\n```\r\n\r\nSee @example.ts for complete implementation with domain-specific helpers (`waitForEvent`, `waitForEventCount`, `waitForEventMatch`) from actual debugging session.",
"Quick Patterns": "| Scenario | Pattern |\r\n|----------|---------|\r\n| Wait for event | `waitFor(() => events.find(e => e.type === 'DONE'))` |\r\n| Wait for state | `waitFor(() => machine.state === 'ready')` |\r\n| Wait for count | `waitFor(() => items.length >= 5)` |\r\n| Wait for file | `waitFor(() => fs.existsSync(path))` |\r\n| Complex condition | `waitFor(() => obj.ready && obj.value > 10)` |",
"Core Pattern": "```typescript\r\n// ❌ BEFORE: Guessing at timing\r\nawait new Promise(r => setTimeout(r, 50));\r\nconst result = getResult();\r\nexpect(result).toBeDefined();\r\n\r\n// ✅ AFTER: Waiting for condition\r\nawait waitFor(() => getResult() !== undefined);\r\nconst result = getResult();\r\nexpect(result).toBeDefined();\r\n```",
"Real-World Impact": "From debugging session (2025-10-03):\r\n- Fixed 15 flaky tests across 3 files\r\n- Pass rate: 60% → 100%\r\n- Execution time: 40% faster\r\n- No more race conditions"
},
"id": "condition-based-waiting_obra",
"name": "condition-based-waiting",
"description": "Use 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"
}---
name: condition-based-waiting
description: Use 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
---
# Condition-Based Waiting
## Overview
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.
**Core principle:** Wait for the actual condition you care about, not a guess about how long it takes.
## When to Use
```dot
digraph when_to_use {
"Test uses setTimeout/sleep?" [shape=diamond];
"Testing timing behavior?" [shape=diamond];
"Document WHY timeout needed" [shape=box];
"Use condition-based waiting" [shape=box];
"Test uses setTimeout/sleep?" -> "Testing timing behavior?" [label="yes"];
"Testing timing behavior?" -> "Document WHY timeout needed" [label="yes"];
"Testing timing behavior?" -> "Use condition-based waiting" [label="no"];
}
```
**Use when:**
- Tests have arbitrary delays (`setTimeout`, `sleep`, `time.sleep()`)
- Tests are flaky (pass sometimes, fail under load)
- Tests timeout when run in parallel
- Waiting for async operations to complete
**Don't use when:**
- Testing actual timing behavior (debounce, throttle intervals)
- Always document WHY if using arbitrary timeout
## Core Pattern
```typescript
// ❌ BEFORE: Guessing at timing
await new Promise(r => setTimeout(r, 50));
const result = getResult();
expect(result).toBeDefined();
// ✅ AFTER: Waiting for condition
await waitFor(() => getResult() !== undefined);
const result = getResult();
expect(result).toBeDefined();
```
## Quick Patterns
| Scenario | Pattern |
|----------|---------|
| Wait for event | `waitFor(() => events.find(e => e.type === 'DONE'))` |
| Wait for state | `waitFor(() => machine.state === 'ready')` |
| Wait for count | `waitFor(() => items.length >= 5)` |
| Wait for file | `waitFor(() => fs.existsSync(path))` |
| Complex condition | `waitFor(() => obj.ready && obj.value > 10)` |
## Implementation
Generic polling function:
```typescript
async function waitFor<T>(
condition: () => T | undefined | null | false,
description: string,
timeoutMs = 5000
): Promise<T> {
const startTime = Date.now();
while (true) {
const result = condition();
if (result) return result;
if (Date.now() - startTime > timeoutMs) {
throw new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`);
}
await new Promise(r => setTimeout(r, 10)); // Poll every 10ms
}
}
```
See @example.ts for complete implementation with domain-specific helpers (`waitForEvent`, `waitForEventCount`, `waitForEventMatch`) from actual debugging session.
## Common Mistakes
**❌ Polling too fast:** `setTimeout(check, 1)` - wastes CPU
**✅ Fix:** Poll every 10ms
**❌ No timeout:** Loop forever if condition never met
**✅ Fix:** Always include timeout with clear error
**❌ Stale data:** Cache state before loop
**✅ Fix:** Call getter inside loop for fresh data
## When Arbitrary Timeout IS Correct
```typescript
// Tool ticks every 100ms - need 2 ticks to verify partial output
await waitForEvent(manager, 'TOOL_STARTED'); // First: wait for condition
await new Promise(r => setTimeout(r, 200)); // Then: wait for timed behavior
// 200ms = 2 ticks at 100ms intervals - documented and justified
```
**Requirements:**
1. First wait for triggering condition
2. Based on known timing (not guessing)
3. Comment explaining WHY
## Real-World Impact
From debugging session (2025-10-03):
- Fixed 15 flaky tests across 3 files
- Pass rate: 60% → 100%
- Execution time: 40% faster
- No more race conditions