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,227 @@
|
|
|
1
|
+
|
|
2
|
+
## Plan Template
|
|
3
|
+
|
|
4
|
+
Use this template when creating new implementation plans:
|
|
5
|
+
|
|
6
|
+
```markdown
|
|
7
|
+
# Plan: [Feature Name]
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
[Brief description of the feature and its purpose]
|
|
12
|
+
|
|
13
|
+
**Based on Discovery**: `flow/discovery/discovery_<feature>_v1.md` (or "Discovery skipped")
|
|
14
|
+
|
|
15
|
+
## Goals
|
|
16
|
+
|
|
17
|
+
- [Goal 1]
|
|
18
|
+
- [Goal 2]
|
|
19
|
+
- [Goal 3]
|
|
20
|
+
|
|
21
|
+
## Non-Goals
|
|
22
|
+
|
|
23
|
+
- [What this plan explicitly does NOT cover]
|
|
24
|
+
|
|
25
|
+
## Requirements Summary
|
|
26
|
+
|
|
27
|
+
### Functional Requirements
|
|
28
|
+
|
|
29
|
+
- [FR-1]: [Description]
|
|
30
|
+
- [FR-2]: [Description]
|
|
31
|
+
|
|
32
|
+
### Non-Functional Requirements
|
|
33
|
+
|
|
34
|
+
- [NFR-1]: [Description]
|
|
35
|
+
|
|
36
|
+
### Constraints
|
|
37
|
+
|
|
38
|
+
- [C-1]: [Description]
|
|
39
|
+
|
|
40
|
+
## Risks
|
|
41
|
+
|
|
42
|
+
| Risk | Impact | Mitigation |
|
|
43
|
+
| -------- | --------------- | -------------------- |
|
|
44
|
+
| [Risk 1] | High/Medium/Low | [Mitigation strategy]|
|
|
45
|
+
|
|
46
|
+
## Phases
|
|
47
|
+
|
|
48
|
+
### Phase 1: [Phase Name]
|
|
49
|
+
|
|
50
|
+
**Scope**: [What this phase covers]
|
|
51
|
+
**Complexity**: X/10
|
|
52
|
+
|
|
53
|
+
- [ ] Task 1
|
|
54
|
+
- [ ] Task 2
|
|
55
|
+
|
|
56
|
+
**Build Verification**: Run `npm run build`
|
|
57
|
+
|
|
58
|
+
### Phase 2: [Phase Name]
|
|
59
|
+
|
|
60
|
+
**Scope**: [What this phase covers]
|
|
61
|
+
**Complexity**: X/10
|
|
62
|
+
|
|
63
|
+
- [ ] Task 1
|
|
64
|
+
- [ ] Task 2
|
|
65
|
+
|
|
66
|
+
**Build Verification**: Run `npm run build`
|
|
67
|
+
|
|
68
|
+
### Phase N: Tests (Final)
|
|
69
|
+
|
|
70
|
+
**Scope**: Write comprehensive tests
|
|
71
|
+
**Complexity**: X/10
|
|
72
|
+
|
|
73
|
+
- [ ] Unit tests for logic hooks
|
|
74
|
+
- [ ] Unit tests for utilities
|
|
75
|
+
- [ ] Integration tests
|
|
76
|
+
|
|
77
|
+
**Build Verification**: Run `npm run build && npm run test`
|
|
78
|
+
|
|
79
|
+
## Key Changes
|
|
80
|
+
|
|
81
|
+
1. **[Category]**: [Description of change]
|
|
82
|
+
2. **[Category]**: [Description of change]
|
|
83
|
+
3. **[Category]**: [Description of change]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Phase Templates
|
|
89
|
+
|
|
90
|
+
### Types and Schemas Phase
|
|
91
|
+
|
|
92
|
+
```markdown
|
|
93
|
+
### Phase 1: Types and Schemas
|
|
94
|
+
|
|
95
|
+
**Scope**: Define all TypeScript types and Zod schemas needed for the feature.
|
|
96
|
+
**Complexity**: 3/10
|
|
97
|
+
|
|
98
|
+
- [ ] Create type definitions in `/src/types/`
|
|
99
|
+
- [ ] Create Zod validation schemas in `/src/types/rest-inputs.ts`
|
|
100
|
+
|
|
101
|
+
**Build Verification**: Run `npm run build`
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Backend Implementation Phase
|
|
105
|
+
|
|
106
|
+
```markdown
|
|
107
|
+
### Phase 2: Backend Implementation
|
|
108
|
+
|
|
109
|
+
**Scope**: Implement API routes and commands.
|
|
110
|
+
**Complexity**: 7/10
|
|
111
|
+
|
|
112
|
+
- [ ] Create command in `/src/commands/`
|
|
113
|
+
- [ ] Create API route in `/src/app/api/`
|
|
114
|
+
- [ ] Add database operations if needed
|
|
115
|
+
|
|
116
|
+
**Build Verification**: Run `npm run build`
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Store and State Management Phase
|
|
120
|
+
|
|
121
|
+
```markdown
|
|
122
|
+
### Phase 3: Store and State Management
|
|
123
|
+
|
|
124
|
+
**Scope**: Implement Zustand stores for state management.
|
|
125
|
+
**Complexity**: 5/10
|
|
126
|
+
|
|
127
|
+
- [ ] Create or extend store in `/src/stores/`
|
|
128
|
+
- [ ] Add actions and selectors
|
|
129
|
+
|
|
130
|
+
**Build Verification**: Run `npm run build`
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### UI Components Phase
|
|
134
|
+
|
|
135
|
+
```markdown
|
|
136
|
+
### Phase 4: UI Components
|
|
137
|
+
|
|
138
|
+
**Scope**: Build the user interface components.
|
|
139
|
+
**Complexity**: 6/10
|
|
140
|
+
|
|
141
|
+
- [ ] Create logic hook (`useFeatureLogic.internal.ts`)
|
|
142
|
+
- [ ] Create view component (`index.tsx`)
|
|
143
|
+
- [ ] Follow view/logic separation pattern
|
|
144
|
+
|
|
145
|
+
**Build Verification**: Run `npm run build`
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Integration Phase
|
|
149
|
+
|
|
150
|
+
```markdown
|
|
151
|
+
### Phase 5: Integration
|
|
152
|
+
|
|
153
|
+
**Scope**: Connect all pieces and verify functionality.
|
|
154
|
+
**Complexity**: 4/10
|
|
155
|
+
|
|
156
|
+
- [ ] Integrate components with pages
|
|
157
|
+
- [ ] Connect to API endpoints
|
|
158
|
+
- [ ] Verify end-to-end flow
|
|
159
|
+
|
|
160
|
+
**Build Verification**: Run `npm run build`
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Tests Phase (Always Last)
|
|
164
|
+
|
|
165
|
+
```markdown
|
|
166
|
+
### Phase N: Tests (Final)
|
|
167
|
+
|
|
168
|
+
**Scope**: Write comprehensive tests for all new code.
|
|
169
|
+
**Complexity**: 5/10
|
|
170
|
+
|
|
171
|
+
- [ ] Unit tests for logic hooks (`*.client.test.ts`)
|
|
172
|
+
- [ ] Unit tests for utility functions
|
|
173
|
+
- [ ] Integration tests for API routes
|
|
174
|
+
- [ ] Command tests
|
|
175
|
+
|
|
176
|
+
**Build Verification**: Run `npm run build && npm run test`
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Key Changes Examples
|
|
182
|
+
|
|
183
|
+
```markdown
|
|
184
|
+
## Key Changes
|
|
185
|
+
|
|
186
|
+
1. **New API Endpoint**: Added `/api/v1/agents/workflow` for workflow management
|
|
187
|
+
2. **Database Schema**: New `workflow_steps` table with foreign key to `agents`
|
|
188
|
+
3. **Store Update**: Extended `agentStore` with workflow state management
|
|
189
|
+
4. **Component Addition**: New `WorkflowEditor` component in `/src/components/agent/`
|
|
190
|
+
5. **Type Definitions**: Added `WorkflowStep` and `WorkflowConfig` types in `/src/types/workflow.ts`
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Complexity Summary Example
|
|
196
|
+
|
|
197
|
+
```markdown
|
|
198
|
+
## Complexity Summary
|
|
199
|
+
|
|
200
|
+
| Phase | Complexity | Description |
|
|
201
|
+
| ----- | ---------- | ------------------------- |
|
|
202
|
+
| 1 | 3/10 | Types and schemas |
|
|
203
|
+
| 2 | 7/10 | Backend implementation |
|
|
204
|
+
| 3 | 5/10 | Store and state |
|
|
205
|
+
| 4 | 6/10 | UI components |
|
|
206
|
+
| 5 | 4/10 | Integration |
|
|
207
|
+
| 6 | 5/10 | Tests |
|
|
208
|
+
|
|
209
|
+
**Total Phases**: 6
|
|
210
|
+
**Average Complexity**: 5/10
|
|
211
|
+
**Highest Complexity**: Phase 2 at 7/10
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Execution Strategy Example
|
|
217
|
+
|
|
218
|
+
```markdown
|
|
219
|
+
## Execution Strategy
|
|
220
|
+
|
|
221
|
+
Based on complexity scores (per `.claude/rules/core/complexity-scoring.md`):
|
|
222
|
+
|
|
223
|
+
- **Phases 1-2**: Execute separately (Phase 2 is 7/10)
|
|
224
|
+
- **Phases 3-4**: Can be aggregated (combined complexity: 11, but cautious)
|
|
225
|
+
- **Phase 5**: Can aggregate with Phase 6 if simple
|
|
226
|
+
- **Phase 6**: Tests - always execute separately
|
|
227
|
+
```
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
|
|
2
|
+
## File Naming Conventions
|
|
3
|
+
|
|
4
|
+
| Pattern | Purpose |
|
|
5
|
+
| ------- | ------- |
|
|
6
|
+
| `test_*.py` | Test modules (preferred) |
|
|
7
|
+
| `*_test.py` | Alternative test module naming |
|
|
8
|
+
| `conftest.py` | Shared fixtures for a directory |
|
|
9
|
+
|
|
10
|
+
**Directory Structure**:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
src/
|
|
14
|
+
├── mymodule/
|
|
15
|
+
│ ├── __init__.py
|
|
16
|
+
│ ├── service.py
|
|
17
|
+
│ └── utils.py
|
|
18
|
+
tests/
|
|
19
|
+
├── conftest.py # Root fixtures
|
|
20
|
+
├── test_service.py # Tests for service.py
|
|
21
|
+
├── test_utils.py # Tests for utils.py
|
|
22
|
+
└── integration/
|
|
23
|
+
├── conftest.py # Integration-specific fixtures
|
|
24
|
+
└── test_api.py
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Test Structure
|
|
30
|
+
|
|
31
|
+
Use descriptive function names and organize with classes when grouping:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import pytest
|
|
35
|
+
from mymodule.service import UserService
|
|
36
|
+
from mymodule.errors import NotFoundError
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TestUserService:
|
|
40
|
+
"""Tests for UserService class."""
|
|
41
|
+
|
|
42
|
+
def test_get_user_returns_user_when_exists(self, mock_db):
|
|
43
|
+
"""Should return user data when user exists in database."""
|
|
44
|
+
# Arrange
|
|
45
|
+
mock_db.find_user.return_value = {"id": "user-1", "name": "Test User"}
|
|
46
|
+
service = UserService(mock_db)
|
|
47
|
+
|
|
48
|
+
# Act
|
|
49
|
+
result = service.get_user("user-1")
|
|
50
|
+
|
|
51
|
+
# Assert
|
|
52
|
+
assert result["id"] == "user-1"
|
|
53
|
+
assert result["name"] == "Test User"
|
|
54
|
+
mock_db.find_user.assert_called_once_with("user-1")
|
|
55
|
+
|
|
56
|
+
def test_get_user_raises_not_found_when_missing(self, mock_db):
|
|
57
|
+
"""Should raise NotFoundError when user doesn't exist."""
|
|
58
|
+
mock_db.find_user.return_value = None
|
|
59
|
+
service = UserService(mock_db)
|
|
60
|
+
|
|
61
|
+
with pytest.raises(NotFoundError) as exc_info:
|
|
62
|
+
service.get_user("nonexistent")
|
|
63
|
+
|
|
64
|
+
assert "user-1" in str(exc_info.value)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Fixtures
|
|
70
|
+
|
|
71
|
+
Use fixtures for reusable test setup:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
# conftest.py - Shared fixtures
|
|
75
|
+
import pytest
|
|
76
|
+
from unittest.mock import Mock, MagicMock, AsyncMock
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@pytest.fixture
|
|
80
|
+
def mock_db():
|
|
81
|
+
"""Provides a mocked database connection."""
|
|
82
|
+
db = Mock()
|
|
83
|
+
db.find_user = Mock()
|
|
84
|
+
db.create_user = Mock()
|
|
85
|
+
db.update_user = Mock()
|
|
86
|
+
return db
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@pytest.fixture
|
|
90
|
+
def mock_user():
|
|
91
|
+
"""Provides a mock user object."""
|
|
92
|
+
return {
|
|
93
|
+
"id": "user-1",
|
|
94
|
+
"email": "test@example.com",
|
|
95
|
+
"name": "Test User",
|
|
96
|
+
"created_at": "2024-01-01T00:00:00Z",
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@pytest.fixture
|
|
101
|
+
def mock_chat(mock_user):
|
|
102
|
+
"""Provides a mock chat with users."""
|
|
103
|
+
return {
|
|
104
|
+
"id": "chat-1",
|
|
105
|
+
"name": "Test Chat",
|
|
106
|
+
"tenant_id": "tenant-1",
|
|
107
|
+
"users": [mock_user],
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@pytest.fixture
|
|
112
|
+
async def mock_async_client():
|
|
113
|
+
"""Provides an async HTTP client mock."""
|
|
114
|
+
client = AsyncMock()
|
|
115
|
+
client.get = AsyncMock()
|
|
116
|
+
client.post = AsyncMock()
|
|
117
|
+
return client
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Factory Fixtures
|
|
123
|
+
|
|
124
|
+
Create parameterized factory fixtures for flexibility:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
@pytest.fixture
|
|
128
|
+
def create_mock_user():
|
|
129
|
+
"""Factory fixture for creating mock users with custom attributes."""
|
|
130
|
+
def _create_user(**overrides):
|
|
131
|
+
defaults = {
|
|
132
|
+
"id": "user-1",
|
|
133
|
+
"email": "test@example.com",
|
|
134
|
+
"name": "Test User",
|
|
135
|
+
"active": True,
|
|
136
|
+
"created_at": "2024-01-01T00:00:00Z",
|
|
137
|
+
}
|
|
138
|
+
return {**defaults, **overrides}
|
|
139
|
+
return _create_user
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@pytest.fixture
|
|
143
|
+
def create_mock_message(create_mock_user):
|
|
144
|
+
"""Factory fixture for creating mock messages."""
|
|
145
|
+
def _create_message(**overrides):
|
|
146
|
+
defaults = {
|
|
147
|
+
"id": "msg-1",
|
|
148
|
+
"chat_id": "chat-1",
|
|
149
|
+
"user": create_mock_user(),
|
|
150
|
+
"content": "Test message",
|
|
151
|
+
"role": "user",
|
|
152
|
+
}
|
|
153
|
+
return {**defaults, **overrides}
|
|
154
|
+
return _create_message
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# Usage in tests
|
|
158
|
+
def test_message_creation(create_mock_message):
|
|
159
|
+
message = create_mock_message(content="Custom content", role="assistant")
|
|
160
|
+
assert message["content"] == "Custom content"
|
|
161
|
+
assert message["role"] == "assistant"
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Mocking
|
|
167
|
+
|
|
168
|
+
### Basic Mocking with unittest.mock
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from unittest.mock import Mock, MagicMock, patch, AsyncMock
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class TestStreamService:
|
|
175
|
+
def test_stream_response_success(self):
|
|
176
|
+
"""Should stream response from engine."""
|
|
177
|
+
mock_engine = Mock()
|
|
178
|
+
mock_engine.stream_response.return_value = iter(["chunk1", "chunk2"])
|
|
179
|
+
|
|
180
|
+
service = StreamService(mock_engine)
|
|
181
|
+
result = list(service.stream("Hello"))
|
|
182
|
+
|
|
183
|
+
assert result == ["chunk1", "chunk2"]
|
|
184
|
+
mock_engine.stream_response.assert_called_once_with("Hello")
|
|
185
|
+
|
|
186
|
+
@patch("mymodule.service.JupiterEngineAPI")
|
|
187
|
+
def test_with_patched_dependency(self, MockEngineAPI):
|
|
188
|
+
"""Should use patched engine API."""
|
|
189
|
+
mock_instance = MockEngineAPI.return_value
|
|
190
|
+
mock_instance.get_agent.return_value = {"id": "agent-1"}
|
|
191
|
+
|
|
192
|
+
service = AgentService()
|
|
193
|
+
result = service.get_agent("agent-1")
|
|
194
|
+
|
|
195
|
+
assert result["id"] == "agent-1"
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Async Mocking
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
import pytest
|
|
202
|
+
from unittest.mock import AsyncMock, patch
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class TestAsyncOperations:
|
|
206
|
+
@pytest.mark.asyncio
|
|
207
|
+
async def test_async_fetch_data(self):
|
|
208
|
+
"""Should fetch data asynchronously."""
|
|
209
|
+
mock_client = AsyncMock()
|
|
210
|
+
mock_client.get.return_value = {"data": "test"}
|
|
211
|
+
|
|
212
|
+
service = AsyncService(mock_client)
|
|
213
|
+
result = await service.fetch_data("endpoint")
|
|
214
|
+
|
|
215
|
+
assert result["data"] == "test"
|
|
216
|
+
mock_client.get.assert_awaited_once_with("endpoint")
|
|
217
|
+
|
|
218
|
+
@pytest.mark.asyncio
|
|
219
|
+
@patch("mymodule.service.aiohttp.ClientSession")
|
|
220
|
+
async def test_with_patched_async_client(self, MockSession):
|
|
221
|
+
"""Should work with patched async client."""
|
|
222
|
+
mock_session = AsyncMock()
|
|
223
|
+
mock_response = AsyncMock()
|
|
224
|
+
mock_response.json.return_value = {"status": "ok"}
|
|
225
|
+
mock_session.get.return_value.__aenter__.return_value = mock_response
|
|
226
|
+
MockSession.return_value.__aenter__.return_value = mock_session
|
|
227
|
+
|
|
228
|
+
result = await fetch_external_data()
|
|
229
|
+
assert result["status"] == "ok"
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Parametrized Tests
|
|
235
|
+
|
|
236
|
+
Use `@pytest.mark.parametrize` for testing multiple cases:
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
import pytest
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class TestValidator:
|
|
243
|
+
@pytest.mark.parametrize("email,expected", [
|
|
244
|
+
("test@example.com", True),
|
|
245
|
+
("user@domain.org", True),
|
|
246
|
+
("invalid-email", False),
|
|
247
|
+
("@no-local-part.com", False),
|
|
248
|
+
("no-domain@", False),
|
|
249
|
+
("", False),
|
|
250
|
+
])
|
|
251
|
+
def test_email_validation(self, email, expected):
|
|
252
|
+
"""Should correctly validate email addresses."""
|
|
253
|
+
result = validate_email(email)
|
|
254
|
+
assert result == expected
|
|
255
|
+
|
|
256
|
+
@pytest.mark.parametrize("input_data,error_type", [
|
|
257
|
+
({"chat_id": None}, ValueError),
|
|
258
|
+
({"messages": []}, EmptyMessagesError),
|
|
259
|
+
({"tenant_id": ""}, InvalidTenantError),
|
|
260
|
+
])
|
|
261
|
+
def test_input_validation_errors(self, input_data, error_type):
|
|
262
|
+
"""Should raise appropriate errors for invalid inputs."""
|
|
263
|
+
with pytest.raises(error_type):
|
|
264
|
+
validate_input(input_data)
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Testing Exceptions
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
import pytest
|
|
273
|
+
from mymodule.errors import NotFoundError, ForbiddenError
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class TestErrorHandling:
|
|
277
|
+
def test_raises_not_found_error(self, mock_db):
|
|
278
|
+
"""Should raise NotFoundError when resource doesn't exist."""
|
|
279
|
+
mock_db.find.return_value = None
|
|
280
|
+
service = MyService(mock_db)
|
|
281
|
+
|
|
282
|
+
with pytest.raises(NotFoundError) as exc_info:
|
|
283
|
+
service.get_item("nonexistent")
|
|
284
|
+
|
|
285
|
+
assert exc_info.value.resource_id == "nonexistent"
|
|
286
|
+
assert "not found" in str(exc_info.value).lower()
|
|
287
|
+
|
|
288
|
+
def test_raises_forbidden_with_details(self, mock_db):
|
|
289
|
+
"""Should raise ForbiddenError with proper details."""
|
|
290
|
+
mock_db.check_permission.return_value = False
|
|
291
|
+
service = MyService(mock_db)
|
|
292
|
+
|
|
293
|
+
with pytest.raises(ForbiddenError) as exc_info:
|
|
294
|
+
service.access_resource("resource-1", user_id="user-1")
|
|
295
|
+
|
|
296
|
+
error = exc_info.value
|
|
297
|
+
assert error.reason == "INSUFFICIENT_PERMISSIONS"
|
|
298
|
+
assert error.user_id == "user-1"
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## Testing Context Managers
|
|
304
|
+
|
|
305
|
+
```python
|
|
306
|
+
from contextlib import contextmanager
|
|
307
|
+
from unittest.mock import Mock
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class TestContextManager:
|
|
311
|
+
def test_database_transaction(self, mock_db):
|
|
312
|
+
"""Should properly manage database transaction."""
|
|
313
|
+
mock_db.begin_transaction = Mock()
|
|
314
|
+
mock_db.commit = Mock()
|
|
315
|
+
mock_db.rollback = Mock()
|
|
316
|
+
|
|
317
|
+
with database_transaction(mock_db) as tx:
|
|
318
|
+
tx.execute("INSERT INTO users ...")
|
|
319
|
+
|
|
320
|
+
mock_db.begin_transaction.assert_called_once()
|
|
321
|
+
mock_db.commit.assert_called_once()
|
|
322
|
+
mock_db.rollback.assert_not_called()
|
|
323
|
+
|
|
324
|
+
def test_transaction_rollback_on_error(self, mock_db):
|
|
325
|
+
"""Should rollback transaction on error."""
|
|
326
|
+
mock_db.begin_transaction = Mock()
|
|
327
|
+
mock_db.commit = Mock()
|
|
328
|
+
mock_db.rollback = Mock()
|
|
329
|
+
|
|
330
|
+
with pytest.raises(ValueError):
|
|
331
|
+
with database_transaction(mock_db) as tx:
|
|
332
|
+
raise ValueError("Simulated error")
|
|
333
|
+
|
|
334
|
+
mock_db.rollback.assert_called_once()
|
|
335
|
+
mock_db.commit.assert_not_called()
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## Async Testing
|
|
341
|
+
|
|
342
|
+
```python
|
|
343
|
+
import pytest
|
|
344
|
+
import asyncio
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class TestAsyncService:
|
|
348
|
+
@pytest.mark.asyncio
|
|
349
|
+
async def test_async_operation_success(self, mock_async_client):
|
|
350
|
+
"""Should complete async operation successfully."""
|
|
351
|
+
mock_async_client.post.return_value = {"status": "success"}
|
|
352
|
+
|
|
353
|
+
service = AsyncService(mock_async_client)
|
|
354
|
+
result = await service.create_resource({"name": "test"})
|
|
355
|
+
|
|
356
|
+
assert result["status"] == "success"
|
|
357
|
+
|
|
358
|
+
@pytest.mark.asyncio
|
|
359
|
+
async def test_concurrent_operations(self):
|
|
360
|
+
"""Should handle concurrent operations correctly."""
|
|
361
|
+
async def mock_operation(delay, value):
|
|
362
|
+
await asyncio.sleep(delay)
|
|
363
|
+
return value
|
|
364
|
+
|
|
365
|
+
results = await asyncio.gather(
|
|
366
|
+
mock_operation(0.1, "first"),
|
|
367
|
+
mock_operation(0.05, "second"),
|
|
368
|
+
mock_operation(0.15, "third"),
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
assert results == ["first", "second", "third"]
|
|
372
|
+
|
|
373
|
+
@pytest.mark.asyncio
|
|
374
|
+
async def test_async_timeout(self, mock_async_client):
|
|
375
|
+
"""Should handle timeout correctly."""
|
|
376
|
+
mock_async_client.get.side_effect = asyncio.TimeoutError()
|
|
377
|
+
|
|
378
|
+
service = AsyncService(mock_async_client)
|
|
379
|
+
|
|
380
|
+
with pytest.raises(ServiceTimeoutError):
|
|
381
|
+
await service.fetch_with_timeout("endpoint", timeout=1.0)
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## Test Naming
|
|
387
|
+
|
|
388
|
+
- Use descriptive names that explain the expected behavior
|
|
389
|
+
- Describe the outcome in the function name
|
|
390
|
+
- Include context about the scenario
|
|
391
|
+
|
|
392
|
+
```python
|
|
393
|
+
# Good
|
|
394
|
+
def test_raises_not_found_error_when_chat_missing(self):
|
|
395
|
+
def test_extracts_text_from_multipart_message(self):
|
|
396
|
+
|
|
397
|
+
# Bad
|
|
398
|
+
def test_1(self):
|
|
399
|
+
def test_chat(self):
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
## Forbidden Patterns
|
|
405
|
+
|
|
406
|
+
### DON'T Share State Between Tests
|
|
407
|
+
|
|
408
|
+
```python
|
|
409
|
+
# BAD - Shared mutable state
|
|
410
|
+
class TestService:
|
|
411
|
+
shared_data = [] # Dangerous!
|
|
412
|
+
|
|
413
|
+
def test_one(self):
|
|
414
|
+
self.shared_data.append("item")
|
|
415
|
+
|
|
416
|
+
def test_two(self):
|
|
417
|
+
# Depends on test_one running first
|
|
418
|
+
assert len(self.shared_data) == 1
|
|
419
|
+
|
|
420
|
+
# GOOD - Fresh fixtures
|
|
421
|
+
@pytest.fixture
|
|
422
|
+
def data_list():
|
|
423
|
+
return []
|
|
424
|
+
|
|
425
|
+
def test_one(data_list):
|
|
426
|
+
data_list.append("item")
|
|
427
|
+
assert len(data_list) == 1
|
|
428
|
+
|
|
429
|
+
def test_two(data_list):
|
|
430
|
+
assert len(data_list) == 0 # Fresh list
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### DON'T Write Tests That Always Pass
|
|
434
|
+
|
|
435
|
+
```python
|
|
436
|
+
# BAD - No real assertion
|
|
437
|
+
def test_something():
|
|
438
|
+
some_function()
|
|
439
|
+
assert True
|
|
440
|
+
|
|
441
|
+
# GOOD - Meaningful assertion
|
|
442
|
+
def test_returns_expected_data():
|
|
443
|
+
result = some_function()
|
|
444
|
+
assert result.data == expected_data
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## Summary
|
|
450
|
+
|
|
451
|
+
Following these testing patterns ensures:
|
|
452
|
+
|
|
453
|
+
- **Consistency**: Same patterns across the codebase
|
|
454
|
+
- **Maintainability**: Tests are easy to understand and update
|
|
455
|
+
- **Reliability**: Tests are isolated and deterministic
|
|
456
|
+
- **Coverage**: Edge cases and errors are tested
|
|
457
|
+
- **Speed**: Tests run efficiently with proper mocking
|