Test the two screens against each other, not the query
The hub said 64 dishes on sale, the Photos screen said 59, same venue, same minute. The fix was one import. The test now compares the two screens.
Our hub has a glance column for every kitchen in a hall: dishes on sale, dishes without a photo. For the live venue it read 64 and 16. The Photos screen, opened a minute later for the same venue, listed 59 dishes and 11 without a photo. Two screens, one menu, two sets of numbers. The hub had been telling that kitchen to photograph five dishes that are not on its menu.
The cause is the one we keep finding. Two screens asked the same question, which items can a guest be shown, and one of them asked its own copy of the rule rather than the function that holds it. This copy was wrong in both directions at once. Loose, because it had no price test, no check for a product behind the dish, and no exclusion of prep components, so a jar of burger sauce counted as something on sale. Narrow, because it dropped items shared across a whole group, which genuinely are on a venue's menu. The loose half is what showed on the live estate. The narrow half was invisible only because no live venue has group-wide items yet.
What the test checks
The fix was the import, and the hub's batched query had rules of its own to respect: one table per statement, no aliases, no interpolation, every column checked against the written-down schema. All of that still holds. The part worth writing about is the test, because it does not check the query at all.
It seeds one database, drives the hub route and the Photos route against it, and asserts that the two numbers a person can see side by side are the same number. A copy of a rule can drift quietly for months. Two routes answering differently cannot, not once something compares them. The fixture was chosen so that every count differs: three dishes on the menu, two of them unphotographed, one photographed. A fixture where the numbers happen to coincide proves nothing.
A floor the broken answer also clears
One of the four tests did not bite at first. It covered the group-wide case and asserted that the count was three or more. The old, broken code counted four for that venue: burger, wings, the box of gloves and the jar of sauce. It cleared the floor while getting both halves of the rule wrong. A threshold that the broken answer also satisfies is not a test. It now asserts equality with the Photos screen's own list.
A third test file failed as well, and this time the fixture was the fault. Its seeded recipes carried only the active flag and the visible flag, with no price, no category and no product behind them. By the product's own definition they were never menu items, and the two assertions that said one on sale and three on sale were encoding the loose rule. The fence those tests exist for was untouched; the rows are now real menu items. When a test fails after a fix, the first question is whether the test or the code is wrong, and the answer has been the test more often than not.
After the deploy
The live venue's hub column now reads 59 on sale and 11 without photos, and the Photos route answers 59 dishes and 11 without a photo. The same two numbers from the two places, where an hour earlier they were 64 and 16 beside 59 and 11.
If two screens in your system disagree about your own menu, do not pick the one you like. Until somebody has compared them, the one you trust is wrong too. The cheapest lasting fix is a check that opens both and refuses to let them differ.