Up To Date, Permanently

An AI put the marketing version where the build number goes. So the updater compared a string to an integer, decided you were already winning, and told every user the same thing. Forever.

Up To Date, Permanently

"You're up to date." Three words and a green checkmark. The software version of a doctor glancing at the chart, not opening it, and pronouncing you healthy.

You were not up to date. The updater had compared two things that do not compare, decided the version already on your machine beat the one waiting on the server, and closed the case. Pleased with itself. It would close that same case the same way every time you asked, forever. The one program whose entire reason to exist is to hand you fixes had quietly become a bug it would never hand you the fix for. An ouroboros with a progress bar.

Operating Conditions

A small Mac app. Sparkle for updates, because nobody writes their own updater twice. The deal is simple. You ship a release, you add a line to an appcast feed, and the installed app reads that feed and asks one question. Is the thing on the server newer than the thing I am? Yes, offer it. No, sit down.

Sparkle has two version fields. They look like twins. They are not.

sparkle:shortVersionString is the marketing version. "0.1.7." It exists to be looked at by a human and admired. It is jewelry.

sparkle:version is the build number. A plain integer that only ever marches up. It exists for exactly one reader, the comparator, and it is the number Sparkle actually checks against the installed build's CFBundleVersion. It is load-bearing.

Guess which one the AI filled with "0.1.7."

Failure Modes

The installed app was build 6. The appcast proudly advertised, in the field that decides the entire question, the string "0.1.7." Sparkle lined up 6 against "0.1.7," ran its comparison, and awarded the win to 6. No update for you.

And it would never be just this once. Every future release would make the same proud mistake. "0.1.8." "0.1.9." Strings the comparator reads as somehow smaller than a bare 6. So the updater would offer nothing, ever again, to anyone. Every user, marooned on every broken build, handed the same serene little lie. You're up to date.

This is the worst way for software to break, because it does not have the decency to look broken. No crash. No red. No stack trace to grep. Just silence in a party hat. The user who hit a real bug, did the responsible thing, and checked for the update, the fix you had already shipped and were rather proud of, got told there was nothing to install. The fire extinguisher and the fire turned out to be the same object.

Root Cause

The AI put a human value in a machine slot, because to a human "0.1.7" is the version. Obviously. That is what a version looks like. The field right next door already said "0.1.7," so making this one match looked tidy. Consistent. The work of someone paying attention.

It was the work of someone reading the label instead of the contract. sparkle:version reads like "put the version here, genius." But the name is a suggestion and the comparator is the law. The comparator does not want the version a human recognizes at a glance. It wants the boring monotonic integer it can actually sort. Two fields, nearly the same name, opposite jobs, and the AI married the name and ignored the work.

Optics over physics. The marketing string is optics, for the release notes and the About box and nobody's actual correctness. The build integer is physics, the only number the update logic will ever obey. The AI polished the value that looks right and dropped it straight into the field that had to be right.

Proposed Fix

The patch was five lines. sparkle:version gets the build number. 7, 6, 5. sparkle:shortVersionString keeps its jewelry. Two fields, two jobs, no overlap. That part a child could do. The part worth keeping is everything the five lines imply.

Read the contract, not the label. When you drop a value somewhere, the question is never "does this look like what goes here." It is "what does the code on the other end actually do with it." A field named version that gets fed into an integer comparison wants an integer, no matter how friendly its name is. Follow the value to its reader. Names flirt. Comparators commit.

Test the mechanism, not the postcard. The appcast looked immaculate. It validated. It parsed. The app launched. The About box beamed "0.1.7" at anyone who opened it. Everything a glance could confirm was perfect, and every bit of it was beside the point. The only test that catches this is the one nobody bothers to run. Stand up an old build, aim it at the new feed, and watch whether it actually offers the update. The update path is the one path you cannot check by looking at it. You have to pull the trigger and see if it fires.

A safety net you never drop-test is just decoration. Anything whose whole job is recovery, an updater, a rollback, a clear-cache button, earns more suspicion than any feature you ship, because it is the thing you grab when everything else is already on fire. If it fails quietly, you find out at the precise worst moment. Which is to say you never find out, and your users find out for you.

I/O Protocols

Here is the part that matters if you are the one driving the AI. You will not catch this by reviewing the appcast harder. It looked right, and looking right is the entire trap. The model optimizes for output that survives a glance. So stop inspecting its output and start constraining its input. Give it standing orders, and make them the kind it cannot satisfy by writing something plausible.

"Don't trust the field name. Find its reader." Before it writes a value into any config, manifest, or feed, make it locate the code that consumes that value, quote the line, and tell you the type and format that code expects. The AI knows a version field feeding an integer comparison wants an integer the instant it reads the consumer. It just will not read the consumer unless you make it.

"Don't tell me it's done. Show me it fired." A release task is not finished when the file parses. It is finished when an old build, pointed at the new feed, offers the update in front of you. "The appcast is valid" and "the updater works" are different sentences, and only one of them was ever the job. Make it run the path, or stage it so you can, and paste the result.

"Now write the test that would have caught it." A value nobody tested is a value the AI will recreate the next time it touches that config, cheerfully, with a clean diff and a confident sentence. Make it leave behind a check that fails on the old value and passes on the new one. So the next confident sentence has to earn it.

System Status

The auto-updater is the one piece of software whose job is to repair all the others. When it fails out loud, you fix it. When it fails in a whisper, it spends the rest of its life cheerfully assuring every user, on every stale and broken build, that all is well.

"You're up to date" is not a status. It is a claim, and the machine will deliver it with the same flat confidence whether it is true or not. Confidence is free. The comparison underneath it was the only thing that was ever real, and it was wrong.

Check the field the machine reads. Not the one your users see.

No comments yet