Skip to content

Don't let the body-location probe's detail line contradict its own verdict (#690) - #918

Merged
ryanbr merged 1 commit into
mainfrom
fix/body-location-bare-stub-overclaim
Jul 28, 2026
Merged

Don't let the body-location probe's detail line contradict its own verdict (#690)#918
ryanbr merged 1 commit into
mainfrom
fix/body-location-bare-stub-overclaim

Conversation

@ryanbr

@ryanbr ryanbr commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Follow-up from the #913 review — found by sweeping the other probes for the same defect class, not reported.

The report contradicts itself

A bare-stub run prints both of these:

Verdict: opcode 84 answered with a bare stub — ambiguous
...
No payload beyond the command byte (bare stub) — no body-location data on this firmware

The verdict is right. The detail line settles what the verdict just called unsettled — and it is the line that reads like a finding.

One reply carrying no payload does not distinguish a firmware without body-location data from one that needs the strap worn, in a determinate position, or asked with an argument this probe does not send. The file's own enum has UNKNOWN(0) and NOT_CONCLUSIVE(128), so the firmware explicitly models "cannot tell right now" — precisely the reading the detail line ruled out.

Same class as #913 and #914: a probe asserting a conclusion the run's inputs could not support, in the sentence someone pastes into an issue.

Both platforms, and it is a shared defect

I checked whether this was a divergence and it is not — Swift and Kotlin carry the byte-identical string, so both are reworded:

No payload beyond the command byte (bare stub) — this reply carried no body-location data, which is not the same as the firmware having none (see the Verdict above)

The tests could not have caught it

Both existing bare-stub tests asserted only text.contains("bare stub"), which passes under either wording. They now assert the over-claim is absent and that the verdict and the hedge are both present. That is the difference between a test that pins behaviour and one that happens to pass.

The golden parity lock is unaffected — it pins a payload-bearing frame, so the bare-stub branch was never inside it. I verified that rather than assuming.

…rdict (#690)

The bare-stub report said two things at once:

    Verdict: opcode 84 answered with a bare stub — ambiguous
    ...
    No payload beyond the command byte (bare stub) — no body-location data on this firmware

The verdict is right and the detail line settles what the verdict just called
unsettled. A reply carrying no payload is one reply: it does not distinguish a
firmware without body-location data from one that needs the strap worn, or in a
determinate position, or asked with an argument this probe does not send. This
file's own enum has UNKNOWN(0) and NOT_CONCLUSIVE(128), so the firmware plainly
models "cannot tell right now" — which is exactly the reading the detail line
ruled out.

Same class as #913 and #914: a probe asserting a conclusion the run's inputs
could not support, in the sentence a reader quotes. Found while sweeping the
probes after #913 rather than reported.

Both platforms carried the identical string — this is a shared defect, not a
divergence — so both are reworded to say what was observed and what it does not
establish, and both tests now assert the over-claim is ABSENT rather than only
that "bare stub" appears. The old assertions passed under either wording, which
is why the contradiction survived.

The golden parity lock is unaffected: it pins a payload-bearing frame, so the
bare-stub branch was never covered by it.
@ryanbr

ryanbr commented Jul 28, 2026

Copy link
Copy Markdown
Owner Author

Re-reviewed before merge, specifically the two claims a reader would poke at.

The new wording survives the one path where the verdict is not "ambiguous". A 5/MG reply with result=UNSUPPORTED(3) and no payload prints the REJECTED verdict and then this detail line. "See the Verdict above" is still correct there — the verdict carries the decisive information, and the detail line's hedge defers to it rather than fighting it. So the line is safe under all three verdict branches, not just the one the test pins.

The sweep's one near-miss, checked and left alone. ExtendedBatteryProbe has a similar-shaped line — No payload beyond the command byte (bare stub) — no data over the battery event; opcode 98 may be an unknown-command ack on this firmware. Different defect profile: the first clause describes this reply (it adds nothing beyond what the BATTERY_LEVEL event already carries), and the firmware clause is explicitly hedged with "may be". That is the honest shape already, on both platforms, so it stays.

Also verified mechanically: the replacement string is byte-identical Swift↔Kotlin, and the Kotlin golden lock (like the Swift one) pins an ACCEPTED payload-bearing frame, so the bare-stub branch sits outside both locks — the strengthened bare-stub tests are the only coverage of this line, which is why they now assert absence of the over-claim rather than presence of a substring both wordings contain.

@ryanbr
ryanbr merged commit 821f49f into main Jul 28, 2026
11 checks passed
@ryanbr
ryanbr deleted the fix/body-location-bare-stub-overclaim branch July 28, 2026 03:52
ryanbr added a commit that referenced this pull request Jul 28, 2026
WhoopBleClient logs every non-SUCCESS command response, which is how the MG
haptics rejection (#48) would surface in-app. But every WHOOP 4.0
GET_BATTERY_LEVEL reply on record carries a zeroed [seq][result] prefix, so a
battery read that returned a perfectly good percentage logs as:

    Command response: GET_BATTERY_LEVEL(26) → FAILURE(0)

next to a battery gauge showing 42%. A log line asserting a failure that did not
happen is exactly the artefact #900 exists to warn about — that issue began when
a FAILURE(0) was read as evidence about firmware, and this line manufactures the
same misreading on every battery read a 4.0 owner makes.

The line still prints. Suppressing it would hide the anomaly, and the anomaly is
the thing #900 wants a capture of. It is annotated instead, when the same frame
also decoded a value:

    Command response: GET_BATTERY_LEVEL(26) → FAILURE(0) (the reply still
    carried a value: battery 42.5% — see #900, the 4.0 result byte is not
    established)

A genuine failure that carried no value — the extended-battery case from #791,
the UNSUPPORTED rejections — is untouched, because the annotation is gated on a
decoded value being present.

This does not decide what the 4.0 result byte means; #900 still wants one real
capture of known provenance. It stops the log asserting an answer in the
meantime, the same discipline as #913, #914 and #918.

Android-only: the Swift side has no command-response result consumer, so there
is no parity twin to change. No test — the string is inside a class that needs
the Android framework, and the annotation is a log line rather than behaviour;
the rendered output was checked against both real 4.0 battery fixtures instead.
ryanbr added a commit that referenced this pull request Jul 28, 2026
WhoopBleClient logs every non-SUCCESS command response, which is how the MG
haptics rejection (#48) would surface in-app. But every WHOOP 4.0
GET_BATTERY_LEVEL reply on record carries a zeroed [seq][result] prefix, so a
battery read that returned a perfectly good percentage logs as:

    Command response: GET_BATTERY_LEVEL(26) → FAILURE(0)

next to a battery gauge showing 42%. A log line asserting a failure that did not
happen is exactly the artefact #900 exists to warn about — that issue began when
a FAILURE(0) was read as evidence about firmware, and this line manufactures the
same misreading on every battery read a 4.0 owner makes.

The line still prints. Suppressing it would hide the anomaly, and the anomaly is
the thing #900 wants a capture of. It is annotated instead, when the same frame
also decoded a value:

    Command response: GET_BATTERY_LEVEL(26) → FAILURE(0) (the reply still
    carried a value: battery 42.5% — see #900, the 4.0 result byte is not
    established)

A genuine failure that carried no value — the extended-battery case from #791,
the UNSUPPORTED rejections — is untouched, because the annotation is gated on a
decoded value being present.

This does not decide what the 4.0 result byte means; #900 still wants one real
capture of known provenance. It stops the log asserting an answer in the
meantime, the same discipline as #913, #914 and #918.

Android-only: the Swift side has no command-response result consumer, so there
is no parity twin to change. No test — the string is inside a class that needs
the Android framework, and the annotation is a log line rather than behaviour;
the rendered output was checked against both real 4.0 battery fixtures instead.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant