You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
test+ci: bulletproof the port's CI gate and add porting test-quality guardrails
This repo is an LLM-generated port re-synced by scripts/upstream, so CI is the
only mechanical thing between a generated diff and main. It was thinner than it
looked, in ways that all shared one failure mode: the suite could stay green
while the port drifted from upstream behavior.
Measured before this change:
- Upstream had 46 test files at the ported commit (680bceb); this port had 15,
all passing. Three upstream invariants had no counterpart at all.
- 11 of 14 test files hand-copied their own fake client (6 byte-identical).
Those stubs populated only `id` and `output`, omitting `status`, `model`,
`created_at`, `tool_choice` — so a stub was strictly more forgiving than the
real API.
- `mypy` ran on `src` only: 41 errors in tests, mostly unchecked Optional
derefs, which is exactly where an assertion silently stops asserting.
- CI tested only Python 3.11 despite requires-python = ">=3.9.2".
- `verify-port` was advisory on a stale premise. Its comment claimed the
required-API check "fails by design until the first sync lands"; the verifier
actually passes 31/31 symbols with 0 failures.
- No concurrency group, no lockfile-drift gate, no packaging check.
CI (.github/workflows/ci.yaml)
- `check` becomes a 3.9/3.11/3.13 matrix with fail-fast: false, so a
3.9-specific break cannot be masked by a passing 3.13 leg. Note that "3.9.2"
is not pinnable: actions/python-versions ships no 3.9.2 build for
ubuntu-24.04, so the matrix uses "3.9" (resolves to 3.9.25).
- New `types` job runs `mypy src tests` plus `uv lock --check`. Not matrixed —
[tool.mypy] python_version pins the analysis target, so output is identical
on every interpreter.
- New `build` job builds on 3.9 and imports the public API from the built wheel
with `--isolated --no-project`, proving the artifact rather than the repo.
- `verify-port` is now blocking, with the stale comment corrected.
- Added concurrency (PR-only cancellation; pushes to main are never cancelled),
and `--frozen` on every sync so lockfile drift fails loudly.
- `e2e` stays non-required on purpose: it exits 0 without the secret, so
requiring it would be a green rubber stamp on forks.
Tests
- New tests/_fixtures.py: `make_response` populates every field
OpenResponsesResult requires, so a stub can no longer be more permissive than
production. `assert_matches_sdk_response_shape` validates the builders against
the generated SDK model, so a required-field change there fails loudly instead
of drifting. Builders keep upstream's camelCase wire shape because that is
what the port's internals consume.
- Migrated 7 files off duplicated stubs (~274 net lines removed). Bespoke stubs
that QueuedClient genuinely cannot express (error injection, SSE sequences)
are kept but now build payloads from the shared builders.
- Three new files close the HIGH-severity gaps, each porting upstream's
invariant rather than its syntax:
test_turn_end_race_condition.py — turn.end is never silently dropped
test_tool_execution_once.py — a tool runs exactly once, zero when denied
test_mixed_manual_tool_round.py — no orphaned function_call in a follow-up
- Strengthened assertions that looked like coverage and were not: the
`"turn.end" in [...]` membership checks became count + ordering assertions
(membership passes even when turn.end fires twice or out of order), and the
vacuous `assert x is None if k in d else True` — which is `assert True` on the
missing branch — now actually can fail.
- mypy on tests: fixed the real classes (Optional derefs, lambdas returning
None). The `tool()`-return-type friction is suppressed narrowly for tests.*
because fixing tool.py is ported source the next sync regenerates.
104 -> 114 tests; coverage 81% -> 83.89% behind an 83% ratchet floor.
Porting guardrails (the durable half)
A code-only fix gets re-broken on the next sync, so the rules live in the
contract:
- .upstreamer/upstreamer.md gains a Test Parity section: 1:1 upstream test file
mapping, the rejectable assertion patterns, use the shared fixtures, coverage
is a ratchet, comment deliberate divergences at the assertion.
- .upstreamer/eval.md gains a test-quality dimension, plus a command to diff the
two suites by file so a gap is visible rather than inferred.
- New .upstreamer/skills/port-test-quality/ carries the procedure, wired into
the converter skill's Step 4.
- verify.sh now reports unported upstream test files (advisory — severity is the
eval's judgment), type-checks tests, and enforces the coverage floor.
Notes
- The three 0%-coverage modules are kept, not deleted: all three exist upstream,
and the contract mandates one Python module per upstream lib module, so
deleting them would be a parity regression the next sync re-creates. They are
documented and excluded from the floor instead.
- One deliberate divergence: outgoing function_call_output uses snake_case
`call_id`, not upstream's `callId`, because _send normalizes at the transport
boundary. Commented at the assertion so it does not get "fixed" back.
- .upstreamer/state.yaml and eval-report.md are untouched, and the `openrouter`
substrate pin is unchanged.
Follow-ups, deliberately not in this PR: ~33 upstream test files still have no
Python counterpart (verify.sh now lists them); and two e2e tests are flaky
because they depend on the model volunteering a tool call — adding retries to a
paid API call did not belong here.
Co-Authored-By: Claude <noreply@anthropic.com>
0 commit comments