@ekkos/cli 0.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/cache/LocalSessionStore.d.ts +129 -0
- package/dist/cache/LocalSessionStore.js +688 -0
- package/dist/cache/capture.d.ts +26 -0
- package/dist/cache/capture.js +461 -0
- package/dist/cache/index.d.ts +7 -0
- package/dist/cache/index.js +23 -0
- package/dist/cache/types.d.ts +147 -0
- package/dist/cache/types.js +40 -0
- package/dist/commands/init.d.ts +9 -0
- package/dist/commands/init.js +478 -0
- package/dist/commands/run.d.ts +12 -0
- package/dist/commands/run.js +829 -0
- package/dist/commands/setup.d.ts +6 -0
- package/dist/commands/setup.js +658 -0
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +109 -0
- package/dist/commands/test.d.ts +1 -0
- package/dist/commands/test.js +157 -0
- package/dist/deploy/agents.d.ts +15 -0
- package/dist/deploy/agents.js +72 -0
- package/dist/deploy/hooks.d.ts +16 -0
- package/dist/deploy/hooks.js +121 -0
- package/dist/deploy/index.d.ts +7 -0
- package/dist/deploy/index.js +24 -0
- package/dist/deploy/instructions.d.ts +12 -0
- package/dist/deploy/instructions.js +36 -0
- package/dist/deploy/mcp.d.ts +19 -0
- package/dist/deploy/mcp.js +109 -0
- package/dist/deploy/plugins.d.ts +19 -0
- package/dist/deploy/plugins.js +62 -0
- package/dist/deploy/settings.d.ts +8 -0
- package/dist/deploy/settings.js +84 -0
- package/dist/deploy/skills.d.ts +19 -0
- package/dist/deploy/skills.js +60 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +71 -0
- package/dist/restore/RestoreOrchestrator.d.ts +48 -0
- package/dist/restore/RestoreOrchestrator.js +481 -0
- package/dist/restore/index.d.ts +4 -0
- package/dist/restore/index.js +20 -0
- package/dist/utils/platform.d.ts +29 -0
- package/dist/utils/platform.js +65 -0
- package/dist/utils/session-words.json +119 -0
- package/dist/utils/state.d.ts +57 -0
- package/dist/utils/state.js +186 -0
- package/dist/utils/templates.d.ts +24 -0
- package/dist/utils/templates.js +118 -0
- package/package.json +48 -0
- package/templates/CLAUDE.md +287 -0
- package/templates/README.md +378 -0
- package/templates/agents/README.md +182 -0
- package/templates/agents/code-reviewer.md +166 -0
- package/templates/agents/debug-detective.md +169 -0
- package/templates/agents/ekkOS_Vercel.md +99 -0
- package/templates/agents/extension-manager.md +229 -0
- package/templates/agents/git-companion.md +185 -0
- package/templates/agents/github-test-agent.md +321 -0
- package/templates/agents/railway-manager.md +179 -0
- package/templates/claude-plugins/PHASE2_COMPLETION.md +346 -0
- package/templates/claude-plugins/PLUGIN_PROPOSALS.md +1776 -0
- package/templates/claude-plugins/README.md +587 -0
- package/templates/claude-plugins/agents/code-reviewer.json +14 -0
- package/templates/claude-plugins/agents/debug-detective.json +15 -0
- package/templates/claude-plugins/agents/git-companion.json +14 -0
- package/templates/claude-plugins/blog-manager/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins/blog-manager/commands/blog.md +691 -0
- package/templates/claude-plugins/golden-loop-monitor/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins/golden-loop-monitor/commands/loop-status.md +434 -0
- package/templates/claude-plugins/learning-tracker/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins/learning-tracker/commands/my-patterns.md +282 -0
- package/templates/claude-plugins/memory-lens/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins/memory-lens/commands/memory-search.md +181 -0
- package/templates/claude-plugins/pattern-coach/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins/pattern-coach/commands/forge.md +365 -0
- package/templates/claude-plugins/project-schema-validator/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins/project-schema-validator/commands/validate-schema.md +582 -0
- package/templates/claude-plugins-admin/AGENT_TEAM_PROPOSALS.md +819 -0
- package/templates/claude-plugins-admin/README.md +446 -0
- package/templates/claude-plugins-admin/autonomous-admin-agent/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins-admin/autonomous-admin-agent/commands/agent.md +595 -0
- package/templates/claude-plugins-admin/backend-agent/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins-admin/backend-agent/commands/backend.md +798 -0
- package/templates/claude-plugins-admin/deploy-guardian/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins-admin/deploy-guardian/commands/deploy.md +554 -0
- package/templates/claude-plugins-admin/frontend-agent/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins-admin/frontend-agent/commands/frontend.md +881 -0
- package/templates/claude-plugins-admin/mcp-server-manager/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins-admin/mcp-server-manager/commands/mcp.md +85 -0
- package/templates/claude-plugins-admin/memory-system-monitor/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins-admin/memory-system-monitor/commands/memory-health.md +569 -0
- package/templates/claude-plugins-admin/qa-agent/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins-admin/qa-agent/commands/qa.md +863 -0
- package/templates/claude-plugins-admin/tech-lead-agent/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins-admin/tech-lead-agent/commands/lead.md +732 -0
- package/templates/commands/continue.md +47 -0
- package/templates/cursor-hooks/after-agent-response.sh +117 -0
- package/templates/cursor-hooks/before-submit-prompt.sh +419 -0
- package/templates/cursor-hooks/hooks.json +20 -0
- package/templates/cursor-hooks/lib/contract.sh +320 -0
- package/templates/cursor-hooks/stop.sh +75 -0
- package/templates/cursor-rules/ekkos-memory.md +187 -0
- package/templates/hooks/assistant-response.sh +96 -0
- package/templates/hooks/hooks.json +28 -0
- package/templates/hooks/lib/contract.sh +320 -0
- package/templates/hooks/lib/state.sh +158 -0
- package/templates/hooks/session-start.ps1 +41 -0
- package/templates/hooks/session-start.sh +318 -0
- package/templates/hooks/stop.ps1 +16 -0
- package/templates/hooks/stop.sh +989 -0
- package/templates/hooks/user-prompt-submit.ps1 +174 -0
- package/templates/hooks/user-prompt-submit.sh +587 -0
- package/templates/hooks-node/lib/state.js +187 -0
- package/templates/hooks-node/stop.js +416 -0
- package/templates/hooks-node/user-prompt-submit.js +337 -0
- package/templates/plan-template.md +306 -0
- package/templates/rules/00-hooks-contract.mdc +89 -0
- package/templates/rules/30-ekkos-core.mdc +188 -0
- package/templates/rules/31-ekkos-messages.mdc +78 -0
- package/templates/skills/continue/SKILL.md +169 -0
- package/templates/skills/ekkOS_Deep_Recall/Skill.md +282 -0
- package/templates/skills/ekkOS_Learn/Skill.md +265 -0
- package/templates/skills/ekkOS_Memory_First/Skill.md +206 -0
- package/templates/skills/ekkOS_Plan_Assist/Skill.md +302 -0
- package/templates/skills/ekkOS_Preferences/Skill.md +247 -0
- package/templates/skills/ekkOS_Reflect/Skill.md +257 -0
- package/templates/skills/ekkOS_Safety/Skill.md +265 -0
- package/templates/skills/ekkOS_Schema/Skill.md +251 -0
- package/templates/skills/ekkOS_Summary/Skill.md +257 -0
- package/templates/skills/ekkOS_Vault/Skill.md +287 -0
- package/templates/skills/permissions/Skill.md +322 -0
- package/templates/spec-template.md +159 -0
- package/templates/windsurf-hooks/before-submit-prompt.sh +238 -0
- package/templates/windsurf-hooks/hooks.json +10 -0
- package/templates/windsurf-hooks/lib/contract.sh +320 -0
- package/templates/windsurf-rules/ekkos-memory.md +129 -0
|
@@ -0,0 +1,863 @@
|
|
|
1
|
+
# QA Agent
|
|
2
|
+
|
|
3
|
+
**ADMIN ONLY** - AI QA Engineer specialized in testing, quality assurance, test automation, coverage analysis, and ensuring production-ready code quality.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The QA Agent is your testing expert. It:
|
|
8
|
+
- Writes comprehensive test suites (unit, integration, E2E)
|
|
9
|
+
- Ensures high test coverage (90%+ target)
|
|
10
|
+
- Tests edge cases and error conditions
|
|
11
|
+
- Performs regression testing
|
|
12
|
+
- Reviews code quality and identifies bugs
|
|
13
|
+
- Automates testing workflows
|
|
14
|
+
|
|
15
|
+
## Commands
|
|
16
|
+
|
|
17
|
+
### `/qa write`
|
|
18
|
+
|
|
19
|
+
Write tests for a feature, component, or API.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
/qa write "Test target description"
|
|
23
|
+
|
|
24
|
+
# Examples
|
|
25
|
+
/qa write "Teams API endpoints"
|
|
26
|
+
/qa write "Teams dashboard component"
|
|
27
|
+
/qa write "Pattern search functionality"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**What happens:**
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
🧪 QA Agent: Writing Tests for "Teams API"
|
|
34
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
35
|
+
|
|
36
|
+
📊 Test Planning
|
|
37
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
38
|
+
|
|
39
|
+
Analyzing target...
|
|
40
|
+
✓ Found 5 API endpoints in apps/web/app/api/teams/
|
|
41
|
+
✓ Checked input validation schemas
|
|
42
|
+
✓ Reviewed RLS policies
|
|
43
|
+
✓ Identified edge cases
|
|
44
|
+
|
|
45
|
+
Test Types:
|
|
46
|
+
• Unit tests (validation, utilities)
|
|
47
|
+
• Integration tests (API endpoints)
|
|
48
|
+
• E2E tests (user workflows)
|
|
49
|
+
|
|
50
|
+
Test Cases: 28 total
|
|
51
|
+
• Happy path: 8
|
|
52
|
+
• Error handling: 12
|
|
53
|
+
• Edge cases: 6
|
|
54
|
+
• Security: 2
|
|
55
|
+
|
|
56
|
+
Coverage Goal: 95%
|
|
57
|
+
|
|
58
|
+
Stack:
|
|
59
|
+
• Vitest for unit/integration tests
|
|
60
|
+
• Playwright for E2E tests
|
|
61
|
+
• Testing Library for React components
|
|
62
|
+
• Supertest for API testing
|
|
63
|
+
|
|
64
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
65
|
+
📝 Writing Tests
|
|
66
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
67
|
+
|
|
68
|
+
Creating apps/web/app/api/teams/__tests__/route.test.ts...
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
72
|
+
import { NextRequest } from 'next/server';
|
|
73
|
+
import { POST, GET } from '../route';
|
|
74
|
+
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
|
|
75
|
+
|
|
76
|
+
// Mock Supabase
|
|
77
|
+
vi.mock('@supabase/auth-helpers-nextjs', () => ({
|
|
78
|
+
createRouteHandlerClient: vi.fn(),
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
vi.mock('next/headers', () => ({
|
|
82
|
+
cookies: vi.fn(() => ({})),
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
describe('POST /api/teams', () => {
|
|
86
|
+
let mockSupabase: any;
|
|
87
|
+
|
|
88
|
+
beforeEach(() => {
|
|
89
|
+
mockSupabase = {
|
|
90
|
+
auth: {
|
|
91
|
+
getUser: vi.fn(),
|
|
92
|
+
},
|
|
93
|
+
from: vi.fn(() => mockSupabase),
|
|
94
|
+
insert: vi.fn(() => mockSupabase),
|
|
95
|
+
select: vi.fn(() => mockSupabase),
|
|
96
|
+
single: vi.fn(),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
vi.mocked(createRouteHandlerClient).mockReturnValue(mockSupabase);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('Happy Path', () => {
|
|
103
|
+
it('should create a team with valid data', async () => {
|
|
104
|
+
// Arrange
|
|
105
|
+
const mockUser = { id: 'user-123' };
|
|
106
|
+
const mockTeam = {
|
|
107
|
+
id: 'team-123',
|
|
108
|
+
name: 'Engineering Team',
|
|
109
|
+
slug: 'engineering-team',
|
|
110
|
+
owner_id: 'user-123',
|
|
111
|
+
created_at: new Date().toISOString(),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
mockSupabase.auth.getUser.mockResolvedValue({
|
|
115
|
+
data: { user: mockUser },
|
|
116
|
+
error: null,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
mockSupabase.single.mockResolvedValue({
|
|
120
|
+
data: mockTeam,
|
|
121
|
+
error: null,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const request = new NextRequest('http://localhost/api/teams', {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
headers: { 'Content-Type': 'application/json' },
|
|
127
|
+
body: JSON.stringify({
|
|
128
|
+
name: 'Engineering Team',
|
|
129
|
+
description: 'Our amazing eng team',
|
|
130
|
+
}),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Act
|
|
134
|
+
const response = await POST(request);
|
|
135
|
+
const data = await response.json();
|
|
136
|
+
|
|
137
|
+
// Assert
|
|
138
|
+
expect(response.status).toBe(201);
|
|
139
|
+
expect(data).toEqual(mockTeam);
|
|
140
|
+
expect(mockSupabase.from).toHaveBeenCalledWith('teams');
|
|
141
|
+
expect(mockSupabase.insert).toHaveBeenCalledWith(
|
|
142
|
+
expect.objectContaining({
|
|
143
|
+
name: 'Engineering Team',
|
|
144
|
+
slug: 'engineering-team',
|
|
145
|
+
owner_id: 'user-123',
|
|
146
|
+
})
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should create team without optional fields', async () => {
|
|
151
|
+
const mockUser = { id: 'user-123' };
|
|
152
|
+
mockSupabase.auth.getUser.mockResolvedValue({
|
|
153
|
+
data: { user: mockUser },
|
|
154
|
+
error: null,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
mockSupabase.single.mockResolvedValue({
|
|
158
|
+
data: { id: 'team-123', name: 'Team', slug: 'team', owner_id: 'user-123' },
|
|
159
|
+
error: null,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const request = new NextRequest('http://localhost/api/teams', {
|
|
163
|
+
method: 'POST',
|
|
164
|
+
body: JSON.stringify({ name: 'Team' }),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const response = await POST(request);
|
|
168
|
+
|
|
169
|
+
expect(response.status).toBe(201);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('Authentication', () => {
|
|
174
|
+
it('should return 401 if user not authenticated', async () => {
|
|
175
|
+
mockSupabase.auth.getUser.mockResolvedValue({
|
|
176
|
+
data: { user: null },
|
|
177
|
+
error: { message: 'Not authenticated' },
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const request = new NextRequest('http://localhost/api/teams', {
|
|
181
|
+
method: 'POST',
|
|
182
|
+
body: JSON.stringify({ name: 'Team' }),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const response = await POST(request);
|
|
186
|
+
const data = await response.json();
|
|
187
|
+
|
|
188
|
+
expect(response.status).toBe(401);
|
|
189
|
+
expect(data.error).toBe('Unauthorized');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should return 401 if auth token expired', async () => {
|
|
193
|
+
mockSupabase.auth.getUser.mockResolvedValue({
|
|
194
|
+
data: { user: null },
|
|
195
|
+
error: { message: 'Token expired' },
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const request = new NextRequest('http://localhost/api/teams', {
|
|
199
|
+
method: 'POST',
|
|
200
|
+
body: JSON.stringify({ name: 'Team' }),
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const response = await POST(request);
|
|
204
|
+
|
|
205
|
+
expect(response.status).toBe(401);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe('Validation', () => {
|
|
210
|
+
beforeEach(() => {
|
|
211
|
+
mockSupabase.auth.getUser.mockResolvedValue({
|
|
212
|
+
data: { user: { id: 'user-123' } },
|
|
213
|
+
error: null,
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should reject team name shorter than 3 characters', async () => {
|
|
218
|
+
const request = new NextRequest('http://localhost/api/teams', {
|
|
219
|
+
method: 'POST',
|
|
220
|
+
body: JSON.stringify({ name: 'AB' }),
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const response = await POST(request);
|
|
224
|
+
const data = await response.json();
|
|
225
|
+
|
|
226
|
+
expect(response.status).toBe(400);
|
|
227
|
+
expect(data.error).toBe('Validation failed');
|
|
228
|
+
expect(data.details).toBeDefined();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should reject team name longer than 50 characters', async () => {
|
|
232
|
+
const longName = 'A'.repeat(51);
|
|
233
|
+
const request = new NextRequest('http://localhost/api/teams', {
|
|
234
|
+
method: 'POST',
|
|
235
|
+
body: JSON.stringify({ name: longName }),
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const response = await POST(request);
|
|
239
|
+
|
|
240
|
+
expect(response.status).toBe(400);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should reject team name with invalid characters', async () => {
|
|
244
|
+
const request = new NextRequest('http://localhost/api/teams', {
|
|
245
|
+
method: 'POST',
|
|
246
|
+
body: JSON.stringify({ name: 'Team@#$%' }),
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const response = await POST(request);
|
|
250
|
+
|
|
251
|
+
expect(response.status).toBe(400);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should reject invalid avatar URL', async () => {
|
|
255
|
+
const request = new NextRequest('http://localhost/api/teams', {
|
|
256
|
+
method: 'POST',
|
|
257
|
+
body: JSON.stringify({
|
|
258
|
+
name: 'Engineering Team',
|
|
259
|
+
avatar_url: 'not-a-url',
|
|
260
|
+
}),
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const response = await POST(request);
|
|
264
|
+
|
|
265
|
+
expect(response.status).toBe(400);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should reject empty request body', async () => {
|
|
269
|
+
const request = new NextRequest('http://localhost/api/teams', {
|
|
270
|
+
method: 'POST',
|
|
271
|
+
body: JSON.stringify({}),
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const response = await POST(request);
|
|
275
|
+
|
|
276
|
+
expect(response.status).toBe(400);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe('Edge Cases', () => {
|
|
281
|
+
beforeEach(() => {
|
|
282
|
+
mockSupabase.auth.getUser.mockResolvedValue({
|
|
283
|
+
data: { user: { id: 'user-123' } },
|
|
284
|
+
error: null,
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('should handle duplicate team name (409)', async () => {
|
|
289
|
+
mockSupabase.single.mockResolvedValue({
|
|
290
|
+
data: null,
|
|
291
|
+
error: { code: '23505', message: 'Duplicate key' },
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const request = new NextRequest('http://localhost/api/teams', {
|
|
295
|
+
method: 'POST',
|
|
296
|
+
body: JSON.stringify({ name: 'Engineering Team' }),
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const response = await POST(request);
|
|
300
|
+
const data = await response.json();
|
|
301
|
+
|
|
302
|
+
expect(response.status).toBe(409);
|
|
303
|
+
expect(data.error).toContain('already exists');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('should generate slug from name with special characters', async () => {
|
|
307
|
+
mockSupabase.single.mockResolvedValue({
|
|
308
|
+
data: {
|
|
309
|
+
id: 'team-123',
|
|
310
|
+
name: 'Team - Engineering & Design',
|
|
311
|
+
slug: 'team-engineering-design',
|
|
312
|
+
owner_id: 'user-123',
|
|
313
|
+
},
|
|
314
|
+
error: null,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const request = new NextRequest('http://localhost/api/teams', {
|
|
318
|
+
method: 'POST',
|
|
319
|
+
body: JSON.stringify({ name: 'Team - Engineering & Design' }),
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const response = await POST(request);
|
|
323
|
+
|
|
324
|
+
expect(response.status).toBe(201);
|
|
325
|
+
expect(mockSupabase.insert).toHaveBeenCalledWith(
|
|
326
|
+
expect.objectContaining({
|
|
327
|
+
slug: 'team-engineering-design',
|
|
328
|
+
})
|
|
329
|
+
);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('should handle database connection failure', async () => {
|
|
333
|
+
mockSupabase.single.mockResolvedValue({
|
|
334
|
+
data: null,
|
|
335
|
+
error: { message: 'Connection timeout', code: 'CONNECTION_ERROR' },
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const request = new NextRequest('http://localhost/api/teams', {
|
|
339
|
+
method: 'POST',
|
|
340
|
+
body: JSON.stringify({ name: 'Team' }),
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const response = await POST(request);
|
|
344
|
+
|
|
345
|
+
expect(response.status).toBe(500);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe('Security', () => {
|
|
350
|
+
it('should prevent SQL injection in team name', async () => {
|
|
351
|
+
mockSupabase.auth.getUser.mockResolvedValue({
|
|
352
|
+
data: { user: { id: 'user-123' } },
|
|
353
|
+
error: null,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const request = new NextRequest('http://localhost/api/teams', {
|
|
357
|
+
method: 'POST',
|
|
358
|
+
body: JSON.stringify({
|
|
359
|
+
name: "Team'; DROP TABLE teams; --",
|
|
360
|
+
}),
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const response = await POST(request);
|
|
364
|
+
|
|
365
|
+
// Should either validate or safely handle
|
|
366
|
+
expect([400, 201]).toContain(response.status);
|
|
367
|
+
// If 201, verify Supabase client escaped the input
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('should prevent XSS in team description', async () => {
|
|
371
|
+
mockSupabase.auth.getUser.mockResolvedValue({
|
|
372
|
+
data: { user: { id: 'user-123' } },
|
|
373
|
+
error: null,
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
mockSupabase.single.mockResolvedValue({
|
|
377
|
+
data: { id: 'team-123', name: 'Team', description: '<script>alert("xss")</script>' },
|
|
378
|
+
error: null,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const request = new NextRequest('http://localhost/api/teams', {
|
|
382
|
+
method: 'POST',
|
|
383
|
+
body: JSON.stringify({
|
|
384
|
+
name: 'Team',
|
|
385
|
+
description: '<script>alert("xss")</script>',
|
|
386
|
+
}),
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const response = await POST(request);
|
|
390
|
+
|
|
391
|
+
expect(response.status).toBe(201);
|
|
392
|
+
// Note: Frontend should sanitize when displaying
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
describe('GET /api/teams', () => {
|
|
398
|
+
// ... similar comprehensive tests for GET endpoint
|
|
399
|
+
// (abbreviated for space)
|
|
400
|
+
});
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
Creating apps/web/app/api/teams/__tests__/integration.test.ts...
|
|
404
|
+
|
|
405
|
+
```typescript
|
|
406
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
407
|
+
import { createClient } from '@supabase/supabase-js';
|
|
408
|
+
|
|
409
|
+
// Integration tests with real Supabase (test database)
|
|
410
|
+
const supabase = createClient(
|
|
411
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
412
|
+
process.env.SUPABASE_SERVICE_ROLE_KEY!
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
describe('Teams API Integration', () => {
|
|
416
|
+
let testUserId: string;
|
|
417
|
+
let testTeamId: string;
|
|
418
|
+
let authToken: string;
|
|
419
|
+
|
|
420
|
+
beforeAll(async () => {
|
|
421
|
+
// Create test user
|
|
422
|
+
const { data: { user } } = await supabase.auth.admin.createUser({
|
|
423
|
+
email: 'test@example.com',
|
|
424
|
+
password: 'test-password-123',
|
|
425
|
+
email_confirm: true,
|
|
426
|
+
});
|
|
427
|
+
testUserId = user!.id;
|
|
428
|
+
|
|
429
|
+
// Sign in to get auth token
|
|
430
|
+
const { data: { session } } = await supabase.auth.signInWithPassword({
|
|
431
|
+
email: 'test@example.com',
|
|
432
|
+
password: 'test-password-123',
|
|
433
|
+
});
|
|
434
|
+
authToken = session!.access_token;
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
afterAll(async () => {
|
|
438
|
+
// Cleanup: delete test data
|
|
439
|
+
if (testTeamId) {
|
|
440
|
+
await supabase.from('teams').delete().eq('id', testTeamId);
|
|
441
|
+
}
|
|
442
|
+
await supabase.auth.admin.deleteUser(testUserId);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('should create, read, update, and delete a team (CRUD)', async () => {
|
|
446
|
+
// CREATE
|
|
447
|
+
const createResponse = await fetch('http://localhost:3000/api/teams', {
|
|
448
|
+
method: 'POST',
|
|
449
|
+
headers: {
|
|
450
|
+
'Content-Type': 'application/json',
|
|
451
|
+
'Authorization': `Bearer ${authToken}`,
|
|
452
|
+
},
|
|
453
|
+
body: JSON.stringify({
|
|
454
|
+
name: 'Integration Test Team',
|
|
455
|
+
description: 'Created by integration test',
|
|
456
|
+
}),
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
expect(createResponse.status).toBe(201);
|
|
460
|
+
const createdTeam = await createResponse.json();
|
|
461
|
+
testTeamId = createdTeam.id;
|
|
462
|
+
|
|
463
|
+
expect(createdTeam.name).toBe('Integration Test Team');
|
|
464
|
+
expect(createdTeam.owner_id).toBe(testUserId);
|
|
465
|
+
|
|
466
|
+
// READ (list)
|
|
467
|
+
const listResponse = await fetch('http://localhost:3000/api/teams', {
|
|
468
|
+
headers: { 'Authorization': `Bearer ${authToken}` },
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
expect(listResponse.status).toBe(200);
|
|
472
|
+
const teams = await listResponse.json();
|
|
473
|
+
expect(teams).toContainEqual(expect.objectContaining({ id: testTeamId }));
|
|
474
|
+
|
|
475
|
+
// READ (single)
|
|
476
|
+
const getResponse = await fetch(`http://localhost:3000/api/teams/${testTeamId}`, {
|
|
477
|
+
headers: { 'Authorization': `Bearer ${authToken}` },
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
expect(getResponse.status).toBe(200);
|
|
481
|
+
const team = await getResponse.json();
|
|
482
|
+
expect(team.id).toBe(testTeamId);
|
|
483
|
+
|
|
484
|
+
// UPDATE
|
|
485
|
+
const updateResponse = await fetch(`http://localhost:3000/api/teams/${testTeamId}`, {
|
|
486
|
+
method: 'PATCH',
|
|
487
|
+
headers: {
|
|
488
|
+
'Content-Type': 'application/json',
|
|
489
|
+
'Authorization': `Bearer ${authToken}`,
|
|
490
|
+
},
|
|
491
|
+
body: JSON.stringify({
|
|
492
|
+
name: 'Updated Team Name',
|
|
493
|
+
}),
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
expect(updateResponse.status).toBe(200);
|
|
497
|
+
const updatedTeam = await updateResponse.json();
|
|
498
|
+
expect(updatedTeam.name).toBe('Updated Team Name');
|
|
499
|
+
|
|
500
|
+
// DELETE
|
|
501
|
+
const deleteResponse = await fetch(`http://localhost:3000/api/teams/${testTeamId}`, {
|
|
502
|
+
method: 'DELETE',
|
|
503
|
+
headers: { 'Authorization': `Bearer ${authToken}` },
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
expect(deleteResponse.status).toBe(200);
|
|
507
|
+
|
|
508
|
+
// Verify soft delete
|
|
509
|
+
const { data: deletedTeam } = await supabase
|
|
510
|
+
.from('teams')
|
|
511
|
+
.select('deleted_at')
|
|
512
|
+
.eq('id', testTeamId)
|
|
513
|
+
.single();
|
|
514
|
+
|
|
515
|
+
expect(deletedTeam.deleted_at).not.toBeNull();
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it('should enforce RLS policies (no access to other users teams)', async () => {
|
|
519
|
+
// Create second user
|
|
520
|
+
const { data: { user: user2 } } = await supabase.auth.admin.createUser({
|
|
521
|
+
email: 'test2@example.com',
|
|
522
|
+
password: 'test-password-456',
|
|
523
|
+
email_confirm: true,
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
const { data: { session: session2 } } = await supabase.auth.signInWithPassword({
|
|
527
|
+
email: 'test2@example.com',
|
|
528
|
+
password: 'test-password-456',
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// Try to access user1's team with user2's token
|
|
532
|
+
const response = await fetch(`http://localhost:3000/api/teams/${testTeamId}`, {
|
|
533
|
+
headers: { 'Authorization': `Bearer ${session2!.access_token}` },
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
expect(response.status).toBe(404); // RLS blocks access
|
|
537
|
+
|
|
538
|
+
// Cleanup user2
|
|
539
|
+
await supabase.auth.admin.deleteUser(user2!.id);
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
545
|
+
✅ Tests Complete
|
|
546
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
547
|
+
|
|
548
|
+
Test Files Created: 2
|
|
549
|
+
✓ route.test.ts (unit tests)
|
|
550
|
+
✓ integration.test.ts (integration tests)
|
|
551
|
+
|
|
552
|
+
Test Cases Written: 28
|
|
553
|
+
✓ Happy path: 8 tests
|
|
554
|
+
✓ Authentication: 2 tests
|
|
555
|
+
✓ Validation: 5 tests
|
|
556
|
+
✓ Edge cases: 3 tests
|
|
557
|
+
✓ Security: 2 tests
|
|
558
|
+
✓ Integration: 8 tests
|
|
559
|
+
|
|
560
|
+
Coverage: 96% (exceeds 90% goal) ✅
|
|
561
|
+
|
|
562
|
+
Running tests...
|
|
563
|
+
|
|
564
|
+
```bash
|
|
565
|
+
$ npm run test apps/web/app/api/teams
|
|
566
|
+
|
|
567
|
+
✓ apps/web/app/api/teams/__tests__/route.test.ts (28 tests)
|
|
568
|
+
✓ POST /api/teams (18 tests) 1245ms
|
|
569
|
+
✓ Happy Path (2 tests) 145ms
|
|
570
|
+
✓ Authentication (2 tests) 89ms
|
|
571
|
+
✓ Validation (5 tests) 234ms
|
|
572
|
+
✓ Edge Cases (3 tests) 178ms
|
|
573
|
+
✓ Security (2 tests) 123ms
|
|
574
|
+
✓ GET /api/teams (10 tests) 567ms
|
|
575
|
+
|
|
576
|
+
✓ apps/web/app/api/teams/__tests__/integration.test.ts (2 tests) 3456ms
|
|
577
|
+
✓ Teams API Integration (2 tests) 3456ms
|
|
578
|
+
|
|
579
|
+
Test Files: 2 passed (2)
|
|
580
|
+
Tests: 30 passed (30)
|
|
581
|
+
Duration: 4.8s
|
|
582
|
+
|
|
583
|
+
Coverage:
|
|
584
|
+
apps/web/app/api/teams/route.ts: 96.42% (54/56 lines)
|
|
585
|
+
apps/web/app/api/teams/[id]/route.ts: 95.83% (46/48 lines)
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
589
|
+
|
|
590
|
+
Next Steps:
|
|
591
|
+
• Run full test suite: npm run test
|
|
592
|
+
• Add E2E tests: /qa e2e "Teams workflow"
|
|
593
|
+
• Review code quality: /lead review all
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
### `/qa test`
|
|
597
|
+
|
|
598
|
+
Run existing tests and analyze results.
|
|
599
|
+
|
|
600
|
+
```bash
|
|
601
|
+
/qa test
|
|
602
|
+
|
|
603
|
+
# Run specific tests
|
|
604
|
+
/qa test "teams"
|
|
605
|
+
|
|
606
|
+
# Run with coverage
|
|
607
|
+
/qa test --coverage
|
|
608
|
+
|
|
609
|
+
# Run integration tests only
|
|
610
|
+
/qa test --integration
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
**What happens:**
|
|
614
|
+
|
|
615
|
+
```
|
|
616
|
+
🧪 QA Agent: Running Tests
|
|
617
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
618
|
+
|
|
619
|
+
Running test suite...
|
|
620
|
+
|
|
621
|
+
✓ apps/web/app/api/teams (30 tests) 4821ms
|
|
622
|
+
✓ apps/web/app/api/patterns (45 tests) 5234ms
|
|
623
|
+
✓ apps/web/components/teams (18 tests) 2345ms
|
|
624
|
+
✓ apps/memory/lib/brain (67 tests) 8934ms
|
|
625
|
+
|
|
626
|
+
Test Files: 12 passed (12)
|
|
627
|
+
Tests: 412 passed (412)
|
|
628
|
+
Duration: 32.4s
|
|
629
|
+
|
|
630
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
631
|
+
📊 Coverage Report
|
|
632
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
633
|
+
|
|
634
|
+
Overall Coverage: 94.2% ✅ (meets 90% threshold)
|
|
635
|
+
|
|
636
|
+
By Directory:
|
|
637
|
+
apps/web/app/api/ 96.8% ✅
|
|
638
|
+
apps/web/components/ 92.3% ✅
|
|
639
|
+
apps/memory/lib/brain/ 95.1% ✅
|
|
640
|
+
packages/shared/ 89.4% 🟡 (below threshold)
|
|
641
|
+
|
|
642
|
+
Uncovered Lines: 24
|
|
643
|
+
Most critical:
|
|
644
|
+
• apps/web/lib/auth/refresh.ts:45-48 (error recovery)
|
|
645
|
+
• packages/shared/utils/retry.ts:67-70 (timeout handling)
|
|
646
|
+
|
|
647
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
648
|
+
✅ All Tests Passed
|
|
649
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
650
|
+
|
|
651
|
+
Status: Ready for deployment ✅
|
|
652
|
+
|
|
653
|
+
Recommendations:
|
|
654
|
+
• Increase coverage in packages/shared (currently 89.4%)
|
|
655
|
+
• Add tests for error recovery in auth/refresh.ts
|
|
656
|
+
• Consider adding performance tests for memory/lib/brain
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
### `/qa regression`
|
|
660
|
+
|
|
661
|
+
Run regression tests to ensure no existing functionality broke.
|
|
662
|
+
|
|
663
|
+
```bash
|
|
664
|
+
/qa regression
|
|
665
|
+
|
|
666
|
+
# Test specific area
|
|
667
|
+
/qa regression "authentication"
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
### `/qa e2e`
|
|
671
|
+
|
|
672
|
+
Write and run end-to-end tests for user workflows.
|
|
673
|
+
|
|
674
|
+
```bash
|
|
675
|
+
/qa e2e "User workflow description"
|
|
676
|
+
|
|
677
|
+
# Examples
|
|
678
|
+
/qa e2e "Create team and invite member"
|
|
679
|
+
/qa e2e "Sign up, forge pattern, verify in dashboard"
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
**What happens:**
|
|
683
|
+
|
|
684
|
+
```
|
|
685
|
+
🧪 QA Agent: E2E Test for "Create Team Workflow"
|
|
686
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
687
|
+
|
|
688
|
+
Creating e2e/teams/create-team.spec.ts...
|
|
689
|
+
|
|
690
|
+
```typescript
|
|
691
|
+
import { test, expect } from '@playwright/test';
|
|
692
|
+
|
|
693
|
+
test.describe('Create Team Workflow', () => {
|
|
694
|
+
test.beforeEach(async ({ page }) => {
|
|
695
|
+
// Sign in
|
|
696
|
+
await page.goto('http://localhost:3000/login');
|
|
697
|
+
await page.fill('[name="email"]', 'test@example.com');
|
|
698
|
+
await page.fill('[name="password"]', 'test-password');
|
|
699
|
+
await page.click('button[type="submit"]');
|
|
700
|
+
|
|
701
|
+
await expect(page).toHaveURL('/dashboard');
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
test('should create a team successfully', async ({ page }) => {
|
|
705
|
+
// Navigate to teams
|
|
706
|
+
await page.goto('http://localhost:3000/teams');
|
|
707
|
+
|
|
708
|
+
// Click create team button
|
|
709
|
+
await page.click('button:has-text("Create Team")');
|
|
710
|
+
|
|
711
|
+
// Fill form
|
|
712
|
+
await page.fill('[name="name"]', 'E2E Test Team');
|
|
713
|
+
await page.fill('[name="description"]', 'Created by E2E test');
|
|
714
|
+
|
|
715
|
+
// Submit
|
|
716
|
+
await page.click('button[type="submit"]:has-text("Create Team")');
|
|
717
|
+
|
|
718
|
+
// Verify success
|
|
719
|
+
await expect(page.locator('text=Team created successfully')).toBeVisible();
|
|
720
|
+
await expect(page.locator('text=E2E Test Team')).toBeVisible();
|
|
721
|
+
|
|
722
|
+
// Verify in database
|
|
723
|
+
const teamName = await page.locator('[data-testid="team-name"]').textContent();
|
|
724
|
+
expect(teamName).toBe('E2E Test Team');
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
test('should show validation error for short name', async ({ page }) => {
|
|
728
|
+
await page.goto('http://localhost:3000/teams');
|
|
729
|
+
await page.click('button:has-text("Create Team")');
|
|
730
|
+
|
|
731
|
+
await page.fill('[name="name"]', 'AB'); // Too short
|
|
732
|
+
await page.click('button[type="submit"]');
|
|
733
|
+
|
|
734
|
+
await expect(page.locator('text=Team name must be at least 3 characters')).toBeVisible();
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
test('should close modal on cancel', async ({ page }) => {
|
|
738
|
+
await page.goto('http://localhost:3000/teams');
|
|
739
|
+
await page.click('button:has-text("Create Team")');
|
|
740
|
+
|
|
741
|
+
await expect(page.locator('role=dialog')).toBeVisible();
|
|
742
|
+
|
|
743
|
+
await page.click('button:has-text("Cancel")');
|
|
744
|
+
|
|
745
|
+
await expect(page.locator('role=dialog')).not.toBeVisible();
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
Running E2E tests with Playwright...
|
|
751
|
+
|
|
752
|
+
```bash
|
|
753
|
+
$ npx playwright test e2e/teams/create-team.spec.ts
|
|
754
|
+
|
|
755
|
+
Running 3 tests using 1 worker
|
|
756
|
+
|
|
757
|
+
✓ Create Team Workflow > should create a team successfully (2.3s)
|
|
758
|
+
✓ Create Team Workflow > should show validation error for short name (1.1s)
|
|
759
|
+
✓ Create Team Workflow > should close modal on cancel (0.8s)
|
|
760
|
+
|
|
761
|
+
3 passed (4.2s)
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
765
|
+
✅ E2E Tests Complete
|
|
766
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
767
|
+
|
|
768
|
+
Test File: e2e/teams/create-team.spec.ts
|
|
769
|
+
Tests: 3 passed (3)
|
|
770
|
+
Duration: 4.2s
|
|
771
|
+
|
|
772
|
+
All user workflows verified ✅
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
## MCP Tools Used
|
|
776
|
+
|
|
777
|
+
The QA Agent uses:
|
|
778
|
+
|
|
779
|
+
- `ekkOS_Search` - Find similar test patterns
|
|
780
|
+
- `ekkOS_Codebase` - Search code to understand what to test
|
|
781
|
+
- `ekkOS_Forge` - Save testing patterns
|
|
782
|
+
- `Read` - Read code to test
|
|
783
|
+
- `Write` - Write test files
|
|
784
|
+
- `Bash` - Run test commands (npm test, playwright)
|
|
785
|
+
- `supabase_execute_sql` - Setup test data
|
|
786
|
+
|
|
787
|
+
## Best Practices
|
|
788
|
+
|
|
789
|
+
### Test the Unhappy Path
|
|
790
|
+
|
|
791
|
+
```typescript
|
|
792
|
+
// ✅ Good: Tests both happy and unhappy paths
|
|
793
|
+
it('should create team with valid data', async () => { /* ... */ });
|
|
794
|
+
it('should reject invalid data', async () => { /* ... */ });
|
|
795
|
+
it('should handle database errors', async () => { /* ... */ });
|
|
796
|
+
|
|
797
|
+
// ❌ Bad: Only tests happy path
|
|
798
|
+
it('should create team', async () => { /* ... */ });
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
### Use Descriptive Test Names
|
|
802
|
+
|
|
803
|
+
```typescript
|
|
804
|
+
// ✅ Good: Clear what is being tested
|
|
805
|
+
it('should return 401 if user not authenticated', async () => { /* ... */ });
|
|
806
|
+
|
|
807
|
+
// ❌ Bad: Unclear
|
|
808
|
+
it('should work', async () => { /* ... */ });
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
### Test Edge Cases
|
|
812
|
+
|
|
813
|
+
```typescript
|
|
814
|
+
// ✅ Good: Tests edge cases
|
|
815
|
+
it('should handle duplicate team name', async () => { /* ... */ });
|
|
816
|
+
it('should handle database connection timeout', async () => { /* ... */ });
|
|
817
|
+
it('should prevent SQL injection', async () => { /* ... */ });
|
|
818
|
+
|
|
819
|
+
// ❌ Bad: Only basic scenarios
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
## Integration with Other Agents
|
|
823
|
+
|
|
824
|
+
QA Agent works closely with:
|
|
825
|
+
|
|
826
|
+
- **Frontend Agent** - Tests UI components
|
|
827
|
+
- **Backend Agent** - Tests APIs and database
|
|
828
|
+
- **Tech Lead** - Reports test results for reviews
|
|
829
|
+
|
|
830
|
+
## Troubleshooting
|
|
831
|
+
|
|
832
|
+
### Tests Failing
|
|
833
|
+
|
|
834
|
+
**Problem:** Tests failing unexpectedly
|
|
835
|
+
**Check:** `/qa test` to see failures
|
|
836
|
+
**Fix:** Agent analyzes failures and suggests fixes
|
|
837
|
+
|
|
838
|
+
### Low Coverage
|
|
839
|
+
|
|
840
|
+
**Problem:** Coverage below 90% threshold
|
|
841
|
+
**Check:** Coverage report
|
|
842
|
+
**Fix:** Agent writes tests for uncovered lines
|
|
843
|
+
|
|
844
|
+
---
|
|
845
|
+
|
|
846
|
+
## Summary
|
|
847
|
+
|
|
848
|
+
The QA Agent is your testing expert that:
|
|
849
|
+
|
|
850
|
+
✅ **Writes** - Comprehensive test suites
|
|
851
|
+
✅ **Runs** - Tests with coverage analysis
|
|
852
|
+
✅ **Validates** - Edge cases and security
|
|
853
|
+
✅ **Reports** - Quality metrics and issues
|
|
854
|
+
|
|
855
|
+
**Ship with confidence.**
|
|
856
|
+
|
|
857
|
+
```bash
|
|
858
|
+
/qa write "Your feature here"
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
---
|
|
862
|
+
|
|
863
|
+
**Test early. Test often. Ship quality.** 🧪
|