164 lines
5.4 KiB
Python
164 lines
5.4 KiB
Python
# [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]
|
|
|