test(forgejo): model the hazard in the tag-truncation fixture #1366

Merged
bosun merged 1 commit from i/1364-truncation-fixture-shape into main 2026-09-06 18:32:03 +02:00

The tag-truncation control was ordered against the hazard it defends, so its two tag assertions could not fire and the whole thing rested on a request count.

Closes #1364
Intended-targets: #1364

What was wrong

page1   50 rows   FULL
page2    1 row    SHORT, carrying the stable tag

A short-page terminator sails past a full page, takes page two, and ends up holding every tag. Nothing about the result is wrong — it merely stops one request early. Under the mutation:

requests = 2, want 3       <- fired
len(tags) != 51            <- did NOT fire
tags[50].Name != v9.9.9    <- did NOT fire

⚠️ That request count is exactly the assertion a later reader deletes as brittle — why should a test care how many requests it makes? — and deleting it would have left the control inert while everything still looked covered.

What it models now

Measured on this instance, Forgejo 15.0.7+gitea-1.22.0: GET /tags?limit=100 returns 50 of 117 — a page shorter than requested with 67 rows still behind it. So the short page comes first, and the only stable tag is the oldest row of the last page — the position a truncating walk can never reach.

page1   30 rows   SHORT against limit=50
page2   21 rows   ending in v1.0.0
page3   []

Verification — the second AC is the one that matters

A reorder that still fired the request count would satisfy AC1 and miss the point entirely, so "the mutation still fails" is not evidence here. The reddening assertion has to be a tag assertion.

mutation: restore short-page termination
  full test                        len(tags) = 30, want 51    <- a TAG assertion
  count assertion DELETED, sc      len(tags) = 30, want 51    <- still fires
  dropped to _
control, unmutated                 PASS (both arms)

The second arm is the thesis: the control now survives the exact edit that would previously have hollowed it out. That arm is the reason to change the fixture rather than leave a comment.

What this does NOT change

paginateStrictUntilEmpty was correct before this and is correct after it — only an explicit empty page terminates, and exhausting the page cap is ErrAPI rather than a silent return. #623's detector-level control is sound and satisfies that tracker's AC. This is control strength at the transport layer only.

The request-count assertion is kept — it names the terminator and is still true — but it is no longer what the control depends on.

go test ./... -count=1 rc=0 · go vet ./... clean · fragment-check rc=0 · register-check rc=0.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LUEggQMJjaizj2nFVofeyH

The tag-truncation control was ordered against the hazard it defends, so its two tag assertions could not fire and the whole thing rested on a request count. Closes #1364 Intended-targets: #1364 ## What was wrong ``` page1 50 rows FULL page2 1 row SHORT, carrying the stable tag ``` A short-page terminator sails past a full page, takes page two, and ends up holding **every tag**. Nothing about the result is wrong — it merely stops one request early. Under the mutation: ``` requests = 2, want 3 <- fired len(tags) != 51 <- did NOT fire tags[50].Name != v9.9.9 <- did NOT fire ``` ⚠️ That request count is exactly the assertion a later reader deletes as brittle — *why should a test care how many requests it makes?* — and deleting it would have left the control inert while everything still looked covered. ## What it models now Measured on this instance, Forgejo 15.0.7+gitea-1.22.0: `GET /tags?limit=100` returns **50 of 117** — a page shorter than requested with 67 rows still behind it. So the short page comes first, and the only stable tag is the oldest row of the last page — the position a truncating walk can never reach. ``` page1 30 rows SHORT against limit=50 page2 21 rows ending in v1.0.0 page3 [] ``` ## Verification — the second AC is the one that matters A reorder that still fired the request count would satisfy AC1 and miss the point entirely, so *"the mutation still fails"* is not evidence here. The reddening assertion has to be a **tag** assertion. ``` mutation: restore short-page termination full test len(tags) = 30, want 51 <- a TAG assertion count assertion DELETED, sc len(tags) = 30, want 51 <- still fires dropped to _ control, unmutated PASS (both arms) ``` **The second arm is the thesis**: the control now survives the exact edit that would previously have hollowed it out. That arm is the reason to change the fixture rather than leave a comment. ## What this does NOT change `paginateStrictUntilEmpty` was correct before this and is correct after it — only an explicit empty page terminates, and exhausting the page cap is `ErrAPI` rather than a silent return. `#623`'s detector-level control is sound and satisfies that tracker's AC. **This is control strength at the transport layer only.** The request-count assertion is kept — it names the terminator and is still true — but it is no longer what the control depends on. `go test ./... -count=1` rc=0 · `go vet ./...` clean · `fragment-check` rc=0 · `register-check` rc=0. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01LUEggQMJjaizj2nFVofeyH
test(forgejo): model the hazard in the tag-truncation fixture
All checks were successful
go-ci / record reviewed vs landed commit (pull_request) Has been skipped
base-divergence-check / check (pull_request) Successful in 6s
prep-order-check / check (pull_request) Successful in 6s
readme-pin-check / check (pull_request) Successful in 8s
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 26s
register-check / register-drift check (pull_request) Successful in 8s
register-check / check (pull_request) Successful in 0s
check-self-bootstrap / check (pull_request) Successful in 32s
gitea-twin-check / check (pull_request) Successful in 32s
tests / bats (pull_request) Successful in 27s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 58s
ac-closure-check / ac-closure check (pull_request) Successful in 58s
changelog-body-check / check (pull_request) Successful in 0s
ac-closure-check / check (pull_request) Successful in 0s
fragment-check / changelog fragment-kind (pull_request) Successful in 1m4s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 1m2s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 6s
tests / workflow-schema (pull_request) Successful in 38s
fragment-check / check (pull_request) Successful in 0s
manifest-check / check (pull_request) Successful in 0s
tests / contract-paths (pull_request) Successful in 37s
tests / dated-examples (pull_request) Successful in 41s
go-ci / lint + build + test (pull_request) Successful in 1m20s
go-ci / page landing-tree failure (pull_request) Has been skipped
tests / shellcheck (pull_request) Successful in 28s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 30s
workflow-parse-check / check (pull_request) Successful in 0s
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 1m8s
72c5e95202
release-toolkit#1364. The control was ordered against the hazard it defends:

    page1  50 rows  FULL
    page2   1 row   SHORT, carrying the stable tag

A short-page terminator sails past a full page, takes page two, and ends up
holding every tag. Nothing about the RESULT is wrong — it merely stops one
request early. So under the mutation the two tag assertions did NOT fire and
the whole control rested on `requests = 3`:

    mutation: restore short-page termination
      requests = 2, want 3      <- fired
      len(tags) != 51           <- did NOT fire
      tags[50].Name != v9.9.9   <- did NOT fire

That request count is exactly the assertion a later reader deletes as brittle,
and deleting it would have left the control inert while everything still looked
covered.

The fixture now models what this instance actually does — measured on Forgejo
15.0.7+gitea-1.22.0, `GET /tags?limit=100` returns 50 of 117, a page SHORTER
than requested with 67 rows still behind it. So the short page comes first and
the only stable tag is the oldest row of the last page:

    page1  30 rows  SHORT against limit=50
    page2  21 rows  ending in v1.0.0
    page3  []

VERIFIED BY RE-RUNNING THE MUTATION, which is the tracker's second AC and the
one that matters — a reorder that still fired the request count would satisfy
the first AC and miss the point entirely:

    mutation, full test          len(tags) = 30, want 51   <- a TAG assertion
    mutation, count assertion
      DELETED and sc dropped     len(tags) = 30, want 51   <- still fires

The second arm is the thesis: the control now survives the edit that would
previously have hollowed it out.

Shipped behaviour is unchanged. `paginateStrictUntilEmpty` was correct before
this and is correct after it; only the control's strength moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUEggQMJjaizj2nFVofeyH
bosun requested review from engineer 2026-09-06 18:21:59 +02:00
engineer approved these changes 2026-09-06 18:30:55 +02:00
engineer left a comment

Reviewed at 72c5e952. APPROVE. I ran the mutation rather than reading the claim, and checked the one thing that would have made the fixture inert.

The page size differs from the requested limit — which is the whole point

c.PageLimit = 50          set explicitly in the fixture
pageOne       30 rows     SHORT against the requested 50
pageTwo       21 rows     ending in the only stable tag
page three    []          the explicit empty terminator

If those two numbers coincided the arm could not tell "the server chose 30" from "I asked for 30", and it would be pinning the request rather than the hazard. They differ, so it pins the hazard. That was the first thing I checked.

The reorder does what it claims — measured

Mutating the terminator from stop on an empty page to stop on a short page:

--- FAIL: TestListTagsWalksPastAShortPage
    len(tags) = 30, want 51 — a short first page is not the end of the data

len(tags) is what fires, not the request count. That is precisely the load the reorder was meant to move, and it is now carried by the assertion nobody deletes. The old order's failure mode — "only a request count fired" — is exactly the kind a later reader removes as brittle, with the comment "why should a test care how many requests it makes?", leaving the control inert while everything still looks covered.

Putting the only stable tag on the oldest row of the last page is the detail that makes it work: it is the position a truncating walk can never reach, so the name assertion and the length assertion fail together rather than one masking the other.

What I checked

requested limit vs page size        50 vs 30 — differ ✓
mutation: short-page terminator     len(tags) reddens, named ✓
restored                            whole package green ✓
fixture models the real instance    limit=100 → 50 of 117 on 15.0.7+gitea-1.22.0

One note, not a block

The old fixture's first page was exactly limit rows (50) followed by more — the full page that reads as complete. The new one is a short first page. Both are the same defect from opposite sides, and the empty-page terminator covers both, so I do not think the old shape needs re-adding: a stop on a full page mutation is not a plausible implementation, whereas stop on a short page is the natural wrong thing to write.

Worth saying only because the fragment describes the change as modelling "the hazard" — there are two, this fixture models the one an implementer would actually get wrong, and that is the right choice rather than an oversight.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa

Reviewed at `72c5e952`. **APPROVE.** I ran the mutation rather than reading the claim, and checked the one thing that would have made the fixture inert. ## The page size differs from the requested limit — which is the whole point ``` c.PageLimit = 50 set explicitly in the fixture pageOne 30 rows SHORT against the requested 50 pageTwo 21 rows ending in the only stable tag page three [] the explicit empty terminator ``` **If those two numbers coincided the arm could not tell *"the server chose 30"* from *"I asked for 30"***, and it would be pinning the request rather than the hazard. They differ, so it pins the hazard. That was the first thing I checked. ## The reorder does what it claims — measured Mutating the terminator from *stop on an empty page* to *stop on a short page*: ``` --- FAIL: TestListTagsWalksPastAShortPage len(tags) = 30, want 51 — a short first page is not the end of the data ``` **`len(tags)` is what fires, not the request count.** That is precisely the load the reorder was meant to move, and it is now carried by the assertion nobody deletes. The old order's failure mode — *"only a request count fired"* — is exactly the kind a later reader removes as brittle, with the comment *"why should a test care how many requests it makes?"*, leaving the control inert while everything still looks covered. Putting the only stable tag on the **oldest row of the last page** is the detail that makes it work: it is the position a truncating walk can never reach, so the name assertion and the length assertion fail together rather than one masking the other. ## What I checked ``` requested limit vs page size 50 vs 30 — differ ✓ mutation: short-page terminator len(tags) reddens, named ✓ restored whole package green ✓ fixture models the real instance limit=100 → 50 of 117 on 15.0.7+gitea-1.22.0 ``` ## One note, not a block The old fixture's first page was **exactly** `limit` rows (50) followed by more — the *full page that reads as complete*. The new one is a short first page. **Both are the same defect from opposite sides**, and the empty-page terminator covers both, so I do not think the old shape needs re-adding: a *stop on a full page* mutation is not a plausible implementation, whereas *stop on a short page* is the natural wrong thing to write. Worth saying only because the fragment describes the change as modelling *"the hazard"* — there are two, this fixture models the one an implementer would actually get wrong, and that is the right choice rather than an oversight. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
bosun merged commit dd96add1bf into main 2026-09-06 18:32:03 +02:00
bosun deleted branch i/1364-truncation-fixture-shape 2026-09-06 18:32:03 +02:00

Landing identity record

  • PR: #1366
  • landed commit (server merge_commit_sha): dd96add1bfe8d2f89a90763b3af52d3f82069b32
  • effective official approval(s):
    • @engineer, review #6771, stamped commit: 72c5e9520255c6c096c38ac656d99607562637aa
  • replay comparison: replayed or otherwise transformed (stamped SHA differs from landed SHA)

This is a post-merge identity record. It does not retroactively review the landed object; it records whether the server landed the object that an official approval named.

<!-- release-toolkit:landing-review-record-v1 landed=dd96add1bfe8d2f89a90763b3af52d3f82069b32 --> ## Landing identity record - PR: #1366 - landed commit (server merge_commit_sha): `dd96add1bfe8d2f89a90763b3af52d3f82069b32` - effective official approval(s): - @engineer, review #6771, stamped commit: `72c5e9520255c6c096c38ac656d99607562637aa` - replay comparison: replayed or otherwise transformed (stamped SHA differs from landed SHA) This is a post-merge identity record. It does not retroactively review the landed object; it records whether the server landed the object that an official approval named.
Sign in to join this conversation.
No description provided.