Skip to content

Commit 3144d0c

Browse files
authored
fix(init): flash init show usage menu when called without arguments (#235)
* fix(init): show usage menu when flash init called without arguments Previously `flash init` with no arguments silently initialized in the current directory (same as `flash init .`). Now it shows a usage panel with examples and exits, matching expected CLI behavior: - `flash init` prints usage menu - `flash init .` initializes in current directory - `flash init <name>` creates new project folder * fix(init): use Typer built-in help instead of hand-crafted usage panel Replace hand-crafted usage text with ctx.get_help() to prevent drift when CLI options change. Update argument help text to clarify that '.' must be explicitly passed for current-directory init. * fix(test): remove stray commit message from test docstring Syntax error from commit message text accidentally appended to a docstring in test_init.py, breaking ruff format checks. * fix(init): use plain help text instead of empty Panel wrapper KAJdev review feedback: Panel(ctx.get_help()) renders an empty panel. Use console.print(ctx.get_help()) directly instead. * fix(init): disable Rich markup parsing on help text output Typer help strings contain [OPTIONS] which Rich interprets as markup tags, causing MarkupError or mangled output. Pass markup=False and highlight=False to console.print. Test updated to verify this. * fix(test): add CliRunner integration test and assert ctx.get_help() call Address Henrik review feedback: - Add CLI-level test via CliRunner to verify Typer context injection - Assert ctx.get_help() was called in no-args test
1 parent 5464ce0 commit 3144d0c

2 files changed

Lines changed: 112 additions & 41 deletions

File tree

src/runpod_flash/cli/commands/init.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,21 @@
1414

1515

1616
def init_command(
17+
ctx: typer.Context,
1718
project_name: Optional[str] = typer.Argument(
18-
None, help="Project name or '.' for current directory"
19+
None, help="Project name, or '.' to initialize in current directory"
1920
),
2021
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files"),
2122
):
2223
"""Create new Flash project with Flash Server and GPU workers."""
2324

25+
# No argument provided — show usage and exit
26+
if project_name is None:
27+
console.print(ctx.get_help(), markup=False, highlight=False)
28+
raise typer.Exit(0)
29+
2430
# Determine target directory and initialization mode
25-
if project_name is None or project_name == ".":
31+
if project_name == ".":
2632
# Initialize in current directory
2733
project_dir = Path.cwd()
2834
is_current_dir = True

tests/unit/cli/commands/test_init.py

Lines changed: 104 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,19 @@
33
from unittest.mock import MagicMock, Mock, patch
44

55
import pytest
6+
import typer
67

78
from runpod_flash.cli.commands.init import init_command
89

910

11+
@pytest.fixture
12+
def mock_typer_ctx():
13+
"""Create a mock typer.Context for direct init_command calls."""
14+
ctx = MagicMock(spec=typer.Context)
15+
ctx.get_help.return_value = "Usage: flash init [OPTIONS] [PROJECT_NAME]"
16+
return ctx
17+
18+
1019
@pytest.fixture
1120
def mock_context(monkeypatch):
1221
"""Set up mocks for init command testing."""
@@ -44,11 +53,13 @@ def mock_context(monkeypatch):
4453
class TestInitCommandNewDirectory:
4554
"""Tests for init command when creating a new directory."""
4655

47-
def test_create_new_directory(self, mock_context, tmp_path, monkeypatch):
56+
def test_create_new_directory(
57+
self, mock_typer_ctx, mock_context, tmp_path, monkeypatch
58+
):
4859
"""Test creating new project directory."""
4960
monkeypatch.chdir(tmp_path)
5061

51-
init_command("my_project")
62+
init_command(mock_typer_ctx, "my_project")
5263

5364
# Verify directory was created
5465
assert (tmp_path / "my_project").exists()
@@ -59,45 +70,87 @@ def test_create_new_directory(self, mock_context, tmp_path, monkeypatch):
5970
# Verify console output
6071
mock_context["console"].print.assert_called()
6172

62-
def test_create_nested_directory(self, mock_context, tmp_path, monkeypatch):
73+
def test_create_nested_directory(
74+
self, mock_typer_ctx, mock_context, tmp_path, monkeypatch
75+
):
6376
"""Test creating project in nested directory structure."""
6477
monkeypatch.chdir(tmp_path)
6578

66-
init_command("path/to/my_project")
79+
init_command(mock_typer_ctx, "path/to/my_project")
6780

6881
# Verify nested directory was created
6982
assert (tmp_path / "path/to/my_project").exists()
7083

71-
def test_force_flag_skips_confirmation(self, mock_context, tmp_path, monkeypatch):
84+
def test_force_flag_skips_confirmation(
85+
self, mock_typer_ctx, mock_context, tmp_path, monkeypatch
86+
):
7287
"""Test that force flag bypasses conflict prompts."""
7388
monkeypatch.chdir(tmp_path)
7489
mock_context["detect_conflicts"].return_value = ["main.py", "requirements.txt"]
7590

76-
init_command("my_project", force=True)
91+
init_command(mock_typer_ctx, "my_project", force=True)
7792

7893
# Verify skeleton was created
7994
mock_context["create_skeleton"].assert_called_once()
8095

8196

82-
class TestInitCommandCurrentDirectory:
83-
"""Tests for init command when using current directory."""
97+
class TestInitCommandNoArgs:
98+
"""Tests for init command when called with no arguments."""
8499

85-
@patch("pathlib.Path.cwd")
86-
def test_init_current_directory_with_none(self, mock_cwd, mock_context, tmp_path):
87-
"""Test initialization in current directory with None argument."""
88-
mock_cwd.return_value = tmp_path
100+
def test_no_args_shows_help_and_exits(self, mock_typer_ctx, mock_context):
101+
"""flash init with no args should show help and exit."""
102+
with pytest.raises(typer.Exit) as exc_info:
103+
init_command(mock_typer_ctx, None)
89104

90-
init_command(None)
105+
assert exc_info.value.exit_code == 0
106+
107+
def test_no_args_does_not_create_skeleton(self, mock_typer_ctx, mock_context):
108+
"""flash init with no args should not create project skeleton."""
109+
with pytest.raises(typer.Exit):
110+
init_command(mock_typer_ctx, None)
111+
112+
mock_context["create_skeleton"].assert_not_called()
113+
114+
def test_no_args_prints_usage_info(self, mock_typer_ctx, mock_context):
115+
"""flash init with no args should print usage information."""
116+
with pytest.raises(typer.Exit):
117+
init_command(mock_typer_ctx, None)
118+
119+
# Verify ctx.get_help() was called and passed to console.print
120+
mock_typer_ctx.get_help.assert_called_once()
121+
mock_context["console"].print.assert_called_once()
122+
help_arg = mock_context["console"].print.call_args[0][0]
123+
assert "flash init" in help_arg
124+
kwargs = mock_context["console"].print.call_args[1]
125+
assert kwargs.get("markup") is False
126+
assert kwargs.get("highlight") is False
91127

92-
# Verify skeleton was created
93-
mock_context["create_skeleton"].assert_called_once()
128+
129+
class TestInitCommandCliRunner:
130+
"""CLI-level test to verify Typer context injection works end-to-end."""
131+
132+
def test_no_args_via_cli(self):
133+
"""flash init with no args should show help and exit 0 via CLI."""
134+
from typer.testing import CliRunner
135+
136+
from runpod_flash.cli.main import app
137+
138+
result = CliRunner().invoke(app, ["init"])
139+
assert result.exit_code == 0
140+
assert "flash init" in result.output.lower()
141+
142+
143+
class TestInitCommandCurrentDirectory:
144+
"""Tests for init command when using current directory."""
94145

95146
@patch("pathlib.Path.cwd")
96-
def test_init_current_directory_with_dot(self, mock_cwd, mock_context, tmp_path):
147+
def test_init_current_directory_with_dot(
148+
self, mock_cwd, mock_typer_ctx, mock_context, tmp_path
149+
):
97150
"""Test initialization in current directory with '.' argument."""
98151
mock_cwd.return_value = tmp_path
99152

100-
init_command(".")
153+
init_command(mock_typer_ctx, ".")
101154

102155
# Verify skeleton was created
103156
mock_context["create_skeleton"].assert_called_once()
@@ -106,21 +159,25 @@ def test_init_current_directory_with_dot(self, mock_cwd, mock_context, tmp_path)
106159
class TestInitCommandConflictDetection:
107160
"""Tests for init command file conflict detection and resolution."""
108161

109-
def test_no_conflicts_no_prompt(self, mock_context, tmp_path, monkeypatch):
162+
def test_no_conflicts_no_prompt(
163+
self, mock_typer_ctx, mock_context, tmp_path, monkeypatch
164+
):
110165
"""Test that prompt is skipped when no conflicts exist."""
111166
monkeypatch.chdir(tmp_path)
112167
mock_context["detect_conflicts"].return_value = []
113168

114-
init_command("my_project")
169+
init_command(mock_typer_ctx, "my_project")
115170

116171
# Verify skeleton was created
117172
mock_context["create_skeleton"].assert_called_once()
118173

119-
def test_console_called_multiple_times(self, mock_context, tmp_path, monkeypatch):
174+
def test_console_called_multiple_times(
175+
self, mock_typer_ctx, mock_context, tmp_path, monkeypatch
176+
):
120177
"""Test that console prints multiple outputs."""
121178
monkeypatch.chdir(tmp_path)
122179

123-
init_command("my_project")
180+
init_command(mock_typer_ctx, "my_project")
124181

125182
# Verify console.print was called multiple times
126183
assert mock_context["console"].print.call_count > 0
@@ -129,42 +186,50 @@ def test_console_called_multiple_times(self, mock_context, tmp_path, monkeypatch
129186
class TestInitCommandOutput:
130187
"""Tests for init command output messages."""
131188

132-
def test_panel_title_for_new_directory(self, mock_context, tmp_path, monkeypatch):
189+
def test_panel_title_for_new_directory(
190+
self, mock_typer_ctx, mock_context, tmp_path, monkeypatch
191+
):
133192
"""Test that panel output is created for new directory."""
134193
monkeypatch.chdir(tmp_path)
135194

136-
init_command("my_project")
195+
init_command(mock_typer_ctx, "my_project")
137196

138197
# Verify console.print was called multiple times
139198
assert mock_context["console"].print.call_count > 0
140199

141200
@patch("pathlib.Path.cwd")
142-
def test_panel_title_for_current_directory(self, mock_cwd, mock_context, tmp_path):
201+
def test_panel_title_for_current_directory(
202+
self, mock_cwd, mock_typer_ctx, mock_context, tmp_path
203+
):
143204
"""Test that panel output is created for current directory."""
144205
mock_cwd.return_value = tmp_path
145206

146-
init_command(".")
207+
init_command(mock_typer_ctx, ".")
147208

148209
# Verify console.print was called
149210
assert mock_context["console"].print.call_count > 0
150211

151-
def test_next_steps_displayed(self, mock_context, tmp_path, monkeypatch):
212+
def test_next_steps_displayed(
213+
self, mock_typer_ctx, mock_context, tmp_path, monkeypatch
214+
):
152215
"""Test next steps are displayed."""
153216
monkeypatch.chdir(tmp_path)
154217

155-
init_command("my_project")
218+
init_command(mock_typer_ctx, "my_project")
156219

157220
# Verify console.print was called with next steps text
158221
assert any(
159222
"Next steps" in str(c) for c in mock_context["console"].print.call_args_list
160223
)
161224

162225
@patch("pathlib.Path.cwd")
163-
def test_flash_login_step_displayed(self, mock_cwd, mock_context, tmp_path):
226+
def test_flash_login_step_displayed(
227+
self, mock_cwd, mock_typer_ctx, mock_context, tmp_path
228+
):
164229
"""Test flash login is shown in the next steps table."""
165230
mock_cwd.return_value = tmp_path
166231

167-
init_command(".")
232+
init_command(mock_typer_ctx, ".")
168233

169234
# The steps table is a Rich Table passed to console.print.
170235
# Render it to plain text and check for "flash login".
@@ -186,12 +251,12 @@ def test_flash_login_step_displayed(self, mock_cwd, mock_context, tmp_path):
186251
assert "flash login" in buf.getvalue()
187252

188253
def test_status_message_for_new_directory(
189-
self, mock_context, tmp_path, monkeypatch
254+
self, mock_typer_ctx, mock_context, tmp_path, monkeypatch
190255
):
191256
"""Test status message while creating new directory."""
192257
monkeypatch.chdir(tmp_path)
193258

194-
init_command("my_project")
259+
init_command(mock_typer_ctx, "my_project")
195260

196261
# Check that status was called with appropriate message
197262
mock_context["console"].status.assert_called_once()
@@ -200,12 +265,12 @@ def test_status_message_for_new_directory(
200265

201266
@patch("pathlib.Path.cwd")
202267
def test_status_message_for_current_directory(
203-
self, mock_cwd, mock_context, tmp_path
268+
self, mock_cwd, mock_typer_ctx, mock_context, tmp_path
204269
):
205270
"""Test status message while initializing current directory."""
206271
mock_cwd.return_value = tmp_path
207272

208-
init_command(".")
273+
init_command(mock_typer_ctx, ".")
209274

210275
# Check that status was called with initialization message
211276
mock_context["console"].status.assert_called_once()
@@ -217,36 +282,36 @@ class TestInitCommandProjectNameHandling:
217282
"""Tests for project name handling."""
218283

219284
def test_special_characters_in_project_name(
220-
self, mock_context, tmp_path, monkeypatch
285+
self, mock_typer_ctx, mock_context, tmp_path, monkeypatch
221286
):
222287
"""Test project name with special characters."""
223288
monkeypatch.chdir(tmp_path)
224289

225-
init_command("my-project_123")
290+
init_command(mock_typer_ctx, "my-project_123")
226291

227292
# Verify directory was created with the exact name
228293
assert (tmp_path / "my-project_123").exists()
229294

230295
def test_console_called_with_panels_and_tables(
231-
self, mock_context, tmp_path, monkeypatch
296+
self, mock_typer_ctx, mock_context, tmp_path, monkeypatch
232297
):
233298
"""Test that console prints panels and tables."""
234299
monkeypatch.chdir(tmp_path)
235300

236-
init_command("test_project")
301+
init_command(mock_typer_ctx, "test_project")
237302

238303
# Verify console.print was called multiple times
239304
assert (
240305
mock_context["console"].print.call_count >= 4
241306
) # Panel, "Next steps:", Table, API key info
242307

243308
def test_directory_created_matches_argument(
244-
self, mock_context, tmp_path, monkeypatch
309+
self, mock_typer_ctx, mock_context, tmp_path, monkeypatch
245310
):
246311
"""Test that directory created matches the argument."""
247312
monkeypatch.chdir(tmp_path)
248313

249-
init_command("my_awesome_project")
314+
init_command(mock_typer_ctx, "my_awesome_project")
250315

251316
# Verify directory was created with exact name
252317
assert (tmp_path / "my_awesome_project").exists()

0 commit comments

Comments
 (0)