create-claude-webapp 1.0.0 → 1.0.1
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/.claude/agents/acceptance-test-generator.md +256 -0
- package/.claude/agents/auth-flow-designer.md +93 -0
- package/.claude/agents/code-reviewer.md +193 -0
- package/.claude/agents/code-verifier.md +194 -0
- package/.claude/agents/deployment-executor.md +90 -0
- package/.claude/agents/design-sync.md +226 -0
- package/.claude/agents/document-reviewer.md +304 -0
- package/.claude/agents/environment-validator.md +100 -0
- package/.claude/agents/integration-test-reviewer.md +196 -0
- package/.claude/agents/investigator.md +162 -0
- package/.claude/agents/prd-creator.md +220 -0
- package/.claude/agents/quality-fixer-frontend.md +323 -0
- package/.claude/agents/quality-fixer.md +280 -0
- package/.claude/agents/requirement-analyzer.md +149 -0
- package/.claude/agents/rls-policy-designer.md +86 -0
- package/.claude/agents/rule-advisor.md +123 -0
- package/.claude/agents/scope-discoverer.md +231 -0
- package/.claude/agents/solver.md +173 -0
- package/.claude/agents/supabase-migration-generator.md +85 -0
- package/.claude/agents/task-decomposer.md +246 -0
- package/.claude/agents/task-executor-frontend.md +264 -0
- package/.claude/agents/task-executor.md +261 -0
- package/.claude/agents/technical-designer-frontend.md +444 -0
- package/.claude/agents/technical-designer.md +370 -0
- package/.claude/agents/verifier.md +193 -0
- package/.claude/agents/work-planner.md +211 -0
- package/.claude/commands/add-integration-tests.md +116 -0
- package/.claude/commands/build.md +77 -0
- package/.claude/commands/db-migrate.md +96 -0
- package/.claude/commands/deploy.md +95 -0
- package/.claude/commands/design.md +75 -0
- package/.claude/commands/diagnose.md +202 -0
- package/.claude/commands/front-build.md +116 -0
- package/.claude/commands/front-design.md +61 -0
- package/.claude/commands/front-plan.md +53 -0
- package/.claude/commands/front-reverse-design.md +183 -0
- package/.claude/commands/front-review.md +89 -0
- package/.claude/commands/implement.md +80 -0
- package/.claude/commands/local-dev.md +94 -0
- package/.claude/commands/plan.md +61 -0
- package/.claude/commands/project-inject.md +76 -0
- package/.claude/commands/refine-skill.md +207 -0
- package/.claude/commands/reverse-engineer.md +301 -0
- package/.claude/commands/review.md +88 -0
- package/.claude/commands/setup-auth.md +68 -0
- package/.claude/commands/setup-supabase.md +66 -0
- package/.claude/commands/setup-vercel.md +71 -0
- package/.claude/commands/sync-skills.md +116 -0
- package/.claude/commands/task.md +13 -0
- package/.claude/skills/coding-standards/SKILL.md +246 -0
- package/.claude/skills/documentation-criteria/SKILL.md +184 -0
- package/.claude/skills/documentation-criteria/references/adr-template.md +64 -0
- package/.claude/skills/documentation-criteria/references/design-template.md +263 -0
- package/.claude/skills/documentation-criteria/references/plan-template.md +130 -0
- package/.claude/skills/documentation-criteria/references/prd-template.md +109 -0
- package/.claude/skills/documentation-criteria/references/task-template.md +38 -0
- package/.claude/skills/frontend/technical-spec/SKILL.md +147 -0
- package/.claude/skills/frontend/typescript-rules/SKILL.md +136 -0
- package/.claude/skills/frontend/typescript-testing/SKILL.md +129 -0
- package/.claude/skills/fullstack-integration/SKILL.md +466 -0
- package/.claude/skills/implementation-approach/SKILL.md +141 -0
- package/.claude/skills/integration-e2e-testing/SKILL.md +146 -0
- package/.claude/skills/interview/SKILL.md +345 -0
- package/.claude/skills/project-context/SKILL.md +53 -0
- package/.claude/skills/stack-auth/SKILL.md +519 -0
- package/.claude/skills/subagents-orchestration-guide/SKILL.md +218 -0
- package/.claude/skills/supabase/SKILL.md +289 -0
- package/.claude/skills/supabase-edge-functions/SKILL.md +386 -0
- package/.claude/skills/supabase-local/SKILL.md +328 -0
- package/.claude/skills/supabase-testing/SKILL.md +513 -0
- package/.claude/skills/task-analyzer/SKILL.md +131 -0
- package/.claude/skills/task-analyzer/references/skills-index.yaml +375 -0
- package/.claude/skills/technical-spec/SKILL.md +86 -0
- package/.claude/skills/typescript-rules/SKILL.md +121 -0
- package/.claude/skills/typescript-testing/SKILL.md +155 -0
- package/.claude/skills/vercel-deployment/SKILL.md +355 -0
- package/.claude/skills/vercel-edge/SKILL.md +407 -0
- package/README.md +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: supabase-testing
|
|
3
|
+
description: Testing Supabase with Vitest. Use when writing tests for Supabase queries, RLS policies, or database operations.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Supabase Testing
|
|
7
|
+
|
|
8
|
+
## Test Database Setup
|
|
9
|
+
|
|
10
|
+
### Local Testing Configuration
|
|
11
|
+
```typescript
|
|
12
|
+
// tests/setup/supabase.ts
|
|
13
|
+
import { createClient } from '@supabase/supabase-js'
|
|
14
|
+
import type { Database } from '@/types/database.types'
|
|
15
|
+
|
|
16
|
+
// Use local Supabase for tests
|
|
17
|
+
const SUPABASE_URL = process.env.SUPABASE_URL ?? 'http://127.0.0.1:54321'
|
|
18
|
+
const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY ?? 'your-local-anon-key'
|
|
19
|
+
const SUPABASE_SERVICE_ROLE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY ?? 'your-local-service-role-key'
|
|
20
|
+
|
|
21
|
+
// Client for authenticated user tests
|
|
22
|
+
export const createTestClient = (accessToken?: string) =>
|
|
23
|
+
createClient<Database>(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
|
24
|
+
global: {
|
|
25
|
+
headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : {},
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// Admin client for setup/teardown (bypasses RLS)
|
|
30
|
+
export const supabaseAdmin = createClient<Database>(
|
|
31
|
+
SUPABASE_URL,
|
|
32
|
+
SUPABASE_SERVICE_ROLE_KEY
|
|
33
|
+
)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Vitest Configuration
|
|
37
|
+
```typescript
|
|
38
|
+
// vitest.config.ts
|
|
39
|
+
import { defineConfig } from 'vitest/config'
|
|
40
|
+
|
|
41
|
+
export default defineConfig({
|
|
42
|
+
test: {
|
|
43
|
+
globals: true,
|
|
44
|
+
environment: 'node',
|
|
45
|
+
setupFiles: ['./tests/setup/global.ts'],
|
|
46
|
+
include: ['tests/**/*.test.ts'],
|
|
47
|
+
// Run database tests sequentially to avoid conflicts
|
|
48
|
+
pool: 'forks',
|
|
49
|
+
poolOptions: {
|
|
50
|
+
forks: {
|
|
51
|
+
singleFork: true,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Global Test Setup
|
|
59
|
+
```typescript
|
|
60
|
+
// tests/setup/global.ts
|
|
61
|
+
import { supabaseAdmin } from './supabase'
|
|
62
|
+
import { beforeAll, afterAll, afterEach } from 'vitest'
|
|
63
|
+
|
|
64
|
+
// Ensure local Supabase is running
|
|
65
|
+
beforeAll(async () => {
|
|
66
|
+
const { error } = await supabaseAdmin.from('_test_health').select('*').limit(1)
|
|
67
|
+
if (error && !error.message.includes('does not exist')) {
|
|
68
|
+
throw new Error('Supabase is not running. Run: supabase start')
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
// Clean up test data after each test
|
|
73
|
+
afterEach(async () => {
|
|
74
|
+
// Delete test data created during tests
|
|
75
|
+
// Use specific test user IDs or prefixes to identify test data
|
|
76
|
+
})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Mocking Strategies
|
|
80
|
+
|
|
81
|
+
### Mocking Supabase Client
|
|
82
|
+
```typescript
|
|
83
|
+
// tests/mocks/supabase.ts
|
|
84
|
+
import { vi } from 'vitest'
|
|
85
|
+
|
|
86
|
+
export const mockSupabaseClient = {
|
|
87
|
+
from: vi.fn(() => mockSupabaseClient),
|
|
88
|
+
select: vi.fn(() => mockSupabaseClient),
|
|
89
|
+
insert: vi.fn(() => mockSupabaseClient),
|
|
90
|
+
update: vi.fn(() => mockSupabaseClient),
|
|
91
|
+
delete: vi.fn(() => mockSupabaseClient),
|
|
92
|
+
eq: vi.fn(() => mockSupabaseClient),
|
|
93
|
+
single: vi.fn(() => Promise.resolve({ data: null, error: null })),
|
|
94
|
+
auth: {
|
|
95
|
+
getUser: vi.fn(() => Promise.resolve({ data: { user: null }, error: null })),
|
|
96
|
+
signInWithPassword: vi.fn(),
|
|
97
|
+
signOut: vi.fn(),
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Reset mocks between tests
|
|
102
|
+
export const resetSupabaseMocks = () => {
|
|
103
|
+
Object.values(mockSupabaseClient).forEach((mock) => {
|
|
104
|
+
if (typeof mock === 'function' && 'mockReset' in mock) {
|
|
105
|
+
mock.mockReset()
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Using Mocks in Unit Tests
|
|
112
|
+
```typescript
|
|
113
|
+
// tests/unit/user-service.test.ts
|
|
114
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
115
|
+
import { mockSupabaseClient, resetSupabaseMocks } from '../mocks/supabase'
|
|
116
|
+
|
|
117
|
+
// Mock the module
|
|
118
|
+
vi.mock('@/lib/supabase', () => ({
|
|
119
|
+
supabase: mockSupabaseClient,
|
|
120
|
+
}))
|
|
121
|
+
|
|
122
|
+
import { getUserById } from '@/services/user-service'
|
|
123
|
+
|
|
124
|
+
describe('UserService', () => {
|
|
125
|
+
beforeEach(() => {
|
|
126
|
+
resetSupabaseMocks()
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('should fetch user by ID', async () => {
|
|
130
|
+
const mockUser = { id: '123', email: 'test@example.com', name: 'Test' }
|
|
131
|
+
|
|
132
|
+
mockSupabaseClient.single.mockResolvedValueOnce({
|
|
133
|
+
data: mockUser,
|
|
134
|
+
error: null,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const result = await getUserById('123')
|
|
138
|
+
|
|
139
|
+
expect(mockSupabaseClient.from).toHaveBeenCalledWith('users')
|
|
140
|
+
expect(mockSupabaseClient.eq).toHaveBeenCalledWith('id', '123')
|
|
141
|
+
expect(result).toEqual(mockUser)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('should handle errors', async () => {
|
|
145
|
+
mockSupabaseClient.single.mockResolvedValueOnce({
|
|
146
|
+
data: null,
|
|
147
|
+
error: { message: 'Not found' },
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
await expect(getUserById('invalid')).rejects.toThrow('Not found')
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## RLS Testing
|
|
156
|
+
|
|
157
|
+
### Testing RLS with Different User Contexts
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
// tests/integration/rls.test.ts
|
|
161
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
162
|
+
import { supabaseAdmin, createTestClient } from '../setup/supabase'
|
|
163
|
+
|
|
164
|
+
describe('RLS Policies', () => {
|
|
165
|
+
const testUserId1 = 'd0d8c4c0-0000-0000-0000-000000000001'
|
|
166
|
+
const testUserId2 = 'd0d8c4c0-0000-0000-0000-000000000002'
|
|
167
|
+
let user1Token: string
|
|
168
|
+
let user2Token: string
|
|
169
|
+
|
|
170
|
+
beforeAll(async () => {
|
|
171
|
+
// Create test users
|
|
172
|
+
await supabaseAdmin.auth.admin.createUser({
|
|
173
|
+
email: 'user1@test.com',
|
|
174
|
+
password: 'password123',
|
|
175
|
+
user_metadata: { id: testUserId1 },
|
|
176
|
+
email_confirm: true,
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
await supabaseAdmin.auth.admin.createUser({
|
|
180
|
+
email: 'user2@test.com',
|
|
181
|
+
password: 'password123',
|
|
182
|
+
user_metadata: { id: testUserId2 },
|
|
183
|
+
email_confirm: true,
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// Get tokens
|
|
187
|
+
const { data: session1 } = await supabaseAdmin.auth.signInWithPassword({
|
|
188
|
+
email: 'user1@test.com',
|
|
189
|
+
password: 'password123',
|
|
190
|
+
})
|
|
191
|
+
user1Token = session1.session!.access_token
|
|
192
|
+
|
|
193
|
+
const { data: session2 } = await supabaseAdmin.auth.signInWithPassword({
|
|
194
|
+
email: 'user2@test.com',
|
|
195
|
+
password: 'password123',
|
|
196
|
+
})
|
|
197
|
+
user2Token = session2.session!.access_token
|
|
198
|
+
|
|
199
|
+
// Create test data
|
|
200
|
+
await supabaseAdmin.from('posts').insert([
|
|
201
|
+
{ id: 'post-1', user_id: testUserId1, title: 'User 1 Post', content: 'Content' },
|
|
202
|
+
{ id: 'post-2', user_id: testUserId2, title: 'User 2 Post', content: 'Content' },
|
|
203
|
+
])
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
afterAll(async () => {
|
|
207
|
+
// Cleanup
|
|
208
|
+
await supabaseAdmin.from('posts').delete().in('id', ['post-1', 'post-2'])
|
|
209
|
+
await supabaseAdmin.auth.admin.deleteUser(testUserId1)
|
|
210
|
+
await supabaseAdmin.auth.admin.deleteUser(testUserId2)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('users can only read their own posts', async () => {
|
|
214
|
+
const user1Client = createTestClient(user1Token)
|
|
215
|
+
const user2Client = createTestClient(user2Token)
|
|
216
|
+
|
|
217
|
+
// User 1 can read their own post
|
|
218
|
+
const { data: user1Posts } = await user1Client.from('posts').select('*')
|
|
219
|
+
expect(user1Posts).toHaveLength(1)
|
|
220
|
+
expect(user1Posts![0].id).toBe('post-1')
|
|
221
|
+
|
|
222
|
+
// User 2 can only read their own post
|
|
223
|
+
const { data: user2Posts } = await user2Client.from('posts').select('*')
|
|
224
|
+
expect(user2Posts).toHaveLength(1)
|
|
225
|
+
expect(user2Posts![0].id).toBe('post-2')
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('users cannot update other users posts', async () => {
|
|
229
|
+
const user1Client = createTestClient(user1Token)
|
|
230
|
+
|
|
231
|
+
const { error } = await user1Client
|
|
232
|
+
.from('posts')
|
|
233
|
+
.update({ title: 'Hacked!' })
|
|
234
|
+
.eq('id', 'post-2')
|
|
235
|
+
|
|
236
|
+
// Update should fail or affect 0 rows
|
|
237
|
+
expect(error).toBeDefined()
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it('users cannot delete other users posts', async () => {
|
|
241
|
+
const user1Client = createTestClient(user1Token)
|
|
242
|
+
|
|
243
|
+
const { error } = await user1Client
|
|
244
|
+
.from('posts')
|
|
245
|
+
.delete()
|
|
246
|
+
.eq('id', 'post-2')
|
|
247
|
+
|
|
248
|
+
expect(error).toBeDefined()
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('unauthenticated users cannot access posts', async () => {
|
|
252
|
+
const anonClient = createTestClient() // No token
|
|
253
|
+
|
|
254
|
+
const { data, error } = await anonClient.from('posts').select('*')
|
|
255
|
+
|
|
256
|
+
expect(data).toHaveLength(0) // or expect error
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Testing Organization-Based RLS
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
describe('Organization RLS Policies', () => {
|
|
265
|
+
it('org members can access org resources', async () => {
|
|
266
|
+
// Setup: Create org, add member
|
|
267
|
+
const orgId = 'test-org-1'
|
|
268
|
+
const memberId = testUserId1
|
|
269
|
+
const nonMemberId = testUserId2
|
|
270
|
+
|
|
271
|
+
await supabaseAdmin.from('organizations').insert({ id: orgId, name: 'Test Org' })
|
|
272
|
+
await supabaseAdmin.from('org_members').insert({ org_id: orgId, user_id: memberId })
|
|
273
|
+
await supabaseAdmin.from('org_resources').insert({
|
|
274
|
+
id: 'resource-1',
|
|
275
|
+
org_id: orgId,
|
|
276
|
+
name: 'Secret Doc',
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
// Member can access
|
|
280
|
+
const memberClient = createTestClient(user1Token)
|
|
281
|
+
const { data: memberData } = await memberClient
|
|
282
|
+
.from('org_resources')
|
|
283
|
+
.select('*')
|
|
284
|
+
.eq('org_id', orgId)
|
|
285
|
+
|
|
286
|
+
expect(memberData).toHaveLength(1)
|
|
287
|
+
|
|
288
|
+
// Non-member cannot access
|
|
289
|
+
const nonMemberClient = createTestClient(user2Token)
|
|
290
|
+
const { data: nonMemberData } = await nonMemberClient
|
|
291
|
+
.from('org_resources')
|
|
292
|
+
.select('*')
|
|
293
|
+
.eq('org_id', orgId)
|
|
294
|
+
|
|
295
|
+
expect(nonMemberData).toHaveLength(0)
|
|
296
|
+
})
|
|
297
|
+
})
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Integration Test Patterns
|
|
301
|
+
|
|
302
|
+
### Testing Database Operations
|
|
303
|
+
```typescript
|
|
304
|
+
// tests/integration/posts.test.ts
|
|
305
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
306
|
+
import { supabaseAdmin, createTestClient } from '../setup/supabase'
|
|
307
|
+
|
|
308
|
+
describe('Posts CRUD', () => {
|
|
309
|
+
const testUserId = 'd0d8c4c0-0000-0000-0000-000000000001'
|
|
310
|
+
let userToken: string
|
|
311
|
+
const createdPostIds: string[] = []
|
|
312
|
+
|
|
313
|
+
beforeEach(async () => {
|
|
314
|
+
// Setup authenticated user
|
|
315
|
+
const { data } = await supabaseAdmin.auth.signInWithPassword({
|
|
316
|
+
email: 'testuser@example.com',
|
|
317
|
+
password: 'password123',
|
|
318
|
+
})
|
|
319
|
+
userToken = data.session!.access_token
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
afterEach(async () => {
|
|
323
|
+
// Cleanup created posts
|
|
324
|
+
if (createdPostIds.length > 0) {
|
|
325
|
+
await supabaseAdmin.from('posts').delete().in('id', createdPostIds)
|
|
326
|
+
createdPostIds.length = 0
|
|
327
|
+
}
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it('should create a post', async () => {
|
|
331
|
+
const client = createTestClient(userToken)
|
|
332
|
+
|
|
333
|
+
const { data, error } = await client
|
|
334
|
+
.from('posts')
|
|
335
|
+
.insert({
|
|
336
|
+
title: 'Test Post',
|
|
337
|
+
content: 'Test content',
|
|
338
|
+
user_id: testUserId,
|
|
339
|
+
})
|
|
340
|
+
.select()
|
|
341
|
+
.single()
|
|
342
|
+
|
|
343
|
+
expect(error).toBeNull()
|
|
344
|
+
expect(data).toMatchObject({
|
|
345
|
+
title: 'Test Post',
|
|
346
|
+
content: 'Test content',
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
createdPostIds.push(data!.id)
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
it('should update a post', async () => {
|
|
353
|
+
const client = createTestClient(userToken)
|
|
354
|
+
|
|
355
|
+
// Create
|
|
356
|
+
const { data: created } = await client
|
|
357
|
+
.from('posts')
|
|
358
|
+
.insert({ title: 'Original', content: 'Content', user_id: testUserId })
|
|
359
|
+
.select()
|
|
360
|
+
.single()
|
|
361
|
+
|
|
362
|
+
createdPostIds.push(created!.id)
|
|
363
|
+
|
|
364
|
+
// Update
|
|
365
|
+
const { data: updated, error } = await client
|
|
366
|
+
.from('posts')
|
|
367
|
+
.update({ title: 'Updated' })
|
|
368
|
+
.eq('id', created!.id)
|
|
369
|
+
.select()
|
|
370
|
+
.single()
|
|
371
|
+
|
|
372
|
+
expect(error).toBeNull()
|
|
373
|
+
expect(updated!.title).toBe('Updated')
|
|
374
|
+
})
|
|
375
|
+
})
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
## CI/CD Integration
|
|
379
|
+
|
|
380
|
+
### GitHub Actions Workflow
|
|
381
|
+
```yaml
|
|
382
|
+
# .github/workflows/test.yml
|
|
383
|
+
name: Test
|
|
384
|
+
|
|
385
|
+
on: [push, pull_request]
|
|
386
|
+
|
|
387
|
+
jobs:
|
|
388
|
+
test:
|
|
389
|
+
runs-on: ubuntu-latest
|
|
390
|
+
|
|
391
|
+
steps:
|
|
392
|
+
- uses: actions/checkout@v4
|
|
393
|
+
|
|
394
|
+
- name: Setup Node.js
|
|
395
|
+
uses: actions/setup-node@v4
|
|
396
|
+
with:
|
|
397
|
+
node-version: '20'
|
|
398
|
+
|
|
399
|
+
- name: Install dependencies
|
|
400
|
+
run: npm ci
|
|
401
|
+
|
|
402
|
+
- name: Setup Supabase CLI
|
|
403
|
+
uses: supabase/setup-cli@v1
|
|
404
|
+
with:
|
|
405
|
+
version: latest
|
|
406
|
+
|
|
407
|
+
- name: Start Supabase
|
|
408
|
+
run: supabase start
|
|
409
|
+
|
|
410
|
+
- name: Run migrations
|
|
411
|
+
run: supabase db reset
|
|
412
|
+
|
|
413
|
+
- name: Generate types
|
|
414
|
+
run: supabase gen types typescript --local > src/types/database.types.ts
|
|
415
|
+
|
|
416
|
+
- name: Run tests
|
|
417
|
+
run: npm test
|
|
418
|
+
env:
|
|
419
|
+
SUPABASE_URL: http://127.0.0.1:54321
|
|
420
|
+
SUPABASE_ANON_KEY: ${{ secrets.LOCAL_ANON_KEY }}
|
|
421
|
+
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.LOCAL_SERVICE_ROLE_KEY }}
|
|
422
|
+
|
|
423
|
+
- name: Stop Supabase
|
|
424
|
+
if: always()
|
|
425
|
+
run: supabase stop
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
## Test Utilities
|
|
429
|
+
|
|
430
|
+
### Factory Functions
|
|
431
|
+
```typescript
|
|
432
|
+
// tests/factories/user.ts
|
|
433
|
+
import { supabaseAdmin } from '../setup/supabase'
|
|
434
|
+
|
|
435
|
+
export const createTestUser = async (overrides: Partial<{
|
|
436
|
+
email: string
|
|
437
|
+
password: string
|
|
438
|
+
metadata: Record<string, unknown>
|
|
439
|
+
}> = {}) => {
|
|
440
|
+
const email = overrides.email ?? `test-${Date.now()}@example.com`
|
|
441
|
+
const password = overrides.password ?? 'password123'
|
|
442
|
+
|
|
443
|
+
const { data, error } = await supabaseAdmin.auth.admin.createUser({
|
|
444
|
+
email,
|
|
445
|
+
password,
|
|
446
|
+
email_confirm: true,
|
|
447
|
+
user_metadata: overrides.metadata ?? {},
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
if (error) throw error
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
user: data.user,
|
|
454
|
+
cleanup: async () => {
|
|
455
|
+
await supabaseAdmin.auth.admin.deleteUser(data.user.id)
|
|
456
|
+
},
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// tests/factories/post.ts
|
|
461
|
+
export const createTestPost = async (userId: string, overrides: Partial<{
|
|
462
|
+
title: string
|
|
463
|
+
content: string
|
|
464
|
+
published: boolean
|
|
465
|
+
}> = {}) => {
|
|
466
|
+
const { data, error } = await supabaseAdmin
|
|
467
|
+
.from('posts')
|
|
468
|
+
.insert({
|
|
469
|
+
title: overrides.title ?? 'Test Post',
|
|
470
|
+
content: overrides.content ?? 'Test content',
|
|
471
|
+
user_id: userId,
|
|
472
|
+
published: overrides.published ?? false,
|
|
473
|
+
})
|
|
474
|
+
.select()
|
|
475
|
+
.single()
|
|
476
|
+
|
|
477
|
+
if (error) throw error
|
|
478
|
+
|
|
479
|
+
return {
|
|
480
|
+
post: data,
|
|
481
|
+
cleanup: async () => {
|
|
482
|
+
await supabaseAdmin.from('posts').delete().eq('id', data.id)
|
|
483
|
+
},
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### Using Factories in Tests
|
|
489
|
+
```typescript
|
|
490
|
+
describe('Post visibility', () => {
|
|
491
|
+
it('published posts are visible to all', async () => {
|
|
492
|
+
const { user, cleanup: cleanupUser } = await createTestUser()
|
|
493
|
+
const { post, cleanup: cleanupPost } = await createTestPost(user.id, {
|
|
494
|
+
published: true,
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
try {
|
|
498
|
+
const anonClient = createTestClient()
|
|
499
|
+
const { data } = await anonClient
|
|
500
|
+
.from('posts')
|
|
501
|
+
.select('*')
|
|
502
|
+
.eq('id', post.id)
|
|
503
|
+
.single()
|
|
504
|
+
|
|
505
|
+
expect(data).toBeDefined()
|
|
506
|
+
expect(data!.id).toBe(post.id)
|
|
507
|
+
} finally {
|
|
508
|
+
await cleanupPost()
|
|
509
|
+
await cleanupUser()
|
|
510
|
+
}
|
|
511
|
+
})
|
|
512
|
+
})
|
|
513
|
+
```
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: task-analyzer
|
|
3
|
+
description: Analyzes task essence and selects appropriate skills. Returns scale estimates and metadata. Use when starting tasks or selecting skills.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Task Analyzer
|
|
7
|
+
|
|
8
|
+
Provides metacognitive task analysis and skill selection guidance.
|
|
9
|
+
|
|
10
|
+
## Skills Index
|
|
11
|
+
|
|
12
|
+
See **[skills-index.yaml](references/skills-index.yaml)** for available skills metadata.
|
|
13
|
+
|
|
14
|
+
## Task Analysis Process
|
|
15
|
+
|
|
16
|
+
### 1. Understand Task Essence
|
|
17
|
+
|
|
18
|
+
Identify the fundamental purpose beyond surface-level work:
|
|
19
|
+
|
|
20
|
+
| Surface Work | Fundamental Purpose |
|
|
21
|
+
|--------------|---------------------|
|
|
22
|
+
| "Fix this bug" | Problem solving, root cause analysis |
|
|
23
|
+
| "Implement this feature" | Feature addition, value delivery |
|
|
24
|
+
| "Refactor this code" | Quality improvement, maintainability |
|
|
25
|
+
| "Update this file" | Change management, consistency |
|
|
26
|
+
|
|
27
|
+
**Key Questions:**
|
|
28
|
+
- What problem are we really solving?
|
|
29
|
+
- What is the expected outcome?
|
|
30
|
+
- What could go wrong if we approach this superficially?
|
|
31
|
+
|
|
32
|
+
### 2. Estimate Task Scale
|
|
33
|
+
|
|
34
|
+
| Scale | File Count | Indicators |
|
|
35
|
+
|-------|------------|------------|
|
|
36
|
+
| Small | 1-2 | Single function/component change |
|
|
37
|
+
| Medium | 3-5 | Multiple related components |
|
|
38
|
+
| Large | 6+ | Cross-cutting concerns, architecture impact |
|
|
39
|
+
|
|
40
|
+
**Scale affects skill priority:**
|
|
41
|
+
- Larger scale → process/documentation skills more important
|
|
42
|
+
- Smaller scale → implementation skills more focused
|
|
43
|
+
|
|
44
|
+
### 3. Identify Task Type
|
|
45
|
+
|
|
46
|
+
| Type | Characteristics | Key Skills |
|
|
47
|
+
|------|-----------------|------------|
|
|
48
|
+
| Implementation | New code, features | coding-standards, typescript-testing |
|
|
49
|
+
| Fix | Bug resolution | coding-standards, typescript-testing |
|
|
50
|
+
| Refactoring | Structure improvement | coding-standards, implementation-approach |
|
|
51
|
+
| Design | Architecture decisions | documentation-criteria, implementation-approach |
|
|
52
|
+
| Quality | Testing, review | typescript-testing, integration-e2e-testing |
|
|
53
|
+
|
|
54
|
+
### 4. Tag-Based Skill Matching
|
|
55
|
+
|
|
56
|
+
Extract relevant tags from task description and match against skills-index.yaml:
|
|
57
|
+
|
|
58
|
+
```yaml
|
|
59
|
+
Task: "Implement user authentication with tests"
|
|
60
|
+
Extracted tags: [implementation, testing, security]
|
|
61
|
+
Matched skills:
|
|
62
|
+
- coding-standards (implementation, security)
|
|
63
|
+
- typescript-testing (testing)
|
|
64
|
+
- typescript-rules (implementation)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 5. Implicit Relationships
|
|
68
|
+
|
|
69
|
+
Consider hidden dependencies:
|
|
70
|
+
|
|
71
|
+
| Task Involves | Also Include |
|
|
72
|
+
|---------------|--------------|
|
|
73
|
+
| Error handling | debugging, testing |
|
|
74
|
+
| New features | design, implementation, documentation |
|
|
75
|
+
| Performance | profiling, optimization, testing |
|
|
76
|
+
| Frontend | typescript-rules, typescript-testing |
|
|
77
|
+
| API/Integration | integration-e2e-testing |
|
|
78
|
+
|
|
79
|
+
## Output Format
|
|
80
|
+
|
|
81
|
+
Return structured analysis with skill metadata from skills-index.yaml:
|
|
82
|
+
|
|
83
|
+
```yaml
|
|
84
|
+
taskAnalysis:
|
|
85
|
+
essence: <string> # Fundamental purpose identified
|
|
86
|
+
type: <implementation|fix|refactoring|design|quality>
|
|
87
|
+
scale: <small|medium|large>
|
|
88
|
+
estimatedFiles: <number>
|
|
89
|
+
tags: [<string>, ...] # Extracted from task description
|
|
90
|
+
|
|
91
|
+
selectedSkills:
|
|
92
|
+
- skill: <skill-name> # From skills-index.yaml
|
|
93
|
+
priority: <high|medium|low>
|
|
94
|
+
reason: <string> # Why this skill was selected
|
|
95
|
+
# Pass through metadata from skills-index.yaml
|
|
96
|
+
tags: [...]
|
|
97
|
+
typical-use: <string>
|
|
98
|
+
size: <small|medium|large>
|
|
99
|
+
sections: [...] # All sections from yaml, unfiltered
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Note**: Section selection (choosing which sections are relevant) is done separately after reading the actual SKILL.md files.
|
|
103
|
+
|
|
104
|
+
## Skill Selection Priority
|
|
105
|
+
|
|
106
|
+
1. **Essential** - Directly related to task type
|
|
107
|
+
2. **Quality** - Testing and quality assurance
|
|
108
|
+
3. **Process** - Workflow and documentation
|
|
109
|
+
4. **Supplementary** - Reference and best practices
|
|
110
|
+
|
|
111
|
+
## Metacognitive Question Design
|
|
112
|
+
|
|
113
|
+
Generate 3-5 questions according to task nature:
|
|
114
|
+
|
|
115
|
+
| Task Type | Question Focus |
|
|
116
|
+
|-----------|----------------|
|
|
117
|
+
| Implementation | Design validity, edge cases, performance |
|
|
118
|
+
| Fix | Root cause (5 Whys), impact scope, regression testing |
|
|
119
|
+
| Refactoring | Current problems, target state, phased plan |
|
|
120
|
+
| Design | Requirement clarity, future extensibility, trade-offs |
|
|
121
|
+
|
|
122
|
+
## Warning Patterns
|
|
123
|
+
|
|
124
|
+
Detect and flag these patterns:
|
|
125
|
+
|
|
126
|
+
| Pattern | Warning | Mitigation |
|
|
127
|
+
|---------|---------|------------|
|
|
128
|
+
| Large change at once | High risk | Split into phases |
|
|
129
|
+
| Implementation without tests | Quality risk | Follow TDD |
|
|
130
|
+
| Immediate fix on error | Root cause missed | Pause, analyze |
|
|
131
|
+
| Coding without plan | Scope creep | Plan first |
|