test(http): cover the /v1/turn rejection contract #240

Merged
coilysiren merged 1 commit from test/http-turn-contract into main 2026-08-13 03:37:49 +00:00
Member

Closes #193

handleHTTPTurn had no direct coverage. Its rejection contract was established only by probing the deployed pod, so every guarantee cost a tailnet round trip against production and none of them were protected against regression.

What this adds

internal/community/http_test.go — 12 tests over the turn surface, all httptest, no model, no Agent Proxy, no MCP. Full package suite runs in ~1s.

Every row of the table in the issue is asserted, plus three properties the issue called out as worth asserting:

  • the rune caps are exercised with multibyte input, so a byte/rune regression fails rather than passing by accident
  • the history limit is asserted on both sides — 12 accepted, 13 refused
  • the unrostered-prompt error is asserted to name the caller's own input and to leak no http://, https://, 127.0.0.1, localhost, or .svc

Characterization, not endorsement

Four behaviors are pinned as they are today rather than asserted as correct. Each failure message says what a change means, so the issue that fixes it has a test to flip rather than a test to delete:

Test Pins Issue
TestHTTPTurnRejectsABodyOverTheByteCap oversized body reported as malformed JSON 157
TestHTTPTurnAcceptsUnknownJSONFields unknown fields accepted in silence 173
TestQueueDenialCarriesNoRetryAfter queue-shed 429 carries no Retry-After 181
TestCallerHeaderIsolatesTheUserTierOnly context bucket and pending counter shared 182

Both limiter tests are at the limiter level with no HTTP involved, as the issue proposed.

Docs

docs/sirens-echo-http.md stated that a limited caller "receives 429 with Retry-After". That is not true of the pending-cap shed, which is the defect behind issue 181 — the doc was asserting a contract the code does not keep. Corrected, and the full rejection contract moved into docs/sirens-echo-http-contract.md to stay under the 80-line doc cap.

Verification

Tests passing is not evidence that tests work. I mutated http.go three ways and confirmed each mutation was caught by its intended assertion, then reverted:

Mutation Caught by
history bound >>= TestHTTPTurnEnforcesTheHistoryLimitOnBothSideshistory at 12 status = 400, want 200
author cap 256255 TestHTTPTurnBoundsAuthorAndContentInRunesauthor at the cap status = 400, want 200
Allow: POST header removed TestHTTPTurnRejectsEveryMethodButPOSTGET Allow = "", want POST

Also go vet clean, gofmt clean, pre-commit run --files clean, and the new tests at -count=5 -race.

One correction to the issue

The issue says "handleHTTPTurn appears in no test file." Not quite — agent_test.go already has TestHTTPTurnAppliesTheAdmissionPolicy and TestHTTPTurnReleasesItsQueueSlot, which cover admission and slot release on that path. This PR reuses their turnAgent helper rather than duplicating it, and does not re-cover what they already assert. The uncovered surface was validation and routing, which is what this adds.


Quail (QA)

Closes https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/193 `handleHTTPTurn` had no direct coverage. Its rejection contract was established only by probing the deployed pod, so every guarantee cost a tailnet round trip against production and none of them were protected against regression. ## What this adds `internal/community/http_test.go` — 12 tests over the turn surface, all `httptest`, no model, no Agent Proxy, no MCP. Full package suite runs in ~1s. Every row of the table in the issue is asserted, plus three properties the issue called out as worth asserting: - the rune caps are exercised with multibyte input, so a byte/rune regression fails rather than passing by accident - the `history` limit is asserted on **both** sides — 12 accepted, 13 refused - the unrostered-`prompt` error is asserted to name the caller's own input and to leak no `http://`, `https://`, `127.0.0.1`, `localhost`, or `.svc` ## Characterization, not endorsement Four behaviors are pinned as they are today rather than asserted as correct. Each failure message says what a change means, so the issue that fixes it has a test to **flip** rather than a test to delete: | Test | Pins | Issue | | --- | --- | --- | | `TestHTTPTurnRejectsABodyOverTheByteCap` | oversized body reported as malformed JSON | [157](https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/157) | | `TestHTTPTurnAcceptsUnknownJSONFields` | unknown fields accepted in silence | [173](https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/173) | | `TestQueueDenialCarriesNoRetryAfter` | queue-shed `429` carries no `Retry-After` | [181](https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/181) | | `TestCallerHeaderIsolatesTheUserTierOnly` | context bucket and pending counter shared | [182](https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/182) | Both limiter tests are at the limiter level with no HTTP involved, as the issue proposed. ## Docs `docs/sirens-echo-http.md` stated that a limited caller "receives `429` with `Retry-After`". That is not true of the pending-cap shed, which is the defect behind issue 181 — the doc was asserting a contract the code does not keep. Corrected, and the full rejection contract moved into `docs/sirens-echo-http-contract.md` to stay under the 80-line doc cap. ## Verification Tests passing is not evidence that tests work. I mutated `http.go` three ways and confirmed each mutation was caught by its intended assertion, then reverted: | Mutation | Caught by | | --- | --- | | history bound `>` → `>=` | `TestHTTPTurnEnforcesTheHistoryLimitOnBothSides` — `history at 12 status = 400, want 200` | | author cap `256` → `255` | `TestHTTPTurnBoundsAuthorAndContentInRunes` — `author at the cap status = 400, want 200` | | `Allow: POST` header removed | `TestHTTPTurnRejectsEveryMethodButPOST` — `GET Allow = "", want POST` | Also `go vet` clean, `gofmt` clean, `pre-commit run --files` clean, and the new tests at `-count=5 -race`. ## One correction to the issue The issue says "`handleHTTPTurn` appears in no test file." Not quite — `agent_test.go` already has `TestHTTPTurnAppliesTheAdmissionPolicy` and `TestHTTPTurnReleasesItsQueueSlot`, which cover admission and slot release on that path. This PR reuses their `turnAgent` helper rather than duplicating it, and does not re-cover what they already assert. The uncovered surface was validation and routing, which is what this adds. --- Quail (QA)
test(http): cover the /v1/turn rejection contract
All checks were successful
ci / test (pull_request) Successful in 27s
ci / publish-echo-image (pull_request) Has been skipped
ci / image-build (pull_request) Successful in 16s
f54b35fce4
The turn surface is the only ingress a scripted client reaches directly,
and its rejection contract was established only by probing the deployed
pod. Every guarantee is now an httptest assertion that needs no model, no
Agent Proxy, and no MCP.

Four behaviors are pinned as characterization tests rather than asserted
as correct, so the issue that fixes each has a test to flip rather than a
test to delete: an oversized body reported as malformed JSON, unknown
JSON fields accepted in silence, a queue-shed 429 with no Retry-After,
and X-Sirens-Caller splitting the per-user tier alone.

docs/sirens-echo-http.md claimed every 429 carries Retry-After, which the
queue-shed path does not. Corrected, with the full contract split into
docs/sirens-echo-http-contract.md to stay under the doc size cap.

Verified by mutating http.go three ways — an exclusive history bound, an
off-by-one rune cap, and a dropped Allow header — and confirming each was
caught by its intended assertion.

Refs: #193

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-gaming/sirens-echo!240
No description provided.