test: close the empty-batch hole in the e2e unstubbed-request guard (closes #187)
Some checks failed
check / check (push) Has been cancelled

The guard that reports unrecognised POST bodies used batch.every(), which is
vacuously true on an empty array, so a POST with body [] was answered 200 []
and escaped the one mechanism whose job is to make unrecognised outbound
traffic fail the suite rather than pass silently. Unreachable in practice
today, which is exactly the qualifier that stops being true later.

The comment explaining the guard also described a mechanism that does not
exist: playwright-core decodes a binary body lossily rather than returning
null, so such a body reaches the JSON parse as mojibake and is reported by the
catch, while only an absent or empty body decodes to null and is reported by
the type guard. Both are reported; the comment now describes the two real
routes.
This commit was merged in pull request #267.
This commit is contained in:
2026-08-12 11:50:38 +02:00
parent 5af89a1b63
commit 18b47cd579
2 changed files with 25 additions and 8 deletions

View File

@@ -44,6 +44,14 @@ undefined identifiers, which is how
# Completed Steps # Completed Steps
- 2026-08-12: Closed the empty-array hole in the end-to-end unstubbed-request
guard. `batch.every()` is vacuously true on `[]`, so a POST with body `[]` was
answered `200 []` instead of failing the suite; the guard now rejects an empty
batch, demonstrated green-before/red-after with a throwaway probe. The comment
claiming `postData()` returns `null` for undecodable bodies was corrected to
the two real paths — an absent or empty body decodes to `null`, a binary body
decodes lossily into invalid JSON
([#187](https://git.eeqj.de/sneak/AutistMask/issues/187)).
- 2026-08-12: The transaction confirmation screen has browser coverage. The - 2026-08-12: The transaction confirmation screen has browser coverage. The
end-to-end suite reaches ConfirmTx for both the native ETH and the ERC-20 path end-to-end suite reaches ConfirmTx for both the native ETH and the ERC-20 path
off a funded-balance fixture, and asserts the pending, funded, over-balance off a funded-balance fixture, and asserts the pending, funded, over-balance

View File

@@ -297,17 +297,26 @@ async function handleRpc(route, postData, opts, report) {
// ethers batches by default, so the body may be an array. // ethers batches by default, so the body may be an array.
const batch = Array.isArray(payload) ? payload : [payload]; const batch = Array.isArray(payload) ? payload : [payload];
// Anything that is not a JSON-RPC object, or a batch of them, is not // Anything that is not a JSON-RPC object, or a NON-EMPTY batch of
// RPC at all and must be reported like any other unrecognised // them, is not RPC at all and must be reported like any other
// outbound traffic rather than dereferenced. request.postData() // unrecognised outbound traffic rather than dereferenced.
// returns null both for a bodyless POST and for a body Playwright //
// cannot decode as UTF-8 (sendBeacon with a Blob, or any binary // The length check is not decoration: every() is vacuously true on an
// payload), so this is not an empty-string special case: it rejects // empty array, so without it a POST with body [] was answered 200 []
// every non-object payload, exactly as the catch above rejects every // and escaped the guard entirely (issue #187). No real batch is empty,
// unparseable one. // so nothing legitimate is caught by it.
//
// Two distinct paths land a non-RPC body here, and neither is an
// empty-string special case. playwright-core's postData() is
// `buffer.toString("utf-8") || null`, so an absent or empty body
// decodes to null, JSON.parse("null") yields null, and the type guard
// below reports it. A binary body is instead decoded LOSSILY into
// mojibake — not null — which is not valid JSON, so the catch above
// reports that one. Both end up reported; only the route differs.
if ( if (
payload === null || payload === null ||
typeof payload !== "object" || typeof payload !== "object" ||
batch.length === 0 ||
!batch.every((req) => req !== null && typeof req === "object") !batch.every((req) => req !== null && typeof req === "object")
) { ) {
report("unstubbed request: POST " + route.request().url()); report("unstubbed request: POST " + route.request().url());