File size: 27,406 Bytes
90c6b42 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 | # Code Quality Standards
**Version:** 1.0
**Last Updated:** 2026-02-16
**Applies to:** All Python code in the Atom backend
## Overview
This document defines the code quality standards for the Atom backend project. Following these standards ensures maintainable, reliable, and production-ready code.
## Type Hints
### Requirements
- **New code:** MUST have 100% type hint coverage on all function signatures
- **Existing code:** Incremental adoption - add type hints when modifying functions
- **Type checking:** Run MyPy locally before committing changes
### Type Hint Patterns
```python
# Function signatures MUST include parameter types and return types
def process_agent(agent_id: str, maturity_level: int) -> Dict[str, Any]:
"""Process an agent with given maturity level."""
pass
# Use Optional for nullable types
def get_agent(agent_id: str) -> Optional[AgentRegistry]:
"""Get agent by ID, returns None if not found."""
pass
# Use complex types for collections
def list_agents(category: Optional[str] = None) -> List[AgentRegistry]:
"""List agents by category."""
pass
# Async functions must specify return types
async def execute_workflow(workflow_id: str) -> WorkflowExecution:
"""Execute a workflow asynchronously."""
pass
```
### Import Required Types
```python
from typing import Any, Dict, List, Optional, Union, Callable, AsyncIterator
```
### MyPy Configuration
MyPy is configured in `backend/mypy.ini`:
- **Python version:** 3.11
- **Incremental adoption:** `disallow_untyped_defs = False`
- **Check untyped defs:** Enabled to gradually add type hints
- **Third-party libs:** Missing imports ignored
### Running MyPy
```bash
# Type check specific files
cd backend && mypy core/llm/byok_handler.py
# Type check entire core directory
cd backend && mypy core/ --config-file mypy.ini
# Type check with verbose output
cd backend && mypy core/ --show-error-codes --show-error-context
```
## Error Handling
### Principles
1. **Use specific exception types** - Never catch bare `Exception:` if you can be more specific
2. **Log with context** - Always include relevant context in error messages
3. **Preserve stack traces** - Use `raise ... from e` for exception chaining
4. **Never swallow exceptions silently** - Always log or handle appropriately
### Error Handling Patterns
```python
# Standardized error handling with context
try:
agent = db.query(AgentRegistry).filter(AgentRegistry.id == agent_id).first()
if not agent:
raise ValueError(f"Agent not found: {agent_id}")
except SQLAlchemyError as e:
logger.error(f"Database error while fetching agent {agent_id}: {e}")
raise
# Validation errors with descriptive messages
def validate_agent_maturity(agent: AgentRegistry, action: str) -> None:
"""Validate agent maturity for performing an action."""
required_level = ACTION_COMPLEXITY.get(action, 2)
if agent.confidence_score < required_level:
raise ValueError(
f"Agent {agent.name} (score: {agent.confidence_score}) "
f"insufficient for {action} (required: {required_level})"
)
# Exception chaining to preserve stack traces
try:
result = await external_service_call()
except httpx.HTTPError as e:
logger.error(f"External service error: {e}")
raise ExternalServiceError(f"Failed to call external service: {e}") from e
```
### Exception Categories
- **Database errors:** `SQLAlchemyError`, `IntegrityError`
- **Validation errors:** `ValueError`, `ValidationError`
- **Not found errors:** Return `None` or raise specific exception
- **Permission errors:** Custom `PermissionDeniedException`
- **External service errors:** `ExternalServiceError`, `HTTPError`
## Logging
### Standards
- **Use structlog** for structured logging with context
- **Include relevant context** in all log messages
- **Use appropriate log levels:** DEBUG, INFO, WARNING, ERROR, CRITICAL
### Logging Patterns
```python
import structlog
logger = structlog.get_logger(__name__)
# Debug level for detailed troubleshooting
logger.debug(f"Cache HIT for governance check: {agent_id}:{action_type}")
# Info level for normal operations
logger.info(f"Registered new agent: {name}", agent_id=agent.id)
# Warning level for unexpected but recoverable issues
logger.warning(f"Failed to initialize {provider_id} client: {e}")
# Error level for failures
logger.error(f"LLM Generation failed: {e}", agent_id=agent_id)
# Critical level for system-wide failures
logger.critical(f"Database connection lost: {e}")
```
### Context Enrichment
```python
# Include relevant context in log messages
logger.info(
"Agent maturity transition",
agent_id=agent.id,
agent_name=agent.name,
previous_status=previous_status,
new_status=agent.status,
confidence_score=new_score
)
```
## Documentation
### Docstring Standards
All functions MUST have Google-style docstrings with Args and Returns sections:
```python
def process_workflow(
workflow_id: str,
inputs: Dict[str, Any],
timeout: int = 30
) -> Optional[WorkflowExecution]:
"""
Process a workflow with given inputs.
Args:
workflow_id: Unique identifier for the workflow
inputs: Dictionary of input parameters for the workflow
timeout: Maximum execution time in seconds (default: 30)
Returns:
WorkflowExecution object if successful, None if failed
Raises:
ValueError: If workflow_id is invalid
TimeoutError: If workflow execution exceeds timeout
"""
pass
```
### Comment Standards
- **Comment WHY, not WHAT** - Code should be self-explanatory about what it does
- **Keep comments up to date** - Outdated comments are worse than no comments
- **Use docstrings for function/module documentation** - Use inline comments for complex logic
```python
# GOOD: Explains why we're doing this
# Use 0.5 only if confidence_score is None, not if it's 0.0
current = agent.confidence_score if agent.confidence_score is not None else 0.5
# BAD: Just repeats what the code says
# Set current to confidence_score
current = agent.confidence_score if agent.confidence_score is not None else 0.5
```
## Code Formatting
### Standards
- **Use Black** for code formatting (88 character line length)
- **Use Ruff** for fast linting
- **Format on save** - Configure your IDE to format code automatically
### Configuration
Black and Ruff configuration in `backend/pyproject.toml`:
```toml
[tool.black]
line-length = 88
target-version = ['py311']
[tool.ruff]
line-length = 88
select = ["E", "F", "I", "N", "W"]
```
### Running Formatters
```bash
# Format code with Black
cd backend && black .
# Check formatting without modifying
cd backend && black --check .
# Lint with Ruff
cd backend && ruff check .
# Auto-fix linting issues
cd backend && ruff check --fix .
```
## Testing Standards
### Principles
- **Use pytest** for all testing
- **Type-safe fixtures** - Add type hints to test fixtures
- **Arrange-Act-Assert** pattern for clear test structure
- **Test names should describe behavior** - `test_agent_promotion_when_confidence_exceeds_threshold`
### Test Patterns
```python
import pytest
from core.models import AgentRegistry
from core.agent_governance_service import AgentGovernanceService
def test_agent_promotion_when_confidence_exceeds_threshold(db_session: Session):
"""
Test that agent is promoted to AUTONOMOUS when confidence exceeds 0.9.
"""
# Arrange
governance = AgentGovernanceService(db_session)
agent = AgentRegistry(
name="Test Agent",
category="testing",
confidence_score=0.95
)
db_session.add(agent)
db_session.commit()
# Act
result = governance.enforce_action(agent.id, "delete")
# Assert
assert result["proceed"] is True
assert result["status"] == "APPROVED"
```
### Test Coverage Goals
- **Critical paths:** 90%+ coverage (governance, LLM routing, workflows)
- **Business logic:** 80%+ coverage
- **Overall:** 70%+ coverage
### Test File Naming Convention
**Critical Rule:** Test filenames must be unique across the entire test suite, regardless of directory.
Python's import system is basename-based, not path-based. Files with identical basenames in different directories cause collection errors.
**Naming Patterns:**
- Primary service tests: `test_<service>_coverage.py` (e.g., `test_agent_governance_service_coverage.py`)
- Module-specific tests: `test_<service>_<module>.py` (e.g., `test_agent_graduation_service_memory.py`)
- Extended coverage: `test_<service>_coverage_extend.py` (e.g., `test_cognitive_tier_system_coverage_extend.py`)
**Examples:**
```
tests/core/agents/test_agent_graduation_service_coverage.py # Primary coverage
tests/core/memory/test_agent_graduation_service_memory.py # Memory module tests
tests/core/episodes/test_episode_retrieval_service_coverage.py # Primary coverage
tests/core/memory/test_episode_retrieval_memory.py # Memory module tests
tests/core/llm/test_cognitive_tier_system_coverage.py # Primary coverage
tests/core/llm/test_cognitive_tier_system_coverage_extend.py # Extended coverage
```
**Anti-Patterns to Avoid:**
- DO NOT use same basename in different directories: `tests/core/agents/test_foo.py` and `tests/core/memory/test_foo.py`
- DO NOT rely on directory structure for uniqueness (Python ignores directories in module resolution)
- DO NOT ignore collection errors (they mask missing tests and inflate coverage gaps)
**Verifying Uniqueness:**
```bash
# Check for duplicate test basenames
find tests/ -name "test_*.py" -type f | xargs -n1 basename | sort | uniq -d
# Expected: No output (no duplicates)
```
**Collection Error Detection:**
```bash
# Check for collection errors before committing
pytest --collect-only -q 2>&1 | grep -c "ERROR collecting"
# Expected: 0
```
### Security Testing Patterns
Security tests validate defense-in-depth protections against real-world attacks. Use these patterns when testing sandbox execution, package installation, or external code execution.
#### Container Escape Tests
Validate Docker isolation prevents breakout attempts:
```python
class TestContainerEscape:
"""Container escape attack prevention tests."""
@patch('core.skill_sandbox.docker.from_env')
def test_privileged_mode_disabled(self, mock_docker, mock_docker_client):
"""
Verify containers NEVER run with --privileged flag.
Privileged mode disables all security mechanisms and allows
full host access (CVE-2019-5736, CVE-2025-9074).
Security: CRITICAL
"""
mock_docker.return_value = mock_docker_client
sandbox = HazardSandbox()
sandbox.execute_python(code="print('test')", inputs={})
# Verify privileged=False (or not set, default is False)
call_kwargs = mock_docker_client.containers.run.call_args[1]
assert call_kwargs.get('privileged', False) == False, \
"Container must NOT run in privileged mode"
@patch('core.skill_sandbox.docker.from_env')
def test_docker_socket_not_mounted(self, mock_docker, mock_docker_client):
"""
Verify Docker socket is NEVER mounted in containers.
Mounting /var/run/docker.sock enables container escape
and full host control (Docker-out-of-Docker attack).
Security: CRITICAL
"""
mock_docker.return_value = mock_docker_client
sandbox = HazardSandbox()
sandbox.execute_python(code="print('test')", inputs={})
call_kwargs = mock_docker_client.containers.run.call_args[1]
volumes = call_kwargs.get('volumes', {})
assert '/var/run/docker.sock' not in str(volumes), \
"Docker socket must NOT be mounted (enables container escape)"
```
#### Resource Exhaustion Tests
Validate resource limits prevent DoS attacks:
```python
class TestResourceExhaustion:
"""Resource limit enforcement tests."""
@patch('core.skill_sandbox.docker.from_env')
def test_memory_limit_enforced(self, mock_docker, mock_docker_client):
"""
Verify memory limit is set to prevent exhaustion attacks.
Security: HIGH - Memory exhaustion can DoS the host
"""
mock_docker.return_value = mock_docker_client
sandbox = HazardSandbox()
sandbox.execute_python(
code="print('test')",
inputs={},
memory_limit="256m"
)
# Verify mem_limit is set
call_kwargs = mock_docker_client.containers.run.call_args[1]
assert call_kwargs.get('mem_limit') == "256m", \
"Memory limit must be enforced to prevent exhaustion attacks"
@patch('core.skill_sandbox.docker.from_env')
def test_cpu_quota_enforced(self, mock_docker, mock_docker_client):
"""
Verify CPU quota is set to prevent CPU exhaustion.
Security: HIGH - CPU exhaustion can starve host processes
"""
mock_docker.return_value = mock_docker_client
sandbox = HazardSandbox()
sandbox.execute_python(
code="print('test')",
inputs={},
cpu_limit=0.5
)
# Verify cpu_quota is set (0.5 * 100000 = 50000)
call_kwargs = mock_docker_client.containers.run.call_args[1]
assert call_kwargs.get('cpu_quota') == 50000, \
"CPU quota must be enforced (0.5 * 100000)"
```
#### Network Isolation Tests
Validate network isolation prevents data exfiltration:
```python
class TestNetworkIsolation:
"""Network isolation enforcement tests."""
@patch('core.skill_sandbox.docker.from_env')
def test_network_disabled(self, mock_docker, mock_docker_client):
"""
Verify network is disabled to prevent data exfiltration.
Security: CRITICAL - Network isolation prevents outbound attacks
"""
mock_docker.return_value = mock_docker_client
sandbox = HazardSandbox()
sandbox.execute_python(code="print('test')", inputs={})
# Verify network_disabled=True
call_kwargs = mock_docker_client.containers.run.call_args[1]
assert call_kwargs.get('network_disabled') is True, \
"Network must be disabled to prevent data exfiltration"
```
#### Malicious Pattern Detection Tests
Validate static scanning detects malicious code patterns:
```python
class TestMaliciousPatternDetection:
"""Static scanning detects malicious patterns."""
def test_subprocess_usage_detected(self, security_scanner):
"""
Static scan detects subprocess usage.
Security: HIGH - subprocess enables arbitrary command execution
"""
malicious_code = """
import subprocess
user_input = 'rm -rf /'
subprocess.call(user_input, shell=True)
"""
result = security_scanner.scan_skill(
skill_name="malicious-subprocess",
skill_content=malicious_code
)
assert result["safe"] == False, "Subprocess usage must be blocked"
assert len(result["findings"]) > 0, "Security findings must be reported"
assert any("subprocess" in f.lower() for f in result["findings"]), \
"Finding must mention subprocess"
def test_base64_obfuscation_detected(self, security_scanner):
"""
Static scan detects base64 obfuscation.
Security: HIGH - Base64 obfuscation hides malicious payloads
"""
malicious_code = """
import base64
payload = 'c3VicHJvY2Vzcy5ydW4oWyJybSIsICJyZiIsICIvIl0p'
decoded = base64.b64decode(payload).decode()
exec(decoded)
"""
result = security_scanner.scan_skill(
skill_name="obfuscated-base64",
skill_content=malicious_code
)
# Should detect base64.b64decode as suspicious
assert result["safe"] == False or len(result["findings"]) > 0, \
"Base64 obfuscation must be flagged"
```
#### Governance Blocking Tests
Validate maturity-based access controls:
```python
class TestGovernanceBlocking:
"""Maturity-based governance blocks unauthorized access."""
def test_student_agent_blocked_from_python_packages(self, governance, db_session: Session):
"""
STUDENT agents cannot use Python packages (non-negotiable).
Security: CRITICAL - Educational restriction, cannot be bypassed
"""
student_agent = StudentAgentFactory(_session=db_session)
db_session.commit()
result = governance.check_package_permission(
agent_id=student_agent.id,
package_name="numpy",
version="1.21.0",
db=db_session
)
assert result["allowed"] == False, \
"STUDENT agents must be blocked from ALL Python packages"
assert "STUDENT agents cannot" in result["reason"], \
"Reason must mention STUDENT restriction"
def test_banned_package_blocks_all_agents(self, governance, db_session: Session):
"""
Banned packages block ALL agents regardless of maturity.
Security: CRITICAL - Ban list overrides all other rules
"""
# Ban package
governance.ban_package(
package_name="malicious-lib",
version="1.0.0",
reason="Security vulnerability: CVE-2025-99999",
db=db_session
)
# Create AUTONOMOUS agent (highest maturity)
autonomous_agent = AutonomousAgentFactory(_session=db_session)
db_session.commit()
result = governance.check_package_permission(
agent_id=autonomous_agent.id,
package_name="malicious-lib",
version="1.0.0",
db=db_session
)
assert result["allowed"] == False, \
"Banned packages must block even AUTONOMOUS agents"
assert "banned" in result["reason"].lower(), \
"Reason must mention ban"
```
#### Security Testing Best Practices
1. **Mock external dependencies** - Use `@patch` decorators to mock Docker, subprocess calls
2. **Test malicious fixtures** - Create fixture files with attack samples for reproducible testing
3. **Defense-in-depth validation** - Test all security layers (static scan + sandbox + governance)
4. **Security level annotations** - Mark tests with security level (CRITICAL, HIGH, MEDIUM, LOW)
5. **Clear assertion messages** - Explain WHY the security constraint is critical (reference CVEs)
6. **Use malicious package fixtures** - Import from `tests.fixtures.malicious_packages` for attack samples
Reference: `backend/tests/test_package_security.py` for comprehensive examples.
### API Route Testing with BaseAPIRouter
Testing API routes that use BaseAPIRouter requires specific patterns for mock patching and error response assertions.
#### Mock Patching: Patch Where Imported
Always patch services at their import location in the route module, not at their definition location.
```python
# Route file imports:
# api/admin/business_facts_routes.py
from core.agent_world_model import WorldModelService
# Test file patches at import location:
with patch('api.admin.business_facts_routes.WorldModelService', return_value=mock_service):
response = client.get("/api/admin/governance/facts/123")
```
**Why:** When the route module imports `WorldModelService` at module level, it creates a reference to the original class. Patching at the definition location (`core.agent_world_model.WorldModelService`) doesn't affect this already-imported reference. Patching at the import location intercepts the reference the route actually uses.
**Exception:** For services imported inside functions (not at module level), patch at the original location:
```python
# Route function with local import:
def upload_document(file: UploadFile):
from core.storage import get_storage_service # Local import
storage = get_storage_service()
# Test patch location:
with patch('core.storage.get_storage_service', return_value=mock_storage):
response = client.post("/upload", files={"file": test_file})
```
#### Error Response Assertions
BaseAPIRouter.error_response() returns structured errors. Access nested message field:
```python
# Success response (200 OK)
response = client.get("/api/admin/governance/facts/123")
assert response.status_code == 200
data = response.json()
assert data["success"] == True
assert data["data"]["fact"] == "Invoices over $500 require VP approval"
# Error response (404, 400, 500, etc.)
response = client.get("/api/admin/governance/facts/non-existent")
assert response.status_code == 404
detail = response.json()["detail"]
assert detail["success"] == False
assert detail["error"]["code"] == "NOT_FOUND"
assert "not found" in detail["error"]["message"].lower() # String operations on message
```
**Error Response Structure:**
```json
{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "Business fact not found: fact-123",
"timestamp": "2026-03-20T11:05:21.123456",
"details": {}
}
}
```
**Common HTTP Status Codes:**
- `400` - VALIDATION_ERROR (invalid input from BaseAPIRouter.validation_error)
- `401` - UNAUTHORIZED (missing/invalid auth)
- `403` - FORBIDDEN (insufficient permissions)
- `404` - NOT_FOUND (resource not found from BaseAPIRouter.not_found_error)
- `500` - INTERNAL_ERROR (server error)
#### Service Mock Fixtures
Use AsyncMock for async services, configure returns in fixture:
```python
@pytest.fixture
def mock_world_model_service(sample_business_fact):
"""Mock WorldModelService with configured return values."""
mock = AsyncMock()
mock.get_fact_by_id.return_value = sample_business_fact
mock.list_all_facts.return_value = [sample_business_fact]
mock.create_fact.return_value = sample_business_fact
return mock
def test_get_fact_success(authenticated_admin_client, mock_world_model_service):
"""Test getting a business fact by ID."""
with patch('api.admin.business_facts_routes.WorldModelService',
return_value=mock_world_model_service):
response = authenticated_admin_client.get("/api/admin/governance/facts/fact-123")
assert response.status_code == 200
data = response.json()
assert data["success"] == True
```
**AsyncMock vs MagicMock:**
- **AsyncMock:** For async service methods (most API services)
- **MagicMock:** For sync methods or non-service objects
#### Configure Mocks Inside Patch Context
When overriding fixture defaults, configure mocks **inside** the patch context manager:
```python
# Before (incorrect):
mock_extractor.extract_facts_from_document.return_value = result
with patch('api.admin.routes.get_policy_fact_extractor', return_value=mock_extractor):
# Test - uses fixture default, not result
# After (correct):
with patch('api.admin.routes.get_policy_fact_extractor',
return_value=mock_extractor) as patched_extractor:
patched_extractor.extract_facts_from_document.return_value = result
# Test - uses test-specific result
```
#### S3/R2 Storage Mocking
Mock storage services for citation verification:
```python
@pytest.fixture
def mock_storage_service():
"""Mock S3/R2 storage service."""
mock = MagicMock()
mock.upload_file.return_value = "s3://atom-business-facts/uploads/test.pdf"
mock.check_exists.return_value = True
mock.download_file.return_value = b"PDF content bytes"
return mock
```
#### PDF Extraction Mocking
Mock PDF extraction services for document upload tests:
```python
@pytest.fixture
def mock_policy_extractor():
"""Mock policy fact extractor."""
mock = AsyncMock()
mock.extract_facts_from_document.return_value = {
"facts": [
{
"fact": "Invoices over $500 require VP approval",
"citations": ["s3://atom-business-facts/policies/ap-policy.pdf:page:5"],
"reason": "Extracted from AP policy document",
"confidence": 0.95
}
],
"metadata": {"source": "ap-policy.pdf", "page_count": 10}
}
return mock
```
#### Complete Example
```python
def test_upload_and_extract_success(authenticated_admin_client,
mock_policy_extractor,
mock_storage_service):
"""Test successful document upload and fact extraction."""
test_file = io.BytesIO(b"%PDF-1.4 ... test PDF content ...")
with patch('api.admin.business_facts_routes.get_policy_fact_extractor',
return_value=mock_policy_extractor):
with patch('core.storage.get_storage_service', return_value=mock_storage_service):
response = authenticated_admin_client.post(
"/api/admin/governance/facts/upload",
files={"file": ("test.pdf", test_file, "application/pdf")}
)
assert response.status_code == 201
data = response.json()
assert len(data["data"]["facts"]) == 1
assert "VP approval" in data["data"]["facts"][0]["fact"]
```
**Reference:** See `.planning/phases/216-fix-business-facts-test-failures/216-PATTERN-DOC.md` for complete patterns with before/after examples from Phase 216 fixes.
## Code Review Checklist
Before submitting a PR, verify:
- [ ] All functions have type hints (parameters and return types)
- [ ] MyPy passes without critical errors
- [ ] Black formatting applied
- [ ] Ruff linting passes
- [ ] All functions have docstrings (Args/Returns)
- [ ] Error handling uses specific exception types
- [ ] No bare `except:` clauses
- [ ] Logging includes relevant context
- [ ] Tests added for new functionality
- [ ] Tests pass locally
## Continuous Integration
MyPy will be integrated into CI/CD pipeline:
```yaml
# Type check with mypy (commented out during incremental adoption)
# - name: Type check with mypy
# run: mypy core/ --config-file backend/mypy.ini
```
**Note:** During incremental adoption, MyPy is run locally but not enforced in CI. Once type coverage reaches 80%, CI enforcement will be enabled.
## Resources
- [MyPy Documentation](https://mypy.readthedocs.io/)
- [Black Documentation](https://black.readthedocs.io/)
- [Ruff Documentation](https://docs.astral.sh/ruff/)
- [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html)
- [PEP 8 - Style Guide for Python Code](https://peps.python.org/pep-0008/)
## Compliance
All Atom backend developers MUST follow these standards. Code that does not meet these standards will not be merged to main.
**Questions?** Contact the platform team or open an issue in the repository.
|