@bradtaylorsf/alpha-loop 1.1.0 → 1.1.2

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.
@@ -0,0 +1,397 @@
1
+ ---
2
+ name: jest-mock-patterns
3
+ description: Common Jest mocking gotchas and solutions. Document common Jest mocking pitfalls including resetMocks behavior, module mocking order, and system global mocking.
4
+ ---
5
+
6
+ # Jest Mock Patterns Skill
7
+
8
+ Comprehensive guide to Jest mocking patterns, common gotchas, and solutions.
9
+
10
+ ## Configuration Gotchas
11
+
12
+ ### resetMocks vs clearMocks vs restoreMocks
13
+
14
+ | Option | Clears call history | Clears return values | Restores original |
15
+ |--------|--------------------|-----------------------|-------------------|
16
+ | `clearMocks` | Yes | No | No |
17
+ | `resetMocks` | Yes | **Yes** | No |
18
+ | `restoreMocks` | Yes | Yes | Yes |
19
+
20
+ **CRITICAL**: AlphaCoder uses `resetMocks: true` in Jest config!
21
+
22
+ ### The resetMocks: true Gotcha
23
+
24
+ **Problem:** Mock return values are cleared between tests.
25
+
26
+ ```typescript
27
+ // ❌ WRONG - Return value lost after first test
28
+ const mockGetSession = jest.fn().mockReturnValue({ id: 1, status: 'active' });
29
+ jest.mock('./session-manager', () => ({ getSession: mockGetSession }));
30
+
31
+ describe('Session tests', () => {
32
+ it('test 1', () => {
33
+ expect(mockGetSession()).toEqual({ id: 1, status: 'active' }); // PASS
34
+ });
35
+
36
+ it('test 2', () => {
37
+ // mockGetSession now returns undefined due to resetMocks!
38
+ expect(mockGetSession()).toEqual({ id: 1, status: 'active' }); // FAIL
39
+ });
40
+ });
41
+ ```
42
+
43
+ **Solution:** Re-set mock return values in `beforeEach`:
44
+
45
+ ```typescript
46
+ // ✅ CORRECT - Reset return values each test
47
+ import { getSession } from './session-manager';
48
+ jest.mock('./session-manager');
49
+
50
+ const mockedGetSession = getSession as jest.Mock;
51
+
52
+ describe('Session tests', () => {
53
+ beforeEach(() => {
54
+ mockedGetSession.mockReturnValue({ id: 1, status: 'active' });
55
+ });
56
+
57
+ it('test 1', () => {
58
+ expect(mockedGetSession()).toEqual({ id: 1, status: 'active' }); // PASS
59
+ });
60
+
61
+ it('test 2', () => {
62
+ expect(mockedGetSession()).toEqual({ id: 1, status: 'active' }); // PASS
63
+ });
64
+ });
65
+ ```
66
+
67
+ **Alternative:** Mock factory function (called for each test):
68
+
69
+ ```typescript
70
+ // ✅ ALSO CORRECT - Factory function approach
71
+ jest.mock('./session-manager', () => ({
72
+ getSession: jest.fn(() => ({ id: 1, status: 'active' })),
73
+ }));
74
+ ```
75
+
76
+ ## Module Mocking Order
77
+
78
+ ### Mocks MUST Be Before Imports
79
+
80
+ Jest hoists `jest.mock()` calls, but the mock factory runs at import time.
81
+
82
+ ```typescript
83
+ // ✅ CORRECT ORDER
84
+ jest.mock('./database');
85
+ import { getDatabase } from './database'; // Receives mocked version
86
+
87
+ // ❌ WRONG ORDER - Import already resolved
88
+ import { getDatabase } from './database'; // Gets real version
89
+ jest.mock('./database'); // Too late!
90
+ ```
91
+
92
+ ### Dynamic Imports with Mocks
93
+
94
+ For complex cases, use dynamic imports:
95
+
96
+ ```typescript
97
+ jest.mock('./database');
98
+
99
+ describe('tests', () => {
100
+ let myModule: typeof import('./my-module');
101
+
102
+ beforeEach(async () => {
103
+ // Fresh import each test
104
+ myModule = await import('./my-module');
105
+ });
106
+ });
107
+ ```
108
+
109
+ ## Mocking System Globals
110
+
111
+ ### process.kill for PID Checking
112
+
113
+ Standard pattern for testing code that checks if processes are alive:
114
+
115
+ ```typescript
116
+ // Create test helpers
117
+ const originalKill = process.kill;
118
+ const deadPids = new Set<number>();
119
+
120
+ beforeEach(() => {
121
+ deadPids.clear();
122
+ (process.kill as jest.Mock) = jest.fn((pid: number, signal?: number) => {
123
+ // Signal 0 = check if process exists (without killing)
124
+ if (signal === 0 && deadPids.has(pid)) {
125
+ const error = new Error('ESRCH: no such process');
126
+ (error as NodeJS.ErrnoException).code = 'ESRCH';
127
+ throw error;
128
+ }
129
+ return true;
130
+ });
131
+ });
132
+
133
+ afterEach(() => {
134
+ process.kill = originalKill;
135
+ });
136
+
137
+ // Usage in tests:
138
+ describe('Process detection', () => {
139
+ it('returns true for alive process', () => {
140
+ expect(isProcessAlive(12345)).toBe(true);
141
+ });
142
+
143
+ it('returns false for dead process', () => {
144
+ deadPids.add(12345);
145
+ expect(isProcessAlive(12345)).toBe(false);
146
+ });
147
+ });
148
+ ```
149
+
150
+ ### process.pid
151
+
152
+ ```typescript
153
+ const originalPid = process.pid;
154
+
155
+ beforeEach(() => {
156
+ Object.defineProperty(process, 'pid', { value: 99999, writable: true });
157
+ });
158
+
159
+ afterEach(() => {
160
+ Object.defineProperty(process, 'pid', { value: originalPid, writable: true });
161
+ });
162
+ ```
163
+
164
+ ### Date/Time Mocking
165
+
166
+ ```typescript
167
+ describe('Time-sensitive tests', () => {
168
+ beforeEach(() => {
169
+ jest.useFakeTimers();
170
+ jest.setSystemTime(new Date('2025-01-01T00:00:00Z'));
171
+ });
172
+
173
+ afterEach(() => {
174
+ jest.useRealTimers();
175
+ });
176
+
177
+ it('uses mocked time', () => {
178
+ expect(new Date().toISOString()).toBe('2025-01-01T00:00:00.000Z');
179
+ });
180
+
181
+ it('can advance time', () => {
182
+ jest.advanceTimersByTime(60000); // 1 minute
183
+ expect(new Date().toISOString()).toBe('2025-01-01T00:01:00.000Z');
184
+ });
185
+ });
186
+ ```
187
+
188
+ ### setTimeout/setInterval
189
+
190
+ ```typescript
191
+ beforeEach(() => {
192
+ jest.useFakeTimers();
193
+ });
194
+
195
+ afterEach(() => {
196
+ jest.useRealTimers();
197
+ });
198
+
199
+ it('handles delayed callback', () => {
200
+ const callback = jest.fn();
201
+ setTimeout(callback, 1000);
202
+
203
+ expect(callback).not.toHaveBeenCalled();
204
+
205
+ jest.advanceTimersByTime(1000);
206
+
207
+ expect(callback).toHaveBeenCalledTimes(1);
208
+ });
209
+ ```
210
+
211
+ ## Mocking Expensive Infrastructure
212
+
213
+ ### Claude CLI / AgentSession
214
+
215
+ Avoid spawning real Claude processes ($$$):
216
+
217
+ ```typescript
218
+ jest.mock('../../src/server/agent.js', () => ({
219
+ AgentSession: jest.fn().mockImplementation(() => ({
220
+ run: jest.fn().mockResolvedValue(undefined),
221
+ on: jest.fn(),
222
+ stop: jest.fn(),
223
+ emit: jest.fn(),
224
+ })),
225
+ }));
226
+ ```
227
+
228
+ ### Session Manager
229
+
230
+ ```typescript
231
+ const mockGetActiveSession = jest.fn();
232
+ const mockStartSession = jest.fn();
233
+ const mockStopSession = jest.fn();
234
+
235
+ jest.mock('../../src/server/session-manager.js', () => ({
236
+ getActiveSession: mockGetActiveSession,
237
+ startSession: mockStartSession,
238
+ stopSession: mockStopSession,
239
+ getActiveSessionCount: jest.fn().mockReturnValue(0),
240
+ }));
241
+
242
+ // In beforeEach, reset return values:
243
+ beforeEach(() => {
244
+ mockGetActiveSession.mockReturnValue(null);
245
+ mockStartSession.mockResolvedValue({ on: jest.fn() });
246
+ });
247
+ ```
248
+
249
+ ### WebSocket Broadcasting
250
+
251
+ ```typescript
252
+ const mockBroadcast = jest.fn();
253
+ jest.mock('../../src/server/websocket-broadcaster.js', () => ({
254
+ broadcast: mockBroadcast,
255
+ }));
256
+
257
+ // Verify broadcasts in tests:
258
+ expect(mockBroadcast).toHaveBeenCalledWith({
259
+ type: 'session_started',
260
+ payload: expect.objectContaining({
261
+ projectId: 1,
262
+ sessionType: 'coding',
263
+ }),
264
+ });
265
+ ```
266
+
267
+ ### Database (In-Memory SQLite)
268
+
269
+ ```typescript
270
+ import Database from 'better-sqlite3';
271
+
272
+ let testDb: Database.Database;
273
+
274
+ beforeAll(() => {
275
+ testDb = new Database(':memory:');
276
+ testDb.exec(`
277
+ CREATE TABLE sessions (
278
+ id INTEGER PRIMARY KEY,
279
+ status TEXT DEFAULT 'pending'
280
+ );
281
+ `);
282
+ });
283
+
284
+ afterAll(() => {
285
+ testDb.close();
286
+ });
287
+
288
+ // Mock getDatabase to return test db
289
+ jest.mock('../../src/server/database.js', () => ({
290
+ getDatabase: () => testDb,
291
+ }));
292
+ ```
293
+
294
+ ## Spy vs Mock
295
+
296
+ ### When to Use Spies
297
+
298
+ Spies observe calls to real implementations:
299
+
300
+ ```typescript
301
+ // Spy on existing method - real implementation runs
302
+ const consoleSpy = jest.spyOn(console, 'log');
303
+
304
+ doSomething(); // console.log actually runs
305
+
306
+ expect(consoleSpy).toHaveBeenCalledWith('expected message');
307
+
308
+ consoleSpy.mockRestore();
309
+ ```
310
+
311
+ ### When to Use Mocks
312
+
313
+ Mocks replace implementations entirely:
314
+
315
+ ```typescript
316
+ // Mock replaces implementation
317
+ jest.spyOn(console, 'log').mockImplementation(() => {});
318
+
319
+ doSomething(); // console.log is silenced
320
+
321
+ expect(console.log).toHaveBeenCalled();
322
+ ```
323
+
324
+ ## Common Patterns
325
+
326
+ ### Testing Async Code
327
+
328
+ ```typescript
329
+ it('handles async operations', async () => {
330
+ mockFetch.mockResolvedValue({ data: 'result' });
331
+
332
+ const result = await fetchData();
333
+
334
+ expect(result).toEqual({ data: 'result' });
335
+ });
336
+
337
+ it('handles async errors', async () => {
338
+ mockFetch.mockRejectedValue(new Error('Network error'));
339
+
340
+ await expect(fetchData()).rejects.toThrow('Network error');
341
+ });
342
+ ```
343
+
344
+ ### Testing Event Emitters
345
+
346
+ ```typescript
347
+ it('emits events correctly', () => {
348
+ const mockCallback = jest.fn();
349
+ emitter.on('event', mockCallback);
350
+
351
+ emitter.emit('event', { data: 'test' });
352
+
353
+ expect(mockCallback).toHaveBeenCalledWith({ data: 'test' });
354
+ });
355
+ ```
356
+
357
+ ### Partial Mocking
358
+
359
+ Mock only specific exports:
360
+
361
+ ```typescript
362
+ jest.mock('./utils', () => ({
363
+ ...jest.requireActual('./utils'), // Keep real implementations
364
+ expensiveFunction: jest.fn(), // Mock only this one
365
+ }));
366
+ ```
367
+
368
+ ## Troubleshooting
369
+
370
+ ### Mock Not Being Applied
371
+
372
+ 1. Check mock order (before imports)
373
+ 2. Check mock path matches import path exactly
374
+ 3. Check for `.js` extension in ESM projects
375
+
376
+ ### Mock Return Value Undefined
377
+
378
+ 1. Check if `resetMocks: true` in Jest config
379
+ 2. Add return value in `beforeEach`
380
+
381
+ ### Tests Pass Individually, Fail Together
382
+
383
+ 1. Mock state leaking between tests
384
+ 2. Missing cleanup in `afterEach`
385
+ 3. Shared mutable state
386
+
387
+ ### Timeout Errors
388
+
389
+ 1. Missing `await` on async operations
390
+ 2. Unresolved promises in mocked functions
391
+ 3. `useFakeTimers()` blocking real timers
392
+
393
+ ## Reference
394
+
395
+ - Jest Mocking: https://jestjs.io/docs/mock-functions
396
+ - Jest Timer Mocks: https://jestjs.io/docs/timer-mocks
397
+ - Jest ES6 Mocks: https://jestjs.io/docs/es6-class-mocks
@@ -0,0 +1,124 @@
1
+ # Playwright E2E Testing Skill
2
+
3
+ ## Overview
4
+
5
+ This skill teaches agents how to write and maintain Playwright end-to-end tests for the Alpha Loop dashboard. E2E tests validate that the app works visually in a browser, catching issues that pass unit tests but break the UI.
6
+
7
+ ## Test Configuration
8
+
9
+ - **Test directory**: `tests/e2e/`
10
+ - **Config file**: `playwright.config.ts`
11
+ - **E2E port**: `4002` (isolated from dev 4001 and prod 4000)
12
+ - **Database**: In-memory SQLite (`DATABASE_PATH=:memory:`)
13
+ - **Mock API**: `MOCK_CLAUDE_API=true` to avoid real AI calls
14
+ - **Browser**: Chromium only
15
+ - **Run command**: `pnpm test:e2e`
16
+
17
+ ## Writing E2E Tests
18
+
19
+ ### File naming
20
+
21
+ - Place tests in `tests/e2e/`
22
+ - Use `.spec.ts` suffix (e.g., `dashboard.spec.ts`)
23
+
24
+ ### Test structure
25
+
26
+ ```typescript
27
+ import { test, expect } from "@playwright/test";
28
+
29
+ test.describe("Feature Name", () => {
30
+ test("describes what it validates", async ({ page }) => {
31
+ await page.goto("/");
32
+ // Use waitForSelector or expect with auto-waiting
33
+ await expect(page.locator("h1")).toHaveText("Alpha Loop");
34
+ });
35
+ });
36
+ ```
37
+
38
+ ### Best practices for reliability
39
+
40
+ 1. **Use Playwright auto-waiting** — `expect(locator).toBeVisible()` auto-waits up to the configured timeout
41
+ 2. **Avoid fixed timeouts** — use `waitForSelector`, `waitForResponse`, or `expect` assertions instead of `page.waitForTimeout()`
42
+ 3. **Use data-testid attributes** — prefer `[data-testid='config-view']` over fragile CSS selectors
43
+ 4. **Clean up state** — each test should work in isolation, don't depend on test execution order
44
+ 5. **Seed test data via API** — create test data by hitting API endpoints, not by manipulating the DB directly
45
+
46
+ ### Seeding test data
47
+
48
+ ```typescript
49
+ test("shows runs created via API", async ({ request, page }) => {
50
+ // Create test data via API
51
+ await request.post("/api/runs", {
52
+ data: {
53
+ issue_number: 42,
54
+ issue_title: "Test issue",
55
+ agent: "claude",
56
+ model: "sonnet",
57
+ },
58
+ });
59
+
60
+ // Navigate and verify
61
+ await page.goto("/");
62
+ await page.click("button:has-text('Run History')");
63
+ await expect(page.locator("text=Test issue")).toBeVisible();
64
+ });
65
+ ```
66
+
67
+ ### Testing SSE / Live View
68
+
69
+ ```typescript
70
+ test("live view connects to SSE stream", async ({ page }) => {
71
+ await page.goto("/");
72
+ // The EventSource connection happens automatically
73
+ await expect(page.locator("text=Connected")).toBeVisible({ timeout: 5000 });
74
+ });
75
+ ```
76
+
77
+ ### Common selectors
78
+
79
+ | Element | Selector |
80
+ |---------|----------|
81
+ | App title | `h1` (text: "Alpha Loop") |
82
+ | Tab buttons | `button:has-text('Live View')`, `button:has-text('Run History')`, `button:has-text('Config')` |
83
+ | Live log | `[data-testid='live-log']` |
84
+ | Config view | `[data-testid='config-view']` |
85
+ | Config editor | `[data-testid='config-editor']` |
86
+ | Status badge | `[data-testid='status-badge']` |
87
+ | History table | `table` within Run History tab |
88
+
89
+ ## Running E2E Tests
90
+
91
+ ```bash
92
+ # Run all E2E tests
93
+ pnpm test:e2e
94
+
95
+ # Run with headed browser (for debugging)
96
+ npx playwright test --headed
97
+
98
+ # Run a specific test file
99
+ npx playwright test tests/e2e/dashboard.spec.ts
100
+
101
+ # Show test report after failure
102
+ npx playwright show-report
103
+ ```
104
+
105
+ ## Environment Variables
106
+
107
+ | Variable | Default | Description |
108
+ |----------|---------|-------------|
109
+ | `SKIP_E2E` | `false` | Skip Playwright in the loop pipeline |
110
+ | `DATABASE_PATH` | file path | Set to `:memory:` for in-memory SQLite |
111
+ | `MOCK_CLAUDE_API` | `false` | Mock Claude API calls |
112
+ | `PORT` | `4000` | Server port (E2E uses `4002`) |
113
+
114
+ ## Integration with Loop Pipeline
115
+
116
+ E2E tests run after unit/API tests in the loop pipeline:
117
+
118
+ 1. Implement
119
+ 2. Run unit/API tests (with retry)
120
+ 3. **Run Playwright E2E tests** (with same retry loop)
121
+ 4. Code review
122
+ 5. Create PR
123
+
124
+ If `SKIP_E2E=true`, the E2E step is skipped. E2E test failures trigger the same retry loop as unit tests — the agent receives the error output and attempts to fix the issue.