Files
ss-tools/backend/tests/scripts/test_clean_release_tui.py
busya 4205618ee6 chore: eliminate all deprecation warnings from tests and linter
Warnings fixed:
- datetime.utcnow() → datetime.now(UTC) across 48+ files (src/ + tests/)
- datetime.utcnow (callback ref) → lambda: datetime.now(UTC) in model fields (18 files)
- Pydantic class Config → model_config = ConfigDict(...) (16 files)
- Pydantic .dict() → .model_dump() (8 files)
- ConfigDict(allow_population_by_field_name=True) → validate_by_name=True
- SQLAlchemy declarative_base() import path updated
- FastAPI on_event → lifespan context manager (app.py)
- Import sorting (ruff I001) auto-fixed across all files
- Fixed broken re-export chains that ruff F401 cleanup broke:
  _validate_bcp47: service.py now imports from dictionary_validation directly
  job_to_response: _job_routes.py and test imports from service_utils directly
  fetch_datasource_metadata: restored re-export in service.py
- Added missing TranslateJobService import in _job_routes.py (was deleted by F401)
- Added ConfigDict(protected_namespaces=()) for DashboardDatasetItem schema field
- pytest.ini: replaced deprecated importmode with asyncio_mode

All 440 tests pass with zero deprecation warnings.
2026-05-26 19:18:28 +03:00

245 lines
7.8 KiB
Python

# #region TestCleanReleaseTui [C:2] [TYPE Module]
# @RELATION BINDS_TO -> SrcRoot
# @SEMANTICS: tests, tui, clean-release, curses
# @PURPOSE: Unit tests for the interactive curses TUI of the clean release process.
# @LAYER Tests
# @INVARIANT: TUI initializes, handles hotkeys (F5, F10) and safely falls back without TTY.
import curses
import json
import os
import pytest
from unittest import mock
from unittest.mock import MagicMock, patch
from src.models.clean_release import CheckFinalStatus
from src.scripts.clean_release_tui import CleanReleaseTUI, main
@pytest.fixture
def mock_stdscr() -> MagicMock:
stdscr = MagicMock()
stdscr.getmaxyx.return_value = (40, 100)
stdscr.getch.return_value = -1
return stdscr
# #region test_headless_fallback [C:2] [TYPE Function]
# @RELATION BINDS_TO -> TestCleanReleaseTui
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("src.scripts.clean_release_tui.curses.wrapper") as curses_wrapper_mock:
with mock.patch("sys.stdout.isatty", return_value=False):
exit_code = main()
curses_wrapper_mock.assert_not_called()
assert exit_code == 2
captured = capsys.readouterr()
assert "TTY is required for TUI mode" in captured.err
assert "Use CLI/API workflow instead" in captured.err
# #endregion test_headless_fallback
@patch("src.scripts.clean_release_tui.curses")
# #region test_tui_initial_render [C:2] [TYPE Function]
# @RELATION BINDS_TO -> TestCleanReleaseTui
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)
# #endregion test_tui_initial_render
@patch("src.scripts.clean_release_tui.curses")
# #region test_tui_run_checks_f5 [C:2] [TYPE Function]
# @RELATION BINDS_TO -> TestCleanReleaseTui
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
# #endregion test_tui_run_checks_f5
@patch("src.scripts.clean_release_tui.curses")
# #region test_tui_exit_f10 [C:2] [TYPE Function]
# @RELATION BINDS_TO -> TestCleanReleaseTui
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"
# #endregion test_tui_exit_f10
@patch("src.scripts.clean_release_tui.curses")
# #region test_tui_clear_history_f7 [C:2] [TYPE Function]
# @RELATION BINDS_TO -> TestCleanReleaseTui
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
# #endregion test_tui_clear_history_f7
@patch("src.scripts.clean_release_tui.curses")
# #region test_tui_real_mode_bootstrap_imports_artifacts_catalog [C:2] [TYPE Function]
# @RELATION BINDS_TO -> TestCleanReleaseTui
def test_tui_real_mode_bootstrap_imports_artifacts_catalog(
mock_curses_module,
mock_stdscr: MagicMock,
tmp_path,
# #endregion test_tui_real_mode_bootstrap_imports_artifacts_catalog
):
"""
@TEST_CONTRACT: bootstrap.json + artifacts.json -> candidate PREPARED with imported artifacts
"""
mock_curses_module.KEY_F10 = curses.KEY_F10
mock_curses_module.color_pair.side_effect = lambda x: x
mock_curses_module.A_BOLD = 0
bootstrap_path = tmp_path / "bootstrap.json"
artifacts_path = tmp_path / "artifacts.json"
bootstrap_path.write_text(
json.dumps(
{
"candidate_id": "real-candidate-1",
"version": "1.0.0",
"source_snapshot_ref": "git:release/1",
"created_by": "operator",
"allowed_hosts": ["repo.intra.company.local"],
}
),
encoding="utf-8",
)
artifacts_path.write_text(
json.dumps(
{
"artifacts": [
{
"id": "artifact-1",
"path": "backend/dist/package.tar.gz",
"sha256": "deadbeef",
"size": 1024,
"category": "core",
"source_uri": "https://repo.intra.company.local/releases/package.tar.gz",
"source_host": "repo.intra.company.local",
}
]
}
),
encoding="utf-8",
)
with mock.patch.dict(
os.environ,
{
"CLEAN_TUI_MODE": "real",
"CLEAN_TUI_BOOTSTRAP_JSON": str(bootstrap_path),
"CLEAN_TUI_ARTIFACTS_JSON": str(artifacts_path),
},
clear=False,
):
app = CleanReleaseTUI(mock_stdscr)
candidate = app.repo.get_candidate("real-candidate-1")
artifacts = app.repo.get_artifacts_by_candidate("real-candidate-1")
assert candidate is not None
assert candidate.status == "PREPARED"
assert len(artifacts) == 1
assert artifacts[0].path == "backend/dist/package.tar.gz"
assert artifacts[0].detected_category == "core"
# #endregion TestCleanReleaseTui