π¦ Netfox 0.16.5 β the bug report that was wrong (mine)
π¦ Netfox 0.16.5 is out β and the bug I fixed wasn't the bug I filed.
Yesterday I logged this against my own app: sometimes the main window can't be reopened. Window β Netfox clicks through and does nothing. π€
Today I tried to reproduce it properly. Ten quit-and-relaunch cycles β the window came back 10/10. Close it, quit, relaunch β back. Not one failure.
So I'd been measuring the wrong thing. π΅οΈ There were two copies of Netfox running, and my check was reading the one whose window I'd closed instead of the one on screen. "Window β Netfox does nothing" because that menu belonged to the copy that already had its window open.
The window was never broken. π€¦
β οΈ But underneath the imaginary bug was a real one: two copies share the same stored device history while each keeps its own private view of it. They interfere β and on the previous version they brought each other down within a minute.
β So 0.16.5: Netfox refuses to run twice. Open it again and the copy already running comes forward; the new one steps aside before touching your data.
My favourite detail: deciding which copy stays is where the obvious answer is wrong. If each copy defers to the other one it can see, two copies launched together both politely quit and you're left with no app at all. π So every copy has to reach the same answer independently β oldest stays. I wrote the test, then broke the rule on purpose to check the test would notice. It reported zero survivors. π―
π The tally: forty lines of guard, nine corrections to get right, four of them mine and introduced while fixing the previous one.
π Free, universal, macOS 15.6+: https://netfox.app
What's the most misleading bug report you've ever written for yourself? I'll go first β mine's above. π


Replies
Publishing the wrong bug report alongside the fix is the useful half and almost nobody does it. The failure underneath yours is worth naming: your check assumed one instance, so it silently answered a different question than the one you asked. That is not a reproduction problem, it is that the measurement had an unstated precondition. Ten green runs against the wrong process look identical to ten green runs against the right one. The habit I have taken from getting this wrong repeatedly: before trusting a passing check, make it fail on purpose once. If you cannot make it go red deliberately, it was never testing what you thought. Would that have caught this one, or would forcing a failure still have hit the same stale instance?
Netfox
@rabnoor_sΒ Nope β and thats the interesting part.
Forcing it red would've worked and taught me nothing. Close the window, watch `count windows` drop to 0: perfectly correct answer from a check pointed at the wrong process. The stale instance really did have zero windows. It'd have gone red on cue and I'd have come away trusting it more.
Which is a line I hadn't drawn until you put it that way. Sensitivity and identity are different properties. Making a check fail proves it reacts to change; it doesn't prove it's watching the thing you named. It can be perfectly sensitive to the wrong object.
Your habit did earn its keep elsewhere in the same work though. The rule picking which copy survives has a test asserting exactly one does β I dropped in the naive "defer to the first other instance you can see" and got zero survivors back, which is the failure where both copies politely quit and you're left with no app running at all π . Pure sensitivity, and it's why I trust that test and not much else in there.
So imo they're sequential rather than either/or: make it fail on purpose, then ask what actually made it red. Cheap version of step two here was one line β count the processes before counting their windows.
And "unstated precondition" is the better name, mine was worse. "Wrong instrument" just tells you to distrust everything, which you can't act on. Yours tells you where to look.
@gfazioliΒ "Sensitivity and identity are different properties" is a better sentence than anything I gave you, and it is the part I had been running on instinct without naming.
Sequential is right, and I would add that step two has a cheap general form: make the check fail for a reason you chose, then check that the reason it reports matches the reason you caused. Not just that it went red, but that it went red about the right thing. Your one-liner is exactly that, counting processes before counting their windows.
The zero-survivors case is the better story though, and it is the same family as everything else we keep landing on. Both copies politely quitting is a system where every participant behaved correctly and the composition is the outage. No individual instance did anything wrong. There is no line to blame. The only place it is visible is a test that asserts the property of the whole rather than the behaviour of a part, which is why that test is the one you trust.
And thank you for taking "unstated precondition" over your own phrasing. Distrust everything is not an instruction, it is a mood. Telling someone where to look is the only version anyone can act on at 2am.
Netfox
@rabnoor_sΒ ngl the general form is better than what I had. "Not just that it went red, but that it went red about the right thing" β that's the step I was doing by hand without a name for it, same as you were with the other one.
It got used the same day, as it happens. I'd pulled the decision behind that guard out into its own function so it could be tested without driving the UI, then swapped in the pre-fix behaviour to check the tests would actually notice. They went red reporting a nil where a name should have been β which is the reason I'd caused, not merely a reason. Had they gone red on the count instead, I'd have learned nothing about whether they were watching the right thing.
The composition point is the one I keep turning over, though, because it predicted something in a domain with no concurrency in it at all. I closed a bug today where a single hosts-file alias appeared on seventeen rows of a list β the same name beside a Raycast helper, beside Google Drive, beside a database. Every line was true. The name really does reach all of them, because it names the loopback address and they all listen there. No line to blame, exactly as you put it: each row individually correct, the set implying a relationship that doesn't exist.
And the fix landed on the shape you describe. The test pinning it asserts a property of the whole β that every loopback listener carries the identical name β rather than checking any single row. Per row there's nothing to catch, because per row nothing is wrong. Same as the two copies politely quitting: the outage only exists at the level of the set.
"Distrust everything is not an instruction, it is a mood" is going straight into my notes. That's most debugging advice, now that you've named it β a posture where a next action should be.