# Story 10.17: Schema Contract Test Infrastructure

**Status:** Complete
**Priority:** High
**Depends on:** Story 1.26 (Canonical Log Schema v1.1 Definition)

---

## Context / Background

The schema inconsistency between `build_envelope()` and `serialize_envelope()` (addressed in Stories 1.26-1.28) went undetected despite extensive test coverage. Root cause analysis reveals structural gaps in the test strategy:

**Why Tests Didn't Catch It:**

1. **Isolation testing** - `build_envelope()` and `serialize_envelope()` tested separately, never together
2. **Synthetic test data** - Property tests generate valid input matching `serialize_envelope()` expectations (tests/property/test_serialization_properties.py:36-37), not actual `build_envelope()` output
3. **Fallback as feature** - Tests treated the exception-driven fallback as expected behavior, not a failure signal
4. **No schema validation** - No tests validate output against `jsonschema/log_envelope_v1.json`

**Evidence:**
```python
# Property test generates synthetic data with context/diagnostics
envelope_logs = st.fixed_dictionaries({
    "context": st.dictionaries(...),      # Manually added - not from build_envelope()
    "diagnostics": st.dictionaries(...),  # Manually added - not from build_envelope()
})

# No test exists that does this:
serialize_envelope(build_envelope(...))  # Would have caught the mismatch
```

This story adds contract tests and schema validation to catch schema drift early.

---

## Scope (In / Out)

### In Scope

- Contract tests verifying producer/consumer schema compatibility
- JSON schema validation at key pipeline boundaries
- Shared type definitions for schema enforcement
- CI enforcement of contract tests
- Test fixture to fail-fast on fallback path
- Documentation of contract testing patterns

### Out of Scope

- Fixing the schema mismatch (Stories 1.26-1.28)
- Performance testing infrastructure
- New test frameworks or tools

---

## Acceptance Criteria

### AC1: Contract Test for Envelope Roundtrip

**Description:** A contract test verifies that `build_envelope()` output is valid input for `serialize_envelope()`.

**Validation:**
```python
# tests/contract/test_envelope_contract.py

import pytest
from fapilog.core.envelope import build_envelope
from fapilog.core.serialization import serialize_envelope

class TestEnvelopeContract:
    """Contract tests ensuring schema compatibility between pipeline stages."""

    def test_build_envelope_output_is_serializable(self):
        """build_envelope() output must be valid serialize_envelope() input."""
        envelope = build_envelope(level="INFO", message="test message")

        # This should NOT raise - if it does, schemas have drifted
        view = serialize_envelope(envelope)
        assert view is not None
        assert len(view.data) > 0

    def test_build_envelope_with_all_options_is_serializable(self):
        """Full-featured envelope must serialize without exception."""
        envelope = build_envelope(
            level="ERROR",
            message="error occurred",
            extra={"user_id": "u-123", "action": "login"},
            correlation_id="corr-abc",
            logger_name="myapp.auth",
            exc_info=False,
        )

        view = serialize_envelope(envelope)
        assert view is not None
```

### AC2: JSON Schema Validation Tests

**Description:** Tests validate pipeline output against the canonical JSON schema.

**Validation:**
```python
# tests/contract/test_schema_validation.py

import json
import jsonschema
import pytest
from fapilog.core.envelope import build_envelope
from fapilog.core.serialization import serialize_envelope

@pytest.fixture
def envelope_schema():
    with open("jsonschema/log_envelope_v1.json") as f:
        return json.load(f)

def test_serialized_output_validates_against_schema(envelope_schema):
    """Serialized envelope must conform to published JSON schema."""
    envelope = build_envelope(level="INFO", message="test")
    view = serialize_envelope(envelope)
    parsed = json.loads(view.data)

    # Should not raise ValidationError
    jsonschema.validate(parsed, envelope_schema)

def test_enriched_envelope_validates_against_schema(envelope_schema):
    """Envelope after enrichment must still conform to schema."""
    # Test with enrichers applied
    ...
```

### AC3: Shared Type Definition

**Description:** A shared TypedDict or Protocol defines the canonical schema, used by both `build_envelope()` and `serialize_envelope()`.

**Validation:**
```python
# src/fapilog/core/schema.py

from typing import Any, TypedDict

class LogContext(TypedDict, total=False):
    correlation_id: str | None
    request_id: str | None
    user_id: str | None
    tenant_id: str | None
    trace_id: str | None
    span_id: str | None

class LogDiagnostics(TypedDict, total=False):
    service: str | None
    env: str | None
    host: str | None
    pid: int | None
    exception: dict[str, Any] | None

class LogEnvelopeV1(TypedDict):
    timestamp: str
    level: str
    message: str
    logger: str | None
    context: LogContext
    diagnostics: LogDiagnostics
    data: dict[str, Any]

# Used in function signatures:
def build_envelope(...) -> LogEnvelopeV1: ...
def serialize_envelope(log: LogEnvelopeV1) -> SerializedView: ...
```

**Validation:**
- `mypy` catches type mismatches at lint time
- IDE provides autocomplete for schema fields
- Refactoring updates all usages

### AC4: Fail-Fast Test Fixture for Fallback Path

**Description:** A pytest fixture makes the fallback path fail loudly, catching unintended schema drift in any test.

**Validation:**
```python
# conftest.py (project root)

import pytest

@pytest.fixture
def strict_serialization(monkeypatch):
    """Fail if fallback serialization is triggered.

    Use this fixture in tests where the happy path should always work.
    If fallback triggers, it indicates schema drift.
    """
    def _fail_on_fallback(*args, **kwargs):
        pytest.fail(
            "Fallback serialization triggered! "
            "This indicates schema mismatch between build_envelope() and serialize_envelope(). "
            "See Story 10.17 for context."
        )

    monkeypatch.setattr(
        "fapilog.core.worker.serialize_mapping_to_json_bytes",
        _fail_on_fallback
    )
    yield

# Usage in tests:
def test_normal_logging_uses_envelope_path(strict_serialization, logger):
    """Normal logging should never trigger fallback."""
    logger.info("test message")
    # If fallback is triggered, test fails with clear message
```

### AC5: Property Test Uses Real build_envelope()

**Description:** Update property tests to use actual `build_envelope()` output, not synthetic data.

**Validation:**
```python
# tests/property/test_serialization_properties.py

from hypothesis import given
from hypothesis import strategies as st
from fapilog.core.envelope import build_envelope
from fapilog.core.serialization import serialize_envelope

@given(
    level=st.sampled_from(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]),
    message=st.text(min_size=1, max_size=200),
    extra=st.dictionaries(st.text(min_size=1, max_size=20), st.text(), max_size=5),
)
def test_build_then_serialize_never_raises(level, message, extra):
    """Property: serialize_envelope(build_envelope(...)) never raises."""
    envelope = build_envelope(level=level, message=message, extra=extra)

    # Must not raise for any valid input combination
    view = serialize_envelope(envelope)
    assert view is not None
```

### AC6: CI Enforces Contract Tests

**Description:** Contract tests run in CI and block PRs if they fail.

**Validation:**
- Contract tests in `tests/contract/` directory
- CI runs `pytest tests/contract/ -v` as separate step
- Contract test failures are clearly labeled in CI output
- PR cannot merge if contract tests fail

### AC7: Contract Testing Documentation

**Description:** Document the contract testing pattern for contributors.

**Validation:**
- `docs/contributing/contract-tests.md` explains:
  - What contract tests are and why they exist
  - How to write new contract tests
  - When to use `strict_serialization` fixture
  - How schema drift is detected
- CONTRIBUTING.md references the contract test docs

---

## Implementation Notes

### File Changes

```
src/fapilog/core/schema.py (NEW - shared type definitions)
tests/contract/__init__.py (NEW)
tests/contract/test_envelope_contract.py (NEW)
tests/contract/test_schema_validation.py (NEW)
tests/contract/test_pipeline_contract.py (NEW)
conftest.py (root) (MODIFIED - add strict_serialization fixture)
tests/property/test_serialization_properties.py (MODIFIED - use build_envelope())
.github/workflows/ci.yml (MODIFIED - add contract test step)
docs/contributing/contract-tests.md (NEW)
CONTRIBUTING.md (MODIFIED - reference contract tests)
```

### Contract Test Categories

| Category | Purpose | Example |
|----------|---------|---------|
| **Roundtrip** | Producer output → Consumer input | `serialize_envelope(build_envelope(...))` |
| **Schema** | Output validates against JSON schema | `jsonschema.validate(output, schema)` |
| **Pipeline** | Full pipeline produces valid output | Logger → Worker → Sink with real components |
| **Enricher** | Enriched events still conform | Event + enrichment → valid schema |

### Directory Structure

```
/
├── conftest.py                  # Root conftest - add strict_serialization fixture here
└── tests/
    ├── contract/                # Contract tests (schema compatibility) - NEW
    │   ├── __init__.py
    │   ├── test_envelope_contract.py
    │   ├── test_schema_validation.py
    │   └── test_pipeline_contract.py
    ├── unit/                    # Unit tests (isolated components)
    ├── integration/             # Integration tests (component interaction)
    └── property/                # Property-based tests (invariants)
```

---

## Tasks

### Phase 1: Contract Test Infrastructure

- [ ] Create `tests/contract/` directory structure
- [ ] Implement `test_envelope_contract.py` with roundtrip tests
- [ ] Implement `test_schema_validation.py` with JSON schema validation
- [ ] Add `strict_serialization` fixture to root `conftest.py`

### Phase 2: Type Definitions

- [ ] Create `src/fapilog/core/schema.py` with TypedDict definitions
- [ ] Update `build_envelope()` return type annotation
- [ ] Update `serialize_envelope()` parameter type annotation
- [ ] Verify `mypy` catches type mismatches

### Phase 3: Property Test Updates

- [ ] Update `test_serialization_properties.py` to use `build_envelope()`
- [ ] Add property test for roundtrip invariant
- [ ] Remove synthetic `envelope_logs` strategy (or keep for edge cases)

### Phase 4: CI Integration

- [ ] Add contract test step to CI workflow
- [ ] Ensure contract test failures block PR merge
- [ ] Add clear labeling for contract test results

### Phase 5: Documentation

- [ ] Create `docs/contributing/contract-tests.md`
- [ ] Update CONTRIBUTING.md with contract test section
- [ ] Add inline comments explaining contract test rationale

---

## Tests

### Contract Tests (New)

- `tests/contract/test_envelope_contract.py`
  - `test_build_envelope_output_is_serializable`
  - `test_build_envelope_with_all_options_is_serializable`
  - `test_build_envelope_with_exception_is_serializable`
  - `test_enriched_envelope_is_serializable`

- `tests/contract/test_schema_validation.py`
  - `test_serialized_output_validates_against_schema`
  - `test_enriched_envelope_validates_against_schema`
  - `test_full_pipeline_output_validates_against_schema`

- `tests/contract/test_pipeline_contract.py`
  - `test_logger_to_sink_produces_valid_output`
  - `test_async_logger_to_sink_produces_valid_output`

### Property Tests (Modified)

- `tests/property/test_serialization_properties.py`
  - `test_build_then_serialize_never_raises` (NEW)
  - Update existing tests to use real `build_envelope()` where appropriate

---

## Definition of Done

### Code Complete

- [ ] All acceptance criteria implemented
- [ ] Contract tests catch the original schema mismatch (verify by temporarily breaking schema)
- [ ] Type definitions provide IDE support and mypy checking
- [ ] `strict_serialization` fixture available for any test

### Quality Assurance

- [ ] Contract tests pass
- [ ] Property tests updated and pass
- [ ] `mypy` passes with new type annotations
- [ ] CI runs contract tests and reports clearly
- [ ] Existing tests unaffected

### Documentation

- [ ] Contract testing guide written
- [ ] CONTRIBUTING.md updated
- [ ] Code comments explain contract test purpose

---

## Risks / Rollback

### Risks

1. **Risk:** Type annotations too strict, break valid code
   - **Mitigation:** Use `total=False` for optional fields; TypedDict is structural

2. **Risk:** Contract tests too slow for CI
   - **Mitigation:** Contract tests are fast (no I/O); separate CI step allows parallel execution

3. **Risk:** False positives from `strict_serialization` fixture
   - **Mitigation:** Fixture is opt-in; only use where fallback should never trigger

### Rollback Plan

If issues occur:
1. Remove contract test CI step (tests still run locally)
2. Make `strict_serialization` fixture no-op
3. Type annotations can be loosened without breaking runtime

---

## Related Stories

- **Depends on:** Story 1.26 - Canonical Log Schema v1.1 (defines the schema to test against)
- **Prevents:** Future schema drift issues like the one identified in audit
- **Related:** Story 10.15 - Documentation Correctness CI Enforcement (similar CI pattern)

---

## Success Metrics

After implementation, the following should be true:

1. **Immediate detection:** If someone changes `build_envelope()` output or `serialize_envelope()` input requirements, contract tests fail immediately
2. **Clear failure message:** Test failure explains *why* it failed and points to relevant documentation
3. **Type safety:** `mypy` catches schema mismatches at lint time, before tests even run
4. **No silent fallback:** Tests using `strict_serialization` fail loudly if fallback path is triggered

---

## Code Review

**Date:** 2026-01-19
**Reviewer:** Claude
**Verdict:** OK to PR

### Summary

Comprehensive schema contract test infrastructure added with 23 contract tests, TypedDict definitions, JSON schema validation, strict_serialization fixture, CI integration, and documentation.

### AC Verification

| Criterion | Evidence |
|-----------|----------|
| AC1: Contract test roundtrip | `tests/contract/test_envelope_contract.py:24-104` - 6 tests |
| AC2: JSON Schema validation | `tests/contract/test_schema_validation.py:29-151` - 8 tests |
| AC3: Shared type definition | `src/fapilog/core/schema.py:1-64` - TypedDicts defined |
| AC4: Fail-fast fixture | `conftest.py:112-146` - strict_serialization fixture |
| AC5: Property tests updated | `tests/property/test_serialization_properties.py:71-124` |
| AC6: CI enforcement | `.github/workflows/ci.yml:99-127` - contract test job |
| AC7: Documentation | `docs/contributing/contract-tests.md` + CONTRIBUTING.md |

### Quality Gates

- [x] ruff check passed
- [x] ruff format passed
- [x] mypy passed
- [x] No weak assertions
- [x] No dead code
- [x] All 23 contract tests pass
- [x] All 4 property tests pass

---

## Change Log

| Date | Change | Author |
|------|--------|--------|
| 2025-01-19 | Initial draft based on audit root cause analysis | Claude |
| 2026-01-19 | Implementation complete with code review | Claude |
