# #region TestFacadeAdapter [C:3] [TYPE Module] [SEMANTICS test,clean-release,facade,tui,adapter] # @BRIEF Tests for TuiFacadeAdapter and CleanReleaseTUI logic methods (non-rendering). # @RELATION BINDS_TO -> [CleanReleaseTuiScript] # @TEST_EDGE: facade_build_config_no_policy -> Raises ValueError # @TEST_EDGE: facade_run_compliance_no_manifests -> Raises ValueError # @TEST_EDGE: facade_approve_latest_no_reports -> Raises ValueError # @TEST_EDGE: facade_publish_latest_no_reports -> Raises ValueError # @TEST_EDGE: resolve_candidate_id_by_env -> Uses CLEAN_TUI_CANDIDATE_ID # @TEST_EDGE: resolve_candidate_id_fallback -> Uses first candidate # @TEST_EDGE: resolve_candidate_id_empty -> Returns "" # @TEST_EDGE: tui_run_checks_error -> FAILED status on exception # @TEST_EDGE: tui_approve_no_report -> Disabled message when no report_id # @TEST_EDGE: tui_publish_no_report -> Disabled message when no report_id # @TEST_EDGE: tui_approve_error -> Error stored on facade failure # @TEST_EDGE: tui_publish_error -> Error stored on facade failure # @TEST_EDGE: tui_build_manifest_error -> Error stored on facade failure # @TEST_EDGE: bootstrap_real_no_path -> Returns early import os import pytest from unittest.mock import MagicMock, patch, PropertyMock from types import SimpleNamespace from src.models.clean_release import ( CheckFinalStatus, CheckStageStatus, ComplianceViolation, ReleaseCandidate, CleanProfilePolicy, ProfileType, ResourceSourceRegistry, ResourceSourceEntry, RegistryStatus, ReleaseCandidateStatus, CandidateArtifact, ) from src.services.clean_release.enums import CandidateStatus from src.scripts.clean_release_tui import TuiFacadeAdapter, CleanReleaseTUI # ============================================================================= # TuiFacadeAdapter tests (no curses required) # ============================================================================= class TestTuiFacadeAdapterBuildConfig: """Verify _build_config_manager.""" def test_build_config_with_policy(self): """Happy: policy found returns config with active_policy_id.""" repo = MagicMock() policy = MagicMock() policy.id = "POL-1" policy.registry_snapshot_id = "REG-1" repo.get_active_policy.return_value = policy adapter = TuiFacadeAdapter(repo) cm = adapter._build_config_manager() config = cm.get_config() assert config.settings.clean_release.active_policy_id == "POL-1" assert config.settings.clean_release.active_registry_id == "REG-1" def test_build_config_no_policy_raises(self): """Edge: no active policy raises ValueError.""" repo = MagicMock() repo.get_active_policy.return_value = None adapter = TuiFacadeAdapter(repo) with pytest.raises(ValueError, match="Active policy not found"): adapter._build_config_manager() class TestTuiFacadeAdapterRunCompliance: """Verify run_compliance.""" def test_run_compliance_with_manifests(self): """Happy: manifests found, executes compliance.""" repo = MagicMock() policy = MagicMock() policy.id = "POL-1" policy.registry_snapshot_id = "REG-1" repo.get_active_policy.return_value = policy manifest_old = MagicMock() manifest_old.manifest_version = 1 manifest_new = MagicMock() manifest_new.manifest_version = 2 repo.get_manifests_by_candidate.return_value = [manifest_old, manifest_new] adapter = TuiFacadeAdapter(repo) with patch('src.scripts.clean_release_tui.ComplianceExecutionService') as MockService: mock_instance = MockService.return_value mock_instance.execute_run.return_value = "compliance_result" result = adapter.run_compliance(candidate_id="cand-1", actor="test") repo.get_manifests_by_candidate.assert_called_once_with("cand-1") assert result == "compliance_result" def test_run_compliance_no_manifests_raises(self): """Edge: no manifests raises ValueError.""" repo = MagicMock() repo.get_manifests_by_candidate.return_value = [] adapter = TuiFacadeAdapter(repo) with pytest.raises(ValueError, match="Manifest required before compliance run"): adapter.run_compliance(candidate_id="cand-1", actor="test") class TestTuiFacadeAdapterApproveLatest: """Verify approve_latest.""" def test_approve_with_reports(self): """Happy: reports found, approves.""" repo = MagicMock() r1 = MagicMock() r1.candidate_id = "cand-1" r1.generated_at = "2024-01-01T00:00:00" r2 = MagicMock() r2.candidate_id = "cand-1" r2.generated_at = "2024-06-01T00:00:00" repo.reports = {"r1": r1, "r2": r2} adapter = TuiFacadeAdapter(repo) with patch('src.scripts.clean_release_tui.approve_candidate') as mock_approve: adapter.approve_latest(candidate_id="cand-1", actor="test") mock_approve.assert_called_once() def test_approve_no_reports_raises(self): """Edge: no reports raises ValueError.""" repo = MagicMock() repo.reports = {} adapter = TuiFacadeAdapter(repo) with pytest.raises(ValueError, match="No compliance report available for approval"): adapter.approve_latest(candidate_id="cand-1", actor="test") class TestTuiFacadeAdapterPublishLatest: """Verify publish_latest.""" def test_publish_with_reports(self): """Happy: reports found, publishes.""" repo = MagicMock() r1 = MagicMock() r1.candidate_id = "cand-1" r1.generated_at = "2024-01-01T00:00:00" repo.reports = {"r1": r1} adapter = TuiFacadeAdapter(repo) with patch('src.scripts.clean_release_tui.publish_candidate') as mock_publish: adapter.publish_latest(candidate_id="cand-1", actor="test") mock_publish.assert_called_once() def test_publish_no_reports_raises(self): """Edge: no reports raises ValueError.""" repo = MagicMock() repo.reports = {} adapter = TuiFacadeAdapter(repo) with pytest.raises(ValueError, match="No compliance report available for publication"): adapter.publish_latest(candidate_id="cand-1", actor="test") class TestTuiFacadeAdapterBuildManifest: """Verify build_manifest.""" def test_build_manifest_delegates(self): """Happy: delegates to build_manifest_snapshot.""" repo = MagicMock() adapter = TuiFacadeAdapter(repo) with patch('src.scripts.clean_release_tui.build_manifest_snapshot') as mock_build: adapter.build_manifest(candidate_id="cand-1", actor="test") mock_build.assert_called_once_with( repository=repo, candidate_id="cand-1", created_by="test", ) class TestTuiFacadeAdapterGetOverview: """Verify get_overview.""" def test_get_overview_full(self): """Happy: all data present.""" repo = MagicMock() candidate = MagicMock() candidate.id = "cand-1" repo.get_candidate.return_value = candidate manifest = MagicMock() manifest.manifest_version = 2 repo.get_manifests_by_candidate.return_value = [manifest] run = MagicMock() run.candidate_id = "cand-1" run.requested_at = "2024-06-01T00:00:00" repo.check_runs = {"r1": run} report = MagicMock() report.run_id = run.id repo.reports = {"rep1": report} approval = MagicMock() approval.candidate_id = "cand-1" approval.decided_at = "2024-06-02T00:00:00" repo.approval_decisions = [approval] publication = MagicMock() publication.candidate_id = "cand-1" publication.published_at = "2024-06-03T00:00:00" repo.publication_records = [publication] policy = MagicMock() policy.internal_source_registry_ref = "REG-1" repo.get_active_policy.return_value = policy registry = MagicMock() repo.get_registry.return_value = registry adapter = TuiFacadeAdapter(repo) overview = adapter.get_overview(candidate_id="cand-1") assert overview["candidate"] is candidate assert overview["manifest"] is manifest assert overview["run"] is run assert overview["report"] is report assert overview["approval"] is approval assert overview["publication"] is publication assert overview["policy"] is policy assert overview["registry"] is registry def test_get_overview_empty(self): """Edge: no data at all, returns safe defaults.""" repo = MagicMock() repo.get_candidate.return_value = None repo.get_manifests_by_candidate.return_value = [] repo.check_runs = {} repo.reports = {} repo.get_active_policy.return_value = None adapter = TuiFacadeAdapter(repo) overview = adapter.get_overview(candidate_id="nonexistent") assert overview["candidate"] is None assert overview["manifest"] is None assert overview["run"] is None assert overview["report"] is None assert overview["approval"] is None assert overview["publication"] is None assert overview["policy"] is None assert overview["registry"] is None # ============================================================================= # CleanReleaseTUI logic tests (curses mocked at module level) # ============================================================================= @pytest.fixture def mock_stdscr(): stdscr = MagicMock() stdscr.getmaxyx.return_value = (40, 100) stdscr.getch.return_value = -1 return stdscr class TestResolveCandidateId: """Verify _resolve_candidate_id.""" @patch("src.scripts.clean_release_tui.curses") def test_returns_env_var(self, mock_curses, mock_stdscr): """Edge: CLEAN_TUI_CANDIDATE_ID set returns env value.""" with patch.dict(os.environ, {"CLEAN_TUI_CANDIDATE_ID": "env-cand"}, clear=False): app = CleanReleaseTUI(mock_stdscr) # After init the candidate_id is already resolved, verify it assert app.candidate_id == "env-cand" @patch("src.scripts.clean_release_tui.curses") def test_falls_back_to_first_candidate(self, mock_curses, mock_stdscr): """Edge: no env var, uses first candidate key.""" app = CleanReleaseTUI(mock_stdscr) # Demo mode creates a candidate "2026.03.03-rc1" assert app.candidate_id == "2026.03.03-rc1" @patch("src.scripts.clean_release_tui.curses") def test_returns_empty_when_no_candidates(self, mock_curses): """Edge: no env var and no candidates returns empty string.""" stdscr = MagicMock() stdscr.getmaxyx.return_value = (40, 100) # Inject a repo with no candidates from src.services.clean_release.repository import CleanReleaseRepository from src.scripts.clean_release_tui import TuiFacadeAdapter with patch.dict(os.environ, {"CLEAN_TUI_MODE": "real"}, clear=False): with patch("src.scripts.clean_release_tui.CleanReleaseTUI._bootstrap_real_repository"): app = CleanReleaseTUI(stdscr) # In real mode with no bootstrap, no candidates exist resolved = app._resolve_candidate_id() # _resolve_candidate_id is called during __init__, so the result is already in app.candidate_id assert app.candidate_id == "" class TestTuiBuildManifestError: """Verify build_manifest error handling.""" @patch("src.scripts.clean_release_tui.curses") def test_build_manifest_captures_error(self, mock_curses, mock_stdscr): """Edge: facade raises, error stored.""" app = CleanReleaseTUI(mock_stdscr) # Replace facade with one that raises app.facade = MagicMock() app.facade.build_manifest.side_effect = ValueError("Build failed") app.build_manifest() assert "Build failed" in app.last_error class TestTuiApproveError: """Verify approve_latest error handling.""" @patch("src.scripts.clean_release_tui.curses") def test_approve_no_report_id(self, mock_curses, mock_stdscr): """Edge: no report_id sets disabled message.""" app = CleanReleaseTUI(mock_stdscr) app.report_id = None app.approve_latest() assert "F8 disabled" in app.last_error @patch("src.scripts.clean_release_tui.curses") def test_approve_facade_error(self, mock_curses, mock_stdscr): """Edge: facade raises, error stored.""" app = CleanReleaseTUI(mock_stdscr) app.report_id = "rep-1" app.facade = MagicMock() app.facade.approve_latest.side_effect = ValueError("Approve failed") app.approve_latest() assert "Approve failed" in app.last_error class TestTuiPublishError: """Verify publish_latest error handling.""" @patch("src.scripts.clean_release_tui.curses") def test_publish_no_report_id(self, mock_curses, mock_stdscr): """Edge: no report_id sets disabled message.""" app = CleanReleaseTUI(mock_stdscr) app.report_id = None app.publish_latest() assert "F9 disabled" in app.last_error @patch("src.scripts.clean_release_tui.curses") def test_publish_facade_error(self, mock_curses, mock_stdscr): """Edge: facade raises, error stored.""" app = CleanReleaseTUI(mock_stdscr) app.report_id = "rep-1" app.facade = MagicMock() app.facade.publish_latest.side_effect = ValueError("Publish failed") app.publish_latest() assert "Publish failed" in app.last_error class TestTuiRunChecksError: """Verify run_checks error handling.""" @patch("src.scripts.clean_release_tui.curses") def test_run_checks_facade_error(self, mock_curses, mock_stdscr): """Edge: facade raises during run_checks -> FAILED status.""" app = CleanReleaseTUI(mock_stdscr) app.facade = MagicMock() app.facade.run_compliance.side_effect = RuntimeError("Compliance crashed") app.run_checks() assert app.status == CheckFinalStatus.FAILED assert "Compliance crashed" in app.last_error class TestTuiClearHistory: """Verify clear_history resets state.""" @patch("src.scripts.clean_release_tui.curses") def test_clear_history_resets_state(self, mock_curses, mock_stdscr): """Edge: after running checks, clear_history resets.""" app = CleanReleaseTUI(mock_stdscr) app.status = CheckFinalStatus.BLOCKED app.report_id = "rep-1" app.violations_list = [MagicMock()] app.checks_progress = [{"stage": "test", "status": "FAIL"}] app.last_error = "some error" app.clear_history() assert app.status == "READY" assert app.report_id is None assert app.violations_list == [] assert app.checks_progress == [] assert app.last_error is None # ============================================================================= # _bootstrap_real_repository tests # ============================================================================= class TestBootstrapRealRepository: """Verify _bootstrap_real_repository.""" @patch("src.scripts.clean_release_tui.curses") def test_no_bootstrap_path_returns_early(self, mock_curses, mock_stdscr): """Edge: CLEAN_TUI_BOOTSTRAP_JSON not set returns early.""" from src.services.clean_release.repository import CleanReleaseRepository app = CleanReleaseTUI(mock_stdscr) repo = CleanReleaseRepository() app._bootstrap_real_repository(repo) # No bootstrap loaded -> no candidates assert len(repo.candidates) == 0 @patch("src.scripts.clean_release_tui.curses") def test_bootstrap_with_valid_json(self, mock_curses, mock_stdscr, tmp_path): """Happy: valid bootstrap.json loads candidate, registry and policy.""" import json bootstrap = tmp_path / "bootstrap.json" bootstrap.write_text(json.dumps({ "candidate_id": "test-cand", "version": "1.0.0", "source_snapshot_ref": "git-ref", "created_by": "test", "allowed_hosts": ["repo.example.com"], }), encoding="utf-8") from src.services.clean_release.repository import CleanReleaseRepository app = CleanReleaseTUI(mock_stdscr) repo = CleanReleaseRepository() with patch.dict(os.environ, { "CLEAN_TUI_BOOTSTRAP_JSON": str(bootstrap), }, clear=False): app._bootstrap_real_repository(repo) assert repo.get_candidate("test-cand") is not None assert len(repo.registries) > 0 assert len(repo.policies) > 0 assert repo.get_candidate("test-cand").status == "DRAFT" @patch("src.scripts.clean_release_tui.curses") def test_bootstrap_with_artifacts_transitions_to_prepared(self, mock_curses, mock_stdscr, tmp_path): """Happy: artifacts imported transitions candidate to PREPARED.""" import json bootstrap = tmp_path / "bootstrap.json" bootstrap.write_text(json.dumps({ "candidate_id": "test-cand-2", "version": "2.0.0", "source_snapshot_ref": "git-ref-2", "created_by": "test", "allowed_hosts": [], }), encoding="utf-8") artifacts = tmp_path / "artifacts.json" artifacts.write_text(json.dumps({ "artifacts": [{ "id": "art-1", "path": "dist/file.tar.gz", "sha256": "abc123", "size": 512, "category": "core", "source_uri": "https://repo.example.com/file.tar.gz", "source_host": "repo.example.com", }] }), encoding="utf-8") from src.services.clean_release.repository import CleanReleaseRepository app = CleanReleaseTUI(mock_stdscr) repo = CleanReleaseRepository() with patch.dict(os.environ, { "CLEAN_TUI_BOOTSTRAP_JSON": str(bootstrap), "CLEAN_TUI_ARTIFACTS_JSON": str(artifacts), }, clear=False): app._bootstrap_real_repository(repo) assert repo.get_candidate("test-cand-2").status == "PREPARED" artifacts_list = repo.get_artifacts_by_candidate("test-cand-2") assert len(artifacts_list) == 1 assert artifacts_list[0].detected_category == "core" @patch("src.scripts.clean_release_tui.curses") def test_bootstrap_with_entries_saves_policy(self, mock_curses, mock_stdscr, tmp_path): """Edge: entries present saves registry and policy.""" import json bootstrap = tmp_path / "bootstrap.json" bootstrap.write_text(json.dumps({ "candidate_id": "test-cand-3", "version": "3.0.0", "source_snapshot_ref": "git-ref-3", "created_by": "test", "allowed_hosts": ["repo.example.com"], "registry_id": "REG-TEST", "policy_id": "POL-TEST", }), encoding="utf-8") from src.services.clean_release.repository import CleanReleaseRepository app = CleanReleaseTUI(mock_stdscr) repo = CleanReleaseRepository() with patch.dict(os.environ, { "CLEAN_TUI_BOOTSTRAP_JSON": str(bootstrap), }, clear=False): app._bootstrap_real_repository(repo) assert repo.get_registry("REG-TEST") is not None policy = repo.get_policy("POL-TEST") assert policy is not None # #endregion TestFacadeAdapter