plan-flow-skill 1.0.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/.claude/commands/create-contract.md +468 -0
- package/.claude/commands/create-plan.md +512 -0
- package/.claude/commands/discovery-plan.md +561 -0
- package/.claude/commands/execute-plan.md +682 -0
- package/.claude/commands/review-code.md +459 -0
- package/.claude/commands/review-pr.md +651 -0
- package/.claude/commands/setup.md +1609 -0
- package/.claude/commands/write-tests.md +543 -0
- package/.claude/rules/core/allowed-patterns.md +175 -0
- package/.claude/rules/core/complexity-scoring.md +225 -0
- package/.claude/rules/core/forbidden-patterns.md +253 -0
- package/.claude/rules/languages/python-patterns.md +6 -0
- package/.claude/rules/languages/typescript-patterns.md +7 -0
- package/.claude/rules/patterns/contract-patterns.md +332 -0
- package/.claude/rules/patterns/discovery-patterns.md +342 -0
- package/.claude/rules/patterns/discovery-templates.md +319 -0
- package/.claude/rules/patterns/jest-patterns.md +482 -0
- package/.claude/rules/patterns/plans-patterns.md +225 -0
- package/.claude/rules/patterns/plans-templates.md +227 -0
- package/.claude/rules/patterns/pytest-patterns.md +457 -0
- package/.claude/rules/patterns/review-code-templates.md +305 -0
- package/.claude/rules/patterns/review-pr-patterns.md +360 -0
- package/.claude/rules/tools/auth-pr-tool.md +30 -0
- package/.claude/rules/tools/interactive-questions-tool.md +235 -0
- package/.claude/rules/tools/jest-testing-tool.md +73 -0
- package/.claude/rules/tools/plan-mode-tool.md +164 -0
- package/.claude/rules/tools/pytest-testing-tool.md +121 -0
- package/.claude/rules/tools/reference-expansion-tool.md +326 -0
- package/LICENSE +21 -0
- package/README.md +167 -0
- package/dist/cli/commands/init.d.ts +6 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +139 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/handlers/claude.d.ts +9 -0
- package/dist/cli/handlers/claude.d.ts.map +1 -0
- package/dist/cli/handlers/claude.js +119 -0
- package/dist/cli/handlers/claude.js.map +1 -0
- package/dist/cli/handlers/codex.d.ts +9 -0
- package/dist/cli/handlers/codex.d.ts.map +1 -0
- package/dist/cli/handlers/codex.js +100 -0
- package/dist/cli/handlers/codex.js.map +1 -0
- package/dist/cli/handlers/cursor.d.ts +8 -0
- package/dist/cli/handlers/cursor.d.ts.map +1 -0
- package/dist/cli/handlers/cursor.js +34 -0
- package/dist/cli/handlers/cursor.js.map +1 -0
- package/dist/cli/handlers/openclaw.d.ts +8 -0
- package/dist/cli/handlers/openclaw.d.ts.map +1 -0
- package/dist/cli/handlers/openclaw.js +34 -0
- package/dist/cli/handlers/openclaw.js.map +1 -0
- package/dist/cli/handlers/shared.d.ts +9 -0
- package/dist/cli/handlers/shared.d.ts.map +1 -0
- package/dist/cli/handlers/shared.js +44 -0
- package/dist/cli/handlers/shared.js.map +1 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +43 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/types.d.ts +26 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/cli/types.js +5 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/cli/utils/files.d.ts +37 -0
- package/dist/cli/utils/files.d.ts.map +1 -0
- package/dist/cli/utils/files.js +122 -0
- package/dist/cli/utils/files.js.map +1 -0
- package/dist/cli/utils/logger.d.ts +11 -0
- package/dist/cli/utils/logger.d.ts.map +1 -0
- package/dist/cli/utils/logger.js +34 -0
- package/dist/cli/utils/logger.js.map +1 -0
- package/dist/cli/utils/prompts.d.ts +10 -0
- package/dist/cli/utils/prompts.d.ts.map +1 -0
- package/dist/cli/utils/prompts.js +65 -0
- package/dist/cli/utils/prompts.js.map +1 -0
- package/dist/test/setup.d.ts +5 -0
- package/dist/test/setup.d.ts.map +1 -0
- package/dist/test/setup.js +7 -0
- package/dist/test/setup.js.map +1 -0
- package/package.json +63 -0
- package/rules/core/_index.mdc +89 -0
- package/rules/core/allowed-patterns.mdc +185 -0
- package/rules/core/complexity-scoring.mdc +235 -0
- package/rules/core/forbidden-patterns.mdc +263 -0
- package/rules/languages/_index.mdc +80 -0
- package/rules/languages/python-patterns.mdc +188 -0
- package/rules/languages/typescript-patterns.mdc +128 -0
- package/rules/patterns/_index.mdc +185 -0
- package/rules/patterns/contract-patterns.mdc +344 -0
- package/rules/patterns/discovery-patterns.mdc +354 -0
- package/rules/patterns/discovery-templates.mdc +329 -0
- package/rules/patterns/jest-patterns.mdc +492 -0
- package/rules/patterns/plans-patterns.mdc +237 -0
- package/rules/patterns/plans-templates.mdc +237 -0
- package/rules/patterns/pytest-patterns.mdc +467 -0
- package/rules/patterns/review-code-templates.mdc +315 -0
- package/rules/patterns/review-pr-patterns.mdc +370 -0
- package/rules/skills/_index.mdc +174 -0
- package/rules/skills/create-contract-skill.mdc +239 -0
- package/rules/skills/create-plan-skill.mdc +271 -0
- package/rules/skills/discovery-skill.mdc +295 -0
- package/rules/skills/execute-plan-skill.mdc +388 -0
- package/rules/skills/review-code-skill.mdc +308 -0
- package/rules/skills/review-pr-skill.mdc +496 -0
- package/rules/skills/setup-skill.mdc +923 -0
- package/rules/skills/write-tests-skill.mdc +294 -0
- package/rules/templates/index-template.mdc +126 -0
- package/rules/tools/_index.mdc +114 -0
- package/rules/tools/auth-pr-tool.mdc +362 -0
- package/rules/tools/interactive-questions-tool.mdc +337 -0
- package/rules/tools/jest-testing-tool.mdc +96 -0
- package/rules/tools/plan-mode-tool.mdc +229 -0
- package/rules/tools/pytest-testing-tool.mdc +144 -0
- package/rules/tools/reference-expansion-tool.mdc +338 -0
- package/skills/plan-flow/SKILL.md +109 -0
- package/skills/plan-flow/create-contract/SKILL.md +139 -0
- package/skills/plan-flow/create-plan/SKILL.md +93 -0
- package/skills/plan-flow/discovery/SKILL.md +85 -0
- package/skills/plan-flow/execute-plan/SKILL.md +89 -0
- package/skills/plan-flow/review-code/SKILL.md +100 -0
- package/skills/plan-flow/review-pr/SKILL.md +122 -0
- package/skills/plan-flow/setup/SKILL.md +73 -0
- package/skills/plan-flow/write-tests/SKILL.md +115 -0
- package/templates/shared/AGENTS.md.template +60 -0
- package/templates/shared/CLAUDE.md.template +62 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Patterns and conventions for writing Jest tests in JavaScript/TypeScript"
|
|
3
|
+
alwaysApply: false
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Jest Testing Patterns
|
|
7
|
+
|
|
8
|
+
Patterns and conventions for writing Jest tests in JavaScript/TypeScript codebases, ensuring consistency, maintainability, and comprehensive coverage.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## File Naming Conventions
|
|
13
|
+
|
|
14
|
+
Tests must follow specific naming conventions based on their execution environment:
|
|
15
|
+
|
|
16
|
+
| Suffix | Environment | Purpose |
|
|
17
|
+
| ------ | ----------- | ------- |
|
|
18
|
+
| `*.client.test.ts` | Client-side (jsdom) | React hooks, components, client utilities |
|
|
19
|
+
| `*.server.test.ts` | Server-side (node) | API routes, commands, server utilities |
|
|
20
|
+
| `*.test.ts` | Default (node) | General utilities without specific environment needs |
|
|
21
|
+
| `*.test.tsx` | JSX support | Components that need JSX in tests |
|
|
22
|
+
|
|
23
|
+
**Examples**:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
src/components/chat/MessageInput/useMessageInputLogic.client.test.ts
|
|
27
|
+
src/commands/streamChatCommand.server.test.ts
|
|
28
|
+
src/utils/linkRewriter.client.test.ts
|
|
29
|
+
src/app/api/chat/stream/route.server.test.ts
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Test File Location
|
|
35
|
+
|
|
36
|
+
Place test files next to the code they test (co-location pattern):
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
src/
|
|
40
|
+
├── components/
|
|
41
|
+
│ └── chat/
|
|
42
|
+
│ └── MessageInput/
|
|
43
|
+
│ ├── index.tsx
|
|
44
|
+
│ ├── useMessageInputLogic.internal.ts
|
|
45
|
+
│ └── useMessageInputLogic.client.test.ts # Co-located
|
|
46
|
+
├── commands/
|
|
47
|
+
│ ├── streamChatCommand.ts
|
|
48
|
+
│ └── streamChatCommand.server.test.ts # Co-located
|
|
49
|
+
├── utils/
|
|
50
|
+
│ ├── linkRewriter.ts
|
|
51
|
+
│ └── linkRewriter.client.test.ts # Co-located
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Why**: Co-location makes tests easy to find, encourages testing, and ensures tests are updated when code changes.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Test Structure
|
|
59
|
+
|
|
60
|
+
Use the **Arrange-Act-Assert** (AAA) pattern with descriptive `describe` and `it` blocks:
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
describe('streamChatCommand', () => {
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
jest.clearAllMocks()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('chat validation', () => {
|
|
69
|
+
it('should throw BadRequestError if chat does not exist', async () => {
|
|
70
|
+
// Arrange
|
|
71
|
+
mockDatabase.findChatWithUsers.mockResolvedValue(null)
|
|
72
|
+
|
|
73
|
+
// Act & Assert
|
|
74
|
+
await expect(streamChatCommand(createValidInput())).rejects.toThrow(
|
|
75
|
+
new BadRequestError(BadRequestReason.NOT_FOUND, { chatId: 'chat-1' }),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
expect(mockDatabase.findChatWithUsers).toHaveBeenCalledWith('chat-1')
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Guidelines**:
|
|
85
|
+
|
|
86
|
+
- Group related tests in `describe` blocks
|
|
87
|
+
- Use descriptive test names that explain the expected behavior
|
|
88
|
+
- Start test names with "should" for consistency
|
|
89
|
+
- One assertion per test when possible (or related assertions)
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Server Test Setup
|
|
94
|
+
|
|
95
|
+
For server-side tests, use the centralized mock setup:
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import { mockDatabase } from '@/test-utils/setup-server-test' // MUST be first import
|
|
99
|
+
import { streamChatCommand } from './streamChatCommand'
|
|
100
|
+
import { JupiterEngineAPI } from '@/datasources/JupiterEngineAPI'
|
|
101
|
+
import { BadRequestError, BadRequestReason } from '@/types/errors'
|
|
102
|
+
|
|
103
|
+
// Mock external dependencies
|
|
104
|
+
jest.mock('@/datasources/JupiterEngineAPI')
|
|
105
|
+
|
|
106
|
+
const mockJupiterEngineAPI = JupiterEngineAPI as jest.MockedClass<
|
|
107
|
+
typeof JupiterEngineAPI
|
|
108
|
+
>
|
|
109
|
+
|
|
110
|
+
describe('streamChatCommand', () => {
|
|
111
|
+
beforeEach(() => {
|
|
112
|
+
jest.clearAllMocks()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// ... tests
|
|
116
|
+
})
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Critical**: Import `@/test-utils/setup-server-test` **before** any other `@/` imports. This ensures the database mock is hoisted correctly.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Client Test Setup
|
|
124
|
+
|
|
125
|
+
For client-side tests, mock stores and providers explicitly:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
import { act, renderHook } from '@testing-library/react'
|
|
129
|
+
import { useMessageInputLogic } from './useMessageInputLogic.internal'
|
|
130
|
+
|
|
131
|
+
// Mock stores
|
|
132
|
+
jest.mock('@/stores/chatStore', () => ({
|
|
133
|
+
useChatStore: jest.fn(),
|
|
134
|
+
}))
|
|
135
|
+
|
|
136
|
+
jest.mock('@/stores/selectedAgentStore', () => ({
|
|
137
|
+
useSelectedAgentStore: jest.fn(),
|
|
138
|
+
}))
|
|
139
|
+
|
|
140
|
+
// Get typed mocks
|
|
141
|
+
const { useChatStore } = jest.requireMock('@/stores/chatStore')
|
|
142
|
+
const { useSelectedAgentStore } = jest.requireMock('@/stores/selectedAgentStore')
|
|
143
|
+
|
|
144
|
+
describe('useMessageInputLogic', () => {
|
|
145
|
+
beforeEach(() => {
|
|
146
|
+
jest.clearAllMocks()
|
|
147
|
+
|
|
148
|
+
// Setup default mock implementations
|
|
149
|
+
useChatStore.mockImplementation((selector) => {
|
|
150
|
+
if (typeof selector === 'function') {
|
|
151
|
+
return selector(mockStoreState)
|
|
152
|
+
}
|
|
153
|
+
return mockStoreState
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
// ... tests
|
|
158
|
+
})
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Mock Factories
|
|
164
|
+
|
|
165
|
+
Use type-safe mock factories from `@/types/mocks` for consistent test data:
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
import {
|
|
169
|
+
createMockUser,
|
|
170
|
+
createMockChatWithUsers,
|
|
171
|
+
createMockMessageWithUser,
|
|
172
|
+
createMockAgent,
|
|
173
|
+
} from '@/types/mocks'
|
|
174
|
+
|
|
175
|
+
describe('chat feature', () => {
|
|
176
|
+
it('should handle user message', async () => {
|
|
177
|
+
const mockUser = createMockUser({ id: 'user-1', name: 'Test User' })
|
|
178
|
+
const mockChat = createMockChatWithUsers({ tenantId: 'tenant-1' })
|
|
179
|
+
const mockMessage = createMockMessageWithUser({
|
|
180
|
+
id: 'msg-1',
|
|
181
|
+
message: 'Hello, world!',
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
// ... test logic
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**Creating Mock Factories**:
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
// In @/types/mocks.ts
|
|
193
|
+
export const createMockEntity = (overrides?: Partial<Entity>): Entity => ({
|
|
194
|
+
id: 'entity-1',
|
|
195
|
+
name: 'Default Name',
|
|
196
|
+
createdAt: new Date('2024-01-01'),
|
|
197
|
+
// ... all required fields with sensible defaults
|
|
198
|
+
...overrides, // Allow overrides
|
|
199
|
+
})
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Testing Logic Hooks
|
|
205
|
+
|
|
206
|
+
Test logic hooks in isolation using `renderHook`:
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
import { renderHook, act } from '@testing-library/react'
|
|
210
|
+
import { useMessageInputLogic } from './useMessageInputLogic.internal'
|
|
211
|
+
|
|
212
|
+
describe('useMessageInputLogic', () => {
|
|
213
|
+
const createBaseProps = (overrides = {}) => ({
|
|
214
|
+
chatId: 'chat-1',
|
|
215
|
+
input: '',
|
|
216
|
+
handleChange: jest.fn(),
|
|
217
|
+
handleSubmit: jest.fn(),
|
|
218
|
+
...overrides,
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('should update input value on change', () => {
|
|
222
|
+
const handleChange = jest.fn()
|
|
223
|
+
const { result } = renderHook(() =>
|
|
224
|
+
useMessageInputLogic(createBaseProps({ handleChange })),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
act(() => {
|
|
228
|
+
result.current.handleChange({
|
|
229
|
+
target: { value: 'Hello' },
|
|
230
|
+
} as React.ChangeEvent<HTMLTextAreaElement>)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
expect(handleChange).toHaveBeenCalled()
|
|
234
|
+
})
|
|
235
|
+
})
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Mocking Dependencies
|
|
241
|
+
|
|
242
|
+
### Mocking Modules
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
// Full module mock
|
|
246
|
+
jest.mock('@/datasources/JupiterEngineAPI')
|
|
247
|
+
|
|
248
|
+
// Partial module mock
|
|
249
|
+
jest.mock('@/utils/streamProcessing', () => ({
|
|
250
|
+
processAssistantStream: jest.fn(),
|
|
251
|
+
toInputJson: jest.fn((data) => data), // Keep some real implementation
|
|
252
|
+
}))
|
|
253
|
+
|
|
254
|
+
// Typed mock access
|
|
255
|
+
const mockJupiterEngineAPI = JupiterEngineAPI as jest.MockedClass<
|
|
256
|
+
typeof JupiterEngineAPI
|
|
257
|
+
>
|
|
258
|
+
const mockProcessStream = processAssistantStream as jest.MockedFunction<
|
|
259
|
+
typeof processAssistantStream
|
|
260
|
+
>
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Mocking Class Instances
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
jest.mock('@/datasources/JupiterEngineAPI')
|
|
267
|
+
|
|
268
|
+
const mockJupiterEngineAPI = JupiterEngineAPI as jest.MockedClass<
|
|
269
|
+
typeof JupiterEngineAPI
|
|
270
|
+
>
|
|
271
|
+
|
|
272
|
+
beforeEach(() => {
|
|
273
|
+
mockJupiterEngineAPI.prototype.streamAgentResponse = jest
|
|
274
|
+
.fn()
|
|
275
|
+
.mockResolvedValue({
|
|
276
|
+
body: {
|
|
277
|
+
tee: () => [mockStream, mockStream],
|
|
278
|
+
},
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Mocking Zustand Stores
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
jest.mock('@/stores/chatStore', () => ({
|
|
287
|
+
useChatStore: jest.fn(),
|
|
288
|
+
}))
|
|
289
|
+
|
|
290
|
+
const { useChatStore } = jest.requireMock('@/stores/chatStore')
|
|
291
|
+
|
|
292
|
+
beforeEach(() => {
|
|
293
|
+
useChatStore.mockImplementation((selector) => {
|
|
294
|
+
if (typeof selector === 'function') {
|
|
295
|
+
return selector(mockStoreState)
|
|
296
|
+
}
|
|
297
|
+
return mockStoreState
|
|
298
|
+
})
|
|
299
|
+
})
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## Testing Async Code
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
describe('async operations', () => {
|
|
308
|
+
it('should handle successful async operation', async () => {
|
|
309
|
+
mockDatabase.findChatWithUsers.mockResolvedValue(mockChat)
|
|
310
|
+
|
|
311
|
+
const result = await streamChatCommand(createValidInput())
|
|
312
|
+
|
|
313
|
+
expect(result.stream).toBeDefined()
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
it('should handle async errors', async () => {
|
|
317
|
+
mockDatabase.findChatWithUsers.mockRejectedValue(new Error('DB Error'))
|
|
318
|
+
|
|
319
|
+
await expect(streamChatCommand(createValidInput())).rejects.toThrow('DB Error')
|
|
320
|
+
})
|
|
321
|
+
})
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Testing Error Cases
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
import { BadRequestError, ForbiddenError, JupiterEngineError } from '@/types/errors'
|
|
330
|
+
|
|
331
|
+
describe('error handling', () => {
|
|
332
|
+
it('should throw BadRequestError for missing chat', async () => {
|
|
333
|
+
mockDatabase.findChatWithUsers.mockResolvedValue(null)
|
|
334
|
+
|
|
335
|
+
await expect(streamChatCommand(createValidInput())).rejects.toThrow(
|
|
336
|
+
new BadRequestError(BadRequestReason.NOT_FOUND, { chatId: 'chat-1' }),
|
|
337
|
+
)
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it('should throw ForbiddenError for tenant mismatch', async () => {
|
|
341
|
+
mockDatabase.findChatWithUsers.mockResolvedValue(
|
|
342
|
+
createMockChatWithUsers({ tenantId: 'different-tenant' }),
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
await expect(streamChatCommand(createValidInput())).rejects.toThrow(
|
|
346
|
+
new ForbiddenError(ForbiddenReason.INVALID_TENANT_MEMBERSHIP),
|
|
347
|
+
)
|
|
348
|
+
})
|
|
349
|
+
})
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## Testing with Input Factories
|
|
355
|
+
|
|
356
|
+
Create factory functions for valid inputs to reduce boilerplate:
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
const createValidInput = (overrides = {}) => ({
|
|
360
|
+
messages: [
|
|
361
|
+
{
|
|
362
|
+
id: 'msg-1',
|
|
363
|
+
role: 'user' as const,
|
|
364
|
+
parts: [{ type: 'text' as const, text: 'Hello' }],
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
chatId: 'chat-1',
|
|
368
|
+
internalTenantId: 'tenant-1',
|
|
369
|
+
accessToken: 'token-123',
|
|
370
|
+
userId: 'user-1',
|
|
371
|
+
...overrides,
|
|
372
|
+
})
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
## Browser API Mocks
|
|
378
|
+
|
|
379
|
+
For client tests requiring browser APIs:
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
// ResizeObserver mock
|
|
383
|
+
const installResizeObserver = () => {
|
|
384
|
+
const callbacks: ResizeObserverCallback[] = []
|
|
385
|
+
|
|
386
|
+
class MockObserver implements ResizeObserver {
|
|
387
|
+
private callback: ResizeObserverCallback
|
|
388
|
+
constructor(callback: ResizeObserverCallback) {
|
|
389
|
+
this.callback = callback
|
|
390
|
+
callbacks.push(callback)
|
|
391
|
+
}
|
|
392
|
+
observe() {}
|
|
393
|
+
unobserve() {}
|
|
394
|
+
disconnect() {}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
globalThis.ResizeObserver = MockObserver as unknown as typeof ResizeObserver
|
|
398
|
+
return callbacks
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Storage mock (in jest.setup.ts)
|
|
402
|
+
const createMockStorage = () => {
|
|
403
|
+
const storage: Record<string, string> = {}
|
|
404
|
+
return {
|
|
405
|
+
getItem: jest.fn((key) => storage[key] ?? null),
|
|
406
|
+
setItem: jest.fn((key, value) => { storage[key] = value }),
|
|
407
|
+
removeItem: jest.fn((key) => { delete storage[key] }),
|
|
408
|
+
clear: jest.fn(() => Object.keys(storage).forEach(k => delete storage[k])),
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## Forbidden Patterns
|
|
416
|
+
|
|
417
|
+
### DON'T Skip Test Cleanup
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
// BAD - No cleanup
|
|
421
|
+
it('should handle error', () => {
|
|
422
|
+
jest.spyOn(console, 'error').mockImplementation()
|
|
423
|
+
// Test runs, but spy is never restored
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
// GOOD - Proper cleanup
|
|
427
|
+
it('should handle error', () => {
|
|
428
|
+
const spy = jest.spyOn(console, 'error').mockImplementation()
|
|
429
|
+
// ... test logic
|
|
430
|
+
spy.mockRestore()
|
|
431
|
+
})
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### DON'T Use Implementation Details in Tests
|
|
435
|
+
|
|
436
|
+
```typescript
|
|
437
|
+
// BAD - Testing implementation
|
|
438
|
+
it('should set internal state', () => {
|
|
439
|
+
const component = render(<MyComponent />)
|
|
440
|
+
expect(component.instance().state.isLoading).toBe(true)
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
// GOOD - Testing behavior
|
|
444
|
+
it('should show loading indicator', () => {
|
|
445
|
+
render(<MyComponent />)
|
|
446
|
+
expect(screen.getByRole('progressbar')).toBeInTheDocument()
|
|
447
|
+
})
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
### DON'T Write Tests That Always Pass
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
// BAD - No real assertion
|
|
454
|
+
it('should work', async () => {
|
|
455
|
+
await someFunction()
|
|
456
|
+
expect(true).toBe(true)
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
// GOOD - Meaningful assertion
|
|
460
|
+
it('should return processed data', async () => {
|
|
461
|
+
const result = await someFunction()
|
|
462
|
+
expect(result.data).toEqual(expectedData)
|
|
463
|
+
})
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### DON'T Ignore Async Behavior
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
// BAD - Missing await
|
|
470
|
+
it('should fetch data', () => {
|
|
471
|
+
const result = fetchData() // Returns promise, not data!
|
|
472
|
+
expect(result).toBeDefined() // Always passes
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
// GOOD - Proper async handling
|
|
476
|
+
it('should fetch data', async () => {
|
|
477
|
+
const result = await fetchData()
|
|
478
|
+
expect(result.data).toBeDefined()
|
|
479
|
+
})
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
## Summary
|
|
485
|
+
|
|
486
|
+
Following these testing patterns ensures:
|
|
487
|
+
|
|
488
|
+
- **Consistency**: Same patterns across the codebase
|
|
489
|
+
- **Maintainability**: Tests are easy to understand and update
|
|
490
|
+
- **Reliability**: Tests are isolated and deterministic
|
|
491
|
+
- **Coverage**: Edge cases and errors are tested
|
|
492
|
+
- **Speed**: Tests run efficiently with proper mocking
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Implementation planning patterns for feature development"
|
|
3
|
+
alwaysApply: false
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Implementation Plan Patterns
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
This document describes the patterns and rules for creating and executing implementation plans. Following these patterns ensures consistent, trackable, and maintainable feature development.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Workflow Folder Structure
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
flow/
|
|
18
|
+
├── archive/ # IGNORE - Completed/abandoned plans
|
|
19
|
+
├── discovery/ # Research, spikes, and exploration
|
|
20
|
+
├── plans/ # Active implementation plans
|
|
21
|
+
└── references/ # Reference materials (API contracts, specs, designs)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
| Folder | Purpose | When to Use |
|
|
25
|
+
| ------------------- | ------------------------------------------ | ---------------------------- |
|
|
26
|
+
| `flow/plans/` | Active implementation plans | Creating new feature plans |
|
|
27
|
+
| `flow/discovery/` | Research, spikes, exploration documents | Investigating before planning|
|
|
28
|
+
| `flow/references/` | Reference materials (API contracts, specs) | Storing documentation |
|
|
29
|
+
| `flow/archive/` | Completed or abandoned plans | After plan completion |
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Critical Rule: Ignore Archived Plans
|
|
34
|
+
|
|
35
|
+
**NEVER consider or reference files inside the `flow/archive/` folder.** These are outdated, superseded, or abandoned plans that should not influence current development.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Allowed Patterns
|
|
40
|
+
|
|
41
|
+
### 1. Store Plans with Snake Case Naming
|
|
42
|
+
|
|
43
|
+
All implementation plans must be created inside `flow/plans/` using snake_case naming with incremental versioning.
|
|
44
|
+
|
|
45
|
+
**Naming Convention**: `plan_<feature_name>_v<version>.md`
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
### 2. Use Discovery Folder for Research
|
|
50
|
+
|
|
51
|
+
Before creating a plan for complex features, create discovery documents in `flow/discovery/`. See `.cursor/rules/patterns/discovery-patterns.mdc` for details.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
### 3. Create Markdown Files with Required Sections
|
|
56
|
+
|
|
57
|
+
Every plan must include:
|
|
58
|
+
|
|
59
|
+
- **Overview**: Brief description
|
|
60
|
+
- **Goals**: Main objectives
|
|
61
|
+
- **Phases**: Implementation steps with complexity scores
|
|
62
|
+
- **Key Changes**: Summary of modifications
|
|
63
|
+
|
|
64
|
+
See `.cursor/rules/patterns/plans-templates.mdc` for the full template.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
### 4. Include Key Changes Summary
|
|
69
|
+
|
|
70
|
+
Every plan must end with a "Key Changes" section summarizing the most important modifications.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
### 5. Follow All Cursor Rules During Execution
|
|
75
|
+
|
|
76
|
+
On every execution of each phase, ensure compliance with all cursor rules:
|
|
77
|
+
|
|
78
|
+
- `.cursor/rules/core/allowed-patterns.mdc` - Follow allowed patterns
|
|
79
|
+
- `.cursor/rules/core/forbidden-patterns.mdc` - Avoid forbidden patterns
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
### 6. Structure Plans with Scoped Phases
|
|
84
|
+
|
|
85
|
+
Every plan must be divided into implementation phases with clear scope boundaries:
|
|
86
|
+
|
|
87
|
+
- **Scope**: What this phase covers
|
|
88
|
+
- **Complexity**: Score from 0-10
|
|
89
|
+
- **Tasks**: Checkboxes for tracking
|
|
90
|
+
- **Build Verification**: Command to verify
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
### 7. Tests Must Be the Last Phase
|
|
95
|
+
|
|
96
|
+
Always schedule testing as the final phase of implementation. Tests validate complete implementation and avoid rewriting during development.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
### 8. Verify Build Success After Each Phase
|
|
101
|
+
|
|
102
|
+
At the end of each phase, run `npm run build` to ensure no build errors.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
### 9. Minimize Code in Plan Documents
|
|
107
|
+
|
|
108
|
+
Plans should describe what to implement, not how to code it. Only include code when it serves as an important reference example.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
### 10. Move Completed Plans to Archive
|
|
113
|
+
|
|
114
|
+
When a plan is fully implemented or no longer relevant:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
mv flow/plans/plan_feature_name_v1.md flow/archive/
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
### 11. Assign Complexity Scores to Each Phase
|
|
123
|
+
|
|
124
|
+
Every phase must include a **Complexity Score** from 0-10.
|
|
125
|
+
|
|
126
|
+
See `.cursor/rules/core/complexity-scoring.mdc` for the complete scoring system.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
### 12. Execute Phases Using Plan Mode
|
|
131
|
+
|
|
132
|
+
When executing a plan, each phase must trigger **Plan mode** before implementation:
|
|
133
|
+
|
|
134
|
+
1. Use the [Plan Mode Tool](.cursor/rules/tools/plan-mode-tool.mdc) to switch to Plan mode automatically
|
|
135
|
+
2. Present phase details and approach
|
|
136
|
+
3. Get user approval
|
|
137
|
+
4. Implement
|
|
138
|
+
5. Verify build
|
|
139
|
+
6. Update progress in plan file
|
|
140
|
+
|
|
141
|
+
**Reference**: See `.cursor/rules/tools/plan-mode-tool.mdc` for complete instructions on switching to Plan mode.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Forbidden Patterns
|
|
146
|
+
|
|
147
|
+
### 1. DON'T Read or Reference Archived Plans
|
|
148
|
+
|
|
149
|
+
Only reference active plans in the `flow/plans/` folder.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
### 2. DON'T Create Plans Outside the Workflow Folder
|
|
154
|
+
|
|
155
|
+
Always create plans in the `flow/plans/` folder with proper naming.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
### 3. DON'T Use Inconsistent Naming
|
|
160
|
+
|
|
161
|
+
Use the pattern `plan_<feature_name>_v<version>.md`.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
### 4. DON'T Skip the Key Changes Section
|
|
166
|
+
|
|
167
|
+
Always include a "Key Changes" section at the end of the plan.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
### 5. DON'T Ignore Cursor Rules During Implementation
|
|
172
|
+
|
|
173
|
+
Review and follow all cursor rules during each phase.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
### 6. DON'T Create Monolithic Plans Without Phases
|
|
178
|
+
|
|
179
|
+
Break down plans into logical, scoped phases.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
### 7. DON'T Write Tests Before Implementation
|
|
184
|
+
|
|
185
|
+
Tests should always be the last phase.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
### 8. DON'T Skip Build Verification
|
|
190
|
+
|
|
191
|
+
Add `npm run build` verification at the end of each phase.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
### 9. DON'T Include Full Implementation Code in Plans
|
|
196
|
+
|
|
197
|
+
Describe what to implement; let the actual code live in source files.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
### 10. DON'T Skip Complexity Scores
|
|
202
|
+
|
|
203
|
+
Always include a complexity score for each phase.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
### 11. DON'T Assign Arbitrary Complexity Scores
|
|
208
|
+
|
|
209
|
+
Use the scoring criteria consistently. When in doubt, score higher.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
### 12. DON'T Execute Phases Without Plan Mode
|
|
214
|
+
|
|
215
|
+
Always switch to Plan mode before executing each phase group. Use the [Plan Mode Tool](.cursor/rules/tools/plan-mode-tool.mdc) to ensure proper switching.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
### 13. DON'T Mix Discovery and Plans
|
|
220
|
+
|
|
221
|
+
Keep discovery documents in `flow/discovery/` and plans in `flow/plans/`.
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Summary
|
|
226
|
+
|
|
227
|
+
Following these planning patterns ensures:
|
|
228
|
+
|
|
229
|
+
- **Discoverability**: All plans in `flow/plans/`, research in `flow/discovery/`
|
|
230
|
+
- **Traceability**: Version history and key changes documentation
|
|
231
|
+
- **Quality**: Cursor rules compliance verified at each phase
|
|
232
|
+
- **Reliability**: Build verification prevents accumulation of errors
|
|
233
|
+
- **Maintainability**: Scoped phases enable incremental, trackable progress
|
|
234
|
+
- **Testability**: Tests as the final phase ensure complete coverage
|
|
235
|
+
- **Clarity**: Archived plans are ignored, only active plans are referenced
|
|
236
|
+
- **Intelligent Pacing**: Complexity scores guide execution strategy
|
|
237
|
+
- **Collaborative Execution**: Plan mode ensures user approval before each phase
|