99 pytest tests/test_cli_args.py -v
1010"""
1111
12+ import ast
1213import sys
1314import os
1415import importlib
2021REPO_ROOT = os .path .dirname (os .path .dirname (os .path .abspath (__file__ )))
2122sys .path .insert (0 , REPO_ROOT )
2223
24+ from app import build_cli_parser
2325from scripts .export import build_parser
2426
2527
28+ def _is_app_run_call (node : ast .AST ) -> bool :
29+ if not isinstance (node , ast .Call ):
30+ return False
31+ func = node .func
32+ return (
33+ isinstance (func , ast .Attribute )
34+ and func .attr == "run"
35+ and isinstance (func .value , ast .Name )
36+ and func .value .id == "app"
37+ )
38+
39+
40+ def _call_passes_hardcoded_debug_true (call : ast .Call ) -> bool :
41+ """True if this Call passes literal True for debug (kwarg or positional)."""
42+ for kw in call .keywords :
43+ if kw .arg == "debug" and isinstance (kw .value , ast .Constant ) and kw .value .value is True :
44+ return True
45+ for arg in call .args :
46+ if isinstance (arg , ast .Constant ) and arg .value is True :
47+ return True
48+ return False
49+
50+
51+ def _debug_kwarg_uses_args (call : ast .Call ) -> bool :
52+ for kw in call .keywords :
53+ if kw .arg != "debug" :
54+ continue
55+ val = kw .value
56+ return (
57+ isinstance (val , ast .Attribute )
58+ and isinstance (val .value , ast .Name )
59+ and val .value .id == "args"
60+ and val .attr == "debug"
61+ )
62+ return False
63+
64+
65+ def _is_args_debug (node : ast .AST ) -> bool :
66+ return (
67+ isinstance (node , ast .Attribute )
68+ and isinstance (node .value , ast .Name )
69+ and node .value .id == "args"
70+ and node .attr == "debug"
71+ )
72+
73+
74+ def _is_sys_platform_ne_win32 (node : ast .AST ) -> bool :
75+ if not isinstance (node , ast .Compare ) or len (node .ops ) != 1 or len (node .comparators ) != 1 :
76+ return False
77+ if not isinstance (node .ops [0 ], ast .NotEq ):
78+ return False
79+ left = node .left
80+ if not (
81+ isinstance (left , ast .Attribute )
82+ and isinstance (left .value , ast .Name )
83+ and left .value .id == "sys"
84+ and left .attr == "platform"
85+ ):
86+ return False
87+ right = node .comparators [0 ]
88+ if isinstance (right , ast .Constant ):
89+ return right .value == "win32"
90+ return isinstance (right , ast .Str ) and right .s == "win32" # py<3.8
91+
92+
93+ def _is_debug_and_platform_guard (node : ast .AST ) -> bool :
94+ """True for ``args.debug and (sys.platform != "win32")`` in either operand order."""
95+ if not isinstance (node , ast .BoolOp ) or not isinstance (node .op , ast .And ) or len (node .values ) != 2 :
96+ return False
97+ a , b = node .values
98+ return (_is_args_debug (a ) and _is_sys_platform_ne_win32 (b )) or (
99+ _is_args_debug (b ) and _is_sys_platform_ne_win32 (a )
100+ )
101+
102+
103+ def _use_reloader_kwarg_tied_to_debug (call : ast .Call ) -> bool :
104+ for kw in call .keywords :
105+ if kw .arg == "use_reloader" :
106+ return _is_debug_and_platform_guard (kw .value )
107+ return False
108+
109+
26110# ---------------------------------------------------------------------------
27111# export.py argument tests
28112# ---------------------------------------------------------------------------
@@ -214,62 +298,72 @@ def test_help_exits_zero(self):
214298# ---------------------------------------------------------------------------
215299
216300class TestAppArgparse :
217- """app.py __main__ block must expose the same flags as cursor's app.py."""
218-
219- def _build_parser (self ) -> argparse .ArgumentParser :
220- """Re-create the argparse parser from app.py without importing Flask."""
221- parser = argparse .ArgumentParser (description = "Claude Code Chat Browser" )
222- parser .add_argument ("--port" , type = int , default = 5000 )
223- parser .add_argument ("--host" , default = "127.0.0.1" )
224- parser .add_argument ("--base-dir" , default = None )
225- parser .add_argument ("--exclude-rules" , "-e" , default = None ,
226- metavar = "PATH" , dest = "exclude_rules" )
227- return parser
301+ """app.py CLI must expose the same flags as cursor's app.py."""
228302
229303 def test_host_default_is_localhost (self ):
230304 """Default host must be 127.0.0.1 to match cursor which binds to localhost only."""
231- parser = self . _build_parser ()
305+ parser = build_cli_parser ()
232306 args = parser .parse_args ([])
233307 assert args .host == "127.0.0.1"
234308
235309 def test_host_override (self ):
236- parser = self . _build_parser ()
310+ parser = build_cli_parser ()
237311 args = parser .parse_args (["--host" , "127.0.0.1" ])
238312 assert args .host == "127.0.0.1"
239313
314+ def test_debug_default_is_false (self ):
315+ parser = build_cli_parser ()
316+ args = parser .parse_args ([])
317+ assert args .debug is False
318+
319+ def test_debug_explicit_true (self ):
320+ parser = build_cli_parser ()
321+ args = parser .parse_args (["--debug" ])
322+ assert args .debug is True
323+
324+ def test_app_py_debug_not_hardcoded_true (self ):
325+ """app.run() must wire debug from args, not a literal True."""
326+ app_path = os .path .join (REPO_ROOT , "app.py" )
327+ with open (app_path , encoding = "utf-8" ) as f :
328+ tree = ast .parse (f .read (), filename = app_path )
329+ app_run_calls = [n for n in ast .walk (tree ) if _is_app_run_call (n )]
330+ assert app_run_calls , "expected at least one app.run() call in app.py"
331+ assert not any (_call_passes_hardcoded_debug_true (c ) for c in app_run_calls )
332+ assert any (_debug_kwarg_uses_args (c ) for c in app_run_calls )
333+
240334 def test_port_default (self ):
241- parser = self . _build_parser ()
335+ parser = build_cli_parser ()
242336 args = parser .parse_args ([])
243337 assert args .port == 5000
244338
245339 def test_port_override (self ):
246- parser = self . _build_parser ()
340+ parser = build_cli_parser ()
247341 args = parser .parse_args (["--port" , "8080" ])
248342 assert args .port == 8080
249343
250344 def test_base_dir_default_none (self ):
251- parser = self . _build_parser ()
345+ parser = build_cli_parser ()
252346 args = parser .parse_args ([])
253347 assert args .base_dir is None
254348
255349 def test_base_dir_override (self ):
256- parser = self . _build_parser ()
350+ parser = build_cli_parser ()
257351 args = parser .parse_args (["--base-dir" , "/tmp/projects" ])
258352 assert args .base_dir == "/tmp/projects"
259353
260354 def test_exclude_rules_default_none (self ):
261- parser = self . _build_parser ()
355+ parser = build_cli_parser ()
262356 args = parser .parse_args ([])
263357 assert args .exclude_rules is None
264358
265359 def test_exclude_rules_long_form (self ):
266- parser = self . _build_parser ()
360+ parser = build_cli_parser ()
267361 args = parser .parse_args (["--exclude-rules" , "/tmp/rules.txt" ])
268362 assert args .exclude_rules == "/tmp/rules.txt"
269363
270364 def test_exclude_rules_short_form (self ):
271365 """Cursor's app.py uses -e as the short form; claude must too."""
272- parser = self . _build_parser ()
366+ parser = build_cli_parser ()
273367 args = parser .parse_args (["-e" , "/tmp/rules.txt" ])
274368 assert args .exclude_rules == "/tmp/rules.txt"
275369
@@ -294,11 +388,10 @@ def test_app_py_host_default_is_localhost(self):
294388 assert '"127.0.0.1"' in src
295389
296390 def test_app_py_use_reloader_is_platform_aware (self ):
297- """use_reloader must depend on sys.platform, not be hardcoded False ."""
391+ """use_reloader must be ``args.debug and ( sys.platform != \" win32 \" )`` ."""
298392 app_path = os .path .join (REPO_ROOT , "app.py" )
299- with open (app_path , "r" , encoding = "utf-8" ) as f :
300- src = f .read ()
301- assert "sys.platform" in src
302- assert "win32" in src
303- # Must NOT have unconditional use_reloader=False
304- assert "use_reloader=False" not in src
393+ with open (app_path , encoding = "utf-8" ) as f :
394+ tree = ast .parse (f .read (), filename = app_path )
395+ app_run_calls = [n for n in ast .walk (tree ) if _is_app_run_call (n )]
396+ assert app_run_calls
397+ assert all (_use_reloader_kwarg_tied_to_debug (c ) for c in app_run_calls )
0 commit comments