specweave 1.0.240 → 1.0.241

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/CLAUDE.md +3 -4
  2. package/dist/plugins/specweave-ado/lib/ado-permission-gate.d.ts.map +1 -1
  3. package/dist/plugins/specweave-ado/lib/ado-permission-gate.js +17 -2
  4. package/dist/plugins/specweave-ado/lib/ado-permission-gate.js.map +1 -1
  5. package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts +7 -0
  6. package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts.map +1 -1
  7. package/dist/plugins/specweave-github/lib/github-feature-sync.js +53 -0
  8. package/dist/plugins/specweave-github/lib/github-feature-sync.js.map +1 -1
  9. package/dist/plugins/specweave-jira/lib/jira-permission-gate.d.ts.map +1 -1
  10. package/dist/plugins/specweave-jira/lib/jira-permission-gate.js +17 -2
  11. package/dist/plugins/specweave-jira/lib/jira-permission-gate.js.map +1 -1
  12. package/dist/plugins/specweave-testing/lib/playwright-routing.d.ts +8 -0
  13. package/dist/plugins/specweave-testing/lib/playwright-routing.d.ts.map +1 -1
  14. package/dist/plugins/specweave-testing/lib/playwright-routing.js +10 -7
  15. package/dist/plugins/specweave-testing/lib/playwright-routing.js.map +1 -1
  16. package/dist/src/adapters/agents-md-generator.js +1 -1
  17. package/dist/src/adapters/agents-md-generator.js.map +1 -1
  18. package/dist/src/adapters/claude/README.md +1 -1
  19. package/dist/src/adapters/claude-md-generator.js +1 -1
  20. package/dist/src/adapters/claude-md-generator.js.map +1 -1
  21. package/dist/src/cli/commands/init.js +2 -1
  22. package/dist/src/cli/commands/init.js.map +1 -1
  23. package/dist/src/cli/commands/refresh-marketplace.d.ts.map +1 -1
  24. package/dist/src/cli/commands/refresh-marketplace.js +7 -67
  25. package/dist/src/cli/commands/refresh-marketplace.js.map +1 -1
  26. package/dist/src/cli/commands/team.d.ts.map +1 -1
  27. package/dist/src/cli/commands/team.js +8 -5
  28. package/dist/src/cli/commands/team.js.map +1 -1
  29. package/dist/src/cli/helpers/init/plugin-installer.d.ts.map +1 -1
  30. package/dist/src/cli/helpers/init/plugin-installer.js +9 -13
  31. package/dist/src/cli/helpers/init/plugin-installer.js.map +1 -1
  32. package/dist/src/cli/helpers/issue-tracker/index.d.ts.map +1 -1
  33. package/dist/src/cli/helpers/issue-tracker/index.js +12 -6
  34. package/dist/src/cli/helpers/issue-tracker/index.js.map +1 -1
  35. package/dist/src/cli/helpers/issue-tracker/types.d.ts +2 -0
  36. package/dist/src/cli/helpers/issue-tracker/types.d.ts.map +1 -1
  37. package/dist/src/cli/helpers/issue-tracker/types.js.map +1 -1
  38. package/dist/src/core/increment/discipline-checker.js +1 -1
  39. package/dist/src/core/increment/discipline-checker.js.map +1 -1
  40. package/dist/src/core/increment/status-commands.d.ts.map +1 -1
  41. package/dist/src/core/increment/status-commands.js +7 -0
  42. package/dist/src/core/increment/status-commands.js.map +1 -1
  43. package/dist/src/core/lazy-loading/llm-plugin-detector.d.ts +2 -2
  44. package/dist/src/core/lazy-loading/llm-plugin-detector.d.ts.map +1 -1
  45. package/dist/src/core/lazy-loading/llm-plugin-detector.js +63 -25
  46. package/dist/src/core/lazy-loading/llm-plugin-detector.js.map +1 -1
  47. package/dist/src/core/reflection/reflect-handler.js +2 -2
  48. package/dist/src/core/reflection/reflect-handler.js.map +1 -1
  49. package/dist/src/core/session/handoff-context.js +2 -2
  50. package/dist/src/core/session/handoff-context.js.map +1 -1
  51. package/dist/src/sync/ado-reconciler.d.ts.map +1 -1
  52. package/dist/src/sync/ado-reconciler.js +21 -2
  53. package/dist/src/sync/ado-reconciler.js.map +1 -1
  54. package/dist/src/sync/github-reconciler.d.ts.map +1 -1
  55. package/dist/src/sync/github-reconciler.js +52 -26
  56. package/dist/src/sync/github-reconciler.js.map +1 -1
  57. package/dist/src/sync/jira-reconciler.d.ts.map +1 -1
  58. package/dist/src/sync/jira-reconciler.js +16 -3
  59. package/dist/src/sync/jira-reconciler.js.map +1 -1
  60. package/dist/src/sync/sync-coordinator.d.ts.map +1 -1
  61. package/dist/src/sync/sync-coordinator.js +31 -6
  62. package/dist/src/sync/sync-coordinator.js.map +1 -1
  63. package/dist/src/utils/auto-install.js +4 -4
  64. package/dist/src/utils/auto-install.js.map +1 -1
  65. package/package.json +1 -1
  66. package/plugins/FINAL-AUDIT-RECOMMENDATIONS.md +3 -3
  67. package/plugins/SKILLS-VS-AGENTS.md +1 -1
  68. package/plugins/specweave/PLUGIN.md +0 -2
  69. package/plugins/specweave/commands/export-skills.md +1 -1
  70. package/plugins/specweave/commands/role-orchestrator.md +1 -1
  71. package/plugins/specweave/hooks/log-decision.sh +6 -0
  72. package/plugins/specweave/hooks/stop-auto-v5.sh +17 -1
  73. package/plugins/specweave/hooks/stop-reflect.sh +16 -2
  74. package/plugins/specweave/hooks/stop-sync.sh +17 -9
  75. package/plugins/specweave/hooks/user-prompt-submit.sh +103 -35
  76. package/plugins/specweave/lib/vendor/sync/github-reconciler.js +52 -26
  77. package/plugins/specweave/lib/vendor/sync/github-reconciler.js.map +1 -1
  78. package/plugins/specweave/skills/code-review/SKILL.md +608 -0
  79. package/plugins/specweave/skills/done/SKILL.md +1 -1
  80. package/plugins/specweave/skills/grill/SKILL.md +91 -0
  81. package/plugins/specweave/skills/performance/SKILL.md +6 -0
  82. package/plugins/specweave/skills/security/SKILL.md +7 -0
  83. package/plugins/specweave/skills/security-patterns/SKILL.md +6 -0
  84. package/plugins/specweave/skills/tdd-orchestrator/SKILL.md +1 -1
  85. package/plugins/specweave/skills/team-build/SKILL.md +1 -1
  86. package/plugins/specweave/skills/team-orchestrate/SKILL.md +1 -1
  87. package/plugins/specweave/skills/tech-lead/SKILL.md +7 -0
  88. package/plugins/specweave-ado/lib/ado-permission-gate.js +18 -2
  89. package/plugins/specweave-ado/lib/ado-permission-gate.ts +19 -2
  90. package/plugins/specweave-frontend/skills/frontend/SKILL.md +138 -2
  91. package/plugins/specweave-frontend/skills/i18n-expert/SKILL.md +989 -0
  92. package/plugins/specweave-github/hooks/github-auto-create-handler.sh +23 -1
  93. package/plugins/specweave-github/lib/github-feature-sync.js +41 -0
  94. package/plugins/specweave-github/lib/github-feature-sync.ts +62 -0
  95. package/plugins/specweave-infrastructure/PLUGIN.md +2 -1
  96. package/plugins/specweave-infrastructure/skills/gcp-deep-dive/SKILL.md +1172 -0
  97. package/plugins/specweave-infrastructure/skills/observability/SKILL.md +6 -0
  98. package/plugins/specweave-infrastructure/skills/opentelemetry/SKILL.md +6 -0
  99. package/plugins/specweave-jira/lib/jira-permission-gate.js +18 -2
  100. package/plugins/specweave-jira/lib/jira-permission-gate.ts +19 -2
  101. package/plugins/specweave-mobile/PLUGIN.md +1 -2
  102. package/plugins/specweave-mobile/README.md +13 -12
  103. package/plugins/specweave-mobile/skills/capacitor-ionic/SKILL.md +4 -18
  104. package/plugins/specweave-mobile/skills/deep-linking-push/SKILL.md +4 -22
  105. package/plugins/specweave-mobile/skills/expo/SKILL.md +4 -24
  106. package/plugins/specweave-mobile/skills/mobile-testing/SKILL.md +4 -22
  107. package/plugins/specweave-mobile/skills/react-native-expert/SKILL.md +404 -47
  108. package/plugins/specweave-testing/PLUGIN.md +3 -11
  109. package/plugins/specweave-testing/lib/playwright-routing.js +1 -6
  110. package/plugins/specweave-testing/lib/playwright-routing.ts +11 -8
  111. package/plugins/specweave-testing/skills/accessibility-testing/SKILL.md +998 -0
  112. package/plugins/specweave-testing/skills/e2e-testing/SKILL.md +1 -1
  113. package/plugins/specweave-testing/skills/mutation-testing/SKILL.md +769 -0
  114. package/plugins/specweave-testing/skills/performance-testing/SKILL.md +961 -0
  115. package/plugins/specweave/.specweave/logs/decisions.jsonl +0 -12
  116. package/plugins/specweave/.specweave/logs/reflect/reflect.log +0 -8
  117. package/plugins/specweave/.specweave/logs/stop-auto.log +0 -6
  118. package/plugins/specweave/.specweave/logs/stop-sync.log +0 -10
  119. package/plugins/specweave/.specweave/state/dashboard.json +0 -43
  120. package/plugins/specweave/skills/infrastructure/SKILL.md +0 -86
  121. package/plugins/specweave/skills/qa-lead/SKILL.md +0 -77
  122. package/plugins/specweave-mobile/skills/mobile-architect/SKILL.md +0 -30
  123. package/plugins/specweave-testing/commands/e2e-setup.md +0 -1103
  124. package/plugins/specweave-testing/commands/test-coverage.md +0 -983
  125. package/plugins/specweave-testing/commands/test-generate.md +0 -1160
  126. package/plugins/specweave-testing/commands/test-init.md +0 -413
  127. package/plugins/specweave-testing/commands/ui-automate.md +0 -182
  128. package/plugins/specweave-testing/commands/ui-inspect.md +0 -82
@@ -1,1160 +0,0 @@
1
- ---
2
- description: Generate comprehensive unit, integration, and E2E tests from components, functions, and API endpoints.
3
- ---
4
-
5
- # /sw-testing:test-generate
6
-
7
- Generate comprehensive unit, integration, and E2E tests from components, functions, and API endpoints.
8
-
9
- You are an expert test generation engineer who creates thorough, production-ready test suites.
10
-
11
- ## Your Task
12
-
13
- Analyze code and automatically generate complete test coverage with unit tests, integration tests, and E2E tests.
14
-
15
- ### 1. Test Generation Strategy
16
-
17
- **Unit Tests**:
18
- - Test individual functions/methods in isolation
19
- - Mock all external dependencies
20
- - Cover edge cases and error conditions
21
- - Achieve 100% branch coverage
22
- - Fast execution (<10ms per test)
23
-
24
- **Integration Tests**:
25
- - Test component interactions
26
- - Use real dependencies where practical
27
- - Test data flow between modules
28
- - Verify API contracts
29
- - Database integration testing
30
-
31
- **E2E Tests**:
32
- - Test complete user workflows
33
- - Verify real browser behavior
34
- - Test authentication flows
35
- - Check responsive design
36
- - Validate accessibility
37
-
38
- ### 2. Unit Test Generation
39
-
40
- **Function Analysis Example**:
41
- ```typescript
42
- // Source: src/utils/formatters.ts
43
- export function formatCurrency(
44
- amount: number,
45
- currency: string = 'USD',
46
- locale: string = 'en-US'
47
- ): string {
48
- if (typeof amount !== 'number' || isNaN(amount)) {
49
- throw new Error('Invalid amount: must be a number');
50
- }
51
-
52
- if (amount < 0) {
53
- throw new Error('Invalid amount: must be non-negative');
54
- }
55
-
56
- return new Intl.NumberFormat(locale, {
57
- style: 'currency',
58
- currency,
59
- }).format(amount);
60
- }
61
- ```
62
-
63
- **Generated Unit Test**:
64
- ```typescript
65
- // Generated: tests/unit/utils/formatters.test.ts
66
- import { describe, it, expect } from 'vitest';
67
- import { formatCurrency } from '@/utils/formatters';
68
-
69
- describe('formatCurrency', () => {
70
- describe('valid inputs', () => {
71
- it('should format USD currency with default locale', () => {
72
- expect(formatCurrency(1234.56)).toBe('$1,234.56');
73
- });
74
-
75
- it('should format EUR currency', () => {
76
- expect(formatCurrency(1234.56, 'EUR', 'de-DE')).toBe('1.234,56 €');
77
- });
78
-
79
- it('should format GBP currency', () => {
80
- expect(formatCurrency(1234.56, 'GBP', 'en-GB')).toBe('£1,234.56');
81
- });
82
-
83
- it('should handle zero amount', () => {
84
- expect(formatCurrency(0)).toBe('$0.00');
85
- });
86
-
87
- it('should handle large amounts', () => {
88
- expect(formatCurrency(1000000)).toBe('$1,000,000.00');
89
- });
90
-
91
- it('should handle small amounts', () => {
92
- expect(formatCurrency(0.01)).toBe('$0.01');
93
- });
94
-
95
- it('should handle floating point precision', () => {
96
- expect(formatCurrency(0.1 + 0.2)).toBe('$0.30');
97
- });
98
- });
99
-
100
- describe('edge cases', () => {
101
- it('should throw error for NaN', () => {
102
- expect(() => formatCurrency(NaN)).toThrow('Invalid amount: must be a number');
103
- });
104
-
105
- it('should throw error for non-number input', () => {
106
- expect(() => formatCurrency('123' as any)).toThrow('Invalid amount: must be a number');
107
- });
108
-
109
- it('should throw error for negative amounts', () => {
110
- expect(() => formatCurrency(-100)).toThrow('Invalid amount: must be non-negative');
111
- });
112
-
113
- it('should throw error for undefined', () => {
114
- expect(() => formatCurrency(undefined as any)).toThrow('Invalid amount: must be a number');
115
- });
116
-
117
- it('should throw error for null', () => {
118
- expect(() => formatCurrency(null as any)).toThrow('Invalid amount: must be a number');
119
- });
120
- });
121
-
122
- describe('locale variations', () => {
123
- it('should use French locale', () => {
124
- expect(formatCurrency(1234.56, 'EUR', 'fr-FR')).toContain('1');
125
- });
126
-
127
- it('should use Japanese locale', () => {
128
- expect(formatCurrency(1234, 'JPY', 'ja-JP')).toContain('¥');
129
- });
130
- });
131
- });
132
- ```
133
-
134
- ### 3. Component Test Generation
135
-
136
- **React Component Analysis**:
137
- ```typescript
138
- // Source: src/components/Button.tsx
139
- interface ButtonProps {
140
- children: React.ReactNode;
141
- onClick?: () => void;
142
- variant?: 'primary' | 'secondary' | 'danger';
143
- disabled?: boolean;
144
- loading?: boolean;
145
- icon?: React.ReactNode;
146
- fullWidth?: boolean;
147
- }
148
-
149
- export function Button({
150
- children,
151
- onClick,
152
- variant = 'primary',
153
- disabled = false,
154
- loading = false,
155
- icon,
156
- fullWidth = false,
157
- }: ButtonProps) {
158
- return (
159
- <button
160
- onClick={onClick}
161
- disabled={disabled || loading}
162
- className={`btn btn-${variant} ${fullWidth ? 'w-full' : ''}`}
163
- data-testid="button"
164
- >
165
- {loading && <Spinner />}
166
- {icon && <span className="icon">{icon}</span>}
167
- {children}
168
- </button>
169
- );
170
- }
171
- ```
172
-
173
- **Generated Component Test**:
174
- ```typescript
175
- // Generated: tests/unit/components/Button.test.tsx
176
- import { describe, it, expect, vi } from 'vitest';
177
- import { render, screen, fireEvent } from '@/tests/utils/test-utils';
178
- import { Button } from '@/components/Button';
179
-
180
- describe('Button', () => {
181
- describe('rendering', () => {
182
- it('should render button with text', () => {
183
- render(<Button>Click me</Button>);
184
- expect(screen.getByText('Click me')).toBeInTheDocument();
185
- });
186
-
187
- it('should render primary variant by default', () => {
188
- render(<Button>Click me</Button>);
189
- expect(screen.getByTestId('button')).toHaveClass('btn-primary');
190
- });
191
-
192
- it('should render secondary variant', () => {
193
- render(<Button variant="secondary">Click me</Button>);
194
- expect(screen.getByTestId('button')).toHaveClass('btn-secondary');
195
- });
196
-
197
- it('should render danger variant', () => {
198
- render(<Button variant="danger">Delete</Button>);
199
- expect(screen.getByTestId('button')).toHaveClass('btn-danger');
200
- });
201
-
202
- it('should render with icon', () => {
203
- const icon = <span data-testid="custom-icon">📧</span>;
204
- render(<Button icon={icon}>Send</Button>);
205
- expect(screen.getByTestId('custom-icon')).toBeInTheDocument();
206
- });
207
-
208
- it('should render full width', () => {
209
- render(<Button fullWidth>Click me</Button>);
210
- expect(screen.getByTestId('button')).toHaveClass('w-full');
211
- });
212
- });
213
-
214
- describe('interactions', () => {
215
- it('should call onClick when clicked', () => {
216
- const handleClick = vi.fn();
217
- render(<Button onClick={handleClick}>Click me</Button>);
218
-
219
- fireEvent.click(screen.getByTestId('button'));
220
- expect(handleClick).toHaveBeenCalledTimes(1);
221
- });
222
-
223
- it('should not call onClick when disabled', () => {
224
- const handleClick = vi.fn();
225
- render(<Button onClick={handleClick} disabled>Click me</Button>);
226
-
227
- fireEvent.click(screen.getByTestId('button'));
228
- expect(handleClick).not.toHaveBeenCalled();
229
- });
230
-
231
- it('should not call onClick when loading', () => {
232
- const handleClick = vi.fn();
233
- render(<Button onClick={handleClick} loading>Click me</Button>);
234
-
235
- fireEvent.click(screen.getByTestId('button'));
236
- expect(handleClick).not.toHaveBeenCalled();
237
- });
238
- });
239
-
240
- describe('states', () => {
241
- it('should be disabled when disabled prop is true', () => {
242
- render(<Button disabled>Click me</Button>);
243
- expect(screen.getByTestId('button')).toBeDisabled();
244
- });
245
-
246
- it('should be disabled when loading', () => {
247
- render(<Button loading>Click me</Button>);
248
- expect(screen.getByTestId('button')).toBeDisabled();
249
- });
250
-
251
- it('should show spinner when loading', () => {
252
- render(<Button loading>Click me</Button>);
253
- expect(screen.getByTestId('spinner')).toBeInTheDocument();
254
- });
255
- });
256
-
257
- describe('accessibility', () => {
258
- it('should have button role', () => {
259
- render(<Button>Click me</Button>);
260
- expect(screen.getByRole('button')).toBeInTheDocument();
261
- });
262
-
263
- it('should be keyboard accessible', () => {
264
- const handleClick = vi.fn();
265
- render(<Button onClick={handleClick}>Click me</Button>);
266
-
267
- const button = screen.getByTestId('button');
268
- button.focus();
269
- expect(button).toHaveFocus();
270
- });
271
- });
272
- });
273
- ```
274
-
275
- ### 4. API Endpoint Test Generation
276
-
277
- **API Route Analysis**:
278
- ```typescript
279
- // Source: src/api/users.ts
280
- export async function GET(request: Request) {
281
- const { searchParams } = new URL(request.url);
282
- const page = parseInt(searchParams.get('page') || '1');
283
- const limit = parseInt(searchParams.get('limit') || '10');
284
-
285
- if (page < 1 || limit < 1 || limit > 100) {
286
- return Response.json(
287
- { error: 'Invalid pagination parameters' },
288
- { status: 400 }
289
- );
290
- }
291
-
292
- const users = await db.users.findMany({
293
- skip: (page - 1) * limit,
294
- take: limit,
295
- });
296
-
297
- return Response.json({ users, page, limit });
298
- }
299
-
300
- export async function POST(request: Request) {
301
- const body = await request.json();
302
-
303
- const { email, name, role } = body;
304
-
305
- if (!email || !name) {
306
- return Response.json(
307
- { error: 'Email and name are required' },
308
- { status: 400 }
309
- );
310
- }
311
-
312
- const existingUser = await db.users.findUnique({ where: { email } });
313
- if (existingUser) {
314
- return Response.json(
315
- { error: 'User already exists' },
316
- { status: 409 }
317
- );
318
- }
319
-
320
- const user = await db.users.create({
321
- data: { email, name, role: role || 'user' },
322
- });
323
-
324
- return Response.json({ user }, { status: 201 });
325
- }
326
- ```
327
-
328
- **Generated API Test**:
329
- ```typescript
330
- // Generated: tests/integration/api/users.test.ts
331
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
332
- import { GET, POST } from '@/api/users';
333
- import { db } from '@/lib/db';
334
-
335
- // Mock database
336
- vi.mock('@/lib/db', () => ({
337
- db: {
338
- users: {
339
- findMany: vi.fn(),
340
- findUnique: vi.fn(),
341
- create: vi.fn(),
342
- },
343
- },
344
- }));
345
-
346
- describe('Users API', () => {
347
- beforeEach(() => {
348
- vi.clearAllMocks();
349
- });
350
-
351
- describe('GET /api/users', () => {
352
- it('should return users with default pagination', async () => {
353
- const mockUsers = [
354
- { id: '1', email: 'user1@example.com', name: 'User 1' },
355
- { id: '2', email: 'user2@example.com', name: 'User 2' },
356
- ];
357
-
358
- vi.mocked(db.users.findMany).mockResolvedValue(mockUsers);
359
-
360
- const request = new Request('http://localhost/api/users');
361
- const response = await GET(request);
362
- const data = await response.json();
363
-
364
- expect(response.status).toBe(200);
365
- expect(data.users).toEqual(mockUsers);
366
- expect(data.page).toBe(1);
367
- expect(data.limit).toBe(10);
368
- expect(db.users.findMany).toHaveBeenCalledWith({
369
- skip: 0,
370
- take: 10,
371
- });
372
- });
373
-
374
- it('should handle custom pagination', async () => {
375
- vi.mocked(db.users.findMany).mockResolvedValue([]);
376
-
377
- const request = new Request('http://localhost/api/users?page=2&limit=20');
378
- const response = await GET(request);
379
- const data = await response.json();
380
-
381
- expect(data.page).toBe(2);
382
- expect(data.limit).toBe(20);
383
- expect(db.users.findMany).toHaveBeenCalledWith({
384
- skip: 20,
385
- take: 20,
386
- });
387
- });
388
-
389
- it('should reject invalid page number', async () => {
390
- const request = new Request('http://localhost/api/users?page=0');
391
- const response = await GET(request);
392
- const data = await response.json();
393
-
394
- expect(response.status).toBe(400);
395
- expect(data.error).toBe('Invalid pagination parameters');
396
- });
397
-
398
- it('should reject limit over 100', async () => {
399
- const request = new Request('http://localhost/api/users?limit=101');
400
- const response = await GET(request);
401
- const data = await response.json();
402
-
403
- expect(response.status).toBe(400);
404
- expect(data.error).toBe('Invalid pagination parameters');
405
- });
406
-
407
- it('should handle database errors', async () => {
408
- vi.mocked(db.users.findMany).mockRejectedValue(new Error('DB error'));
409
-
410
- const request = new Request('http://localhost/api/users');
411
-
412
- await expect(GET(request)).rejects.toThrow('DB error');
413
- });
414
- });
415
-
416
- describe('POST /api/users', () => {
417
- it('should create new user', async () => {
418
- const newUser = {
419
- email: 'new@example.com',
420
- name: 'New User',
421
- role: 'user',
422
- };
423
-
424
- vi.mocked(db.users.findUnique).mockResolvedValue(null);
425
- vi.mocked(db.users.create).mockResolvedValue({
426
- id: '123',
427
- ...newUser,
428
- });
429
-
430
- const request = new Request('http://localhost/api/users', {
431
- method: 'POST',
432
- body: JSON.stringify(newUser),
433
- });
434
-
435
- const response = await POST(request);
436
- const data = await response.json();
437
-
438
- expect(response.status).toBe(201);
439
- expect(data.user).toMatchObject(newUser);
440
- expect(db.users.create).toHaveBeenCalledWith({
441
- data: newUser,
442
- });
443
- });
444
-
445
- it('should default role to user', async () => {
446
- vi.mocked(db.users.findUnique).mockResolvedValue(null);
447
- vi.mocked(db.users.create).mockResolvedValue({
448
- id: '123',
449
- email: 'test@example.com',
450
- name: 'Test',
451
- role: 'user',
452
- });
453
-
454
- const request = new Request('http://localhost/api/users', {
455
- method: 'POST',
456
- body: JSON.stringify({ email: 'test@example.com', name: 'Test' }),
457
- });
458
-
459
- await POST(request);
460
-
461
- expect(db.users.create).toHaveBeenCalledWith({
462
- data: {
463
- email: 'test@example.com',
464
- name: 'Test',
465
- role: 'user',
466
- },
467
- });
468
- });
469
-
470
- it('should reject missing email', async () => {
471
- const request = new Request('http://localhost/api/users', {
472
- method: 'POST',
473
- body: JSON.stringify({ name: 'Test' }),
474
- });
475
-
476
- const response = await POST(request);
477
- const data = await response.json();
478
-
479
- expect(response.status).toBe(400);
480
- expect(data.error).toBe('Email and name are required');
481
- });
482
-
483
- it('should reject missing name', async () => {
484
- const request = new Request('http://localhost/api/users', {
485
- method: 'POST',
486
- body: JSON.stringify({ email: 'test@example.com' }),
487
- });
488
-
489
- const response = await POST(request);
490
- const data = await response.json();
491
-
492
- expect(response.status).toBe(400);
493
- expect(data.error).toBe('Email and name are required');
494
- });
495
-
496
- it('should reject duplicate email', async () => {
497
- vi.mocked(db.users.findUnique).mockResolvedValue({
498
- id: '123',
499
- email: 'existing@example.com',
500
- name: 'Existing',
501
- role: 'user',
502
- });
503
-
504
- const request = new Request('http://localhost/api/users', {
505
- method: 'POST',
506
- body: JSON.stringify({
507
- email: 'existing@example.com',
508
- name: 'Test',
509
- }),
510
- });
511
-
512
- const response = await POST(request);
513
- const data = await response.json();
514
-
515
- expect(response.status).toBe(409);
516
- expect(data.error).toBe('User already exists');
517
- });
518
- });
519
- });
520
- ```
521
-
522
- ### 5. Custom Hook Test Generation
523
-
524
- **Hook Analysis**:
525
- ```typescript
526
- // Source: src/hooks/useDebounce.ts
527
- import { useState, useEffect } from 'react';
528
-
529
- export function useDebounce<T>(value: T, delay: number): T {
530
- const [debouncedValue, setDebouncedValue] = useState<T>(value);
531
-
532
- useEffect(() => {
533
- const handler = setTimeout(() => {
534
- setDebouncedValue(value);
535
- }, delay);
536
-
537
- return () => {
538
- clearTimeout(handler);
539
- };
540
- }, [value, delay]);
541
-
542
- return debouncedValue;
543
- }
544
- ```
545
-
546
- **Generated Hook Test**:
547
- ```typescript
548
- // Generated: tests/unit/hooks/useDebounce.test.ts
549
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
550
- import { renderHook, waitFor } from '@testing-library/react';
551
- import { useDebounce } from '@/hooks/useDebounce';
552
-
553
- describe('useDebounce', () => {
554
- beforeEach(() => {
555
- vi.useFakeTimers();
556
- });
557
-
558
- afterEach(() => {
559
- vi.restoreAllMocks();
560
- });
561
-
562
- it('should return initial value immediately', () => {
563
- const { result } = renderHook(() => useDebounce('initial', 500));
564
- expect(result.current).toBe('initial');
565
- });
566
-
567
- it('should debounce value changes', async () => {
568
- const { result, rerender } = renderHook(
569
- ({ value, delay }) => useDebounce(value, delay),
570
- { initialProps: { value: 'initial', delay: 500 } }
571
- );
572
-
573
- expect(result.current).toBe('initial');
574
-
575
- // Update value
576
- rerender({ value: 'updated', delay: 500 });
577
-
578
- // Value should not change immediately
579
- expect(result.current).toBe('initial');
580
-
581
- // Fast-forward time
582
- vi.advanceTimersByTime(500);
583
-
584
- // Value should update after delay
585
- await waitFor(() => {
586
- expect(result.current).toBe('updated');
587
- });
588
- });
589
-
590
- it('should cancel previous timeout on rapid changes', async () => {
591
- const { result, rerender } = renderHook(
592
- ({ value }) => useDebounce(value, 500),
593
- { initialProps: { value: 'initial' } }
594
- );
595
-
596
- // Rapid updates
597
- rerender({ value: 'update1' });
598
- vi.advanceTimersByTime(200);
599
-
600
- rerender({ value: 'update2' });
601
- vi.advanceTimersByTime(200);
602
-
603
- rerender({ value: 'final' });
604
- vi.advanceTimersByTime(500);
605
-
606
- await waitFor(() => {
607
- expect(result.current).toBe('final');
608
- });
609
- });
610
-
611
- it('should handle delay changes', async () => {
612
- const { result, rerender } = renderHook(
613
- ({ value, delay }) => useDebounce(value, delay),
614
- { initialProps: { value: 'initial', delay: 500 } }
615
- );
616
-
617
- rerender({ value: 'updated', delay: 1000 });
618
- vi.advanceTimersByTime(500);
619
-
620
- // Should not update yet (new delay is 1000ms)
621
- expect(result.current).toBe('initial');
622
-
623
- vi.advanceTimersByTime(500);
624
-
625
- await waitFor(() => {
626
- expect(result.current).toBe('updated');
627
- });
628
- });
629
-
630
- it('should handle different value types', async () => {
631
- // Number
632
- const { result: numberResult } = renderHook(() => useDebounce(42, 100));
633
- expect(numberResult.current).toBe(42);
634
-
635
- // Object
636
- const obj = { name: 'test' };
637
- const { result: objResult } = renderHook(() => useDebounce(obj, 100));
638
- expect(objResult.current).toBe(obj);
639
-
640
- // Array
641
- const arr = [1, 2, 3];
642
- const { result: arrResult } = renderHook(() => useDebounce(arr, 100));
643
- expect(arrResult.current).toBe(arr);
644
- });
645
- });
646
- ```
647
-
648
- ### 6. Store/State Management Test Generation
649
-
650
- **Zustand Store Analysis**:
651
- ```typescript
652
- // Source: src/stores/useCartStore.ts
653
- import { create } from 'zustand';
654
-
655
- interface CartItem {
656
- id: string;
657
- name: string;
658
- price: number;
659
- quantity: number;
660
- }
661
-
662
- interface CartStore {
663
- items: CartItem[];
664
- addItem: (item: Omit<CartItem, 'quantity'>) => void;
665
- removeItem: (id: string) => void;
666
- updateQuantity: (id: string, quantity: number) => void;
667
- clearCart: () => void;
668
- total: () => number;
669
- }
670
-
671
- export const useCartStore = create<CartStore>((set, get) => ({
672
- items: [],
673
-
674
- addItem: (item) => {
675
- set((state) => {
676
- const existing = state.items.find((i) => i.id === item.id);
677
- if (existing) {
678
- return {
679
- items: state.items.map((i) =>
680
- i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
681
- ),
682
- };
683
- }
684
- return { items: [...state.items, { ...item, quantity: 1 }] };
685
- });
686
- },
687
-
688
- removeItem: (id) => {
689
- set((state) => ({
690
- items: state.items.filter((i) => i.id !== id),
691
- }));
692
- },
693
-
694
- updateQuantity: (id, quantity) => {
695
- if (quantity <= 0) {
696
- get().removeItem(id);
697
- return;
698
- }
699
- set((state) => ({
700
- items: state.items.map((i) =>
701
- i.id === id ? { ...i, quantity } : i
702
- ),
703
- }));
704
- },
705
-
706
- clearCart: () => {
707
- set({ items: [] });
708
- },
709
-
710
- total: () => {
711
- return get().items.reduce((sum, item) => sum + item.price * item.quantity, 0);
712
- },
713
- }));
714
- ```
715
-
716
- **Generated Store Test**:
717
- ```typescript
718
- // Generated: tests/unit/stores/useCartStore.test.ts
719
- import { describe, it, expect, beforeEach } from 'vitest';
720
- import { renderHook, act } from '@testing-library/react';
721
- import { useCartStore } from '@/stores/useCartStore';
722
-
723
- describe('useCartStore', () => {
724
- beforeEach(() => {
725
- // Reset store before each test
726
- const { result } = renderHook(() => useCartStore());
727
- act(() => {
728
- result.current.clearCart();
729
- });
730
- });
731
-
732
- describe('addItem', () => {
733
- it('should add new item to cart', () => {
734
- const { result } = renderHook(() => useCartStore());
735
-
736
- act(() => {
737
- result.current.addItem({
738
- id: '1',
739
- name: 'Product A',
740
- price: 29.99,
741
- });
742
- });
743
-
744
- expect(result.current.items).toHaveLength(1);
745
- expect(result.current.items[0]).toMatchObject({
746
- id: '1',
747
- name: 'Product A',
748
- price: 29.99,
749
- quantity: 1,
750
- });
751
- });
752
-
753
- it('should increment quantity for existing item', () => {
754
- const { result } = renderHook(() => useCartStore());
755
-
756
- act(() => {
757
- result.current.addItem({
758
- id: '1',
759
- name: 'Product A',
760
- price: 29.99,
761
- });
762
- result.current.addItem({
763
- id: '1',
764
- name: 'Product A',
765
- price: 29.99,
766
- });
767
- });
768
-
769
- expect(result.current.items).toHaveLength(1);
770
- expect(result.current.items[0].quantity).toBe(2);
771
- });
772
-
773
- it('should add multiple different items', () => {
774
- const { result } = renderHook(() => useCartStore());
775
-
776
- act(() => {
777
- result.current.addItem({ id: '1', name: 'Product A', price: 29.99 });
778
- result.current.addItem({ id: '2', name: 'Product B', price: 39.99 });
779
- });
780
-
781
- expect(result.current.items).toHaveLength(2);
782
- });
783
- });
784
-
785
- describe('removeItem', () => {
786
- it('should remove item from cart', () => {
787
- const { result } = renderHook(() => useCartStore());
788
-
789
- act(() => {
790
- result.current.addItem({ id: '1', name: 'Product A', price: 29.99 });
791
- result.current.removeItem('1');
792
- });
793
-
794
- expect(result.current.items).toHaveLength(0);
795
- });
796
-
797
- it('should not affect other items', () => {
798
- const { result } = renderHook(() => useCartStore());
799
-
800
- act(() => {
801
- result.current.addItem({ id: '1', name: 'Product A', price: 29.99 });
802
- result.current.addItem({ id: '2', name: 'Product B', price: 39.99 });
803
- result.current.removeItem('1');
804
- });
805
-
806
- expect(result.current.items).toHaveLength(1);
807
- expect(result.current.items[0].id).toBe('2');
808
- });
809
-
810
- it('should handle removing non-existent item', () => {
811
- const { result } = renderHook(() => useCartStore());
812
-
813
- act(() => {
814
- result.current.addItem({ id: '1', name: 'Product A', price: 29.99 });
815
- result.current.removeItem('999');
816
- });
817
-
818
- expect(result.current.items).toHaveLength(1);
819
- });
820
- });
821
-
822
- describe('updateQuantity', () => {
823
- it('should update item quantity', () => {
824
- const { result } = renderHook(() => useCartStore());
825
-
826
- act(() => {
827
- result.current.addItem({ id: '1', name: 'Product A', price: 29.99 });
828
- result.current.updateQuantity('1', 5);
829
- });
830
-
831
- expect(result.current.items[0].quantity).toBe(5);
832
- });
833
-
834
- it('should remove item when quantity is 0', () => {
835
- const { result } = renderHook(() => useCartStore());
836
-
837
- act(() => {
838
- result.current.addItem({ id: '1', name: 'Product A', price: 29.99 });
839
- result.current.updateQuantity('1', 0);
840
- });
841
-
842
- expect(result.current.items).toHaveLength(0);
843
- });
844
-
845
- it('should remove item when quantity is negative', () => {
846
- const { result } = renderHook(() => useCartStore());
847
-
848
- act(() => {
849
- result.current.addItem({ id: '1', name: 'Product A', price: 29.99 });
850
- result.current.updateQuantity('1', -1);
851
- });
852
-
853
- expect(result.current.items).toHaveLength(0);
854
- });
855
- });
856
-
857
- describe('clearCart', () => {
858
- it('should remove all items', () => {
859
- const { result } = renderHook(() => useCartStore());
860
-
861
- act(() => {
862
- result.current.addItem({ id: '1', name: 'Product A', price: 29.99 });
863
- result.current.addItem({ id: '2', name: 'Product B', price: 39.99 });
864
- result.current.clearCart();
865
- });
866
-
867
- expect(result.current.items).toHaveLength(0);
868
- });
869
- });
870
-
871
- describe('total', () => {
872
- it('should calculate total for single item', () => {
873
- const { result } = renderHook(() => useCartStore());
874
-
875
- act(() => {
876
- result.current.addItem({ id: '1', name: 'Product A', price: 29.99 });
877
- });
878
-
879
- expect(result.current.total()).toBe(29.99);
880
- });
881
-
882
- it('should calculate total for multiple items', () => {
883
- const { result } = renderHook(() => useCartStore());
884
-
885
- act(() => {
886
- result.current.addItem({ id: '1', name: 'Product A', price: 29.99 });
887
- result.current.addItem({ id: '2', name: 'Product B', price: 39.99 });
888
- });
889
-
890
- expect(result.current.total()).toBe(69.98);
891
- });
892
-
893
- it('should calculate total with quantities', () => {
894
- const { result } = renderHook(() => useCartStore());
895
-
896
- act(() => {
897
- result.current.addItem({ id: '1', name: 'Product A', price: 10 });
898
- result.current.updateQuantity('1', 3);
899
- });
900
-
901
- expect(result.current.total()).toBe(30);
902
- });
903
-
904
- it('should return 0 for empty cart', () => {
905
- const { result } = renderHook(() => useCartStore());
906
- expect(result.current.total()).toBe(0);
907
- });
908
- });
909
- });
910
- ```
911
-
912
- ### 7. Test Generation CLI
913
-
914
- **Test Generator Script**:
915
- ```typescript
916
- // scripts/generate-tests.ts
917
- import fs from 'fs';
918
- import path from 'path';
919
- import { parse } from '@babel/parser';
920
- import traverse from '@babel/traverse';
921
-
922
- interface TestConfig {
923
- sourceFile: string;
924
- testType: 'unit' | 'integration' | 'e2e';
925
- outputFile?: string;
926
- }
927
-
928
- export async function generateTests(config: TestConfig) {
929
- const code = fs.readFileSync(config.sourceFile, 'utf-8');
930
- const ast = parse(code, {
931
- sourceType: 'module',
932
- plugins: ['typescript', 'jsx'],
933
- });
934
-
935
- const tests: string[] = [];
936
-
937
- traverse(ast, {
938
- FunctionDeclaration(path) {
939
- const name = path.node.id?.name;
940
- if (name) {
941
- tests.push(generateFunctionTest(name, path.node));
942
- }
943
- },
944
- ExportNamedDeclaration(path) {
945
- if (path.node.declaration?.type === 'FunctionDeclaration') {
946
- const name = path.node.declaration.id?.name;
947
- if (name) {
948
- tests.push(generateFunctionTest(name, path.node.declaration));
949
- }
950
- }
951
- },
952
- });
953
-
954
- const outputPath = config.outputFile || generateOutputPath(config.sourceFile);
955
- const testContent = generateTestFile(tests, config.sourceFile);
956
-
957
- fs.writeFileSync(outputPath, testContent, 'utf-8');
958
- console.log(`✓ Generated tests: ${outputPath}`);
959
- }
960
-
961
- function generateOutputPath(sourceFile: string): string {
962
- const relativePath = path.relative('src', sourceFile);
963
- return path.join('tests', 'unit', relativePath.replace(/\.ts$/, '.test.ts'));
964
- }
965
-
966
- function generateFunctionTest(name: string, node: any): string {
967
- return `
968
- describe('${name}', () => {
969
- it('should work correctly', () => {
970
- // TODO: Implement test
971
- expect(${name}).toBeDefined();
972
- });
973
- });
974
- `;
975
- }
976
-
977
- function generateTestFile(tests: string[], sourceFile: string): string {
978
- const importPath = path.relative(
979
- path.dirname(generateOutputPath(sourceFile)),
980
- sourceFile
981
- ).replace(/\.ts$/, '');
982
-
983
- return `
984
- import { describe, it, expect } from 'vitest';
985
- import { } from '${importPath}';
986
-
987
- describe('${path.basename(sourceFile)}', () => {
988
- ${tests.join('\n')}
989
- });
990
- `.trim();
991
- }
992
- ```
993
-
994
- ### 8. Test Templates
995
-
996
- **Template Registry** (`tests/templates/`):
997
- ```typescript
998
- // tests/templates/component.template.ts
999
- export const componentTestTemplate = `
1000
- import { describe, it, expect, vi } from 'vitest';
1001
- import { render, screen } from '@/tests/utils/test-utils';
1002
- import { {{ComponentName}} } from '@/components/{{ComponentName}}';
1003
-
1004
- describe('{{ComponentName}}', () => {
1005
- it('should render correctly', () => {
1006
- render(<{{ComponentName}} />);
1007
- expect(screen.getByTestId('{{testId}}')).toBeInTheDocument();
1008
- });
1009
-
1010
- it('should handle user interactions', () => {
1011
- const handleClick = vi.fn();
1012
- render(<{{ComponentName}} onClick={handleClick} />);
1013
-
1014
- fireEvent.click(screen.getByTestId('{{testId}}'));
1015
- expect(handleClick).toHaveBeenCalled();
1016
- });
1017
- });
1018
- `;
1019
-
1020
- // tests/templates/api.template.ts
1021
- export const apiTestTemplate = `
1022
- import { describe, it, expect, beforeEach, vi } from 'vitest';
1023
- import { {{functionName}} } from '@/api/{{fileName}}';
1024
-
1025
- describe('{{functionName}}', () => {
1026
- beforeEach(() => {
1027
- vi.clearAllMocks();
1028
- });
1029
-
1030
- it('should return success response', async () => {
1031
- const request = new Request('http://localhost/api/{{endpoint}}');
1032
- const response = await {{functionName}}(request);
1033
-
1034
- expect(response.status).toBe(200);
1035
- });
1036
-
1037
- it('should handle errors', async () => {
1038
- // TODO: Implement error case
1039
- });
1040
- });
1041
- `;
1042
- ```
1043
-
1044
- ### 9. Coverage Gaps Detection
1045
-
1046
- **Coverage Analyzer**:
1047
- ```typescript
1048
- // scripts/analyze-coverage-gaps.ts
1049
- import fs from 'fs';
1050
- import path from 'path';
1051
-
1052
- interface CoverageSummary {
1053
- total: {
1054
- lines: { pct: number };
1055
- statements: { pct: number };
1056
- functions: { pct: number };
1057
- branches: { pct: number };
1058
- };
1059
- }
1060
-
1061
- export function analyzeCoverageGaps(coverageFile: string) {
1062
- const coverage: CoverageSummary = JSON.parse(
1063
- fs.readFileSync(coverageFile, 'utf-8')
1064
- );
1065
-
1066
- const gaps = [];
1067
-
1068
- Object.entries(coverage).forEach(([file, data]) => {
1069
- if (file === 'total') return;
1070
-
1071
- const { lines, functions, branches } = data as any;
1072
-
1073
- if (lines.pct < 80) {
1074
- gaps.push({
1075
- file,
1076
- type: 'lines',
1077
- current: lines.pct,
1078
- target: 80,
1079
- gap: 80 - lines.pct,
1080
- });
1081
- }
1082
-
1083
- if (functions.pct < 80) {
1084
- gaps.push({
1085
- file,
1086
- type: 'functions',
1087
- current: functions.pct,
1088
- target: 80,
1089
- gap: 80 - functions.pct,
1090
- });
1091
- }
1092
-
1093
- if (branches.pct < 80) {
1094
- gaps.push({
1095
- file,
1096
- type: 'branches',
1097
- current: branches.pct,
1098
- target: 80,
1099
- gap: 80 - branches.pct,
1100
- });
1101
- }
1102
- });
1103
-
1104
- return gaps.sort((a, b) => b.gap - a.gap);
1105
- }
1106
-
1107
- // Generate test stubs for gaps
1108
- export function generateGapTests(gaps: any[]) {
1109
- gaps.slice(0, 10).forEach((gap) => {
1110
- console.log(`
1111
- Priority: ${gap.file}
1112
- Coverage: ${gap.current}% ${gap.type}
1113
- Gap: ${gap.gap}%
1114
-
1115
- Suggested tests:
1116
- - Add ${gap.type} coverage for uncovered paths
1117
- - Focus on edge cases and error handling
1118
- `);
1119
- });
1120
- }
1121
- ```
1122
-
1123
- ### 10. Package Scripts
1124
-
1125
- ```json
1126
- {
1127
- "scripts": {
1128
- "test:generate": "tsx scripts/generate-tests.ts",
1129
- "test:generate:component": "tsx scripts/generate-tests.ts --type component",
1130
- "test:generate:api": "tsx scripts/generate-tests.ts --type api",
1131
- "test:coverage:gaps": "tsx scripts/analyze-coverage-gaps.ts coverage/coverage-summary.json",
1132
- "test:stub": "tsx scripts/generate-test-stubs.ts"
1133
- }
1134
- }
1135
- ```
1136
-
1137
- ## Workflow
1138
-
1139
- 1. Ask about code to generate tests for (file path or pattern)
1140
- 2. Analyze code structure and identify test targets
1141
- 3. Determine appropriate test types (unit/integration/E2E)
1142
- 4. Generate test scaffolding with proper imports
1143
- 5. Create test cases for happy paths
1144
- 6. Add edge case and error handling tests
1145
- 7. Generate mocks and fixtures
1146
- 8. Add accessibility and performance tests (if applicable)
1147
- 9. Run tests to verify they work
1148
- 10. Analyze coverage and suggest improvements
1149
-
1150
- ## When to Use
1151
-
1152
- - Adding tests to existing code
1153
- - Achieving test coverage targets
1154
- - Creating test stubs for new features
1155
- - Migrating to new testing framework
1156
- - Identifying coverage gaps
1157
- - Implementing TDD workflow
1158
- - Standardizing test patterns
1159
-
1160
- Generate comprehensive test coverage automatically!