Skip to content

Both testing blind spots are closed — what remains is a fetch-deadline floor in safeFetch, then two ratchets (deadline + responsive-label) #438

Description

@serge-ivo

Architecture scan: the bug rate is not a size or coverage problem — it is two specific blind spots, and the codebase already invented the fix for one of them

Prompted by "we're getting lots of bugs lately". I used the 14 issues filed in one session (#420-#433) as the sample, then looked for shared structural causes rather than surveying file sizes.

What is NOT the problem (checked, so it can be ruled out)

So "write more tests" and "split big files" are both the wrong prescription. The bugs are getting through a suite that is large and a codebase that is well-decomposed.

Blind spot 1 — no SQL in the test suite is ever executed

test files mocking D1 by string-matching SQL : 59
@cloudflare/vitest-pool-workers / miniflare  : not configured

Every D1 test is of the shape if (sql.includes("FROM error_log")) return { results: … }. The SQL string is matched, never parsed, never run. So the suite cannot detect a syntax error, a wrong column, or a platform limit.

This is precisely how #423 shipped. activeInstancesForDay builds 6 SELECTs joined by 5 UNIONs — which D1 rejects outright. It failed on every cron tick for 29.6 hours, produced 1780 identical error rows (97% of the entire error log), and the stats feature it powers has never written a row in production. It passed CI green, because stats-rollup.test.ts covers buildSeries / completedDay / enumerateDays / trendCards — the pure helpers — and never executes the query.

A test that runs SQL against real D1 would have caught it the moment it was written.

Blind spot 2 — the contracts between layers are unverified

tests asserting the query string a route passes to its DO : 0

This is how #428 shipped. "Load older messages" is broken in three places at once:

  1. the client sends a UUID as an ordering cursor (InstanceDetail.tsx:475),
  2. the route rebuilds the query string with only limit, silently dropping before (chat.ts:160),
  3. the DO never implemented before at all (agent-do.ts:768).

Each layer is individually tested. Each test passes. Nothing tests the hop. Verified against production: page 2 is byte-identical to page 1.

#432 is the same shape one layer down — terminal snapshots are persisted, fetched in full, and then all but the last are discarded in the client.

The pattern behind the rest: a fix applied to one sibling and not the other

Issue Correct code exists at The sibling that missed it
#431 CodingTab.tsx:807hidden sm:inline on the 2-button toggle :887 — the 4-button row, 80 lines below, same file
#430 lengthRule computed at agent-think.ts:645 for every agent emitted only inside if (plainSpeech) — 1 of 4 branches
#427 the same non-streaming-under-a-deadline defect as #421, in a different subsystem
#426 mobile overflow, same class as #431

Two of these are literally the same bug in two places (#421/#427, #426/#431). Nothing propagates a fix to analogous code, so a defect class gets fixed once and survives everywhere else.

The codebase already invented the right tool — and applies it inconsistently

This is the important finding. There is an existing, sophisticated pattern for exactly this failure mode:

mute-invariant.test.ts · mute-touch-invariant.test.ts · security-invariants.test.ts
prompt-claims.ts · agent-claims-lint.ts · usage-claims.test.ts · seed-drift.test.ts
+ 6 scripts/check-*.mjs CI guards

mute-invariant.test.ts is the model: it does not pin today's implementation, it asserts a reachability property across every phase, and it includes a structural scan of use-voice.ts because "no pure test can see it". Its header explains why a locally-correct one-line fix would have silently deleted a feature — the exact failure mode above.

That tool exists for mute, security, prompt claims, usage claims and seed drift. It does not exist for:

Recommendation — three changes, in order of value

1. Execute SQL in tests. Adopt @cloudflare/vitest-pool-workers for the query-bearing modules so D1 statements actually run against real D1/miniflare. Highest value per unit of effort in this list: it converts an entire invisible defect class (#423) into a CI failure. It does not require rewriting the 59 existing mocks — start with the modules that build dynamic SQL, which is where the risk concentrates.

2. Contract tests at the seams. For each parameter crossing client → route → DO, one test asserting it survives the hop. Cheap, mechanical, and would have caught #428 and #432. The natural home is beside the existing route tests.

3. Extend the invariant-test pattern to the four properties above. This is not new methodology — it is applying a proven in-house pattern to the properties that are currently unguarded. Each is a small, pure, scanning test in the shape mute-invariant.test.ts already established.

What I deliberately do NOT recommend

  • A large refactor or re-architecture. The evidence does not support it. Modules are well-separated, purity is used heavily and deliberately, and the docs are accurate. The defects are at joins and in unexecuted branches, which a re-architecture would move rather than remove.
  • Splitting more files. The ratchet already governs size and passes.
  • More unit tests. The ratio is already 273/296. More of the same kind would not have caught a single one of the 14.
  • Adopting an outside testing framework. The #138 refactor was undone within hours — add a size ratchet so decomposition holds #302's history is instructive: a decomposition was undone within hours and needed a ratchet to hold. A pattern already used in this repo will stick; an imported one is likely to be abandoned.

Acceptance criteria — three of five are MET; only the last two remain (audit 2026-08-23)

Both blind spots the title names are CLOSED. Do not rebuild them.

  • A deliberately malformed D1 query fails a test rather than production.workers/api/src/lib/d1-sqlite.ts (298 lines, a real SQLite engine for the suite), lib/sql-execution.test.ts, lib/sql-schema.test.ts. Commit 77e17c1c. The sweep runs 502 statements across 117 modules against the schema the migrations build.
  • Removing a parameter from a route's DO request fails a test.workers/api/src/lib/do-seam.ts + lib/do-seam.test.ts (commit 597e2377, the dispatch table that can be asked which query parameters a path takes) and workers/api/src/routes/do-seam.contract.test.ts (435 lines, commit 99174a9c). Proven red on the exact [bug] "Load older messages" re-returns the newest page — the before cursor is a UUID, the route drops it, and the DO never implemented it #428 hunk.
  • Adding a styleGuidance branch without a length rule fails a test — satisfied by the TYPE SYSTEM, which is stronger than the scan this asked for. lib/agent-style-prompt.ts:196-217 makes StyleBranch.lengthDefault a required field, so a fifth branch omitting it fails tsc --noEmit, which CI runs as a named gate. Behaviour also pinned at lib/agent-style-prompt.test.ts:59 describe("every styleGuidance branch carries a length rule (#430)").

What remains — two guards, and one of them needs a floor built first

  • Adding an external call with no deadline fails a test.

    Do not write the scan first — it would go red on 62 of 65 call sites and be suppressed. Measured on main, workers/api/src, excluding tests: 65 fetch( sites, 3 files constructing an AbortController (lib/user-ai.ts:191,421, lib/steps.ts:332, lib/mcp-credentials.ts:520), and 0 uses of AbortSignal.timeout.

    Two steps, in order:

    1. The floor — give safeFetch (workers/api/src/lib/ssrf.ts) a default AbortSignal.timeout. It is already the chokepoint every connector, fetch_url and ingest path goes through, so this converts most of the 62 in one edit without touching a call site. This is a finding in its own right and is the higher-value half.
    2. The ratchet — only then the scan, in the sql-schema.test.ts shape: extract fetch( sites, assert each carries a signal or routes through safeFetch. Pin it EXACTLY (like control-shapes.test.ts), not as a <= ceiling.
  • Adding a mobile control row without responsive labels fails a test.

    No responsive-label scan exists. Its natural home is the existing harness, not a new file: store/console/src/lib/control-shapes.test.ts (the exact-pinned ratchet over hand-authored button/card shapes), alongside store/console/src/lib/control-classes.test.ts:160-169 which already holds the console / agents/coder/web / store/admin copies equal.

    Its first assertion should be the live defect the harness already recordscontrol-shapes.test.ts:150: // className="hidden sm:flex" on a <Button> does not hide it — BUTTON_BASE wins, silently. That is a real class of silently-dead responsive utility, not a hypothetical.

Files this work touches: workers/api/src/lib/ssrf.ts (the floor) · a new fetch-deadline scan beside lib/sql-schema.test.ts · store/console/src/lib/control-shapes.test.ts. Collides with nothing else currently open — the two halves are independent of each other and of every other issue in the delivered-work audit.

Sequencing note

#423 should be fixed before (1) lands, not after — 97% of the error log is that one bug, and the log is the instrument for judging whether any of this works. #424's audit and its alerting gap (errors24h is computed, thresholded by nobody) are the other half: these changes are only verifiable if someone finds out when they fail.

Related: #423 (the SQL that no test ran), #428 / #432 (the seam), #421 / #427 and #426 / #431 (one defect class, two sites each), #430 (computed everywhere, used in one branch), #302 (the ratchet that proves guards hold where good intentions did not), #424 (nobody is watching the log).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3: laterDesign, strategy, or deferred pending a decision or demandbackendBackend / Worker / API workenhancementNew feature or requesttestingGuards, test doubles and coverage — not a product defect

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions