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.