Repository navigation
Make the SPM country tests unit tests, with two integration tests against the installed US model - #4136
Merged
Conversation
Add unit tests that send /us/calculate and /us/calculate-full requests through the household routes to a PolicyEngineCountry whose simulations are stand-ins. For each SPM input error code, with and without axes, a chosen measurement returns the 400 validation response with the code and message, and an inherited one returns HTTP 200 with the dependent cell null. A tax-only request returns the chosen area's receipt with no years, and /us/calculate-full asks the country for every omitted yearly variable under the chosen measurement. These check the error-to-response mapping that the real-model HTTP cases repeated for each combination of settings, in milliseconds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ion tests tests/unit/test_country_spm.py ran 37 of its 63 tests against the installed US model, through function-scoped fixtures that reloaded the SPM forecast and built a new app for each test. Those cases are now unit tests over stand-in simulations that raise the installed model's errors, with its messages, and report its receipt shape. They check what the API does with each: a chosen measurement's missing county, unplaceable county, non-FIPS county, unclassified adult and year after the forecast each return the 400 validation response with the model's code and text; the same households without `spm` return HTTP 200 with the threshold null; an area the forecast lacks is rejected before any simulation is built; each explicit geography returns the threshold and its full compact receipt; and a stored household replays its saved selection once, then from the calculation cache. No unit test loads the US model or its forecast. Two tests in tests/integration/test_installed_us_model_spm.py run the installed model, with the app and calculation service built once for the session: a chosen county returns a positive threshold and a receipt for the pinned forecast and the installed packages, and a state-only household is null when it chose nothing and a 400 when it chose a county. make test lists the file, so the pull request Test job and the release tests still run it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add a testing-skill section saying that unit tests stand in for the country model, that new SPM cases go in tests/unit/test_country_spm.py, and what the two integration tests against the installed US model check. Add the changelog fragment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The HTTP stand-in tests replaced PolicyEngineCountry._create_simulation, so they skipped its second SPM normalization and the arguments it passes to the country package's Simulation. The stand-in is now that package's Simulation class, and the country's own _create_simulation builds it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The real-model explicit-geography test asserted this, and removing it left nothing that would notice if PolicyEngineCountry.prepare_calculation stopped copying the household and wrote results into the caller's dictionary. The HTTP tests cannot see it, because the calculation service copies the household first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The unit module's docstring said nothing in it loads the US model or forecast; importing the country module loads both once per process. - The integration module's docstring gave the wrong reason for its session fixtures, and installed_us_country's docstring claimed a certification check it does not make. - The state-only integration test needs the bundle's default SPM geography to be a county; it now says so with an assertion. - The testing skill's rule against the real model applied to all unit tests, which other modules contradict; it now covers the SPM country module, and points route storage, replay and cache cases at the canonical SPM route tests. - The older stand-in tests use the module's shared error, value and forecast-hash constants instead of repeating them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #4136 +/- ##
==========================================
- Coverage 88.13% 88.10% -0.03%
==========================================
Files 273 273
Lines 16299 16299
Branches 2668 2668
==========================================
- Hits 14365 14361 -4
- Misses 1268 1270 +2
- Partials 666 668 +2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
anth-volk
marked this pull request as ready for review
October 29, 2026 21:50
This was referenced Oct 30, 2026
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4135
Summary
tests/unit/test_country_spm.pyran 37 of its 63 tests against the installed policyengine-us model, through function-scoped fixtures that reloaded the 26.6 MB SPM forecast and built a new Flask app for every test. On #4039 the file took about 14 of theTestjob's 41 minutes.This PR makes the file a unit test file and keeps two checks against the installed model as integration tests.
tests/unit/test_country_spm.py). Every case now runs through the household routes and the realPolicyEngineCountry.prepare_calculation,_create_simulationandcalculatecode, over a stand-in for the country package'sSimulationclass. A stand-in raises the error the installed model raises for that household, with the model's code and message, or reports the model's receipt shape. The tests then check what the Public API does with it: status, code, message, null cells, receipt and caching. Nothing in the file usesCOUNTRIES["us"],load_forecast()or a realSimulation. The longest test takes 0.01 s.tests/integration/test_installed_us_model_spm.py). Two tests run the installed model. Session-scoped fixtures build the Flask app and its calculation service once.make testlists the integration file next totests/integration/test_budget_window_in_flight_dedupe.py, the one integration file it already ran. The file needs no database, network or credentials. So it still runs in the pull requestTestjob and inpush.yml's release tests, before the staging deploy, as the real-model checks did. A separate job would save no wall-clock time: on Report which parameters a policy sets that the current model no longer has #4039 the cross-database integration job took 29.8 minutes, longer than theTestjob will take without these tests./us/calculate-full. A stand-in test checks the route's own behaviour: it adds every omitted yearly variable, calculates them under the chosen measurement, and returns the receipt. No test calculates every variable of the real model any more.spm._current_bundle,spm._selected_forecastandspm.simulation_supports_spm, the same seamstests/unit/routes/test_canonical_spm.py,test_spm_settings.pyandtest_readiness.pyuse. No public route can replace the installed bundle.The two integration tests
coverage run --branchtest_a_chosen_county_yields_a_threshold_and_the_installed_forecasts_receipttest_a_state_only_household_is_rejected_only_when_it_chose_the_measurementdocs/canonical-spm.mdcommitment, end to end. Withoutspm, the household inherits the county default and gets HTTP 200 with the threshold null. Withspm: {"geography_kind": "county"}, the model's missing-county error becomes a 400 withSPM_GEOGRAPHY_REQUIREDand its own text.Times are each test's call time on an Apple-silicon Mac. Session setup takes 0.01 s: importing the country module already loads the US model, and the US metadata it builds loads the Public API's cached copy of the forecast. Process start-up, mostly importing the country models, takes 17 s without coverage and 87 s under coverage;
make testpays that once for the whole run.The second test checks both halves of the commitment in one test. The inherited half alone cannot show that the model raises a typed SPM input error: the country leaves any failed cell null when nothing was chosen, typed or not. The chosen half shows the typed code.
Where each real-model check went
test_real_country_state_only_tax_succeeds_and_receipt_stays_empty(1),test_real_http_tax_only_succeeds_without_geography(1),test_real_http_tax_only_outside_artifact_years_is_lazy(4)test_http_tax_only_result_carries_a_receipt_that_measured_no_yeartest_real_country_state_only_spm_dependency_requires_geography(2),test_real_http_spm_errors_use_validation_response(5),test_real_country_unknown_county_is_structured(2),test_real_country_unclassified_composition_is_structured(1),test_real_http_unsupported_spm_year_is_structured_only_when_calculated(8)test_http_reports_a_chosen_measurements_missing_input_as_a_400, one case per household (no county, county not a FIPS code, county the forecast cannot place, no classified adult, year after the forecast), with and without axes. Integration: the chosen half of the state-only test.test_real_country_state_only_dependency_is_null_when_nothing_was_chosen(2),test_real_http_missing_primitives_are_null_when_nothing_was_chosen(4)test_http_returns_an_inherited_measurements_dependants_as_null, the same households withoutspm, with and without axes. Integration: the inherited half of the state-only test.test_real_country_unknown_area_is_structured(1)test_http_reports_an_area_the_forecast_lacks_before_any_simulationtest_real_country_explicit_geography_yields_json_receipt(3)test_http_explicit_geography_returns_the_threshold_and_its_receipt(national, county, metro), andtest_calculate_leaves_the_callers_household_unchangedfor its check that the country does not write into the caller's household. Integration: the chosen-county test.test_real_http_national_result_and_receipt_survive_response_cache(1)tests/unit/routes/test_canonical_spm.py::test_http_cache_varies_with_measurement_settingsasserts the identical cached national response, receipt included, after one country call.test_real_http_full_national_calculation_keeps_threshold_and_provenance(1)test_http_full_calculation_adds_omitted_variables_under_the_chosen_measurementtest_real_http_stored_national_replay_preserves_threshold_and_cached_receipt(1)test_http_stored_household_replays_its_saved_selection_once_then_from_the_cacheWhich code the installed model raises for each missing input is policyengine-us's to test. The unit stand-ins use the messages the installed model raised for these households on this branch's lockfile.
Review
PolicyEngineCountry.calculateleaves the caller's household unchanged had been dropped, and the HTTP tests cannot see it, because the calculation service copies the household first. It is restored astest_calculate_leaves_the_callers_household_unchanged. I confirmed that test fails when the copy inprepare_calculationis removed. Code-quality findings, all fixed:_create_simulation. They now stand in for the package'sSimulation, so the second SPM normalization and the constructor arguments run.certified_bundleandhttp_client_forrepeat the shape ofcertifiedandharnessintests/unit/routes/test_canonical_spm.py. Sharing them would change that module's fixtures, which is outside this PR.Testing
Run only the tests this PR adds or modifies, each selected by its node ID:
tests/unit/test_country_spm.py: everytest_http_*case (27),test_missing_input_cases_cover_every_spm_input_error_code,test_calculate_leaves_the_callers_household_unchanged,test_country_never_swallows_spm_input_errors_for_a_chosen_measurement(8),test_inherited_measurement_leaves_dependent_variables_unavailable(8),test_a_configuration_failure_is_never_left_as_a_null_cell(6),test_tax_only_result_reads_provenance_without_calculating_spmandtest_an_inherited_measurement_still_returns_the_rest_of_the_calculation, by node ID withuv run --frozen --extra dev python -m pytest: all pass, none over 0.01 s.test_country_passes_resolved_spm_and_uses_the_simulations_private_systemandtest_uncertifiable_country_receipt_does_not_become_an_internal_failureare unchanged and were not run.tests/integration/test_installed_us_model_spm.py: both tests by node ID, with and withoutcoverage run --branch: both pass.ruff format --check .,ruff check .,uv run --frozen --extra dev mypyandpython scripts/run_quality_guards.py: all pass.No full test suite, whole test file or
make testwas run.🤖 Generated with Claude Code