|
1707c67…
|
noreply
|
1 |
"""Tests for CLI UX improvements — doctor, init wizard, and tab completion.""" |
|
1707c67…
|
noreply
|
2 |
|
|
1707c67…
|
noreply
|
3 |
import os |
|
1707c67…
|
noreply
|
4 |
from unittest.mock import MagicMock, patch |
|
1707c67…
|
noreply
|
5 |
|
|
1707c67…
|
noreply
|
6 |
from click.testing import CliRunner |
|
1707c67…
|
noreply
|
7 |
|
|
1707c67…
|
noreply
|
8 |
from video_processor.cli.commands import cli |
|
1707c67…
|
noreply
|
9 |
from video_processor.cli.companion import CompanionREPL |
|
1707c67…
|
noreply
|
10 |
from video_processor.cli.doctor import ( |
|
1707c67…
|
noreply
|
11 |
check_api_keys, |
|
1707c67…
|
noreply
|
12 |
check_dotenv, |
|
1707c67…
|
noreply
|
13 |
check_ffmpeg, |
|
1707c67…
|
noreply
|
14 |
check_optional_deps, |
|
1707c67…
|
noreply
|
15 |
check_python_version, |
|
1707c67…
|
noreply
|
16 |
format_results, |
|
1707c67…
|
noreply
|
17 |
run_all_checks, |
|
1707c67…
|
noreply
|
18 |
) |
|
1707c67…
|
noreply
|
19 |
|
|
1707c67…
|
noreply
|
20 |
|
|
1707c67…
|
noreply
|
21 |
class TestDoctor: |
|
1707c67…
|
noreply
|
22 |
def test_check_python_version(self): |
|
1707c67…
|
noreply
|
23 |
name, status, detail = check_python_version() |
|
1707c67…
|
noreply
|
24 |
assert name == "Python" |
|
1707c67…
|
noreply
|
25 |
assert status == "ok" |
|
1707c67…
|
noreply
|
26 |
|
|
1707c67…
|
noreply
|
27 |
def test_check_ffmpeg_found(self): |
|
1707c67…
|
noreply
|
28 |
with patch("video_processor.cli.doctor.shutil") as mock_shutil: |
|
1707c67…
|
noreply
|
29 |
mock_shutil.which.return_value = "/usr/bin/ffmpeg" |
|
1707c67…
|
noreply
|
30 |
name, status, detail = check_ffmpeg() |
|
1707c67…
|
noreply
|
31 |
assert status == "ok" |
|
1707c67…
|
noreply
|
32 |
|
|
1707c67…
|
noreply
|
33 |
def test_check_ffmpeg_missing(self): |
|
1707c67…
|
noreply
|
34 |
with patch("video_processor.cli.doctor.shutil") as mock_shutil: |
|
1707c67…
|
noreply
|
35 |
mock_shutil.which.return_value = None |
|
1707c67…
|
noreply
|
36 |
name, status, detail = check_ffmpeg() |
|
1707c67…
|
noreply
|
37 |
assert status == "missing" |
|
1707c67…
|
noreply
|
38 |
|
|
1707c67…
|
noreply
|
39 |
def test_check_api_keys_with_key(self): |
|
1707c67…
|
noreply
|
40 |
with patch.dict(os.environ, {"OPENAI_API_KEY": "sk-test1234567890"}): |
|
1707c67…
|
noreply
|
41 |
results = check_api_keys() |
|
1707c67…
|
noreply
|
42 |
openai = [r for r in results if r[0].strip() == "OpenAI"] |
|
1707c67…
|
noreply
|
43 |
assert len(openai) == 1 |
|
1707c67…
|
noreply
|
44 |
assert openai[0][1] == "ok" |
|
1707c67…
|
noreply
|
45 |
assert "sk-t" in openai[0][2] |
|
1707c67…
|
noreply
|
46 |
|
|
1707c67…
|
noreply
|
47 |
def test_check_api_keys_without_key(self): |
|
1707c67…
|
noreply
|
48 |
with patch.dict(os.environ, {}, clear=True): |
|
1707c67…
|
noreply
|
49 |
results = check_api_keys() |
|
1707c67…
|
noreply
|
50 |
openai = [r for r in results if "OpenAI" in r[0]] |
|
1707c67…
|
noreply
|
51 |
assert openai[0][1] == "not set" |
|
1707c67…
|
noreply
|
52 |
|
|
1707c67…
|
noreply
|
53 |
def test_check_dotenv_exists(self, tmp_path, monkeypatch): |
|
1707c67…
|
noreply
|
54 |
monkeypatch.chdir(tmp_path) |
|
1707c67…
|
noreply
|
55 |
(tmp_path / ".env").write_text("KEY=val\n") |
|
1707c67…
|
noreply
|
56 |
name, status, detail = check_dotenv() |
|
1707c67…
|
noreply
|
57 |
assert status == "ok" |
|
1707c67…
|
noreply
|
58 |
|
|
1707c67…
|
noreply
|
59 |
def test_check_dotenv_missing(self, tmp_path, monkeypatch): |
|
1707c67…
|
noreply
|
60 |
monkeypatch.chdir(tmp_path) |
|
1707c67…
|
noreply
|
61 |
name, status, detail = check_dotenv() |
|
1707c67…
|
noreply
|
62 |
assert status == "not found" |
|
1707c67…
|
noreply
|
63 |
|
|
1707c67…
|
noreply
|
64 |
def test_check_optional_deps(self): |
|
1707c67…
|
noreply
|
65 |
results = check_optional_deps() |
|
1707c67…
|
noreply
|
66 |
assert len(results) > 0 |
|
1707c67…
|
noreply
|
67 |
# All results should have 3 elements |
|
1707c67…
|
noreply
|
68 |
for name, status, detail in results: |
|
1707c67…
|
noreply
|
69 |
assert status in ("ok", "not installed") |
|
1707c67…
|
noreply
|
70 |
|
|
1707c67…
|
noreply
|
71 |
def test_format_results(self): |
|
1707c67…
|
noreply
|
72 |
results = [ |
|
1707c67…
|
noreply
|
73 |
("Python", "ok", "3.12.0"), |
|
1707c67…
|
noreply
|
74 |
("FFmpeg", "missing", "Install it"), |
|
1707c67…
|
noreply
|
75 |
] |
|
1707c67…
|
noreply
|
76 |
output = format_results(results) |
|
1707c67…
|
noreply
|
77 |
assert "PlanOpticon Doctor" in output |
|
1707c67…
|
noreply
|
78 |
assert "[ok]" in output |
|
1707c67…
|
noreply
|
79 |
assert "[XX]" in output |
|
1707c67…
|
noreply
|
80 |
|
|
1707c67…
|
noreply
|
81 |
def test_run_all_checks(self): |
|
1707c67…
|
noreply
|
82 |
with patch( |
|
1707c67…
|
noreply
|
83 |
"video_processor.integrators.graph_discovery.find_nearest_graph", |
|
1707c67…
|
noreply
|
84 |
return_value=None, |
|
1707c67…
|
noreply
|
85 |
): |
|
1707c67…
|
noreply
|
86 |
results = run_all_checks() |
|
1707c67…
|
noreply
|
87 |
assert len(results) > 5 |
|
1707c67…
|
noreply
|
88 |
# Should have section headers |
|
1707c67…
|
noreply
|
89 |
sections = [r for r in results if r[1] == "section"] |
|
1707c67…
|
noreply
|
90 |
assert len(sections) >= 2 |
|
1707c67…
|
noreply
|
91 |
|
|
1707c67…
|
noreply
|
92 |
def test_doctor_cli_command(self): |
|
1707c67…
|
noreply
|
93 |
runner = CliRunner() |
|
1707c67…
|
noreply
|
94 |
with patch( |
|
1707c67…
|
noreply
|
95 |
"video_processor.integrators.graph_discovery.find_nearest_graph", |
|
1707c67…
|
noreply
|
96 |
return_value=None, |
|
1707c67…
|
noreply
|
97 |
): |
|
1707c67…
|
noreply
|
98 |
result = runner.invoke(cli, ["doctor"]) |
|
1707c67…
|
noreply
|
99 |
assert result.exit_code == 0 |
|
1707c67…
|
noreply
|
100 |
assert "PlanOpticon Doctor" in result.output |
|
1707c67…
|
noreply
|
101 |
|
|
1707c67…
|
noreply
|
102 |
|
|
1707c67…
|
noreply
|
103 |
class TestInitWizard: |
|
1707c67…
|
noreply
|
104 |
def test_init_cli_command_exists(self): |
|
1707c67…
|
noreply
|
105 |
runner = CliRunner() |
|
1707c67…
|
noreply
|
106 |
result = runner.invoke(cli, ["init", "--help"]) |
|
1707c67…
|
noreply
|
107 |
assert result.exit_code == 0 |
|
1707c67…
|
noreply
|
108 |
assert "setup wizard" in result.output.lower() or "wizard" in result.output.lower() |
|
1707c67…
|
noreply
|
109 |
|
|
1707c67…
|
noreply
|
110 |
def test_wizard_provider_selection(self): |
|
1707c67…
|
noreply
|
111 |
"""Test the wizard runs with simulated input.""" |
|
1707c67…
|
noreply
|
112 |
runner = CliRunner() |
|
1707c67…
|
noreply
|
113 |
# Select provider 1 (OpenAI), enter a key, decline additional providers |
|
1707c67…
|
noreply
|
114 |
result = runner.invoke( |
|
1707c67…
|
noreply
|
115 |
cli, |
|
1707c67…
|
noreply
|
116 |
["init"], |
|
1707c67…
|
noreply
|
117 |
input="1\nsk-test-key-1234567890\nn\n", |
|
1707c67…
|
noreply
|
118 |
) |
|
1707c67…
|
noreply
|
119 |
assert result.exit_code == 0 |
|
1707c67…
|
noreply
|
120 |
assert "Setup complete" in result.output |
|
1707c67…
|
noreply
|
121 |
|
|
1707c67…
|
noreply
|
122 |
def test_wizard_ollama_provider(self): |
|
1707c67…
|
noreply
|
123 |
"""Test selecting Ollama (no API key needed).""" |
|
1707c67…
|
noreply
|
124 |
runner = CliRunner() |
|
1707c67…
|
noreply
|
125 |
with patch( |
|
1707c67…
|
noreply
|
126 |
"video_processor.cli.init_wizard.shutil.which", |
|
1707c67…
|
noreply
|
127 |
return_value="/usr/local/bin/ollama", |
|
1707c67…
|
noreply
|
128 |
): |
|
1707c67…
|
noreply
|
129 |
with patch("subprocess.run") as mock_run: |
|
1707c67…
|
noreply
|
130 |
mock_run.return_value = MagicMock(returncode=0, stdout="NAME\nllama3\n") |
|
1707c67…
|
noreply
|
131 |
result = runner.invoke( |
|
1707c67…
|
noreply
|
132 |
cli, |
|
1707c67…
|
noreply
|
133 |
["init"], |
|
1707c67…
|
noreply
|
134 |
input="4\nn\n", |
|
1707c67…
|
noreply
|
135 |
) |
|
1707c67…
|
noreply
|
136 |
assert result.exit_code == 0 |
|
1707c67…
|
noreply
|
137 |
assert "Setup complete" in result.output |
|
1707c67…
|
noreply
|
138 |
|
|
1707c67…
|
noreply
|
139 |
|
|
1707c67…
|
noreply
|
140 |
class TestCompanionTabCompletion: |
|
1707c67…
|
noreply
|
141 |
def test_commands_list_exists(self): |
|
1707c67…
|
noreply
|
142 |
assert len(CompanionREPL.COMMANDS) > 10 |
|
1707c67…
|
noreply
|
143 |
assert "/help" in CompanionREPL.COMMANDS |
|
1707c67…
|
noreply
|
144 |
assert "/quit" in CompanionREPL.COMMANDS |
|
1707c67…
|
noreply
|
145 |
|
|
1707c67…
|
noreply
|
146 |
def test_setup_readline_no_crash(self): |
|
1707c67…
|
noreply
|
147 |
"""Readline setup should not crash even if readline is unavailable.""" |
|
1707c67…
|
noreply
|
148 |
repl = CompanionREPL() |
|
1707c67…
|
noreply
|
149 |
# Just ensure it doesn't raise |
|
1707c67…
|
noreply
|
150 |
repl._setup_readline() |
|
1707c67…
|
noreply
|
151 |
|
|
1707c67…
|
noreply
|
152 |
def test_all_commands_in_dispatch(self): |
|
1707c67…
|
noreply
|
153 |
"""Every command in COMMANDS should be handled by handle_input.""" |
|
1707c67…
|
noreply
|
154 |
repl = CompanionREPL() |
|
1707c67…
|
noreply
|
155 |
for cmd in CompanionREPL.COMMANDS: |
|
1707c67…
|
noreply
|
156 |
if cmd in ("/quit", "/exit"): |
|
1707c67…
|
noreply
|
157 |
result = repl.handle_input(cmd) |
|
1707c67…
|
noreply
|
158 |
assert result == "__QUIT__" |
|
1707c67…
|
noreply
|
159 |
else: |
|
1707c67…
|
noreply
|
160 |
result = repl.handle_input(cmd) |
|
1707c67…
|
noreply
|
161 |
assert "Unknown command" not in result, f"{cmd} not handled" |