chore: fix all ruff lint errors + format — 0 errors, 306 tests pass

This commit is contained in:
leocrm-bot
2026-06-29 17:43:56 +02:00
parent 316f323ff4
commit a2452cc04b
81 changed files with 2317 additions and 1128 deletions
+163 -52
View File
@@ -3,31 +3,31 @@
from __future__ import annotations
import asyncio
import uuid
from typing import Any
import pytest
import pytest_asyncio
from httpx import ASGITransport, AsyncClient
from sqlalchemy import text, select, inspect
from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, AsyncSession
from sqlalchemy import inspect, select, text
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker
from app.core.db import Base, reset_engine_for_testing, close_engine, get_engine
from app.core.db import close_engine, reset_engine_for_testing
from app.core.event_bus import get_event_bus
from app.core.service_container import get_container
from app.main import create_app
from app.models.plugin import Plugin as PluginModel, PluginMigration
from app.models.plugin import Plugin as PluginModel
from app.models.plugin import PluginMigration
from app.plugins.base import BasePlugin
from app.plugins.manifest import PluginManifest
from app.plugins.registry import get_registry, reset_registry_for_testing
from app.plugins.migration_runner import MigrationRunner, MigrationValidationError
from app.plugins.builtins.test_sample import TestSamplePlugin
from app.plugins.manifest import PluginManifest
from app.plugins.migration_runner import MigrationRunner, MigrationValidationError
from app.plugins.registry import get_registry, reset_registry_for_testing
from app.services.plugin_service import reset_plugin_service_for_testing
from tests.conftest import TEST_DB_URL, seed_tenant_and_users, login_client, ORIGIN_HEADER
from tests.conftest import ORIGIN_HEADER, login_client, seed_tenant_and_users
# ─── Bad Plugin for AC11 (migration without tenant_id) ───
class BadMigrationPlugin(BasePlugin):
"""Plugin with a migration that creates a table WITHOUT tenant_id."""
@@ -46,6 +46,7 @@ class BadMigrationPlugin(BasePlugin):
# ─── Fixtures ───
@pytest_asyncio.fixture
async def plugin_app(engine: AsyncEngine, redis_client):
"""FastAPI app with plugin registry initialized for testing."""
@@ -82,7 +83,7 @@ async def plugin_client(plugin_app) -> AsyncClient:
@pytest_asyncio.fixture
async def authed_plugin_client(plugin_client: AsyncClient, db_session: AsyncSession) -> AsyncClient:
"""Authenticated admin client for plugin tests."""
seed = await seed_tenant_and_users(db_session)
await seed_tenant_and_users(db_session)
await login_client(plugin_client, "admin@tenanta.com")
return plugin_client
@@ -98,6 +99,7 @@ async def db_session_for_plugins(engine: AsyncEngine) -> AsyncSession:
# ─── AC1: GET /api/v1/plugins → 200 + list of plugins with status ───
@pytest.mark.asyncio
async def test_ac01_list_plugins(authed_plugin_client: AsyncClient):
"""AC1: GET /api/v1/plugins returns 200 with plugin list and status."""
@@ -119,10 +121,15 @@ async def test_ac01_list_plugins(authed_plugin_client: AsyncClient):
# ─── AC2: POST /api/v1/plugins/{name}/install → 200, status=installed, migrations run ───
@pytest.mark.asyncio
async def test_ac02_install_plugin(authed_plugin_client: AsyncClient, db_session_for_plugins: AsyncSession):
async def test_ac02_install_plugin(
authed_plugin_client: AsyncClient, db_session_for_plugins: AsyncSession
):
"""AC2: Install plugin runs migrations and sets status=installed."""
resp = await authed_plugin_client.post("/api/v1/plugins/test_sample/install", headers=ORIGIN_HEADER)
resp = await authed_plugin_client.post(
"/api/v1/plugins/test_sample/install", headers=ORIGIN_HEADER
)
assert resp.status_code == 200
data = resp.json()
assert data["name"] == "test_sample"
@@ -132,22 +139,29 @@ async def test_ac02_install_plugin(authed_plugin_client: AsyncClient, db_session
# Verify migration table was created (tenant_id column exists)
result = await db_session_for_plugins.execute(
text("SELECT column_name FROM information_schema.columns WHERE table_name = 'plugin_test_data' AND column_name = 'tenant_id'")
text(
"SELECT column_name FROM information_schema.columns WHERE table_name = 'plugin_test_data' AND column_name = 'tenant_id'"
)
)
assert result.fetchone() is not None, "plugin_test_data table should have tenant_id column"
# ─── AC3: POST /api/v1/plugins/{name}/activate → 200, status=active, routes registered ───
@pytest.mark.asyncio
async def test_ac03_activate_plugin(authed_plugin_client: AsyncClient):
"""AC3: Activate plugin sets status=active and registers event listeners."""
# Install first
resp = await authed_plugin_client.post("/api/v1/plugins/test_sample/install", headers=ORIGIN_HEADER)
resp = await authed_plugin_client.post(
"/api/v1/plugins/test_sample/install", headers=ORIGIN_HEADER
)
assert resp.status_code == 200
# Activate
resp = await authed_plugin_client.post("/api/v1/plugins/test_sample/activate", headers=ORIGIN_HEADER)
resp = await authed_plugin_client.post(
"/api/v1/plugins/test_sample/activate", headers=ORIGIN_HEADER
)
assert resp.status_code == 200
data = resp.json()
assert data["name"] == "test_sample"
@@ -157,6 +171,7 @@ async def test_ac03_activate_plugin(authed_plugin_client: AsyncClient):
# ─── AC4: POST /api/v1/plugins/{name}/deactivate → 200, status=inactive, routes unregistered ───
@pytest.mark.asyncio
async def test_ac04_deactivate_plugin(authed_plugin_client: AsyncClient):
"""AC4: Deactivate plugin sets status=inactive and unregisters event listeners."""
@@ -165,7 +180,9 @@ async def test_ac04_deactivate_plugin(authed_plugin_client: AsyncClient):
await authed_plugin_client.post("/api/v1/plugins/test_sample/activate", headers=ORIGIN_HEADER)
# Deactivate
resp = await authed_plugin_client.post("/api/v1/plugins/test_sample/deactivate", headers=ORIGIN_HEADER)
resp = await authed_plugin_client.post(
"/api/v1/plugins/test_sample/deactivate", headers=ORIGIN_HEADER
)
assert resp.status_code == 200
data = resp.json()
assert data["name"] == "test_sample"
@@ -175,8 +192,11 @@ async def test_ac04_deactivate_plugin(authed_plugin_client: AsyncClient):
# ─── AC5: DELETE /api/v1/plugins/{name} → 200, plugin removed ───
@pytest.mark.asyncio
async def test_ac05_uninstall_plugin(authed_plugin_client: AsyncClient, db_session_for_plugins: AsyncSession):
async def test_ac05_uninstall_plugin(
authed_plugin_client: AsyncClient, db_session_for_plugins: AsyncSession
):
"""AC5: Uninstall plugin removes DB record."""
# Install first
await authed_plugin_client.post("/api/v1/plugins/test_sample/install", headers=ORIGIN_HEADER)
@@ -197,6 +217,7 @@ async def test_ac05_uninstall_plugin(authed_plugin_client: AsyncClient, db_sessi
# ─── AC6: DELETE /api/v1/plugins/{name}?remove_data=true → 200, plugin tables dropped ───
@pytest.mark.asyncio
async def test_ac06_uninstall_remove_data(authed_plugin_client: AsyncClient, engine: AsyncEngine):
"""AC6: Uninstall with remove_data=true drops plugin tables."""
@@ -207,6 +228,7 @@ async def test_ac06_uninstall_remove_data(authed_plugin_client: AsyncClient, eng
def _check_table(sync_conn):
insp = inspect(sync_conn)
return "plugin_test_data" in insp.get_table_names()
async with engine.connect() as conn:
table_exists_before = await conn.run_sync(_check_table)
assert table_exists_before, "plugin_test_data table should exist after install"
@@ -223,11 +245,14 @@ async def test_ac06_uninstall_remove_data(authed_plugin_client: AsyncClient, eng
# Verify table is gone
async with engine.connect() as conn:
table_exists_after = await conn.run_sync(_check_table)
assert not table_exists_after, "plugin_test_data table should be dropped after uninstall with remove_data=true"
assert (
not table_exists_after
), "plugin_test_data table should be dropped after uninstall with remove_data=true"
# ─── AC7: GET /api/v1/plugins/manifest → 200 + manifest schema documentation ───
@pytest.mark.asyncio
async def test_ac07_manifest_schema(authed_plugin_client: AsyncClient):
"""AC7: GET /api/v1/plugins/manifest returns schema documentation."""
@@ -250,6 +275,7 @@ async def test_ac07_manifest_schema(authed_plugin_client: AsyncClient):
# ─── AC8: Plugin activation registers event listeners on event bus ───
@pytest.mark.asyncio
async def test_ac08_activation_registers_event_listeners(
authed_plugin_client: AsyncClient, db_session_for_plugins: AsyncSession
@@ -281,6 +307,7 @@ async def test_ac08_activation_registers_event_listeners(
# ─── AC9: Plugin deactivation unregisters event listeners ───
@pytest.mark.asyncio
async def test_ac09_deactivation_unregisters_event_listeners(authed_plugin_client: AsyncClient):
"""AC9: Deactivating a plugin unregisters its event listeners from the event bus."""
@@ -297,11 +324,15 @@ async def test_ac09_deactivation_unregisters_event_listeners(authed_plugin_clien
# Publish an event — handler should NOT be called after deactivation
initial_log_count = len(plugin.event_log)
await event_bus.publish("company.created", {"company_id": "test-456", "name": "After Deactivate"})
await event_bus.publish(
"company.created", {"company_id": "test-456", "name": "After Deactivate"}
)
await asyncio.sleep(0.1)
# Event log should not have grown
assert len(plugin.event_log) == initial_log_count, "Event handler should not be called after deactivation"
assert (
len(plugin.event_log) == initial_log_count
), "Event handler should not be called after deactivation"
# Check event bus no longer has the plugin's handlers
handlers = event_bus._handlers.get("company.created", [])
@@ -312,11 +343,16 @@ async def test_ac09_deactivation_unregisters_event_listeners(authed_plugin_clien
# ─── AC10: Plugin migration creates tables with tenant_id column ───
@pytest.mark.asyncio
async def test_ac10_migration_creates_tenant_id(authed_plugin_client: AsyncClient, engine: AsyncEngine):
async def test_ac10_migration_creates_tenant_id(
authed_plugin_client: AsyncClient, engine: AsyncEngine
):
"""AC10: Plugin migration creates tables that have tenant_id column."""
# Install the test_sample plugin which has a migration creating plugin_test_data
resp = await authed_plugin_client.post("/api/v1/plugins/test_sample/install", headers=ORIGIN_HEADER)
resp = await authed_plugin_client.post(
"/api/v1/plugins/test_sample/install", headers=ORIGIN_HEADER
)
assert resp.status_code == 200
# Verify the table has tenant_id column
@@ -335,6 +371,7 @@ async def test_ac10_migration_creates_tenant_id(authed_plugin_client: AsyncClien
# ─── AC11: Plugin migration validator rejects tables without tenant_id ───
@pytest.mark.asyncio
async def test_ac11_validator_rejects_no_tenant_id(authed_plugin_client: AsyncClient):
"""AC11: Migration validator rejects tables created without tenant_id column."""
@@ -357,11 +394,16 @@ async def test_ac11_validator_rejects_no_tenant_id(authed_plugin_client: AsyncCl
# ─── AC12: Plugin DB migrations tracked in plugin_migrations table ───
@pytest.mark.asyncio
async def test_ac12_migrations_tracked(authed_plugin_client: AsyncClient, db_session_for_plugins: AsyncSession):
async def test_ac12_migrations_tracked(
authed_plugin_client: AsyncClient, db_session_for_plugins: AsyncSession
):
"""AC12: Plugin migrations are tracked in plugin_migrations table."""
# Install the test_sample plugin
resp = await authed_plugin_client.post("/api/v1/plugins/test_sample/install", headers=ORIGIN_HEADER)
resp = await authed_plugin_client.post(
"/api/v1/plugins/test_sample/install", headers=ORIGIN_HEADER
)
assert resp.status_code == 200
# Check plugin_migrations table has a record
@@ -376,17 +418,22 @@ async def test_ac12_migrations_tracked(authed_plugin_client: AsyncClient, db_ses
# ─── AC13: Activating already-active plugin → idempotent (200, no error) ───
@pytest.mark.asyncio
async def test_ac13_activate_already_active(authed_plugin_client: AsyncClient):
"""AC13: Activating an already-active plugin is idempotent (returns 200, no error)."""
# Install and activate
await authed_plugin_client.post("/api/v1/plugins/test_sample/install", headers=ORIGIN_HEADER)
resp = await authed_plugin_client.post("/api/v1/plugins/test_sample/activate", headers=ORIGIN_HEADER)
resp = await authed_plugin_client.post(
"/api/v1/plugins/test_sample/activate", headers=ORIGIN_HEADER
)
assert resp.status_code == 200
assert resp.json()["status"] == "active"
# Activate again — should be idempotent
resp = await authed_plugin_client.post("/api/v1/plugins/test_sample/activate", headers=ORIGIN_HEADER)
resp = await authed_plugin_client.post(
"/api/v1/plugins/test_sample/activate", headers=ORIGIN_HEADER
)
assert resp.status_code == 200
data = resp.json()
assert data["status"] == "active"
@@ -396,6 +443,7 @@ async def test_ac13_activate_already_active(authed_plugin_client: AsyncClient):
# ─── AC14: Deactivating inactive plugin → idempotent (200) ───
@pytest.mark.asyncio
async def test_ac14_deactivate_already_inactive(authed_plugin_client: AsyncClient):
"""AC14: Deactivating an already-inactive plugin is idempotent (returns 200, no error)."""
@@ -404,12 +452,16 @@ async def test_ac14_deactivate_already_inactive(authed_plugin_client: AsyncClien
await authed_plugin_client.post("/api/v1/plugins/test_sample/activate", headers=ORIGIN_HEADER)
# Deactivate
resp = await authed_plugin_client.post("/api/v1/plugins/test_sample/deactivate", headers=ORIGIN_HEADER)
resp = await authed_plugin_client.post(
"/api/v1/plugins/test_sample/deactivate", headers=ORIGIN_HEADER
)
assert resp.status_code == 200
assert resp.json()["status"] == "inactive"
# Deactivate again — should be idempotent
resp = await authed_plugin_client.post("/api/v1/plugins/test_sample/deactivate", headers=ORIGIN_HEADER)
resp = await authed_plugin_client.post(
"/api/v1/plugins/test_sample/deactivate", headers=ORIGIN_HEADER
)
assert resp.status_code == 200
data = resp.json()
assert data["status"] == "inactive"
@@ -419,6 +471,7 @@ async def test_ac14_deactivate_already_inactive(authed_plugin_client: AsyncClien
# ─── Additional Tests: Direct MigrationRunner Unit Tests ───
@pytest.mark.asyncio
async def test_registry_discover_builtins(engine: AsyncEngine):
"""Test that registry can discover built-in plugins."""
@@ -431,7 +484,9 @@ async def test_registry_discover_builtins(engine: AsyncEngine):
@pytest.mark.asyncio
async def test_registry_list_plugins_mixed_states(engine: AsyncEngine, db_session_for_plugins: AsyncSession):
async def test_registry_list_plugins_mixed_states(
engine: AsyncEngine, db_session_for_plugins: AsyncSession
):
"""Test listing plugins in various states (discovered, installed, active, inactive)."""
registry = reset_registry_for_testing()
registry.initialize(engine, None)
@@ -464,7 +519,9 @@ async def test_registry_list_plugins_mixed_states(engine: AsyncEngine, db_sessio
@pytest.mark.asyncio
async def test_registry_install_idempotent(engine: AsyncEngine, db_session_for_plugins: AsyncSession):
async def test_registry_install_idempotent(
engine: AsyncEngine, db_session_for_plugins: AsyncSession
):
"""Test that installing an already-installed plugin is idempotent."""
registry = reset_registry_for_testing()
registry.initialize(engine, None)
@@ -500,7 +557,9 @@ async def test_registry_not_found_errors(engine: AsyncEngine, db_session_for_plu
@pytest.mark.asyncio
async def test_registry_activate_without_install(engine: AsyncEngine, db_session_for_plugins: AsyncSession):
async def test_registry_activate_without_install(
engine: AsyncEngine, db_session_for_plugins: AsyncSession
):
"""Test that activating without install raises error."""
registry = reset_registry_for_testing()
registry.initialize(engine, None)
@@ -511,7 +570,9 @@ async def test_registry_activate_without_install(engine: AsyncEngine, db_session
@pytest.mark.asyncio
async def test_migration_runner_drop_tables(engine: AsyncEngine, db_session_for_plugins: AsyncSession):
async def test_migration_runner_drop_tables(
engine: AsyncEngine, db_session_for_plugins: AsyncSession
):
"""Test MigrationRunner.drop_plugin_tables removes tables and migration records."""
runner = MigrationRunner(engine)
@@ -545,7 +606,9 @@ async def test_migration_runner_drop_tables(engine: AsyncEngine, db_session_for_
@pytest.mark.asyncio
async def test_migration_runner_file_not_found(engine: AsyncEngine, db_session_for_plugins: AsyncSession):
async def test_migration_runner_file_not_found(
engine: AsyncEngine, db_session_for_plugins: AsyncSession
):
"""Test MigrationRunner raises FileNotFoundError for missing migration file."""
runner = MigrationRunner(engine)
with pytest.raises(FileNotFoundError):
@@ -592,7 +655,7 @@ def test_plugin_manifest_validation():
assert m2.name == "myplugin"
# Invalid name with special chars
with pytest.raises(Exception):
with pytest.raises(Exception): # noqa: B017
PluginManifest(name="my-plugin!", version="1.0.0", display_name="Test")
@@ -606,6 +669,7 @@ def test_base_plugin_repr():
def test_base_plugin_no_manifest_error():
"""Test that BasePlugin without manifest raises ValueError."""
class NoManifestPlugin(BasePlugin):
pass
@@ -623,10 +687,14 @@ def test_base_plugin_name_version_properties():
@pytest.mark.asyncio
async def test_base_plugin_on_install_default():
"""Test default on_install returns None (no-op)."""
class MinimalPlugin(BasePlugin):
manifest = PluginManifest(
name="minimal", version="1.0.0", display_name="Minimal",
name="minimal",
version="1.0.0",
display_name="Minimal",
)
plugin = MinimalPlugin()
result = await plugin.on_install(None, None)
assert result is None
@@ -635,10 +703,14 @@ async def test_base_plugin_on_install_default():
@pytest.mark.asyncio
async def test_base_plugin_on_uninstall_default():
"""Test default on_uninstall returns None (no-op)."""
class MinimalPlugin(BasePlugin):
manifest = PluginManifest(
name="minimal2", version="1.0.0", display_name="Minimal",
name="minimal2",
version="1.0.0",
display_name="Minimal",
)
plugin = MinimalPlugin()
result = await plugin.on_uninstall(None, None)
assert result is None
@@ -653,11 +725,15 @@ def test_base_plugin_get_routes_empty():
def test_base_plugin_make_event_handler_noop():
"""Test that _make_event_handler creates noop for unknown events."""
class MinimalPlugin(BasePlugin):
manifest = PluginManifest(
name="minimal3", version="1.0.0", display_name="Minimal",
name="minimal3",
version="1.0.0",
display_name="Minimal",
events=["unknown.event"],
)
plugin = MinimalPlugin()
handler = plugin._make_event_handler("unknown.event")
assert handler is not None
@@ -669,6 +745,7 @@ def test_base_plugin_make_event_handler_noop():
async def test_service_manifest_schema():
"""Test plugin service get_manifest_schema returns dict."""
from app.services.plugin_service import PluginService
service = PluginService()
schema = service.get_manifest_schema()
assert isinstance(schema, dict)
@@ -694,10 +771,13 @@ async def test_uninstall_inactive_plugin(engine: AsyncEngine, db_session_for_plu
@pytest.mark.asyncio
async def test_registry_activate_with_app_routes(engine: AsyncEngine, db_session_for_plugins: AsyncSession):
async def test_registry_activate_with_app_routes(
engine: AsyncEngine, db_session_for_plugins: AsyncSession
):
"""Test that activating a plugin with a FastAPI app registers routes."""
from fastapi import FastAPI, APIRouter
from app.plugins.manifest import PluginManifest, PluginRouteDef
from fastapi import APIRouter, FastAPI
from app.plugins.manifest import PluginManifest
# Create a plugin with a route
class RoutePlugin(BasePlugin):
@@ -707,11 +787,14 @@ async def test_registry_activate_with_app_routes(engine: AsyncEngine, db_session
display_name="Route Plugin",
events=["test.event"],
)
def get_routes(self) -> list:
test_router = APIRouter()
@test_router.get("/api/v1/route-plugin/test")
async def test_endpoint():
return {"status": "ok"}
return [test_router]
app = FastAPI()
@@ -737,7 +820,9 @@ async def test_registry_activate_with_app_routes(engine: AsyncEngine, db_session
@pytest.mark.asyncio
async def test_registry_uninstall_with_remove_data(engine: AsyncEngine, db_session_for_plugins: AsyncSession):
async def test_registry_uninstall_with_remove_data(
engine: AsyncEngine, db_session_for_plugins: AsyncSession
):
"""Test registry uninstall with remove_data drops tables."""
registry = reset_registry_for_testing()
registry.initialize(engine, None)
@@ -763,7 +848,9 @@ async def test_registry_not_initialized_errors():
@pytest.mark.asyncio
async def test_registry_deactivate_already_inactive_direct(engine: AsyncEngine, db_session_for_plugins: AsyncSession):
async def test_registry_deactivate_already_inactive_direct(
engine: AsyncEngine, db_session_for_plugins: AsyncSession
):
"""Test registry.deactivate is idempotent when already inactive."""
registry = reset_registry_for_testing()
registry.initialize(engine, None)
@@ -785,9 +872,12 @@ async def test_registry_deactivate_already_inactive_direct(engine: AsyncEngine,
@pytest.mark.asyncio
async def test_plugin_service_install_error_handling(engine: AsyncEngine, db_session_for_plugins: AsyncSession):
async def test_plugin_service_install_error_handling(
engine: AsyncEngine, db_session_for_plugins: AsyncSession
):
"""Test plugin service raises ValueError for unknown plugin."""
from app.services.plugin_service import PluginService
registry = reset_registry_for_testing()
registry.initialize(engine, None)
service = PluginService(registry=registry)
@@ -797,9 +887,12 @@ async def test_plugin_service_install_error_handling(engine: AsyncEngine, db_ses
@pytest.mark.asyncio
async def test_plugin_service_activate_error_handling(engine: AsyncEngine, db_session_for_plugins: AsyncSession):
async def test_plugin_service_activate_error_handling(
engine: AsyncEngine, db_session_for_plugins: AsyncSession
):
"""Test plugin service raises ValueError when activating uninstalled plugin."""
from app.services.plugin_service import PluginService
registry = reset_registry_for_testing()
registry.initialize(engine, None)
registry.register_plugin(TestSamplePlugin())
@@ -810,9 +903,12 @@ async def test_plugin_service_activate_error_handling(engine: AsyncEngine, db_se
@pytest.mark.asyncio
async def test_plugin_service_deactivate_error_handling(engine: AsyncEngine, db_session_for_plugins: AsyncSession):
async def test_plugin_service_deactivate_error_handling(
engine: AsyncEngine, db_session_for_plugins: AsyncSession
):
"""Test plugin service raises ValueError for deactivating unknown plugin."""
from app.services.plugin_service import PluginService
registry = reset_registry_for_testing()
registry.initialize(engine, None)
service = PluginService(registry=registry)
@@ -822,9 +918,12 @@ async def test_plugin_service_deactivate_error_handling(engine: AsyncEngine, db_
@pytest.mark.asyncio
async def test_plugin_service_uninstall_error_handling(engine: AsyncEngine, db_session_for_plugins: AsyncSession):
async def test_plugin_service_uninstall_error_handling(
engine: AsyncEngine, db_session_for_plugins: AsyncSession
):
"""Test plugin service raises ValueError for uninstalling unknown plugin."""
from app.services.plugin_service import PluginService
registry = reset_registry_for_testing()
registry.initialize(engine, None)
service = PluginService(registry=registry)
@@ -842,7 +941,9 @@ async def test_migration_runner_split_sql_dollar_quotes():
@pytest.mark.asyncio
async def test_registry_list_plugins_db_only_record(engine: AsyncEngine, db_session_for_plugins: AsyncSession):
async def test_registry_list_plugins_db_only_record(
engine: AsyncEngine, db_session_for_plugins: AsyncSession
):
"""Test listing plugins includes DB-only records (installed but not discovered)."""
registry = reset_registry_for_testing()
registry.initialize(engine, None)
@@ -869,11 +970,15 @@ async def test_registry_list_plugins_db_only_record(engine: AsyncEngine, db_sess
@pytest.mark.asyncio
async def test_base_plugin_on_event_fallback():
"""Test that on_event fallback handler works."""
class FallbackPlugin(BasePlugin):
manifest = PluginManifest(
name="fallback", version="1.0.0", display_name="Fallback",
name="fallback",
version="1.0.0",
display_name="Fallback",
events=["custom.event"],
)
async def on_event(self, payload: dict[str, Any]) -> None:
self.event_received = payload
@@ -884,7 +989,9 @@ async def test_base_plugin_on_event_fallback():
@pytest.mark.asyncio
async def test_migration_runner_valid_migration(engine: AsyncEngine, db_session_for_plugins: AsyncSession):
async def test_migration_runner_valid_migration(
engine: AsyncEngine, db_session_for_plugins: AsyncSession
):
"""Test MigrationRunner directly with a valid migration (has tenant_id)."""
runner = MigrationRunner(engine)
record = await runner.run_migration(
@@ -899,7 +1006,9 @@ async def test_migration_runner_valid_migration(engine: AsyncEngine, db_session_
@pytest.mark.asyncio
async def test_migration_runner_invalid_migration(engine: AsyncEngine, db_session_for_plugins: AsyncSession):
async def test_migration_runner_invalid_migration(
engine: AsyncEngine, db_session_for_plugins: AsyncSession
):
"""Test MigrationRunner directly rejects migration without tenant_id."""
runner = MigrationRunner(engine)
with pytest.raises(MigrationValidationError) as exc_info:
@@ -912,7 +1021,9 @@ async def test_migration_runner_invalid_migration(engine: AsyncEngine, db_sessio
@pytest.mark.asyncio
async def test_migration_runner_idempotent(engine: AsyncEngine, db_session_for_plugins: AsyncSession):
async def test_migration_runner_idempotent(
engine: AsyncEngine, db_session_for_plugins: AsyncSession
):
"""Test MigrationRunner skips already-applied migrations."""
runner = MigrationRunner(engine)
# Run migration