@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.
- package/README.md +10 -12
- package/dist/cli.js +1 -1
- package/dist/commands/sync.js +9 -1
- package/dist/commands/sync.js.map +1 -1
- package/package.json +1 -1
- package/templates/agents/reviewer.md +1 -1
- package/templates/skills/api-contracts/SKILL.md +676 -0
- package/templates/skills/api-patterns/SKILL.md +346 -0
- package/templates/skills/api-patterns/examples/complete-rest-api.ts +293 -0
- package/templates/skills/api-patterns/templates/express-router-template.ts +294 -0
- package/templates/skills/docs-sync/SKILL.md +42 -0
- package/templates/skills/jest-mock-patterns/SKILL.md +397 -0
- package/templates/skills/playwright-testing/SKILL.md +124 -0
- package/templates/skills/sqlite-patterns/SKILL.md +229 -0
- package/templates/skills/test-caching/SKILL.md +99 -0
|
@@ -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.
|