Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a8cf513
feat(utils): split MCP and plugin requirements, add plugin integratio…
omid-aignostics Mar 4, 2026
e422566
chore: exclude tests/resources/ from name-tests-test pre-commit hook
omid-aignostics Mar 4, 2026
67ddbf8
chore: fix regex in name-tests-test pre-commit hook exclusion
omid-aignostics Mar 4, 2026
83a52b5
refactor(tests): centralise dummy plugin install fixture in utils con…
omid-aignostics Mar 4, 2026
d5a0281
chore: fix YAML escape in pre-commit hook exclusion regex
omid-aignostics Mar 4, 2026
0ed3d00
style: reformat test signature to single line per ruff
omid-aignostics Mar 4, 2026
5008493
style: fix ruff formatting in mcp_test.py
omid-aignostics Mar 4, 2026
bb06bc4
chore(deps): upgrade lxml-html-clean to 0.4.4 and authlib to 1.6.9
omid-aignostics Mar 5, 2026
74a5339
fix(tests): address Copilot review feedback on plugin tests
omid-aignostics Mar 5, 2026
01e16f7
docs(requirements): add MCP servers to SHR-UTILS-2 plugin contributio…
omid-aignostics Mar 5, 2026
403ba60
refactor(requirements): restructure UTILS requirements hierarchy
omid-aignostics Mar 5, 2026
5848730
docs(requirements): simplify SWR-UTILS-2-4 wording to match SWR pattern
omid-aignostics Mar 5, 2026
44d08c5
docs(specs): align FR-10 in SPEC-UTILS-SERVICE with SWR-UTILS-2-4
omid-aignostics Mar 5, 2026
2d9cc35
fix(tests): use uv for dummy plugin install to avoid network access
omid-aignostics Mar 5, 2026
01e6b01
docs(requirements): align SWR-UTILS-2-3 and FR-13 with implemented be…
omid-aignostics Mar 5, 2026
b3d57bb
fix(tests): make dummy plugin uninstall best-effort in fixture teardown
omid-aignostics Mar 5, 2026
91c808b
fix(tests): update TC-UTILS-MCP-01 traceability tag from SWR-UTILS-1-…
omid-aignostics Mar 5, 2026
f0f5472
fix(tests): fall back to pip when uv is unavailable in plugin fixture
omid-aignostics Mar 5, 2026
aea6f5c
fix(tests): only suppress uninstall errors when package is already ab…
omid-aignostics Mar 5, 2026
ec4c878
docs(tests): align TC-UTILS-PLUGIN-03 feature title with SWR-UTILS-2-…
omid-aignostics Mar 5, 2026
f764695
docs(tests): remove function names from Gherkin scenario steps
omid-aignostics Mar 5, 2026
1504e1d
fix(tests): use pip instead of uv for dummy plugin teardown uninstall
omid-aignostics Mar 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
10 changes: 0 additions & 10 deletions requirements/SHR-UTILS-1.md

This file was deleted.

10 changes: 10 additions & 0 deletions requirements/SHR-UTILS-2.md
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 0 additions & 10 deletions requirements/SWR-UTILS-1-1.md

This file was deleted.

10 changes: 10 additions & 0 deletions requirements/SWR-UTILS-2-1.md
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 10 additions & 0 deletions requirements/SWR-UTILS-2-2.md
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 10 additions & 0 deletions requirements/SWR-UTILS-2-3.md
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 10 additions & 0 deletions requirements/SWR-UTILS-2-4.md
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 5 additions & 2 deletions specifications/SPEC-UTILS-SERVICE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
6 changes: 3 additions & 3 deletions tests/aignostics/utils/TC-UTILS-MCP-01.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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"
12 changes: 12 additions & 0 deletions tests/aignostics/utils/TC-UTILS-PLUGIN-02.feature
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions tests/aignostics/utils/TC-UTILS-PLUGIN-03.feature
Original file line number Diff line number Diff line change
@@ -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
59 changes: 59 additions & 0 deletions tests/aignostics/utils/conftest.py
Original file line number Diff line number Diff line change
@@ -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)
12 changes: 12 additions & 0 deletions tests/aignostics/utils/di_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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__ = []
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -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__ = []
Expand All @@ -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__ = []
Expand All @@ -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
Expand Down Expand Up @@ -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] = []

Expand Down
48 changes: 1 addition & 47 deletions tests/aignostics/utils/mcp_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -190,54 +187,13 @@ 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."""
_implementation_cache.pop(FastMCP, 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."""
Expand All @@ -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")

Expand Down
Loading
Loading