Skip to content

Calculate stored households through /v2/household-calculations - #4274

Merged
anth-volk merged 60 commits into
masterfrom
v2plan/c1-3-household-calculations
Nov 11, 2026
Merged

anth-volk merged 60 commits into
masterfrom
v2plan/c1-3-household-calculations

Conversation

@anth-volk

@anth-volk anth-volk commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #4273

Summary

Adds POST /v2/household-calculations and GET /v2/household-calculations/{calculation_id}, which calculate a household stored in the Public API through the simulation service's household jobs. Replaces #4092, whose migration chain, reads through the simulation service and module layout no longer fit master.

POST needs no account. In order, it:

  1. refuses a country without household_enabled (422);
  2. answers 404 for a household, or a named policy, not stored in that country;
  3. refuses a household with no default_year (422);
  4. refuses a policy that sets parameters the running catalog version no longer has, with 422 code: "missing_parameters" and the list in missing_parameters, calling nothing (the check is require_policy_parameters_present, reusable by the run routes);
  5. submits the household, the policy, add_missing_variables, optional axes and optional spm settings to the simulation service.

The simulation service's answers map as follows:

Simulation service POST answers
202 {job_id, status}, for any of its four statuses 202 with calculation_id, status, country_id, household_id, policy_id and year
422 422, with its reasons passed through
400, with its reason 503: "The simulation service refused the calculation (): "
401, 403, 404, 405, 413 or 429 (answered before its route runs), its own 503 naming its code, a connection never made, or a gateway token that can't be fetched 503: "The simulation service is unavailable ( )." No job of the submission will run.
A 500, an unreadable 202, or any other status (such as 200, a redirect, 408, 409 or 501) 502: "The simulation service may have created the household job, but it failed ( ). Submit the household calculation again; if the first request went through, this creates a second job."
A lost answer (read timeout, dropped connection), or a proxy's 502, 504, or 503 without the simulation service's code 502: "The simulation service may have received the request, but its answer was lost (<transport error, or a proxy answered >). Submit the household calculation again; if the first request went through, this creates a second job."

No route finds a household job without its ID, so a 502 asks the caller to submit again rather than check first. The simulation service's own text may hold stored data, and the route needs no account, so only a 400 or 422 passes its reasons on; every other message gives only its status and a code that is a fixed identifier (lower-case letters, digits and underscores, starting with a letter, at most 64 characters).

Every answer given after the simulation service was called carries X-PolicyEngine-Observability-Id with the ID of that attempt. Refusals before it carry none.

GET reads household_jobs through the v2 layers, as the runtime role, and never calls the simulation service. It answers:

  • the status, the IDs, the year, the catalog version and both package versions, and the timestamps;
  • on success, the calculated household in its stored key order, warnings, spm_config and spm_provenance, from the simulation service's single-policy result document (contract version 1);
  • on failure, code, message, input_errors and spm_error from the failure document, or the job's error_message when it has none;
  • 404 for an unknown ID, 500 for a succeeded job whose document can't be read, and 503 when storage is down.

SPM availability in the catalog. A new nullable JSON column, tax_benefit_model_versions.spm_availability (autogenerated revision 0c136a278f29 on 970be5d29283), is recorded at catalog publication from the simulation service's GET /versions. /v2/model-catalog serves it as spm: available: true or false once recorded, null while unrecorded. When /versions can't be read, publication still succeeds, and its evidence lists the versions still unrecorded.

Also:

  • the route group in the migration registry, request logging and the regenerated migration contract documents;
  • live staging checks, with a US and UK parity set compared with /calculate and /calculate-full, under a 30-minute job limit;
  • SIMULATION_ENTRYPOINT_URL added to the publication step.

The call to the simulation service sits behind the HouseholdCalculationSubmitter protocol. The adapter, policyengine_api/libs/simulation_household_calculations.py, reuses SimulationEntrypointClient's HTTP client, so the gateway credentials and the request and observability ID headers come from there. Wiring the released generated client is one change, once PolicyEngine/policyengine-sim-api#1046 merges and the simulation service publishes a client release with the household routes: add policyengine-api-simulation-client>=<that release> and relock, then replace post_household_calculation's body with the generated submit_household_calculation.sync_detailed(...), as its docstring shows.

The request fields and the answers the adapter reads were checked against PolicyEngine/policyengine-sim-api#1046 at 5fe2a584: the request (country, household_id, policy_id, add_missing_variables, and axes and spm only when given), the 202 {job_id, status}, the 422 field list, the 400 and 503 {code, detail} (the adapter keeps the 400's reason, and of a 503 or 500 only its code, ignoring a 503's household_job_id), and the JSON 500 internal_error. When the simulation service cannot tell whether a job's call started, it now answers 202 pending rather than 503, which the adapter already reads as an accepted job.

Merge order

Merge only after the simulation service's /household-calculations route, PolicyEngine/policyengine-sim-api#1046, is deployed to staging. Until then, the live staging checks fail, and with them every Public API staging promotion and release.

Tests

  • Each test was run individually, by node ID, and seen failing for the expected reason before its change: 124 node IDs across the build and three review rounds, 191 passed and 9 skipped (the live tests, which need API_BASE_URL).
  • Route and service tests run through the real country, household and policy services on SQLite, including the catalog failure path.
  • Adapter tests use the real HTTP client over a mock transport, covering each class of transport error and the ID headers.
  • Integration tests on PostgreSQL 17 read a job as the runtime role and record SPM availability as the catalog writer.
  • alembic -c alembic-v2.ini check: no new operations; one head, 0c136a278f29.
  • lint-imports, the rules registry, the quality guards, the migration contract export, ruff, ruff format, shellcheck, uv lock --check and mypy (including the changed data/v2 files) pass.
  • Review: three rounds and a follow-up of the fixes. Round 1 found two bugs: a catalog failure answered 500 instead of 503, and the live checks' merge order, now handled by merge order. Round 3 found one: a lost answer was answered 503, inviting a resubmission. All are fixed test-first, and the follow-up's findings (a stray warning, a proxy's 502 or 504, docs) are fixed.
  • The 502 messages were reworded to ask the caller to submit again, test-first: 25 test cases across the adapter's four 502 cases and the OpenAPI description, seen failing on the old wording, then passing. One more review round found no bugs; its two docstring findings are fixed.
  • After merging Send the CI server credential from push.yml's deployed-API tests #4265, which makes the deployed-API tests send the CI server credential, the parallel live check's own clients send it too. They come from deployed_api_client in tests/integration/deployed_api_credentials.py, which the api_client fixture now builds its client with as well, so both send the same credential and settings. Unit tests were seen failing first; two review rounds of this change found no bugs.
  • Answers the simulation service may have given after creating the job (any status not listed above, and a 503 without its code) answer 502, and only a 400 or 422 passes on its text, test-first: the new cases were seen failing, then passing. Two review rounds found three bugs, all fixed test-first: a front-end 503 answered as "no job", the OpenAPI 503 still promising the simulation service's reason, and a deeply nested body answered 500 (each answer is now parsed once, by pydantic's parser).

Environment variables and secrets

Value Provider How it reaches the code
SIMULATION_ENTRYPOINT_URL GitHub environment secret in staging and production Runtime: the deploy job's env:, then deploy_cloud_run_candidate.sh, then the Cloud Run revision. Publication: the publish step's env: in seed-v2-database.yml (new), whose job declares environment: and is called with secrets: inherit.
GATEWAY_AUTH_ISSUER, GATEWAY_AUTH_AUDIENCE, GATEWAY_AUTH_CLIENT_ID, GATEWAY_AUTH_CLIENT_SECRET_RESOURCE Organization secrets The deploy step's env:, then the deploy script with GATEWAY_AUTH_REQUIRED=1. Unchanged.
V2_RUNTIME_DATABASE_URL_SECRET_RESOURCE, V2_DATA_WRITE_DATABASE_URL Environment secrets Unchanged wiring.
API_BASE_URL, STAGING_API_TEST_PROBE_ID The deploy job's outputs The live test step. STAGING_HOUSEHOLD_* and the poll interval are optional and use their defaults.
CI_SERVER_ACCESS_TOKEN Requested in the live staging job by request_ci_server_token.sh (from #4265) from the staging environment's CI_SERVER_AUTH0_DOMAIN, CI_SERVER_AUTH0_CLIENT_ID and CI_SERVER_AUTH0_AUDIENCE variables and CI_SERVER_AUTH0_CLIENT_SECRET secret The live test step's env:. The household checks send it through the api_client fixture, and the parallel check through deployed_api_client, one client per thread.

Not verified:

  • that each environment's SIMULATION_ENTRYPOINT_URL names its own simulation service;
  • that the runtime can read the gateway client secret from Secret Manager;
  • that the Auth0 client holds the simulation service's simulation:submit permission;
  • that GET /versions describes the v2 household functions' SPM support.

🤖 Generated with Claude Code

anth-volk and others added 30 commits November 10, 2026 23:01
…cks (failing)

The request model's axis limits, the country, default year and
missing-parameter checks come before anything is sent to the simulation
service. The modules they test do not exist yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HouseholdCalculationInput takes a stored household and an optional stored
policy, with the simulation service's axis limits, add_missing_variables
and optional SPM settings. The validators refuse a country without
household calculations and a household without a default year, and the
policy validators refuse a policy that sets parameters the running catalog
version lacks, a check later calculation routes can share.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A job's status, result document and error are read into a calculation: a
succeeded job's household in its stored key order, with its warnings and
SPM choice and receipt; a refused job's error; a job that failed
unexpectedly by its error message. The integration tests read the jobs as
the runtime role, through the service. The modules do not exist yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET's read path: a read connector loads one household job by ID, a
database session module opens the read sessions, a validator reads the
job's result document as its status says, and a transformation builds the
calculation from the job. A succeeded job must hold one policy result,
which keeps the key order the json column stored; a refused job gives the
household function's error, and one that failed without a failure document
its error message. Database failures become a typed storage error, so the
routes need not import SQLAlchemy. The cross-database integration check
runs the read as the runtime role.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… (failing)

The service checks the country's household_enabled, that the household and
policy exist in that country, the household's default year and the
policy's missing parameters, in that order, and calls the simulation
service only when every check passes. The submission method does not
exist yet. The last test, of a database failure while reading, checks the
read path committed before and passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
submit_calculation checks the country, the household and policy, the
household's default year and the policy's missing parameters, then sends
the calculation to the simulation service through the
HouseholdCalculationSubmitter protocol, and returns the job the service
created with the year it calculates. require_default_year now returns the
year it checked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ling)

The client posts a calculation's IDs and options to the Cloud Run
simulation service's POST /household-calculations, reads the 202's job ID
and status, passes the service's 422 reasons through, and treats every
other answer, no answer, an unreadable answer, a gateway token it cannot
fetch and missing configuration as the service being unavailable. The
module does not exist yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ocol

SimulationHouseholdCalculationClient implements the service's
HouseholdCalculationSubmitter. It posts to the Cloud Run simulation
service's POST /household-calculations through the HTTP client
SimulationEntrypointClient builds, so it shares its gateway credentials,
correlation headers, transport and instrumentation. A 202's job ID and
status are read with hand-written models that ignore fields they don't
name; a 422's reasons reach the caller; every other answer is the service
being unavailable. post_household_calculation is the only code that sends
the request, so wiring the generated simulation client changes that one
function and adds the dependency.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ailing)

POST answers 202 with the new job, needs no account, and answers each
refusal: 422 for a malformed request, a country without household
calculations, a household without a default year and the simulation
service's 422 reasons; 422 missing_parameters naming the parameters; 404
for a household or policy not stored in that country; 503 when the
simulation service or v2 storage is unavailable. GET reads the job and
answers 404, 500 for an unreadable result and 503. The OpenAPI test lists
the two new paths. The routes do not exist yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST takes a stored household, an optional stored policy, axes,
add_missing_variables and SPM settings, needs no account, and answers 202
with the new job's ID and status, the household and policy, and the year
calculated. A policy setting parameters the running catalog version lacks
gets 422 with code missing_parameters and their names, in a new error
model in errors_support that later calculation routes can share. GET reads
the job from household_jobs and answers its status, result and error.
The service is built by a default factory that imports the simulation
client only on first use, and the new modules join ruff's list of
modules loaded only inside functions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…failing)

/v2/household-calculations belongs to the household route group. Its
request log records what it uses whatever the household flags select: a
submission reads Supabase and computes on the Cloud Run simulation
service's v2 household functions, a read reads Supabase only. The contract
registry and the generated contract documents gain the two routes. None
of this exists yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tracts

/v2/household-calculations joins the household route group. Its request
log records the sources it uses, not the household group's database,
entrypoint and compute flags: both routes read Supabase, and a submission
computes on the Cloud Run simulation service's v2 household functions.
The contract registry gains the two routes, and the generated contract
documents are regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The simulation service's household contract names exactly one policy, so
a succeeded job's HouseholdExecutionResult holds that policy's household,
warnings and SPM receipt in its output, beside the SPM selection applied,
instead of a list of policy results. The failure document keeps its
shape. The reader still expects the list, so these tests fail with an
unreadable result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stored document models now read the household, warnings and SPM
receipt from the output of a HouseholdExecutionResult that names one
policy. The two document models convert themselves into the calculation's
output and failure, so they are the only code that names the simulation
contract's fields, and a change to those names stays in one place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Catalog publication records, for each US catalog version, whether the
simulation service's public GET /versions lists a certified SPM capability
for its PolicyEngine.py version, with the settings schema and defaults,
in a new nullable tax_benefit_model_versions.spm_availability column added
by a revision on the saved-item keys revision. /v2/model-catalog serves it
as spm. Publication needs SIMULATION_ENTRYPOINT_URL, which the seeding
workflow passes, and the catalog writer role records the column. None of
this exists yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tax_benefit_model_versions gains the nullable JSON column
spm_availability, in revision 0c136a278f29, autogenerated on the
saved-item keys revision 81b728a088e7, which is now
V2_ALEMBIC_HEAD_REVISION. After publishing the catalog, publication
records for each US version of the installed PolicyEngine.py whether the
simulation service's public GET /versions lists a certified SPM capability
for it, with the settings schema and defaults; a version it does not serve
yet, or an unreadable answer, stays unrecorded for the next run.
/v2/model-catalog serves it as spm. Publication needs
SIMULATION_ENTRYPOINT_URL, which the seeding workflow now passes from the
environment's secret. The documents that list what changes on a version
after publication name the new column, including its effect on each
process's kept responses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The live checks store households and reforms through /v2/households and
/v2/policies, submit them to /v2/household-calculations and read each job
until it finishes: a submit and read cycle, six calculations submitted at
once within their latency limits, a household across 401 axis points, and
a parity set of three US and three UK households compared value by value
with the in-process /calculate and /calculate-full. A unit test checks the
parity households that add variables calculate one year on both sides.
The workflow test expects the household checks to run after the other
live tests with fewer workers, and the job's limit to be 30 minutes, which
the script and workflow don't do yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
run_live_staging_tests.sh runs the household calculation and parity
checks in a second pytest run, on 4 workers, once the other live tests
finish, so the six submissions sent at once never share the candidate's
six request slots with them. The live job's limit rises from 20 to 30
minutes; it has taken about 10 minutes before this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Stage 11 household deployment notes describe both routes: the checks
before submission and their answers, how the simulation service's answers
are mapped, how GET reads household_jobs, where the simulation call is
made, and the live staging checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…docs

The simulation contract's household functions record a job that raised or
was cancelled with a null result and an error message starting
household_function_error or household_function_cancelled. The error
model's documentation names them, and the tests use that format. Nothing
else changes: the single-policy result and the failure document already
match the contract.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…4261

Master gained the user types revisions 617f531f1506 and 970be5d29283 on
the saved-item keys revision, the parent of the SPM availability revision
0c136a278f29. Its down_revision moves to 970be5d29283, which
V2_ALEMBIC_HEAD_REVISION's 0c136a278f29 now follows; generating it again
on 970be5d29283 gives the same two operations. The revision chain test
and the Alembic skill list both master's revisions and this one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The route runs with the real countries, households and policies services
over the v2 tables in SQLite, so the policy check runs the real catalog
query. A submission, a missing-parameter refusal and an unknown household
pass. When the catalog query fails, the policy service raises
MetadataCatalogUnavailableError, which the route answers 500 instead of
503, as /v2/policies answers it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The policy service reports a catalog it cannot query as
MetadataCatalogUnavailableError. The household calculation service now
turns it, like a SQLAlchemy error, into its storage error, which the route
answers 503, as /v2/policies does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…02 (failing)

Through the route and the real simulation client, the submission carries
the request ID and an observability ID: the caller's, or a new one. Sim's
400 and 503 answers name their code, and a 503 after Modal refused the
call names the household job it failed. A 202 whose body cannot be read
means sim has already created the job, so it is its own error, which the
route answers 502 rather than the 503 that invites a retry. The
submission sends no observability ID yet, and the error does not exist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r an unreadable 202

The submission binds the request's observability ID (the caller's, or a
new one) before it calls the simulation service, so the shared request
hook sends it with the request ID, and sim passes it to the household
function. A 400 or 503 from sim gives its code and reason in the 503's
message, and a 503 after Modal refused the call names the job sim failed.
A 202 whose body cannot be read raises
SimulationServiceAnswerUnreadableError, answered 502: the job already
exists, so the caller is not invited to submit again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A submission should be sent again only after a 503 or a failed
connection, never after a 502, which now means the simulation service
created the job; a read may be sent again after a 502, 503 or 504 or a
failed connection. The live fixture sends a submission again after any of
502, 503 and 504, and never after a failed connection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…onnection

The live fixture now sends a submission again only after a 503 or a
failed connection, never after a 502, which means the simulation service
created the job, and sends a read again after a 502, 503 or 504 or a
failed connection. The live script and modules say the checks pass only
once the simulation service's household calculation route is on staging,
so the change that adds them merges only after that deployment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Publication's evidence should list the versions it recorded SPM
availability for and those it left unknown, with the reason when the
simulation service's versions couldn't be read, which the command also
prints to stderr. /v2/model-catalog should serve a version without a
recorded availability as available null, unknown, apart from a recorded
absence, available false. Publication now reports nothing, and the
catalog serves both as false.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nown

record_published_spm_availability returns what it recorded and which
versions it didn't, with the reason when the simulation service's
versions couldn't be read. Initialization adds that to the publication
evidence, which the command prints, and the command also prints the
reason to stderr. /v2/model-catalog serves a version without a recorded
availability as available null, unknown, apart from a recorded absence,
available false. The Stage 9 runbook's publication step names
SIMULATION_ENTRYPOINT_URL, which publication now needs, and says how a
failed versions read is reported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A succeeded job's document whose schema_version is missing or isn't 1
should be unreadable, and a failure document of another version should
be read as none, so the job's error message says why it failed. The
document models ignore schema_version, so both are read as version 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk and others added 2 commits November 11, 2026 02:19
…2s and 504s with 502

- Publication logs that SPM availability was not recorded only when some
  version is still unrecorded, after reading which are.
- A 502 or 504 from sim, which only a proxy in front of it answers, now
  raises SimulationServiceAnswerUnreadableError (502): the job may exist.
- The OpenAPI 502 description and the Stage 11 notes list the lost-answer
  and proxy cases, the Stage 11 notes no longer list "no answer" as 503,
  and the header text says it carries the attempt's ID, which sim never
  received after a connection never made or a token failure.
- The warning test keeps the module's logger enabled, since Alembic's
  logging setup, which another test may run in the same process,
  disables loggers that existed before it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.61017% with 22 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.66%. Comparing base (8b906f8) to head (3dc1f8b).
⚠️ Report is 61 commits behind head on master.

Files with missing lines Patch % Lines
...licyengine_api/data/v2/catalog/spm_availability.py 89.65% 4 Missing and 5 partials ⚠️
policyengine_api/fastapi_routes/dependencies.py 28.57% 5 Missing ⚠️
...gine_api/libs/simulation_household_calculations.py 97.20% 1 Missing and 3 partials ⚠️
...ne_api/services/v2/household_calculations/types.py 97.65% 1 Missing and 2 partials ⚠️
...icyengine_api/data/v2/catalog/publication_types.py 87.50% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4274      +/-   ##
==========================================
+ Coverage   92.51%   92.66%   +0.15%     
==========================================
  Files         318      329      +11     
  Lines       18137    18783     +646     
  Branches     3002     3114     +112     
==========================================
+ Hits        16779    17405     +626     
- Misses        814      824      +10     
- Partials      544      554      +10     
Flag Coverage Δ
integration 53.77% <57.16%> (+0.12%) ⬆️
unit 90.42% <94.45%> (+0.16%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

anth-volk and others added 3 commits November 11, 2026 19:27
…on again (failing)

The Public API won't choose household job IDs, and no route finds a job
without its ID, so a 502 can't ask the caller to check whether the job
exists. The tests expect each 502 to ask for the calculation to be
submitted again and to say this creates a second job if the first
request went through, keeping each case's detail: the transport error,
the proxy's status, sim's 500 code and reason, or the unreadable 202.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No route finds a household job without its ID, so the 502 can no longer
ask the caller to check whether the job exists. After an unreadable 202,
sim's 500, an answer lost after sending, or a proxy's 502 or 504, the
message now reads "The simulation service may have received the request,
but its answer was lost (<what happened>). Submit the household
calculation again; if the first request went through, this creates a
second job." The parentheses keep each case's detail: the transport
error, the proxy's status, sim's 500 code and reason, or the unreadable
202. The OpenAPI 502 description, the error types' docstrings and the
Stage 11 notes say the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 503s

Both submit docstrings now list a proxy's 502 or 504 with the unreadable
202 and the lost answer, and the protocol's docstring no longer leaves
those to "any other refusal or failure". The 500 error's docstring and
the route's comment say directly that a 502 is used because the job may
already exist, so submitting again may create a second one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk and others added 6 commits November 11, 2026 20:15
…edential (failing)

Master's deployed-API tests now send the CI server credential through
the api_client fixture, since the Public API refuses requests without it
once PUBLIC_API_SERVER_CREDENTIAL_REQUIRED=1. The parallel household
check opens a client per calculation, so the tests expect a client of
the deployed Public API that sends CI_SERVER_ACCESS_TOKEN, and that
fails without it rather than skipping.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The parallel household check opens a client per calculation, outside
the api_client fixture that now sends the CI server credential. A new
deployed_api_client in the live fixture builds the same client: the
credential from CI_SERVER_ACCESS_TOKEN in Authorization, failing rather
than skipping without it, the same timeout and redirects. The parallel
check uses it, so it keeps passing once the Public API requires a server
credential.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…re builds it (failing)

The review found that deployed_api_client copied the api_client
fixture's settings. The tests now expect the builder in
tests/integration/deployed_api_credentials.py, which the fixture will
use too, so both clients are built the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s in one place

deployed_api_client moves to tests/integration/deployed_api_credentials.py,
and the api_client fixture builds its client with it, so a check that
needs clients of its own sends exactly what the fixture sends. The live
fixture's docstring no longer says it needs only httpx, since the live
job installs the locked dev environment, and the testing skill names
deployed_api_client for checks that need their own clients.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…irect rule once

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk and others added 6 commits November 11, 2026 20:36
…ext stays out (failing)

Sim may create the household job before answering a status the Public
API doesn't otherwise handle (200, a redirect, 408, 409, 501 and the
like), so the tests expect those to answer 502: "The simulation service
may have created the household job, but it failed (<status> <code>).",
as an unreadable 202 and sim's 500 do. Only answers sim gives before its
route creates a job (401, 403, 404, 405, 413, 429 and 503), its 400, a
connection never made and a gateway token that can't be fetched stay
503. Sim's 500 and 503 text may hold stored data, so only the status and
a code that is a fixed identifier reach the caller; the 400 and 422
refusals keep their reasons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the caller

Sim may create the household job before answering a status the Public
API doesn't otherwise handle, so any status other than 202, 400, 422,
500, a proxy's 502 or 504, and the ones sim answers before its route
runs (401, 403, 404, 405, 413, 429 and 503) now answers 502: "The
simulation service may have created the household job, but it failed
(<status> <code>). Submit the household calculation again; if the first
request went through, this creates a second job." An unreadable 202 and
sim's 500 use the same first sentence; lost answers and a proxy's 502 or
504 keep "its answer was lost".

Sim's text may hold stored data and the route needs no account, so after
its 500 or 503, or an unexpected status, the message gives only the
status and a code that is a fixed identifier (lower-case letters, digits
and underscores, starting with a letter, at most 64 characters); the
failed job's ID is no longer named either. The 400 and 422 refusals keep
their reasons. The answer reading moves out of submit into one function.
The OpenAPI 502 description, the error docstrings and the Stage 11 notes
describe the new classes of answer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ead apart from reasons (failing)

Sim's own 503 always names its code; without one, Cloud Run's front end
answered in its place, possibly after the job was created, so the tests
expect 502 "its answer was lost (a proxy answered 503)". A code that
isn't text no longer makes a 400's reason or a 422's field list
unreadable, a 400 without a textual reason gives its status and code,
the submit route's OpenAPI 503 says only a 400 keeps sim's reason, and
the tests cover a 403 with FastAPI's body, statuses outside 200 to 599,
and the exact unreadable-422 message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…om reasons

Sim's own 503 always names its code. A 503 without one came from Cloud
Run's front end, possibly after sim created the job, so it now answers
502 "its answer was lost (a proxy answered 503)", like a proxy's 502 or
504. The code is read from the answer's JSON on its own, so a code that
isn't text no longer makes a 400's reason or a 422's field list
unreadable; a 400 without a textual reason gives its status and code.

The submit route's OpenAPI 503 no longer promises sim's reason: only a
400 keeps it. The docs and names now say that no job of the submission
runs after a 503-class answer, rather than that none was created, since
sim fails the job it created when Modal refuses the call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… as a 500 (failing)

The standard-library parser that reads sim's code raises RecursionError
on a deeply nested body, which the route answers 500 without the
"submit again" message. The tests expect such a body, a top-level JSON
value that isn't an object and bytes that aren't UTF-8 to read as no
body, and the OpenAPI 503 to list sim's not_configured 503.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…3 into a 500

The code was read with the standard-library parser, which raises
RecursionError on a deeply nested body; the route answered that 500,
without the "submit again" message. Each answer other than 202 is now
parsed once, by pydantic's parser, which reports its depth limit and
bytes that aren't UTF-8 as a ValidationError, into a record of its
status, its fixed-identifier code and its refusal, from which every
message is built. The OpenAPI 503, the error docstring and the Stage 11
notes list sim's not_configured 503, and the constant of statuses after
which no job runs says it holds only sim's own coded 503.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch was previously deployed

1 inactive deployment
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Calculate stored households through /v2/household-calculations on the simulation service's household jobs

1 participant