Full sweep of every machine_npub deref found one more reachable crash:
_record_rejected (tasks.py) logs machine_npub[:12], and the
assert_nostr_attribution guard now routes an unpaired machine there, so
None[:12] -> TypeError. Fall back to machine.id.
Every other deref is safe by the attribution-gate invariant: a settlement only
flows past assert_nostr_attribution (now rejecting unpaired) for a paired
machine, so the downstream distribution / parse-path / "landed" logs can't see
None; the collision-loop display already uses `(m.machine_npub or m.id)`.
Adds tests/test_unpaired_machine_guards.py: attribution rejects an unpaired
machine with the domain SettlementAttributionError (not AttributeError), and
build_state_d_tags skips it. New tests + every guard-affected suite pass.
(Two pre-existing test_pair_endpoint failures — #29 drift: fake_pair lacks
bunker_relay, and the test DB lacks super_config — are out of scope; filed
separately.)
machine_npub became nullable in #29/m011 (register-unpaired flow), but
several consumers still assumed it's non-None and crashed
`normalize_public_key(None)` with `AttributeError: 'NoneType' object has no
attribute 'startswith'`. On the demo (which had an unpaired machine) this
broke the platform-fee update (500) and spammed the cassette consumer with
errors every 2s. The #29 create/pair paths were guarded; these were missed:
- views_api `api_update_super_config`: the "republish fee to every active
machine" loop → skip unpaired (they get their config at pairing).
- cassette_transport `build_state_d_tags_for_machines`: skip unpaired (no
state-beacon d-tag yet) — the cassette-consumer loop crash.
- crud `get_machine_by_atm_pubkey_hex`: its `except (ValueError,
AssertionError)` didn't catch the AttributeError; skip unpaired before
normalize — the cassette event-handler crash.
- bitspire `assert_nostr_attribution`: reject (SettlementAttributionError) an
unpaired machine instead of crashing the payment listener.
- views_api cassettes/publish endpoint: 400 (not paired) instead of crashing
publish_to_atm.
Verified on the dev stack: with an unpaired active machine present, the
cassette consumer registers (skipping it) and runs clean — no AttributeError.