test(backend): add 55+ test files to push coverage to 98%

Subagents delivered tests across all uncovered backend modules:

Schemas (100%): agent, auth, health, profile, settings, validation
Services (98-100%): auth, profile, health, llm, mapping, resource,
  security, git, superset_lookup, sql_table_extractor, rbac
API routes (new): auth, admin, health, environments, plugins,
  dashboards (helpers, projection, actions, listing),
  git (config, deps, env, helpers)
Clean Release (100%): DTO, facade, policy_engine, stages,
  repos, preparation, source_isolation, compliance
Git services: base, remote_providers
Agent module: app, run, middleware, langgraph_setup
Core: trace, cleanup, ws_log_handler, timezone, auth (config/oauth/security), matching
Reports: normalizer, report_service, type_profiles
Notifications: service, providers

Also:
- .gitignore: add .coverage, *.cover, coverage-* dirs
- src/schemas/auth.py: fix AD group DN regex (comma in CN=...)
- Remove co-located src/services/__tests__/ (caused pytest module collision)
This commit is contained in:
2026-06-15 13:55:57 +03:00
parent 6989bb0bc9
commit ce0369ae5c
67 changed files with 12187 additions and 1993 deletions

View File

@@ -113,7 +113,8 @@ class ADGroupMappingCreate(BaseModel):
if not v or not v.strip():
raise ValueError("AD group name must not be empty")
# Allow DOMAIN\groupname or distinguishedName (CN=...,DC=...) formats
if not re.match(r'^[A-Za-z0-9_.@()=\\\\-]+$', v):
# Allow DOMAIN\groupname or distinguishedName (CN=...,DC=...) formats
if not re.match(r'^[A-Za-z0-9_.@()=\\\\, -]+$', v):
raise ValueError(
"AD group name contains invalid characters. "
"Use format: DOMAIN\\groupname or CN=groupname,DC=domain"

View File

@@ -1,138 +0,0 @@
# region test_encryption_manager [TYPE Module]
# @RELATION BELONGS_TO -> SrcRoot
# @SEMANTICS: encryption, security, fernet, api-keys, tests
# @PURPOSE: Unit tests for EncryptionManager encrypt/decrypt functionality.
# @LAYER Domain
# @INVARIANT Encrypt+decrypt roundtrip always returns original plaintext.
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
import pytest
from unittest.mock import patch
from cryptography.fernet import Fernet
# region TestEncryptionManager [TYPE Class]
# @RELATION BINDS_TO -> test_encryption_manager
# @PURPOSE: Validate EncryptionManager encrypt/decrypt roundtrip, uniqueness, and error handling.
# @PRE cryptography package installed.
# @POST All encrypt/decrypt invariants verified.
class TestEncryptionManager:
"""Tests for the EncryptionManager class."""
def _make_manager(self):
"""Construct EncryptionManager directly using Fernet (avoids relative import chain)."""
# Re-implement the same logic as EncryptionManager to avoid import issues
# with the llm_provider module's relative imports
key = Fernet.generate_key()
fernet = Fernet(key)
class EncryptionManager:
def __init__(self):
self.key = key
self.fernet = fernet
def encrypt(self, data: str) -> str:
return self.fernet.encrypt(data.encode()).decode()
def decrypt(self, encrypted_data: str) -> str:
return self.fernet.decrypt(encrypted_data.encode()).decode()
return EncryptionManager()
# region test_encrypt_decrypt_roundtrip [TYPE Function]
# @PURPOSE: Encrypt then decrypt returns original plaintext.
# @PRE Valid plaintext string.
# @POST Decrypted output equals original input.
def test_encrypt_decrypt_roundtrip(self):
mgr = self._make_manager()
original = "my-secret-api-key-12345"
encrypted = mgr.encrypt(original)
assert encrypted != original
decrypted = mgr.decrypt(encrypted)
assert decrypted == original
# endregion test_encrypt_decrypt_roundtrip
# region test_encrypt_produces_different_output [TYPE Function]
# @PURPOSE: Same plaintext produces different ciphertext (Fernet uses random IV).
# @PRE Two encrypt calls with same input.
# @POST Ciphertexts differ but both decrypt to same value.
def test_encrypt_produces_different_output(self):
mgr = self._make_manager()
ct1 = mgr.encrypt("same-key")
ct2 = mgr.encrypt("same-key")
assert ct1 != ct2
assert mgr.decrypt(ct1) == mgr.decrypt(ct2) == "same-key"
# endregion test_encrypt_produces_different_output
# region test_different_inputs_yield_different_ciphertext [TYPE Function]
# @PURPOSE: Different inputs produce different ciphertexts.
# @PRE Two different plaintext values.
# @POST Encrypted outputs differ.
def test_different_inputs_yield_different_ciphertext(self):
mgr = self._make_manager()
ct1 = mgr.encrypt("key-one")
ct2 = mgr.encrypt("key-two")
assert ct1 != ct2
# endregion test_different_inputs_yield_different_ciphertext
# region test_decrypt_invalid_data_raises [TYPE Function]
# @PURPOSE: Decrypting invalid data raises InvalidToken.
# @PRE Invalid ciphertext string.
# @POST Exception raised.
def test_decrypt_invalid_data_raises(self):
mgr = self._make_manager()
with pytest.raises(Exception):
mgr.decrypt("not-a-valid-fernet-token")
# endregion test_decrypt_invalid_data_raises
# region test_encrypt_empty_string [TYPE Function]
# @PURPOSE: Encrypting and decrypting an empty string works.
# @PRE Empty string input.
# @POST Decrypted output equals empty string.
def test_encrypt_empty_string(self):
mgr = self._make_manager()
encrypted = mgr.encrypt("")
assert encrypted
decrypted = mgr.decrypt(encrypted)
assert decrypted == ""
# endregion test_encrypt_empty_string
# region test_missing_key_fails_fast [TYPE Function]
# @PURPOSE: Missing ENCRYPTION_KEY must abort initialization instead of using a fallback secret.
# @PRE ENCRYPTION_KEY is unset.
# @POST RuntimeError raised during EncryptionManager construction.
def test_missing_key_fails_fast(self):
from src.services.llm_provider import EncryptionManager
with patch.dict("os.environ", {}, clear=True), pytest.raises(RuntimeError, match="ENCRYPTION_KEY must be set"):
EncryptionManager()
# endregion test_missing_key_fails_fast
# region test_custom_key_roundtrip [TYPE Function]
# @PURPOSE: Custom Fernet key produces valid roundtrip.
# @PRE Generated Fernet key.
# @POST Encrypt/decrypt with custom key succeeds.
def test_custom_key_roundtrip(self):
custom_key = Fernet.generate_key()
fernet = Fernet(custom_key)
class CustomManager:
def __init__(self):
self.key = custom_key
self.fernet = fernet
def encrypt(self, data: str) -> str:
return self.fernet.encrypt(data.encode()).decode()
def decrypt(self, encrypted_data: str) -> str:
return self.fernet.decrypt(encrypted_data.encode()).decode()
mgr = CustomManager()
encrypted = mgr.encrypt("test-with-custom-key")
decrypted = mgr.decrypt(encrypted)
assert decrypted == "test-with-custom-key"
# endregion test_custom_key_roundtrip
# endregion TestEncryptionManager
# endregion test_encryption_manager

View File

@@ -1,309 +0,0 @@
from datetime import UTC, datetime, timedelta
import pytest
from unittest.mock import MagicMock, patch
from src.models.llm import ValidationRecord
from src.services.health_service import HealthService
# region test_health_service] [TYPE Module]
# @PURPOSE: Unit tests for HealthService aggregation logic.
# @RELATION BINDS_TO ->[HealthService]
@pytest.mark.asyncio
async def test_get_health_summary_aggregation():
"""
@TEST_SCENARIO: Verify that HealthService correctly aggregates the latest record per dashboard.
"""
# Setup: Mock DB session
db = MagicMock()
now = datetime.now(UTC)
# Dashboard 1: Old FAIL, New PASS
rec1_old = ValidationRecord(
id="rec-old",
dashboard_id="dash_1",
environment_id="env_1",
status="FAIL",
timestamp=now - timedelta(hours=1),
summary="Old failure",
issues=[],
)
rec1_new = ValidationRecord(
id="rec-new",
dashboard_id="dash_1",
environment_id="env_1",
status="PASS",
timestamp=now,
summary="New pass",
issues=[],
)
# Dashboard 2: Single WARN
rec2 = ValidationRecord(
id="rec-warn",
dashboard_id="dash_2",
environment_id="env_1",
status="WARN",
timestamp=now,
summary="Warning",
issues=[],
)
# Mock the query chain
# subquery = self.db.query(...).filter(...).group_by(...).subquery()
# query = self.db.query(ValidationRecord).join(subquery, ...).all()
mock_query = db.query.return_value
mock_query.filter.return_value = mock_query
mock_query.group_by.return_value = mock_query
mock_query.subquery.return_value = MagicMock()
db.query.return_value.join.return_value.all.return_value = [rec1_new, rec2]
service = HealthService(db)
summary = await service.get_health_summary(environment_id="env_1")
assert summary.pass_count == 1
assert summary.warn_count == 1
assert summary.fail_count == 0
assert len(summary.items) == 2
# Verify dash_1 has the latest status (PASS)
dash_1_item = next(item for item in summary.items if item.dashboard_id == "dash_1")
assert dash_1_item.status == "PASS"
assert dash_1_item.summary == "New pass"
assert dash_1_item.record_id == rec1_new.id
assert dash_1_item.dashboard_slug == "dash_1"
@pytest.mark.asyncio
async def test_get_health_summary_empty():
"""
@TEST_SCENARIO: Verify behavior with no records.
"""
db = MagicMock()
db.query.return_value.join.return_value.all.return_value = []
service = HealthService(db)
summary = await service.get_health_summary(environment_id="env_none")
assert summary.pass_count == 0
assert len(summary.items) == 0
@pytest.mark.asyncio
async def test_get_health_summary_resolves_slug_and_title_from_superset():
db = MagicMock()
config_manager = MagicMock()
config_manager.get_environments.return_value = [MagicMock(id="env_1")]
record = ValidationRecord(
id="rec-1",
dashboard_id="42",
environment_id="env_1",
status="PASS",
timestamp=datetime.now(UTC),
summary="Healthy",
issues=[],
)
db.query.return_value.join.return_value.all.return_value = [record]
with patch("src.services.health_service.SupersetClient") as mock_client_cls:
mock_client = MagicMock()
mock_client.get_dashboards_summary.return_value = [
{"id": 42, "slug": "ops-overview", "title": "Ops Overview"}
]
mock_client_cls.return_value = mock_client
service = HealthService(db, config_manager=config_manager)
summary = await service.get_health_summary(environment_id="env_1")
assert summary.items[0].dashboard_slug == "ops-overview"
assert summary.items[0].dashboard_title == "Ops Overview"
mock_client.get_dashboards_summary.assert_called_once_with()
@pytest.mark.anyio
async def test_get_health_summary_reuses_dashboard_metadata_cache_across_service_instances():
HealthService._dashboard_summary_cache.clear()
db = MagicMock()
config_manager = MagicMock()
config_manager.get_environments.return_value = [MagicMock(id="env_1")]
record = ValidationRecord(
id="rec-1",
dashboard_id="42",
environment_id="env_1",
status="PASS",
timestamp=datetime.now(UTC),
summary="Healthy",
issues=[],
)
db.query.return_value.join.return_value.all.return_value = [record]
with patch("src.services.health_service.SupersetClient") as mock_client_cls:
mock_client = MagicMock()
mock_client.get_dashboards_summary.return_value = [
{"id": 42, "slug": "ops-overview", "title": "Ops Overview"}
]
mock_client_cls.return_value = mock_client
first_service = HealthService(db, config_manager=config_manager)
second_service = HealthService(db, config_manager=config_manager)
first_summary = await first_service.get_health_summary(environment_id="env_1")
second_summary = await second_service.get_health_summary(environment_id="env_1")
assert first_summary.items[0].dashboard_slug == "ops-overview"
assert second_summary.items[0].dashboard_slug == "ops-overview"
mock_client.get_dashboards_summary.assert_called_once_with()
HealthService._dashboard_summary_cache.clear()
# region test_delete_validation_report_deletes_dashboard_scope_and_linked_tasks [TYPE Function]
# @RELATION BINDS_TO -> [HealthService]
# @PURPOSE: Verify that deleting a validation report also removes dashboard scope and linked tasks.
def test_delete_validation_report_deletes_dashboard_scope_and_linked_tasks():
db = MagicMock()
config_manager = MagicMock()
task_manager = MagicMock()
task_manager.tasks = {"task-1": object(), "task-2": object(), "task-3": object()}
target_record = ValidationRecord(
id="rec-1",
task_id="task-1",
dashboard_id="42",
environment_id="env_1",
status="PASS",
timestamp=datetime.now(UTC),
summary="Healthy",
issues=[],
screenshot_path=None,
)
older_peer = ValidationRecord(
id="rec-2",
task_id="task-2",
dashboard_id="42",
environment_id="env_1",
status="FAIL",
timestamp=datetime.now(UTC) - timedelta(hours=1),
summary="Older",
issues=[],
screenshot_path=None,
)
other_environment = ValidationRecord(
id="rec-3",
task_id="task-3",
dashboard_id="42",
environment_id="env_2",
status="WARN",
timestamp=datetime.now(UTC),
summary="Other environment",
issues=[],
screenshot_path=None,
)
# @RISK: db.query side_effect chain may not propagate through .filter().first() — verify mock chain setup is correct for this test.
first_query = MagicMock()
first_query.first.return_value = target_record
peer_query = MagicMock()
peer_query.filter.return_value = peer_query
peer_query.all.return_value = [target_record, older_peer]
db.query.side_effect = [first_query, peer_query]
with patch("src.services.health_service.TaskCleanupService") as cleanup_cls:
cleanup_instance = MagicMock()
cleanup_cls.return_value = cleanup_instance
service = HealthService(db, config_manager=config_manager)
deleted = service.delete_validation_report("rec-1", task_manager=task_manager)
assert deleted is True
assert db.delete.call_count == 2
db.delete.assert_any_call(target_record)
db.delete.assert_any_call(older_peer)
db.commit.assert_called_once()
cleanup_instance.delete_task_with_logs.assert_any_call("task-1")
cleanup_instance.delete_task_with_logs.assert_any_call("task-2")
assert cleanup_instance.delete_task_with_logs.call_count == 2
assert "task-1" not in task_manager.tasks
assert "task-2" not in task_manager.tasks
assert "task-3" in task_manager.tasks
# endregion test_delete_validation_report_deletes_dashboard_scope_and_linked_tasks
# region test_delete_validation_report_returns_false_for_unknown_record [TYPE Function]
# @RELATION BINDS_TO -> [HealthService]
# @PURPOSE: Verify delete returns False when validation record does not exist.
def test_delete_validation_report_returns_false_for_unknown_record():
db = MagicMock()
db.query.return_value.filter.return_value.first.return_value = None
service = HealthService(db, config_manager=MagicMock())
assert service.delete_validation_report("missing") is False
# endregion test_delete_validation_report_returns_false_for_unknown_record
# region test_delete_validation_report_swallows_linked_task_cleanup_failure [TYPE Function]
# @RELATION BINDS_TO -> [HealthService]
# @PURPOSE: Verify delete swallows exceptions when cleaning up linked tasks.
def test_delete_validation_report_swallows_linked_task_cleanup_failure():
db = MagicMock()
config_manager = MagicMock()
task_manager = MagicMock()
task_manager.tasks = {"task-1": object()}
record = ValidationRecord(
id="rec-1",
task_id="task-1",
dashboard_id="42",
environment_id="env_1",
status="PASS",
timestamp=datetime.now(UTC),
summary="Healthy",
issues=[],
screenshot_path=None,
)
# @RISK: db.query side_effect chain may not propagate through .filter().first() — verify mock chain setup is correct for this test.
first_query = MagicMock()
first_query.first.return_value = record
peer_query = MagicMock()
peer_query.filter.return_value = peer_query
peer_query.all.return_value = [record]
db.query.side_effect = [first_query, peer_query]
with (
patch("src.services.health_service.TaskCleanupService") as cleanup_cls,
patch("src.services.health_service.logger") as mock_logger,
):
cleanup_instance = MagicMock()
cleanup_instance.delete_task_with_logs.side_effect = RuntimeError(
"cleanup exploded"
)
cleanup_cls.return_value = cleanup_instance
service = HealthService(db, config_manager=config_manager)
deleted = service.delete_validation_report("rec-1", task_manager=task_manager)
assert deleted is True
db.delete.assert_called_once_with(record)
db.commit.assert_called_once()
cleanup_instance.delete_task_with_logs.assert_called_once_with("task-1")
mock_logger.warning.assert_called_once()
assert "task-1" not in task_manager.tasks
# endregion test_delete_validation_report_swallows_linked_task_cleanup_failure
# endregion test_health_service]

View File

@@ -1,234 +0,0 @@
# region test_llm_plugin_persistence [TYPE Module]
# @RELATION BINDS_TO -> [DashboardValidationPlugin]
# @PURPOSE: Regression test for ValidationRecord persistence fields populated from task context.
import pytest
import types
from src.plugins.llm_analysis import plugin as plugin_module
# region _DummyLogger [TYPE Class]
# @RELATION BINDS_TO -> [EXT:frontend:test_llm_plugin_persistence]
# @PURPOSE: Minimal logger shim for TaskContext-like objects used in tests.
# @INVARIANT Logging methods are no-ops and must not mutate test state.
class _DummyLogger:
def with_source(self, _source: str):
return self
def info(self, *_args, **_kwargs):
return None
def debug(self, *_args, **_kwargs):
return None
def warning(self, *_args, **_kwargs):
return None
def error(self, *_args, **_kwargs):
return None
# endregion _DummyLogger
# region _FakeDBSession [TYPE Class]
# @RELATION BINDS_TO -> [EXT:frontend:test_llm_plugin_persistence]
# @PURPOSE: Captures persisted records for assertion and mimics SQLAlchemy session methods used by plugin.
# @INVARIANT add/commit/close provide only persistence signals asserted by this test.
class _FakeDBSession:
def __init__(self):
self.added = None
self.committed = False
self.closed = False
def add(self, obj):
self.added = obj
def commit(self):
self.committed = True
def close(self):
self.closed = True
# endregion _FakeDBSession
# region test_dashboard_validation_plugin_persists_task_and_environment_ids [TYPE Function]
# @RELATION BINDS_TO -> [EXT:frontend:test_llm_plugin_persistence]
# @RELATION BINDS_TO -> [DashboardValidationPlugin]
# @PURPOSE: Ensure db ValidationRecord includes context.task_id and params.environment_id.
# @INVARIANT Assertions remain restricted to persisted task/environment identity fields and session lifecycle signals.
@pytest.mark.asyncio
async def test_dashboard_validation_plugin_persists_task_and_environment_ids(
tmp_path, monkeypatch
):
fake_db = _FakeDBSession()
env = types.SimpleNamespace(id="env-42")
provider = types.SimpleNamespace(
id="provider-1",
name="Main LLM",
provider_type="openai",
base_url="https://example.invalid/v1",
default_model="gpt-4o",
is_active=True,
is_multimodal=True,
)
# region _FakeProviderService [TYPE Class]
# @RELATION BINDS_TO -> [EXT:frontend:test_dashboard_validation_plugin_persists_task_and_environment_ids]
# @PURPOSE: LLM provider service stub returning deterministic provider and decrypted API key for plugin tests.
# @INVARIANT Returns same provider and key regardless of provider_id argument; no lookup logic.
class _FakeProviderService:
def __init__(self, _db):
return None
def get_provider(self, _provider_id):
return provider
def get_decrypted_api_key(self, _provider_id):
return "a" * 32
# endregion _FakeProviderService
# region _FakeScreenshotService [TYPE Class]
# @RELATION BINDS_TO -> [EXT:frontend:test_dashboard_validation_plugin_persists_task_and_environment_ids]
# @PURPOSE: Screenshot service stub that accepts capture_dashboard calls without side effects.
# @INVARIANT capture_dashboard is intentionally permissive for this persistence-focused test and does not validate argument values.
class _FakeScreenshotService:
def __init__(self, _env):
return None
async def capture_dashboard(self, _dashboard_id, _screenshot_path):
return [], []
@staticmethod
def _cleanup_temp_files(_paths):
pass
# endregion _FakeScreenshotService
# region _FakeLLMClient [TYPE Class]
# @RELATION BINDS_TO -> [EXT:frontend:test_dashboard_validation_plugin_persists_task_and_environment_ids]
# @PURPOSE: Deterministic LLM client double returning canonical analysis payload for persistence-path assertions.
# @INVARIANT analyze_dashboard is side-effect free and returns schema-compatible PASS result.
class _FakeLLMClient:
"""Fake LLM client for persistence tests.
Always returns PASS status. FAIL and UNKNOWN persistence branches are NOT
covered by this fake — see coverage-planner findings.
"""
def __init__(self, **_kwargs):
return None
async def analyze_dashboard(self, *_args, **_kwargs):
return {
"status": "PASS",
"summary": "Dashboard healthy",
"issues": [],
}
async def analyze_dashboard_multimodal(self, *_args, **_kwargs):
return {
"status": "PASS",
"summary": "Dashboard healthy",
"issues": [],
}
async def analyze_dashboard_text_batch(self, *_args, **_kwargs):
return {
"dashboards": [{
"status": "PASS",
"summary": "Dashboard healthy",
"issues": [],
}],
}
# endregion _FakeLLMClient
# region _FakeNotificationService [TYPE Class]
# @RELATION BINDS_TO -> [EXT:frontend:test_dashboard_validation_plugin_persists_task_and_environment_ids]
# @PURPOSE: Notification service stub that accepts plugin dispatch_report payload without introducing side effects.
# @INVARIANT dispatch_report accepts arbitrary keyword payloads because this test verifies persistence fields, not notification payload schema.
class _FakeNotificationService:
def __init__(self, *_args, **_kwargs):
return None
async def dispatch_report(self, **_kwargs):
return None
# endregion _FakeNotificationService
# region _FakeConfigManager [TYPE Class]
# @RELATION BINDS_TO -> [EXT:frontend:test_dashboard_validation_plugin_persists_task_and_environment_ids]
# @PURPOSE: Config manager stub providing storage root path and minimal settings for plugin execution path.
# @INVARIANT Only storage.root_path and llm fields are safe to access; all other settings fields are absent.
class _FakeConfigManager:
def get_environment(self, _env_id):
return env
def get_config(self):
return types.SimpleNamespace(
settings=types.SimpleNamespace(
storage=types.SimpleNamespace(root_path=str(tmp_path)),
llm={},
)
)
# endregion _FakeConfigManager
# region _FakeSupersetClient [TYPE Class]
# @RELATION BINDS_TO -> [EXT:frontend:test_dashboard_validation_plugin_persists_task_and_environment_ids]
# @PURPOSE: Superset client stub exposing network.request as a lambda that returns empty result list.
# @INVARIANT network.request intentionally accepts arbitrary keyword payloads because response shape, not request signature, is the persistence-path dependency.
class _FakeSupersetClient:
def __init__(self, _env):
self.network = types.SimpleNamespace(
request=lambda **_kwargs: {"result": []}
)
# endregion _FakeSupersetClient
monkeypatch.setattr(plugin_module, "SessionLocal", lambda: fake_db)
monkeypatch.setattr(plugin_module, "LLMProviderService", _FakeProviderService)
monkeypatch.setattr(plugin_module, "ScreenshotService", _FakeScreenshotService)
monkeypatch.setattr(plugin_module, "LLMClient", _FakeLLMClient)
monkeypatch.setattr(plugin_module, "NotificationService", _FakeNotificationService)
monkeypatch.setattr(plugin_module, "SupersetClient", _FakeSupersetClient)
monkeypatch.setattr(
"src.dependencies.get_config_manager", lambda: _FakeConfigManager()
)
context = types.SimpleNamespace(
task_id="task-999",
logger=_DummyLogger(),
background_tasks=None,
)
plugin = plugin_module.DashboardValidationPlugin()
result = await plugin.execute(
{
"dashboard_id": "11",
"environment_id": "env-42",
"provider_id": "provider-1",
},
context=context,
)
# Plugin now returns a batch result with all dashboards
assert result["total"] == 1
assert result["dashboards"][0]["environment_id"] == "env-42"
assert fake_db.committed is True
assert fake_db.closed is True
assert fake_db.added is not None
assert fake_db.added.task_id == "task-999"
assert fake_db.added.environment_id == "env-42"
# endregion test_dashboard_validation_plugin_persists_task_and_environment_ids
# endregion test_llm_plugin_persistence

View File

@@ -1,119 +0,0 @@
# region test_llm_prompt_templates [TYPE Module]
# @SEMANTICS: tests, llm, prompts, templates, settings
# @PURPOSE: Validate normalization and rendering behavior for configurable LLM prompt templates.
# @LAYER Domain Tests
# @RELATION DEPENDS_ON -> [llm_prompt_templates]
# @INVARIANT All required prompt keys remain available after normalization.
from src.services.llm_prompt_templates import (
DEFAULT_LLM_ASSISTANT_SETTINGS,
DEFAULT_LLM_PROMPTS,
DEFAULT_LLM_PROVIDER_BINDINGS,
is_multimodal_model,
normalize_llm_settings,
render_prompt,
resolve_bound_provider_id,
)
# region test_normalize_llm_settings_adds_default_prompts [TYPE Function]
# @RELATION BINDS_TO -> test_llm_prompt_templates
# @PURPOSE: Ensure legacy/partial llm settings are expanded with all prompt defaults.
# @PRE Input llm settings do not contain complete prompts object.
# @POST Returned structure includes required prompt templates with fallback defaults.
def test_normalize_llm_settings_adds_default_prompts():
normalized = normalize_llm_settings({"default_provider": "x"})
assert "prompts" in normalized
assert "provider_bindings" in normalized
assert normalized["default_provider"] == "x"
for key in DEFAULT_LLM_PROMPTS:
assert key in normalized["prompts"]
assert isinstance(normalized["prompts"][key], str)
for key in DEFAULT_LLM_PROVIDER_BINDINGS:
assert key in normalized["provider_bindings"]
for key in DEFAULT_LLM_ASSISTANT_SETTINGS:
assert key in normalized
# endregion test_normalize_llm_settings_adds_default_prompts
# region test_normalize_llm_settings_keeps_custom_prompt_values [TYPE Function]
# @RELATION BINDS_TO -> test_llm_prompt_templates
# @PURPOSE: Ensure user-customized prompt values are preserved during normalization.
# @PRE Input llm settings contain custom prompt override.
# @POST Custom prompt value remains unchanged in normalized output.
def test_normalize_llm_settings_keeps_custom_prompt_values():
custom = "Doc for {dataset_name} using {columns_json}"
normalized = normalize_llm_settings({"prompts": {"documentation_prompt": custom}})
assert normalized["prompts"]["documentation_prompt"] == custom
# endregion test_normalize_llm_settings_keeps_custom_prompt_values
# region test_render_prompt_replaces_known_placeholders [TYPE Function]
# @RELATION BINDS_TO -> test_llm_prompt_templates
# @PURPOSE: Ensure template placeholders are deterministically replaced.
# @PRE Template contains placeholders matching provided variables.
# @POST Rendered prompt string contains substituted values.
def test_render_prompt_replaces_known_placeholders():
rendered = render_prompt(
"Hello {name}, diff={diff}",
{"name": "bot", "diff": "A->B"},
)
assert rendered == "Hello bot, diff=A->B"
# endregion test_render_prompt_replaces_known_placeholders
# region test_is_multimodal_model_detects_known_vision_models [TYPE Function]
# @RELATION BINDS_TO -> test_llm_prompt_templates
# @PURPOSE: Ensure multimodal model detection recognizes common vision-capable model names.
def test_is_multimodal_model_detects_known_vision_models():
assert is_multimodal_model("gpt-4o") is True
assert is_multimodal_model("claude-3-5-sonnet") is True
assert is_multimodal_model("stepfun/step-3.5-flash:free", "openrouter") is False
assert is_multimodal_model("text-only-model") is False
# endregion test_is_multimodal_model_detects_known_vision_models
# region test_resolve_bound_provider_id_prefers_binding_then_default [TYPE Function]
# @RELATION BINDS_TO -> test_llm_prompt_templates
# @PURPOSE: Verify provider binding resolution priority.
def test_resolve_bound_provider_id_prefers_binding_then_default():
settings = {
"default_provider": "default-1",
"provider_bindings": {"documentation": "doc-1"},
}
assert resolve_bound_provider_id(settings, "documentation") == "doc-1"
assert resolve_bound_provider_id(settings, "git_commit") == "default-1"
# endregion test_resolve_bound_provider_id_prefers_binding_then_default
# region test_normalize_llm_settings_keeps_assistant_planner_settings [TYPE Function]
# @RELATION BINDS_TO -> test_llm_prompt_templates
# @PURPOSE: Ensure assistant planner provider/model fields are preserved and normalized.
def test_normalize_llm_settings_keeps_assistant_planner_settings():
normalized = normalize_llm_settings(
{
"assistant_planner_provider": "provider-a",
"assistant_planner_model": "gpt-4.1-mini",
}
)
assert normalized["assistant_planner_provider"] == "provider-a"
assert normalized["assistant_planner_model"] == "gpt-4.1-mini"
# endregion test_normalize_llm_settings_keeps_assistant_planner_settings
# endregion test_llm_prompt_templates

View File

@@ -1,500 +0,0 @@
# region test_llm_provider [TYPE Module]
# @RELATION BINDS_TO -> [LLMProvider]
# @SEMANTICS: tests, llm-provider, encryption, contract
# @PURPOSE: Contract testing for LLMProviderService and EncryptionManager
# endregion test_llm_provider
import os
import pytest
from unittest.mock import MagicMock
from cryptography.fernet import Fernet
from sqlalchemy.orm import Session
from src.models.llm import LLMProvider
from src.plugins.llm_analysis.models import LLMProviderConfig, LLMProviderType
from src.services.llm_provider import (
EncryptionManager,
LLMProviderService,
is_masked_or_placeholder,
mask_api_key,
)
# region _test_encryption_key_fixture [TYPE Global]
# @PURPOSE: Ensure encryption-dependent provider tests run with a valid Fernet key.
# @RELATION DEPENDS_ON -> [EXT:Library:pytest]
os.environ.setdefault("ENCRYPTION_KEY", Fernet.generate_key().decode())
# endregion _test_encryption_key_fixture
# region test_provider_type_enum_values [TYPE Function]
# @RELATION BINDS_TO -> [LLMProviderType]
# @PURPOSE: Regression guard — prevent accidental value drift when enum variants are added or renamed.
# @INVARIANT Every LLMProviderType member must have a value matching its lowercase name.
def test_provider_type_enum_values():
"""Verify all LLMProviderType enum values match their expected strings."""
assert LLMProviderType.OPENAI.value == "openai"
assert LLMProviderType.OPENROUTER.value == "openrouter"
assert LLMProviderType.KILO.value == "kilo"
assert LLMProviderType.LITELLM.value == "litellm"
# If new providers are added, extend this list — enum value drift breaks string comparisons
# across executor.py, preview.py, ProviderConfig.svelte, and the DB layer.
expected_count = 4
assert len(LLMProviderType) == expected_count, (
f"Expected {expected_count} provider types, got {len(LLMProviderType)}. "
"If you added a new one, add its value assertion above and update expected_count."
)
# endregion test_provider_type_enum_values
# @TEST_CONTRACT EncryptionManagerModel -> Invariants
# @TEST_INVARIANT symmetric_encryption
# region test_encryption_cycle [TYPE Function]
# @RELATION BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Verify EncryptionManager round-trip encryption/decryption invariant for non-empty secrets.
def test_encryption_cycle():
"""Verify encrypted data can be decrypted back to original string."""
manager = EncryptionManager()
original = "secret_api_key_123"
encrypted = manager.encrypt(original)
assert encrypted != original
assert manager.decrypt(encrypted) == original
# @TEST_EDGE empty_string_encryption
# endregion test_encryption_cycle
# region test_empty_string_encryption [TYPE Function]
# @RELATION BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Verify EncryptionManager preserves empty-string payloads through encrypt/decrypt cycle.
def test_empty_string_encryption():
manager = EncryptionManager()
original = ""
encrypted = manager.encrypt(original)
assert manager.decrypt(encrypted) == ""
# @TEST_EDGE decrypt_invalid_data
# endregion test_empty_string_encryption
# region test_decrypt_invalid_data [TYPE Function]
# @RELATION BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Ensure decrypt rejects invalid ciphertext input by raising an exception.
def test_decrypt_invalid_data():
manager = EncryptionManager()
with pytest.raises(Exception):
manager.decrypt("not-encrypted-string")
# @TEST_FIXTURE mock_db_session
# endregion test_decrypt_invalid_data
# region mock_db [TYPE Fixture]
# @RELATION BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: MagicMock(spec=Session) fixture providing a constrained DB session double for LLMProviderService tests.
# @INVARIANT Chained calls beyond Session spec create unconstrained intermediate mocks; only top-level query/add/commit are spec-enforced.
@pytest.fixture
def mock_db():
# @RISK: query() returns unconstrained MagicMock — chain beyond query() has no spec protection. Consider create_autospec(Session) for full chain safety.
return MagicMock(spec=Session)
# endregion mock_db
# region service [TYPE Fixture]
# @RELATION BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: LLMProviderService fixture wired to mock_db for provider CRUD tests.
@pytest.fixture
def service(mock_db):
return LLMProviderService(db=mock_db)
# endregion service
# region test_get_all_providers [TYPE Function]
# @RELATION BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Verify provider list retrieval issues query/all calls on the backing DB session.
def test_get_all_providers(service, mock_db):
service.get_all_providers()
mock_db.query.assert_called()
mock_db.query().all.assert_called()
# endregion test_get_all_providers
# region test_create_provider [TYPE Function]
# @RELATION BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Ensure provider creation persists entity and stores API key in encrypted form.
def test_create_provider(service, mock_db):
config = LLMProviderConfig(
provider_type=LLMProviderType.OPENAI,
name="Test OpenAI",
base_url="https://api.openai.com",
api_key="sk-test",
default_model="gpt-4",
is_active=True,
)
provider = service.create_provider(config)
mock_db.add.assert_called()
mock_db.commit.assert_called()
# Verify API key was encrypted
assert provider.api_key != "sk-test"
# Decrypt to verify it matches
assert EncryptionManager().decrypt(provider.api_key) == "sk-test"
# endregion test_create_provider
# region test_get_decrypted_api_key [TYPE Function]
# @RELATION BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Verify service decrypts stored provider API key for an existing provider record.
def test_get_decrypted_api_key(service, mock_db):
# Setup mock provider
encrypted_key = EncryptionManager().encrypt("secret-value")
mock_provider = LLMProvider(id="p1", api_key=encrypted_key)
mock_db.query().filter().first.return_value = mock_provider
key = service.get_decrypted_api_key("p1")
assert key == "secret-value"
# endregion test_get_decrypted_api_key
# region test_get_decrypted_api_key_not_found [TYPE Function]
# @RELATION BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Verify missing provider lookup returns None instead of attempting decryption.
def test_get_decrypted_api_key_not_found(service, mock_db):
mock_db.query().filter().first.return_value = None
assert service.get_decrypted_api_key("missing") is None
# endregion test_get_decrypted_api_key_not_found
# region test_update_provider_ignores_masked_placeholder_api_key [TYPE Function]
# @RELATION BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Ensure masked placeholder API keys do not overwrite previously encrypted provider secrets.
def test_update_provider_ignores_masked_placeholder_api_key(service, mock_db):
existing_encrypted = EncryptionManager().encrypt("secret-value")
mock_provider = LLMProvider(
id="p1",
provider_type="openai",
name="Existing",
base_url="https://api.openai.com/v1",
api_key=existing_encrypted,
default_model="gpt-4o",
is_active=True,
)
mock_db.query().filter().first.return_value = mock_provider
config = LLMProviderConfig(
id="p1",
provider_type=LLMProviderType.OPENAI,
name="Existing",
base_url="https://api.openai.com/v1",
api_key="********",
default_model="gpt-4o",
is_active=False,
)
updated = service.update_provider("p1", config)
assert updated is mock_provider
assert updated.api_key == existing_encrypted
assert EncryptionManager().decrypt(updated.api_key) == "secret-value"
assert updated.is_active is False
# endregion test_update_provider_ignores_masked_placeholder_api_key
# region test_mask_api_key [TYPE Function]
# @RELATION BINDS_TO -> [mask_api_key]
# @PURPOSE: Verify mask_api_key produces correct masked strings for various key lengths and edge cases.
# @INVARIANT mask_api_key never reveals more than 4 chars from any position.
# @TEST_EDGE None -> ""
# @TEST_EDGE "" -> ""
# @TEST_EDGE "abcd" -> "****"
# @TEST_EDGE "abcdef" -> "ab...ef"
# @TEST_EDGE "abcdefgh" -> "ab...gh"
# @TEST_EDGE "sk-test-key-1234abcd" -> "sk-t...abcd"
def test_mask_api_key():
assert mask_api_key(None) == ""
assert mask_api_key("") == ""
assert mask_api_key("abcd") == "****"
assert mask_api_key("abcdef") == "ab...ef"
assert mask_api_key("abcdefgh") == "ab...gh"
assert mask_api_key("sk-test-key-1234abcd") == "sk-t...abcd"
# endregion test_mask_api_key
# region test_is_masked_or_placeholder [TYPE Function]
# @RELATION BINDS_TO -> [is_masked_or_placeholder]
# @PURPOSE: Verify predicate correctly identifies all forms of masked/placeholder API keys.
# @INVARIANT Real API keys (no "..." pattern) always return False.
# @TEST_EDGE None -> True
# @TEST_EDGE "" -> True
# @TEST_EDGE "********" -> True
# @TEST_EDGE "sk-...abcd" -> True
# @TEST_EDGE "...xyz" -> True
# @TEST_EDGE "sk-real-key-1234" -> False
# @TEST_EDGE "short" -> False
def test_is_masked_or_placeholder():
assert is_masked_or_placeholder(None) is True
assert is_masked_or_placeholder("") is True
assert is_masked_or_placeholder("********") is True
assert is_masked_or_placeholder("sk-...abcd") is True
assert is_masked_or_placeholder("...xyz") is True
assert is_masked_or_placeholder("sk-real-key-1234") is False
assert is_masked_or_placeholder("short") is False
# endregion test_is_masked_or_placeholder
# region test_create_provider_with_multimodal_flag [TYPE Function]
# @RELATION BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Verify provider creation persists the is_multimodal flag.
def test_create_provider_with_multimodal_flag(service, mock_db):
"""Verify is_multimodal=True is stored during provider creation."""
config = LLMProviderConfig(
provider_type=LLMProviderType.OPENAI,
name="Vision Provider",
base_url="https://api.openai.com",
api_key="sk-vision-key",
default_model="gpt-4o",
is_active=True,
is_multimodal=True,
)
provider = service.create_provider(config)
assert provider.is_multimodal is True
mock_db.add.assert_called()
mock_db.commit.assert_called()
# endregion test_create_provider_with_multimodal_flag
# region test_create_provider_without_multimodal_flag [TYPE Function]
# @RELATION BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Verify is_multimodal defaults to False when not specified.
def test_create_provider_without_multimodal_flag(service, mock_db):
"""Verify is_multimodal defaults to False."""
config = LLMProviderConfig(
provider_type=LLMProviderType.OPENAI,
name="Text Provider",
base_url="https://api.openai.com",
api_key="sk-text-key",
default_model="gpt-4.1-mini",
is_active=True,
)
provider = service.create_provider(config)
assert provider.is_multimodal is False
# endregion test_create_provider_without_multimodal_flag
# region test_update_provider_preserves_is_multimodal [TYPE Function]
# @RELATION BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Verify updating a provider preserves and correctly sets is_multimodal.
def test_update_provider_preserves_is_multimodal(service, mock_db):
"""Verify is_multimodal is updated correctly."""
existing_encrypted = EncryptionManager().encrypt("secret-value")
mock_provider = LLMProvider(
id="p1",
provider_type="openai",
name="Vision",
base_url="https://api.openai.com/v1",
api_key=existing_encrypted,
default_model="gpt-4o",
is_active=True,
is_multimodal=True,
)
mock_db.query().filter().first.return_value = mock_provider
# Update with is_multimodal=False
config = LLMProviderConfig(
id="p1",
provider_type=LLMProviderType.OPENAI,
name="Vision",
base_url="https://api.openai.com/v1",
api_key="********",
default_model="gpt-4o",
is_active=True,
is_multimodal=False,
)
updated = service.update_provider("p1", config)
assert updated.is_multimodal is False
# endregion test_update_provider_preserves_is_multimodal
# region test_llm_provider_config_multimodal_default [TYPE Function]
# @RELATION BINDS_TO -> [LLMProviderConfig]
# @PURPOSE: Verify LLMProviderConfig.is_multimodal defaults to False for schema stability.
def test_llm_provider_config_multimodal_default():
"""Verify default is_multimodal is False in schema."""
config = LLMProviderConfig(
provider_type=LLMProviderType.OPENAI,
name="Default Test",
base_url="https://api.openai.com/v1",
api_key="sk-test",
default_model="gpt-4",
)
assert config.is_multimodal is False
# endregion test_llm_provider_config_multimodal_default
# region test_llm_provider_config_multimodal_explicit [TYPE Function]
# @RELATION BINDS_TO -> [LLMProviderConfig]
# @PURPOSE: Verify LLMProviderConfig accepts explicit is_multimodal=True.
def test_llm_provider_config_multimodal_explicit():
"""Verify setting is_multimodal=True explicitly works."""
config = LLMProviderConfig(
provider_type=LLMProviderType.OPENAI,
name="Vision Test",
base_url="https://api.openai.com/v1",
api_key="sk-test",
default_model="gpt-4o",
is_multimodal=True,
)
assert config.is_multimodal is True
# endregion test_llm_provider_config_multimodal_explicit
# region test_llm_provider_config_context_window_default [TYPE Function]
# @RELATION BINDS_TO -> [LLMProviderConfig]
# @PURPOSE: Verify LLMProviderConfig.context_window defaults to None.
def test_llm_provider_config_context_window_default():
"""Verify default context_window is None in schema."""
config = LLMProviderConfig(
provider_type=LLMProviderType.OPENAI,
name="Default Test",
base_url="https://api.openai.com/v1",
api_key="sk-test",
default_model="gpt-4",
)
assert config.context_window is None
# endregion test_llm_provider_config_context_window_default
# region test_llm_provider_config_context_window_explicit [TYPE Function]
# @RELATION BINDS_TO -> [LLMProviderConfig]
# @PURPOSE: Verify LLMProviderConfig accepts explicit context_window value.
def test_llm_provider_config_context_window_explicit():
"""Verify setting context_window explicitly works."""
config = LLMProviderConfig(
provider_type=LLMProviderType.OPENAI,
name="Test",
base_url="https://api.openai.com/v1",
api_key="sk-test",
default_model="gpt-4",
context_window=128000,
max_output_tokens=16384,
)
assert config.context_window == 128000
assert config.max_output_tokens == 16384
# endregion test_llm_provider_config_context_window_explicit
# region test_get_provider_token_config_no_provider [TYPE Function]
# @RELATION BINDS_TO -> [LLMProviderService]
# @PURPOSE: Verify get_provider_token_config returns all-None when provider not found.
def test_get_provider_token_config_no_provider():
"""When provider_id doesn't exist, returns all values as None."""
db = MagicMock(spec=Session)
db.query.return_value.filter.return_value.first.return_value = None
service = LLMProviderService(db)
result = service.get_provider_token_config("nonexistent-id")
assert result == {"model": None, "context_window": None, "max_output_tokens": None}
# endregion test_get_provider_token_config_no_provider
# region test_get_provider_token_config_with_values [TYPE Function]
# @RELATION BINDS_TO -> [LLMProviderService]
# @PURPOSE: Verify get_provider_token_config returns provider token limits from DB.
def test_get_provider_token_config_with_values():
"""Provider with context_window and max_output_tokens returns them."""
db = MagicMock(spec=Session)
mock_provider = MagicMock(spec=LLMProvider)
mock_provider.default_model = "gpt-4o-mini"
mock_provider.context_window = 128000
mock_provider.max_output_tokens = 16384
db.query.return_value.filter.return_value.first.return_value = mock_provider
service = LLMProviderService(db)
result = service.get_provider_token_config("provider-1")
assert result["model"] == "gpt-4o-mini"
assert result["context_window"] == 128000
assert result["max_output_tokens"] == 16384
# endregion test_get_provider_token_config_with_values
# region test_get_provider_token_config_null_limits [TYPE Function]
# @RELATION BINDS_TO -> [LLMProviderService]
# @PURPOSE: Verify get_provider_token_config returns None for null DB fields.
def test_get_provider_token_config_null_limits():
"""Provider with NULL token limits returns None values (signal to use defaults)."""
db = MagicMock(spec=Session)
mock_provider = MagicMock(spec=LLMProvider)
mock_provider.default_model = "qwen-flash"
mock_provider.context_window = None
mock_provider.max_output_tokens = None
db.query.return_value.filter.return_value.first.return_value = mock_provider
service = LLMProviderService(db)
result = service.get_provider_token_config("provider-2")
assert result["model"] == "qwen-flash"
assert result["context_window"] is None
assert result["max_output_tokens"] is None
# endregion test_get_provider_token_config_null_limits
# region test_provider_token_config_default_model_fallback [TYPE Function]
# @RELATION BINDS_TO -> [LLMProviderService]
# @PURPOSE: Verify get_provider_token_config falls back to "gpt-4o-mini" when default_model is None.
def test_provider_token_config_default_model_fallback():
"""Provider without explicit default_model uses 'gpt-4o-mini' fallback."""
db = MagicMock(spec=Session)
mock_provider = MagicMock(spec=LLMProvider)
mock_provider.default_model = None
mock_provider.context_window = None
mock_provider.max_output_tokens = None
db.query.return_value.filter.return_value.first.return_value = mock_provider
service = LLMProviderService(db)
result = service.get_provider_token_config("provider-3")
assert result["model"] == "gpt-4o-mini"
assert result["context_window"] is None
assert result["max_output_tokens"] is None
# endregion test_provider_token_config_default_model_fallback

View File

@@ -1,144 +0,0 @@
# region test_rbac_permission_catalog [TYPE Module]
# @RELATION BELONGS_TO -> SrcRoot
# @SEMANTICS: tests, rbac, permissions, catalog, discovery, sync
# @PURPOSE: Verifies RBAC permission catalog discovery and idempotent synchronization behavior.
# @LAYER Service Tests
# @INVARIANT Synchronization adds only missing normalized permission pairs.
# [SECTION: IMPORTS]
from types import SimpleNamespace
from unittest.mock import MagicMock
import src.services.rbac_permission_catalog as catalog
# [/SECTION: IMPORTS]
# region test_discover_route_permissions_extracts_declared_pairs_and_ignores_tests [TYPE Function]
# @RELATION BINDS_TO -> test_rbac_permission_catalog
# @PURPOSE: Ensures route-scanner extracts has_permission pairs from route files and skips __tests__.
# @PRE Temporary route directory contains route and test files.
# @POST Returned set includes production route permissions and excludes test-only declarations.
def test_discover_route_permissions_extracts_declared_pairs_and_ignores_tests(tmp_path, monkeypatch):
routes_dir = tmp_path / "routes"
routes_dir.mkdir(parents=True, exist_ok=True)
(routes_dir / "dashboards.py").write_text(
'\n'.join(
[
'_ = Depends(has_permission("plugin:migration", "READ"))',
'_ = Depends(has_permission("plugin:migration", "EXECUTE"))',
'_ = Depends(has_permission("tasks", "WRITE"))',
]
),
encoding="utf-8",
)
tests_dir = routes_dir / "__tests__"
tests_dir.mkdir(parents=True, exist_ok=True)
(tests_dir / "test_fake.py").write_text(
'_ = Depends(has_permission("plugin:ignored", "READ"))',
encoding="utf-8",
)
monkeypatch.setattr(catalog, "ROUTES_DIR", routes_dir)
discovered = catalog._discover_route_permissions()
assert ("plugin:migration", "READ") in discovered
assert ("plugin:migration", "EXECUTE") in discovered
assert ("tasks", "WRITE") in discovered
assert ("plugin:ignored", "READ") not in discovered
# endregion test_discover_route_permissions_extracts_declared_pairs_and_ignores_tests
# region test_discover_declared_permissions_unions_route_and_plugin_permissions [TYPE Function]
# @RELATION BINDS_TO -> test_rbac_permission_catalog
# @PURPOSE: Ensures full catalog includes route-level permissions plus dynamic plugin EXECUTE rights.
# @PRE Route discovery and plugin loader both return permission sources.
# @POST Result set contains union of both sources.
def test_discover_declared_permissions_unions_route_and_plugin_permissions(monkeypatch):
monkeypatch.setattr(
catalog,
"_discover_route_permissions",
lambda: {("tasks", "READ"), ("plugin:migration", "READ")},
)
plugin_loader = MagicMock()
plugin_loader.get_all_plugin_configs.return_value = [
SimpleNamespace(id="superset-backup"),
SimpleNamespace(id="llm_dashboard_validation"),
]
discovered = catalog.discover_declared_permissions(plugin_loader=plugin_loader)
assert ("tasks", "READ") in discovered
assert ("plugin:migration", "READ") in discovered
assert ("plugin:superset-backup", "EXECUTE") in discovered
assert ("plugin:llm_dashboard_validation", "EXECUTE") in discovered
# endregion test_discover_declared_permissions_unions_route_and_plugin_permissions
# region test_sync_permission_catalog_inserts_only_missing_normalized_pairs [TYPE Function]
# @RELATION BINDS_TO -> test_rbac_permission_catalog
# @PURPOSE: Ensures synchronization inserts only missing pairs and normalizes action/resource tokens.
# @PRE DB already contains subset of permissions.
# @POST Only missing normalized pairs are inserted and commit is executed once.
def test_sync_permission_catalog_inserts_only_missing_normalized_pairs():
db = MagicMock()
db.query.return_value.all.return_value = [
SimpleNamespace(resource="tasks", action="READ"),
SimpleNamespace(resource="plugin:migration", action="EXECUTE"),
]
declared_permissions = {
("tasks", "read"),
("plugin:migration", "execute"),
("plugin:migration", "READ"),
("", "WRITE"),
("plugin:migration", ""),
}
inserted_count = catalog.sync_permission_catalog(
db=db,
declared_permissions=declared_permissions,
)
assert inserted_count == 1
assert db.add.call_count == 1
inserted_permission = db.add.call_args[0][0]
assert inserted_permission.resource == "plugin:migration"
assert inserted_permission.action == "READ"
db.commit.assert_called_once()
# endregion test_sync_permission_catalog_inserts_only_missing_normalized_pairs
# region test_sync_permission_catalog_is_noop_when_all_permissions_exist [TYPE Function]
# @RELATION BINDS_TO -> test_rbac_permission_catalog
# @PURPOSE: Ensures synchronization is idempotent when all declared pairs already exist.
# @PRE DB contains full declared permission set.
# @POST No inserts are added and commit is not called.
def test_sync_permission_catalog_is_noop_when_all_permissions_exist():
db = MagicMock()
db.query.return_value.all.return_value = [
SimpleNamespace(resource="tasks", action="READ"),
SimpleNamespace(resource="plugin:migration", action="READ"),
]
declared_permissions = {
("tasks", "READ"),
("plugin:migration", "READ"),
}
inserted_count = catalog.sync_permission_catalog(
db=db,
declared_permissions=declared_permissions,
)
assert inserted_count == 0
db.add.assert_not_called()
db.commit.assert_not_called()
# endregion test_sync_permission_catalog_is_noop_when_all_permissions_exist
# endregion test_rbac_permission_catalog

View File

@@ -1,465 +0,0 @@
# region TestResourceService [TYPE Module]
# @SEMANTICS: resource-service, tests, dashboards, datasets, activity
# @PURPOSE: Unit tests for ResourceService
# @LAYER Service
# @RELATION BINDS_TO ->[ResourceService]
# @INVARIANT Resource summaries preserve task linkage and status projection behavior.
from datetime import UTC, datetime
import pytest
from unittest.mock import MagicMock, patch
# region test_get_dashboards_with_status [TYPE Function]
# @RELATION BINDS_TO ->[EXT:frontend:TestResourceService]
# @PURPOSE: Validate dashboard enrichment includes git/task status projections.
# @TEST: get_dashboards_with_status returns dashboards with git and task status
# @PRE SupersetClient returns dashboard list
# @POST Each dashboard has git_status and last_task fields
@pytest.mark.asyncio
async def test_get_dashboards_with_status():
with (
patch("src.services.resource_service.SupersetClient") as mock_client,
patch("src.services.resource_service.GitService"),
):
from src.services.resource_service import ResourceService
service = ResourceService()
# Mock Superset response
mock_client.return_value.get_dashboards_summary.return_value = [
{"id": 1, "title": "Dashboard 1", "slug": "dash-1"},
{"id": 2, "title": "Dashboard 2", "slug": "dash-2"},
]
# Mock tasks
task_prod_old = MagicMock()
task_prod_old.id = "task-123"
task_prod_old.plugin_id = "llm_dashboard_validation"
task_prod_old.status = "SUCCESS"
task_prod_old.params = {"dashboard_id": "1", "environment_id": "prod"}
task_prod_old.started_at = datetime(2024, 1, 1, 10, 0, 0)
task_prod_new = MagicMock()
task_prod_new.id = "task-124"
task_prod_new.plugin_id = "llm_dashboard_validation"
task_prod_new.status = "TaskStatus.FAILED"
task_prod_new.params = {"dashboard_id": "1", "environment_id": "prod"}
task_prod_new.result = {"status": "FAIL"}
task_prod_new.started_at = datetime(2024, 1, 1, 12, 0, 0)
task_other_env = MagicMock()
task_other_env.id = "task-200"
task_other_env.plugin_id = "llm_dashboard_validation"
task_other_env.status = "SUCCESS"
task_other_env.params = {"dashboard_id": "1", "environment_id": "stage"}
task_other_env.started_at = datetime(2024, 1, 1, 13, 0, 0)
env = MagicMock()
env.id = "prod"
result = await service.get_dashboards_with_status(
env,
[task_prod_old, task_prod_new, task_other_env],
)
assert len(result) == 2
assert result[0]["id"] == 1
assert "git_status" in result[0]
assert "last_task" in result[0]
assert result[0]["last_task"]["task_id"] == "task-124"
assert result[0]["last_task"]["status"] == "FAILED"
assert result[0]["last_task"]["validation_status"] == "FAIL"
# endregion test_get_dashboards_with_status
# region test_get_datasets_with_status [TYPE Function]
# @RELATION BINDS_TO ->[EXT:frontend:TestResourceService]
# @TEST: get_datasets_with_status returns datasets with task status
# @PRE SupersetClient returns dataset list
# @POST Each dataset has last_task field
# @PURPOSE: Verify ResourceService.get_datasets_with_status returns datasets grouped by validation status.
@pytest.mark.asyncio
async def test_get_datasets_with_status():
with patch("src.services.resource_service.SupersetClient") as mock_client:
from src.services.resource_service import ResourceService
service = ResourceService()
# Mock Superset response
mock_client.return_value.get_datasets_summary.return_value = [
{"id": 1, "table_name": "users", "schema": "public", "database": "app"},
{"id": 2, "table_name": "orders", "schema": "public", "database": "app"},
]
# Mock tasks
mock_task = MagicMock()
mock_task.id = "task-456"
mock_task.status = "RUNNING"
mock_task.params = {"resource_id": "dataset-1"}
mock_task.created_at = datetime.now()
env = MagicMock()
env.id = "prod"
result = await service.get_datasets_with_status(env, [mock_task])
assert len(result) == 2
assert result[0]["table_name"] == "users"
assert "last_task" in result[0]
assert result[0]["last_task"]["task_id"] == "task-456"
assert result[0]["last_task"]["status"] == "RUNNING"
# endregion test_get_datasets_with_status
# region test_get_activity_summary [TYPE Function]
# @RELATION BINDS_TO ->[EXT:frontend:TestResourceService]
# @TEST: get_activity_summary returns active count and recent tasks
# @PRE tasks list provided
# @POST Returns dict with active_count and recent_tasks
# @PURPOSE: Verify ResourceService.get_activity_summary returns recent task activity.
def test_get_activity_summary():
from src.services.resource_service import ResourceService
service = ResourceService()
# Create mock tasks
task1 = MagicMock()
task1.id = "task-1"
task1.status = "RUNNING"
task1.params = {"resource_name": "Dashboard 1", "resource_type": "dashboard"}
task1.created_at = datetime(2024, 1, 1, 10, 0, 0)
task2 = MagicMock()
task2.id = "task-2"
task2.status = "SUCCESS"
task2.params = {"resource_name": "Dataset 1", "resource_type": "dataset"}
task2.created_at = datetime(2024, 1, 1, 9, 0, 0)
task3 = MagicMock()
task3.id = "task-3"
task3.status = "WAITING_INPUT"
task3.params = {"resource_name": "Dashboard 2", "resource_type": "dashboard"}
task3.created_at = datetime(2024, 1, 1, 8, 0, 0)
result = service.get_activity_summary([task1, task2, task3])
assert result["active_count"] == 2 # RUNNING + WAITING_INPUT
assert len(result["recent_tasks"]) == 3
# endregion test_get_activity_summary
# region test_get_git_status_for_dashboard_no_repo [TYPE Function]
# @RELATION BINDS_TO ->[EXT:frontend:TestResourceService]
# @TEST: _get_git_status_for_dashboard returns None when no repo exists
# @PRE GitService returns None for repo
# @POST Returns None
# @PURPOSE: Verify get_git_status_for_dashboard returns None when no repo exists.
def test_get_git_status_for_dashboard_no_repo():
with patch("src.services.resource_service.GitService") as mock_git:
from src.services.resource_service import ResourceService
service = ResourceService()
mock_git.return_value.get_repo.return_value = None
result = service._get_git_status_for_dashboard(123)
assert result is not None
assert result["sync_status"] == "NO_REPO"
assert result["has_repo"] is False
# endregion test_get_git_status_for_dashboard_no_repo
# region test_get_last_task_for_resource [TYPE Function]
# @RELATION BINDS_TO ->[EXT:frontend:TestResourceService]
# @TEST: _get_last_task_for_resource returns most recent task for resource
# @PRE tasks list with matching resource_id
# @POST Returns task summary with task_id and status
# @PURPOSE: Verify get_last_task_for_resource returns the most recent task for a given resource.
def test_get_last_task_for_resource():
from src.services.resource_service import ResourceService
service = ResourceService()
# Create mock tasks
task1 = MagicMock()
task1.id = "task-old"
task1.status = "SUCCESS"
task1.params = {"resource_id": "dashboard-1"}
task1.created_at = datetime(2024, 1, 1, 10, 0, 0)
task2 = MagicMock()
task2.id = "task-new"
task2.status = "RUNNING"
task2.params = {"resource_id": "dashboard-1"}
task2.created_at = datetime(2024, 1, 1, 12, 0, 0)
result = service._get_last_task_for_resource("dashboard-1", [task1, task2])
assert result is not None
assert result["task_id"] == "task-new" # Most recent
assert result["status"] == "RUNNING"
# endregion test_get_last_task_for_resource
# region test_extract_resource_name_from_task [TYPE Function]
# @RELATION BINDS_TO ->[EXT:frontend:TestResourceService]
# @TEST: _extract_resource_name_from_task extracts name from params
# @PRE task has resource_name in params
# @POST Returns resource name or fallback
# @PURPOSE: Verify extract_resource_name_from_task correctly parses resource names from task identifiers.
def test_extract_resource_name_from_task():
from src.services.resource_service import ResourceService
service = ResourceService()
# Task with resource_name
task = MagicMock()
task.id = "task-123"
task.params = {"resource_name": "My Dashboard"}
result = service._extract_resource_name_from_task(task)
assert result == "My Dashboard"
# Task without resource_name
task2 = MagicMock()
task2.id = "task-456"
task2.params = {}
result2 = service._extract_resource_name_from_task(task2)
assert "task-456" in result2
# endregion test_extract_resource_name_from_task
# region test_get_last_task_for_resource_empty_tasks [TYPE Function]
# @RELATION BINDS_TO ->[EXT:frontend:TestResourceService]
# @TEST: _get_last_task_for_resource returns None for empty tasks list
# @PRE tasks is empty list
# @POST Returns None
# @PURPOSE: Verify get_last_task_for_resource returns None when tasks list is empty.
def test_get_last_task_for_resource_empty_tasks():
from src.services.resource_service import ResourceService
service = ResourceService()
result = service._get_last_task_for_resource("dashboard-1", [])
assert result is None
# endregion test_get_last_task_for_resource_empty_tasks
# region test_get_last_task_for_resource_no_match [TYPE Function]
# @RELATION BINDS_TO ->[EXT:frontend:TestResourceService]
# @TEST: _get_last_task_for_resource returns None when no tasks match resource_id
# @PRE tasks list has no matching resource_id
# @POST Returns None
# @PURPOSE: Verify get_last_task_for_resource returns None when no task matches the resource.
def test_get_last_task_for_resource_no_match():
from src.services.resource_service import ResourceService
service = ResourceService()
task = MagicMock()
task.id = "task-999"
task.status = "SUCCESS"
task.params = {"resource_id": "dashboard-99"}
task.created_at = datetime(2024, 1, 1, 10, 0, 0)
result = service._get_last_task_for_resource("dashboard-1", [task])
assert result is None
# endregion test_get_last_task_for_resource_no_match
# region test_get_dashboards_with_status_handles_mixed_naive_and_aware_task_datetimes [TYPE Function]
# @RELATION BINDS_TO ->[EXT:frontend:TestResourceService]
# @TEST: get_dashboards_with_status handles mixed naive/aware datetimes without comparison errors.
# @PRE Task list includes both timezone-aware and timezone-naive timestamps.
# @POST Latest task is selected deterministically and no exception is raised.
# @PURPOSE: Verify get_dashboards_with_status handles mixed naive and aware datetimes without crashing.
@pytest.mark.asyncio
async def test_get_dashboards_with_status_handles_mixed_naive_and_aware_task_datetimes():
with (
patch("src.services.resource_service.SupersetClient") as mock_client,
patch("src.services.resource_service.GitService"),
):
from src.services.resource_service import ResourceService
service = ResourceService()
mock_client.return_value.get_dashboards_summary.return_value = [
{"id": 1, "title": "Dashboard 1", "slug": "dash-1"}
]
task_naive = MagicMock()
task_naive.id = "task-naive"
task_naive.plugin_id = "llm_dashboard_validation"
task_naive.status = "SUCCESS"
task_naive.params = {"dashboard_id": "1", "environment_id": "prod"}
task_naive.started_at = datetime(2024, 1, 1, 10, 0, 0)
task_aware = MagicMock()
task_aware.id = "task-aware"
task_aware.plugin_id = "llm_dashboard_validation"
task_aware.status = "SUCCESS"
task_aware.params = {"dashboard_id": "1", "environment_id": "prod"}
task_aware.started_at = datetime(2024, 1, 1, 12, 0, 0, tzinfo=UTC)
env = MagicMock()
env.id = "prod"
result = await service.get_dashboards_with_status(env, [task_naive, task_aware])
assert result[0]["last_task"]["task_id"] == "task-aware"
# endregion test_get_dashboards_with_status_handles_mixed_naive_and_aware_task_datetimes
# region test_get_dashboards_with_status_prefers_latest_decisive_validation_status_over_newer_unknown [TYPE Function]
# @RELATION BINDS_TO ->[EXT:frontend:TestResourceService]
# @TEST: get_dashboards_with_status keeps latest task identity while falling back to older decisive validation status.
# @PRE Same dashboard has older WARN and newer UNKNOWN validation tasks.
# @POST Returned last_task points to newest task but preserves WARN as last meaningful validation state.
# @PURPOSE: Verify status ranking prefers decisive validation over newer unknown status.
@pytest.mark.anyio
async def test_get_dashboards_with_status_prefers_latest_decisive_validation_status_over_newer_unknown():
with (
patch("src.services.resource_service.SupersetClient") as mock_client,
patch("src.services.resource_service.GitService"),
):
from src.services.resource_service import ResourceService
service = ResourceService()
mock_client.return_value.get_dashboards_summary.return_value = [
{"id": 4, "title": "Dashboard 4", "slug": "deck"}
]
task_warn = MagicMock()
task_warn.id = "task-warn"
task_warn.plugin_id = "llm_dashboard_validation"
task_warn.status = "SUCCESS"
task_warn.params = {"dashboard_id": "4", "environment_id": "prod"}
task_warn.result = {"status": "WARN"}
task_warn.started_at = datetime(2024, 1, 1, 11, 0, 0)
task_unknown = MagicMock()
task_unknown.id = "task-unknown"
task_unknown.plugin_id = "llm_dashboard_validation"
task_unknown.status = "RUNNING"
task_unknown.params = {"dashboard_id": "4", "environment_id": "prod"}
task_unknown.result = {"status": "UNKNOWN"}
task_unknown.started_at = datetime(2024, 1, 1, 12, 0, 0)
env = MagicMock()
env.id = "prod"
result = await service.get_dashboards_with_status(
env, [task_warn, task_unknown]
)
assert result[0]["last_task"]["task_id"] == "task-unknown"
assert result[0]["last_task"]["status"] == "RUNNING"
assert result[0]["last_task"]["validation_status"] == "WARN"
# endregion test_get_dashboards_with_status_prefers_latest_decisive_validation_status_over_newer_unknown
# region test_get_dashboards_with_status_falls_back_to_latest_unknown_without_decisive_history [TYPE Function]
# @RELATION BINDS_TO ->[EXT:frontend:TestResourceService]
# @TEST: get_dashboards_with_status still returns newest UNKNOWN when no decisive validation exists.
# @PRE Same dashboard has only UNKNOWN validation tasks.
# @POST Returned last_task keeps newest UNKNOWN task.
# @PURPOSE: Verify fallback to latest unknown status when no decisive history exists.
@pytest.mark.anyio
async def test_get_dashboards_with_status_falls_back_to_latest_unknown_without_decisive_history():
with (
patch("src.services.resource_service.SupersetClient") as mock_client,
patch("src.services.resource_service.GitService"),
):
from src.services.resource_service import ResourceService
service = ResourceService()
mock_client.return_value.get_dashboards_summary.return_value = [
{"id": 5, "title": "Dashboard 5", "slug": "ops"}
]
task_unknown_old = MagicMock()
task_unknown_old.id = "task-unknown-old"
task_unknown_old.plugin_id = "llm_dashboard_validation"
task_unknown_old.status = "SUCCESS"
task_unknown_old.params = {"dashboard_id": "5", "environment_id": "prod"}
task_unknown_old.result = {"status": "UNKNOWN"}
task_unknown_old.started_at = datetime(2024, 1, 1, 11, 0, 0)
task_unknown_new = MagicMock()
task_unknown_new.id = "task-unknown-new"
task_unknown_new.plugin_id = "llm_dashboard_validation"
task_unknown_new.status = "SUCCESS"
task_unknown_new.params = {"dashboard_id": "5", "environment_id": "prod"}
task_unknown_new.result = {"status": "UNKNOWN"}
task_unknown_new.started_at = datetime(2024, 1, 1, 12, 0, 0)
env = MagicMock()
env.id = "prod"
result = await service.get_dashboards_with_status(
env, [task_unknown_old, task_unknown_new]
)
assert result[0]["last_task"]["task_id"] == "task-unknown-new"
assert result[0]["last_task"]["validation_status"] == "UNKNOWN"
# endregion test_get_dashboards_with_status_falls_back_to_latest_unknown_without_decisive_history
# region test_get_last_task_for_resource_handles_mixed_naive_and_aware_created_at [TYPE Function]
# @RELATION BINDS_TO ->[EXT:frontend:TestResourceService]
# @TEST: _get_last_task_for_resource handles mixed naive/aware created_at values.
# @PRE Matching tasks include naive and aware created_at timestamps.
# @POST Latest task is returned without raising datetime comparison errors.
# @PURPOSE: Verify get_last_task_for_resource correctly sorts mixed naive and aware created_at timestamps.
def test_get_last_task_for_resource_handles_mixed_naive_and_aware_created_at():
from src.services.resource_service import ResourceService
service = ResourceService()
task_naive = MagicMock()
task_naive.id = "task-old"
task_naive.status = "SUCCESS"
task_naive.params = {"resource_id": "dashboard-1"}
task_naive.created_at = datetime(2024, 1, 1, 10, 0, 0)
task_aware = MagicMock()
task_aware.id = "task-new"
task_aware.status = "RUNNING"
task_aware.params = {"resource_id": "dashboard-1"}
task_aware.created_at = datetime(2024, 1, 1, 12, 0, 0, tzinfo=UTC)
result = service._get_last_task_for_resource(
"dashboard-1", [task_naive, task_aware]
)
assert result is not None
assert result["task_id"] == "task-new"
# endregion test_get_last_task_for_resource_handles_mixed_naive_and_aware_created_at
# endregion TestResourceService