| M1-71/0 |
2026-10-16 |
Base int/m1-wave28 at c83c68a, branch feat/M1-71-contract-tests in its own worktree; no local merge, no push, the integration lane lands it. Brief, spec and contract were read from origin/main; the base holds the rules the ticket needs in its chapters (06 "Contract tests and gate" G7 and G8, 19 SEC-IN-1, SEC-IN-2 and SEC-T06, 23 R-QA-11, PLAN W7), so no contract commit was brought in. |
Instructions of the launcher, which override the brief's merge step. |
| M1-71/1 |
2026-10-16 |
TestG8_ProductionBuildExcludesValidator checks that the untagged cmd/bothy does not depend on contractcheck and that the build with contract_validate does; github.com/pb33f/libopenapi-validator itself stays in the production build. |
The request validator of the chain (SEC-IN-1, M1-18) already uses the same library in production; only the response check is test-only. |
| M1-71/2 |
2026-10-16 |
A response that violates the contract in the test build writes its report to standard error and ends the process with exit code 3. |
A hard exit is the only mechanism that fails every integration test regardless of how it asserts, without touching each test; it exists only in the test build. |
| M1-71/3 |
2026-10-16 |
G8 runs on the request validator of the chain: Chain.Public hands its parsed contract and mounted operations to contractcheck.Attach, which checks only when that contract is byte for byte docs/api/openapi.yaml of the repository (SHA-256; the file is found by walking up, a missing file ends the process with exit code 3) and passes chains over test fixture contracts unchecked. It takes the operation from the route of the server (net/http patterns) and validates with the path item of that route, not with the path search of the library. A request that matches no mounted operation is not checked. |
A second parse of the contract per test binary cost more than a minute under -race and broke the readiness waits of the serve tests; api/rest.Spec equals docs/api/openapi.yaml (codegen-check). The library prefers a literal path of another method (GET /grants/preview against getGrant) and reported a missing operation for a request the server routes correctly. |
| M1-71/4 |
2026-10-16 |
The project check correlation_id requires a lower-case UUID (schema CorrelationId), not a UUIDv7 as the brief writes, and equality with correlation_id of a problem body. |
06 "Basic rules", row Correlation: the value is the trace ID written as a UUID, not a UUIDv7; the server could never pass a v7 check. |
| M1-71/5 |
2026-10-16 |
The credential broker answers kind: owner with a new device of the first owner through a recovery code of the setup service (bothy setup issue-enrollment-code) and kind: user with a new device of Alex of the matrix world (createEnrollmentCode by a fresh owner session), not with a new person. |
Brief design note 5 makes Alex the plain user of the second pass; a new person would see nothing of the world. A recovery code does not depend on any session, so a generated call that revokes the owner's sessions does not end the run. |
| M1-71/6 |
2026-10-16 |
harness.Start reserves a free port of 127.0.0.1 and binds the public listener to it, retrying a start whose port was taken in between; the internal listener stays on port 0. Client.Gateway is not built (the gateway of M1-64 is not in this base), and Options.Telemetry is refused until M1-69 adds its sinks. |
server.public_url must name the port before the listener exists, because every DPoP htu is built from it. |
| M1-71/7 |
2026-10-16 |
G7 runs the seven built-in checks the brief names (not_a_server_error, status_code_conformance, content_type_conformance, response_headers_conformance, response_schema_conformance, negative_data_rejection, ignored_auth) plus the four project checks; the seed is hypothesis.seed(CONTRACT_SEED) with derandomize off and no example database. Positive cases get a fresh UUIDv7 as Idempotency-Key. |
The listed checks define the scope; with derandomize on Hypothesis would ignore the seed. The schema Uuid allows any version, the server requires v7, so a generated key would turn every positive case into a 400. |
| M1-71/8 |
2026-10-16 |
The reproduction command calls PYTHONPATH=tools/contract uv run --project tools python -m dpop, not python -m contract.dpop; tools/contract/live_contract.py is left out of the tools coverage. make contract-test has the named time limit quality.contract.timeout. |
The modules under tools/ are top-level modules for pytest and mypy; the live module runs only against a server. Fixed requirement 8: a new gate value is a named value of chapter 23. |
| M1-71/10 |
2026-10-16 |
wire.WarmRequestValidator (exported) builds the request validator of the enabled operations once per process; harness.Main calls it before the tests, and harness.Start waits up to three minutes for readiness. |
The build takes tens of seconds under -race and host load; the serve tests of internal/wire warm it the same way (M1-31/18). |
| M1-71/11 |
2026-10-16 |
The stateful phase is test_stateful, which runs the state machine with only the generate phase and, like the Schemathesis engine, treats a Hypothesis Flaky (a replay that draws differently because the server kept the state of earlier scenarios) as the end of the phase unless a check failed before it, which it then reports. The CI job gates gets timeout-minutes: 240. |
The first full run at 100 examples failed only with FlakyStrategyDefinition while selecting a rule; G7 took about 40 minutes there, beside a make test of more than 30, so the former 60 minutes of the job cannot hold make gates with contract-test. |
| M1-71/9 |
2026-10-16 |
egress.Deps.DenyAll refuses every request before resolution; wire.Deps.EgressDenyAll passes it to the OIDC client, the webhook sender and the audit checkpoint sink. The response validator test of httpservertest moved to a route of its test contract. |
A resolver that fails would still let an IP literal through; a test with a deliberately wrong answer on a route of docs/api/openapi.yaml ends the binary under G8. |
| M1-71/12 |
2026-10-18 |
Port onto main: code and tests as in the ticket range (c83c68a..af5e86e) together with the integration fix INT-W32/1, except the points named here. WarmEnabledValidator in internal/wire/export_test.go keeps the body of main, which warms the frame validator of the gateway (M1-64) and the request validator; WarmRequestValidator stays as the ticket built it for the harness. M2-24 is not on main, so there is no webhook sender: wire.Deps.EgressDenyAll reaches the OIDC client and the audit checkpoint sink, and its comment names only those (M1-71/9 stays as approved). The harness and the matrix world use only operations main has, so M3-18, M3-13, M2-24, M2-26 and M6-01 of the base need no other change; Client.Gateway stays unbuilt although main serves the gateway (INT-W32/2, a later ticket). The message texts of the matrix world are English. make test adds -tags contract_validate to the -coverpkg=./... and -timeout=$(GO_TEST_TIMEOUT) of main, and the CI job gates keeps the runner of main with the 240 minutes of M1-71/11. The job ci-ok of main has no needs and polls the other jobs of the run (tools/ci/ci_ok.py), so its timeout-minutes rises from the 120 of PL/6 to 250, above the limit of gates plus queueing; gates goes from the 90 of PL/6 to 240; otherwise a green but long gates would leave the required check red. The exclusion of goconst and gocognit in services/server/.golangci.yml covers test/ as well as *_test.go, the rule M2-26 brought on the integration line, because the harness and the matrix world under test/ repeat literals as scenarios. The module docstrings of tools/contract/checks.py and tools/contract/dpop.py follow the ruff rules of main (D205, D301). |
The specification is English on main (D-W-94), and the tickets that the base held beyond main are ported later; each change keeps the behaviour of the approved ticket. |
| M1-71/13 |
2026-10-18 |
G7 drops a Host header in any spelling from every generated case before the call (auth.without_host); the transport sets the real one. Current state: superseded by PL/45, which drops Host, Content-Length, Transfer-Encoding and Expect (auth.without_transport_headers). |
The negative mode of Schemathesis adds arbitrary headers, sometimes Host with a value that is no host; net/http answers it with its own plain text 400 before any handler, which G7 reported as an undocumented content type (for example addGroupMember, reproduced with a fixed seed on the integration line, INT-W33/4). That answer says nothing about the contract. Test: test_without_host_drops_generated_host_headers. |
| M1-71/14 |
2026-10-20 |
Since ci.yml runs the gates in parallel jobs (#165), the contract tests G7 (make contract-test) run in the job test (server) after its tests, as in build-verify.yml; that job gets the 240 minutes that M1-71/11 gave the single gates job, and ci-ok 290 (plan 5, the longest test job 240, sonar 30, with margin). |
Every target of make gates runs in exactly one CI job (tools/ci/tests/test_ci_parallel_gates.py); G7 needs the server toolchain and Docker of that job. |
| M1-71/15 |
2026-10-20 |
Port onto main after M1-62, M1-63 and M1-64: the code and tests are the ones of M1-71/12 to /14, except that ci-ok keeps its 10 minutes: since #191 it is the last job of the run and needs every other job, so it no longer waits through the run and the 290 minutes of M1-71/14 do not apply; only test (server) gets the 240 minutes. The component map of #192 names tools/contract/ with the component server, because make contract-test runs its live module in the job test (server); its unit tests run in test (tools) like every tools test. |
The tickets the ticket depends on are on main; the job layout of main decides the limits. |
| M1-71/16 |
2026-10-21 |
TestMigrateStepHonoursLockTimeout (M1-12) builds the pool and the migrator before its clock starts and bounds the wait by testkit.Patience instead of five seconds; SQLSTATE 55P03 stays the proof that lock_timeout ended the wait. |
With the contract tests in make test the job test (server) is busier: the test failed twice on this pull request with 5.4 s, most of it the connection of the pool, which the old clock included. |