feat: add slug-only dashboard profile filter and unify backend imports

This commit is contained in:
2026-03-11 12:20:34 +03:00
parent 0f0e1700fc
commit 42976e72ff
40 changed files with 376 additions and 149 deletions

View File

@@ -1,3 +1,19 @@
[build-system]
requires = ["setuptools>=69", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "ss-tools-backend"
version = "0.0.0"
requires-python = ">=3.13"
[tool.setuptools]
include-package-data = true
[tool.setuptools.packages.find]
where = ["."]
include = ["src*"]
[tool.pytest.ini_options]
pythonpath = ["."]
importmode = "importlib"

3
backend/src/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
# [DEF:src:Package]
# @PURPOSE: Canonical backend package root for application, scripts, and tests.
# [/DEF:src:Package]

View File

@@ -0,0 +1,3 @@
# [DEF:src.api:Package]
# @PURPOSE: Backend API package root.
# [/DEF:src.api:Package]

View File

@@ -82,6 +82,7 @@ def _build_preference_response(user_id: str = "u-1") -> ProfilePreferenceRespons
superset_username="John_Doe",
superset_username_normalized="john_doe",
show_only_my_dashboards=True,
show_only_slug_dashboards=True,
git_username="ivan.ivanov",
git_email="ivan@company.local",
has_git_personal_access_token=True,
@@ -126,6 +127,7 @@ def test_get_profile_preferences_returns_self_payload(profile_route_deps_fixture
assert payload["preference"]["superset_username_normalized"] == "john_doe"
assert payload["preference"]["git_username"] == "ivan.ivanov"
assert payload["preference"]["git_email"] == "ivan@company.local"
assert payload["preference"]["show_only_slug_dashboards"] is True
assert payload["preference"]["has_git_personal_access_token"] is True
assert payload["preference"]["git_personal_access_token_masked"] == "iv***al"
assert payload["preference"]["start_page"] == "reports"
@@ -153,6 +155,7 @@ def test_patch_profile_preferences_success(profile_route_deps_fixture):
json={
"superset_username": "John_Doe",
"show_only_my_dashboards": True,
"show_only_slug_dashboards": True,
"git_username": "ivan.ivanov",
"git_email": "ivan@company.local",
"git_personal_access_token": "ghp_1234567890",
@@ -167,6 +170,7 @@ def test_patch_profile_preferences_success(profile_route_deps_fixture):
assert payload["status"] == "success"
assert payload["preference"]["superset_username"] == "John_Doe"
assert payload["preference"]["show_only_my_dashboards"] is True
assert payload["preference"]["show_only_slug_dashboards"] is True
assert payload["preference"]["git_username"] == "ivan.ivanov"
assert payload["preference"]["git_email"] == "ivan@company.local"
assert payload["preference"]["start_page"] == "reports"
@@ -179,6 +183,7 @@ def test_patch_profile_preferences_success(profile_route_deps_fixture):
assert called_kwargs["payload"].git_username == "ivan.ivanov"
assert called_kwargs["payload"].git_email == "ivan@company.local"
assert called_kwargs["payload"].git_personal_access_token == "ghp_1234567890"
assert called_kwargs["payload"].show_only_slug_dashboards is True
assert called_kwargs["payload"].start_page == "reports-logs"
assert called_kwargs["payload"].auto_open_task_drawer is False
assert called_kwargs["payload"].dashboards_table_density == "free"
@@ -290,4 +295,4 @@ def test_lookup_superset_accounts_env_not_found(profile_route_deps_fixture):
assert payload["detail"] == "Environment 'missing-env' not found"
# [/DEF:test_lookup_superset_accounts_env_not_found:Function]
# [/DEF:backend.src.api.routes.__tests__.test_profile_api:Module]
# [/DEF:backend.src.api.routes.__tests__.test_profile_api:Module]

View File

@@ -98,7 +98,9 @@ class EffectiveProfileFilter(BaseModel):
source_page: Literal["dashboards_main", "other"] = "dashboards_main"
override_show_all: bool = False
username: Optional[str] = None
match_logic: Optional[Literal["owners_or_modified_by"]] = None
match_logic: Optional[
Literal["owners_or_modified_by", "slug_only", "owners_or_modified_by+slug_only"]
] = None
# [/DEF:EffectiveProfileFilter:DataClass]
# [DEF:DashboardsResponse:DataClass]
@@ -535,6 +537,7 @@ async def get_dashboards(
profile_service = ProfileService(db=db, config_manager=config_manager)
bound_username: Optional[str] = None
can_apply_profile_filter = False
can_apply_slug_filter = False
effective_profile_filter = EffectiveProfileFilter(
applied=False,
source_page=page_context,
@@ -560,13 +563,27 @@ async def get_dashboards(
and bool(getattr(profile_preference, "show_only_my_dashboards", False))
and bool(bound_username)
)
can_apply_slug_filter = (
page_context == "dashboards_main"
and bool(apply_profile_default)
and not bool(override_show_all)
and bool(getattr(profile_preference, "show_only_slug_dashboards", True))
)
profile_match_logic = None
if can_apply_profile_filter and can_apply_slug_filter:
profile_match_logic = "owners_or_modified_by+slug_only"
elif can_apply_profile_filter:
profile_match_logic = "owners_or_modified_by"
elif can_apply_slug_filter:
profile_match_logic = "slug_only"
effective_profile_filter = EffectiveProfileFilter(
applied=bool(can_apply_profile_filter),
applied=bool(can_apply_profile_filter or can_apply_slug_filter),
source_page=page_context,
override_show_all=bool(override_show_all),
username=bound_username if can_apply_profile_filter else None,
match_logic="owners_or_modified_by" if can_apply_profile_filter else None,
match_logic=profile_match_logic,
)
except Exception as profile_error:
logger.explore(
@@ -589,7 +606,7 @@ async def get_dashboards(
actor_filters,
)
)
needs_full_scan = has_column_filters or bool(can_apply_profile_filter)
needs_full_scan = has_column_filters or bool(can_apply_profile_filter) or bool(can_apply_slug_filter)
if isinstance(resource_service, ResourceService) and not needs_full_scan:
try:
@@ -600,6 +617,7 @@ async def get_dashboards(
page_size=page_size,
search=search,
include_git_status=False,
require_slug=bool(can_apply_slug_filter),
)
paginated_dashboards = page_payload["dashboards"]
total = page_payload["total"]
@@ -613,6 +631,7 @@ async def get_dashboards(
env,
all_tasks,
include_git_status=False,
require_slug=bool(can_apply_slug_filter),
)
if search:
@@ -633,6 +652,7 @@ async def get_dashboards(
env,
all_tasks,
include_git_status=bool(git_filters),
require_slug=bool(can_apply_slug_filter),
)
if can_apply_profile_filter and bound_username:
@@ -674,6 +694,13 @@ async def get_dashboards(
)
dashboards = filtered_dashboards
if can_apply_slug_filter:
dashboards = [
dashboard
for dashboard in dashboards
if str(dashboard.get("slug") or "").strip()
]
if search:
search_lower = search.lower()
dashboards = [

View File

@@ -0,0 +1,3 @@
# [DEF:src.core:Package]
# @PURPOSE: Backend core services and infrastructure package root.
# [/DEF:src.core:Package]

View File

@@ -0,0 +1,3 @@
# [DEF:src.core.auth:Package]
# @PURPOSE: Authentication and authorization package root.
# [/DEF:src.core.auth:Package]

View File

@@ -141,6 +141,11 @@ def _ensure_user_dashboard_preferences_columns(bind_engine):
"ALTER TABLE user_dashboard_preferences "
"ADD COLUMN dashboards_table_density VARCHAR NOT NULL DEFAULT 'comfortable'"
)
if "show_only_slug_dashboards" not in existing_columns:
alter_statements.append(
"ALTER TABLE user_dashboard_preferences "
"ADD COLUMN show_only_slug_dashboards BOOLEAN NOT NULL DEFAULT TRUE"
)
if not alter_statements:
return

View File

@@ -76,17 +76,8 @@ class PluginLoader:
"""
Loads a single Python module and extracts PluginBase subclasses.
"""
# Try to determine the correct package prefix based on how the app is running
# For standalone execution, we need to handle the import differently
if __name__ == "__main__" or "test" in __name__:
# When running as standalone or in tests, use relative import
package_name = f"plugins.{module_name}"
elif "backend.src" in __name__:
package_prefix = "backend.src.plugins"
package_name = f"{package_prefix}.{module_name}"
else:
package_prefix = "src.plugins"
package_name = f"{package_prefix}.{module_name}"
# All runtime code is imported through the canonical `src` package root.
package_name = f"src.plugins.{module_name}"
# print(f"DEBUG: Loading plugin {module_name} as {package_name}")
spec = importlib.util.spec_from_file_location(package_name, file_path)
@@ -198,4 +189,4 @@ class PluginLoader:
return plugin_id in self._plugins
# [/DEF:has_plugin:Function]
# [/DEF:PluginLoader:Class]
# [/DEF:PluginLoader:Class]

View File

@@ -150,11 +150,19 @@ class SupersetClient:
# @PRE: Client is authenticated.
# @POST: Returns a list of dashboard metadata summaries.
# @RETURN: List[Dict]
def get_dashboards_summary(self) -> List[Dict]:
def get_dashboards_summary(self, require_slug: bool = False) -> List[Dict]:
with belief_scope("SupersetClient.get_dashboards_summary"):
# Rely on list endpoint default projection to stay compatible
# across Superset versions and preserve owners in one request.
query: Dict[str, Any] = {}
if require_slug:
query["filters"] = [
{
"col": "slug",
"opr": "neq",
"value": "",
}
]
_, dashboards = self.get_dashboards(query=query)
# Map fields to DashboardMetadata schema
@@ -232,23 +240,35 @@ class SupersetClient:
page: int,
page_size: int,
search: Optional[str] = None,
require_slug: bool = False,
) -> Tuple[int, List[Dict]]:
with belief_scope("SupersetClient.get_dashboards_summary_page"):
query: Dict[str, Any] = {
"page": max(page - 1, 0),
"page_size": page_size,
}
filters: List[Dict[str, Any]] = []
if require_slug:
filters.append(
{
"col": "slug",
"opr": "neq",
"value": "",
}
)
normalized_search = (search or "").strip()
if normalized_search:
# Superset list API supports filter objects with `opr` operator.
# `ct` -> contains (ILIKE on most Superset backends).
query["filters"] = [
filters.append(
{
"col": "dashboard_title",
"opr": "ct",
"value": normalized_search,
}
]
)
if filters:
query["filters"] = filters
total_count, dashboards = self.get_dashboards_page(query=query)

View File

@@ -0,0 +1,3 @@
# [DEF:src.core.utils:Package]
# @PURPOSE: Shared utility package root.
# [/DEF:src.core.utils:Package]

View File

@@ -0,0 +1,3 @@
# [DEF:src.models:Package]
# @PURPOSE: Domain model package root.
# [/DEF:src.models:Package]

View File

@@ -32,6 +32,7 @@ class UserDashboardPreference(Base):
superset_username_normalized = Column(String, nullable=True, index=True)
show_only_my_dashboards = Column(Boolean, nullable=False, default=False)
show_only_slug_dashboards = Column(Boolean, nullable=False, default=True)
git_username = Column(String, nullable=True)
git_email = Column(String, nullable=True)
@@ -56,4 +57,4 @@ class UserDashboardPreference(Base):
user = relationship("User")
# [/DEF:UserDashboardPreference:Class]
# [/DEF:backend.src.models.profile:Module]
# [/DEF:backend.src.models.profile:Module]

View File

@@ -0,0 +1,3 @@
# [DEF:src.plugins:Package]
# @PURPOSE: Plugin package root for dynamic discovery and runtime imports.
# [/DEF:src.plugins:Package]

View File

@@ -0,0 +1,3 @@
# [DEF:src.plugins.git:Package]
# @PURPOSE: Git plugin extension package root.
# [/DEF:src.plugins.git:Package]

View File

@@ -0,0 +1,3 @@
# [DEF:src.schemas:Package]
# @PURPOSE: API schema package root.
# [/DEF:src.schemas:Package]

View File

@@ -45,6 +45,7 @@ class ProfilePreference(BaseModel):
superset_username: Optional[str] = None
superset_username_normalized: Optional[str] = None
show_only_my_dashboards: bool = False
show_only_slug_dashboards: bool = True
git_username: Optional[str] = None
git_email: Optional[str] = None
@@ -79,6 +80,10 @@ class ProfilePreferenceUpdateRequest(BaseModel):
default=None,
description='When true, "/dashboards" can auto-apply profile filter in main context.',
)
show_only_slug_dashboards: Optional[bool] = Field(
default=None,
description='When true, "/dashboards" hides dashboards without slug by default.',
)
git_username: Optional[str] = Field(
default=None,
description="Git author username used for commit signature.",
@@ -172,4 +177,4 @@ class SupersetAccountLookupResponse(BaseModel):
items: List[SupersetAccountCandidate] = Field(default_factory=list)
# [/DEF:SupersetAccountLookupResponse:Class]
# [/DEF:backend.src.schemas.profile:Module]
# [/DEF:backend.src.schemas.profile:Module]

View File

@@ -0,0 +1,3 @@
# [DEF:src.scripts:Package]
# @PURPOSE: Script entrypoint package root.
# [/DEF:src.scripts:Package]

View File

@@ -15,13 +15,13 @@ from datetime import datetime, timezone
from types import SimpleNamespace
from typing import List, Optional, Any, Dict
# Standardize sys.path for direct execution from project root or scripts dir
# Standardize sys.path for direct execution from project root or scripts dir.
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, "..", "..", ".."))
if PROJECT_ROOT not in sys.path:
sys.path.insert(0, PROJECT_ROOT)
BACKEND_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, "..", ".."))
if BACKEND_ROOT not in sys.path:
sys.path.insert(0, BACKEND_ROOT)
from backend.src.models.clean_release import (
from src.models.clean_release import (
CandidateArtifact,
CheckFinalStatus,
CheckStageName,
@@ -35,12 +35,12 @@ from backend.src.models.clean_release import (
RegistryStatus,
ReleaseCandidateStatus,
)
from backend.src.services.clean_release.approval_service import approve_candidate
from backend.src.services.clean_release.compliance_execution_service import ComplianceExecutionService
from backend.src.services.clean_release.enums import CandidateStatus
from backend.src.services.clean_release.manifest_service import build_manifest_snapshot
from backend.src.services.clean_release.publication_service import publish_candidate
from backend.src.services.clean_release.repository import CleanReleaseRepository
from src.services.clean_release.approval_service import approve_candidate
from src.services.clean_release.compliance_execution_service import ComplianceExecutionService
from src.services.clean_release.enums import CandidateStatus
from src.services.clean_release.manifest_service import build_manifest_snapshot
from src.services.clean_release.publication_service import publish_candidate
from src.services.clean_release.repository import CleanReleaseRepository
# [DEF:TuiFacadeAdapter:Class]
# @PURPOSE: Thin TUI adapter that routes business mutations through application services.

View File

@@ -0,0 +1,3 @@
# [DEF:src.services.notifications:Package]
# @PURPOSE: Notification service package root.
# [/DEF:src.services.notifications:Package]

View File

@@ -145,6 +145,14 @@ class ProfileService:
if "show_only_my_dashboards" in provided_fields:
effective_show_only = bool(payload.show_only_my_dashboards)
effective_show_only_slug = (
bool(preference.show_only_slug_dashboards)
if preference.show_only_slug_dashboards is not None
else True
)
if "show_only_slug_dashboards" in provided_fields:
effective_show_only_slug = bool(payload.show_only_slug_dashboards)
effective_git_username = self._sanitize_text(preference.git_username)
if "git_username" in provided_fields:
effective_git_username = self._sanitize_text(payload.git_username)
@@ -206,6 +214,7 @@ class ProfileService:
effective_superset_username
)
preference.show_only_my_dashboards = effective_show_only
preference.show_only_slug_dashboards = effective_show_only_slug
preference.git_username = effective_git_username
preference.git_email = effective_git_email
@@ -460,6 +469,11 @@ class ProfileService:
preference.superset_username_normalized
),
show_only_my_dashboards=bool(preference.show_only_my_dashboards),
show_only_slug_dashboards=(
bool(preference.show_only_slug_dashboards)
if preference.show_only_slug_dashboards is not None
else True
),
git_username=self._sanitize_text(preference.git_username),
git_email=self._sanitize_text(preference.git_email),
has_git_personal_access_token=bool(encrypted_token),
@@ -586,6 +600,7 @@ class ProfileService:
superset_username=None,
superset_username_normalized=None,
show_only_my_dashboards=False,
show_only_slug_dashboards=True,
git_username=None,
git_email=None,
has_git_personal_access_token=False,
@@ -709,4 +724,4 @@ class ProfileService:
# [/DEF:_normalize_owner_tokens:Function]
# [/DEF:ProfileService:Class]
# [/DEF:backend.src.services.profile_service:Module]
# [/DEF:backend.src.services.profile_service:Module]

View File

@@ -0,0 +1,3 @@
# [DEF:src.services.reports:Package]
# @PURPOSE: Report service package root.
# [/DEF:src.services.reports:Package]

View File

@@ -46,10 +46,11 @@ class ResourceService:
env: Any,
tasks: Optional[List[Task]] = None,
include_git_status: bool = True,
require_slug: bool = False,
) -> List[Dict[str, Any]]:
with belief_scope("get_dashboards_with_status", f"env={env.id}"):
client = SupersetClient(env)
dashboards = client.get_dashboards_summary()
dashboards = client.get_dashboards_summary(require_slug=require_slug)
# Enhance each dashboard with Git status and task status
result = []
@@ -96,6 +97,7 @@ class ResourceService:
page_size: int = 10,
search: Optional[str] = None,
include_git_status: bool = True,
require_slug: bool = False,
) -> Dict[str, Any]:
with belief_scope(
"get_dashboards_page_with_status",
@@ -106,6 +108,7 @@ class ResourceService:
page=page,
page_size=page_size,
search=search,
require_slug=require_slug,
)
result = []

View File

@@ -8,13 +8,13 @@
from types import SimpleNamespace
import json
from backend.src.dependencies import get_clean_release_repository, get_config_manager
from src.dependencies import get_clean_release_repository, get_config_manager
from datetime import datetime, timezone
from uuid import uuid4
from backend.src.models.clean_release import CleanPolicySnapshot, ComplianceReport, ReleaseCandidate, SourceRegistrySnapshot
from backend.src.services.clean_release.enums import CandidateStatus, ComplianceDecision
from backend.src.scripts.clean_release_cli import main as cli_main
from src.models.clean_release import CleanPolicySnapshot, ComplianceReport, ReleaseCandidate, SourceRegistrySnapshot
from src.services.clean_release.enums import CandidateStatus, ComplianceDecision
from src.scripts.clean_release_cli import main as cli_main
def test_cli_candidate_register_scaffold() -> None:
@@ -302,4 +302,4 @@ def test_cli_release_gate_commands_scaffold() -> None:
assert revoke_exit == 0
# [/DEF:test_clean_release_cli:Module]
# [/DEF:test_clean_release_cli:Module]

View File

@@ -14,8 +14,8 @@ from unittest.mock import MagicMock, patch
import pytest
from backend.src.scripts.clean_release_tui import CleanReleaseTUI, main, tui_main
from backend.src.models.clean_release import CheckFinalStatus
from src.scripts.clean_release_tui import CleanReleaseTUI, main, tui_main
from src.models.clean_release import CheckFinalStatus
@pytest.fixture
@@ -31,7 +31,7 @@ def test_headless_fallback(capsys):
@TEST_EDGE: stdout_unavailable
Tests that non-TTY startup is explicitly refused and wrapper is not invoked.
"""
with mock.patch("backend.src.scripts.clean_release_tui.curses.wrapper") as curses_wrapper_mock:
with mock.patch("src.scripts.clean_release_tui.curses.wrapper") as curses_wrapper_mock:
with mock.patch("sys.stdout.isatty", return_value=False):
exit_code = main()
@@ -43,7 +43,7 @@ def test_headless_fallback(capsys):
assert "Use CLI/API workflow instead" in captured.err
@patch("backend.src.scripts.clean_release_tui.curses")
@patch("src.scripts.clean_release_tui.curses")
def test_tui_initial_render(mock_curses_module, mock_stdscr: MagicMock):
"""
Simulates the initial rendering cycle of the TUI application to ensure
@@ -76,7 +76,7 @@ def test_tui_initial_render(mock_curses_module, mock_stdscr: MagicMock):
assert any("F5 Run" in str(call) for call in addstr_calls)
@patch("backend.src.scripts.clean_release_tui.curses")
@patch("src.scripts.clean_release_tui.curses")
def test_tui_run_checks_f5(mock_curses_module, mock_stdscr: MagicMock):
"""
Simulates pressing F5 to transition into the RUNNING checks flow.
@@ -111,7 +111,7 @@ def test_tui_run_checks_f5(mock_curses_module, mock_stdscr: MagicMock):
assert len(app.violations_list) > 0
@patch("backend.src.scripts.clean_release_tui.curses")
@patch("src.scripts.clean_release_tui.curses")
def test_tui_exit_f10(mock_curses_module, mock_stdscr: MagicMock):
"""
Simulates pressing F10 to exit the application immediately without running checks.
@@ -128,7 +128,7 @@ def test_tui_exit_f10(mock_curses_module, mock_stdscr: MagicMock):
assert app.status == "READY"
@patch("backend.src.scripts.clean_release_tui.curses")
@patch("src.scripts.clean_release_tui.curses")
def test_tui_clear_history_f7(mock_curses_module, mock_stdscr: MagicMock):
"""
Simulates pressing F7 to clear history.
@@ -153,4 +153,3 @@ def test_tui_clear_history_f7(mock_curses_module, mock_stdscr: MagicMock):
# [/DEF:backend.tests.scripts.test_clean_release_tui:Module]

View File

@@ -11,8 +11,8 @@ from __future__ import annotations
import curses
from unittest.mock import MagicMock, patch
from backend.src.models.clean_release import CheckFinalStatus
from backend.src.scripts.clean_release_tui import CleanReleaseTUI, main
from src.models.clean_release import CheckFinalStatus
from src.scripts.clean_release_tui import CleanReleaseTUI, main
def _build_mock_stdscr() -> MagicMock:
@@ -22,7 +22,7 @@ def _build_mock_stdscr() -> MagicMock:
return stdscr
@patch("backend.src.scripts.clean_release_tui.curses")
@patch("src.scripts.clean_release_tui.curses")
def test_tui_f5_dispatches_run_action(mock_curses_module: MagicMock) -> None:
"""F5 should dispatch run action from TUI loop."""
mock_curses_module.KEY_F10 = curses.KEY_F10
@@ -40,7 +40,7 @@ def test_tui_f5_dispatches_run_action(mock_curses_module: MagicMock) -> None:
run_checks_mock.assert_called_once_with()
@patch("backend.src.scripts.clean_release_tui.curses")
@patch("src.scripts.clean_release_tui.curses")
def test_tui_f5_run_smoke_reports_blocked_state(mock_curses_module: MagicMock) -> None:
"""F5 smoke test should expose blocked outcome state after run action."""
mock_curses_module.KEY_F10 = curses.KEY_F10
@@ -76,7 +76,7 @@ def test_tui_non_tty_refuses_startup(capsys) -> None:
assert "Use CLI/API workflow instead" in captured.err
@patch("backend.src.scripts.clean_release_tui.curses")
@patch("src.scripts.clean_release_tui.curses")
def test_tui_f8_blocked_without_facade_binding(mock_curses_module: MagicMock) -> None:
"""F8 should not perform hidden state mutation when facade action is not bound."""
mock_curses_module.KEY_F10 = curses.KEY_F10
@@ -94,4 +94,4 @@ def test_tui_f8_blocked_without_facade_binding(mock_curses_module: MagicMock) ->
assert "F8 disabled" in app.last_error
# [/DEF:test_clean_release_tui_v2:Module]
# [/DEF:test_clean_release_tui_v2:Module]

View File

@@ -9,10 +9,10 @@ from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from src.core.database import Base
from src.models.clean_release import ReleaseCandidate, DistributionManifest, CandidateArtifact
from backend.src.services.clean_release.enums import CandidateStatus
from backend.src.services.clean_release.candidate_service import register_candidate
from backend.src.services.clean_release.manifest_service import build_manifest_snapshot
from backend.src.services.clean_release.repository import CleanReleaseRepository
from src.services.clean_release.enums import CandidateStatus
from src.services.clean_release.candidate_service import register_candidate
from src.services.clean_release.manifest_service import build_manifest_snapshot
from src.services.clean_release.repository import CleanReleaseRepository
@pytest.fixture
def db_session():
@@ -43,7 +43,7 @@ def test_candidate_lifecycle_transitions(db_session):
assert candidate.status == CandidateStatus.PREPARED
# Invalid transition: PREPARED -> DRAFT (should raise IllegalTransitionError)
from backend.src.services.clean_release.exceptions import IllegalTransitionError
from src.services.clean_release.exceptions import IllegalTransitionError
with pytest.raises(IllegalTransitionError, match="Forbidden transition"):
candidate.transition_to(CandidateStatus.DRAFT)
@@ -200,4 +200,4 @@ def test_manifest_service_rejects_missing_candidate():
with pytest.raises(ValueError, match="not found"):
build_manifest_snapshot(repository=repository, candidate_id="missing-candidate", created_by="operator")
# [/DEF:test_candidate_manifest_services:Module]
# [/DEF:test_candidate_manifest_services:Module]

View File

@@ -13,17 +13,17 @@ from datetime import datetime, timezone
import pytest
from backend.src.models.clean_release import (
from src.models.clean_release import (
CleanPolicySnapshot,
ComplianceDecision,
DistributionManifest,
ReleaseCandidate,
SourceRegistrySnapshot,
)
from backend.src.services.clean_release.compliance_orchestrator import CleanComplianceOrchestrator
from backend.src.services.clean_release.enums import CandidateStatus, RunStatus
from backend.src.services.clean_release.report_builder import ComplianceReportBuilder
from backend.src.services.clean_release.repository import CleanReleaseRepository
from src.services.clean_release.compliance_orchestrator import CleanComplianceOrchestrator
from src.services.clean_release.enums import CandidateStatus, RunStatus
from src.services.clean_release.report_builder import ComplianceReportBuilder
from src.services.clean_release.repository import CleanReleaseRepository
# [DEF:_seed_with_candidate_policy_registry:Function]
@@ -170,4 +170,4 @@ def test_blocked_run_finalization_blocks_report_builder():
builder.build_report_payload(run, [])
# [/DEF:test_blocked_run_finalization_blocks_report_builder:Function]
# [/DEF:backend.tests.services.clean_release.test_compliance_execution_service:Module]
# [/DEF:backend.tests.services.clean_release.test_compliance_execution_service:Module]

View File

@@ -151,8 +151,8 @@ class _PluginLoaderStub:
def _make_task_manager() -> TaskManager:
plugin_loader = _PluginLoaderStub(CleanReleaseCompliancePlugin())
with patch("backend.src.core.task_manager.manager.TaskPersistenceService") as mock_persistence, patch(
"backend.src.core.task_manager.manager.TaskLogPersistenceService"
with patch("src.core.task_manager.manager.TaskPersistenceService") as mock_persistence, patch(
"src.core.task_manager.manager.TaskLogPersistenceService"
) as mock_log_persistence:
mock_persistence.return_value.load_tasks.return_value = []
mock_persistence.return_value.persist_task = MagicMock()
@@ -247,4 +247,4 @@ async def test_compliance_run_missing_manifest_marks_task_failed():
manager._flusher_thread.join(timeout=2)
# [/DEF:test_compliance_run_missing_manifest_marks_task_failed:Function]
# [/DEF:backend.tests.services.clean_release.test_compliance_task_integration:Module]
# [/DEF:backend.tests.services.clean_release.test_compliance_task_integration:Module]

View File

@@ -9,8 +9,8 @@ from __future__ import annotations
from datetime import datetime, timezone
from backend.src.models.clean_release import ReleaseCandidate
from backend.src.services.clean_release.demo_data_service import (
from src.models.clean_release import ReleaseCandidate
from src.services.clean_release.demo_data_service import (
build_namespaced_id,
create_isolated_repository,
resolve_namespace,
@@ -84,4 +84,4 @@ def test_create_isolated_repository_keeps_mode_data_separate() -> None:
assert real_repo.get_candidate(demo_candidate_id) is None
# [/DEF:test_create_isolated_repository_keeps_mode_data_separate:Function]
# [/DEF:backend.tests.services.clean_release.test_demo_mode_isolation:Module]
# [/DEF:backend.tests.services.clean_release.test_demo_mode_isolation:Module]

View File

@@ -14,10 +14,10 @@ from types import SimpleNamespace
import pytest
from backend.src.models.clean_release import CleanPolicySnapshot, SourceRegistrySnapshot
from backend.src.services.clean_release.exceptions import PolicyResolutionError
from backend.src.services.clean_release.policy_resolution_service import resolve_trusted_policy_snapshots
from backend.src.services.clean_release.repository import CleanReleaseRepository
from src.models.clean_release import CleanPolicySnapshot, SourceRegistrySnapshot
from src.services.clean_release.exceptions import PolicyResolutionError
from src.services.clean_release.policy_resolution_service import resolve_trusted_policy_snapshots
from src.services.clean_release.repository import CleanReleaseRepository
# [DEF:_config_manager:Function]
@@ -102,4 +102,4 @@ def test_resolve_trusted_policy_snapshots_rejects_override_attempt():
)
# [/DEF:test_resolve_trusted_policy_snapshots_rejects_override_attempt:Function]
# [/DEF:backend.tests.services.clean_release.test_policy_resolution_service:Module]
# [/DEF:backend.tests.services.clean_release.test_policy_resolution_service:Module]