File size: 5,632 Bytes
e25024e
 
 
 
 
 
 
 
 
 
 
45113e6
e25024e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
"""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 basic, advanced, and aggressive."""
        valid_methods = ["basic", "advanced", "aggressive"]
        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. informed is NOT a valid --method choice on the CLI
    def test_obliterate_rejects_informed_method(self):
        """The CLI --method flag does not accept 'informed' (separate pipeline)."""
        stderr_text = _capture_exit(
            ["obliterate", "fake/model", "--method", "informed"],
            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

    # 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"