fix(tests): remove deleted test_sample plugin, inline SamplePlugin definition
Check Cross-Plugin Imports / check (push) Has been cancelled

- Remove import from app.plugins.builtins.test_sample (deleted in Phase 4)
- Define SamplePlugin inline in test_plugins.py with same lifecycle behavior
- Replace all test_sample/TestSamplePlugin references with sample_plugin/SamplePlugin
- Create migration SQL files: 0001_sample_plugin.sql, 0001_bad_migration.sql
- Update discover_builtins test to check for tags plugin instead
This commit is contained in:
Agent Zero
2026-08-06 14:30:15 +02:00
parent 20288da567
commit aab2f3d898
3 changed files with 147 additions and 86 deletions
@@ -0,0 +1,6 @@
-- Bad migration: creates table WITHOUT tenant_id (for validator testing)
CREATE TABLE IF NOT EXISTS plugin_bad_data (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
@@ -0,0 +1,9 @@
-- Sample plugin migration: creates plugin_sample_data table with tenant_id
CREATE TABLE IF NOT EXISTS plugin_sample_data (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS ix_plugin_sample_data_tenant ON plugin_sample_data(tenant_id);
+132 -86
View File
@@ -18,8 +18,54 @@ from app.main import create_app
from app.models.plugin import Plugin as PluginModel from app.models.plugin import Plugin as PluginModel
from app.models.plugin import PluginMigration from app.models.plugin import PluginMigration
from app.plugins.base import BasePlugin from app.plugins.base import BasePlugin
from app.plugins.builtins.test_sample import TestSamplePlugin
from app.plugins.manifest import PluginManifest from app.plugins.manifest import PluginManifest
# ─── Sample Plugin for lifecycle tests ───
class SamplePlugin(BasePlugin):
"""A sample plugin for testing the plugin lifecycle."""
manifest = PluginManifest(
name="sample_plugin",
version="1.0.0",
display_name="Sample Plugin",
description="A sample plugin for testing install/activate/deactivate/uninstall lifecycle.",
dependencies=[],
routes=[],
events=["contact.created", "company.created"],
migrations=["0001_sample_plugin.sql"],
permissions=[],
)
def __init__(self) -> None:
super().__init__()
self.install_called = False
self.activate_called = False
self.deactivate_called = False
self.uninstall_called = False
self.event_log: list[dict[str, Any]] = []
async def on_install(self, db, service_container) -> None:
self.install_called = True
async def on_activate(self, db, service_container, event_bus) -> None:
self.activate_called = True
await super().on_activate(db, service_container, event_bus)
async def on_deactivate(self, db, service_container, event_bus) -> None:
self.deactivate_called = True
await super().on_deactivate(db, service_container, event_bus)
async def on_uninstall(self, db, service_container) -> None:
self.uninstall_called = True
async def on_contact_created(self, payload: dict[str, Any]) -> None:
self.event_log.append({"event": "contact.created", "payload": payload})
async def on_company_created(self, payload: dict[str, Any]) -> None:
self.event_log.append({"event": "company.created", "payload": payload})
from app.plugins.migration_runner import MigrationRunner, MigrationValidationError from app.plugins.migration_runner import MigrationRunner, MigrationValidationError
from app.plugins.registry import get_registry, reset_registry_for_testing from app.plugins.registry import get_registry, reset_registry_for_testing
from app.services.plugin_service import reset_plugin_service_for_testing from app.services.plugin_service import reset_plugin_service_for_testing
@@ -62,7 +108,7 @@ async def plugin_app(engine: AsyncEngine, redis_client):
await container.initialize() await container.initialize()
# Register test plugins manually (don't auto-discover to avoid side effects) # Register test plugins manually (don't auto-discover to avoid side effects)
registry.register_plugin(TestSamplePlugin()) registry.register_plugin(SamplePlugin())
registry.register_plugin(BadMigrationPlugin()) registry.register_plugin(BadMigrationPlugin())
# Reset plugin service to use the new registry # Reset plugin service to use the new registry
@@ -110,7 +156,7 @@ async def test_ac01_list_plugins(authed_plugin_client: AsyncClient):
assert "total" in data assert "total" in data
assert data["total"] >= 2 assert data["total"] >= 2
plugin_names = [p["name"] for p in data["plugins"]] plugin_names = [p["name"] for p in data["plugins"]]
assert "test_sample" in plugin_names assert "sample_plugin" in plugin_names
assert "bad_migration_plugin" in plugin_names assert "bad_migration_plugin" in plugin_names
# Check status field exists # Check status field exists
for p in data["plugins"]: for p in data["plugins"]:
@@ -128,11 +174,11 @@ async def test_ac02_install_plugin(
): ):
"""AC2: Install plugin runs migrations and sets status=installed.""" """AC2: Install plugin runs migrations and sets status=installed."""
resp = await authed_plugin_client.post( resp = await authed_plugin_client.post(
"/api/v1/plugins/test_sample/install", headers=ORIGIN_HEADER "/api/v1/plugins/sample_plugin/install", headers=ORIGIN_HEADER
) )
assert resp.status_code == 200 assert resp.status_code == 200
data = resp.json() data = resp.json()
assert data["name"] == "test_sample" assert data["name"] == "sample_plugin"
assert data["status"] == "installed" assert data["status"] == "installed"
assert data["installed"] is True assert data["installed"] is True
assert data["active"] is False assert data["active"] is False
@@ -140,10 +186,10 @@ async def test_ac02_install_plugin(
# Verify migration table was created (tenant_id column exists) # Verify migration table was created (tenant_id column exists)
result = await db_session_for_plugins.execute( result = await db_session_for_plugins.execute(
text( text(
"SELECT column_name FROM information_schema.columns WHERE table_name = 'plugin_test_data' AND column_name = 'tenant_id'" "SELECT column_name FROM information_schema.columns WHERE table_name = 'plugin_sample_data' AND column_name = 'tenant_id'"
) )
) )
assert result.fetchone() is not None, "plugin_test_data table should have tenant_id column" assert result.fetchone() is not None, "plugin_sample_data table should have tenant_id column"
# ─── AC3: POST /api/v1/plugins/{name}/activate → 200, status=active, routes registered ─── # ─── AC3: POST /api/v1/plugins/{name}/activate → 200, status=active, routes registered ───
@@ -154,17 +200,17 @@ async def test_ac03_activate_plugin(authed_plugin_client: AsyncClient):
"""AC3: Activate plugin sets status=active and registers event listeners.""" """AC3: Activate plugin sets status=active and registers event listeners."""
# Install first # Install first
resp = await authed_plugin_client.post( resp = await authed_plugin_client.post(
"/api/v1/plugins/test_sample/install", headers=ORIGIN_HEADER "/api/v1/plugins/sample_plugin/install", headers=ORIGIN_HEADER
) )
assert resp.status_code == 200 assert resp.status_code == 200
# Activate # Activate
resp = await authed_plugin_client.post( resp = await authed_plugin_client.post(
"/api/v1/plugins/test_sample/activate", headers=ORIGIN_HEADER "/api/v1/plugins/sample_plugin/activate", headers=ORIGIN_HEADER
) )
assert resp.status_code == 200 assert resp.status_code == 200
data = resp.json() data = resp.json()
assert data["name"] == "test_sample" assert data["name"] == "sample_plugin"
assert data["status"] == "active" assert data["status"] == "active"
assert data["active"] is True assert data["active"] is True
@@ -176,16 +222,16 @@ async def test_ac03_activate_plugin(authed_plugin_client: AsyncClient):
async def test_ac04_deactivate_plugin(authed_plugin_client: AsyncClient): async def test_ac04_deactivate_plugin(authed_plugin_client: AsyncClient):
"""AC4: Deactivate plugin sets status=inactive and unregisters event listeners.""" """AC4: Deactivate plugin sets status=inactive and unregisters event listeners."""
# Install and activate first # Install and activate first
await authed_plugin_client.post("/api/v1/plugins/test_sample/install", headers=ORIGIN_HEADER) await authed_plugin_client.post("/api/v1/plugins/sample_plugin/install", headers=ORIGIN_HEADER)
await authed_plugin_client.post("/api/v1/plugins/test_sample/activate", headers=ORIGIN_HEADER) await authed_plugin_client.post("/api/v1/plugins/sample_plugin/activate", headers=ORIGIN_HEADER)
# Deactivate # Deactivate
resp = await authed_plugin_client.post( resp = await authed_plugin_client.post(
"/api/v1/plugins/test_sample/deactivate", headers=ORIGIN_HEADER "/api/v1/plugins/sample_plugin/deactivate", headers=ORIGIN_HEADER
) )
assert resp.status_code == 200 assert resp.status_code == 200
data = resp.json() data = resp.json()
assert data["name"] == "test_sample" assert data["name"] == "sample_plugin"
assert data["status"] == "inactive" assert data["status"] == "inactive"
assert data["active"] is False assert data["active"] is False
@@ -199,10 +245,10 @@ async def test_ac05_uninstall_plugin(
): ):
"""AC5: Uninstall plugin removes DB record.""" """AC5: Uninstall plugin removes DB record."""
# Install first # Install first
await authed_plugin_client.post("/api/v1/plugins/test_sample/install", headers=ORIGIN_HEADER) await authed_plugin_client.post("/api/v1/plugins/sample_plugin/install", headers=ORIGIN_HEADER)
# Uninstall # Uninstall
resp = await authed_plugin_client.delete("/api/v1/plugins/test_sample", headers=ORIGIN_HEADER) resp = await authed_plugin_client.delete("/api/v1/plugins/sample_plugin", headers=ORIGIN_HEADER)
assert resp.status_code == 200 assert resp.status_code == 200
data = resp.json() data = resp.json()
assert data["status"] == "uninstalled" assert data["status"] == "uninstalled"
@@ -210,7 +256,7 @@ async def test_ac05_uninstall_plugin(
# Verify DB record is gone # Verify DB record is gone
result = await db_session_for_plugins.execute( result = await db_session_for_plugins.execute(
select(PluginModel).where(PluginModel.name == "test_sample") select(PluginModel).where(PluginModel.name == "sample_plugin")
) )
assert result.scalar_one_or_none() is None assert result.scalar_one_or_none() is None
@@ -221,33 +267,33 @@ async def test_ac05_uninstall_plugin(
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_ac06_uninstall_remove_data(authed_plugin_client: AsyncClient, engine: AsyncEngine): async def test_ac06_uninstall_remove_data(authed_plugin_client: AsyncClient, engine: AsyncEngine):
"""AC6: Uninstall with remove_data=true drops plugin tables.""" """AC6: Uninstall with remove_data=true drops plugin tables."""
# Install first (creates plugin_test_data table) # Install first (creates plugin_sample_data table)
await authed_plugin_client.post("/api/v1/plugins/test_sample/install", headers=ORIGIN_HEADER) await authed_plugin_client.post("/api/v1/plugins/sample_plugin/install", headers=ORIGIN_HEADER)
# Verify table exists # Verify table exists
def _check_table(sync_conn): def _check_table(sync_conn):
insp = inspect(sync_conn) insp = inspect(sync_conn)
return "plugin_test_data" in insp.get_table_names() return "plugin_sample_data" in insp.get_table_names()
async with engine.connect() as conn: async with engine.connect() as conn:
table_exists_before = await conn.run_sync(_check_table) table_exists_before = await conn.run_sync(_check_table)
assert table_exists_before, "plugin_test_data table should exist after install" assert table_exists_before, "plugin_sample_data table should exist after install"
# Uninstall with remove_data=true # Uninstall with remove_data=true
resp = await authed_plugin_client.delete( resp = await authed_plugin_client.delete(
"/api/v1/plugins/test_sample?remove_data=true", headers=ORIGIN_HEADER "/api/v1/plugins/sample_plugin?remove_data=true", headers=ORIGIN_HEADER
) )
assert resp.status_code == 200 assert resp.status_code == 200
data = resp.json() data = resp.json()
assert data["status"] == "uninstalled" assert data["status"] == "uninstalled"
assert "plugin_test_data" in data.get("dropped_tables", []) assert "plugin_sample_data" in data.get("dropped_tables", [])
# Verify table is gone # Verify table is gone
async with engine.connect() as conn: async with engine.connect() as conn:
table_exists_after = await conn.run_sync(_check_table) table_exists_after = await conn.run_sync(_check_table)
assert ( assert (
not table_exists_after not table_exists_after
), "plugin_test_data table should be dropped after uninstall with remove_data=true" ), "plugin_sample_data table should be dropped after uninstall with remove_data=true"
# ─── AC7: GET /api/v1/plugins/manifest → 200 + manifest schema documentation ─── # ─── AC7: GET /api/v1/plugins/manifest → 200 + manifest schema documentation ───
@@ -282,12 +328,12 @@ async def test_ac08_activation_registers_event_listeners(
): ):
"""AC8: Activating a plugin registers its event listeners on the event bus.""" """AC8: Activating a plugin registers its event listeners on the event bus."""
registry = get_registry() registry = get_registry()
plugin = registry.get_plugin("test_sample") plugin = registry.get_plugin("sample_plugin")
assert plugin is not None assert plugin is not None
# Install and activate # Install and activate
await authed_plugin_client.post("/api/v1/plugins/test_sample/install", headers=ORIGIN_HEADER) await authed_plugin_client.post("/api/v1/plugins/sample_plugin/install", headers=ORIGIN_HEADER)
await authed_plugin_client.post("/api/v1/plugins/test_sample/activate", headers=ORIGIN_HEADER) await authed_plugin_client.post("/api/v1/plugins/sample_plugin/activate", headers=ORIGIN_HEADER)
event_bus = get_event_bus() event_bus = get_event_bus()
@@ -312,13 +358,13 @@ async def test_ac08_activation_registers_event_listeners(
async def test_ac09_deactivation_unregisters_event_listeners(authed_plugin_client: AsyncClient): async def test_ac09_deactivation_unregisters_event_listeners(authed_plugin_client: AsyncClient):
"""AC9: Deactivating a plugin unregisters its event listeners from the event bus.""" """AC9: Deactivating a plugin unregisters its event listeners from the event bus."""
registry = get_registry() registry = get_registry()
plugin = registry.get_plugin("test_sample") plugin = registry.get_plugin("sample_plugin")
assert plugin is not None assert plugin is not None
# Install, activate, then deactivate # Install, activate, then deactivate
await authed_plugin_client.post("/api/v1/plugins/test_sample/install", headers=ORIGIN_HEADER) await authed_plugin_client.post("/api/v1/plugins/sample_plugin/install", headers=ORIGIN_HEADER)
await authed_plugin_client.post("/api/v1/plugins/test_sample/activate", headers=ORIGIN_HEADER) await authed_plugin_client.post("/api/v1/plugins/sample_plugin/activate", headers=ORIGIN_HEADER)
await authed_plugin_client.post("/api/v1/plugins/test_sample/deactivate", headers=ORIGIN_HEADER) await authed_plugin_client.post("/api/v1/plugins/sample_plugin/deactivate", headers=ORIGIN_HEADER)
event_bus = get_event_bus() event_bus = get_event_bus()
@@ -349,24 +395,24 @@ async def test_ac10_migration_creates_tenant_id(
authed_plugin_client: AsyncClient, engine: AsyncEngine authed_plugin_client: AsyncClient, engine: AsyncEngine
): ):
"""AC10: Plugin migration creates tables that have tenant_id column.""" """AC10: Plugin migration creates tables that have tenant_id column."""
# Install the test_sample plugin which has a migration creating plugin_test_data # Install the sample_plugin plugin which has a migration creating plugin_sample_data
resp = await authed_plugin_client.post( resp = await authed_plugin_client.post(
"/api/v1/plugins/test_sample/install", headers=ORIGIN_HEADER "/api/v1/plugins/sample_plugin/install", headers=ORIGIN_HEADER
) )
assert resp.status_code == 200 assert resp.status_code == 200
# Verify the table has tenant_id column # Verify the table has tenant_id column
def _get_columns(sync_conn): def _get_columns(sync_conn):
insp = inspect(sync_conn) insp = inspect(sync_conn)
if "plugin_test_data" not in insp.get_table_names(): if "plugin_sample_data" not in insp.get_table_names():
return None return None
return [col["name"] for col in insp.get_columns("plugin_test_data")] return [col["name"] for col in insp.get_columns("plugin_sample_data")]
async with engine.connect() as conn: async with engine.connect() as conn:
columns = await conn.run_sync(_get_columns) columns = await conn.run_sync(_get_columns)
assert columns is not None, "plugin_test_data table should exist" assert columns is not None, "plugin_sample_data table should exist"
assert "tenant_id" in columns, "plugin_test_data table must have tenant_id column" assert "tenant_id" in columns, "plugin_sample_data table must have tenant_id column"
# ─── AC11: Plugin migration validator rejects tables without tenant_id ─── # ─── AC11: Plugin migration validator rejects tables without tenant_id ───
@@ -400,19 +446,19 @@ async def test_ac12_migrations_tracked(
authed_plugin_client: AsyncClient, db_session_for_plugins: AsyncSession authed_plugin_client: AsyncClient, db_session_for_plugins: AsyncSession
): ):
"""AC12: Plugin migrations are tracked in plugin_migrations table.""" """AC12: Plugin migrations are tracked in plugin_migrations table."""
# Install the test_sample plugin # Install the sample_plugin plugin
resp = await authed_plugin_client.post( resp = await authed_plugin_client.post(
"/api/v1/plugins/test_sample/install", headers=ORIGIN_HEADER "/api/v1/plugins/sample_plugin/install", headers=ORIGIN_HEADER
) )
assert resp.status_code == 200 assert resp.status_code == 200
# Check plugin_migrations table has a record # Check plugin_migrations table has a record
result = await db_session_for_plugins.execute( result = await db_session_for_plugins.execute(
select(PluginMigration).where(PluginMigration.plugin_name == "test_sample") select(PluginMigration).where(PluginMigration.plugin_name == "sample_plugin")
) )
migrations = result.scalars().all() migrations = result.scalars().all()
assert len(migrations) > 0, "plugin_migrations table should have a record for test_sample" assert len(migrations) > 0, "plugin_migrations table should have a record for sample_plugin"
assert migrations[0].migration_file == "0001_test_plugin.sql" assert migrations[0].migration_file == "0001_sample_plugin.sql"
assert migrations[0].status == "applied" assert migrations[0].status == "applied"
@@ -423,16 +469,16 @@ async def test_ac12_migrations_tracked(
async def test_ac13_activate_already_active(authed_plugin_client: AsyncClient): async def test_ac13_activate_already_active(authed_plugin_client: AsyncClient):
"""AC13: Activating an already-active plugin is idempotent (returns 200, no error).""" """AC13: Activating an already-active plugin is idempotent (returns 200, no error)."""
# Install and activate # Install and activate
await authed_plugin_client.post("/api/v1/plugins/test_sample/install", headers=ORIGIN_HEADER) await authed_plugin_client.post("/api/v1/plugins/sample_plugin/install", headers=ORIGIN_HEADER)
resp = await authed_plugin_client.post( resp = await authed_plugin_client.post(
"/api/v1/plugins/test_sample/activate", headers=ORIGIN_HEADER "/api/v1/plugins/sample_plugin/activate", headers=ORIGIN_HEADER
) )
assert resp.status_code == 200 assert resp.status_code == 200
assert resp.json()["status"] == "active" assert resp.json()["status"] == "active"
# Activate again — should be idempotent # Activate again — should be idempotent
resp = await authed_plugin_client.post( resp = await authed_plugin_client.post(
"/api/v1/plugins/test_sample/activate", headers=ORIGIN_HEADER "/api/v1/plugins/sample_plugin/activate", headers=ORIGIN_HEADER
) )
assert resp.status_code == 200 assert resp.status_code == 200
data = resp.json() data = resp.json()
@@ -448,19 +494,19 @@ async def test_ac13_activate_already_active(authed_plugin_client: AsyncClient):
async def test_ac14_deactivate_already_inactive(authed_plugin_client: AsyncClient): async def test_ac14_deactivate_already_inactive(authed_plugin_client: AsyncClient):
"""AC14: Deactivating an already-inactive plugin is idempotent (returns 200, no error).""" """AC14: Deactivating an already-inactive plugin is idempotent (returns 200, no error)."""
# Install and activate # Install and activate
await authed_plugin_client.post("/api/v1/plugins/test_sample/install", headers=ORIGIN_HEADER) await authed_plugin_client.post("/api/v1/plugins/sample_plugin/install", headers=ORIGIN_HEADER)
await authed_plugin_client.post("/api/v1/plugins/test_sample/activate", headers=ORIGIN_HEADER) await authed_plugin_client.post("/api/v1/plugins/sample_plugin/activate", headers=ORIGIN_HEADER)
# Deactivate # Deactivate
resp = await authed_plugin_client.post( resp = await authed_plugin_client.post(
"/api/v1/plugins/test_sample/deactivate", headers=ORIGIN_HEADER "/api/v1/plugins/sample_plugin/deactivate", headers=ORIGIN_HEADER
) )
assert resp.status_code == 200 assert resp.status_code == 200
assert resp.json()["status"] == "inactive" assert resp.json()["status"] == "inactive"
# Deactivate again — should be idempotent # Deactivate again — should be idempotent
resp = await authed_plugin_client.post( resp = await authed_plugin_client.post(
"/api/v1/plugins/test_sample/deactivate", headers=ORIGIN_HEADER "/api/v1/plugins/sample_plugin/deactivate", headers=ORIGIN_HEADER
) )
assert resp.status_code == 200 assert resp.status_code == 200
data = resp.json() data = resp.json()
@@ -478,9 +524,9 @@ async def test_registry_discover_builtins(engine: AsyncEngine):
registry = reset_registry_for_testing() registry = reset_registry_for_testing()
registry.initialize(engine, None) registry.initialize(engine, None)
discovered = registry.discover_builtins() discovered = registry.discover_builtins()
# test_sample should be discovered from builtins package # tags should be discovered from builtins package
assert "test_sample" in discovered assert "tags" in discovered
assert registry.get_plugin("test_sample") is not None assert registry.get_plugin("tags") is not None
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -490,7 +536,7 @@ async def test_registry_list_plugins_mixed_states(
"""Test listing plugins in various states (discovered, installed, active, inactive).""" """Test listing plugins in various states (discovered, installed, active, inactive)."""
registry = reset_registry_for_testing() registry = reset_registry_for_testing()
registry.initialize(engine, None) registry.initialize(engine, None)
registry.register_plugin(TestSamplePlugin()) registry.register_plugin(SamplePlugin())
# List before install — all discovered # List before install — all discovered
plugins = await registry.list_plugins(db_session_for_plugins) plugins = await registry.list_plugins(db_session_for_plugins)
@@ -499,7 +545,7 @@ async def test_registry_list_plugins_mixed_states(
assert plugins[0]["installed"] is False assert plugins[0]["installed"] is False
# Install # Install
await registry.install(db_session_for_plugins, "test_sample") await registry.install(db_session_for_plugins, "sample_plugin")
await db_session_for_plugins.commit() await db_session_for_plugins.commit()
# List after install — installed # List after install — installed
@@ -509,7 +555,7 @@ async def test_registry_list_plugins_mixed_states(
assert plugins[0]["installed"] is True assert plugins[0]["installed"] is True
# Activate # Activate
await registry.activate(db_session_for_plugins, "test_sample") await registry.activate(db_session_for_plugins, "sample_plugin")
await db_session_for_plugins.commit() await db_session_for_plugins.commit()
# List after activate — active # List after activate — active
@@ -525,12 +571,12 @@ async def test_registry_install_idempotent(
"""Test that installing an already-installed plugin is idempotent.""" """Test that installing an already-installed plugin is idempotent."""
registry = reset_registry_for_testing() registry = reset_registry_for_testing()
registry.initialize(engine, None) registry.initialize(engine, None)
registry.register_plugin(TestSamplePlugin()) registry.register_plugin(SamplePlugin())
record1 = await registry.install(db_session_for_plugins, "test_sample") record1 = await registry.install(db_session_for_plugins, "sample_plugin")
await db_session_for_plugins.commit() await db_session_for_plugins.commit()
record2 = await registry.install(db_session_for_plugins, "test_sample") record2 = await registry.install(db_session_for_plugins, "sample_plugin")
await db_session_for_plugins.commit() await db_session_for_plugins.commit()
# Same record returned # Same record returned
@@ -563,10 +609,10 @@ async def test_registry_activate_without_install(
"""Test that activating without install raises error.""" """Test that activating without install raises error."""
registry = reset_registry_for_testing() registry = reset_registry_for_testing()
registry.initialize(engine, None) registry.initialize(engine, None)
registry.register_plugin(TestSamplePlugin()) registry.register_plugin(SamplePlugin())
with pytest.raises(ValueError, match="not installed"): with pytest.raises(ValueError, match="not installed"):
await registry.activate(db_session_for_plugins, "test_sample") await registry.activate(db_session_for_plugins, "sample_plugin")
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -577,12 +623,12 @@ async def test_migration_runner_drop_tables(
runner = MigrationRunner(engine) runner = MigrationRunner(engine)
# Run migration first # Run migration first
await runner.run_migration(db_session_for_plugins, "drop_test", "0001_test_plugin.sql") await runner.run_migration(db_session_for_plugins, "drop_test", "0001_sample_plugin.sql")
await db_session_for_plugins.commit() await db_session_for_plugins.commit()
# Verify table exists # Verify table exists
result = await db_session_for_plugins.execute( result = await db_session_for_plugins.execute(
text("SELECT tablename FROM pg_tables WHERE tablename = 'plugin_test_data'") text("SELECT tablename FROM pg_tables WHERE tablename = 'plugin_sample_data'")
) )
assert result.fetchone() is not None assert result.fetchone() is not None
@@ -590,11 +636,11 @@ async def test_migration_runner_drop_tables(
dropped = await runner.drop_plugin_tables(db_session_for_plugins, "drop_test") dropped = await runner.drop_plugin_tables(db_session_for_plugins, "drop_test")
await db_session_for_plugins.commit() await db_session_for_plugins.commit()
assert "plugin_test_data" in dropped assert "plugin_sample_data" in dropped
# Verify table is gone # Verify table is gone
result = await db_session_for_plugins.execute( result = await db_session_for_plugins.execute(
text("SELECT tablename FROM pg_tables WHERE tablename = 'plugin_test_data'") text("SELECT tablename FROM pg_tables WHERE tablename = 'plugin_sample_data'")
) )
assert result.fetchone() is None assert result.fetchone() is None
@@ -661,9 +707,9 @@ def test_plugin_manifest_validation():
def test_base_plugin_repr(): def test_base_plugin_repr():
"""Test BasePlugin __repr__ method.""" """Test BasePlugin __repr__ method."""
plugin = TestSamplePlugin() plugin = SamplePlugin()
repr_str = repr(plugin) repr_str = repr(plugin)
assert "test_sample" in repr_str assert "sample_plugin" in repr_str
assert "1.0.0" in repr_str assert "1.0.0" in repr_str
@@ -679,8 +725,8 @@ def test_base_plugin_no_manifest_error():
def test_base_plugin_name_version_properties(): def test_base_plugin_name_version_properties():
"""Test BasePlugin name and version properties.""" """Test BasePlugin name and version properties."""
plugin = TestSamplePlugin() plugin = SamplePlugin()
assert plugin.name == "test_sample" assert plugin.name == "sample_plugin"
assert plugin.version == "1.0.0" assert plugin.version == "1.0.0"
@@ -718,7 +764,7 @@ async def test_base_plugin_on_uninstall_default():
def test_base_plugin_get_routes_empty(): def test_base_plugin_get_routes_empty():
"""Test get_routes returns empty list when manifest has no routes.""" """Test get_routes returns empty list when manifest has no routes."""
plugin = TestSamplePlugin() plugin = SamplePlugin()
routes = plugin.get_routes() routes = plugin.get_routes()
assert routes == [] assert routes == []
@@ -758,14 +804,14 @@ async def test_uninstall_inactive_plugin(engine: AsyncEngine, db_session_for_plu
"""Test uninstalling a plugin that is installed but not active.""" """Test uninstalling a plugin that is installed but not active."""
registry = reset_registry_for_testing() registry = reset_registry_for_testing()
registry.initialize(engine, None) registry.initialize(engine, None)
registry.register_plugin(TestSamplePlugin()) registry.register_plugin(SamplePlugin())
# Install only (not activated) # Install only (not activated)
await registry.install(db_session_for_plugins, "test_sample") await registry.install(db_session_for_plugins, "sample_plugin")
await db_session_for_plugins.commit() await db_session_for_plugins.commit()
# Uninstall should work without deactivation # Uninstall should work without deactivation
record = await registry.uninstall(db_session_for_plugins, "test_sample") record = await registry.uninstall(db_session_for_plugins, "sample_plugin")
await db_session_for_plugins.commit() await db_session_for_plugins.commit()
assert record.status == "uninstalled" assert record.status == "uninstalled"
@@ -826,15 +872,15 @@ async def test_registry_uninstall_with_remove_data(
"""Test registry uninstall with remove_data drops tables.""" """Test registry uninstall with remove_data drops tables."""
registry = reset_registry_for_testing() registry = reset_registry_for_testing()
registry.initialize(engine, None) registry.initialize(engine, None)
registry.register_plugin(TestSamplePlugin()) registry.register_plugin(SamplePlugin())
await registry.install(db_session_for_plugins, "test_sample") await registry.install(db_session_for_plugins, "sample_plugin")
await db_session_for_plugins.commit() await db_session_for_plugins.commit()
record = await registry.uninstall(db_session_for_plugins, "test_sample", remove_data=True) record = await registry.uninstall(db_session_for_plugins, "sample_plugin", remove_data=True)
await db_session_for_plugins.commit() await db_session_for_plugins.commit()
assert record.status == "uninstalled" assert record.status == "uninstalled"
assert "plugin_test_data" in getattr(record, "dropped_tables", []) assert "plugin_sample_data" in getattr(record, "dropped_tables", [])
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -854,19 +900,19 @@ async def test_registry_deactivate_already_inactive_direct(
"""Test registry.deactivate is idempotent when already inactive.""" """Test registry.deactivate is idempotent when already inactive."""
registry = reset_registry_for_testing() registry = reset_registry_for_testing()
registry.initialize(engine, None) registry.initialize(engine, None)
registry.register_plugin(TestSamplePlugin()) registry.register_plugin(SamplePlugin())
await registry.install(db_session_for_plugins, "test_sample") await registry.install(db_session_for_plugins, "sample_plugin")
await db_session_for_plugins.commit() await db_session_for_plugins.commit()
# Deactivate without activating first — status is 'installed', not 'inactive' # Deactivate without activating first — status is 'installed', not 'inactive'
# But calling deactivate should still work (sets to inactive) # But calling deactivate should still work (sets to inactive)
record = await registry.deactivate(db_session_for_plugins, "test_sample") record = await registry.deactivate(db_session_for_plugins, "sample_plugin")
await db_session_for_plugins.commit() await db_session_for_plugins.commit()
assert record.status == "inactive" assert record.status == "inactive"
# Deactivate again — idempotent # Deactivate again — idempotent
record2 = await registry.deactivate(db_session_for_plugins, "test_sample") record2 = await registry.deactivate(db_session_for_plugins, "sample_plugin")
await db_session_for_plugins.commit() await db_session_for_plugins.commit()
assert record2.status == "inactive" assert record2.status == "inactive"
@@ -895,11 +941,11 @@ async def test_plugin_service_activate_error_handling(
registry = reset_registry_for_testing() registry = reset_registry_for_testing()
registry.initialize(engine, None) registry.initialize(engine, None)
registry.register_plugin(TestSamplePlugin()) registry.register_plugin(SamplePlugin())
service = PluginService(registry=registry) service = PluginService(registry=registry)
with pytest.raises(ValueError, match="not installed"): with pytest.raises(ValueError, match="not installed"):
await service.activate_plugin(db_session_for_plugins, "test_sample") await service.activate_plugin(db_session_for_plugins, "sample_plugin")
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -997,11 +1043,11 @@ async def test_migration_runner_valid_migration(
record = await runner.run_migration( record = await runner.run_migration(
db_session_for_plugins, db_session_for_plugins,
"test_plugin", "test_plugin",
"0001_test_plugin.sql", "0001_sample_plugin.sql",
) )
await db_session_for_plugins.commit() await db_session_for_plugins.commit()
assert record.plugin_name == "test_plugin" assert record.plugin_name == "test_plugin"
assert record.migration_file == "0001_test_plugin.sql" assert record.migration_file == "0001_sample_plugin.sql"
assert record.status == "applied" assert record.status == "applied"
@@ -1027,11 +1073,11 @@ async def test_migration_runner_idempotent(
"""Test MigrationRunner skips already-applied migrations.""" """Test MigrationRunner skips already-applied migrations."""
runner = MigrationRunner(engine) runner = MigrationRunner(engine)
# Run migration # Run migration
await runner.run_migration(db_session_for_plugins, "test_idempotent", "0001_test_plugin.sql") await runner.run_migration(db_session_for_plugins, "test_idempotent", "0001_sample_plugin.sql")
await db_session_for_plugins.commit() await db_session_for_plugins.commit()
# Run all migrations again — should skip # Run all migrations again — should skip
records = await runner.run_all_migrations( records = await runner.run_all_migrations(
db_session_for_plugins, "test_idempotent", ["0001_test_plugin.sql"] db_session_for_plugins, "test_idempotent", ["0001_sample_plugin.sql"]
) )
assert len(records) == 0, "Already-applied migration should be skipped" assert len(records) == 0, "Already-applied migration should be skipped"