@bradtaylorsf/alpha-loop 1.1.2 → 1.2.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.
- package/dist/cli.js +1 -1
- package/package.json +1 -1
- package/templates/agents/implementer.md +1 -1
- package/templates/skills/skill-creator/SKILL.md +485 -0
- package/templates/skills/api-contracts/SKILL.md +0 -676
- package/templates/skills/api-patterns/SKILL.md +0 -346
- package/templates/skills/api-patterns/examples/complete-rest-api.ts +0 -293
- package/templates/skills/api-patterns/templates/express-router-template.ts +0 -294
- package/templates/skills/jest-mock-patterns/SKILL.md +0 -397
- package/templates/skills/playwright-testing/SKILL.md +0 -124
- package/templates/skills/sqlite-patterns/SKILL.md +0 -229
- package/templates/skills/test-caching/SKILL.md +0 -99
|
@@ -1,124 +0,0 @@
|
|
|
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.
|
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: sqlite-patterns
|
|
3
|
-
description: SQLite database patterns using better-sqlite3. Synchronous API, file-based storage, great for single-user apps and development.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# SQLite Patterns Skill
|
|
7
|
-
|
|
8
|
-
Patterns for SQLite database operations using better-sqlite3.
|
|
9
|
-
|
|
10
|
-
## When to Use SQLite
|
|
11
|
-
|
|
12
|
-
- Single-user applications
|
|
13
|
-
- Development/prototyping
|
|
14
|
-
- File-based storage needs
|
|
15
|
-
- Simple applications without concurrency
|
|
16
|
-
- Embedded databases
|
|
17
|
-
|
|
18
|
-
## Setup
|
|
19
|
-
|
|
20
|
-
```typescript
|
|
21
|
-
import Database from 'better-sqlite3';
|
|
22
|
-
|
|
23
|
-
// Production
|
|
24
|
-
const db = new Database('./data/app.db');
|
|
25
|
-
|
|
26
|
-
// Testing (in-memory)
|
|
27
|
-
const testDb = new Database(':memory:');
|
|
28
|
-
|
|
29
|
-
// Enable WAL mode for better performance
|
|
30
|
-
db.pragma('journal_mode = WAL');
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## Basic Patterns
|
|
34
|
-
|
|
35
|
-
### Query Single Row
|
|
36
|
-
|
|
37
|
-
```typescript
|
|
38
|
-
function getUserById(id: number): User | null {
|
|
39
|
-
const result = db.prepare(`
|
|
40
|
-
SELECT id, email, name, created_at as createdAt
|
|
41
|
-
FROM users
|
|
42
|
-
WHERE id = ?
|
|
43
|
-
`).get(id);
|
|
44
|
-
|
|
45
|
-
return result ?? null; // better-sqlite3 returns undefined, not null
|
|
46
|
-
}
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
### Query Multiple Rows
|
|
50
|
-
|
|
51
|
-
```typescript
|
|
52
|
-
function getAllUsers(): User[] {
|
|
53
|
-
return db.prepare(`
|
|
54
|
-
SELECT id, email, name, created_at as createdAt
|
|
55
|
-
FROM users
|
|
56
|
-
ORDER BY created_at DESC
|
|
57
|
-
`).all() as User[];
|
|
58
|
-
}
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### Insert and Get ID
|
|
62
|
-
|
|
63
|
-
```typescript
|
|
64
|
-
function createUser(data: CreateUserDto): User {
|
|
65
|
-
const stmt = db.prepare(`
|
|
66
|
-
INSERT INTO users (email, password_hash, name)
|
|
67
|
-
VALUES (?, ?, ?)
|
|
68
|
-
`);
|
|
69
|
-
|
|
70
|
-
const info = stmt.run(data.email, passwordHash, data.name);
|
|
71
|
-
const id = info.lastInsertRowid;
|
|
72
|
-
|
|
73
|
-
return getUserById(Number(id))!;
|
|
74
|
-
}
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
### Update
|
|
78
|
-
|
|
79
|
-
```typescript
|
|
80
|
-
function updateUser(id: number, updates: UpdateUserDto): User | null {
|
|
81
|
-
const stmt = db.prepare(`
|
|
82
|
-
UPDATE users
|
|
83
|
-
SET email = COALESCE(?, email),
|
|
84
|
-
name = COALESCE(?, name),
|
|
85
|
-
updated_at = datetime('now')
|
|
86
|
-
WHERE id = ?
|
|
87
|
-
`);
|
|
88
|
-
|
|
89
|
-
const info = stmt.run(updates.email, updates.name, id);
|
|
90
|
-
|
|
91
|
-
if (info.changes === 0) return null;
|
|
92
|
-
return getUserById(id);
|
|
93
|
-
}
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
### Delete
|
|
97
|
-
|
|
98
|
-
```typescript
|
|
99
|
-
function deleteUser(id: number): boolean {
|
|
100
|
-
const info = db.prepare('DELETE FROM users WHERE id = ?').run(id);
|
|
101
|
-
return info.changes > 0;
|
|
102
|
-
}
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
## Transactions
|
|
106
|
-
|
|
107
|
-
```typescript
|
|
108
|
-
function transferFunds(fromId: number, toId: number, amount: number): void {
|
|
109
|
-
const transfer = db.transaction(() => {
|
|
110
|
-
db.prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?')
|
|
111
|
-
.run(amount, fromId);
|
|
112
|
-
|
|
113
|
-
db.prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?')
|
|
114
|
-
.run(amount, toId);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
transfer(); // Automatically rolls back on error
|
|
118
|
-
}
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
## JSON Fields
|
|
122
|
-
|
|
123
|
-
```typescript
|
|
124
|
-
// Store JSON
|
|
125
|
-
function saveSettings(userId: number, settings: Settings): void {
|
|
126
|
-
db.prepare(`
|
|
127
|
-
UPDATE users SET settings = ? WHERE id = ?
|
|
128
|
-
`).run(JSON.stringify(settings), userId);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Parse JSON
|
|
132
|
-
function getSettings(userId: number): Settings {
|
|
133
|
-
const result = db.prepare(`
|
|
134
|
-
SELECT settings FROM users WHERE id = ?
|
|
135
|
-
`).get(userId) as { settings: string } | undefined;
|
|
136
|
-
|
|
137
|
-
return result ? JSON.parse(result.settings) : {};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Helper for safe JSON parsing
|
|
141
|
-
function parseJsonField<T>(value: string | null | undefined, fallback: T): T {
|
|
142
|
-
if (!value) return fallback;
|
|
143
|
-
try {
|
|
144
|
-
return JSON.parse(value) as T;
|
|
145
|
-
} catch {
|
|
146
|
-
return fallback;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
## Migrations
|
|
152
|
-
|
|
153
|
-
```sql
|
|
154
|
-
-- migrations/001_create_users.sql
|
|
155
|
-
CREATE TABLE IF NOT EXISTS users (
|
|
156
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
157
|
-
email TEXT UNIQUE NOT NULL,
|
|
158
|
-
password_hash TEXT NOT NULL,
|
|
159
|
-
name TEXT NOT NULL,
|
|
160
|
-
settings TEXT DEFAULT '{}',
|
|
161
|
-
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
162
|
-
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
CREATE INDEX idx_users_email ON users(email);
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
```typescript
|
|
169
|
-
// Run migrations
|
|
170
|
-
function runMigrations(db: Database.Database): void {
|
|
171
|
-
const migrations = fs.readdirSync('./migrations')
|
|
172
|
-
.filter(f => f.endsWith('.sql'))
|
|
173
|
-
.sort();
|
|
174
|
-
|
|
175
|
-
for (const file of migrations) {
|
|
176
|
-
const sql = fs.readFileSync(`./migrations/${file}`, 'utf-8');
|
|
177
|
-
db.exec(sql);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
## Error Handling
|
|
183
|
-
|
|
184
|
-
```typescript
|
|
185
|
-
function createUser(data: CreateUserDto): User {
|
|
186
|
-
try {
|
|
187
|
-
// ... insert logic
|
|
188
|
-
} catch (error: unknown) {
|
|
189
|
-
if (error instanceof Error && 'code' in error) {
|
|
190
|
-
const sqliteError = error as { code: string };
|
|
191
|
-
if (sqliteError.code === 'SQLITE_CONSTRAINT') {
|
|
192
|
-
throw new AppError('Email already exists', 400);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
throw error;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
## Timestamps
|
|
201
|
-
|
|
202
|
-
```typescript
|
|
203
|
-
// SQLite datetime format
|
|
204
|
-
const now = new Date().toISOString(); // '2024-01-15T10:30:00.000Z'
|
|
205
|
-
|
|
206
|
-
// Or use SQLite's built-in
|
|
207
|
-
db.prepare(`
|
|
208
|
-
INSERT INTO logs (message, created_at)
|
|
209
|
-
VALUES (?, datetime('now'))
|
|
210
|
-
`).run(message);
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
## Best Practices
|
|
214
|
-
|
|
215
|
-
1. **Use prepared statements** - Prevents SQL injection
|
|
216
|
-
2. **Use transactions** - For multiple related operations
|
|
217
|
-
3. **Enable WAL mode** - Better concurrent read performance
|
|
218
|
-
4. **Use in-memory for tests** - Fast, isolated testing
|
|
219
|
-
5. **Handle undefined** - better-sqlite3 returns undefined, not null
|
|
220
|
-
6. **Index foreign keys** - Improve JOIN performance
|
|
221
|
-
7. **Use COALESCE for updates** - Partial updates without overwriting
|
|
222
|
-
|
|
223
|
-
## Common Gotchas
|
|
224
|
-
|
|
225
|
-
- Returns `undefined` not `null` for missing rows
|
|
226
|
-
- `lastInsertRowid` is a BigInt (convert with Number())
|
|
227
|
-
- JSON must be stringified before storage
|
|
228
|
-
- Datetime stored as TEXT (ISO format recommended)
|
|
229
|
-
- No native boolean type (use INTEGER 0/1)
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: test-caching
|
|
3
|
-
description: API response caching for expensive AI API calls in tests. Use when writing tests that call Claude, OpenAI, or other AI APIs.
|
|
4
|
-
auto_load: false
|
|
5
|
-
priority: medium
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# Test Caching Skill
|
|
9
|
-
|
|
10
|
-
## Trigger
|
|
11
|
-
When writing tests that make HTTP calls to AI/LLM APIs (Claude, OpenAI, etc.) or any expensive external API.
|
|
12
|
-
|
|
13
|
-
## How It Works
|
|
14
|
-
|
|
15
|
-
The `mockExpensiveAPI()` helper intercepts HTTP requests matching a URL pattern.
|
|
16
|
-
|
|
17
|
-
- **Default mode** (`pnpm test`): Replays responses from fixture files in `tests/fixtures/`. No network calls.
|
|
18
|
-
- **Record mode** (`RECORD_FIXTURES=true` / `pnpm test:full`): Lets real requests through, captures responses to fixture files.
|
|
19
|
-
|
|
20
|
-
## Usage
|
|
21
|
-
|
|
22
|
-
```typescript
|
|
23
|
-
import { mockExpensiveAPI } from '../../src/testing/cache';
|
|
24
|
-
|
|
25
|
-
describe('my AI feature', () => {
|
|
26
|
-
let mock: ReturnType<typeof mockExpensiveAPI>;
|
|
27
|
-
|
|
28
|
-
beforeEach(() => {
|
|
29
|
-
mock = mockExpensiveAPI({
|
|
30
|
-
name: 'my-feature-openai', // fixture filename
|
|
31
|
-
pattern: 'https://api.openai.com', // URL prefix to intercept
|
|
32
|
-
service: 'openai', // metadata
|
|
33
|
-
estimatedCostUSD: 0.02, // metadata
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
afterEach(() => {
|
|
38
|
-
mock.restore(); // always restore HTTP patches
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('calls the AI API', async () => {
|
|
42
|
-
// In replay mode, this returns the cached response
|
|
43
|
-
// In record mode, this hits the real API and saves the response
|
|
44
|
-
const result = await callMyAIFeature();
|
|
45
|
-
expect(result).toBeDefined();
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
## Options
|
|
51
|
-
|
|
52
|
-
| Option | Required | Description |
|
|
53
|
-
|--------|----------|-------------|
|
|
54
|
-
| `name` | Yes | Unique fixture name (becomes `<name>.fixture.json`) |
|
|
55
|
-
| `pattern` | Yes | URL prefix (string) or RegExp to intercept |
|
|
56
|
-
| `service` | No | Service name for metadata (default: `"unknown"`) |
|
|
57
|
-
| `estimatedCostUSD` | No | Cost per call for metadata (default: `0`) |
|
|
58
|
-
| `fixturesDir` | No | Override fixture directory (default: `tests/fixtures/`) |
|
|
59
|
-
|
|
60
|
-
## Commands
|
|
61
|
-
|
|
62
|
-
| Command | Description |
|
|
63
|
-
|---------|-------------|
|
|
64
|
-
| `pnpm test` | Run tests with cached API responses (default) |
|
|
65
|
-
| `pnpm test:full` | Run tests with real API calls, re-record fixtures |
|
|
66
|
-
|
|
67
|
-
## Fixture Format
|
|
68
|
-
|
|
69
|
-
Fixtures are stored as JSON arrays in `tests/fixtures/<name>.fixture.json`:
|
|
70
|
-
|
|
71
|
-
```json
|
|
72
|
-
[
|
|
73
|
-
{
|
|
74
|
-
"request": { "url": "https://api.openai.com/v1/chat", "method": "POST", "body": { "model": "gpt-4" } },
|
|
75
|
-
"response": { "status": 200, "headers": {}, "body": { "choices": [] } },
|
|
76
|
-
"metadata": { "recordedAt": "2025-01-15T10:00:00.000Z", "service": "openai", "estimatedCostUSD": 0.02 }
|
|
77
|
-
}
|
|
78
|
-
]
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
## Cache Invalidation
|
|
82
|
-
|
|
83
|
-
Fixtures older than 30 days produce a warning at test startup. Re-record with `RECORD_FIXTURES=true` to refresh.
|
|
84
|
-
|
|
85
|
-
## In the Loop
|
|
86
|
-
|
|
87
|
-
`scripts/loop.sh` uses cached mode by default. Pass `--run-full` to bypass cache:
|
|
88
|
-
|
|
89
|
-
```bash
|
|
90
|
-
bash scripts/loop.sh --run-full # real API calls
|
|
91
|
-
bash scripts/loop.sh # cached (default)
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
## Rules
|
|
95
|
-
|
|
96
|
-
1. Always call `mock.restore()` in `afterEach` to undo HTTP patches
|
|
97
|
-
2. Commit fixture files to git so CI can replay without API keys
|
|
98
|
-
3. Use descriptive `name` values — one fixture per test scenario
|
|
99
|
-
4. Check `mock.warnings` for staleness alerts in your test setup
|