diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4d3354a46..90a9b6dc3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,7 +41,7 @@ repos: exclude: "^tests/fixtures/|.json|^codegen" - id: mixed-line-ending - id: name-tests-test - exclude: "^tests/main.py" + exclude: "^(tests/main\\.py|tests/resources/)" - id: requirements-txt-fixer - id: trailing-whitespace exclude: "docs/source/_static|ATTRIBUTIONS.md||API_REFEREENCE" diff --git a/requirements/SHR-UTILS-1.md b/requirements/SHR-UTILS-1.md index 902012792..de36e1131 100644 --- a/requirements/SHR-UTILS-1.md +++ b/requirements/SHR-UTILS-1.md @@ -1,10 +1,10 @@ --- itemId: SHR-UTILS-1 -itemTitle: Central MCP Server for SDK and Plugin Tool Access +itemTitle: Central MCP Server for SDK Tool Access itemType: Requirement Requirement type: ENVIRONMENT --- ## Description -Users shall be able to expose SDK and plugin functionality to AI agents via a central MCP server for use in AI-assisted development workflows. +Developers shall be able to expose SDK functionality to AI agents via a central MCP server for use in AI-assisted development workflows. diff --git a/requirements/SHR-UTILS-2.md b/requirements/SHR-UTILS-2.md new file mode 100644 index 000000000..2d5036cfa --- /dev/null +++ b/requirements/SHR-UTILS-2.md @@ -0,0 +1,10 @@ +--- +itemId: SHR-UTILS-2 +itemTitle: Plugin System for SDK Extension +itemType: Requirement +Requirement type: ENVIRONMENT +--- + +## Description + +Developers shall be able to extend the Python SDK at runtime with custom plugin modules that contribute business logic, CLI commands, and UI pages. diff --git a/requirements/SWR-UTILS-2-1.md b/requirements/SWR-UTILS-2-1.md new file mode 100644 index 000000000..ce0e48cc9 --- /dev/null +++ b/requirements/SWR-UTILS-2-1.md @@ -0,0 +1,10 @@ +--- +itemId: SWR-UTILS-2-1 +itemTitle: Plugin Module Discovery and Loading +itemHasParent: SHR-UTILS-2 +itemType: Requirement +Requirement type: FUNCTIONAL +Layer: System (backend logic) +--- + +System shall discover and load externally installed plugin modules at runtime, making their functionality available without requiring changes to the core SDK codebase. diff --git a/requirements/SWR-UTILS-2-2.md b/requirements/SWR-UTILS-2-2.md new file mode 100644 index 000000000..47a7b15f0 --- /dev/null +++ b/requirements/SWR-UTILS-2-2.md @@ -0,0 +1,10 @@ +--- +itemId: SWR-UTILS-2-2 +itemTitle: Plugin CLI Command Integration +itemHasParent: SHR-UTILS-2 +itemType: Requirement +Requirement type: FUNCTIONAL +Layer: System (backend logic) +--- + +System shall automatically register CLI commands contributed by plugin modules into the SDK command-line interface. diff --git a/requirements/SWR-UTILS-2-3.md b/requirements/SWR-UTILS-2-3.md new file mode 100644 index 000000000..8aba8022c --- /dev/null +++ b/requirements/SWR-UTILS-2-3.md @@ -0,0 +1,10 @@ +--- +itemId: SWR-UTILS-2-3 +itemTitle: Plugin GUI Page Integration +itemHasParent: SHR-UTILS-2 +itemType: Requirement +Requirement type: FUNCTIONAL +Layer: System (backend logic) +--- + +System shall automatically register GUI pages contributed by plugin modules into the SDK graphical user interface. diff --git a/specifications/SPEC-UTILS-SERVICE.md b/specifications/SPEC-UTILS-SERVICE.md index 833ff6e5b..4e5489810 100644 --- a/specifications/SPEC-UTILS-SERVICE.md +++ b/specifications/SPEC-UTILS-SERVICE.md @@ -3,7 +3,7 @@ itemId: SPEC-UTILS-SERVICE itemTitle: Utils Module Specification itemType: Software Item Spec itemIsRelatedTo: SPEC-GUI-SERVICE, SPEC-BUCKET-SERVICE, SPEC-DATASET-SERVICE, SPEC-NOTEBOOK-SERVICE, SPEC-PLATFORM-SERVICE, SPEC-QUPATH-SERVICE, SPEC-SYSTEM-SERVICE, SPEC-WSI-SERVICE -itemFulfills: SWR-APPLICATION-1-1, SWR-APPLICATION-1-2, SWR-APPLICATION-2-1, SWR-APPLICATION-2-2, SWR-APPLICATION-2-3, SWR-APPLICATION-2-4, SWR-APPLICATION-2-5, SWR-APPLICATION-2-6, SWR-APPLICATION-2-7, SWR-APPLICATION-2-8, SWR-APPLICATION-2-9, SWR-APPLICATION-2-10, SWR-APPLICATION-2-11, SWR-APPLICATION-2-12, SWR-APPLICATION-2-13, SWR-APPLICATION-2-14, SWR-APPLICATION-2-15, SWR-APPLICATION-2-16, SWR-APPLICATION-3-1, SWR-APPLICATION-3-2, SWR-APPLICATION-3-3, SWR-BUCKET-1-1, SWR-BUCKET-1-2, SWR-BUCKET-1-3, SWR-BUCKET-1-4, SWR-BUCKET-1-5, SWR-BUCKET-1-6, SWR-BUCKET-1-7, SWR-BUCKET-1-8, SWR-BUCKET-1-9, SWR-DATASET-1-1, SWR-DATASET-1-2, SWR-DATASET-1-3, SWR-NOTEBOOK-1-1, SWR-UTILS-1-1, SWR-VISUALIZATION-1-1, SWR-VISUALIZATION-1-2, SWR-VISUALIZATION-1-3, SWR-VISUALIZATION-1-4, SWR_SYSTEM_CLI_HEALTH_1, SWR_SYSTEM_GUI_HEALTH_1, SWR_SYSTEM_GUI_SETTINGS_1 +itemFulfills: SWR-APPLICATION-1-1, SWR-APPLICATION-1-2, SWR-APPLICATION-2-1, SWR-APPLICATION-2-2, SWR-APPLICATION-2-3, SWR-APPLICATION-2-4, SWR-APPLICATION-2-5, SWR-APPLICATION-2-6, SWR-APPLICATION-2-7, SWR-APPLICATION-2-8, SWR-APPLICATION-2-9, SWR-APPLICATION-2-10, SWR-APPLICATION-2-11, SWR-APPLICATION-2-12, SWR-APPLICATION-2-13, SWR-APPLICATION-2-14, SWR-APPLICATION-2-15, SWR-APPLICATION-2-16, SWR-APPLICATION-3-1, SWR-APPLICATION-3-2, SWR-APPLICATION-3-3, SWR-BUCKET-1-1, SWR-BUCKET-1-2, SWR-BUCKET-1-3, SWR-BUCKET-1-4, SWR-BUCKET-1-5, SWR-BUCKET-1-6, SWR-BUCKET-1-7, SWR-BUCKET-1-8, SWR-BUCKET-1-9, SWR-DATASET-1-1, SWR-DATASET-1-2, SWR-DATASET-1-3, SWR-NOTEBOOK-1-1, SWR-UTILS-1-1, SWR-UTILS-2-1, SWR-UTILS-2-2, SWR-UTILS-2-3, SWR-VISUALIZATION-1-1, SWR-VISUALIZATION-1-2, SWR-VISUALIZATION-1-3, SWR-VISUALIZATION-1-4, SWR_SYSTEM_CLI_HEALTH_1, SWR_SYSTEM_GUI_HEALTH_1, SWR_SYSTEM_GUI_SETTINGS_1 Layer: Infrastructure Service Version: 1.0.0 Date: 2025-10-13 @@ -29,6 +29,9 @@ The Utils Module shall: - **[FR-08]** Provide file system utilities for user data directory management and path sanitization - **[FR-09]** Support process information gathering and runtime environment detection - **[FR-10]** Provide a central MCP server with auto-discovery of plugin tools, namespace isolation, and CLI commands for running the server and listing available tools +- **[FR-11]** Discover and load externally installed plugin modules at runtime without requiring changes to the core SDK codebase +- **[FR-12]** Automatically register CLI commands contributed by plugin modules into the SDK command-line interface +- **[FR-13]** Automatically register GUI pages contributed by plugin modules into the SDK graphical user interface ### 1.3 Non-Functional Requirements diff --git a/specifications/SPEC_PLATFORM_SERVICE.md b/specifications/SPEC_PLATFORM_SERVICE.md index 950d9e52e..a0b198ac2 100644 --- a/specifications/SPEC_PLATFORM_SERVICE.md +++ b/specifications/SPEC_PLATFORM_SERVICE.md @@ -22,7 +22,7 @@ The Platform Module shall: - **[FR-01]** Provide secure OAuth 2.0 authentication with support for both Authorization Code with PKCE and Device Authorization flows - **[FR-02]** Manage JWT token lifecycle including acquisition, caching, validation, and refresh operations - **[FR-03]** Configure and provide authenticated API clients for interaction with Aignostics Platform services -- **[FR-04]** Support multiple deployment environments (production, staging, development) with automatic endpoint configuration +- **[FR-04]** Support multiple deployment environments (production, staging, development, test) with automatic endpoint configuration - **[FR-05]** Provide CLI commands for user authentication operations (login, logout, whoami) - **[FR-06]** Handle authentication errors with retry mechanisms and fallback flows - **[FR-07]** Support proxy configurations and SSL certificate handling for enterprise environments @@ -313,8 +313,13 @@ class ApplicationRun: def cancel(self) -> None: """Cancels the application run.""" - def results(self) -> Iterator[ItemResultData]: - """Retrieves the results of all items in the run.""" + def results( + self, + nocache: bool = False, + item_ids: list[str] | None = None, + external_ids: list[str] | None = None, + ) -> Iterator[ItemResultData]: + """Retrieves the results of items in the run, optionally filtered by item or external IDs.""" def item_status(self) -> dict[str, ItemStatus]: """Retrieves the status of all items in the run.""" diff --git a/tests/aignostics/utils/TC-UTILS-PLUGIN-02.feature b/tests/aignostics/utils/TC-UTILS-PLUGIN-02.feature new file mode 100644 index 000000000..e21149a40 --- /dev/null +++ b/tests/aignostics/utils/TC-UTILS-PLUGIN-02.feature @@ -0,0 +1,12 @@ +Feature: Plugin CLI Command Integration + + The SDK automatically registers CLI commands contributed by plugin modules + into the SDK command-line interface when the plugin is installed. + + @tests:SWR-UTILS-2-2 + @id:TC-UTILS-PLUGIN-02 + Scenario: Plugin CLI commands are registered in the SDK CLI after installation + Given a plugin package registers an entry point under "aignostics.plugins" + And the plugin exposes a Typer CLI instance + When the SDK CLI is prepared via prepare_cli() + Then the plugin's CLI is registered in the SDK command-line interface diff --git a/tests/aignostics/utils/TC-UTILS-PLUGIN-03.feature b/tests/aignostics/utils/TC-UTILS-PLUGIN-03.feature new file mode 100644 index 000000000..9609ebeac --- /dev/null +++ b/tests/aignostics/utils/TC-UTILS-PLUGIN-03.feature @@ -0,0 +1,12 @@ +Feature: Plugin GUI Page Integration + + The SDK automatically registers GUI navigation entries contributed by plugin + modules into the SDK graphical user interface when the plugin is installed. + + @tests:SWR-UTILS-2-3 + @id:TC-UTILS-PLUGIN-03 + Scenario: Plugin GUI navigation entries are available in the SDK after installation + Given a plugin package registers an entry point under "aignostics.plugins" + And the plugin exposes a BaseNavBuilder subclass + When the SDK GUI collects navigation groups via gui_get_nav_groups() + Then the plugin's navigation entries are included in the SDK navigation diff --git a/tests/aignostics/utils/conftest.py b/tests/aignostics/utils/conftest.py new file mode 100644 index 000000000..ce687218d --- /dev/null +++ b/tests/aignostics/utils/conftest.py @@ -0,0 +1,45 @@ +"""Shared fixtures for utils tests.""" + +from __future__ import annotations + +import importlib +import site +import subprocess +import sys +from pathlib import Path +from typing import TYPE_CHECKING + +import pytest + +if TYPE_CHECKING: + from collections.abc import Iterator + +DUMMY_PLUGIN_DIR = Path(__file__).resolve().parents[2] / "resources" / "mcp_dummy_plugin" + + +@pytest.fixture(scope="session") +def install_dummy_plugin() -> Iterator[None]: + """Install the dummy plugin package in editable mode for the test session. + + Refreshes site-packages so the running interpreter sees the new package + and its entry points without a process restart. + """ + subprocess.run( + [sys.executable, "-m", "pip", "install", "--no-deps", "-e", str(DUMMY_PLUGIN_DIR)], + check=True, + capture_output=True, + text=True, + ) + + importlib.invalidate_caches() + for sp in site.getsitepackages(): + site.addsitedir(sp) + + yield + + subprocess.run( + [sys.executable, "-m", "pip", "uninstall", "-y", "mcp-dummy-plugin"], + check=True, + capture_output=True, + text=True, + ) diff --git a/tests/aignostics/utils/di_test.py b/tests/aignostics/utils/di_test.py index 37fa32ec5..3d3776251 100644 --- a/tests/aignostics/utils/di_test.py +++ b/tests/aignostics/utils/di_test.py @@ -240,7 +240,7 @@ def clear_di_caches() -> Generator[None, None, None]: @pytest.mark.unit def test_discover_plugin_packages_returns_tuple(clear_di_caches, record_property) -> None: """Test that discover_plugin_packages returns a tuple.""" - record_property("tested-item-id", "SPEC-UTILS-DI") + record_property("tested-item-id", "SPEC-UTILS-SERVICE") result = discover_plugin_packages() assert isinstance(result, tuple) @@ -248,7 +248,7 @@ def test_discover_plugin_packages_returns_tuple(clear_di_caches, record_property @pytest.mark.unit def test_discover_plugin_packages_uses_correct_entry_point_group(clear_di_caches, record_property) -> None: """Test that discover_plugin_packages uses the correct entry point group.""" - record_property("tested-item-id", "SPEC-UTILS-DI") + record_property("tested-item-id", "SPEC-UTILS-SERVICE") assert PLUGIN_ENTRY_POINT_GROUP == "aignostics.plugins" @@ -258,7 +258,7 @@ def test_discover_plugin_packages_extracts_values_from_entry_points( mock_entry_points: Mock, clear_di_caches, record_property ) -> None: """Test that discover_plugin_packages extracts values from entry points.""" - record_property("tested-item-id", "SPEC-UTILS-DI") + record_property("tested-item-id", "SPEC-UTILS-SERVICE") # Setup mock entry points mock_ep1 = MagicMock() mock_ep1.value = "plugin_one" @@ -280,7 +280,7 @@ def test_discover_plugin_packages_returns_empty_tuple_when_no_plugins( mock_entry_points: Mock, clear_di_caches, record_property ) -> None: """Test that discover_plugin_packages returns empty tuple when no plugins registered.""" - record_property("tested-item-id", "SPEC-UTILS-DI") + record_property("tested-item-id", "SPEC-UTILS-SERVICE") mock_entry_points.return_value = [] result = discover_plugin_packages() @@ -292,7 +292,7 @@ def test_discover_plugin_packages_returns_empty_tuple_when_no_plugins( @patch("aignostics.utils._di.entry_points") def test_discover_plugin_packages_is_cached(mock_entry_points: Mock, clear_di_caches, record_property) -> None: """Test that discover_plugin_packages caches results.""" - record_property("tested-item-id", "SPEC-UTILS-DI") + record_property("tested-item-id", "SPEC-UTILS-SERVICE") mock_ep = MagicMock() mock_ep.value = "cached_plugin" mock_entry_points.return_value = [mock_ep] @@ -309,7 +309,7 @@ def test_discover_plugin_packages_is_cached(mock_entry_points: Mock, clear_di_ca @pytest.mark.unit def test_locate_implementations_searches_plugins(clear_di_caches, record_property) -> None: """Test that locate_implementations searches plugin packages.""" - record_property("tested-item-id", "SPEC-UTILS-DI") + record_property("tested-item-id", "SPEC-UTILS-SERVICE") import aignostics.utils._di as di_module plugin_instance = AnotherDummyBase() @@ -339,7 +339,7 @@ def import_side_effect(name: str) -> ModuleType: @pytest.mark.unit def test_locate_implementations_caches_results(clear_di_caches, record_property) -> None: """Test that locate_implementations caches results.""" - record_property("tested-item-id", "SPEC-UTILS-DI") + record_property("tested-item-id", "SPEC-UTILS-SERVICE") import aignostics.utils._di as di_module mock_package = MagicMock() @@ -359,7 +359,7 @@ def test_locate_implementations_caches_results(clear_di_caches, record_property) @pytest.mark.unit def test_locate_subclasses_searches_plugins(clear_di_caches, record_property) -> None: """Test that locate_subclasses searches plugin packages.""" - record_property("tested-item-id", "SPEC-UTILS-DI") + record_property("tested-item-id", "SPEC-UTILS-SERVICE") import aignostics.utils._di as di_module class PluginSubClass(AnotherDummyBase): @@ -391,7 +391,7 @@ def import_side_effect(name: str) -> ModuleType: @pytest.mark.unit def test_locate_subclasses_excludes_base_class(clear_di_caches, record_property) -> None: """Test that locate_subclasses excludes the base class itself.""" - record_property("tested-item-id", "SPEC-UTILS-DI") + record_property("tested-item-id", "SPEC-UTILS-SERVICE") import aignostics.utils._di as di_module mock_package = MagicMock() @@ -411,7 +411,7 @@ def test_locate_subclasses_excludes_base_class(clear_di_caches, record_property) @pytest.mark.unit def test_locate_subclasses_caches_results(clear_di_caches, record_property) -> None: """Test that locate_subclasses caches results.""" - record_property("tested-item-id", "SPEC-UTILS-DI") + record_property("tested-item-id", "SPEC-UTILS-SERVICE") import aignostics.utils._di as di_module mock_package = MagicMock() @@ -431,7 +431,7 @@ def test_locate_subclasses_caches_results(clear_di_caches, record_property) -> N @pytest.mark.unit def test_locate_subclasses_handles_plugin_import_error(clear_di_caches, record_property) -> None: """Test that locate_subclasses handles ImportError for plugin packages gracefully.""" - record_property("tested-item-id", "SPEC-UTILS-DI") + record_property("tested-item-id", "SPEC-UTILS-SERVICE") import aignostics.utils._di as di_module mock_package = MagicMock() @@ -453,7 +453,7 @@ def import_side_effect(name: str) -> ModuleType: @pytest.mark.unit def test_locate_subclasses_handles_module_import_error(clear_di_caches, record_property) -> None: """Test that locate_subclasses handles ImportError for individual modules gracefully.""" - record_property("tested-item-id", "SPEC-UTILS-DI") + record_property("tested-item-id", "SPEC-UTILS-SERVICE") import aignostics.utils._di as di_module mock_package = MagicMock() @@ -482,7 +482,7 @@ def test_locate_implementations_and_subclasses_search_both_plugins_and_main_pack record_property, ) -> None: """Test that both functions search plugins first, then main package.""" - record_property("tested-item-id", "SPEC-UTILS-DI") + record_property("tested-item-id", "SPEC-UTILS-SERVICE") import aignostics.utils._di as di_module import_order: list[str] = [] diff --git a/tests/aignostics/utils/mcp_test.py b/tests/aignostics/utils/mcp_test.py index 0ad17674f..05a5ac6b0 100644 --- a/tests/aignostics/utils/mcp_test.py +++ b/tests/aignostics/utils/mcp_test.py @@ -3,9 +3,6 @@ from __future__ import annotations import asyncio -import subprocess -import sys -from pathlib import Path from typing import TYPE_CHECKING from unittest.mock import patch @@ -190,8 +187,6 @@ def test_mcp_list_tools_empty(record_property) -> None: # Integration Plugin Auto-Discovery Tests # ============================================================================= -DUMMY_PLUGIN_DIR = Path(__file__).resolve().parents[2] / "resources" / "mcp_dummy_plugin" - def _clear_mcp_discovery_caches() -> None: """Invalidate DI and plugin caches so MCP discovery starts fresh.""" @@ -199,45 +194,6 @@ def _clear_mcp_discovery_caches() -> None: discover_plugin_packages.cache_clear() -@pytest.fixture(scope="session") -def install_dummy_mcp_plugin() -> Iterator[None]: - """Install the dummy MCP plugin in editable mode and make it importable. - - Refreshes site-packages so the running interpreter sees the new package - and its entry points without a process restart. - """ - import importlib - import site - - subprocess.run( - [ - sys.executable, - "-m", - "pip", - "install", - "--no-deps", - "-e", - str(DUMMY_PLUGIN_DIR), - ], - check=True, - capture_output=True, - text=True, - ) - - importlib.invalidate_caches() - for sp in site.getsitepackages(): - site.addsitedir(sp) - - yield - - subprocess.run( - [sys.executable, "-m", "pip", "uninstall", "-y", "mcp-dummy-plugin"], - check=True, - capture_output=True, - text=True, - ) - - @pytest.fixture def clear_mcp_caches() -> Iterator[None]: """Clear MCP discovery caches before and after the test.""" @@ -249,9 +205,7 @@ def clear_mcp_caches() -> Iterator[None]: @pytest.mark.integration @pytest.mark.sequential @pytest.mark.timeout(timeout=60) -def test_mcp_server_discovers_and_serves_plugin_tools( - install_dummy_mcp_plugin, clear_mcp_caches, record_property -) -> None: +def test_mcp_server_discovers_and_serves_plugin_tools(install_dummy_plugin, clear_mcp_caches, record_property) -> None: """Integration: entry point registration -> discovery -> mount -> client round-trip.""" record_property("tested-item-id", "TC-UTILS-MCP-01") diff --git a/tests/aignostics/utils/plugin_test.py b/tests/aignostics/utils/plugin_test.py new file mode 100644 index 000000000..3d1651ec5 --- /dev/null +++ b/tests/aignostics/utils/plugin_test.py @@ -0,0 +1,63 @@ +"""Integration tests for plugin CLI and GUI registration.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest +import typer + +from aignostics.utils._di import _implementation_cache, _subclass_cache, discover_plugin_packages +from aignostics.utils._nav import BaseNavBuilder + +if TYPE_CHECKING: + from collections.abc import Iterator + + +def _clear_plugin_caches() -> None: + """Clear DI caches so plugin discovery starts fresh.""" + _implementation_cache.pop(typer.Typer, None) + _subclass_cache.pop(BaseNavBuilder, None) + discover_plugin_packages.cache_clear() + + +@pytest.fixture +def clear_plugin_caches() -> Iterator[None]: + """Clear plugin DI caches before and after each test.""" + _clear_plugin_caches() + yield + _clear_plugin_caches() + + +@pytest.mark.integration +@pytest.mark.sequential +@pytest.mark.timeout(timeout=60) +def test_plugin_cli_registered(install_dummy_plugin, clear_plugin_caches, record_property) -> None: + """Integration: plugin Typer CLI instance is discovered via DI after installation.""" + record_property("tested-item-id", "TC-UTILS-PLUGIN-02") + + from aignostics.utils._di import locate_implementations + + typer_instances = locate_implementations(typer.Typer) + names = [t.info.name for t in typer_instances if hasattr(t, "info") and t.info.name] + + assert "dummy-plugin" in names + + +@pytest.mark.integration +@pytest.mark.sequential +@pytest.mark.timeout(timeout=60) +def test_plugin_nav_builder_registered(install_dummy_plugin, clear_plugin_caches, record_property) -> None: + """Integration: plugin BaseNavBuilder subclass is discovered via DI after installation.""" + record_property("tested-item-id", "TC-UTILS-PLUGIN-03") + + from aignostics.utils._di import locate_subclasses + from aignostics.utils._nav import gui_get_nav_groups + + nav_builder_classes = locate_subclasses(BaseNavBuilder) + class_names = [cls.__name__ for cls in nav_builder_classes] + assert "DummyPluginNavBuilder" in class_names + + nav_groups = gui_get_nav_groups() + group_names = [g.name for g in nav_groups] + assert "Dummy Plugin" in group_names diff --git a/tests/resources/mcp_dummy_plugin/pyproject.toml b/tests/resources/mcp_dummy_plugin/pyproject.toml index 7e37c32b1..65de8cf0d 100644 --- a/tests/resources/mcp_dummy_plugin/pyproject.toml +++ b/tests/resources/mcp_dummy_plugin/pyproject.toml @@ -7,7 +7,7 @@ name = "mcp-dummy-plugin" version = "0.0.1" description = "Dummy MCP plugin for integration testing of plugin auto-discovery." requires-python = ">=3.11" -dependencies = ["fastmcp>=2.0.0,<3"] +dependencies = ["fastmcp>=2.0.0,<3", "typer>=0.12", "aignostics"] [project.entry-points."aignostics.plugins"] mcp_dummy_plugin = "mcp_dummy_plugin" diff --git a/tests/resources/mcp_dummy_plugin/src/mcp_dummy_plugin/__init__.py b/tests/resources/mcp_dummy_plugin/src/mcp_dummy_plugin/__init__.py index 186fed3f5..0c58ddddc 100644 --- a/tests/resources/mcp_dummy_plugin/src/mcp_dummy_plugin/__init__.py +++ b/tests/resources/mcp_dummy_plugin/src/mcp_dummy_plugin/__init__.py @@ -1,5 +1,7 @@ """Dummy MCP plugin for integration testing of plugin auto-discovery.""" +from ._cli import cli from ._mcp import mcp +from ._nav import DummyPluginNavBuilder -__all__ = ["mcp"] +__all__ = ["DummyPluginNavBuilder", "cli", "mcp"] diff --git a/tests/resources/mcp_dummy_plugin/src/mcp_dummy_plugin/_cli.py b/tests/resources/mcp_dummy_plugin/src/mcp_dummy_plugin/_cli.py new file mode 100644 index 000000000..561ccd992 --- /dev/null +++ b/tests/resources/mcp_dummy_plugin/src/mcp_dummy_plugin/_cli.py @@ -0,0 +1,11 @@ +"""Dummy CLI for integration testing of plugin CLI command registration.""" + +import typer + +cli = typer.Typer(name="dummy-plugin", help="Dummy plugin CLI for integration testing.") + + +@cli.command("hello") +def hello() -> None: + """Print a greeting.""" + typer.echo("Hello from dummy plugin!") diff --git a/tests/resources/mcp_dummy_plugin/src/mcp_dummy_plugin/_nav.py b/tests/resources/mcp_dummy_plugin/src/mcp_dummy_plugin/_nav.py new file mode 100644 index 000000000..641cd24b6 --- /dev/null +++ b/tests/resources/mcp_dummy_plugin/src/mcp_dummy_plugin/_nav.py @@ -0,0 +1,17 @@ +"""Dummy nav builder for integration testing of plugin GUI page registration.""" + +from aignostics.utils._nav import BaseNavBuilder, NavItem + + +class DummyPluginNavBuilder(BaseNavBuilder): + """Dummy navigation builder exposed by the dummy plugin for integration testing.""" + + @staticmethod + def get_nav_name() -> str: + """Return the nav group name.""" + return "Dummy Plugin" + + @staticmethod + def get_nav_items() -> list[NavItem]: + """Return dummy navigation items.""" + return [NavItem(icon="extension", label="Dummy Page", target="/dummy-plugin")] diff --git a/uv.lock b/uv.lock index 2ec9080e5..57b6c35a3 100644 --- a/uv.lock +++ b/uv.lock @@ -613,14 +613,14 @@ wheels = [ [[package]] name = "authlib" -version = "1.6.6" +version = "1.6.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/9b/b1661026ff24bc641b76b78c5222d614776b0c085bcfdac9bd15a1cb4b35/authlib-1.6.6.tar.gz", hash = "sha256:45770e8e056d0f283451d9996fbb59b70d45722b45d854d58f32878d0a40c38e", size = 164894, upload-time = "2025-12-12T08:01:41.464Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/98/00d3dd826d46959ad8e32af2dbb2398868fd9fd0683c26e56d0789bd0e68/authlib-1.6.9.tar.gz", hash = "sha256:d8f2421e7e5980cc1ddb4e32d3f5fa659cfaf60d8eaf3281ebed192e4ab74f04", size = 165134, upload-time = "2026-03-02T07:44:01.998Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/51/321e821856452f7386c4e9df866f196720b1ad0c5ea1623ea7399969ae3b/authlib-1.6.6-py2.py3-none-any.whl", hash = "sha256:7d9e9bc535c13974313a87f53e8430eb6ea3d1cf6ae4f6efcd793f2e949143fd", size = 244005, upload-time = "2025-12-12T08:01:40.209Z" }, + { url = "https://files.pythonhosted.org/packages/53/23/b65f568ed0c22f1efacb744d2db1a33c8068f384b8c9b482b52ebdbc3ef6/authlib-1.6.9-py2.py3-none-any.whl", hash = "sha256:f08b4c14e08f0861dc18a32357b33fbcfd2ea86cfe3fe149484b4d764c4a0ac3", size = 244197, upload-time = "2026-03-02T07:44:00.307Z" }, ] [[package]] @@ -3764,14 +3764,14 @@ wheels = [ [[package]] name = "lxml-html-clean" -version = "0.4.3" +version = "0.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "lxml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d9/cb/c9c5bb2a9c47292e236a808dd233a03531f53b626f36259dcd32b49c76da/lxml_html_clean-0.4.3.tar.gz", hash = "sha256:c9df91925b00f836c807beab127aac82575110eacff54d0a75187914f1bd9d8c", size = 21498, upload-time = "2025-10-02T20:49:24.895Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/a4/5c62acfacd69ff4f5db395100f5cfb9b54e7ac8c69a235e4e939fd13f021/lxml_html_clean-0.4.4.tar.gz", hash = "sha256:58f39a9d632711202ed1d6d0b9b47a904e306c85de5761543b90e3e3f736acfb", size = 23899, upload-time = "2026-02-27T09:35:52.911Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/4a/63a9540e3ca73709f4200564a737d63a4c8c9c4dd032bab8535f507c190a/lxml_html_clean-0.4.3-py3-none-any.whl", hash = "sha256:63fd7b0b9c3a2e4176611c2ca5d61c4c07ffca2de76c14059a81a2825833731e", size = 14177, upload-time = "2025-10-02T20:49:23.749Z" }, + { url = "https://files.pythonhosted.org/packages/d9/76/7ffc1d3005cf7749123bc47cb3ea343cd97b0ac2211bab40f57283577d0e/lxml_html_clean-0.4.4-py3-none-any.whl", hash = "sha256:ce2ef506614ecb85ee1c5fe0a2aa45b06a19514ec7949e9c8f34f06925cfabcb", size = 14565, upload-time = "2026-02-27T09:35:51.86Z" }, ] [[package]]