mirror of
https://github.com/elder-plinius/OBLITERATUS.git
synced 2026-08-17 16:37:30 +02:00
Cover legacy console fallbacks, platform temp paths, local UI disk probing, and lazy analysis exports. Bind the new local UI test to the executable source-to-test risk map.
200 lines
8.0 KiB
Python
200 lines
8.0 KiB
Python
"""CLI dispatch tests for obliteratus.cli.main().
|
|
|
|
These tests verify argument parsing and subcommand routing without
|
|
downloading real models or running any pipeline. They use
|
|
``unittest.mock.patch`` to capture stdout/stderr and
|
|
``pytest.raises(SystemExit)`` for argparse exits.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from io import StringIO
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from obliteratus.cli import main
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _capture_exit(argv: list[str] | None, *, expect_code: int | None = None):
|
|
"""Call main(argv), expecting SystemExit; return captured stderr text."""
|
|
buf = StringIO()
|
|
with pytest.raises(SystemExit) as exc_info, patch("sys.stderr", buf):
|
|
main(argv)
|
|
if expect_code is not None:
|
|
assert exc_info.value.code == expect_code
|
|
return buf.getvalue()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestCLIDispatch:
|
|
"""Test suite for CLI argument parsing and subcommand dispatch."""
|
|
|
|
# 1. No args -> prints help / exits with error
|
|
def test_main_no_args_prints_help(self):
|
|
"""Calling main() with no args should exit (subcommand is required)."""
|
|
stderr_text = _capture_exit([], expect_code=2)
|
|
# argparse prints usage info to stderr on error
|
|
assert "usage" in stderr_text.lower() or "required" in stderr_text.lower()
|
|
|
|
# 2. models command lists models without error
|
|
def test_models_command(self):
|
|
"""Calling main(['models']) should list models without raising."""
|
|
with patch("obliteratus.cli.console") as mock_console:
|
|
main(["models"])
|
|
# console.print is called at least once to render the table
|
|
assert mock_console.print.call_count >= 1
|
|
|
|
# 3. obliterate without model arg -> error
|
|
def test_obliterate_requires_model(self):
|
|
"""Calling main(['obliterate']) without a model arg should error."""
|
|
stderr_text = _capture_exit(["obliterate"], expect_code=2)
|
|
assert "model" in stderr_text.lower() or "required" in stderr_text.lower()
|
|
|
|
# 4. obliterate --method accepts valid methods
|
|
def test_obliterate_valid_methods(self):
|
|
"""Test that --method accepts all 9 pipeline methods."""
|
|
valid_methods = [
|
|
"basic", "advanced", "aggressive", "spectral_cascade",
|
|
"informed", "surgical", "optimized", "inverted", "nuclear",
|
|
]
|
|
for method in valid_methods:
|
|
# Patch the actual pipeline execution so nothing runs
|
|
with patch("obliteratus.cli._cmd_abliterate") as mock_cmd:
|
|
main(["obliterate", "fake/model", "--method", method])
|
|
mock_cmd.assert_called_once()
|
|
args_passed = mock_cmd.call_args[0][0]
|
|
assert args_passed.method == method
|
|
|
|
# 4b. invalid methods are rejected
|
|
def test_obliterate_rejects_invalid_method(self):
|
|
"""The CLI --method flag rejects unknown method names."""
|
|
stderr_text = _capture_exit(
|
|
["obliterate", "fake/model", "--method", "nonexistent"],
|
|
expect_code=2,
|
|
)
|
|
assert "invalid choice" in stderr_text.lower()
|
|
|
|
# 5. run requires config path
|
|
def test_run_requires_config(self):
|
|
"""Calling main(['run']) without a config path should error."""
|
|
stderr_text = _capture_exit(["run"], expect_code=2)
|
|
assert "config" in stderr_text.lower() or "required" in stderr_text.lower()
|
|
|
|
# 6. aggregate with nonexistent dir handles gracefully
|
|
def test_aggregate_command_missing_dir(self):
|
|
"""Calling main(['aggregate']) with nonexistent dir should handle gracefully."""
|
|
with patch("obliteratus.cli.console") as mock_console:
|
|
main(["aggregate", "--dir", "/nonexistent/path/to/nowhere"])
|
|
# The command prints a message about no contributions found and returns
|
|
printed_text = " ".join(
|
|
str(call) for call in mock_console.print.call_args_list
|
|
)
|
|
assert "no contributions found" in printed_text.lower() or mock_console.print.called
|
|
|
|
def test_aggregate_accepts_format_metric_min_runs(self):
|
|
"""aggregate accepts --format, --metric and --min-runs flags."""
|
|
with patch("obliteratus.cli._cmd_aggregate") as mock_cmd:
|
|
main([
|
|
"aggregate",
|
|
"--format", "latex",
|
|
"--metric", "refusal_rate",
|
|
"--min-runs", "3",
|
|
])
|
|
mock_cmd.assert_called_once()
|
|
args_passed = mock_cmd.call_args[0][0]
|
|
assert args_passed.format == "latex"
|
|
assert args_passed.metric == "refusal_rate"
|
|
assert args_passed.min_runs == 3
|
|
|
|
def test_aggregate_rejects_invalid_format(self):
|
|
"""aggregate rejects unknown --format choices."""
|
|
stderr_text = _capture_exit(
|
|
["aggregate", "--format", "invalid"],
|
|
expect_code=2,
|
|
)
|
|
assert "invalid choice" in stderr_text.lower()
|
|
|
|
# 7. --help flag prints help
|
|
def test_help_flag(self):
|
|
"""Calling main(['--help']) should print help and exit 0."""
|
|
buf = StringIO()
|
|
with pytest.raises(SystemExit) as exc_info, patch("sys.stdout", buf):
|
|
main(["--help"])
|
|
assert exc_info.value.code == 0
|
|
output = buf.getvalue()
|
|
assert "obliteratus" in output.lower() or "usage" in output.lower()
|
|
|
|
# 8. interactive subcommand is registered
|
|
def test_interactive_command_exists(self):
|
|
"""Verify 'interactive' subcommand is registered and dispatches."""
|
|
with patch("obliteratus.cli._cmd_interactive") as mock_cmd:
|
|
main(["interactive"])
|
|
mock_cmd.assert_called_once()
|
|
|
|
# 9. --contribute and --contribute-notes are accepted on obliterate
|
|
def test_contribute_flags_on_obliterate(self):
|
|
"""Verify --contribute and --contribute-notes are accepted args."""
|
|
with patch("obliteratus.cli._cmd_abliterate") as mock_cmd:
|
|
main([
|
|
"obliterate", "fake/model",
|
|
"--contribute",
|
|
"--contribute-notes", "Testing contribution system",
|
|
])
|
|
mock_cmd.assert_called_once()
|
|
args_passed = mock_cmd.call_args[0][0]
|
|
assert args_passed.contribute is True
|
|
assert args_passed.contribute_notes == "Testing contribution system"
|
|
|
|
|
|
class _EncodingOnlyStdout:
|
|
"""Minimal stream stand-in for encoding-selection tests."""
|
|
|
|
def __init__(self, encoding: str | None) -> None:
|
|
self.encoding = encoding
|
|
|
|
|
|
class TestConsoleEncoding:
|
|
"""The CLI must remain renderable on legacy Windows code pages."""
|
|
|
|
@pytest.mark.parametrize("encoding", ["cp1252", "ascii", "not-a-codec"])
|
|
def test_console_text_falls_back_when_text_is_not_encodable(self, encoding):
|
|
from obliteratus.cli import _console_text
|
|
|
|
with patch("sys.stdout", _EncodingOnlyStdout(encoding)):
|
|
assert _console_text("█→", "FALLBACK") == "FALLBACK"
|
|
|
|
@pytest.mark.parametrize("encoding", ["utf-8", None])
|
|
def test_console_text_keeps_unicode_on_utf8_compatible_streams(self, encoding):
|
|
from obliteratus.cli import _console_text
|
|
|
|
with patch("sys.stdout", _EncodingOnlyStdout(encoding)):
|
|
assert _console_text("█→", "FALLBACK") == "█→"
|
|
|
|
def test_banner_degrades_to_ascii_on_cp1252(self):
|
|
from obliteratus.cli import _banner_for_console
|
|
|
|
with patch("sys.stdout", _EncodingOnlyStdout("cp1252")):
|
|
banner = _banner_for_console()
|
|
|
|
assert "OBLITERATUS" in banner
|
|
assert banner.isascii()
|
|
|
|
def test_main_renders_selected_banner(self):
|
|
with (
|
|
patch("sys.stdout", _EncodingOnlyStdout("cp1252")),
|
|
patch("obliteratus.cli.console") as mock_console,
|
|
):
|
|
main(["models"])
|
|
|
|
assert mock_console.print.call_args_list[0].args[0].isascii()
|