
============================================================
 SL-004 — UPDATE OUTDATED TEST CONTRACT
============================================================
UTC=2026-09-10T15:28:09Z
HOST=Azeroth
RC=/data/files/bmw/stocklab-release-candidate-sl003-final-20260910-111453
TEST=/data/files/bmw/stocklab-release-candidate-sl003-final-20260910-111453/tests/test_provider_resilience.py

=== 1. SAFETY ===
FOUND /data/files/bmw/stocklab-release-candidate-sl003-final-20260910-111453/app/adapters/resolver.py
FOUND /data/files/bmw/stocklab-release-candidate-sl003-final-20260910-111453/app/adapters/validation.py
FOUND /data/files/bmw/stocklab-release-candidate-sl003-final-20260910-111453/app/workers/ingest.py
FOUND /data/files/bmw/stocklab-release-candidate-sl003-final-20260910-111453/tests/test_provider_resilience.py
FOUND /data/files/bmw/stocklab-release-candidate-sl003-final-20260910-111453/tests/test_validation.py

=== 2. PRODUCTION SNAPSHOT BEFORE ===
stocklab-redis	docker.io/library/redis:7-alpine	Up 33 hours (healthy)
stocklab-db	docker.io/library/postgres:16-alpine	Up 33 hours (healthy)
stocklab-release-qa-redis	docker.io/library/redis:7	Up 26 hours
stocklab-frontend	localhost/stocklab-frontend:release-qa-20260909-182234	Up 24 hours (healthy)
stocklab-backend	localhost/stocklab-backend:screener-percent-final-20260909-194639	Up 21 hours (healthy)
stocklab-worker	localhost/stocklab-backend:screener-percent-final-20260909-194639	Up 21 hours (healthy)
stocklab-beat	localhost/stocklab-backend:screener-percent-final-20260909-194639	Up 21 hours

Restart counters BEFORE:
stocklab-backend restart=0
stocklab-worker restart=0
stocklab-beat restart=0
stocklab-db restart=0
stocklab-redis restart=0
stocklab-frontend restart=0

Ports BEFORE:
LISTEN 0      2048       127.0.0.1:18000      0.0.0.0:*    users:(("uvicorn",pid=1574355,fd=11))                                                                                                                                                             
LISTEN 0      511        127.0.0.1:6379       0.0.0.0:*    users:(("redis-server",pid=1133711,fd=6))                                                                                                                                                         
LISTEN 0      200        127.0.0.1:5432       0.0.0.0:*    users:(("postgres",pid=1141225,fd=6))                                                                                                                                                             
LISTEN 0      511        127.0.0.1:13000      0.0.0.0:*    users:(("next-server (v1",pid=1497838,fd=18))                                                                                                                                                     

=== 3. SOURCE HASHES BEFORE ===
resolver   183842352c4c3b0ec9e2fabffa043ff6fbb125c2a0b602c9da53477d17b7bfb8
validation 4ff976fb3b68594b7991c8d172143278670ae3685d49ad61f5b945db61b4f19a
ingest     782820e1a686029f751c148d327dbef76784fb6ef1d52107aefa9c3a75c08af6
test       0dd74d391feda2eb9705b09fedcc3c81894c18e092d723b285721a29af1215e6

=== 4. INSPECT EXACT OUTDATED TEST ===
    primary = {"revenue": 100.0, "net_income": 10.0}
    secondary = {"revenue": 100.0, "net_income": 50.0}  # net_income disagrees by 400%
    resolved, conflicts = resolve_line_items(primary, secondary, _FIELDS, "PRIMARY", "SECONDARY")
    assert resolved["net_income"] == 10.0  # PRIMARY still wins tier order despite the conflict
    assert len(conflicts) == 1
    assert conflicts[0].field == "net_income"
    assert conflicts[0].value_a == 10.0 and conflicts[0].value_b == 50.0


def test_resolve_line_items_official_filing_secondary_overrides_primary_tier():
    # if the "secondary" adapter is actually the higher-priority tier for this field (e.g. an
    # OFFICIAL_FILING source plugged in as `secondary`), its value must still win -- tier order,
    # not argument position, decides.
    primary = {"revenue": 100.0}  # tier PRIMARY
    secondary = {"revenue": 105.0}  # tier OFFICIAL_FILING -- higher priority
    resolved, conflicts = resolve_line_items(primary, secondary, ("revenue",), "PRIMARY", "OFFICIAL_FILING")
    assert resolved["revenue"] == 105.0


def test_resolve_line_items_one_source_missing_a_field_other_has_it():
    primary = {"revenue": 100.0}  # net_income missing from primary entirely
    secondary = {"revenue": 100.0, "net_income": 10.0}
    resolved, conflicts = resolve_line_items(primary, secondary, _FIELDS, "PRIMARY", "SECONDARY")
    assert resolved["net_income"] == 10.0  # only source with a value wins, not treated as a conflict
    assert conflicts == []


# --- Part A9: merge_statement_line_items (the critical ingestion bug fix) ---


def test_merge_combines_all_three_statements():
    """The bug this guards: before Part A9, only the income dict reached the BalanceSheet/CashFlow
    tables, so every balance-sheet and cash-flow column was NULL for FMP-sourced data."""
    merged = merge_statement_line_items(
        {"revenue": 1000.0, "net_income": 100.0},
        {"total_assets": 5000.0, "shareholders_equity": 2000.0},
        {"operating_cash_flow": 150.0, "capital_expenditure": 40.0},
    )

=== 5. BACKUP TEST FILE ===
TEST_BACKUP=PASS

=== 6. GUARDED TEST CONTRACT UPDATE ===
TEST_CONTRACT_UPDATE=PASS

=== 7. SHOW UPDATED TEST ===
    # not argument position, decides.
    primary = {"revenue": 100.0}  # tier PRIMARY
    secondary = {"revenue": 105.0}  # tier OFFICIAL_FILING -- higher priority
    resolved, conflicts = resolve_line_items(primary, secondary, ("revenue",), "PRIMARY", "OFFICIAL_FILING")
    assert resolved["revenue"] == 105.0


def test_resolve_line_items_one_source_missing_a_field_other_has_it():
    primary = {"revenue": 100.0}  # net_income missing from primary entirely
    secondary = {"revenue": 100.0, "net_income": 10.0}
    resolved, conflicts = resolve_line_items(primary, secondary, _FIELDS, "PRIMARY", "SECONDARY")
    assert resolved["net_income"] == 10.0
    assert len(conflicts) == 1
    assert conflicts[0].field == "net_income"
    assert conflicts[0].value_a is None
    assert conflicts[0].value_b == 10.0
    assert conflicts[0].selected_value == 10.0
    assert conflicts[0].selection_reason == "Primary field missing; value supplied by secondary source"


# --- Part A9: merge_statement_line_items (the critical ingestion bug fix) ---


def test_merge_combines_all_three_statements():
    """The bug this guards: before Part A9, only the income dict reached the BalanceSheet/CashFlow
    tables, so every balance-sheet and cash-flow column was NULL for FMP-sourced data."""

=== 8. RUN EXISTING TEST SUITES ===
Collecting pytest==8.3.3
  Downloading pytest-8.3.3-py3-none-any.whl.metadata (7.5 kB)
Collecting pytest-asyncio==0.24.0
  Downloading pytest_asyncio-0.24.0-py3-none-any.whl.metadata (3.9 kB)
Collecting pytest-cov==5.0.0
  Downloading pytest_cov-5.0.0-py3-none-any.whl.metadata (27 kB)
Collecting freezegun==1.5.1
  Downloading freezegun-1.5.1-py3-none-any.whl.metadata (11 kB)
Collecting iniconfig (from pytest==8.3.3)
  Downloading iniconfig-2.3.0-py3-none-any.whl.metadata (2.5 kB)
Requirement already satisfied: packaging in /usr/local/lib/python3.11/site-packages (from pytest==8.3.3) (26.3)
Collecting pluggy<2,>=1.5 (from pytest==8.3.3)
  Downloading pluggy-1.6.0-py3-none-any.whl.metadata (4.8 kB)
Collecting coverage>=5.2.1 (from coverage[toml]>=5.2.1->pytest-cov==5.0.0)
  Downloading coverage-7.16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl.metadata (8.6 kB)
Requirement already satisfied: python-dateutil>=2.7 in /usr/local/lib/python3.11/site-packages (from freezegun==1.5.1) (2.9.0.post0)
Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.11/site-packages (from python-dateutil>=2.7->freezegun==1.5.1) (1.17.0)
Downloading pytest-8.3.3-py3-none-any.whl (342 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 342.3/342.3 kB 39.8 MB/s eta 0:00:00
Downloading pytest_asyncio-0.24.0-py3-none-any.whl (18 kB)
Downloading pytest_cov-5.0.0-py3-none-any.whl (21 kB)
Downloading freezegun-1.5.1-py3-none-any.whl (17 kB)
Downloading coverage-7.16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl (255 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 255.8/255.8 kB 50.2 MB/s eta 0:00:00
Downloading pluggy-1.6.0-py3-none-any.whl (20 kB)
Downloading iniconfig-2.3.0-py3-none-any.whl (7.5 kB)
Installing collected packages: pluggy, iniconfig, coverage, pytest, freezegun, pytest-cov, pytest-asyncio
Successfully installed coverage-7.16.0 freezegun-1.5.1 iniconfig-2.3.0 pluggy-1.6.0 pytest-8.3.3 pytest-asyncio-0.24.0 pytest-cov-5.0.0
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv

=== PROVIDER RESILIENCE ===
/usr/local/lib/python3.11/site-packages/pytest_asyncio/plugin.py:208: PytestDeprecationWarning: The configuration option "asyncio_default_fixture_loop_scope" is unset.
The event loop scope for asynchronous fixtures will default to the fixture caching scope. Future versions of pytest-asyncio will default the loop scope for asynchronous fixtures to function scope. Set the default fixture loop scope explicitly in order to avoid unexpected behavior in the future. Valid fixture loop scopes are: "function", "class", "module", "package", "session"

  warnings.warn(PytestDeprecationWarning(_DEFAULT_FIXTURE_LOOP_SCOPE_UNSET))
....................                                                     [100%]
20 passed in 0.26s

=== VALIDATION ===
/usr/local/lib/python3.11/site-packages/pytest_asyncio/plugin.py:208: PytestDeprecationWarning: The configuration option "asyncio_default_fixture_loop_scope" is unset.
The event loop scope for asynchronous fixtures will default to the fixture caching scope. Future versions of pytest-asyncio will default the loop scope for asynchronous fixtures to function scope. Set the default fixture loop scope explicitly in order to avoid unexpected behavior in the future. Valid fixture loop scopes are: "function", "class", "module", "package", "session"

  warnings.warn(PytestDeprecationWarning(_DEFAULT_FIXTURE_LOOP_SCOPE_UNSET))
.............................                                            [100%]
29 passed in 0.27s

PROVIDER_STATUS=0
VALIDATION_STATUS=0
EXISTING_TESTS=PASS
EXISTING_TESTS=PASS

=== 9. DIRECT SL-004 CONTRACT REGRESSION ===
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv
/usr/local/lib/python3.11/site-packages/pytest_asyncio/plugin.py:208: PytestDeprecationWarning: The configuration option "asyncio_default_fixture_loop_scope" is unset.
The event loop scope for asynchronous fixtures will default to the fixture caching scope. Future versions of pytest-asyncio will default the loop scope for asynchronous fixtures to function scope. Set the default fixture loop scope explicitly in order to avoid unexpected behavior in the future. Valid fixture loop scopes are: "function", "class", "module", "package", "session"

  warnings.warn(PytestDeprecationWarning(_DEFAULT_FIXTURE_LOOP_SCOPE_UNSET))
...                                                                      [100%]
3 passed, 46 deselected in 0.30s
SL004_DIRECT_REGRESSION=PASS

=== 10. VERIFY RELEVANT SOURCE INTEGRITY ===
resolver   BEFORE=183842352c4c3b0ec9e2fabffa043ff6fbb125c2a0b602c9da53477d17b7bfb8
resolver   AFTER =183842352c4c3b0ec9e2fabffa043ff6fbb125c2a0b602c9da53477d17b7bfb8
validation BEFORE=4ff976fb3b68594b7991c8d172143278670ae3685d49ad61f5b945db61b4f19a
validation AFTER =4ff976fb3b68594b7991c8d172143278670ae3685d49ad61f5b945db61b4f19a
ingest     BEFORE=782820e1a686029f751c148d327dbef76784fb6ef1d52107aefa9c3a75c08af6
ingest     AFTER =782820e1a686029f751c148d327dbef76784fb6ef1d52107aefa9c3a75c08af6
test       BEFORE=0dd74d391feda2eb9705b09fedcc3c81894c18e092d723b285721a29af1215e6
test       AFTER =489911e9376fdf06b9a5d96517f5a5b79fac810e762acbdcbe4514da877b99dc
TEST_CONTRACT_CHANGED=PASS

=== 11. PRODUCTION SNAPSHOT AFTER ===
stocklab-redis	docker.io/library/redis:7-alpine	Up 33 hours (healthy)
stocklab-db	docker.io/library/postgres:16-alpine	Up 33 hours (healthy)
stocklab-release-qa-redis	docker.io/library/redis:7	Up 26 hours
stocklab-frontend	localhost/stocklab-frontend:release-qa-20260909-182234	Up 24 hours (healthy)
stocklab-backend	localhost/stocklab-backend:screener-percent-final-20260909-194639	Up 21 hours (healthy)
stocklab-worker	localhost/stocklab-backend:screener-percent-final-20260909-194639	Up 21 hours (healthy)
stocklab-beat	localhost/stocklab-backend:screener-percent-final-20260909-194639	Up 21 hours

Restart counters AFTER:
stocklab-backend restart=0
stocklab-worker restart=0
stocklab-beat restart=0
stocklab-db restart=0
stocklab-redis restart=0
stocklab-frontend restart=0

Ports AFTER:
LISTEN 0      2048       127.0.0.1:18000      0.0.0.0:*    users:(("uvicorn",pid=1574355,fd=11))                                                                                                                                                             
LISTEN 0      511        127.0.0.1:6379       0.0.0.0:*    users:(("redis-server",pid=1133711,fd=6))                                                                                                                                                         
LISTEN 0      200        127.0.0.1:5432       0.0.0.0:*    users:(("postgres",pid=1141225,fd=6))                                                                                                                                                             
LISTEN 0      511        127.0.0.1:13000      0.0.0.0:*    users:(("next-server (v1",pid=1497838,fd=18))                                                                                                                                                     

=== 12. FINAL RESULT ===
SL004_TEST_CONTRACT=UPDATED
EXISTING_TESTS=PASS
SL004_DIRECT_REGRESSION=PASS
PRODUCTION=UNCHANGED
RELEASE_GATE=PASS_FOR_SL004_TESTS

LOG=/data/files/bmw/stocklab-sl004-test-contract-fix-20260910-182809.log
TEST_BACKUP=/data/files/bmw/stocklab-release-candidate-sl003-final-20260910-111453/tests/test_provider_resilience.py.before-sl004-test-contract-20260910-182809
============================================================
