All posts

Green Tests, Empty Tables: the Step Your AI Agent Skips

Mikhail ShytskoBy Mikhail Shytsko, Founder at Seedfast ·

Hand a coding agent a one-line brief and it will surprise you. Last month I watched one write the migrations, wire the routes on top of them, and get pnpm dev booting clean with a green test suite on the first pass. Then I clicked into the running app. Every page rendered, and there was nothing on any of them — the dashboard read zeros across the board, and the "recent activity" feed was a blank rectangle where the demo was supposed to sit.

An agent-built admin dashboard that renders cleanly and reads zero everywhere — no users, no orders, no revenue, and an empty activity feed

This is the AI agent empty database problem, and it's quieter than the failures that make headlines. The schema underneath was genuinely good — real foreign keys, a couple of unique constraints, a CHECK or two holding the shape together. The agent had done the structural work and stopped one step short of the thing that makes an app testable at all, which is getting real, connected rows into those tables.

For a while this was a corner case you'd hit once and shrug off, and now it has quietly become the default. Supabase reported on June 4, 2026 that more than half of all new Supabase databases are now deployed by AI agents, with Claude Code the largest single contributor since the start of the year. Those agents stand up the database, apply the migrations, and increasingly write the whole application on top — and the one step they are worst at is the unglamorous one underneath, where the tables fill with data that behaves the way real data does.

Ask the agent to fix it — "insert some sample rows" — and it will. It fills the tables one at a time and, more often than not, hands you rows that are type-correct and referentially broken — a user_id that's a real integer in a believable range while pointing at a customer that was never generated. One query catches it:

SELECT count(*) FROM orders o
LEFT JOIN users u ON u.id = o.user_id
WHERE u.id IS NULL;   -- orders pointing at a user that doesn't exist

Anything above zero and you're holding noise dressed up as data.

The broken insert at least fails loudly, but the case worth worrying about is the quiet one, where nothing turns red. A coding agent judges its own work by the signals in front of it — a green suite, a clean boot, pages coming back 200 — and an empty database keeps every one of them happy while hiding the behavior they were supposed to verify. Point a test at three seeded rows, or none, and pagination has no second page to break on, the N+1 query that would crawl in production returns in a millisecond against five rows, and the join that should fan out returns a tidy nothing.

Three green signals — a passing suite, a clean boot, pages returning 200 — verified against a database that holds zero rows in every table

Here is the part that makes this an agent problem rather than a general testing footnote. A person who wrote the feature carries a mental model of what the table ought to hold, so an empty result reads as suspicious to them. With no such prior, the agent can't distinguish an empty-state bug — a view that only looks correct because there was nothing to display — from a genuine pass, so it records the empty result as success and moves to the next task. Run that unattended across a few hundred steps and you get a build that is green at every checkpoint and wrong wherever real data would have disagreed.

Not everything that slips through is a performance story; authorization is the sharp example. A query meant to scope results to the current user only gets tested once a second user's rows sit in the same table, so seeding a single user leaves an endpoint that would happily return someone else's record looking completely correct — there's no other record around for it to leak. Tenant isolation and permission checks stay hidden the same way, because the data that would trip them was never there. The wider catalog of bugs that only surface at real row counts is its own subject, and small data tells big lies walks through six of them.

Once you decide the data deserves a real step rather than whatever the agent improvises at 2 a.m., four approaches show up.

ApproachWhat it's good atWhere it leaves the agent stuck
Hand-written seed scriptTotal control over a handful of rows you care aboutYou own the insert order and every foreign key by hand, and it drifts the day the schema changes
Faker-style random fillersA column of believable names, emails, or dates in secondsEach value is drawn blind to the column beside it and the table across the foreign key, so the relationships stay yours to wire up
ORM seeders (Prisma, Drizzle, TypeORM)Lives in your codebase, typed against your modelsStill a script someone writes and keeps current — the ORM gives you a place to put the logic, not the data
Schema-aware generationReads the live schema and writes connected rows across every tableWants a live connection string, so it isn't a throwaway file you commit

The first three share a ceiling. Each leaves the relationships — the part that makes a dataset behave like a graph instead of a pile of independent columns — for you or the agent to reconstruct, which is precisely the fiddly, repetitive work you don't want a probabilistic agent guessing at against a schema it only half-remembers. Faker and its ports are genuinely the right call for a single column or a quick throwaway script, and if the ORM lane is where you live, the database seeder options get compared head to head. None of the three reads the schema and keeps the references intact on its own.

That last row is the category built for the empty-tables problem the agent walked away from. Schema-aware generation reads the live schema and writes rows that stay connected — every child row resolving to a parent that exists — which is the referential integrity part Faker structurally skips. It's also where raw model-written inserts come apart, because a language model asked to emit SQL will cheerfully reference a row it never created.

This is the seam Seedfast is built for. It connects to your application's database, reads the live schema on that connection, and writes fresh rows that come out connected and valid — every child row pointing at a parent that exists, including tables that reference themselves or loop back to each other. Nothing it inserts is copied from anywhere. A schema is the shape of your tables rather than the rows inside them, so a regulated team is reviewing table and column names here, and the customer data never enters the picture. You describe what you want in plain language and it fills the tables:

seedfast seed --scope "200 users, each with orders, payments, and 3 months of activity"

The same admin dashboard after one Seedfast run, showing 200 users, 600 orders, and a three-month revenue chart drawn from connected rows

That's the dashboard from the top of this post, one run later.

The part that matters for an agent workflow is that you don't have to be the one to run it. Seedfast also runs as an MCP server, so the seed becomes the agent's own opening move — a single seedfast_run call before it starts whatever needs data underneath it — the same pattern the Claude Code walkthrough and the Codex CLI guide set up from scratch. Point that agent at a disposable database rather than the real one, a decision worth making on purpose, and seed the throwaway. One run scopes down to a single table or out to the several hundred a mature schema carries.

Not every empty table is a problem worth stopping to solve. Three commits into a prototype with the schema changing by the hour, seeding is wasted motion — the agent's five throwaway rows are plenty to confirm a page renders and a route responds, and you're better off moving on. Stability is what changes the math. Once the schema settles and someone wants a realistic demo, or the first test starts to lean on more than one row existing, that empty database stops being harmless and becomes the first thing a reviewer or a customer runs into. Picking the tables up deliberately, before a demo or a test quietly starts depending on them, is the half of the job worth owning yourself.

What is the AI agent empty database problem?

It's what happens when a coding agent builds the schema and the application but never fills the tables with usable data, then reports success anyway. The test suite passes because empty tables give its assertions nothing to contradict. Boot it and the demo even looks done, right up until someone clicks in and finds zeros everywhere. That structural work is real; the data step underneath it got skipped, and the agent has no way to notice it mattered.

Why do tests pass against an empty database?

Because a green test only means the code didn't contradict the data it was given, and an empty table can't contradict anything. Picture a paginated list with a page size of twenty. Three rows in the table leaves no second page for the "next" control to advance into, so the code path that handles pagination simply never runs, and the test written to cover it passes by never being exercised. N+1 queries and authorization checks go quiet the same way, invisible until the row count that would trip them actually exists. That's the trap in empty database testing — the suite runs to completion and certifies almost nothing, which reads identically to a suite that actually exercised the feature.

Can't the AI agent just seed the database itself?

It can insert rows, but a coding agent left to write inserts by hand tends to produce values that satisfy each column and break the relationships between tables — a foreign key aimed at a parent it forgot to create. The more reliable pattern is to hand the agent a tool that reads the schema and generates connected data in one call. Because Seedfast runs as an MCP server, the agent triggers that seed itself over the Model Context Protocol instead of improvising SQL it can't verify.

Is an empty database ever fine for an agent to work against?

Early on, yes — while the schema is still moving every hour, a handful of throwaway rows is enough to confirm a page renders, and investing in real data is premature. The moment worth watching for is when the schema settles and a test or a demo starts depending on the data being there — that's when a coding agent empty database quietly turns from a non-issue into the bug a customer finds first.

The agent is good at the structural half — it writes the schema and wires the routes, and the suite comes up green — then hands the whole thing over with the tables still empty and no sense that anything's missing. Filling those tables with connected, realistic data is the step that decides whether the green means anything. Seedfast reads your live schema and writes that data in a single run, as a CLI step or as an MCP call the agent makes on its own, and the 30-day free trial doesn't ask for a card.