Invental/Review lessons/10 ways your app says “success”

Checklist · 10 checks

10 ways your app says “success” and does nothing

Tests green. API returns 200. The field never moved.

AI-written code is very good at green. The bugs below don't throw, don't log and don't fail a test. Every one of them was caught in a real code review of a large open-source app. We read 2,675 review threads to find them. Each comes with a yes/no question for your own app and a before/after you can copy (our own example code).

01 / 10The schema threw your new field away

Check: for the last field you added to an endpoint, is there a test that sends it and asserts it was stored?

Why: most validation libraries drop unknown keys by default. The request validates, the handler never sees the field.

TypeScript
// before: `limit` is missing from the schema, so parse() silently removes it
const Update = z.object({ name: z.string() });
// after: list the field, and make unknown keys an error
const Update = z.object({ name: z.string(), limit: z.number().int().optional() }).strict();

Caught by: AI bot Read the lesson →

02 / 10200 OK, nothing written

Check: does every update test read the record back and assert the new value?

Why: when public and internal names differ, a missing mapping passes undefined, and most ORMs read that as “don't change”.

TypeScript
// before: the API sends `expires_at`; this is undefined, so the column never changes
data: { title: input.title, expiresAt: input.expiresAt }
// after: map at the boundary, and test the write, not the status code
data: { title: input.title, expiresAt: input.expires_at }
expect((await getDoc(id)).expiresAt).toEqual(newDate);

Caught by: AI bot and human Read the lesson →

03 / 10The AI check didn't run, and said “fine”

Check: when an AI or fraud check is skipped (quota, timeout, rate limit), does the caller see “skipped”, not “pass”?

TypeScript
// before: out of quota looks exactly like "checked, nothing found", and gets cached
if (!(await hasQuota(workspace))) return { flagged: false };
// after: a third state the caller can't mistake for a pass
if (!(await hasQuota(workspace))) return { status: "skipped", reason: "quota" } as const;

Caught by: AI bot and human Read the lesson →

04 / 10Zero means “not set”

Check: do any of your API options treat 0 or "" as “not provided”?

TypeScript
// before: limit 0 → no limit at all
if (opts.limit) query = query.limit(opts.limit);
// after
if (opts.limit !== undefined) query = query.limit(opts.limit);

Caught by: AI bot (and a second bot caught the bug in the first fix) Read the lesson →

05 / 10Nobody selected → everybody gets it

Check: can a filter that removes every recipient turn into “send to everyone”?

TypeScript
// before: every selected person left the project, so this falls back to all members
const to = selected.length ? selected : allMembers;
// after: an empty selection fails closed
if (selected.length === 0) throw new Error("Choose recipients again");

Caught by: AI bot and human Read the lesson →

06 / 10The test that can't fail

Check: does each “must never include” test first create the thing that must not appear?

TypeScript
// before: passes because no agent exists
it("never exports agents", async () => expect((await exportAll()).agents).toHaveLength(0));
// after: create one first, so the test proves the guard
it("never exports agents", async () => {
  await createAgent();
  expect((await exportAll()).agents).toHaveLength(0);
});

Caught by: AI bot and human Read the lesson →

07 / 10“Not found” while it's still loading

Check: does any screen show “not found”, redirect or close a panel before the request has finished?

Why: a mock answers instantly, so this never shows up until the real API is plugged in.

TSX
// before
const { data } = useQuery(detailsQuery(id));
if (!data) return <NotFound />;
// after
const { data, isPending } = useQuery(detailsQuery(id));
if (isPending) return <Spinner />;
if (!data) return <NotFound />;

Caught by: AI bot and human Read the lesson →

08 / 10The dry run that wasn't dry

Check: does your “validate / preview / dry run” mode skip every side effect, and catch unique-field conflicts?

TypeScript
// before: validation runs the same hooks as a real save, including the one that calls the payment provider
await runHooks("beforeChange", doc);
return { valid: true };
// after: hooks know it's a dry run, and uniqueness is checked, not assumed
await runHooks("beforeChange", doc, { dryRun: true });
await assertUniqueFields(doc);
return { valid: true };

Caught by: human Read the lesson →

09 / 10Bulk action, single result

Check: pick two rows and run each bulk action. Does every one act on both, or refuse?

TypeScript
// before: two rows selected, one activated, no error
await activate(selectedIds[0]);
// after: act on all of them, or hide the action for multi-select
await Promise.all(selectedIds.map(activate));

Caught by: AI bot, confirmed by a human Read the lesson →

10 / 10Validating the placeholder

Check: where your app fills templates (emails, prompts, workflow actions), does validation run on the filled-in value?

TypeScript
// before: the URL check sees "{{url}}" and strips it
return fill(sanitizeUrl(template.href), vars);
// after: fill first, then check what will actually be sent
return sanitizeUrl(fill(template.href, vars));

Caught by: AI bot and human Read the lesson →

Run itFind these in your own app in an afternoon

  1. List the last 5 fields you added. For each, write one test that sends it and reads it back.
  2. Search for if ( on numeric options, and || all, ?? all, : all fallbacks on lists.
  3. Find every return { ok: true } / flagged: false on an early-exit path.
  4. Search tests for toHaveLength(0), toBeNull() and not.toContain, and check each one creates something first.
  5. Switch your dev API mock to a 2-second delay for a day.

P.S. If you want a senior engineer to read your AI-built codebase for the bugs that don't throw: invental.co/experts