@bradtaylorsf/alpha-loop 1.0.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 (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +294 -0
  3. package/agents/implementer.md +30 -0
  4. package/agents/reviewer.md +29 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +57 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/commands/auth.d.ts +1 -0
  9. package/dist/commands/auth.js +89 -0
  10. package/dist/commands/auth.js.map +1 -0
  11. package/dist/commands/history.d.ts +8 -0
  12. package/dist/commands/history.js +185 -0
  13. package/dist/commands/history.js.map +1 -0
  14. package/dist/commands/init.d.ts +1 -0
  15. package/dist/commands/init.js +241 -0
  16. package/dist/commands/init.js.map +1 -0
  17. package/dist/commands/run.d.ts +15 -0
  18. package/dist/commands/run.js +321 -0
  19. package/dist/commands/run.js.map +1 -0
  20. package/dist/commands/scan.d.ts +1 -0
  21. package/dist/commands/scan.js +50 -0
  22. package/dist/commands/scan.js.map +1 -0
  23. package/dist/commands/sync.d.ts +20 -0
  24. package/dist/commands/sync.js +149 -0
  25. package/dist/commands/sync.js.map +1 -0
  26. package/dist/commands/vision.d.ts +1 -0
  27. package/dist/commands/vision.js +194 -0
  28. package/dist/commands/vision.js.map +1 -0
  29. package/dist/engine/agents.d.ts +41 -0
  30. package/dist/engine/agents.js +90 -0
  31. package/dist/engine/agents.js.map +1 -0
  32. package/dist/engine/config.d.ts +71 -0
  33. package/dist/engine/config.js +73 -0
  34. package/dist/engine/config.js.map +1 -0
  35. package/dist/engine/prerequisites.d.ts +34 -0
  36. package/dist/engine/prerequisites.js +90 -0
  37. package/dist/engine/prerequisites.js.map +1 -0
  38. package/dist/lib/agent.d.ts +25 -0
  39. package/dist/lib/agent.js +97 -0
  40. package/dist/lib/agent.js.map +1 -0
  41. package/dist/lib/config.d.ts +35 -0
  42. package/dist/lib/config.js +179 -0
  43. package/dist/lib/config.js.map +1 -0
  44. package/dist/lib/context.d.ts +17 -0
  45. package/dist/lib/context.js +96 -0
  46. package/dist/lib/context.js.map +1 -0
  47. package/dist/lib/github.d.ts +61 -0
  48. package/dist/lib/github.js +313 -0
  49. package/dist/lib/github.js.map +1 -0
  50. package/dist/lib/learning.d.ts +43 -0
  51. package/dist/lib/learning.js +207 -0
  52. package/dist/lib/learning.js.map +1 -0
  53. package/dist/lib/logger.d.ts +9 -0
  54. package/dist/lib/logger.js +28 -0
  55. package/dist/lib/logger.js.map +1 -0
  56. package/dist/lib/pipeline.d.ts +18 -0
  57. package/dist/lib/pipeline.js +456 -0
  58. package/dist/lib/pipeline.js.map +1 -0
  59. package/dist/lib/preflight.d.ts +33 -0
  60. package/dist/lib/preflight.js +123 -0
  61. package/dist/lib/preflight.js.map +1 -0
  62. package/dist/lib/prerequisites.d.ts +12 -0
  63. package/dist/lib/prerequisites.js +54 -0
  64. package/dist/lib/prerequisites.js.map +1 -0
  65. package/dist/lib/prompts.d.ts +44 -0
  66. package/dist/lib/prompts.js +102 -0
  67. package/dist/lib/prompts.js.map +1 -0
  68. package/dist/lib/session.d.ts +28 -0
  69. package/dist/lib/session.js +173 -0
  70. package/dist/lib/session.js.map +1 -0
  71. package/dist/lib/shell.d.ts +32 -0
  72. package/dist/lib/shell.js +95 -0
  73. package/dist/lib/shell.js.map +1 -0
  74. package/dist/lib/testing.d.ts +10 -0
  75. package/dist/lib/testing.js +51 -0
  76. package/dist/lib/testing.js.map +1 -0
  77. package/dist/lib/verify.d.ts +18 -0
  78. package/dist/lib/verify.js +235 -0
  79. package/dist/lib/verify.js.map +1 -0
  80. package/dist/lib/vision.d.ts +9 -0
  81. package/dist/lib/vision.js +21 -0
  82. package/dist/lib/vision.js.map +1 -0
  83. package/dist/lib/worktree.d.ts +29 -0
  84. package/dist/lib/worktree.js +153 -0
  85. package/dist/lib/worktree.js.map +1 -0
  86. package/package.json +63 -0
  87. package/templates/agents/implementer.md +34 -0
  88. package/templates/agents/reviewer.md +48 -0
  89. package/templates/skills/code-review/SKILL.md +58 -0
  90. package/templates/skills/git-workflow/SKILL.md +53 -0
  91. package/templates/skills/implementation-planning/SKILL.md +64 -0
  92. package/templates/skills/security-analysis/SKILL.md +560 -0
  93. package/templates/skills/security-analysis/scripts/security-scanner.sh +227 -0
  94. package/templates/skills/test-robustness/SKILL.md +897 -0
  95. package/templates/skills/testing-patterns/SKILL.md +75 -0
@@ -0,0 +1,897 @@
1
+ ---
2
+ name: test-robustness
3
+ description: Comprehensive patterns for writing robust non-brittle tests including eliminating waitForTimeout, mocking time properly, and generating unique test data. Use when writing any tests to ensure fast reliable maintainable test suites.
4
+ ---
5
+
6
+ # Test Robustness Skill
7
+
8
+ Comprehensive guide for writing robust, non-brittle tests that are fast, reliable, and maintainable.
9
+
10
+ ## Critical Anti-Patterns to AVOID
11
+
12
+ ### 1. ❌ Fixed Time Delays (`waitForTimeout`)
13
+
14
+ **NEVER use `page.waitForTimeout()` or arbitrary delays in tests.**
15
+
16
+ ```typescript
17
+ // ❌ BRITTLE - Do NOT do this
18
+ await page.click('[data-testid="submit"]');
19
+ await page.waitForTimeout(1000); // Flaky! May be too short or unnecessarily long
20
+ expect(page.locator('.success')).toBeVisible();
21
+
22
+ // ❌ BRITTLE - Another bad example
23
+ await page.fill('#username', 'test');
24
+ await page.waitForTimeout(500); // Why 500ms? What are we waiting for?
25
+ await page.click('#submit');
26
+
27
+ // ✅ ROBUST - Do this instead
28
+ await page.click('[data-testid="submit"]');
29
+ await page.waitForSelector('.success', { state: 'visible' });
30
+ expect(page.locator('.success')).toBeVisible();
31
+
32
+ // ✅ ROBUST - Wait for specific condition
33
+ await page.fill('#username', 'test');
34
+ await expect(page.locator('#submit')).toBeEnabled();
35
+ await page.click('#submit');
36
+ ```
37
+
38
+ **Why this is bad:**
39
+ - **Flaky**: May be too short on slow CI servers, causing random failures
40
+ - **Slow**: May be unnecessarily long on fast machines, wasting time
41
+ - **Unclear**: Doesn't express what you're actually waiting for
42
+ - **Brittle**: Breaks when timing changes (network, CPU, etc.)
43
+
44
+ **What to use instead:**
45
+ - `page.waitForSelector()` - Wait for element to appear
46
+ - `page.waitForResponse()` - Wait for specific network request
47
+ - `page.waitForNavigation()` - Wait for page navigation
48
+ - `expect().toBeVisible()` - Assert element is visible (auto-waits)
49
+ - `expect().toHaveText()` - Assert text content (auto-waits)
50
+
51
+ ---
52
+
53
+ ### 2. ❌ Real Time Delays in Tests (`setTimeout`)
54
+
55
+ **NEVER use real timers in tests that check time-dependent behavior.**
56
+
57
+ ```typescript
58
+ // ❌ BRITTLE - Do NOT do this (adds 1.1 seconds to test!)
59
+ it('timestamps should be recent', async () => {
60
+ const timestamp1 = Date.now();
61
+ setTimeout(() => {
62
+ const timestamp2 = Date.now();
63
+ expect(timestamp2).toBeGreaterThan(timestamp1);
64
+ }, 1100); // Slows test by 1.1 seconds!
65
+ });
66
+
67
+ // ✅ ROBUST - Mock time instead (instant!)
68
+ it('timestamps should be recent', async () => {
69
+ jest.useFakeTimers();
70
+ const timestamp1 = Date.now();
71
+
72
+ jest.advanceTimersByTime(1100);
73
+
74
+ const timestamp2 = Date.now();
75
+ expect(timestamp2).toBeGreaterThan(timestamp1);
76
+
77
+ jest.useRealTimers();
78
+ });
79
+ ```
80
+
81
+ **Why this is bad:**
82
+ - **Slow**: Tests take actual real-world time to run (6.6 seconds wasted in our codebase!)
83
+ - **Flaky**: Can fail on slow machines or under load
84
+ - **Unnecessary**: Time can be mocked to run instantly
85
+
86
+ **What to use instead:**
87
+ - `jest.useFakeTimers()` - Mock all timer functions
88
+ - `jest.advanceTimersByTime(ms)` - Move time forward instantly
89
+ - `jest.runAllTimers()` - Run all pending timers
90
+ - `jest.useRealTimers()` - Restore real timers after test
91
+
92
+ See: `tests/helpers/time-helpers.ts` for reusable utilities.
93
+
94
+ ---
95
+
96
+ ### 3. ❌ Hard-Coded Test Data
97
+
98
+ **NEVER use hard-coded IDs, names, or data that could conflict between tests.**
99
+
100
+ ```typescript
101
+ // ❌ BRITTLE - Hard-coded data causes conflicts
102
+ it('creates session', async () => {
103
+ await createSession('my-session'); // What if another test uses this name?
104
+ const session = await getSession('my-session');
105
+ expect(session.name).toBe('my-session');
106
+ });
107
+
108
+ // ❌ BRITTLE - Hard-coded IDs
109
+ it('deletes session', async () => {
110
+ await deleteSession('session-123'); // Assumes this ID exists!
111
+ });
112
+
113
+ // ✅ ROBUST - Use templates with unique suffixes
114
+ import { createUniqueSession } from '../helpers/seed-data';
115
+
116
+ it('creates session', async () => {
117
+ const sessionName = createUniqueSession('my-session'); // 'my-session-1671234567890'
118
+ await createSession(sessionName);
119
+ const session = await getSession(sessionName);
120
+ expect(session.name).toBe(sessionName);
121
+ });
122
+
123
+ // ✅ ROBUST - Create data in test setup
124
+ it('deletes session', async () => {
125
+ const session = await createSession(createUniqueSession('test'));
126
+ await deleteSession(session.id);
127
+ await expect(getSession(session.id)).rejects.toThrow();
128
+ });
129
+ ```
130
+
131
+ **Why this is bad:**
132
+ - **Test Interference**: Tests can conflict when run in parallel
133
+ - **Fragile**: Breaks if database is cleared or data changes
134
+ - **Hard to Debug**: Failures don't indicate what data was expected
135
+ - **Not Repeatable**: Tests may pass/fail depending on order
136
+
137
+ **What to use instead:**
138
+ - Template-based data with unique suffixes (timestamps, UUIDs)
139
+ - Test setup/teardown to create/destroy data
140
+ - Database transactions (rollback after test)
141
+ - In-memory databases for unit tests
142
+
143
+ See: `tests/helpers/seed-data.ts` for data generation utilities.
144
+
145
+ ---
146
+
147
+ ### 4. ❌ Inconsistent Uniqueness Strategies
148
+
149
+ **Use a CONSISTENT strategy for generating unique test data.**
150
+
151
+ ```typescript
152
+ // ❌ INCONSISTENT - Different strategies in different tests
153
+ it('test 1', async () => {
154
+ const name = `session-${Date.now()}`; // Uses timestamp
155
+ });
156
+
157
+ it('test 2', async () => {
158
+ const name = 'session-' + Math.random(); // Uses random number
159
+ });
160
+
161
+ it('test 3', async () => {
162
+ const name = 'session-test'; // No uniqueness at all!
163
+ });
164
+
165
+ // ✅ CONSISTENT - Use shared utility
166
+ import { uniqueSessionName } from '../helpers/seed-data';
167
+
168
+ it('test 1', async () => {
169
+ const name = uniqueSessionName('session'); // session-1671234567890
170
+ });
171
+
172
+ it('test 2', async () => {
173
+ const name = uniqueSessionName('session'); // session-1671234567891
174
+ });
175
+
176
+ it('test 3', async () => {
177
+ const name = uniqueSessionName('session'); // session-1671234567892
178
+ });
179
+ ```
180
+
181
+ **Why this is bad:**
182
+ - **Hard to Maintain**: Every test does it differently
183
+ - **Potential Collisions**: Different strategies may conflict
184
+ - **Unclear Intent**: Hard to understand the pattern
185
+ - **Duplication**: Same logic repeated everywhere
186
+
187
+ **What to use instead:**
188
+ - Single utility function: `uniqueSessionName(prefix)`
189
+ - Consistent format: `${prefix}-${timestamp}` or `${prefix}-${uuid}`
190
+ - Centralized in test helpers
191
+
192
+ ---
193
+
194
+ ## Correct Patterns to USE
195
+
196
+ ### ✅ Wait for Specific Elements
197
+
198
+ ```typescript
199
+ // Wait for element to appear
200
+ await page.waitForSelector('[data-testid="success-message"]', {
201
+ state: 'visible'
202
+ });
203
+
204
+ // Wait for element to disappear
205
+ await page.waitForSelector('[data-testid="loading-spinner"]', {
206
+ state: 'hidden'
207
+ });
208
+
209
+ // Wait for element to be enabled
210
+ await page.waitForSelector('[data-testid="submit-button"]:not([disabled])');
211
+
212
+ // Playwright assertions (auto-wait built-in)
213
+ await expect(page.locator('[data-testid="result"]')).toBeVisible();
214
+ await expect(page.locator('[data-testid="result"]')).toHaveText('Success');
215
+ await expect(page.locator('[data-testid="input"]')).toBeEnabled();
216
+ ```
217
+
218
+ ---
219
+
220
+ ### ✅ Wait for Network Activity
221
+
222
+ ```typescript
223
+ // Wait for specific API response
224
+ const responsePromise = page.waitForResponse(
225
+ response => response.url().includes('/api/session') && response.status() === 200
226
+ );
227
+
228
+ await page.click('[data-testid="create-session"]');
229
+ const response = await responsePromise;
230
+ const data = await response.json();
231
+
232
+ expect(data.id).toBeDefined();
233
+
234
+ // Wait for multiple requests to complete
235
+ await Promise.all([
236
+ page.waitForResponse('/api/session'),
237
+ page.waitForResponse('/api/features'),
238
+ page.click('[data-testid="load-data"]')
239
+ ]);
240
+
241
+ // Wait for navigation
242
+ await Promise.all([
243
+ page.waitForNavigation(),
244
+ page.click('[data-testid="logout"]')
245
+ ]);
246
+ ```
247
+
248
+ ---
249
+
250
+ ### ✅ Mock Time Properly
251
+
252
+ ```typescript
253
+ import { useFakeTimers, advanceTime, useRealTimers } from '../helpers/time-helpers';
254
+
255
+ it('session expires after 1 hour', async () => {
256
+ useFakeTimers(); // Mock Date.now(), setTimeout, setInterval
257
+
258
+ const session = await createSession('test');
259
+ expect(session.expiresAt).toBe(Date.now() + 3600000); // 1 hour
260
+
261
+ advanceTime(3600001); // Advance 1 hour + 1ms
262
+
263
+ const expired = await isSessionExpired(session.id);
264
+ expect(expired).toBe(true);
265
+
266
+ useRealTimers(); // Restore real timers
267
+ });
268
+
269
+ it('retries after 5 seconds', async () => {
270
+ useFakeTimers();
271
+
272
+ const retryPromise = retryOperation(); // Returns after 5s timeout
273
+
274
+ advanceTime(5000); // Instantly "wait" 5 seconds
275
+
276
+ await expect(retryPromise).resolves.toBe('success');
277
+
278
+ useRealTimers();
279
+ });
280
+ ```
281
+
282
+ ---
283
+
284
+ ### ✅ Generate Unique Test Data
285
+
286
+ ```typescript
287
+ // Use template-based seed data
288
+ import { seedSession, seedFeature, seedProject } from '../helpers/seed-data';
289
+
290
+ it('creates session with unique data', async () => {
291
+ // Use .alphacoder paths to avoid /tmp permission issues
292
+ const sessionData = seedSession({
293
+ projectPath: '.alphacoder/sessions/test-data/test-project'
294
+ });
295
+
296
+ // sessionData = {
297
+ // id: 'session-1671234567890',
298
+ // projectPath: '.alphacoder/sessions/test-data/test-project',
299
+ // features: [...],
300
+ // createdAt: 1671234567890
301
+ // }
302
+
303
+ const session = await createSession(sessionData);
304
+ expect(session.id).toBe(sessionData.id);
305
+ });
306
+
307
+ it('creates multiple sessions without conflicts', async () => {
308
+ const session1 = seedSession();
309
+ const session2 = seedSession();
310
+
311
+ expect(session1.id).not.toBe(session2.id); // Different IDs
312
+
313
+ await createSession(session1);
314
+ await createSession(session2); // No conflicts!
315
+ });
316
+ ```
317
+
318
+ ---
319
+
320
+ ## Helper Utilities
321
+
322
+ ### Wait Helpers (`tests/helpers/wait-helpers.ts`)
323
+
324
+ ```typescript
325
+ import { Page } from '@playwright/test';
326
+
327
+ /**
328
+ * Wait for element to be visible and enabled
329
+ */
330
+ export async function waitForInteractive(page: Page, selector: string) {
331
+ await page.waitForSelector(selector, { state: 'visible' });
332
+ await page.waitForSelector(`${selector}:not([disabled])`);
333
+ }
334
+
335
+ /**
336
+ * Wait for loading spinner to disappear
337
+ */
338
+ export async function waitForLoadingComplete(page: Page) {
339
+ await page.waitForSelector('[data-testid="loading"]', { state: 'hidden' });
340
+ }
341
+
342
+ /**
343
+ * Wait for API call to complete
344
+ */
345
+ export async function waitForApiResponse(page: Page, urlPattern: string) {
346
+ return page.waitForResponse(
347
+ response => response.url().includes(urlPattern) && response.ok()
348
+ );
349
+ }
350
+
351
+ /**
352
+ * Wait for navigation and page load
353
+ */
354
+ export async function waitForPageLoad(page: Page) {
355
+ await page.waitForLoadState('domcontentloaded');
356
+ await page.waitForLoadState('networkidle');
357
+ }
358
+ ```
359
+
360
+ ### Time Helpers (`tests/helpers/time-helpers.ts`)
361
+
362
+ ```typescript
363
+ /**
364
+ * Enable fake timers for testing time-dependent code
365
+ */
366
+ export function useFakeTimers() {
367
+ jest.useFakeTimers();
368
+ }
369
+
370
+ /**
371
+ * Advance time by milliseconds (instant)
372
+ */
373
+ export function advanceTime(ms: number) {
374
+ jest.advanceTimersByTime(ms);
375
+ }
376
+
377
+ /**
378
+ * Run all pending timers
379
+ */
380
+ export function runAllTimers() {
381
+ jest.runAllTimers();
382
+ }
383
+
384
+ /**
385
+ * Restore real timers
386
+ */
387
+ export function useRealTimers() {
388
+ jest.useRealTimers();
389
+ }
390
+
391
+ /**
392
+ * Mock Date.now() to return specific timestamp
393
+ */
394
+ export function mockNow(timestamp: number) {
395
+ jest.spyOn(Date, 'now').mockReturnValue(timestamp);
396
+ }
397
+
398
+ /**
399
+ * Restore original Date.now()
400
+ */
401
+ export function restoreNow() {
402
+ jest.spyOn(Date, 'now').mockRestore();
403
+ }
404
+ ```
405
+
406
+ ### Seed Data (`tests/helpers/seed-data.ts`)
407
+
408
+ ```typescript
409
+ let counter = 0;
410
+
411
+ /**
412
+ * Generate unique ID
413
+ */
414
+ export function uniqueId(prefix = 'test'): string {
415
+ return `${prefix}-${Date.now()}-${counter++}`;
416
+ }
417
+
418
+ /**
419
+ * Generate unique session name
420
+ */
421
+ export function uniqueSessionName(prefix = 'session'): string {
422
+ return uniqueId(prefix);
423
+ }
424
+
425
+ /**
426
+ * Create session seed data with defaults
427
+ * Uses .alphacoder paths to avoid /tmp permission issues
428
+ */
429
+ export function seedSession(overrides: Partial<SessionData> = {}): SessionData {
430
+ return {
431
+ id: uniqueId('session'),
432
+ projectPath: `.alphacoder/sessions/test-data/test-${uniqueId()}`,
433
+ features: [],
434
+ createdAt: Date.now(),
435
+ status: 'initializing',
436
+ ...overrides
437
+ };
438
+ }
439
+
440
+ /**
441
+ * Create feature seed data with defaults
442
+ */
443
+ export function seedFeature(overrides: Partial<FeatureData> = {}): FeatureData {
444
+ return {
445
+ id: uniqueId('feature'),
446
+ name: `Test Feature ${counter}`,
447
+ description: 'Test feature description',
448
+ status: 'pending',
449
+ ...overrides
450
+ };
451
+ }
452
+
453
+ /**
454
+ * Create project seed data with defaults
455
+ * Uses .alphacoder paths to avoid /tmp permission issues
456
+ */
457
+ export function seedProject(overrides: Partial<ProjectData> = {}): ProjectData {
458
+ return {
459
+ id: uniqueId('project'),
460
+ name: `Test Project ${counter}`,
461
+ path: `.alphacoder/sessions/test-data/project-${uniqueId()}`,
462
+ ...overrides
463
+ };
464
+ }
465
+ ```
466
+
467
+ ---
468
+
469
+ ## Before/After Examples
470
+
471
+ ### Example 1: Button Click and Response
472
+
473
+ **❌ BRITTLE**
474
+ ```typescript
475
+ it('shows success message', async () => {
476
+ await page.click('[data-testid="submit"]');
477
+ await page.waitForTimeout(1000); // Arbitrary delay!
478
+
479
+ const message = await page.textContent('.message');
480
+ expect(message).toBe('Success');
481
+ });
482
+ ```
483
+
484
+ **✅ ROBUST**
485
+ ```typescript
486
+ it('shows success message', async () => {
487
+ await page.click('[data-testid="submit"]');
488
+
489
+ // Wait for specific element
490
+ await page.waitForSelector('.message', { state: 'visible' });
491
+
492
+ // Or use auto-waiting assertion
493
+ await expect(page.locator('.message')).toHaveText('Success');
494
+ });
495
+ ```
496
+
497
+ ---
498
+
499
+ ### Example 2: Form Validation
500
+
501
+ **❌ BRITTLE**
502
+ ```typescript
503
+ it('validates email', async () => {
504
+ await page.fill('#email', 'invalid');
505
+ await page.click('#submit');
506
+ await page.waitForTimeout(500); // What are we waiting for?
507
+
508
+ expect(await page.textContent('.error')).toContain('Invalid email');
509
+ });
510
+ ```
511
+
512
+ **✅ ROBUST**
513
+ ```typescript
514
+ it('validates email', async () => {
515
+ await page.fill('#email', 'invalid');
516
+ await page.click('#submit');
517
+
518
+ // Wait for error message to appear
519
+ await expect(page.locator('.error')).toContainText('Invalid email');
520
+ });
521
+ ```
522
+
523
+ ---
524
+
525
+ ### Example 3: Time-Based Logic
526
+
527
+ **❌ BRITTLE**
528
+ ```typescript
529
+ it('session expires', async () => {
530
+ const session = await createSession('test');
531
+
532
+ // Wait actual 1.1 seconds!
533
+ await new Promise(resolve => setTimeout(resolve, 1100));
534
+
535
+ expect(session.expiresAt).toBeLessThan(Date.now());
536
+ });
537
+ ```
538
+
539
+ **✅ ROBUST**
540
+ ```typescript
541
+ it('session expires', async () => {
542
+ jest.useFakeTimers();
543
+
544
+ const session = await createSession('test');
545
+
546
+ // Instantly advance time
547
+ jest.advanceTimersByTime(1100);
548
+
549
+ expect(session.expiresAt).toBeLessThan(Date.now());
550
+
551
+ jest.useRealTimers();
552
+ });
553
+ ```
554
+
555
+ ---
556
+
557
+ ### Example 4: Test Data
558
+
559
+ **❌ BRITTLE**
560
+ ```typescript
561
+ it('creates session', async () => {
562
+ // Hard-coded name - conflicts with parallel tests!
563
+ await createSession('my-session');
564
+
565
+ const session = await getSession('my-session');
566
+ expect(session).toBeDefined();
567
+ });
568
+ ```
569
+
570
+ **✅ ROBUST**
571
+ ```typescript
572
+ it('creates session', async () => {
573
+ const sessionName = uniqueSessionName('my-session');
574
+ await createSession(sessionName);
575
+
576
+ const session = await getSession(sessionName);
577
+ expect(session).toBeDefined();
578
+ });
579
+ ```
580
+
581
+ ---
582
+
583
+ ### Example 5: Loading States
584
+
585
+ **❌ BRITTLE**
586
+ ```typescript
587
+ it('loads data', async () => {
588
+ await page.click('[data-testid="load"]');
589
+ await page.waitForTimeout(2000); // Hope data loads by then!
590
+
591
+ expect(await page.locator('.data').count()).toBeGreaterThan(0);
592
+ });
593
+ ```
594
+
595
+ **✅ ROBUST**
596
+ ```typescript
597
+ it('loads data', async () => {
598
+ const responsePromise = page.waitForResponse('/api/data');
599
+
600
+ await page.click('[data-testid="load"]');
601
+ await responsePromise;
602
+
603
+ // Wait for UI to update
604
+ await expect(page.locator('.data').first()).toBeVisible();
605
+ expect(await page.locator('.data').count()).toBeGreaterThan(0);
606
+ });
607
+ ```
608
+
609
+ ---
610
+
611
+ ### Example 6: Multi-Step Workflows
612
+
613
+ **❌ BRITTLE**
614
+ ```typescript
615
+ it('completes workflow', async () => {
616
+ await page.click('[data-testid="step1"]');
617
+ await page.waitForTimeout(500);
618
+
619
+ await page.click('[data-testid="step2"]');
620
+ await page.waitForTimeout(500);
621
+
622
+ await page.click('[data-testid="step3"]');
623
+ await page.waitForTimeout(1000);
624
+
625
+ expect(await page.textContent('.result')).toBe('Complete');
626
+ });
627
+ ```
628
+
629
+ **✅ ROBUST**
630
+ ```typescript
631
+ it('completes workflow', async () => {
632
+ // Step 1
633
+ await page.click('[data-testid="step1"]');
634
+ await expect(page.locator('[data-testid="step2"]')).toBeEnabled();
635
+
636
+ // Step 2
637
+ await page.click('[data-testid="step2"]');
638
+ await expect(page.locator('[data-testid="step3"]')).toBeEnabled();
639
+
640
+ // Step 3
641
+ await page.click('[data-testid="step3"]');
642
+ await expect(page.locator('.result')).toHaveText('Complete');
643
+ });
644
+ ```
645
+
646
+ ---
647
+
648
+ ## When to Use This Skill
649
+
650
+ - ✅ **Before writing any E2E tests** - Establish patterns upfront
651
+ - ✅ **Before writing integration tests** - Async behavior needs proper waits
652
+ - ✅ **When tests are flaky** - Random failures indicate timing issues
653
+ - ✅ **When tests are slow** - Look for `waitForTimeout` and `setTimeout`
654
+ - ✅ **When debugging test failures** - Check for brittle patterns
655
+ - ✅ **During code review** - Verify tests follow robust patterns
656
+
657
+ ---
658
+
659
+ ## Test Robustness Checklist
660
+
661
+ Use this checklist when writing or reviewing tests:
662
+
663
+ ### Before Writing Tests
664
+ - [ ] Reviewed this skill document
665
+ - [ ] Imported helper utilities (`wait-helpers`, `time-helpers`, `seed-data`)
666
+ - [ ] Understand what conditions I'm waiting for (element, network, state)
667
+
668
+ ### While Writing Tests
669
+ - [ ] **No `waitForTimeout` calls** - Use `waitForSelector`, `waitForResponse`, or assertions
670
+ - [ ] **No real time delays** - Use `jest.useFakeTimers()` and `advanceTimersByTime()`
671
+ - [ ] **All async operations have explicit waits** - Don't rely on arbitrary delays
672
+ - [ ] **Test data is unique** - Use `uniqueId()` or `seedSession()` helpers
673
+ - [ ] **Assertions use auto-wait** - Playwright's `expect()` automatically waits
674
+
675
+ ### After Writing Tests
676
+ - [ ] **Run tests 10 times** - Verify no flakiness: `for i in {1..10}; do npm test; done`
677
+ - [ ] **Check test duration** - Should be fast (< 1s per test for unit, < 10s for E2E)
678
+ - [ ] **Verify in CI** - Tests pass consistently in CI environment
679
+ - [ ] **Review for clarity** - Explicit waits show what test expects
680
+
681
+ ### Code Review Checklist
682
+ - [ ] **Zero `waitForTimeout` calls** - Flag for replacement
683
+ - [ ] **Zero `setTimeout` in tests** - Flag for fake timers
684
+ - [ ] **No hard-coded test data** - Should use helpers
685
+ - [ ] **Consistent uniqueness strategy** - All tests use same pattern
686
+ - [ ] **Clear wait conditions** - Can understand what test waits for
687
+
688
+ ---
689
+
690
+ ## Common Issues and Solutions
691
+
692
+ ### Issue: "Element not found" Errors
693
+
694
+ **Problem**: Clicking element before it's ready
695
+
696
+ ```typescript
697
+ // ❌ Flaky
698
+ await page.click('[data-testid="button"]'); // May not exist yet!
699
+
700
+ // ✅ Robust
701
+ await page.waitForSelector('[data-testid="button"]', { state: 'visible' });
702
+ await page.click('[data-testid="button"]');
703
+
704
+ // ✅ Even better (auto-waits)
705
+ await expect(page.locator('[data-testid="button"]')).toBeVisible();
706
+ await page.click('[data-testid="button"]');
707
+ ```
708
+
709
+ ### Issue: "Timeout" Errors
710
+
711
+ **Problem**: Waiting for something that takes variable time
712
+
713
+ ```typescript
714
+ // ❌ Arbitrary timeout
715
+ await page.waitForTimeout(5000); // May be too short or too long
716
+
717
+ // ✅ Wait for specific condition
718
+ await page.waitForResponse(response =>
719
+ response.url().includes('/api/data') && response.ok()
720
+ );
721
+ ```
722
+
723
+ ### Issue: Tests Pass Locally, Fail in CI
724
+
725
+ **Problem**: CI is slower, arbitrary timeouts are too short
726
+
727
+ ```typescript
728
+ // ❌ Works locally (fast machine), fails CI (slow machine)
729
+ await page.waitForTimeout(1000);
730
+
731
+ // ✅ Works everywhere (waits for actual condition)
732
+ await expect(page.locator('.result')).toBeVisible();
733
+ ```
734
+
735
+ ### Issue: Random Test Failures
736
+
737
+ **Problem**: Race conditions from parallel tests
738
+
739
+ ```typescript
740
+ // ❌ Tests conflict when run in parallel
741
+ it('test 1', async () => {
742
+ await createSession('my-session'); // Conflicts with test 2!
743
+ });
744
+
745
+ it('test 2', async () => {
746
+ await createSession('my-session'); // Same name!
747
+ });
748
+
749
+ // ✅ Each test uses unique data
750
+ it('test 1', async () => {
751
+ await createSession(uniqueSessionName());
752
+ });
753
+
754
+ it('test 2', async () => {
755
+ await createSession(uniqueSessionName());
756
+ });
757
+ ```
758
+
759
+ ### Issue: Slow Test Suite
760
+
761
+ **Problem**: Using real timers for time-based tests
762
+
763
+ ```typescript
764
+ // ❌ Each test takes 1.1 seconds of real time
765
+ await new Promise(resolve => setTimeout(resolve, 1100));
766
+
767
+ // ✅ Each test takes ~0ms (instant)
768
+ jest.useFakeTimers();
769
+ jest.advanceTimersByTime(1100);
770
+ jest.useRealTimers();
771
+ ```
772
+
773
+ ---
774
+
775
+ ## Performance Impact
776
+
777
+ ### Current State (Brittle Tests)
778
+ - **33 instances of `waitForTimeout`** - Each adds 100-2000ms
779
+ - **6 instances of `setTimeout(1100)`** - Adds 6.6 seconds
780
+ - **Total overhead**: ~15-30 seconds per test run
781
+
782
+ ### After Refactoring (Robust Tests)
783
+ - **0 arbitrary timeouts** - All waits are condition-based
784
+ - **0 real time delays** - All timers are mocked
785
+ - **Total overhead**: ~0-2 seconds (only real network/UI time)
786
+
787
+ **Expected speedup**: 10-15x faster test suite
788
+
789
+ ---
790
+
791
+ ## Migration Strategy
792
+
793
+ ### Step 1: Audit Current Tests
794
+ ```bash
795
+ # Find all waitForTimeout calls
796
+ grep -rn "waitForTimeout" tests/
797
+
798
+ # Find all setTimeout calls
799
+ grep -rn "setTimeout" tests/
800
+
801
+ # Find hard-coded test data
802
+ grep -rn "'session-" tests/
803
+ grep -rn "'feature-" tests/
804
+ ```
805
+
806
+ ### Step 2: Create Helper Files
807
+ 1. Create `tests/helpers/wait-helpers.ts`
808
+ 2. Create `tests/helpers/time-helpers.ts`
809
+ 3. Create `tests/helpers/seed-data.ts`
810
+
811
+ ### Step 3: Replace Patterns (One Test at a Time)
812
+ 1. Replace `waitForTimeout` → `waitForSelector` or assertions
813
+ 2. Replace `setTimeout` → `jest.useFakeTimers()` + `advanceTimersByTime()`
814
+ 3. Replace hard-coded data → `uniqueSessionName()` or `seedSession()`
815
+
816
+ ### Step 4: Verify Improvements
817
+ ```bash
818
+ # Run tests 10 times to check for flakiness
819
+ for i in {1..10}; do npm test && echo "Run $i: PASS" || echo "Run $i: FAIL"; done
820
+
821
+ # Measure test duration
822
+ time npm test
823
+ ```
824
+
825
+ ### Step 5: Add to CI Checks
826
+ - Lint rule: Flag `waitForTimeout` in PR reviews
827
+ - Lint rule: Flag `setTimeout` in test files
828
+ - Pre-commit hook: Validate test patterns
829
+
830
+ ---
831
+
832
+ ## Reference
833
+
834
+ - **Playwright Waiting**: https://playwright.dev/docs/actionability
835
+ - **Playwright Assertions**: https://playwright.dev/docs/test-assertions
836
+ - **Jest Fake Timers**: https://jestjs.io/docs/timer-mocks
837
+ - **Testing Best Practices**: https://kentcdodds.com/blog/common-mistakes-with-react-testing-library
838
+
839
+ ---
840
+
841
+ ## Quick Reference
842
+
843
+ ### Replace These Patterns
844
+
845
+ | ❌ Brittle Pattern | ✅ Robust Pattern |
846
+ |-------------------|------------------|
847
+ | `await page.waitForTimeout(1000)` | `await page.waitForSelector('.element')` |
848
+ | `await page.waitForTimeout(500)` | `await expect(page.locator('.element')).toBeVisible()` |
849
+ | `setTimeout(() => {}, 1100)` | `jest.advanceTimersByTime(1100)` |
850
+ | `await createSession('my-session')` | `await createSession(uniqueSessionName())` |
851
+ | `const id = 'test-123'` | `const id = uniqueId('test')` |
852
+ | `await page.click(); await delay(500)` | `await page.click(); await waitForLoadingComplete(page)` |
853
+
854
+ ### Import These Helpers
855
+
856
+ ```typescript
857
+ // Wait helpers
858
+ import {
859
+ waitForInteractive,
860
+ waitForLoadingComplete,
861
+ waitForApiResponse,
862
+ waitForPageLoad
863
+ } from '../helpers/wait-helpers';
864
+
865
+ // Time helpers
866
+ import {
867
+ useFakeTimers,
868
+ advanceTime,
869
+ runAllTimers,
870
+ useRealTimers,
871
+ mockNow,
872
+ restoreNow
873
+ } from '../helpers/time-helpers';
874
+
875
+ // Seed data helpers
876
+ import {
877
+ uniqueId,
878
+ uniqueSessionName,
879
+ seedSession,
880
+ seedFeature,
881
+ seedProject
882
+ } from '../helpers/seed-data';
883
+ ```
884
+
885
+ ---
886
+
887
+ ## Summary
888
+
889
+ **Golden Rules for Robust Tests:**
890
+
891
+ 1. **Never use `waitForTimeout`** - Wait for specific conditions
892
+ 2. **Never use real timers** - Mock time for instant tests
893
+ 3. **Never hard-code test data** - Use unique, generated data
894
+ 4. **Always wait for conditions** - Element visible, network complete, state changed
895
+ 5. **Always use helpers** - DRY, consistent, maintainable
896
+
897
+ **Result**: Fast, reliable, maintainable test suite that never flakes.