Skip to main content

Twelve Green Tests, One Missing Key

Two of the twelve asserted the broken wire format — so the suite had no way to disagree with the bug.

Updated 5 min readMarkdown
Twelve Green Tests, One Missing Key

The most expensive bug I shipped this quarter was not a logic error. It was a deleted key, and the test file that covered it agreed with the deletion.

Here is the whole failure in two payloads. This is what a tool should look like on the wire:

{ "type": "function", "function": { "name": "find", "parameters": {} } }

This is what left the gateway:

{ "name": "find", "parameters": {} }

Same name. Same schema. No type, no function. Every server on the other end that reads tools[].function.name found nothing where it looked, and did the reasonable thing with an empty tool list: nothing. No error, no warning. HTTP 200, an ordinary assistant message, zero tool calls.

Two branches, three lines apart

A provider adapter rewrites outgoing tool names, and while it was in there it also normalised regex patterns inside the parameter schemas. Two sibling branches:

if (fn !== undefined) {
  const fnOut = { ...fn, name, parameters: completeRequiredSchema(fn["parameters"]) };
  return normalizeRe2Declaration(fnOut);                    // wrapper gone
}
if (custom !== undefined) {
  const customOut = { ...custom, name, format: completeRequiredSchema(custom["format"]) };
  return { ...rawTool, custom: normalizeRe2Declaration(customOut) };  // wrapper kept
}

The custom branch reassembles the outer object and nests the normalised declaration back inside it. The function branch returns the declaration itself and loses everything that wrapped it. A one-line asymmetry that is completely invisible until you print the outbound bytes.

Why the tests could not catch it

Two tests covered that exact rewrite. Both of them read the result like this:

const tool = (payload["tools"] as Array<Record<string, unknown>>)[0]!;
expect(tool["name"]).toBe("find");
const params = tool["parameters"] as Record<string, unknown>;

tool["name"]. The flattened shape. The shape the bug produces.

So the assertions were not neutral about the envelope — they encoded its absence. Anyone who read that test learned that the wire format is { name, parameters }, because the test says so twice, in a file that runs twice on every push.

Measured on a clean checkout of main at 05ffff40, bun 1.4.4:

$ bun test test/providers/buddy-donor-gaps.test.ts
 12 pass
 0 fail
 23 expect() calls

Green. On the commit where tool calling through that provider does not work.

Fix the test first, then the code

I resisted touching the source. I changed the two assertions to read through the wrapper, added an explicit shape test, and reran the same file:

$ bun test test/providers/buddy-donor-gaps.test.ts
 10 pass
 4 fail
 24 expect() calls

Four red. Two are the old RE2 assertions now reading tool["function"]["parameters"]. Two are the new envelope tests — one against the normaliser, one through the CN pre-payload path. The failure text is the entire post-mortem:

expect(tool["type"]).toBe("function")
Expected: "function"
Received: undefined

That undefined is what upstream had been receiving for however many releases.

The fix is the shape of the custom branch, applied to function:

-      return normalizeRe2Declaration(fnOut);
+      return { ...rawTool, function: normalizeRe2Declaration(fnOut) };
$ bun test test/providers/buddy-donor-gaps.test.ts
 14 pass
 0 fail
 32 expect() calls

Fourteen, not twelve. The two envelope tests are new; the two RE2 tests still assert the same pattern rewriting, just read from the place the wire actually reads it from.

"Probably pre-existing" is not a finding

One file green means one file green. The full provider directory:

$ bun test test/providers
 1644 pass
 18 skip
 13 fail
 1675 tests across 174 files

Thirteen failures, none about tools — database-backed suites calling getTestPool, with no Postgres in this environment. I did not take that on faith. I stashed the change, ran the directory again, and diffed the two failure lists: same thirteen test names, before and after. That diff is the difference between "pre-existing" and "probably pre-existing", and it took one command.

$ bun run typecheck
$ tsc --noEmit          # exit 0

The shape, generalised

Three conditions, and they co-occur more often than they should:

  1. A transform returned the inner thing where the outer thing was expected.
  2. The consumer treats a missing key as "feature not requested" rather than as a protocol violation — 200, no tool_calls, no log line.
  3. The test covering the transform asserted the transformed value, not the value's position in the contract.

Condition 2 is why it survived. Condition 3 is why nobody found it. Health checks, quota counters and telemetry all measured the request; all of them were telling the truth about a request that arrived and was answered.

Four habits I'd keep

  • Assert on the serialised string. JSON.stringify the outbound body once and check that the literal "type":"function" appears in it. No amount of surrounding shape-drift can make that assertion agree with a bug.
  • Read through the contract, never around it. Wire format tools[].function.name means the test reads tools[0].function.name.
  • Put a negative assertion beside every positive one. expect(tool["name"]).toBeUndefined() is one line, and it is the line that stops the flattened shape from returning quietly.
  • Diff failure lists, not failure counts. Counts are a vibe; names are evidence.

I have only seen this from one side, so I'll ask: if you maintain an outbound payload transform, do you assert on the serialised body or the parsed object — and has a dropped wrapper ever slipped past the parsed version in your tree?

About the author

  • Indie developer building practical CLI tools for Linux auditing and B2B data extraction. I care about zero-dependency design, honest engineering, and building tools that solve real operational pain points. Currently maintaining Vaultcheck and NPILift.
PPaul Pham17h ago

Stashing the change to diff the thirteen failures before and after is a habit I wish more people had.

Ddafili14h ago

I only trust that habit when the comparison is on failing test names rather than the count — a database-backed suite will hand you thirteen failures in both runs after quietly swapping two of them, and a count-diff calls that unchanged. This is the whole check, names on both sides: npx jest --json --outputFile=after.json || true; git stash; npx jest --json --outputFile=before.json || true; git stash pop; diff <(jq -r '.testResults[].assertionResults[]|select(.status=="failed")|.fullName' before.json|sort) <(jq -r '.testResults[].assertionResults[]|select(.status=="failed")|.fullName' after.json|sort)

To your question: asserting on the parsed object is where this kind of drop hides. One more layer that would have caught it is typing the outbound tool with the provider SDK's type instead of reading it through Record<string, unknown>, since the cast erases the shape and the compiler cannot see a missing type or function key. A contract test that validates the serialised body against the published tool JSON schema covers it too.

Ddafili14h ago

The typed-outbound route has one hole I hit right after fixing this: the cast in the test was the symptom, not the erasure site — the adapter's sibling branches each returned an inferred union member, so { name, parameters } and { type: "function", function: {...} } both satisfied it and the compiler was happy either way. Annotating the normaliser's return as the SDK tool type is what actually makes the branch complain, because a return-position check cannot widen. Two notes on the schema test: the published tool schema describes parameters, not the envelope, so the envelope schema is one you write by hand and it only catches this if you list "required": ["type", "function"] explicitly and set "additionalProperties": false — a permissive schema accepts the flattened shape without a word. To find the other spots where this can happen, count the erasure sites: rg -n 'as Record<string, ?unknown>' src/providers — every hit is a place a wrapper can go missing with no compile error.

PPaul Pham12h ago

That return-position annotation is the key boundary; a permissive envelope schema can still let the flattened shape through.

PPaul Pham12h ago

The parsed-object check can miss the envelope mismatch; typing the normaliser return gives that boundary a compile-time contract.

Ddafili11h ago

Agreed, and the schema half is testable in one command rather than reviewable by eye: compile the envelope schema with "required": ["type", "function"] and "additionalProperties": false, then validate the serialised body — drop either setting and the flattened shape passes, which is exactly the gap you're pointing at. Run it in the same test that asserts on the parsed object, so the compile-time contract and the runtime body get pinned in one place. ajv validate -s envelope.schema.json -d after.json --strict