v3 M1a: provider framework — config v2 + Provider abstraction (daemon)

Introduce the multi-provider seam on the host, behaviour-identical for Anthropic.

- config v2: `providers` (per-provider enable + creds), `active_provider`,
  `display_order`; v1 configs migrate transparently (anthropic enabled+active).
  New accessors: active_provider_id / provider_conf / enabled_providers.
- daemon/providers/: Provider ABC + normalized ProviderStatus (maps to the same
  s/sr/w/wr/st/tk/to/tc/tn BLE fields), AnthropicProvider (wraps the existing
  OAuth+poll+local-usage logic), StubProvider for openai/zai until M2/M3, and a
  get_provider() registry.
- poll loop: polls the config-selected active provider each cycle and tags the
  payload with `pv` (the brand-theme id the watch will wear). Watch->PC
  `{"cmd":"prov","id":".."}` persists the active provider and repolls at once.
- clawdmeter.spec: bundle daemon.providers for the frozen exe.
- test: 7 new provider/config tests; fix a stale start_notify count assertion
  (the M2 command channel means both REQ and CMD subscriptions are attempted).

Verified live: config loads as v2, active provider resolves to AnthropicProvider,
a real poll returns ok=True (s=53%, w=5%); full suite 87 passed (+7 new).
This commit is contained in:
wenil
2026-07-10 00:20:37 +03:00
parent e99c27babd
commit 6e3f8e9084
8 changed files with 405 additions and 55 deletions
+4 -2
View File
@@ -599,8 +599,10 @@ def test_start_notify_oserror_does_not_crash_connect_and_run():
# Must NOT raise OSError — graceful degradation into the poll loop.
result = _run(connect_and_run(device, stop_event))
# start_notify was actually attempted (and raised), but was swallowed.
assert mock_client.start_notify.call_count == 1
# start_notify was actually attempted (and raised), but was swallowed — for
# BOTH subscriptions the daemon sets up: the REQ refresh channel and the CMD
# command channel (battery + HA). Both degrade gracefully.
assert mock_client.start_notify.call_count == 2
# Function returned normally instead of propagating the OSError.
assert result is False
# The link was cleaned up via the finally block.