diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 1a017e39..390a3789 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -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" diff --git a/backend/src/__init__.py b/backend/src/__init__.py new file mode 100644 index 00000000..0f97a802 --- /dev/null +++ b/backend/src/__init__.py @@ -0,0 +1,3 @@ +# [DEF:src:Package] +# @PURPOSE: Canonical backend package root for application, scripts, and tests. +# [/DEF:src:Package] diff --git a/backend/src/api/__init__.py b/backend/src/api/__init__.py new file mode 100644 index 00000000..4b6a6c7e --- /dev/null +++ b/backend/src/api/__init__.py @@ -0,0 +1,3 @@ +# [DEF:src.api:Package] +# @PURPOSE: Backend API package root. +# [/DEF:src.api:Package] diff --git a/backend/src/api/routes/__tests__/test_profile_api.py b/backend/src/api/routes/__tests__/test_profile_api.py index fc57af2b..8cbd9365 100644 --- a/backend/src/api/routes/__tests__/test_profile_api.py +++ b/backend/src/api/routes/__tests__/test_profile_api.py @@ -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] \ No newline at end of file +# [/DEF:backend.src.api.routes.__tests__.test_profile_api:Module] diff --git a/backend/src/api/routes/dashboards.py b/backend/src/api/routes/dashboards.py index 0feee006..49e245cc 100644 --- a/backend/src/api/routes/dashboards.py +++ b/backend/src/api/routes/dashboards.py @@ -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 = [ diff --git a/backend/src/core/__init__.py b/backend/src/core/__init__.py new file mode 100644 index 00000000..d50a06dc --- /dev/null +++ b/backend/src/core/__init__.py @@ -0,0 +1,3 @@ +# [DEF:src.core:Package] +# @PURPOSE: Backend core services and infrastructure package root. +# [/DEF:src.core:Package] diff --git a/backend/src/core/auth/__init__.py b/backend/src/core/auth/__init__.py new file mode 100644 index 00000000..038a3a80 --- /dev/null +++ b/backend/src/core/auth/__init__.py @@ -0,0 +1,3 @@ +# [DEF:src.core.auth:Package] +# @PURPOSE: Authentication and authorization package root. +# [/DEF:src.core.auth:Package] diff --git a/backend/src/core/database.py b/backend/src/core/database.py index cf200670..c4632911 100644 --- a/backend/src/core/database.py +++ b/backend/src/core/database.py @@ -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 diff --git a/backend/src/core/plugin_loader.py b/backend/src/core/plugin_loader.py index 9ac05f8e..505f4e3c 100755 --- a/backend/src/core/plugin_loader.py +++ b/backend/src/core/plugin_loader.py @@ -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] \ No newline at end of file +# [/DEF:PluginLoader:Class] diff --git a/backend/src/core/superset_client.py b/backend/src/core/superset_client.py index 52b3907b..8f0335e2 100644 --- a/backend/src/core/superset_client.py +++ b/backend/src/core/superset_client.py @@ -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) diff --git a/backend/src/core/utils/__init__.py b/backend/src/core/utils/__init__.py new file mode 100644 index 00000000..a9cb655a --- /dev/null +++ b/backend/src/core/utils/__init__.py @@ -0,0 +1,3 @@ +# [DEF:src.core.utils:Package] +# @PURPOSE: Shared utility package root. +# [/DEF:src.core.utils:Package] diff --git a/backend/src/models/__init__.py b/backend/src/models/__init__.py new file mode 100644 index 00000000..0ba3f8d2 --- /dev/null +++ b/backend/src/models/__init__.py @@ -0,0 +1,3 @@ +# [DEF:src.models:Package] +# @PURPOSE: Domain model package root. +# [/DEF:src.models:Package] diff --git a/backend/src/models/profile.py b/backend/src/models/profile.py index 551a08d2..1a2f8278 100644 --- a/backend/src/models/profile.py +++ b/backend/src/models/profile.py @@ -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] \ No newline at end of file +# [/DEF:backend.src.models.profile:Module] diff --git a/backend/src/plugins/__init__.py b/backend/src/plugins/__init__.py new file mode 100644 index 00000000..f96e7af1 --- /dev/null +++ b/backend/src/plugins/__init__.py @@ -0,0 +1,3 @@ +# [DEF:src.plugins:Package] +# @PURPOSE: Plugin package root for dynamic discovery and runtime imports. +# [/DEF:src.plugins:Package] diff --git a/backend/src/plugins/git/__init__.py b/backend/src/plugins/git/__init__.py new file mode 100644 index 00000000..38844227 --- /dev/null +++ b/backend/src/plugins/git/__init__.py @@ -0,0 +1,3 @@ +# [DEF:src.plugins.git:Package] +# @PURPOSE: Git plugin extension package root. +# [/DEF:src.plugins.git:Package] diff --git a/backend/src/schemas/__init__.py b/backend/src/schemas/__init__.py new file mode 100644 index 00000000..cb4a2ff5 --- /dev/null +++ b/backend/src/schemas/__init__.py @@ -0,0 +1,3 @@ +# [DEF:src.schemas:Package] +# @PURPOSE: API schema package root. +# [/DEF:src.schemas:Package] diff --git a/backend/src/schemas/profile.py b/backend/src/schemas/profile.py index ce3e1a9c..14dc7aae 100644 --- a/backend/src/schemas/profile.py +++ b/backend/src/schemas/profile.py @@ -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] \ No newline at end of file +# [/DEF:backend.src.schemas.profile:Module] diff --git a/backend/src/scripts/__init__.py b/backend/src/scripts/__init__.py new file mode 100644 index 00000000..d0522090 --- /dev/null +++ b/backend/src/scripts/__init__.py @@ -0,0 +1,3 @@ +# [DEF:src.scripts:Package] +# @PURPOSE: Script entrypoint package root. +# [/DEF:src.scripts:Package] diff --git a/backend/src/scripts/clean_release_tui.py b/backend/src/scripts/clean_release_tui.py index da625ab4..68a8ff5b 100644 --- a/backend/src/scripts/clean_release_tui.py +++ b/backend/src/scripts/clean_release_tui.py @@ -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. diff --git a/backend/src/services/notifications/__init__.py b/backend/src/services/notifications/__init__.py new file mode 100644 index 00000000..6f79d663 --- /dev/null +++ b/backend/src/services/notifications/__init__.py @@ -0,0 +1,3 @@ +# [DEF:src.services.notifications:Package] +# @PURPOSE: Notification service package root. +# [/DEF:src.services.notifications:Package] diff --git a/backend/src/services/profile_service.py b/backend/src/services/profile_service.py index 2a504820..e1b7ef88 100644 --- a/backend/src/services/profile_service.py +++ b/backend/src/services/profile_service.py @@ -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] \ No newline at end of file +# [/DEF:backend.src.services.profile_service:Module] diff --git a/backend/src/services/reports/__init__.py b/backend/src/services/reports/__init__.py new file mode 100644 index 00000000..02ff7137 --- /dev/null +++ b/backend/src/services/reports/__init__.py @@ -0,0 +1,3 @@ +# [DEF:src.services.reports:Package] +# @PURPOSE: Report service package root. +# [/DEF:src.services.reports:Package] diff --git a/backend/src/services/resource_service.py b/backend/src/services/resource_service.py index eabc2469..1c012caa 100644 --- a/backend/src/services/resource_service.py +++ b/backend/src/services/resource_service.py @@ -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 = [] diff --git a/backend/tests/scripts/test_clean_release_cli.py b/backend/tests/scripts/test_clean_release_cli.py index 08deb9e1..45f6b42e 100644 --- a/backend/tests/scripts/test_clean_release_cli.py +++ b/backend/tests/scripts/test_clean_release_cli.py @@ -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] \ No newline at end of file +# [/DEF:test_clean_release_cli:Module] diff --git a/backend/tests/scripts/test_clean_release_tui.py b/backend/tests/scripts/test_clean_release_tui.py index 682aa6f2..87a87e55 100644 --- a/backend/tests/scripts/test_clean_release_tui.py +++ b/backend/tests/scripts/test_clean_release_tui.py @@ -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] - diff --git a/backend/tests/scripts/test_clean_release_tui_v2.py b/backend/tests/scripts/test_clean_release_tui_v2.py index 46ae71b4..77185611 100644 --- a/backend/tests/scripts/test_clean_release_tui_v2.py +++ b/backend/tests/scripts/test_clean_release_tui_v2.py @@ -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] \ No newline at end of file +# [/DEF:test_clean_release_tui_v2:Module] diff --git a/backend/tests/services/clean_release/test_candidate_manifest_services.py b/backend/tests/services/clean_release/test_candidate_manifest_services.py index ba8443fd..88089578 100644 --- a/backend/tests/services/clean_release/test_candidate_manifest_services.py +++ b/backend/tests/services/clean_release/test_candidate_manifest_services.py @@ -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] \ No newline at end of file +# [/DEF:test_candidate_manifest_services:Module] diff --git a/backend/tests/services/clean_release/test_compliance_execution_service.py b/backend/tests/services/clean_release/test_compliance_execution_service.py index 88d0906d..58643e1b 100644 --- a/backend/tests/services/clean_release/test_compliance_execution_service.py +++ b/backend/tests/services/clean_release/test_compliance_execution_service.py @@ -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] \ No newline at end of file +# [/DEF:backend.tests.services.clean_release.test_compliance_execution_service:Module] diff --git a/backend/tests/services/clean_release/test_compliance_task_integration.py b/backend/tests/services/clean_release/test_compliance_task_integration.py index 01c12143..f27d2916 100644 --- a/backend/tests/services/clean_release/test_compliance_task_integration.py +++ b/backend/tests/services/clean_release/test_compliance_task_integration.py @@ -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] \ No newline at end of file +# [/DEF:backend.tests.services.clean_release.test_compliance_task_integration:Module] diff --git a/backend/tests/services/clean_release/test_demo_mode_isolation.py b/backend/tests/services/clean_release/test_demo_mode_isolation.py index f5440493..30f19485 100644 --- a/backend/tests/services/clean_release/test_demo_mode_isolation.py +++ b/backend/tests/services/clean_release/test_demo_mode_isolation.py @@ -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] \ No newline at end of file +# [/DEF:backend.tests.services.clean_release.test_demo_mode_isolation:Module] diff --git a/backend/tests/services/clean_release/test_policy_resolution_service.py b/backend/tests/services/clean_release/test_policy_resolution_service.py index 09606dfe..516715ef 100644 --- a/backend/tests/services/clean_release/test_policy_resolution_service.py +++ b/backend/tests/services/clean_release/test_policy_resolution_service.py @@ -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] \ No newline at end of file +# [/DEF:backend.tests.services.clean_release.test_policy_resolution_service:Module] diff --git a/frontend/src/lib/components/health/HealthMatrix.svelte b/frontend/src/lib/components/health/HealthMatrix.svelte index 5e729152..c7eca3ef 100644 --- a/frontend/src/lib/components/health/HealthMatrix.svelte +++ b/frontend/src/lib/components/health/HealthMatrix.svelte @@ -9,6 +9,8 @@ @UX_REATIVITY: State: $state, Derived: $derived. --> @@ -1440,7 +1442,7 @@