feat(clean-release): complete compliance redesign phases and polish tasks T047-T052
This commit is contained in:
305
backend/tests/scripts/test_clean_release_cli.py
Normal file
305
backend/tests/scripts/test_clean_release_cli.py
Normal file
@@ -0,0 +1,305 @@
|
||||
# [DEF:test_clean_release_cli:Module]
|
||||
# @TIER: STANDARD
|
||||
# @PURPOSE: Smoke tests for the redesigned clean release CLI.
|
||||
# @LAYER: Domain
|
||||
|
||||
"""Smoke tests for the redesigned clean release CLI commands."""
|
||||
|
||||
from types import SimpleNamespace
|
||||
import json
|
||||
|
||||
from backend.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
|
||||
|
||||
|
||||
def test_cli_candidate_register_scaffold() -> None:
|
||||
"""Candidate register CLI command smoke test."""
|
||||
exit_code = cli_main(
|
||||
[
|
||||
"candidate-register",
|
||||
"--candidate-id",
|
||||
"cli-candidate-1",
|
||||
"--version",
|
||||
"1.0.0",
|
||||
"--source-snapshot-ref",
|
||||
"git:sha123",
|
||||
"--created-by",
|
||||
"cli-test",
|
||||
]
|
||||
)
|
||||
assert exit_code == 0
|
||||
|
||||
|
||||
def test_cli_manifest_build_scaffold() -> None:
|
||||
"""Manifest build CLI command smoke test."""
|
||||
register_exit = cli_main(
|
||||
[
|
||||
"candidate-register",
|
||||
"--candidate-id",
|
||||
"cli-candidate-2",
|
||||
"--version",
|
||||
"1.0.0",
|
||||
"--source-snapshot-ref",
|
||||
"git:sha234",
|
||||
"--created-by",
|
||||
"cli-test",
|
||||
]
|
||||
)
|
||||
assert register_exit == 0
|
||||
|
||||
import_exit = cli_main(
|
||||
[
|
||||
"artifact-import",
|
||||
"--candidate-id",
|
||||
"cli-candidate-2",
|
||||
"--artifact-id",
|
||||
"artifact-2",
|
||||
"--path",
|
||||
"bin/app",
|
||||
"--sha256",
|
||||
"feedbeef",
|
||||
"--size",
|
||||
"24",
|
||||
]
|
||||
)
|
||||
assert import_exit == 0
|
||||
|
||||
manifest_exit = cli_main(
|
||||
[
|
||||
"manifest-build",
|
||||
"--candidate-id",
|
||||
"cli-candidate-2",
|
||||
"--created-by",
|
||||
"cli-test",
|
||||
]
|
||||
)
|
||||
assert manifest_exit == 0
|
||||
|
||||
|
||||
def test_cli_compliance_run_scaffold() -> None:
|
||||
"""Compliance CLI command smoke test for run/status/report/violations."""
|
||||
repository = get_clean_release_repository()
|
||||
config_manager = get_config_manager()
|
||||
|
||||
registry = SourceRegistrySnapshot(
|
||||
id="cli-registry",
|
||||
registry_id="trusted-registry",
|
||||
registry_version="1.0.0",
|
||||
allowed_hosts=["repo.internal.local"],
|
||||
allowed_schemes=["https"],
|
||||
allowed_source_types=["repo"],
|
||||
immutable=True,
|
||||
)
|
||||
policy = CleanPolicySnapshot(
|
||||
id="cli-policy",
|
||||
policy_id="trusted-policy",
|
||||
policy_version="1.0.0",
|
||||
content_json={"rules": []},
|
||||
registry_snapshot_id=registry.id,
|
||||
immutable=True,
|
||||
)
|
||||
repository.save_registry(registry)
|
||||
repository.save_policy(policy)
|
||||
|
||||
config = config_manager.get_config()
|
||||
if getattr(config, "settings", None) is None:
|
||||
config.settings = SimpleNamespace()
|
||||
config.settings.clean_release = SimpleNamespace(
|
||||
active_policy_id=policy.id,
|
||||
active_registry_id=registry.id,
|
||||
)
|
||||
|
||||
register_exit = cli_main(
|
||||
[
|
||||
"candidate-register",
|
||||
"--candidate-id",
|
||||
"cli-candidate-3",
|
||||
"--version",
|
||||
"1.0.0",
|
||||
"--source-snapshot-ref",
|
||||
"git:sha345",
|
||||
"--created-by",
|
||||
"cli-test",
|
||||
]
|
||||
)
|
||||
assert register_exit == 0
|
||||
|
||||
import_exit = cli_main(
|
||||
[
|
||||
"artifact-import",
|
||||
"--candidate-id",
|
||||
"cli-candidate-3",
|
||||
"--artifact-id",
|
||||
"artifact-1",
|
||||
"--path",
|
||||
"bin/app",
|
||||
"--sha256",
|
||||
"deadbeef",
|
||||
"--size",
|
||||
"42",
|
||||
]
|
||||
)
|
||||
assert import_exit == 0
|
||||
|
||||
manifest_exit = cli_main(
|
||||
[
|
||||
"manifest-build",
|
||||
"--candidate-id",
|
||||
"cli-candidate-3",
|
||||
"--created-by",
|
||||
"cli-test",
|
||||
]
|
||||
)
|
||||
assert manifest_exit == 0
|
||||
|
||||
run_exit = cli_main(
|
||||
[
|
||||
"compliance-run",
|
||||
"--candidate-id",
|
||||
"cli-candidate-3",
|
||||
"--actor",
|
||||
"cli-test",
|
||||
"--json",
|
||||
]
|
||||
)
|
||||
assert run_exit == 0
|
||||
|
||||
run_id = next(run.id for run in repository.check_runs.values() if run.candidate_id == "cli-candidate-3")
|
||||
|
||||
status_exit = cli_main(["compliance-status", "--run-id", run_id, "--json"])
|
||||
assert status_exit == 0
|
||||
|
||||
violations_exit = cli_main(["compliance-violations", "--run-id", run_id, "--json"])
|
||||
assert violations_exit == 0
|
||||
|
||||
report_exit = cli_main(["compliance-report", "--run-id", run_id, "--json"])
|
||||
assert report_exit == 0
|
||||
|
||||
|
||||
def test_cli_release_gate_commands_scaffold() -> None:
|
||||
"""Release gate CLI smoke test for approve/reject/publish/revoke commands."""
|
||||
repository = get_clean_release_repository()
|
||||
|
||||
approved_candidate_id = f"cli-release-approved-{uuid4()}"
|
||||
rejected_candidate_id = f"cli-release-rejected-{uuid4()}"
|
||||
approved_report_id = f"CCR-cli-release-approved-{uuid4()}"
|
||||
rejected_report_id = f"CCR-cli-release-rejected-{uuid4()}"
|
||||
|
||||
repository.save_candidate(
|
||||
ReleaseCandidate(
|
||||
id=approved_candidate_id,
|
||||
version="1.0.0",
|
||||
source_snapshot_ref="git:sha-approved",
|
||||
created_by="cli-test",
|
||||
created_at=datetime.now(timezone.utc),
|
||||
status=CandidateStatus.CHECK_PASSED.value,
|
||||
)
|
||||
)
|
||||
repository.save_candidate(
|
||||
ReleaseCandidate(
|
||||
id=rejected_candidate_id,
|
||||
version="1.0.0",
|
||||
source_snapshot_ref="git:sha-rejected",
|
||||
created_by="cli-test",
|
||||
created_at=datetime.now(timezone.utc),
|
||||
status=CandidateStatus.CHECK_PASSED.value,
|
||||
)
|
||||
)
|
||||
repository.save_report(
|
||||
ComplianceReport(
|
||||
id=approved_report_id,
|
||||
run_id=f"run-{uuid4()}",
|
||||
candidate_id=approved_candidate_id,
|
||||
final_status=ComplianceDecision.PASSED.value,
|
||||
summary_json={"operator_summary": "ok", "violations_count": 0, "blocking_violations_count": 0},
|
||||
generated_at=datetime.now(timezone.utc),
|
||||
immutable=True,
|
||||
)
|
||||
)
|
||||
repository.save_report(
|
||||
ComplianceReport(
|
||||
id=rejected_report_id,
|
||||
run_id=f"run-{uuid4()}",
|
||||
candidate_id=rejected_candidate_id,
|
||||
final_status=ComplianceDecision.PASSED.value,
|
||||
summary_json={"operator_summary": "ok", "violations_count": 0, "blocking_violations_count": 0},
|
||||
generated_at=datetime.now(timezone.utc),
|
||||
immutable=True,
|
||||
)
|
||||
)
|
||||
|
||||
approve_exit = cli_main(
|
||||
[
|
||||
"approve",
|
||||
"--candidate-id",
|
||||
approved_candidate_id,
|
||||
"--report-id",
|
||||
approved_report_id,
|
||||
"--actor",
|
||||
"cli-test",
|
||||
"--comment",
|
||||
"approve candidate",
|
||||
"--json",
|
||||
]
|
||||
)
|
||||
assert approve_exit == 0
|
||||
|
||||
reject_exit = cli_main(
|
||||
[
|
||||
"reject",
|
||||
"--candidate-id",
|
||||
rejected_candidate_id,
|
||||
"--report-id",
|
||||
rejected_report_id,
|
||||
"--actor",
|
||||
"cli-test",
|
||||
"--comment",
|
||||
"reject candidate",
|
||||
"--json",
|
||||
]
|
||||
)
|
||||
assert reject_exit == 0
|
||||
|
||||
publish_exit = cli_main(
|
||||
[
|
||||
"publish",
|
||||
"--candidate-id",
|
||||
approved_candidate_id,
|
||||
"--report-id",
|
||||
approved_report_id,
|
||||
"--actor",
|
||||
"cli-test",
|
||||
"--target-channel",
|
||||
"stable",
|
||||
"--publication-ref",
|
||||
"rel-cli-001",
|
||||
"--json",
|
||||
]
|
||||
)
|
||||
assert publish_exit == 0
|
||||
|
||||
publication_records = getattr(repository, "publication_records", [])
|
||||
assert publication_records
|
||||
publication_id = publication_records[-1].id
|
||||
|
||||
revoke_exit = cli_main(
|
||||
[
|
||||
"revoke",
|
||||
"--publication-id",
|
||||
publication_id,
|
||||
"--actor",
|
||||
"cli-test",
|
||||
"--comment",
|
||||
"rollback",
|
||||
"--json",
|
||||
]
|
||||
)
|
||||
assert revoke_exit == 0
|
||||
|
||||
|
||||
# [/DEF:test_clean_release_cli:Module]
|
||||
@@ -29,25 +29,18 @@ def mock_stdscr() -> MagicMock:
|
||||
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.
|
||||
Tests that non-TTY startup is explicitly refused and wrapper is not invoked.
|
||||
"""
|
||||
# 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
|
||||
|
||||
assert exit_code == 2
|
||||
captured = capsys.readouterr()
|
||||
assert "Enterprise Clean Release Validator (Headless Mode)" in captured.out
|
||||
assert "FINAL STATUS: READY" in captured.out
|
||||
assert "TTY is required for TUI mode" in captured.err
|
||||
assert "Use CLI/API workflow instead" in captured.err
|
||||
|
||||
|
||||
@patch("backend.src.scripts.clean_release_tui.curses")
|
||||
|
||||
97
backend/tests/scripts/test_clean_release_tui_v2.py
Normal file
97
backend/tests/scripts/test_clean_release_tui_v2.py
Normal file
@@ -0,0 +1,97 @@
|
||||
# [DEF:test_clean_release_tui_v2:Module]
|
||||
# @TIER: STANDARD
|
||||
# @PURPOSE: Smoke tests for thin-client TUI action dispatch and blocked transition behavior.
|
||||
# @LAYER: Domain
|
||||
# @RELATION: TESTS -> backend.src.scripts.clean_release_tui
|
||||
|
||||
"""Smoke tests for the redesigned clean release TUI."""
|
||||
|
||||
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
|
||||
|
||||
|
||||
def _build_mock_stdscr() -> MagicMock:
|
||||
stdscr = MagicMock()
|
||||
stdscr.getmaxyx.return_value = (40, 120)
|
||||
stdscr.getch.return_value = curses.KEY_F10
|
||||
return stdscr
|
||||
|
||||
|
||||
@patch("backend.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
|
||||
mock_curses_module.KEY_F5 = curses.KEY_F5
|
||||
mock_curses_module.color_pair.side_effect = lambda value: value
|
||||
mock_curses_module.A_BOLD = 0
|
||||
|
||||
stdscr = _build_mock_stdscr()
|
||||
app = CleanReleaseTUI(stdscr)
|
||||
|
||||
stdscr.getch.side_effect = [curses.KEY_F5, curses.KEY_F10]
|
||||
with patch.object(app, "run_checks", autospec=True) as run_checks_mock:
|
||||
app.loop()
|
||||
|
||||
run_checks_mock.assert_called_once_with()
|
||||
|
||||
|
||||
@patch("backend.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
|
||||
mock_curses_module.KEY_F5 = curses.KEY_F5
|
||||
mock_curses_module.color_pair.side_effect = lambda value: value
|
||||
mock_curses_module.A_BOLD = 0
|
||||
|
||||
stdscr = _build_mock_stdscr()
|
||||
app = CleanReleaseTUI(stdscr)
|
||||
stdscr.getch.side_effect = [curses.KEY_F5, curses.KEY_F10]
|
||||
|
||||
def _set_blocked_state() -> None:
|
||||
app.status = CheckFinalStatus.BLOCKED
|
||||
app.report_id = "CCR-smoke-blocked"
|
||||
app.violations_list = [object()]
|
||||
|
||||
with patch.object(app, "run_checks", side_effect=_set_blocked_state, autospec=True):
|
||||
app.loop()
|
||||
|
||||
assert app.status == CheckFinalStatus.BLOCKED
|
||||
assert app.report_id == "CCR-smoke-blocked"
|
||||
assert app.violations_list
|
||||
|
||||
|
||||
def test_tui_non_tty_refuses_startup(capsys) -> None:
|
||||
"""Non-TTY startup must refuse TUI mode and redirect operator to CLI/API flow."""
|
||||
with patch("sys.stdout.isatty", return_value=False):
|
||||
exit_code = main()
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert exit_code == 2
|
||||
assert "TTY is required for TUI mode" in captured.err
|
||||
assert "Use CLI/API workflow instead" in captured.err
|
||||
|
||||
|
||||
@patch("backend.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
|
||||
mock_curses_module.KEY_F8 = curses.KEY_F8
|
||||
mock_curses_module.color_pair.side_effect = lambda value: value
|
||||
mock_curses_module.A_BOLD = 0
|
||||
|
||||
stdscr = _build_mock_stdscr()
|
||||
app = CleanReleaseTUI(stdscr)
|
||||
stdscr.getch.side_effect = [curses.KEY_F8, curses.KEY_F10]
|
||||
|
||||
app.loop()
|
||||
|
||||
assert app.last_error is not None
|
||||
assert "F8 disabled" in app.last_error
|
||||
|
||||
|
||||
# [/DEF:test_clean_release_tui_v2:Module]
|
||||
Reference in New Issue
Block a user