fix: an absent battery reading is an absent KEY, not undefined (MX-835) - #13
Merged
MGrin merged 1 commit intoSep 9, 2026
Conversation
`bb plugin list` has read, since this Mac was last plugged in:
system@0.1.0 running (rpc current failed: rpc result at
$result.sample.battery.minutesRemaining is not a JSON value (undefined))
and the System panel rendered nothing at all. `si.battery()` returns
timeRemaining 65535 (IOKit's "no estimate") on AC power, `asMinutes`
correctly refuses it as an estimate — and then the result was assigned
anyway, as `minutesRemaining: undefined`. bb's RPC serialiser refuses
`undefined` and fails the WHOLE call, not the field.
`JSON.stringify` DROPS such keys, so `encodeBattery`/`decodeBattery`
round-trip clean and the persisted path never saw it. Only the live
object carries the key, which is why this reached a running machine.
Every optional field on the RPC surface is now built so that "no
reading" means the key is absent:
* `presentBattery()` writes only the optional readings that have a
value — pct, minutesRemaining, cycleCount, healthPct — used by both
`batteryFromSi` and `batteryFromRemote`.
* `withBattery()` does the same one level up: `sample.battery` is
optional too, so `{ battery: undefined }` fails identically. All
three Sample constructors (local, persisted row, remote sampler) go
through it.
Those are the only optional fields on `current`/`history`; procShape and
machineShape are all-required.
No frontend change: `app.tsx` already tests `!== undefined`, which an
absent key satisfies.
Tests: this repo had none, so `typecheck` was the whole story and a
type-correct `undefined` passed it. Adds `node --test
--experimental-strip-types` over lib/*.test.ts (the pattern
bb-plugin-cf-tunnel uses) and a Test CI job. The assertion is
`assertJsonValue` — bb's own rule, walked over the whole result — rather
than a spot check on one field name. On the old construction 4 of 12
fail, the first with the verbatim production message.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
bb plugin liston mgrin's Mac reads, whenever it is plugged in:The System panel renders nothing at all.
Cause
si.battery()returnstimeRemaining: 65535(IOKit's "no estimate") on AC power.asMinutescorrectly refuses it — and the refusal was then assigned, asminutesRemaining: undefined. bb's RPC serialiser refusesundefinedand fails the wholecurrentcall, not the field.JSON.stringifydrops such keys, soencodeBattery/decodeBatteryround-trip clean and the persisted path never saw it. Only the live object carries the key — which is why this reached a running machine.Fix
"No reading" now means the key is absent, everywhere on the RPC surface:
presentBattery()writes only the optional readings that have a value (pct,minutesRemaining,cycleCount,healthPct) — used by bothbatteryFromSiandbatteryFromRemote.withBattery()does the same one level up:sample.batteryis optional too, so{ battery: undefined }fails identically. All threeSampleconstructors (local sample, persisted row, remote sampler) go through it.Those are the only optional fields reachable from
current/history;procShapeandmachineShapeare all-required. No frontend change —app.tsxalready tests!== undefined, which an absent key satisfies.Tests
This repo had none, so
typecheckwas the whole story and a type-correctundefinedsailed through it. Addsnode --test --experimental-strip-typesoverlib/*.test.ts(the pattern bb-plugin-cf-tunnel uses, no framework) and aTestCI job.The assertion is
assertJsonValue— bb's own rule walked over the whole result — rather than a spot check on one field name, because the next optional field added is the next outage.Falsified: on the old construction 4 of 12 fail, the first with the verbatim production message
$result.sample.battery.minutesRemaining is not a JSON value (undefined). On this branch 12/12 pass.Gates
npm testnpm run typecheckbb plugin build .No pre-existing failures; there was nothing to be pre-existing.
MX-835
🤖 Generated with Claude Code