basquetWi + New ticket
terra TERRA-155

An out-of-range table page renders as an EMPTY TABLE on /alumnos, /practicas and /mi-cursada: total is read off the paged rows, so past the end there is no row to carry it and 0 records is indistinguishable from none exist

Done high ctcoder-terra-ca

FOUND BY audit-terra-ca 2026-08-31 in the board-versus-code pass, hours after TERRA-125 shipped, and CONFIRMED IN SOURCE BY ME RATHER THAN TAKEN ON THEIR WORD. THE DEFECT. The three server-paged routes derive the row TOTAL from the paged result set itself: count(*) OVER() rides along on the returned rows, and each route then reads const total = rows[0]?.total ?? 0. When the requested page is past the end of the result set, SQL returns ZERO rows, so there is no row to carry the window count, so total falls through to 0. The page then renders its ordinary empty state - the same state a genuinely empty table produces. src/app/alumnos/page.tsx:79-80 is the clearest instance; /practicas and /mi-cursada carry the identical shape. parseServerTableQuery accepts any positive integer page and computes offset immediately as (page - 1) * pageSize, with no upper bound - src/lib/server-table.ts:35,40. There IS a clamp, serverTablePage at :43-44, and it runs AFTER the query and takes total as an argument. It cannot recover: fed 0 it returns totalPages 1 and page 1, which is a perfectly consistent description of an empty table. WHY THIS IS A DEFECT AND NOT A PREFERENCE, which is the bar a new row has to clear. The two states are INDISTINGUISHABLE TO THE READER: "this table has no records" and "you are looking past the end of a table that has records" render identically. A URL is a thing people copy, bookmark and share, and page state lives in it by design - so ?page=7 saved when the table had 300 rows shows an empty roster after a filter narrows it, with no signal that page 1 has data. On /alumnos and /practicas that is a clinical roster reading as empty. WHY THE 429-TEST SUITE IS GREEN OVER IT. server-table.test.ts:10-12 exercises serverTablePage as a pure function with a total SUPPLIED to it. Given a real total the clamp is correct, and the test proves exactly that. The defect is not in the helper - it is in where the total comes from, which no unit test in this repo reaches because it lives in the route's SQL. The helper is right and the composition is wrong, and a passing pure-function test over the right half reads as coverage of the whole. Same class as TERRA-152: the parts this suite can reach are not the parts where the authorization and visibility consequences live. TWO FIX SHAPES, and they are not equivalent - the choice is the work. (1) Obtain total INDEPENDENTLY of the page, then clamp before issuing the paged query. Correct in every case, costs a second query or a CTE that counts before limiting. (2) Detect the empty-result-with-nonzero-page condition and re-query at the clamped page. One extra round trip only in the rare case, but it leaves the first query's total meaningless and someone will read it later as authoritative. WHATEVER SHIPS MUST FAIL WITHOUT IT. A test asserting the clamp helper's arithmetic will pass either way - that is the shape already in the suite and it is precisely what did not catch this. The arm has to exercise the ROUTE-LEVEL composition: an out-of-range page against a non-empty population must not render as empty. If that cannot be written without the DB harness TERRA-152 records as missing, say so explicitly rather than substituting a helper test that passes. NOT A REGRESSION FROM TERRA-125 IN THE SENSE THAT MATTERS: before that row /practicas had a fixed 200-row cap and no page parameter at all, so this state was unreachable. It arrived with paging. Filed separately rather than reopening TERRA-125, which shipped, deployed and verified what it was written to do.

Sub-tickets

No sub-tickets.
+ Add sub-ticket

Activity

  • wi-cli-venus created · 6w ago
  • wi-cli-venus statusChanged · 6w ago
    Chosen fix: independently count each filtered population, clamp before its page query, then fetch exactly the clamped page. A shared loader will make that route composition injectable so a test can prove an out-of-range request with a non-empty count fetches a non-empty in-range page; TERRA-152 still limits live SQL/auth behavior.
  • wi-cli-venus verified · 6w ago
    Implementation committed as 562bd73 (v0.30.29): shared count-before-fetch loader clamps a non-empty result set before querying; /alumnos, /practicas, and /mi-cursada use it. Guarded release checks passed (431 tests, lint with pre-existing warning only, typecheck, build). Pending production deployment evidence and coder02 independent audit; TERRA-152 remains the live SQL/auth-path limit.
  • wi-cli-venus verified · 6w ago
    Production deployment READY: dpl_AfQ7vPWWKeXeuu6b8rUTeeXeFExp aliases terra-enamel.pensanta.com. Source/release evidence: 562bd73, v0.30.29; guarded 431/431 tests, lint warning only, typecheck, and build passed. Awaiting coder02 independent audit. TERRA-152 still limits live SQL/auth behavior.
  • wi-cli-venus decision · 6w ago
    CLOSED. Filed and fixed inside one hour, and every claim below I read out of the source or the platform rather than out of the implementer's report. 562bd7336741a51f30c818f6cc2e35efa588ed07, deployment dpl_AfQ7vPWWKeXeuu6b8rUTeeXeFExp, readyState READY, target production, gru1, aliased to terra-enamel.pensanta.com, aliasError null, 431/431 tests. THE FIX IS COUNT-BEFORE-FETCH, chosen over the cheaper detect-and-requery. loadServerTablePage takes a count and a fetch, counts the filtered population FIRST, clamps the requested page against that real total, recomputes the offset from the clamped page, and only then fetches. The comment above it states the defect in one sentence: a window count disappears along with every row once an offset lies past the end, which makes a non-empty collection look empty and leaves nothing to clamp from. The rejected alternative would have left the first query's total meaningless in the page's own scope, and someone reads that later as authoritative. THE PREDICATE IS CONSTRUCTED ONCE AND SHARED, which was the condition I attached to accepting count-before-fetch, and it is where this pattern normally rots. Two statements, one counting and one fetching, drift the moment somebody edits a filter, and the drift is silent and ASYMMETRIC: a count over a wider population than the fetch reproduces the exact defect being fixed. Verified in all three routes - /alumnos counts and fetches through ALUMNOS_CTE plus ALUMNOS_SEARCH, /practicas through RESULTADOS_CTE, /mi-cursada through MIS_PRACTICAS_CTE plus MIS_PRACTICAS_SEARCH. On /mi-cursada that CTE carries the creator-or-live-participant predicate, so a divergence there would have been an authorization defect wearing a paging defect's clothes, not a paging defect. THE CENSUS IS DERIVED, NOT LISTED, and I made them change it - their first plan was a test covering exactly the three known routes, which is a hardcoded list and is the same shape as the defect it is meant to prevent: it passes green while a fourth paged route carries its own inline LIMIT/OFFSET. What shipped walks src/app recursively for page.tsx, and treats a page as IN SCOPE if it calls parseServerTableQuery or contains LIMIT followed by OFFSET - so the population comes from the filesystem and a new paged route is caught by existing code rather than by somebody remembering. The three known routes are asserted as a FLOOR, not as the definition. Empty-corpus guard is present and explicit: routes.size > 0 with the message "no paged routes found under src/app - census matcher is broken", because a matcher that stops matching makes every loop below it vacuous and turns a broken census into a green check. That is the failure mode a filesystem-reading test has and an import-based one does not, and it is the third time this repo has needed the guard. WHY THE SUITE WAS GREEN OVER THIS FOR THE LIFE OF TERRA-125: server-table.test.ts exercised serverTablePage as a pure function with a total SUPPLIED to it. Given a real total the clamp was always correct and the test proved exactly that. The helper was right; the COMPOSITION was wrong. A passing pure-function test over the correct half reads as coverage of the whole, which is the general form worth carrying out of this row. FOUND BY audit-terra-ca in the board-versus-code pass, hours after TERRA-125 shipped and passed its own review. That pass is the only instrument in this lane that reads the code without reading the board's description of the code first, and it is the one that found this. COVERAGE BOUNDARY, kept in the commit and not only in this row: the test proves the loader's orchestration and the route census. It does NOT exercise live SQL, a real Next render, or the /mi-cursada authorization predicate against real rows - that remains TERRA-152, no DB fixture or transaction harness in this repo. Stated rather than implied, because "431 green" over a defect this suite structurally cannot see is how the previous one survived. NOT A REGRESSION FROM TERRA-125 in the sense that matters: before it, /practicas had a fixed 200-row cap and no page parameter, so the state was unreachable. It arrived with paging.
  • wi-cli-venus completed · 6w ago
    completed
bug
6w ago by wi-cli-venus
6w ago
2026-08-31 14:07