@oleksandr.rudnychenko/sync_loop 0.2.5 → 0.3.2
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/README.md +25 -4
- package/bin/cli.js +3 -128
- package/bin/cli.ts +171 -0
- package/dist/bin/cli.d.ts +15 -0
- package/dist/bin/cli.js +137 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/src/init.d.ts +24 -0
- package/dist/src/init.js +410 -0
- package/dist/src/init.js.map +1 -0
- package/dist/src/server.d.ts +13 -0
- package/dist/src/server.js +265 -0
- package/dist/src/server.js.map +1 -0
- package/dist/src/template/.agent-loop/README.md +75 -0
- package/dist/src/template/.agent-loop/feedback.md +395 -0
- package/dist/src/template/.agent-loop/glossary.md +113 -0
- package/dist/src/template/.agent-loop/patterns/api-standards.md +132 -0
- package/dist/src/template/.agent-loop/patterns/code-patterns.md +300 -0
- package/dist/src/template/.agent-loop/patterns/refactoring-workflow.md +114 -0
- package/dist/src/template/.agent-loop/patterns/testing-guide.md +258 -0
- package/dist/src/template/.agent-loop/patterns.md +256 -0
- package/dist/src/template/.agent-loop/reasoning-kernel.md +521 -0
- package/dist/src/template/.agent-loop/validate-env.md +332 -0
- package/dist/src/template/.agent-loop/validate-n.md +321 -0
- package/dist/src/template/AGENTS.md +157 -0
- package/dist/src/template/README.md +144 -0
- package/dist/src/template/bootstrap-prompt.md +37 -0
- package/dist/src/template/protocol-summary.md +54 -0
- package/dist/src/template/wiring/agents-claude.md +203 -0
- package/dist/src/template/wiring/agents-github.md +211 -0
- package/dist/src/template/wiring/api-standards.md +15 -0
- package/dist/src/template/wiring/code-patterns.md +15 -0
- package/dist/src/template/wiring/feedback.md +18 -0
- package/dist/src/template/wiring/glossary.md +11 -0
- package/dist/src/template/wiring/patterns.md +18 -0
- package/dist/src/template/wiring/reasoning-kernel.md +18 -0
- package/dist/src/template/wiring/refactoring-workflow.md +15 -0
- package/dist/src/template/wiring/testing-guide.md +15 -0
- package/dist/src/template/wiring/validate-env.md +17 -0
- package/dist/src/template/wiring/validate-n.md +17 -0
- package/package.json +48 -34
- package/src/template/wiring/agents-claude.md +203 -0
- package/src/template/wiring/agents-github.md +211 -0
- package/src/template/wiring/api-standards.md +15 -0
- package/src/template/wiring/code-patterns.md +15 -0
- package/src/template/wiring/feedback.md +18 -0
- package/src/template/wiring/glossary.md +11 -0
- package/src/template/wiring/patterns.md +18 -0
- package/src/template/wiring/reasoning-kernel.md +18 -0
- package/src/template/wiring/refactoring-workflow.md +15 -0
- package/src/template/wiring/testing-guide.md +15 -0
- package/src/template/wiring/validate-env.md +17 -0
- package/src/template/wiring/validate-n.md +17 -0
- package/src/init.js +0 -569
- package/src/server.js +0 -292
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# Code Patterns (P1–P11)
|
|
2
|
+
|
|
3
|
+
Reusable implementation patterns for layered application code.
|
|
4
|
+
Referenced from [../patterns.md](../patterns.md).
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## P1 · Port/Adapter
|
|
9
|
+
|
|
10
|
+
Abstracts infrastructure behind protocol interfaces. Decouples domain logic from external systems.
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
# Port (interface/protocol)
|
|
14
|
+
class StoragePort(Protocol):
|
|
15
|
+
def search(
|
|
16
|
+
self,
|
|
17
|
+
collection: str,
|
|
18
|
+
query: str,
|
|
19
|
+
*,
|
|
20
|
+
filters: dict | None = None,
|
|
21
|
+
limit: int = 10,
|
|
22
|
+
) -> list[Record]: ...
|
|
23
|
+
|
|
24
|
+
# Adapter (concrete implementation)
|
|
25
|
+
class DatabaseAdapter:
|
|
26
|
+
def __init__(self, client: DBClient) -> None:
|
|
27
|
+
self._client = client
|
|
28
|
+
|
|
29
|
+
def search(
|
|
30
|
+
self,
|
|
31
|
+
collection: str,
|
|
32
|
+
query: str,
|
|
33
|
+
*,
|
|
34
|
+
filters: dict | None = None,
|
|
35
|
+
limit: int = 10,
|
|
36
|
+
) -> list[Record]:
|
|
37
|
+
# Implementation against real infrastructure
|
|
38
|
+
...
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Key rules:**
|
|
42
|
+
- Port lives in `libs/{component}/port.*`
|
|
43
|
+
- Adapter lives in `libs/{component}/{impl}.*`
|
|
44
|
+
- Services depend on port interfaces, not adapters directly
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## P2 · Domain Module
|
|
49
|
+
|
|
50
|
+
Each domain module follows a consistent multi-file layout:
|
|
51
|
+
|
|
52
|
+
| File | Purpose |
|
|
53
|
+
|------|---------|
|
|
54
|
+
| `models.*` | Domain entities and value objects |
|
|
55
|
+
| `services.*` | Business logic and orchestration |
|
|
56
|
+
| `routes.*` | Transport endpoints |
|
|
57
|
+
| `tasks.*` | Background tasks (if async processing needed) |
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
# services.py — business logic only, no transport concerns
|
|
61
|
+
class OrderService:
|
|
62
|
+
def __init__(self, repository: OrderRepository) -> None:
|
|
63
|
+
self._repository = repository
|
|
64
|
+
|
|
65
|
+
def process(self, *, order_id: str) -> ProcessResult:
|
|
66
|
+
order = self._repository.get(order_id)
|
|
67
|
+
# business logic here
|
|
68
|
+
return ProcessResult(order_id=order.id, status="completed")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## P3 · Background Task Boundary
|
|
74
|
+
|
|
75
|
+
Task handlers stay thin. Business logic always lives in services.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
# tasks.py — thin wrapper, delegates to service
|
|
79
|
+
def process_order_task(runtime: TaskRuntime, order_id: str):
|
|
80
|
+
"""Background task that delegates to service."""
|
|
81
|
+
service = runtime.order_service
|
|
82
|
+
service.update_status(order_id, status="processing")
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
service.process(order_id=order_id)
|
|
86
|
+
service.update_status(order_id, status="completed")
|
|
87
|
+
except Exception as exc:
|
|
88
|
+
service.update_status(order_id, status="failed", error=str(exc))
|
|
89
|
+
raise
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Key rules:**
|
|
93
|
+
- Tasks never contain business logic
|
|
94
|
+
- Dependencies injected via runtime, not imported directly
|
|
95
|
+
- Always update status on success and failure
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## P4 · App Context / Composition Root
|
|
100
|
+
|
|
101
|
+
Centralized dependency wiring, initialized once at startup:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
@dataclass
|
|
105
|
+
class AppContext:
|
|
106
|
+
config: Config
|
|
107
|
+
session_factory: SessionFactory
|
|
108
|
+
services: ServiceRegistry
|
|
109
|
+
logger: Logger
|
|
110
|
+
|
|
111
|
+
_context: AppContext | None = None
|
|
112
|
+
|
|
113
|
+
def init_app_context(config: Config) -> AppContext:
|
|
114
|
+
global _context
|
|
115
|
+
if _context:
|
|
116
|
+
return _context
|
|
117
|
+
_context = AppContext(config=config, ...)
|
|
118
|
+
return _context
|
|
119
|
+
|
|
120
|
+
def get_app_context() -> AppContext:
|
|
121
|
+
if _context is None:
|
|
122
|
+
return init_app_context(load_config())
|
|
123
|
+
return _context
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## P5 · Transport Route
|
|
129
|
+
|
|
130
|
+
Routes only handle transport concerns; all logic delegated to services:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
router = APIRouter()
|
|
134
|
+
|
|
135
|
+
def get_service() -> OrderService:
|
|
136
|
+
ctx = get_app_context()
|
|
137
|
+
return OrderService(ctx.repository)
|
|
138
|
+
|
|
139
|
+
@router.post("/orders")
|
|
140
|
+
def create_order(
|
|
141
|
+
data: CreateOrderRequest,
|
|
142
|
+
service: OrderService = Depends(get_service),
|
|
143
|
+
) -> OrderResponse:
|
|
144
|
+
result = service.create(data)
|
|
145
|
+
return OrderResponse(id=result.id, status=result.status)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## P6 · Typed Models
|
|
151
|
+
|
|
152
|
+
Domain entities with explicit types and serialization:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
@dataclass(slots=True)
|
|
156
|
+
class OrderItem:
|
|
157
|
+
product_id: str
|
|
158
|
+
quantity: int
|
|
159
|
+
unit_price: float
|
|
160
|
+
tags: list[str] = field(default_factory=list)
|
|
161
|
+
|
|
162
|
+
def to_dict(self) -> dict[str, Any]:
|
|
163
|
+
return {
|
|
164
|
+
"product_id": self.product_id,
|
|
165
|
+
"quantity": self.quantity,
|
|
166
|
+
"unit_price": self.unit_price,
|
|
167
|
+
"tags": self.tags,
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## P7 · Collection/Enum Safety
|
|
174
|
+
|
|
175
|
+
Replace magic strings with typed enums:
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
class Collection(str, Enum):
|
|
179
|
+
ORDERS = "orders"
|
|
180
|
+
PRODUCTS = "products"
|
|
181
|
+
USERS = "users"
|
|
182
|
+
|
|
183
|
+
# Usage: repository.query(Collection.ORDERS, ...)
|
|
184
|
+
# NOT: repository.query("orders", ...)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## P8 · Error Handling
|
|
190
|
+
|
|
191
|
+
Layered exception hierarchy with boundary translation:
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
# Domain exceptions
|
|
195
|
+
class DomainError(Exception):
|
|
196
|
+
"""Base error for domain."""
|
|
197
|
+
|
|
198
|
+
class NotFoundError(DomainError):
|
|
199
|
+
"""Resource not found."""
|
|
200
|
+
|
|
201
|
+
class ValidationError(DomainError):
|
|
202
|
+
"""Invalid input or state."""
|
|
203
|
+
|
|
204
|
+
# Route-level translation
|
|
205
|
+
@router.get("/orders/{order_id}")
|
|
206
|
+
def get_order(order_id: str, service = Depends(get_service)):
|
|
207
|
+
try:
|
|
208
|
+
return service.get(order_id)
|
|
209
|
+
except NotFoundError as exc:
|
|
210
|
+
raise HTTPException(status_code=404, detail=str(exc))
|
|
211
|
+
except ValidationError as exc:
|
|
212
|
+
raise HTTPException(status_code=400, detail=str(exc))
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## P9 · Type Hints Everywhere
|
|
218
|
+
|
|
219
|
+
All code must have complete type annotations:
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
# ✅ Good — fully typed
|
|
223
|
+
def process(
|
|
224
|
+
order_id: str,
|
|
225
|
+
*,
|
|
226
|
+
callback: Callable[..., Awaitable[Response]] | None = None,
|
|
227
|
+
) -> tuple[str, dict[str, Any]] | None:
|
|
228
|
+
...
|
|
229
|
+
|
|
230
|
+
# ❌ Bad — missing annotations
|
|
231
|
+
def process(order_id, callback=None):
|
|
232
|
+
...
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**Common type aliases:**
|
|
236
|
+
```python
|
|
237
|
+
SessionFactory = Callable[[], Session]
|
|
238
|
+
Filters = Mapping[str, Any]
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## P10 · Service Orchestration
|
|
244
|
+
|
|
245
|
+
Services accept all dependencies via constructor — no hidden state:
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
# Production code
|
|
249
|
+
class AnalysisService:
|
|
250
|
+
def __init__(
|
|
251
|
+
self,
|
|
252
|
+
repository: Repository,
|
|
253
|
+
evaluator: EvaluationService,
|
|
254
|
+
):
|
|
255
|
+
self._repository = repository
|
|
256
|
+
self._evaluator = evaluator
|
|
257
|
+
|
|
258
|
+
# Test code — inject mocks
|
|
259
|
+
service = AnalysisService(
|
|
260
|
+
repository=mock_repository,
|
|
261
|
+
evaluator=mock_evaluator,
|
|
262
|
+
)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## P11 · Config Isolation
|
|
268
|
+
|
|
269
|
+
Centralized, environment-based configuration with startup validation:
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
@dataclass
|
|
273
|
+
class Config:
|
|
274
|
+
database_url: str
|
|
275
|
+
debug: bool = False
|
|
276
|
+
max_workers: int = 4
|
|
277
|
+
|
|
278
|
+
@classmethod
|
|
279
|
+
def from_env(cls) -> "Config":
|
|
280
|
+
return cls(
|
|
281
|
+
database_url=os.environ["DATABASE_URL"],
|
|
282
|
+
debug=os.environ.get("DEBUG", "0") == "1",
|
|
283
|
+
max_workers=int(os.environ.get("MAX_WORKERS", "4")),
|
|
284
|
+
)
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Key rules:**
|
|
288
|
+
- All config read from environment at startup
|
|
289
|
+
- No scattered `os.environ` calls inside business logic
|
|
290
|
+
- Test config overrides controlled via fixtures
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Related Documents
|
|
295
|
+
|
|
296
|
+
| Document | Purpose |
|
|
297
|
+
|----------|---------|
|
|
298
|
+
| [../patterns.md](../patterns.md) | Pattern routing index |
|
|
299
|
+
| [refactoring-workflow.md](refactoring-workflow.md) | Safe structural changes |
|
|
300
|
+
| [testing-guide.md](testing-guide.md) | Verification strategy |
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Refactoring Workflow (R1)
|
|
2
|
+
|
|
3
|
+
Checklist and approach for moving files, changing imports, or restructuring modules.
|
|
4
|
+
Referenced from [../patterns.md](../patterns.md).
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Refactoring Checklist
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
Phase 1: PLAN
|
|
12
|
+
☐ Identify all files to move/rename/extract
|
|
13
|
+
☐ Map old imports → new imports
|
|
14
|
+
☐ Check for documentation references (README, docstrings, agent-loop specs)
|
|
15
|
+
☐ Identify public interfaces that must remain stable
|
|
16
|
+
|
|
17
|
+
Phase 2: EXECUTE
|
|
18
|
+
☐ Move files
|
|
19
|
+
☐ Update imports in moved files (internal references)
|
|
20
|
+
☐ Update all caller imports (use grep to find every reference)
|
|
21
|
+
☐ Update documentation examples if they contain import paths
|
|
22
|
+
☐ Update .agent-loop/patterns.md structure section if layout changed
|
|
23
|
+
|
|
24
|
+
Phase 3: VALIDATE (NON-NEGOTIABLE)
|
|
25
|
+
☐ 1. Type check: run project type checker
|
|
26
|
+
☐ 2. Run tests: execute test suite with fail-fast
|
|
27
|
+
☐ 3. Check docs: grep for old import paths across all files
|
|
28
|
+
☐ 4. Verify no orphaned imports (unused or broken)
|
|
29
|
+
|
|
30
|
+
Phase 4: DOCUMENT
|
|
31
|
+
☐ Update README structure section
|
|
32
|
+
☐ Update architecture docs if needed
|
|
33
|
+
☐ Create report in docs/reports/ if the refactor is major
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Example: Moving a Module File
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# 1. Move file
|
|
42
|
+
mv src/modules/old_location/processor.py src/modules/new_location/processor.py
|
|
43
|
+
|
|
44
|
+
# 2. Update imports inside the moved file (internal references)
|
|
45
|
+
# e.g., relative imports that changed due to new directory depth
|
|
46
|
+
|
|
47
|
+
# 3. Find ALL references to the old path
|
|
48
|
+
grep -r "from src.modules.old_location.processor" .
|
|
49
|
+
grep -r "import old_location.processor" .
|
|
50
|
+
|
|
51
|
+
# 4. Update each reference found:
|
|
52
|
+
# - tests/unit/test_processor.py
|
|
53
|
+
# - src/modules/new_location/tasks.py
|
|
54
|
+
# - docs/architecture.md (if it mentions import paths)
|
|
55
|
+
|
|
56
|
+
# 5. MANDATORY: Run validation
|
|
57
|
+
# Type check → Tests → Grep for leftover old paths
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Why tests after docs?** Documentation often contains import examples. Tests verify imports actually work and catch typos in updated paths.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Example: Extracting a Function to a New Module
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# 1. Create the new module
|
|
68
|
+
touch src/libs/parsing/helpers.py
|
|
69
|
+
|
|
70
|
+
# 2. Move the function definition to the new file
|
|
71
|
+
# Update its internal imports
|
|
72
|
+
|
|
73
|
+
# 3. In the original file, replace the function body with an import:
|
|
74
|
+
# from src.libs.parsing.helpers import parse_response
|
|
75
|
+
|
|
76
|
+
# 4. Find all OTHER callers of the original location
|
|
77
|
+
grep -r "from src.modules.analysis.utils import parse_response" .
|
|
78
|
+
|
|
79
|
+
# 5. Update all callers to use the new import path
|
|
80
|
+
|
|
81
|
+
# 6. VALIDATE: type check + tests + grep for old path
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Guardrails
|
|
87
|
+
|
|
88
|
+
- **Prefer reversible steps** — move one file at a time, validate, then move next
|
|
89
|
+
- **Never combine unrelated refactors** — one logical change per commit
|
|
90
|
+
- **Do not hide breaking API changes** — if a public interface moved, update all consumers
|
|
91
|
+
- **Never skip Phase 3** — validation is non-negotiable, even for "simple" moves
|
|
92
|
+
- **Grep is your friend** — always search for the old path after moving; IDEs miss things
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Common Pitfalls
|
|
97
|
+
|
|
98
|
+
| Pitfall | Prevention |
|
|
99
|
+
|---------|------------|
|
|
100
|
+
| Circular imports after move | Map dependency graph before moving |
|
|
101
|
+
| Tests pass but type checker fails | Always run type checker first |
|
|
102
|
+
| Docs reference old paths | Grep docs/ and *.md files explicitly |
|
|
103
|
+
| Forgot to update __init__.py re-exports | Check all `__init__.py` files in affected packages |
|
|
104
|
+
| Moved too many files at once | Move one file → validate → repeat |
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Related Documents
|
|
109
|
+
|
|
110
|
+
| Document | Purpose |
|
|
111
|
+
|----------|---------|
|
|
112
|
+
| [../validate-env.md](../validate-env.md) | Stage 1 gates (type check, tests) |
|
|
113
|
+
| [../validate-n.md](../validate-n.md) | Stage 2 neighbor checks (boundary impact) |
|
|
114
|
+
| [testing-guide.md](testing-guide.md) | Regression test strategy |
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# Testing Guide (R2)
|
|
2
|
+
|
|
3
|
+
Comprehensive testing patterns for safe iteration and regression prevention.
|
|
4
|
+
Referenced from [../patterns.md](../patterns.md).
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Test Organization
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
tests/
|
|
12
|
+
├── conftest.py # Shared fixtures, global setup
|
|
13
|
+
├── factories.py # Test data builders
|
|
14
|
+
├── mocks.py # Mock implementations for external deps
|
|
15
|
+
├── api/ # API/endpoint tests (HTTP client)
|
|
16
|
+
├── integration/ # Multi-component workflow tests
|
|
17
|
+
└── unit/ # Pure logic tests (no I/O)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Pattern 1: Fixture-Based Setup
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
# Environment isolation — reset global state each test
|
|
26
|
+
@pytest.fixture(autouse=True)
|
|
27
|
+
def reset_context():
|
|
28
|
+
"""Reset global state before each test."""
|
|
29
|
+
import app.context
|
|
30
|
+
app.context._context = None
|
|
31
|
+
yield
|
|
32
|
+
app.context._context = None
|
|
33
|
+
|
|
34
|
+
# Test app configuration
|
|
35
|
+
@pytest.fixture
|
|
36
|
+
def test_app(monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
|
|
37
|
+
"""Configure app with test-safe environment."""
|
|
38
|
+
monkeypatch.setenv("APP_ENV", "testing")
|
|
39
|
+
monkeypatch.setenv("ENABLE_BACKGROUND_TASKS", "0")
|
|
40
|
+
yield create_app()
|
|
41
|
+
|
|
42
|
+
# API client for endpoint tests
|
|
43
|
+
@pytest.fixture
|
|
44
|
+
def client(test_app):
|
|
45
|
+
return TestClient(test_app)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Pattern 2: Factory Pattern for Test Data
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
class EntityFactory:
|
|
54
|
+
@staticmethod
|
|
55
|
+
def create(
|
|
56
|
+
id: str | None = None,
|
|
57
|
+
name: str = "Test Entity",
|
|
58
|
+
entity_type: str = "default",
|
|
59
|
+
**extras: Any,
|
|
60
|
+
) -> dict[str, Any]:
|
|
61
|
+
return {
|
|
62
|
+
"id": id or str(uuid4()),
|
|
63
|
+
"name": name,
|
|
64
|
+
"metadata": {"type": entity_type, **extras},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def with_full_analysis() -> dict[str, Any]:
|
|
69
|
+
"""Semantic constructor for entity with all analysis fields populated."""
|
|
70
|
+
return EntityFactory.create(
|
|
71
|
+
name="Fully Analyzed Entity",
|
|
72
|
+
entity_type="analyzed",
|
|
73
|
+
score=85.0,
|
|
74
|
+
grade="B",
|
|
75
|
+
)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Pattern 3: Mock External Dependencies
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
class MockExternalService:
|
|
84
|
+
"""Mock for any external service boundary (LLM, API, storage)."""
|
|
85
|
+
def __init__(self, response: dict | str | None = None):
|
|
86
|
+
self.response = response or self._default_response()
|
|
87
|
+
self.calls: list[dict] = []
|
|
88
|
+
|
|
89
|
+
async def __call__(self, prompt: str, context: str, **kwargs):
|
|
90
|
+
self.calls.append({"prompt": prompt, "context": context})
|
|
91
|
+
return json.dumps(self.response), {}
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def call_count(self) -> int:
|
|
95
|
+
return len(self.calls)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Usage:
|
|
99
|
+
```python
|
|
100
|
+
@pytest.fixture
|
|
101
|
+
def mock_service() -> MockExternalService:
|
|
102
|
+
return MockExternalService(response={"score": 85})
|
|
103
|
+
|
|
104
|
+
def test_analysis_delegates_to_service(mock_service):
|
|
105
|
+
service = AnalysisService(external=mock_service)
|
|
106
|
+
result = service.analyze(...)
|
|
107
|
+
assert mock_service.call_count == 1
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Pattern 4: Class-Based Test Organization
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
class TestParseResponse:
|
|
116
|
+
def test_valid_json_response(self):
|
|
117
|
+
response = '{"entity_type": "analyzed", "score": 85}'
|
|
118
|
+
result = parse_response(response)
|
|
119
|
+
assert result.entity_type == EntityType.ANALYZED
|
|
120
|
+
|
|
121
|
+
def test_invalid_json_fallback(self):
|
|
122
|
+
result = parse_response("Not JSON")
|
|
123
|
+
assert result.entity_type == EntityType.UNKNOWN
|
|
124
|
+
|
|
125
|
+
def test_missing_fields_use_defaults(self):
|
|
126
|
+
result = parse_response('{"entity_type": "analyzed"}')
|
|
127
|
+
assert result.score == 0.0
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Pattern 5: Parametrized Tests
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
@pytest.mark.parametrize("score,expected_grade", [
|
|
136
|
+
(95, "A"), (90, "A"),
|
|
137
|
+
(89.9, "B"), (85, "B"), (80, "B"),
|
|
138
|
+
(79, "C"), (70, "C"),
|
|
139
|
+
(69, "D"), (60, "D"),
|
|
140
|
+
(59, "F"), (0, "F"),
|
|
141
|
+
])
|
|
142
|
+
def test_grade_thresholds(score: float, expected_grade: str):
|
|
143
|
+
assert compute_grade(score) == expected_grade
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Pattern 6: Three-Layer Test Strategy
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
# UNIT: Pure logic, no I/O
|
|
152
|
+
def test_parse_response():
|
|
153
|
+
result = parse_response('{"entity_type": "analyzed"}')
|
|
154
|
+
assert result.entity_type == EntityType.ANALYZED
|
|
155
|
+
|
|
156
|
+
# INTEGRATION: Multiple components, mocked external I/O
|
|
157
|
+
def test_analysis_pipeline(mock_service, mock_repository):
|
|
158
|
+
service = AnalysisService(external=mock_service, repo=mock_repository)
|
|
159
|
+
report = service.analyze(entities=[...])
|
|
160
|
+
assert report.aggregate_score > 0
|
|
161
|
+
|
|
162
|
+
# API: HTTP endpoint, full stack with test client
|
|
163
|
+
def test_analyze_endpoint(client, mock_data):
|
|
164
|
+
response = client.post("/api/v1/analyze", json=mock_data)
|
|
165
|
+
assert response.status_code == 200
|
|
166
|
+
assert "score" in response.json()
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Pattern 7: Assertion Patterns
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
# ✅ Specific property checks
|
|
175
|
+
assert report.task_id == "task-1"
|
|
176
|
+
assert len(report.metrics) > 0
|
|
177
|
+
assert "total_items" in [m.name for m in report.metrics]
|
|
178
|
+
|
|
179
|
+
# ✅ Range/constraint checks
|
|
180
|
+
assert 0 <= result.score <= 100
|
|
181
|
+
assert result.entity_type in EntityType
|
|
182
|
+
|
|
183
|
+
# ✅ Collection checks
|
|
184
|
+
assert all(m.value >= 0 for m in report.metrics)
|
|
185
|
+
assert any(r.entity_type == "analyzed" for r in results)
|
|
186
|
+
|
|
187
|
+
# ❌ Vague — what is being verified?
|
|
188
|
+
assert report
|
|
189
|
+
assert result is not None
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Pattern 8: Naming Convention
|
|
195
|
+
|
|
196
|
+
Format: `test_<action>_<condition>_<outcome>`
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
test_returns_report # Basic happy path
|
|
200
|
+
test_metrics_computed_correctly # Specific behavior
|
|
201
|
+
test_unknown_type_falls_back_to_default # Edge case
|
|
202
|
+
test_invalid_json_fallback # Error handling
|
|
203
|
+
test_missing_fields_use_defaults # Default behavior
|
|
204
|
+
test_empty_input_returns_empty_report # Boundary condition
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Test Metrics (Target)
|
|
210
|
+
|
|
211
|
+
| Metric | Target |
|
|
212
|
+
|--------|--------|
|
|
213
|
+
| Pass Rate | 100% |
|
|
214
|
+
| Unit proportion | ≥ 70% |
|
|
215
|
+
| Integration proportion | ≤ 20% |
|
|
216
|
+
| API proportion | ≤ 10% |
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Test Heuristics
|
|
221
|
+
|
|
222
|
+
| Heuristic | Guideline |
|
|
223
|
+
|-----------|-----------|
|
|
224
|
+
| **Speed** | Unit < 10ms, Integration < 100ms, API < 1s |
|
|
225
|
+
| **Isolation** | Each test independent, no shared mutable state |
|
|
226
|
+
| **Coverage** | Critical paths and edge cases tested, not chasing 100% line coverage |
|
|
227
|
+
| **Clarity** | Test name explains what behavior is being verified |
|
|
228
|
+
| **Simplicity** | One logical assertion per test (multiple `assert` OK if testing one concept) |
|
|
229
|
+
| **Structure** | Arrange → Act → Assert (AAA pattern) |
|
|
230
|
+
| **Determinism** | No flaky tests — mock time, randomness, external systems |
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Validation Strategy
|
|
235
|
+
|
|
236
|
+
1. **Changed symbols first** — run tests for files you changed
|
|
237
|
+
2. **Adjacent modules second** — run tests for callers/consumers
|
|
238
|
+
3. **Full suite last** — once local confidence is high, run everything
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
# Targeted
|
|
242
|
+
pytest tests/unit/test_processor.py -x -v
|
|
243
|
+
|
|
244
|
+
# Adjacent
|
|
245
|
+
pytest tests/unit/ tests/integration/ -x -v -k "processor or analysis"
|
|
246
|
+
|
|
247
|
+
# Full suite
|
|
248
|
+
pytest tests/ -x -v
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Related Documents
|
|
254
|
+
|
|
255
|
+
| Document | Purpose |
|
|
256
|
+
|----------|---------|
|
|
257
|
+
| [../validate-env.md](../validate-env.md) | Gate orchestration (test gate details) |
|
|
258
|
+
| [refactoring-workflow.md](refactoring-workflow.md) | Safe code movement (tests after each move) |
|