# [DEF:backend.tests.scripts.test_clean_release_tui:Module] # @TIER: STANDARD # @SEMANTICS: tests, tui, clean-release, curses # @PURPOSE: Unit tests for the interactive curses TUI of the clean release process. # @LAYER: Scripts # @RELATION: TESTS -> backend.src.scripts.clean_release_tui # @INVARIANT: TUI initializes, handles hotkeys (F5, F10) and safely falls back without TTY. import os import sys import curses from unittest import mock 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 @pytest.fixture def mock_stdscr() -> MagicMock: stdscr = MagicMock() stdscr.getmaxyx.return_value = (40, 100) stdscr.getch.return_value = -1 return stdscr def test_headless_fallback(capsys): """ @TEST_EDGE: stdout_unavailable Tests that if the stream is not a TTY or PYTEST_CURRENT_TEST is set, the script falls back to a simple stdout print instead of trapping in curses.wrapper. """ # Environment should trigger headless fallback due to PYTEST_CURRENT_TEST being set with mock.patch("backend.src.scripts.clean_release_tui.curses.wrapper") as curses_wrapper_mock: with mock.patch("sys.stdout.isatty", return_value=False): exit_code = main() # Ensures wrapper wasn't used curses_wrapper_mock.assert_not_called() # Verify it still exits 0 assert exit_code == 0 # Verify headless info is printed captured = capsys.readouterr() assert "Enterprise Clean Release Validator (Headless Mode)" in captured.out assert "FINAL STATUS: READY" in captured.out @patch("backend.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 titles, headers, footers and the READY state are drawn appropriately. """ # Ensure constants match mock_curses_module.KEY_F10 = curses.KEY_F10 mock_curses_module.KEY_F5 = curses.KEY_F5 mock_curses_module.color_pair.side_effect = lambda x: x mock_curses_module.A_BOLD = 0 app = CleanReleaseTUI(mock_stdscr) assert app.status == "READY" # We only want to run one loop iteration, so we mock getch to return F10 mock_stdscr.getch.return_value = curses.KEY_F10 app.loop() # Assert header was drawn addstr_calls = mock_stdscr.addstr.call_args_list assert any("Enterprise Clean Release Validator" in str(call) for call in addstr_calls) assert any("Candidate: [2026.03.03-rc1]" in str(call) for call in addstr_calls) # Assert checks list is shown assert any("Data Purity" in str(call) for call in addstr_calls) assert any("Internal Sources Only" in str(call) for call in addstr_calls) # Assert footer is shown assert any("F5 Run" in str(call) for call in addstr_calls) @patch("backend.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. """ # Ensure constants match mock_curses_module.KEY_F10 = curses.KEY_F10 mock_curses_module.KEY_F5 = curses.KEY_F5 mock_curses_module.color_pair.side_effect = lambda x: x mock_curses_module.A_BOLD = 0 app = CleanReleaseTUI(mock_stdscr) # getch sequence: # 1. First loop: F5 (triggers run_checks) # 2. Next call after run_checks: F10 to exit mock_stdscr.f5_pressed = False def side_effect(): if not mock_stdscr.f5_pressed: mock_stdscr.f5_pressed = True return curses.KEY_F5 return curses.KEY_F10 mock_stdscr.getch.side_effect = side_effect with mock.patch("time.sleep", return_value=None): app.loop() # After F5 is pressed, status should be BLOCKED due to deliberate 'test-data' violation assert app.status == CheckFinalStatus.BLOCKED assert app.report_id is not None assert "CCR-" in app.report_id assert len(app.violations_list) > 0 @patch("backend.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. """ # Ensure constants match mock_curses_module.KEY_F10 = curses.KEY_F10 app = CleanReleaseTUI(mock_stdscr) mock_stdscr.getch.return_value = curses.KEY_F10 # loop() should return cleanly app.loop() assert app.status == "READY" @patch("backend.src.scripts.clean_release_tui.curses") def test_tui_clear_history_f7(mock_curses_module, mock_stdscr: MagicMock): """ Simulates pressing F7 to clear history. """ mock_curses_module.KEY_F10 = curses.KEY_F10 mock_curses_module.KEY_F7 = curses.KEY_F7 mock_curses_module.color_pair.side_effect = lambda x: x mock_curses_module.A_BOLD = 0 app = CleanReleaseTUI(mock_stdscr) app.status = CheckFinalStatus.BLOCKED app.report_id = "SOME-REPORT" # F7 then F10 mock_stdscr.getch.side_effect = [curses.KEY_F7, curses.KEY_F10] app.loop() assert app.status == "READY" assert app.report_id is None assert len(app.checks_progress) == 0 # [/DEF:backend.tests.scripts.test_clean_release_tui:Module]