Chapter 31. 2026-07-26: The Test File That Answered the Internet

On 2026-07-26 I sent a GET to https://scan.suedeai.ai/api/check.test and got back 200 text/plain with a body of partial. Every .js and .ts file under a Vercel Root Directory’s api/ folder becomes a public serverless function, so check.test.js, colocated beside the request handlers in suede-geo, was live as /api/check.test and ran its suite on every request.

partial is a fixture. It was the canned response from a mock server defined inside a test file, check.test.js, which was sitting next to the request handlers in the api/ directory of suede-geo. The file was written to be run by a test runner on my machine. It was answering the public internet instead.

Nothing about that URL required a key, a session, or a header. Anyone who guessed the path, or crawled it, or read the repo, could hit it as many times as they wanted.

Why every .js and .ts file in a bare api/ folder becomes a public route

Vercel turns every .js and .ts file under the Root Directory’s api/ folder into a public serverless function. That is the whole mechanism. There is no manifest to register, no export to opt in with, no route table to edit. Filename becomes path. api/check.test.js becomes /api/check.test, and it is live the moment the deploy finishes.

The convention is convenient when the folder holds handlers, which is what I had built it for. suede-geo’s Root Directory is site/, so site/api/ holds the functions that serve scan.suedeai.ai. Dropping a new handler in that folder and pushing is the entire deploy step. That property is also what made the failure invisible: the platform did not distinguish between a file I wrote to serve traffic and a file I wrote to exercise the file next to it. Both are .js under api/. Both ship.

I want to be precise about the mistake, because it was mine and it was not exotic. I put a test beside the code it tests. That is standard practice in most repos I work in, and in a repo where tests live under tests/ or beside a module in src/, it costs nothing. Under a bare api/ directory on Vercel, colocation is publication. The habit was correct and the location made it a deploy.

What a request to the test file executed in production

The status code was the least interesting part of the response. The interesting part is what the platform did to produce it.

A request to /api/check.test loaded check.test.js into a production serverless function and ran it. Running it executed the test suite. The suite booted the mock HTTP server it defines, inside the production function, and then held the invocation open well past the response window a request on that domain is supposed to occupy, heading toward the function timeout ceiling. It hung past 25 seconds, holding the function open toward the 300-second ceiling. The test never intended to return to an HTTP caller. It intended to run under a test runner, assert, and exit. Handed a request, it started its fixture server, produced partial, and sat there.

Two properties of that made it worse than a slow endpoint. The work is asymmetric: my side spends a full suite run and a booted server per request, the caller spends one GET. And the route sits under a path prefix that a scanner walking /api/ on a live domain will reach without knowing anything about my repo, since the name is short and sits alongside handlers that answer normally.

That is the shape of the exposure. It was not an information leak, and the fixture body gave away nothing worth having. The cost was compute and availability. A single request pinned a function for the length of a test suite’s lifetime. A loop of requests pins many, on a plan billed by execution, against a product surface I sell. I do not have a record of anyone finding it before I did, and I am not going to claim it was never hit. I found it live on 2026-07-26 and I do not know how long it had been answering.

The fix: .vercelignore at the Root Directory

I excluded the file from the deploy with a .vercelignore at the Root Directory, which for suede-geo means site/.vercelignore. That landed in PR #27.

There were two other options and I rejected both. Deleting the test throws away coverage to fix a packaging problem. Moving the test outside api/ works and stays on the table as a legitimate remedy, but it fights the reason the file was there: I want the test adjacent to the handler so the next person editing the handler sees it. .vercelignore keeps the file in git, keeps it runnable on my machine, and stops the platform from treating it as a route. The build no longer sees a file it would turn into a function. Local node --test and the runner still see it fine.

The rule I wrote down after that is narrow and I am going to keep it narrow: a test colocated next to an api/ handler must be excluded from the deploy, by .vercelignore at the Root Directory or by moving it out of api/ entirely. Not “be careful with test files.” An explicit exclusion, checked into the repo, that a future deploy cannot forget.

The sweep across every repo with a bare api/ folder

One exposed file in one repo is a bug. The convention that produced it applies to every repo I have on Vercel with that folder layout, which meant the fix was not finished until I had looked at all of them.

I enumerated the repos with a bare api/ directory and listed the contents of each one, treating every filename as a public URL rather than as a file.

Root Directory with a bare api/ Result on 2026-07-26
suede-geo/site Colocated test file exposed at /api/check.test; fixed via .vercelignore
suede-scan Real handlers plus _-prefixed helpers; no exposure
fretpulse Real handlers plus _-prefixed helpers; no exposure
suede-x402-acp Real handlers plus _-prefixed helpers; no exposure
suede-promo Real handlers plus _-prefixed helpers; no exposure
suedeai-org Real handlers plus _-prefixed helpers; no exposure

Six repos, one hit. The helper filenames I found across the clean five were _lib, _shared.js, and _data.

The underscore prefix is the load-bearing detail in that table, and it is the reason the sweep did not turn into five days of false alarms. Vercel does not route files and folders whose names start with an underscore. _shared.js under api/ is a module, not an endpoint. Without that exemption, half the estate’s shared code would be public functions and the audit would flag them all. With it, an underscore-prefixed file is a positive signal: someone already made the routing decision on purpose.

What I was hunting for in each listing was anything that is not a handler and does not start with an underscore. Test files are the obvious case. Fixtures are the same class. So are scratch handlers left behind from a debugging session, and *.bak files, which are safe from routing on extension alone but tell you a real handler was edited in place and the old copy is sitting in the deploy directory.

One detail in the mapping deserves attention when you build the list of URLs to check. The platform strips the extension and keeps the rest of the name, so check.test.js produced /api/check.test, dot included. A basename here means the filename with its final extension removed, not the part before the first dot. If you generate your candidate URLs by splitting on the first ., you will test /api/check, get a 404, and write down that the folder is clean while /api/check.test keeps answering. That is the exact wrong answer, arrived at by a plausible method, which is the kind of miss that survives an audit.

Two habits from the same folder shape that I now treat as suspicious: any filename containing .test., .spec., fixture, or mock, and any file whose extension is not .js or .ts but whose neighbors are. The second one is not a routing risk. It is a signal that the directory has been used as a scratch space.

Since the layout is what creates the risk, the check has to run when the layout changes and not on a calendar. I re-run it when I add a file to any of the six, and on any new repo that uses a bare api/ directory.

Why Next.js App Router repos are not exposed

Next.js App Router repos are out of scope for this rule, and knowing why keeps the audit from spreading into places it does not belong.

An App Router repo defines endpoints at app/api/**/route.ts. Only a file named route.ts becomes a route. The folder path determines the URL and the filename is fixed. A test dropped into that directory is named something else, and something else is not a route, so it is inert. Colocation there is safe in exactly the way I assumed it was safe in suede-geo.

The risk belongs to the bare api/ directory convention, where filename maps to path with no other gate. Those are two different deploy conventions living in one estate under folder names that read alike, and I mixed them up because api/ looked like api/ from a distance. The App Router repos in the estate are the Suede-AI-App frontend, landing-site, and suede-home, plus strumly, suede-muse, and suede-agent-studio. In those, only a file named route.ts becomes a route, so a colocated test there is safe. The risk belongs to the bare api/ convention alone.

Curl production for every basename in api/

Everything above is a file-tree exercise, and the file tree is not the thing serving traffic. It is a good proxy right up until it is stale, which is the case that matters: a route deployed from a branch that no longer exists, a file deleted locally but present in the last production build, an alias pinned to an older deployment that still carries a route the current tree does not have. I have been bitten by that last one before on ip.suedeai.ai, which sat alias-pinned to an ancient deployment on 2026-06-10 while newly deployed email routes were absent from the domain. vercel alias set fixed the pin. The lesson generalizes: what the repo says is deployed and what the domain is serving are two claims, and only one of them is checked by looking at files.

the standing verification is a request against production for every basename in api/, and the expected result for anything that is not a real endpoint is 404.

# for each file in the Root Directory's api/ folder
curl -s -o /dev/null -w '%{http_code}' \
https://<domain>/api/<basename>

The shape is a status-code-only request per candidate URL, discarding the body. A 404 means the platform is not routing that name. Any 2xx on a name you did not intend to publish is the finding, and you have it in the form that matters: the production response, not an inference from the repo.

That distinction is the part I want to carry out of this incident, because it keeps recurring in my own logs under different costumes. The convenient layer answers fast and it answers about the wrong thing.

On 2026-07-16 I found Agent Studio’s production environment variables pulling as empty strings, which sent the serverless runtime down a silent fallback into a non-durable SQLite repository. The config intent said Supabase. The deployment said something else, and no error was raised to tell me. It took curl against production to establish which datastore was serving. Earlier, on 2026-06-11, that same surface had been recorded as verified live on the strength of a gateway 200 and a 402 challenge with a real payTo. Five weeks later the Supabase runtime turned out never to have been implemented. My source does not resolve which of those two records is wrong: either the June checks passed against the SQLite fallback, since 200s and 402 challenges return either way, or the production environment regressed between the dates. The endpoint was green in both readings. A green endpoint does not identify the backend that served it.

On 2026-07-18 a duplicate articlePath declaration in magazine.ts, introduced by a rebase auto-merge with no conflict markers, got caught by tsc --noEmit and fixed with an editor change. Every check I ran afterward came back clean, because every check read the working tree. The fix was never committed before the force-push and the merge. Production builds then failed on every deploy with Module parse failed: Identifier 'articlePath' has already been declared until hotfix PR #673 landed. The site itself stayed up, since Vercel does not promote a failed build, and I only caught it by checking deploy status after the merge rather than by trusting the green local run.

On 2026-06-08, auditing which repos carried the preview-skip ignoreCommand, my detection tooling lied by omission. GitHub code search lags and skips repos. Rapid recursive tree calls hit secondary rate limits and return empty rather than an error. Root-only contents checks miss nested configs in web/ and frontend/. An audit tool that fails by returning nothing hands you a clean bill of health for a broken estate. The working method was a per-repo authoritative git-tree sweep, throttled with a sleep between calls, checking .truncated on each response.

The api/ incident is the same failure with a different proxy. The repo is not the deploy. The deploy is not the domain. The domain answers when you ask it.

A test artifact that was wrong as a deployed object

One more entry belongs next to this, from 2026-07-16, because it is the same species at a smaller scale and it cost real money rather than compute. I was paying $20 a month for Pinata’s Picnic plan with no functional need for it. The reason was a single leftover 188-byte test metadata record that pushed my pinned-file count to 501, one file over the 500-file Free-tier cap. Unpinning that record put the account back under the ceiling, with the downgrade to Free scheduled to take effect 2026-07-24.

A 188-byte test artifact bought a paid plan. A colocated test file became an unauthenticated production endpoint that executed a suite and held a function open. In both cases the artifact was correct as a test and wrong as a deployed object, and in both cases the deploy or upload path had no concept of the difference. The platform is not going to make that distinction on my behalf. The exclusion has to be written down in the repo, and the check has to run against production.

The full standing discipline, as it now reads:

  1. A test colocated next to an api/ handler is excluded from the deploy, via .vercelignore at the Root Directory or by moving it out of api/.
  2. On any Vercel repo with a bare api/ folder, run ls <root>/api/ and read every filename as a public URL. Look for tests, fixtures, scratch handlers, and *.bak. Underscore-prefixed names are not routed and do not need action.
  3. Verify against production and not the file tree: one status-code request per basename in api/, expecting 404 for anything that is not a real endpoint.
  4. Re-run steps 2 and 3 when files are added to suede-geo/site, suede-scan, fretpulse, suede-x402-acp, suede-promo, or suedeai-org, and on any new repo that adopts the bare api/ layout.

Next.js App Router repos are excluded from all four. Only route.ts becomes a route there, and a test file in that tree stays a test file.