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 deleted file mode 100644 index 902012792..000000000 --- a/requirements/SHR-UTILS-1.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -itemId: SHR-UTILS-1 -itemTitle: Central MCP Server for SDK and Plugin 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. diff --git a/requirements/SHR-UTILS-2.md b/requirements/SHR-UTILS-2.md new file mode 100644 index 000000000..ec64a952c --- /dev/null +++ b/requirements/SHR-UTILS-2.md @@ -0,0 +1,10 @@ +--- +itemId: SHR-UTILS-2 +itemTitle: SDK Plugin System +itemType: Requirement +Requirement type: ENVIRONMENT +--- + +## Description + +Developers shall be able to extend the SDK with custom plugins. diff --git a/requirements/SWR-UTILS-1-1.md b/requirements/SWR-UTILS-1-1.md deleted file mode 100644 index b526e18c8..000000000 --- a/requirements/SWR-UTILS-1-1.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -itemId: SWR-UTILS-1-1 -itemTitle: MCP Server with Auto-Discovery and CLI Commands -itemHasParent: SHR-UTILS-1 -itemType: Requirement -Requirement type: FUNCTIONAL -Layer: System (backend logic) ---- - -System shall provide a central MCP server that automatically discovers plugin tools via entry-point-based service discovery, mounts them with namespace isolation to prevent tool name collisions, and exposes CLI commands (`mcp run` to start the stdio transport server, `mcp list-tools` to enumerate all registered tools). 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..9dbc9f553 --- /dev/null +++ b/requirements/SWR-UTILS-2-3.md @@ -0,0 +1,10 @@ +--- +itemId: SWR-UTILS-2-3 +itemTitle: Plugin GUI Navigation Integration +itemHasParent: SHR-UTILS-2 +itemType: Requirement +Requirement type: FUNCTIONAL +Layer: System (backend logic) +--- + +System shall automatically register GUI navigation entries contributed by plugin modules into the SDK graphical user interface. diff --git a/requirements/SWR-UTILS-2-4.md b/requirements/SWR-UTILS-2-4.md new file mode 100644 index 000000000..ce2ab2d44 --- /dev/null +++ b/requirements/SWR-UTILS-2-4.md @@ -0,0 +1,10 @@ +--- +itemId: SWR-UTILS-2-4 +itemTitle: Plugin MCP Server Integration +itemHasParent: SHR-UTILS-2 +itemType: Requirement +Requirement type: FUNCTIONAL +Layer: System (backend logic) +--- + +System shall automatically register MCP servers contributed by plugin modules into the SDK MCP server. diff --git a/specifications/SPEC-UTILS-SERVICE.md b/specifications/SPEC-UTILS-SERVICE.md index 833ff6e5b..5b8db2d7b 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-2-1, SWR-UTILS-2-2, SWR-UTILS-2-3, SWR-UTILS-2-4, 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 @@ -28,7 +28,10 @@ The Utils Module shall: - **[FR-07]** Implement settings management with validation, serialization, and sensitive data handling - **[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-10]** Automatically register MCP servers contributed by plugin modules into the SDK MCP server +- **[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 navigation entries contributed by plugin modules into the SDK graphical user interface ### 1.3 Non-Functional Requirements diff --git a/tests/aignostics/utils/TC-UTILS-MCP-01.feature b/tests/aignostics/utils/TC-UTILS-MCP-01.feature index 7b8f1f0f0..752ee7843 100644 --- a/tests/aignostics/utils/TC-UTILS-MCP-01.feature +++ b/tests/aignostics/utils/TC-UTILS-MCP-01.feature @@ -4,12 +4,12 @@ Feature: MCP Server Plugin Auto-Discovery points, mounts them with namespace isolation, and serves them to MCP clients. @tests:SPEC-UTILS-SERVICE - @tests:SWR-UTILS-1-1 + @tests:SWR-UTILS-2-4 @id:TC-UTILS-MCP-01 Scenario: Server discovers plugin tools via entry points and serves them to a client Given a plugin package registers an entry point under "aignostics.plugins" And the plugin exposes a FastMCP instance with tools "dummy_echo" and "dummy_add" - When the MCP server is created via mcp_create_server() - And a client connects and lists tools via client.list_tools() + When the SDK MCP server is started + And a client connects and lists available tools Then the returned tool list includes "dummy_plugin_dummy_echo" and "dummy_plugin_dummy_add" And calling "dummy_plugin_dummy_echo" with message "hello" returns "hello" 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..577702ac6 --- /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 plugin is installed + Then the plugin's CLI commands are available 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..515430c30 --- /dev/null +++ b/tests/aignostics/utils/TC-UTILS-PLUGIN-03.feature @@ -0,0 +1,12 @@ +Feature: Plugin GUI Navigation 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 + 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..8bfd2e12d --- /dev/null +++ b/tests/aignostics/utils/conftest.py @@ -0,0 +1,59 @@ +"""Shared fixtures for utils tests.""" + +from __future__ import annotations + +import importlib +import shutil +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. + + Note: Plugin discovery caches (discover_plugin_packages, DI caches) may have + been populated before this fixture runs. Tests that rely on post-install + discovery must pair this fixture with clear_plugin_caches to ensure caches + are reset before and after each test. + + Raises: + subprocess.CalledProcessError: If install fails, or if uninstall fails for + a reason other than the package already being absent. + """ + uv = shutil.which("uv") + install_cmd = ( + [uv, "pip", "install", "--no-deps", "-e", str(DUMMY_PLUGIN_DIR)] + if uv + else [sys.executable, "-m", "pip", "install", "--no-deps", "-e", str(DUMMY_PLUGIN_DIR)] + ) + # Always use pip for uninstall: uv pip uninstall can fail with exit code 2 + # for editable installs it did not itself create the dist-info for. + uninstall_cmd = [sys.executable, "-m", "pip", "uninstall", "-y", "mcp-dummy-plugin"] + + subprocess.run(install_cmd, check=True, capture_output=True, text=True) + + importlib.invalidate_caches() + for sp in site.getsitepackages(): + site.addsitedir(sp) + + yield + + result = subprocess.run(uninstall_cmd, check=False, capture_output=True, text=True) + if result.returncode != 0: + output = (result.stderr or result.stdout or "").lower() + if not any(marker in output for marker in ("not installed", "no packages found")): + raise subprocess.CalledProcessError(result.returncode, result.args, result.stdout, result.stderr) diff --git a/tests/aignostics/utils/di_test.py b/tests/aignostics/utils/di_test.py index f7f8d3c7a..13439ea44 100644 --- a/tests/aignostics/utils/di_test.py +++ b/tests/aignostics/utils/di_test.py @@ -465,6 +465,9 @@ def test_discover_plugin_packages_is_cached(mock_entry_points: Mock, clear_di_ca def test_locate_implementations_searches_plugins(clear_di_caches, record_property) -> None: """Test that locate_implementations shallow-scans plugin packages for top-level exports.""" record_property("tested-item-id", "SPEC-UTILS-SERVICE") + """Test that locate_implementations searches plugin packages.""" + record_property("tested-item-id", "SPEC-UTILS-SERVICE") + import aignostics.utils._di as di_module plugin_instance = AnotherDummyBase() mock_plugin_package = ModuleType("test_plugin") @@ -596,6 +599,7 @@ class _Base: 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-SERVICE") + import aignostics.utils._di as di_module mock_package = MagicMock() mock_package.__path__ = [] @@ -640,6 +644,9 @@ class _Base: def test_locate_subclasses_searches_plugins(clear_di_caches, record_property) -> None: """Test that locate_subclasses shallow-scans plugin packages for top-level exports.""" record_property("tested-item-id", "SPEC-UTILS-SERVICE") + """Test that locate_subclasses searches plugin packages.""" + record_property("tested-item-id", "SPEC-UTILS-SERVICE") + import aignostics.utils._di as di_module class PluginSubClass(AnotherDummyBase): pass @@ -780,6 +787,7 @@ class MainSub(_Base): 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-SERVICE") + import aignostics.utils._di as di_module mock_package = _mock_package() mock_module = ModuleType("aignostics.testmodule") @@ -798,6 +806,7 @@ def test_locate_subclasses_excludes_base_class(clear_di_caches, record_property) 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-SERVICE") + import aignostics.utils._di as di_module mock_package = MagicMock() mock_package.__path__ = [] @@ -817,6 +826,7 @@ def test_locate_subclasses_caches_results(clear_di_caches, record_property) -> N 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-SERVICE") + import aignostics.utils._di as di_module mock_package = MagicMock() mock_package.__path__ = [] @@ -838,6 +848,7 @@ def import_side_effect(name: str) -> ModuleType: 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-SERVICE") + import aignostics.utils._di as di_module mock_package = _mock_package() call_count = 0 @@ -887,6 +898,7 @@ def test_locate_implementations_and_subclasses_search_both_plugins_and_main_pack ) -> None: """Test that both functions search plugins first, then main package.""" 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..87ccef490 --- /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 import BaseNavBuilder +from aignostics.utils._di import _implementation_cache, _subclass_cache, discover_plugin_packages + +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 import gui_get_nav_groups + from aignostics.utils._di import locate_subclasses + + 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..53c14dd4e --- /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 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 a6e2d57eb..57b6c35a3 100644 --- a/uv.lock +++ b/uv.lock @@ -613,14 +613,14 @@ wheels = [ [[package]] name = "authlib" -version = "1.6.7" +version = "1.6.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/49/dc/ed1681bf1339dd6ea1ce56136bad4baabc6f7ad466e375810702b0237047/authlib-1.6.7.tar.gz", hash = "sha256:dbf10100011d1e1b34048c9d120e83f13b35d69a826ae762b93d2fb5aafc337b", size = 164950, upload-time = "2026-02-06T14:04:14.171Z" } +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/f8/00/3ed12264094ec91f534fae429945efbaa9f8c666f3aa7061cc3b2a26a0cd/authlib-1.6.7-py2.py3-none-any.whl", hash = "sha256:c637340d9a02789d2efa1d003a7437d10d3e565237bcb5fcbc6c134c7b95bab0", size = 244115, upload-time = "2026-02-06T14:04:12.141Z" }, + { 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]]