@complexthings/superpowers-agent 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/.agents/docs/SUPERPOWERS.md +108 -0
  2. package/.agents/prompts/001-copilot-tool-mapping-do/001-copilot-tool-mapping-do.md +116 -0
  3. package/.agents/prompts/001-copilot-tool-mapping-do/SUMMARY.md +72 -0
  4. package/.agents/prompts/001-copilot-tool-mapping-do/copilot-tool-mapping-do-output.md +83 -0
  5. package/.agents/prompts/001-release-management-do/001-release-management-do.md +161 -0
  6. package/.agents/prompts/001-release-management-do/SUMMARY.md +19 -0
  7. package/.agents/prompts/001-release-management-do/completed/001-release-management-do.md +161 -0
  8. package/.agents/prompts/002-pull-command-do/SUMMARY.md +28 -0
  9. package/.agents/prompts/002-pull-command-do/completed/002-pull-command-do.md +122 -0
  10. package/.agents/prompts/002-pull-command-do/pull-command-do-output.md +174 -0
  11. package/.agents/prompts/command_updates_prompt.md +69 -0
  12. package/.agents/prompts/current_prompt.md +8 -0
  13. package/.agents/prompts/refactor.prompt.md +57 -0
  14. package/.agents/skills/.gitkeep +0 -0
  15. package/.agents/superpowers-agent +355 -0
  16. package/.agents/superpowers-bootstrap.md +42 -0
  17. package/.agents/templates/AGENTS.md.template +40 -0
  18. package/.agents/templates/SUPERPOWERS.md.template +108 -0
  19. package/.agents/templates/TOOLS.md.template +15 -0
  20. package/.github/workflows/main.yaml +32 -0
  21. package/AGENTS.md +60 -0
  22. package/LICENSE +21 -0
  23. package/README.md +724 -0
  24. package/hooks/hooks.json +15 -0
  25. package/hooks/session-start.sh +27 -0
  26. package/package.json +51 -0
  27. package/skills/architecture/ABOUT.md +20 -0
  28. package/skills/architecture/preserving-productive-tensions/SKILL.md +146 -0
  29. package/skills/architecture/preserving-productive-tensions/skill.json +9 -0
  30. package/skills/collaboration/brainstorming/SKILL.md +166 -0
  31. package/skills/collaboration/brainstorming/skill.json +9 -0
  32. package/skills/collaboration/brainstorming/spec-document-reviewer-prompt.md +50 -0
  33. package/skills/collaboration/brainstorming/visual-companion.md +277 -0
  34. package/skills/collaboration/dispatching-parallel-agents/SKILL.md +174 -0
  35. package/skills/collaboration/dispatching-parallel-agents/skill.json +9 -0
  36. package/skills/collaboration/executing-plans/SKILL.md +130 -0
  37. package/skills/collaboration/executing-plans/skill.json +9 -0
  38. package/skills/collaboration/finishing-a-development-branch/SKILL.md +261 -0
  39. package/skills/collaboration/finishing-a-development-branch/skill.json +9 -0
  40. package/skills/collaboration/leveraging-cli-tools/SKILL.md +132 -0
  41. package/skills/collaboration/leveraging-cli-tools/skill.json +9 -0
  42. package/skills/collaboration/receiving-code-review/SKILL.md +233 -0
  43. package/skills/collaboration/receiving-code-review/skill.json +9 -0
  44. package/skills/collaboration/requesting-code-review/SKILL.md +110 -0
  45. package/skills/collaboration/requesting-code-review/code-reviewer.md +146 -0
  46. package/skills/collaboration/requesting-code-review/skill.json +12 -0
  47. package/skills/collaboration/subagent-driven-development/SKILL.md +255 -0
  48. package/skills/collaboration/subagent-driven-development/code-quality-reviewer-prompt.md +26 -0
  49. package/skills/collaboration/subagent-driven-development/implementer-prompt.md +113 -0
  50. package/skills/collaboration/subagent-driven-development/skill.json +15 -0
  51. package/skills/collaboration/subagent-driven-development/spec-reviewer-prompt.md +61 -0
  52. package/skills/collaboration/using-git-worktrees/SKILL.md +366 -0
  53. package/skills/collaboration/using-git-worktrees/skill.json +9 -0
  54. package/skills/collaboration/writing-plans/SKILL.md +121 -0
  55. package/skills/collaboration/writing-plans/plan-document-reviewer-prompt.md +52 -0
  56. package/skills/collaboration/writing-plans/skill.json +9 -0
  57. package/skills/debugging/defense-in-depth/SKILL.md +380 -0
  58. package/skills/debugging/defense-in-depth/skill.json +9 -0
  59. package/skills/debugging/root-cause-tracing/SKILL.md +361 -0
  60. package/skills/debugging/root-cause-tracing/find-polluter.sh +63 -0
  61. package/skills/debugging/root-cause-tracing/skill.json +12 -0
  62. package/skills/debugging/systematic-debugging/SKILL.md +299 -0
  63. package/skills/debugging/systematic-debugging/condition-based-waiting-example.ts +158 -0
  64. package/skills/debugging/systematic-debugging/condition-based-waiting.md +115 -0
  65. package/skills/debugging/systematic-debugging/defense-in-depth.md +122 -0
  66. package/skills/debugging/systematic-debugging/find-polluter.sh +63 -0
  67. package/skills/debugging/systematic-debugging/root-cause-tracing.md +169 -0
  68. package/skills/debugging/systematic-debugging/skill.json +9 -0
  69. package/skills/debugging/systematic-debugging/test-academic.md +14 -0
  70. package/skills/debugging/systematic-debugging/test-pressure-1.md +58 -0
  71. package/skills/debugging/systematic-debugging/test-pressure-2.md +68 -0
  72. package/skills/debugging/systematic-debugging/test-pressure-3.md +69 -0
  73. package/skills/debugging/verification-before-completion/SKILL.md +143 -0
  74. package/skills/debugging/verification-before-completion/skill.json +9 -0
  75. package/skills/finding-skills/SKILL.md +101 -0
  76. package/skills/finding-skills/skill.json +8 -0
  77. package/skills/meta/create-skill-json/SKILL.md +370 -0
  78. package/skills/meta/create-skill-json/skill.json +9 -0
  79. package/skills/meta/create-skill-json/test-scenarios.md +29 -0
  80. package/skills/meta/creating-prompts/SKILL.md +349 -0
  81. package/skills/meta/creating-prompts/examples/do-example.md +65 -0
  82. package/skills/meta/creating-prompts/examples/plan-example.md +75 -0
  83. package/skills/meta/creating-prompts/examples/refine-example.md +65 -0
  84. package/skills/meta/creating-prompts/examples/research-example.md +63 -0
  85. package/skills/meta/creating-prompts/scripts/get-next-number.sh +27 -0
  86. package/skills/meta/creating-prompts/skill.json +20 -0
  87. package/skills/meta/creating-prompts/templates/do-template.md +59 -0
  88. package/skills/meta/creating-prompts/templates/plan-template.md +58 -0
  89. package/skills/meta/creating-prompts/templates/refine-template.md +54 -0
  90. package/skills/meta/creating-prompts/templates/research-template.md +56 -0
  91. package/skills/meta/using-superpowers/SKILL.md +122 -0
  92. package/skills/meta/using-superpowers/skill.json +5 -0
  93. package/skills/meta/writing-prompts/SKILL.md +345 -0
  94. package/skills/meta/writing-prompts/skill.json +9 -0
  95. package/skills/problem-solving/ABOUT.md +40 -0
  96. package/skills/problem-solving/collision-zone-thinking/SKILL.md +188 -0
  97. package/skills/problem-solving/collision-zone-thinking/references/historical-examples.md +393 -0
  98. package/skills/problem-solving/collision-zone-thinking/skill.json +9 -0
  99. package/skills/problem-solving/inversion-exercise/SKILL.md +174 -0
  100. package/skills/problem-solving/inversion-exercise/skill.json +9 -0
  101. package/skills/problem-solving/meta-pattern-recognition/SKILL.md +116 -0
  102. package/skills/problem-solving/meta-pattern-recognition/skill.json +9 -0
  103. package/skills/problem-solving/scale-game/SKILL.md +222 -0
  104. package/skills/problem-solving/scale-game/skill.json +9 -0
  105. package/skills/problem-solving/simplification-cascades/SKILL.md +113 -0
  106. package/skills/problem-solving/simplification-cascades/skill.json +9 -0
  107. package/skills/problem-solving/when-stuck/SKILL.md +69 -0
  108. package/skills/problem-solving/when-stuck/skill.json +9 -0
  109. package/skills/research/ABOUT.md +20 -0
  110. package/skills/research/tracing-knowledge-lineages/SKILL.md +241 -0
  111. package/skills/research/tracing-knowledge-lineages/skill.json +9 -0
  112. package/skills/setup-skills/SKILL.md +47 -0
  113. package/skills/testing/condition-based-waiting/SKILL.md +359 -0
  114. package/skills/testing/condition-based-waiting/example.ts +158 -0
  115. package/skills/testing/condition-based-waiting/skill.json +12 -0
  116. package/skills/testing/test-driven-development/SKILL.md +434 -0
  117. package/skills/testing/test-driven-development/skill.json +9 -0
  118. package/skills/testing/testing-anti-patterns/SKILL.md +298 -0
  119. package/skills/testing/testing-anti-patterns/skill.json +9 -0
  120. package/skills/testing/verification-before-completion/SKILL.md +246 -0
  121. package/skills/testing/verification-before-completion/skill.json +10 -0
  122. package/skills/using-a-skill/SKILL.md +101 -0
  123. package/skills/using-a-skill/skill.json +8 -0
@@ -0,0 +1,359 @@
1
+ ---
2
+ name: condition-based-waiting
3
+ description: Replace arbitrary timeouts with condition polling for reliable async tests. Use this skill whenever you see setTimeout/sleep/time.sleep in tests, tests are flaky or inconsistent across machines or CI, tests timeout under parallel load, or you're waiting for async operations like events, state changes, DOM updates, file writes, or API responses. Also use when debugging race conditions, investigating "why does this test sometimes fail", or looking for test speed improvements by removing unnecessary waits.
4
+ metadata:
5
+ version: 2.1.0
6
+ languages: all
7
+ ---
8
+
9
+ # Condition-Based Waiting
10
+
11
+ ## Overview
12
+
13
+ Flaky tests often guess at timing with arbitrary delays. This creates race conditions where tests pass on a fast dev machine but fail under load or in CI.
14
+
15
+ **Core principle:** Wait for the actual condition you care about, not a guess about how long it takes.
16
+
17
+ ## When to Use
18
+
19
+ ```
20
+ Test uses setTimeout/sleep?
21
+ └─ Testing actual timing behavior? (debounce, throttle, tick intervals)
22
+ ├─ YES → Document WHY the timeout is needed (see "When Arbitrary Timeout IS Correct")
23
+ └─ NO → Replace with condition-based waiting
24
+ └─ System emits events? → Prefer event-based waiting (fastest, no polling)
25
+ Otherwise → Use condition polling
26
+ ```
27
+
28
+ **Use when:**
29
+ - Tests have arbitrary delays (`setTimeout`, `sleep`, `time.sleep()`)
30
+ - Tests are flaky (pass sometimes, fail under load or in CI)
31
+ - Tests timeout when run in parallel
32
+ - Waiting for: async operations, DOM updates, state changes, file writes, queue drains
33
+
34
+ **Don't use when:**
35
+ - Testing actual timing behavior (debounce delay, throttle interval, tick frequency)
36
+ - Your test framework already has built-in auto-waiting (Playwright, Cypress) — use those instead
37
+ - Always document WHY when keeping an arbitrary timeout
38
+
39
+ ## Decision: Poll vs Event
40
+
41
+ | Approach | Use when | Latency | CPU |
42
+ |----------|----------|---------|-----|
43
+ | Event listener | System emits events/promises | ~0ms | Zero |
44
+ | Framework built-ins | Playwright, Cypress, Testing Library | ~0ms | Zero |
45
+ | Polling | System has no observable events | 0–50ms | Low |
46
+
47
+ **Event-based (zero latency, preferred when possible):**
48
+ ```typescript
49
+ // ✅ If system emits events, listen instead of polling
50
+ async function waitForEvent<T>(emitter: EventEmitter, event: string, timeoutMs = 5000): Promise<T> {
51
+ return new Promise((resolve, reject) => {
52
+ const timer = setTimeout(() => {
53
+ emitter.removeListener(event, resolve);
54
+ reject(new Error(`Timeout waiting for "${event}" event after ${timeoutMs}ms`));
55
+ }, timeoutMs);
56
+ emitter.once(event, (data: T) => { clearTimeout(timer); resolve(data); });
57
+ });
58
+ }
59
+ ```
60
+
61
+ **Polling (when no events available):**
62
+ ```typescript
63
+ // ✅ Poll at 50ms — responsive without hammering the CPU
64
+ await waitFor(() => system.isReady(), 'system to be ready');
65
+ ```
66
+
67
+ ## Core Pattern
68
+
69
+ ```typescript
70
+ // ❌ BEFORE: Guessing at timing
71
+ await new Promise(r => setTimeout(r, 300));
72
+ const result = getResult();
73
+ expect(result).toBeDefined();
74
+
75
+ // ✅ AFTER: Waiting for condition
76
+ await waitFor(() => getResult() !== undefined, 'result to be defined');
77
+ const result = getResult();
78
+ expect(result).toBeDefined();
79
+ ```
80
+
81
+ ## Quick Patterns
82
+
83
+ | Scenario | Pattern |
84
+ |----------|---------|
85
+ | Wait for event | `waitFor(() => events.find(e => e.type === 'DONE'), 'DONE event')` |
86
+ | Wait for state | `waitFor(() => machine.state === 'ready' && machine.state, 'ready state')` |
87
+ | Wait for count | `waitFor(() => items.length >= 5 && items, '5+ items')` |
88
+ | Wait for file | `waitFor(() => fs.existsSync(path) && path, 'file to exist')` |
89
+ | Wait for disappearance | `waitFor(() => !getElement() || true, 'element removed')` |
90
+ | Complex condition | `waitFor(() => obj.ready && obj.value > 10 && obj, 'obj ready')` |
91
+
92
+ > **Note:** The condition must return a truthy value to resolve — returning `false`, `null`, `undefined`, or `0` triggers another poll.
93
+
94
+ ## Generic Implementation (TypeScript)
95
+
96
+ ```typescript
97
+ async function waitFor<T>(
98
+ condition: () => T | undefined | null | false,
99
+ description: string,
100
+ timeoutMs = 5000,
101
+ intervalMs = 50 // 50ms matches Testing Library's default — fast enough, CPU-friendly
102
+ ): Promise<T> {
103
+ const startTime = Date.now();
104
+
105
+ while (true) {
106
+ const result = condition();
107
+ if (result) return result as T;
108
+
109
+ const elapsed = Date.now() - startTime;
110
+ if (elapsed > timeoutMs) {
111
+ // Include current state in error — saves a debugging round-trip
112
+ const current = (() => { try { return condition(); } catch { return 'error evaluating'; } })();
113
+ throw new Error(
114
+ `Timeout waiting for "${description}" after ${timeoutMs}ms. Current value: ${JSON.stringify(current)}`
115
+ );
116
+ }
117
+
118
+ await new Promise(r => setTimeout(r, intervalMs));
119
+ }
120
+ }
121
+ ```
122
+
123
+ See `example.ts` for domain-specific helper implementations (`waitForEvent`, `waitForEventCount`, `waitForEventMatch`).
124
+
125
+ ## Throw-Based vs Falsy-Return Retry
126
+
127
+ Two valid retry styles — choose based on context:
128
+
129
+ **Falsy-return** (above implementation — simple boolean conditions):
130
+ ```typescript
131
+ // Retries while condition returns falsy
132
+ await waitFor(() => queue.length > 0 && queue, 'queue to have items');
133
+ ```
134
+
135
+ **Throw-based** (Testing Library style — assertions as conditions):
136
+ ```typescript
137
+ // Retries while callback throws; stops when it doesn't throw
138
+ await waitFor(() => {
139
+ expect(getItems()).toHaveLength(3); // throws until length === 3
140
+ });
141
+ ```
142
+
143
+ Use throw-based when wrapping `expect()` assertions. Use falsy-return for plain boolean/value conditions. Don't mix the two styles in the same `waitFor` call.
144
+
145
+ ## ⚠️ Fake Timer Incompatibility (Critical Pitfall)
146
+
147
+ The `setTimeout(r, interval)` inside `waitFor` **never fires** when Jest/Vitest fake timers are active:
148
+
149
+ ```typescript
150
+ // ❌ This hangs forever — fake timers swallow the setTimeout inside waitFor
151
+ vi.useFakeTimers();
152
+ await waitFor(() => store.isReady(), 'store ready'); // deadlock
153
+ ```
154
+
155
+ **Fix option 1 — Flush pending timers before restoring real timers (recommended):**
156
+ ```typescript
157
+ afterEach(() => {
158
+ jest.runOnlyPendingTimers(); // flush 3rd-party timers before switching
159
+ jest.useRealTimers();
160
+ });
161
+ ```
162
+
163
+ **Fix option 2 — Advance fake timers to cover polling intervals:**
164
+ ```typescript
165
+ vi.useFakeTimers();
166
+ const waiting = waitFor(() => store.isReady(), 'store ready');
167
+ vi.advanceTimersByTime(5000); // advance past all polling intervals
168
+ await waiting;
169
+ ```
170
+
171
+ **Fix option 3 — Switch to real timers for the wait:**
172
+ ```typescript
173
+ vi.useFakeTimers();
174
+ // ... trigger work with fake timers ...
175
+ vi.useRealTimers();
176
+ await waitFor(() => store.isReady(), 'store ready');
177
+ vi.useFakeTimers(); // restore if needed
178
+ ```
179
+
180
+ **Fix option 4 — Skip `waitFor` entirely (when feasible):**
181
+ ```typescript
182
+ // If condition can be checked synchronously after advancing timers:
183
+ vi.useFakeTimers();
184
+ vi.runAllTimers(); // flush all pending fake timers
185
+ expect(store.isReady()).toBe(true); // direct check, no waitFor needed
186
+ ```
187
+
188
+ **Rule:** If your test uses fake timers, you cannot use `setTimeout`-based polling without explicitly advancing time.
189
+
190
+ ## Framework Built-Ins (Use These First)
191
+
192
+ If you're already using one of these frameworks, use their built-in waiting — don't reinvent the wheel:
193
+
194
+ ### Testing Library (`@testing-library/react`, etc.)
195
+
196
+ ```typescript
197
+ import { waitFor, waitForElementToBeRemoved, screen } from '@testing-library/react';
198
+
199
+ // waitFor: poll until assertion passes (retries when callback THROWS)
200
+ await waitFor(() => expect(screen.getByText('Done')).toBeVisible());
201
+ // Defaults: timeout=1000ms, interval=50ms
202
+
203
+ // findBy* = getBy* + waitFor (most common pattern)
204
+ const button = await screen.findByRole('button', { name: 'Submit' });
205
+ await screen.findByText('Loaded', {}, { timeout: 2000 });
206
+
207
+ // waitForElementToBeRemoved: wait for disappearance
208
+ await waitForElementToBeRemoved(() => screen.queryByText('Loading...'));
209
+ ```
210
+
211
+ **Critical:** Testing Library's `waitFor` retries when the callback **throws** — not when it returns falsy. Always use `expect(...)` assertions inside, not boolean returns.
212
+
213
+ ### Playwright (auto-waits on every action)
214
+
215
+ ```typescript
216
+ // ✅ Actions auto-wait (visible + stable + enabled + receives events):
217
+ await page.click('button#submit');
218
+ await page.fill('input[name=email]', 'test@example.com');
219
+
220
+ // ✅ Assertions auto-retry until timeout:
221
+ await expect(page.locator('.status')).toHaveText('Ready');
222
+ await expect(page.locator('.spinner')).toBeHidden();
223
+ await expect(page).toHaveURL('/dashboard');
224
+
225
+ // ✅ Explicit element state wait:
226
+ await page.locator('.spinner').waitFor({ state: 'hidden' });
227
+
228
+ // ✅ Custom condition evaluated in browser JS:
229
+ await page.waitForFunction(() => window.__appReady === true);
230
+ ```
231
+
232
+ Manual polling is rarely needed in Playwright — auto-wait handles the common cases.
233
+
234
+ ### Cypress (retry-ability built into query chains)
235
+
236
+ ```javascript
237
+ // ✅ .get() + .should() retries the chain automatically
238
+ cy.get('.status').should('have.text', 'Ready');
239
+ cy.get('@mySpy').should('have.been.calledOnce');
240
+
241
+ // ✅ For explicit polling:
242
+ cy.waitUntil(() => cy.window().then(w => w.store?.initialized));
243
+ ```
244
+
245
+ ## Cross-Language Patterns
246
+
247
+ ### Python (pytest-asyncio)
248
+
249
+ ```python
250
+ import asyncio
251
+
252
+ async def wait_for_condition(condition_fn, description, timeout=5.0, interval=0.05):
253
+ """Poll until condition_fn() returns truthy or timeout expires."""
254
+ deadline = asyncio.get_event_loop().time() + timeout
255
+ while asyncio.get_event_loop().time() < deadline:
256
+ if condition_fn():
257
+ return
258
+ await asyncio.sleep(interval)
259
+ current = condition_fn()
260
+ raise TimeoutError(
261
+ f'Timeout waiting for "{description}" after {timeout}s. Current: {current!r}'
262
+ )
263
+
264
+ @pytest.mark.asyncio
265
+ async def test_queue_drains():
266
+ items = list(range(10))
267
+ asyncio.get_event_loop().call_later(0.1, items.clear)
268
+ await wait_for_condition(lambda: len(items) == 0, 'queue to drain')
269
+ assert items == []
270
+ ```
271
+
272
+ > **Pitfall:** Call the getter *inside* the loop — never cache state before entering the loop or you'll check a stale snapshot.
273
+
274
+ ### Go
275
+
276
+ ```go
277
+ // stdlib — good for goroutine synchronization
278
+ func TestWorkerFinishes(t *testing.T) {
279
+ done := make(chan struct{})
280
+ go func() { time.Sleep(50 * time.Millisecond); close(done) }()
281
+
282
+ select {
283
+ case <-done:
284
+ // success
285
+ case <-time.After(2 * time.Second):
286
+ t.Fatal("worker did not finish within 2s")
287
+ }
288
+ }
289
+
290
+ // testify — good for polling arbitrary conditions
291
+ require.Eventually(t,
292
+ func() bool { return cache.Has("users") },
293
+ 2*time.Second, // total timeout
294
+ 10*time.Millisecond, // poll interval
295
+ "cache never populated 'users' key",
296
+ )
297
+ ```
298
+
299
+ > **Pitfall (gomega/testify):** Pass a *getter function* — not a value. `Eventually(cache.Has("users"), ...)` snapshots once; `Eventually(func() bool { return cache.Has("users") }, ...)` re-evaluates live.
300
+
301
+ ### Rust (tokio)
302
+
303
+ ```rust
304
+ use std::sync::{Arc, Mutex};
305
+ use tokio::time::{sleep, timeout, Duration};
306
+
307
+ #[tokio::test]
308
+ async fn test_flag_set_by_background_task() {
309
+ let flag = Arc::new(Mutex::new(false));
310
+ let flag_clone = Arc::clone(&flag);
311
+ tokio::spawn(async move {
312
+ sleep(Duration::from_millis(100)).await;
313
+ *flag_clone.lock().unwrap() = true;
314
+ });
315
+
316
+ timeout(Duration::from_secs(2), async {
317
+ loop {
318
+ if *flag.lock().unwrap() { break; }
319
+ sleep(Duration::from_millis(50)).await; // use sleep, not yield_now
320
+ }
321
+ }).await.expect("flag was never set within 2s");
322
+ }
323
+ ```
324
+
325
+ > **Pitfall:** Use `sleep()`, not `yield_now()`, in test polling loops — `yield_now` starves other tasks on the single-threaded test runtime.
326
+
327
+ ## Common Mistakes
328
+
329
+ | Mistake | Fix |
330
+ |---------|-----|
331
+ | `setTimeout(check, 1)` — polls too fast | Use 50ms; sub-10ms wastes CPU with no benefit |
332
+ | No timeout — loops forever | Always include timeout with descriptive error |
333
+ | Check stale data — cached before loop | Call getter *inside* loop for fresh data |
334
+ | Vague timeout error — "condition not met" | Include current state: `Current value: ${JSON.stringify(val)}` |
335
+ | Returning falsy from Testing Library `waitFor` | Throw inside `waitFor` — use `expect()` assertions |
336
+ | `waitFor` under fake timers — hangs | See [Fake Timer Incompatibility] above |
337
+
338
+ ## When Arbitrary Timeout IS Correct
339
+
340
+ Some behavior is inherently time-based. The pattern: wait for condition first, then wait for timed behavior:
341
+
342
+ ```typescript
343
+ // Tool ticks every 100ms — need 2 ticks to verify partial output
344
+ await waitForEvent('TOOL_STARTED'); // 1. Wait for triggering condition
345
+ await new Promise(r => setTimeout(r, 200)); // 2. Wait for known timing behavior
346
+ // 200ms = 2 ticks at 100ms intervals — documented and justified
347
+ ```
348
+
349
+ **Requirements for a legitimate arbitrary timeout:**
350
+ 1. First wait for the triggering condition (don't skip this)
351
+ 2. Duration based on known timing (e.g., "this runs every 100ms") — not a guess
352
+ 3. Comment explains WHY the specific duration was chosen
353
+
354
+ ## Real-World Impact
355
+
356
+ From a debugging session replacing 15 arbitrary timeouts across 3 test files:
357
+ - Pass rate: **60% → 100%** (eliminated race conditions)
358
+ - Execution time: **40% faster** (no more waiting out unnecessary delays)
359
+ - Zero flaky failures in CI after the change
@@ -0,0 +1,158 @@
1
+ // Complete implementation of condition-based waiting utilities
2
+ // From: Lace test infrastructure improvements (2025-10-03)
3
+ // Context: Fixed 15 flaky tests by replacing arbitrary timeouts
4
+
5
+ import type { ThreadManager } from '~/threads/thread-manager';
6
+ import type { LaceEvent, LaceEventType } from '~/threads/types';
7
+
8
+ /**
9
+ * Wait for a specific event type to appear in thread
10
+ *
11
+ * @param threadManager - The thread manager to query
12
+ * @param threadId - Thread to check for events
13
+ * @param eventType - Type of event to wait for
14
+ * @param timeoutMs - Maximum time to wait (default 5000ms)
15
+ * @returns Promise resolving to the first matching event
16
+ *
17
+ * Example:
18
+ * await waitForEvent(threadManager, agentThreadId, 'TOOL_RESULT');
19
+ */
20
+ export function waitForEvent(
21
+ threadManager: ThreadManager,
22
+ threadId: string,
23
+ eventType: LaceEventType,
24
+ timeoutMs = 5000
25
+ ): Promise<LaceEvent> {
26
+ return new Promise((resolve, reject) => {
27
+ const startTime = Date.now();
28
+
29
+ const check = () => {
30
+ const events = threadManager.getEvents(threadId);
31
+ const event = events.find((e) => e.type === eventType);
32
+
33
+ if (event) {
34
+ resolve(event);
35
+ } else if (Date.now() - startTime > timeoutMs) {
36
+ reject(new Error(`Timeout waiting for ${eventType} event after ${timeoutMs}ms`));
37
+ } else {
38
+ setTimeout(check, 50); // Poll every 50ms — efficient without hammering CPU
39
+ }
40
+ };
41
+
42
+ check();
43
+ });
44
+ }
45
+
46
+ /**
47
+ * Wait for a specific number of events of a given type
48
+ *
49
+ * @param threadManager - The thread manager to query
50
+ * @param threadId - Thread to check for events
51
+ * @param eventType - Type of event to wait for
52
+ * @param count - Number of events to wait for
53
+ * @param timeoutMs - Maximum time to wait (default 5000ms)
54
+ * @returns Promise resolving to all matching events once count is reached
55
+ *
56
+ * Example:
57
+ * // Wait for 2 AGENT_MESSAGE events (initial response + continuation)
58
+ * await waitForEventCount(threadManager, agentThreadId, 'AGENT_MESSAGE', 2);
59
+ */
60
+ export function waitForEventCount(
61
+ threadManager: ThreadManager,
62
+ threadId: string,
63
+ eventType: LaceEventType,
64
+ count: number,
65
+ timeoutMs = 5000
66
+ ): Promise<LaceEvent[]> {
67
+ return new Promise((resolve, reject) => {
68
+ const startTime = Date.now();
69
+
70
+ const check = () => {
71
+ const events = threadManager.getEvents(threadId);
72
+ const matchingEvents = events.filter((e) => e.type === eventType);
73
+
74
+ if (matchingEvents.length >= count) {
75
+ resolve(matchingEvents);
76
+ } else if (Date.now() - startTime > timeoutMs) {
77
+ reject(
78
+ new Error(
79
+ `Timeout waiting for ${count} ${eventType} events after ${timeoutMs}ms (got ${matchingEvents.length})`
80
+ )
81
+ );
82
+ } else {
83
+ setTimeout(check, 50);
84
+ }
85
+ };
86
+
87
+ check();
88
+ });
89
+ }
90
+
91
+ /**
92
+ * Wait for an event matching a custom predicate
93
+ * Useful when you need to check event data, not just type
94
+ *
95
+ * @param threadManager - The thread manager to query
96
+ * @param threadId - Thread to check for events
97
+ * @param predicate - Function that returns true when event matches
98
+ * @param description - Human-readable description for error messages
99
+ * @param timeoutMs - Maximum time to wait (default 5000ms)
100
+ * @returns Promise resolving to the first matching event
101
+ *
102
+ * Example:
103
+ * // Wait for TOOL_RESULT with specific ID
104
+ * await waitForEventMatch(
105
+ * threadManager,
106
+ * agentThreadId,
107
+ * (e) => e.type === 'TOOL_RESULT' && e.data.id === 'call_123',
108
+ * 'TOOL_RESULT with id=call_123'
109
+ * );
110
+ */
111
+ export function waitForEventMatch(
112
+ threadManager: ThreadManager,
113
+ threadId: string,
114
+ predicate: (event: LaceEvent) => boolean,
115
+ description: string,
116
+ timeoutMs = 5000
117
+ ): Promise<LaceEvent> {
118
+ return new Promise((resolve, reject) => {
119
+ const startTime = Date.now();
120
+
121
+ const check = () => {
122
+ const events = threadManager.getEvents(threadId);
123
+ const event = events.find(predicate);
124
+
125
+ if (event) {
126
+ resolve(event);
127
+ } else if (Date.now() - startTime > timeoutMs) {
128
+ reject(new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`));
129
+ } else {
130
+ setTimeout(check, 50);
131
+ }
132
+ };
133
+
134
+ check();
135
+ });
136
+ }
137
+
138
+ // Usage example from actual debugging session:
139
+ //
140
+ // BEFORE (flaky):
141
+ // ---------------
142
+ // const messagePromise = agent.sendMessage('Execute tools');
143
+ // await new Promise(r => setTimeout(r, 300)); // Hope tools start in 300ms
144
+ // agent.abort();
145
+ // await messagePromise;
146
+ // await new Promise(r => setTimeout(r, 50)); // Hope results arrive in 50ms
147
+ // expect(toolResults.length).toBe(2); // Fails randomly
148
+ //
149
+ // AFTER (reliable):
150
+ // ----------------
151
+ // const messagePromise = agent.sendMessage('Execute tools');
152
+ // await waitForEventCount(threadManager, threadId, 'TOOL_CALL', 2); // Wait for tools to start
153
+ // agent.abort();
154
+ // await messagePromise;
155
+ // await waitForEventCount(threadManager, threadId, 'TOOL_RESULT', 2); // Wait for results
156
+ // expect(toolResults.length).toBe(2); // Always succeeds
157
+ //
158
+ // Result: 60% pass rate → 100%, 40% faster execution
@@ -0,0 +1,12 @@
1
+ {
2
+ "version": "2.1.0",
3
+ "name": "superpowers:testing/condition-based-waiting",
4
+ "title": "Condition-Based Waiting",
5
+ "helpers": [
6
+ "example.ts"
7
+ ],
8
+ "aliases": [
9
+ "condition-based-waiting",
10
+ "testing/condition-based-waiting"
11
+ ]
12
+ }