Skip to content

fix: an absent battery reading is an absent KEY, not undefined (MX-835) - #13

Merged
MGrin merged 1 commit into
mainfrom
bb/mx-835-system-plugin-battery-minutesremaining-un-thr_5dxkbrave5
Sep 9, 2026
Merged

MGrin merged 1 commit into
mainfrom
bb/mx-835-system-plugin-battery-minutesremaining-un-thr_5dxkbrave5

Conversation

@MGrin

@MGrin MGrin commented Sep 9, 2026

Copy link
Copy Markdown
Owner

bb plugin list on mgrin's Mac reads, whenever it is plugged in:

system@0.1.0  running  (rpc current failed: rpc result at $result.sample.battery.minutesRemaining is not a JSON value (undefined))

The System panel renders nothing at all.

Cause

si.battery() returns timeRemaining: 65535 (IOKit's "no estimate") on AC power. asMinutes correctly refuses it — and the refusal was then assigned, as minutesRemaining: undefined. bb's RPC serialiser refuses undefined and fails the whole current 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.

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 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 sample, persisted row, remote sampler) go through it.

Those are the only optional fields reachable from 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 sailed through it. Adds node --test --experimental-strip-types over lib/*.test.ts (the pattern bb-plugin-cf-tunnel uses, no framework) 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, 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

gate result
npm test 12 passed, 0 failed
npm run typecheck rc=0
bb plugin build . rc=0, all six artifacts

No pre-existing failures; there was nothing to be pre-existing.

MX-835

🤖 Generated with Claude Code

`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>
@MGrin
MGrin merged commit c068060 into main Sep 9, 2026
3 checks passed
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