refactor: decouple adapter class-identity (kind) from instance-identity (name) (#120)

Adds a `kind` TEXT column to `config.adapters` (migration 043) so one adapter
class can later back many operator-created instances.  Every built-in adapter
row is back-filled with `kind = name`, keeping all 23 adapters working
unchanged.

- Migration 043: ADD COLUMN IF NOT EXISTS kind, back-fill, SET NOT NULL.
- AdapterConfig gains `kind: str | None` with a model_validator that falls
  back to `name` when the column is NULL/absent (pre-043 rows).
- config_store.get_adapter / list_adapters: add `kind` to SELECT.
- supervisor._create_adapter AND _start_adapter (api-key precondition):
  resolve the class by config.kind (class key) not config.name (instance key);
  runtime state keying stays on config.name.
- routes.py GET/POST /adapters/{name} and adapters_list: add `kind` to SELECT,
  resolve the adapter class by row["kind"].
- adapter_discovery.py: comment clarifying .name is the kind (class identity).
- Out of scope, left as-is (built-ins only, name==kind): setup wizard paths in
  routes.py (~750/~891/~1109) and gui/__init__.py.
- Tests: 17 new pure-unit tests (migration SQL shape, AdapterConfig back-compat,
  _create_adapter + _start_adapter kind dispatch). Existing test mock rows
  updated to include the `kind` field.

Co-authored-by: Ubuntu <zvx@cortex.echo6.co>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
malice 2026-06-30 00:53:34 -06:00 committed by GitHub
commit 13722d07dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 370 additions and 26 deletions

View file

@ -74,7 +74,7 @@ class TestRoutesApiKeyMissing:
mock_pool = MagicMock()
mock_conn = MagicMock()
mock_conn.fetch = AsyncMock(return_value=[
{"name": "firms", "enabled": False, "cadence_s": 300, "settings": {}, "paused_at": None, "updated_at": None, "last_error": None},
{"name": "firms", "kind": "firms", "enabled": False, "cadence_s": 300, "settings": {}, "paused_at": None, "updated_at": None, "last_error": None},
])
mock_conn.fetchval = AsyncMock(return_value=None) # No API key exists
mock_conn.__aenter__ = AsyncMock(return_value=mock_conn)
@ -307,6 +307,7 @@ class TestAdaptersEditSubmitErrorRerender:
# First call: adapter row
{
"name": "firms",
"kind": "firms",
"enabled": False,
"cadence_s": 300,
"settings": {"api_key_alias": "firms", "satellites": ["VIIRS_SNPP_NRT"]},