{"slug": "unit-tests-passed-the-feature-never-ran-three-times-in-one-session", "title": "Unit Tests Passed. The Feature Never Ran. Three Times in One Session.", "summary": "A developer reported that three features shipped in a single session with all unit tests passing, yet none ever executed in the live app due to missing argument wiring. The incidents occurred in a solo-built SwiftUI iOS app that scores restaurant reviews, where LLM subagents implemented features but failed to connect them to production call sites. Despite diagnosing the first incident and adding a dedicated test file, the same pattern recurred twice more, highlighting that tests cannot catch integration gaps.", "body_md": "*Originally published on hexisteme notes.*\n\nThree features shipped in one session. All three had dedicated unit tests. All the tests passed. None of the three ever ran in the live app.\n\nNot \"had a bug.\" Not \"worked but looked wrong.\" Never executed — the argument that would have activated each feature was never populated on any live path. The app fell back to its default rendering, crashed nowhere, logged nothing.\n\nThe part worth writing down isn't the first incident. It's the second and third, which happened *after* I had diagnosed the first and added a dedicated test file for this exact failure mode. Knowing the pattern did not stop the pattern — so the fix cannot be vigilance.\n\nSetting: a solo-built SwiftUI iOS app that scores restaurant reviews for trustworthiness, with implementation fanned out to LLM subagents that each own a slice of files.\n\nThe first feature was a handoff row — buttons that take you from the app to a phone dialer or a maps app. The URL construction lived in a pure function, `HandoffRow.urls()`\n\n, written that way on purpose; a comment in the source declares it testable. Eight dedicated tests, all passing: whether coordinates are present, whether the target app is installed, percent-encoding of the business name, the precedence rule for using a place identifier over a name search.\n\nIn production those arguments were never populated. The wiring broke in two places.\n\nFirst, a merge function. The live app composes two review sources, and that merge is hand-written — it copies fields one explicit line at a time. Three new fields had been added upstream (a place identifier, a phone number, a maps URI). None were added to the copy list. It compiled fine, because dropping a field during an explicit field-by-field copy is not a type error. It is just... not copying.\n\nSecond, the call sites. Four view call sites construct this row. Two — the two on the live path — didn't pass the new arguments at all.\n\nSo the phone button never rendered, in any state, ever, and the maps deep link always degraded to a plain name search. No crash, no error, tests green. The tests didn't find it; an adversarial code review did, run with three lenses, two of which flagged it independently.\n\nSame session. I added a new axis to the verdict: how long the restaurant has been operating, benchmarked against how long comparable businesses survive. The deliverables:\n\nZero of the five production call sites passed the `tenure:`\n\nargument. Grepping for the type constructor found every construction site in the app inside a `#Preview`\n\nblock — three of them. The feature existed exclusively in the preview canvas.\n\nThe most instructive call site was the map-pin lookup path. That function already held both values the new axis needed — the license date and the category — in local variables a few lines above the call. It just didn't pass them.\n\nSo the pipeline, the table, the types, the view, and fifteen green tests all existed, and the feature rendered zero pixels on a real device. *After* I had diagnosed round one and written a dedicated test file for it.\n\nSame session, same workflow. I wired restaurant photos in from a places API. The verdict object — the thing that carries the analysis from the logic layer to the view layer — didn't carry the photo fields, so the hero image fell back to a stock illustration on every live path.\n\nThe detail that stings: the subagent that did this work wrote *\"backend not wired\"* in its own completion report. It was honest. It said the thing. That report was one of several returning at once, and the line got buried.\n\nThree for three, on a pattern I already knew.\n\nA pure-function test looks like this:\n\n``` js\nlet destinations = HandoffRow.urls(placeName: \"...\", googlePlaceId: \"ChIJabc\", ...)\nXCTAssertEqual(destinations.google.absoluteString, \"...query_place_id=ChIJabc\")\n```\n\nThe proposition it verifies: **does the function handle the argument correctly?**\n\nThe proposition it does not verify: **does that argument ever arrive?**\n\nThe second is out of reach for as long as the test supplies the argument itself. That isn't a gap in my suite — it's what isolation *means*. More tests, or better ones, hit the same wall, because every one constructs the input by hand, which is exactly the step production was skipping. Coverage doesn't rescue it either: coverage counts lines that executed, and the failure here is a call site that *does* execute and passes nothing.\n\nSo the category matters more than the three bugs. Unit tests validate components in isolation, which makes every state where components are *not connected to each other* invisible to them. Here that was unpassed arguments and a lossy merge. Elsewhere it's the handler written but never registered, the route that exists but was never mounted, the feature flag defaulting to off, the DI binding that never made it into the container. Different surface, same blind spot. All of them go green.\n\nAll three had the same thing at the root: to add a new field without breaking existing call sites, I gave it a default.\n\n``` js\nvar googlePlaceId: String? = nil\nstatic func read(from: FetchedSignals, tenure: TenureRecord? = nil) -> TransparencyRead\n```\n\nThat single `= nil`\n\nis the whole story. Without it, the compiler lists every call site needing an update, immediately, as errors. With it, the compiler is satisfied and the failure relocates to runtime, where it manifests as an absence — and absences don't throw.\n\nThe uncomfortable part: **the default almost always looks like the correct call at the moment you make it.** It's the textbook incremental migration, and often the only way to add a field without breaking a file somebody else is editing right now. You reach for it because it's the professional move, and it quietly trades a compile-time guarantee for a runtime nothing.\n\nThis predates LLMs, but delegating implementation to a code generator makes it structurally more likely.\n\nScoping. A generator builds the component you asked for. \"Add a tenure axis\" yields a pipeline, types, a view, and tests — all self-contained, all good. Connecting it to the five places that should call it was never in the request, and a component that compiles with passing tests looks finished from the inside. The generator produces the thing; it does not produce the thing's callers.\n\nFile ownership. Parallel delegation only works if you split files between workers, or they collide on edits. So the instruction becomes \"don't touch files you don't own\" — which, followed faithfully, becomes \"give the new parameter a default so files you don't own keep compiling.\" Wiring is precisely the work that crosses ownership boundaries, which is the work nobody was assigned.\n\nEvery worker did its job correctly. Nobody lied. Round three's worker reported the gap explicitly. And the feature was dead. That's a structural outcome, not a diligence problem, and it recurs until you change the structure.\n\nStop hand-passing the data. Instead of threading new values through view properties at each call site, load them onto an object that already flows to the consumer — here, the verdict itself. It travels with the verdict, so a new call site cannot fail to bring it.\n\n``` php\n// Before: hand-passed at every call site -> 2 of 4 forgot\nTransparencyReadView(read: read, googlePlaceId: ..., phoneNumber: ..., googleMapsUri: ...)\n\n// After: the verdict carries it -> the call site has nothing to forget\nTransparencyReadView(read: read)\n```\n\nThe principle isn't \"remember to pass it.\" It's **make forgetting inexpressible.** A rule that depends on remembering had already failed three times inside this one story.\n\nA separate file whose only job is asserting the *assembly path* — not the functions, the connections between them. Three assertion points:\n\nIt's a separate file because it answers a different question and would otherwise get deleted as redundant — so a comment at the top explains why it exists, for the next person doing a cleanup pass.\n\nAdversarial code review, every time. Three reviewers with different lenses — regression risk, contract integrity, honesty of reporting — run independently, converging on the same finding.\n\nOne line in the review instruction did most of the work:\n\nTrace new features to their call sites and verify they actually receive values on the live path.\n\nIn round one that line wasn't there, and the reviewer saw eight passing tests and rated the item low severity — a reasonable read of the evidence in front of it. The instruction is what changes the evidence a reviewer goes and collects. You don't get call-site tracing by hoping the reviewer is thorough; you get it by asking for it by name.\n\nLanguage-agnostic. Applies anywhere default arguments and optional fields exist.\n\n`merge`\n\n, `combine`\n\n, `reduce`\n\n, any hand-written field-by-field copy — get no compiler help when a field is added. Add a field, grep the merges first.One project, one developer, one session. A case study, not a statistic. The review caught all three this time, which is not evidence it catches everything — I have no way to count what it missed.\n\n\"Put the data on the domain object\" isn't universally right; the counter-cost is that the object grows. It was cheap here because that object was already a display-oriented container carrying a dozen derived values, and never gets persisted. If it were a persisted model, adding fields for view convenience would be a schema decision, and I'd have chosen differently.\n\nTwo conditions would tell me I'm wrong. If a wiring gap of the same shape recurs *despite* the pass-through test, the answer isn't more test placement — it's applying the structural fix more broadly. And if six months pass with no recurrence, that only counts if new fields with defaults were actually added in that window. No incidents can mean no attempts.\n\n*More notes at hexisteme.github.io/notes.*", "url": "https://wpnews.pro/news/unit-tests-passed-the-feature-never-ran-three-times-in-one-session", "canonical_source": "https://dev.to/hexisteme/unit-tests-passed-the-feature-never-ran-three-times-in-one-session-1f50", "published_at": "2026-08-01 00:00:08+00:00", "updated_at": "2026-08-01 00:11:58.490037+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence"], "entities": ["SwiftUI", "iOS", "LLM"], "alternates": {"html": "https://wpnews.pro/news/unit-tests-passed-the-feature-never-ran-three-times-in-one-session", "markdown": "https://wpnews.pro/news/unit-tests-passed-the-feature-never-ran-three-times-in-one-session.md", "text": "https://wpnews.pro/news/unit-tests-passed-the-feature-never-ran-three-times-in-one-session.txt", "jsonld": "https://wpnews.pro/news/unit-tests-passed-the-feature-never-ran-three-times-in-one-session.jsonld"}}