@@ -222,6 +222,56 @@ def test_cmd_project_list_empty(tmp_path, capsys):
222222 assert rc == 0
223223
224224
225+ def test_cmd_project_list_non_tty_falls_back_to_table (tmp_path , monkeypatch , capsys ):
226+ """interactive=True (デフォルト) でも非 TTY では一覧表示にフォールバックする。"""
227+ from devbase .commands import project as project_mod
228+ from devbase .commands import status as status_mod
229+ from devbase .commands import container as container_mod
230+
231+ _make_plugin_project (tmp_path , "repos/o--r/alpha" , "alpha-proj" )
232+ _link_project (tmp_path , "alpha-proj" , "repos/o--r/alpha" , "alpha-proj" )
233+ monkeypatch .setattr (status_mod , "_container_status_for" ,
234+ lambda entry : {"name" : entry .name , "status" : "stopped" , "count" : 0 })
235+ monkeypatch .setattr (project_mod .sys .stdin , "isatty" , lambda : False )
236+
237+ called = []
238+ monkeypatch .setattr (container_mod , "cmd_project" , lambda args : called .append (1 ) or 0 )
239+
240+ args = types .SimpleNamespace (interactive = True )
241+ rc = project_mod .cmd_project_list (tmp_path , args )
242+ out = capsys .readouterr ().out
243+
244+ assert rc == 0
245+ assert called == [], "非 TTY では対話起動しない"
246+ assert "alpha-proj" in out
247+
248+
249+ def test_cmd_project_list_stdout_non_tty_falls_back_to_table (tmp_path , monkeypatch , capsys ):
250+ """stdin が TTY でも stdout が非 TTY (`devbase list | cat` / `> out.txt`) なら
251+ 対話起動せず一覧表示へフォールバックする。"""
252+ from devbase .commands import project as project_mod
253+ from devbase .commands import status as status_mod
254+ from devbase .commands import container as container_mod
255+
256+ _make_plugin_project (tmp_path , "repos/o--r/alpha" , "alpha-proj" )
257+ _link_project (tmp_path , "alpha-proj" , "repos/o--r/alpha" , "alpha-proj" )
258+ monkeypatch .setattr (status_mod , "_container_status_for" ,
259+ lambda entry : {"name" : entry .name , "status" : "stopped" , "count" : 0 })
260+ monkeypatch .setattr (project_mod .sys .stdin , "isatty" , lambda : True )
261+ monkeypatch .setattr (project_mod .sys .stdout , "isatty" , lambda : False )
262+
263+ called = []
264+ monkeypatch .setattr (container_mod , "cmd_project" , lambda args : called .append (1 ) or 0 )
265+
266+ args = types .SimpleNamespace (interactive = True )
267+ rc = project_mod .cmd_project_list (tmp_path , args )
268+ out = capsys .readouterr ().out
269+
270+ assert rc == 0
271+ assert called == [], "stdout 非 TTY では対話起動しない"
272+ assert "alpha-proj" in out
273+
274+
225275# ---------------------------------------------------------------------------
226276# cmd_project_list: --interactive
227277# ---------------------------------------------------------------------------
@@ -237,6 +287,9 @@ def test_cmd_project_list_interactive_selects_and_ups(tmp_path, monkeypatch):
237287 _link_project (tmp_path , "beta-proj" , "plugins/beta" , "beta-proj" )
238288 monkeypatch .setattr (status_mod , "_container_status_for" , lambda entry : None )
239289
290+ # 対話選択は TTY 環境でのみ起動するため isatty を True に固定する。
291+ monkeypatch .setattr (project_mod .sys .stdin , "isatty" , lambda : True )
292+ monkeypatch .setattr (project_mod .sys .stdout , "isatty" , lambda : True )
240293 # 番号 "2" を選択 (sorted: alpha-proj=1, beta-proj=2)
241294 monkeypatch .setattr ("builtins.input" , lambda * a , ** k : "2" )
242295
@@ -261,6 +314,8 @@ def test_cmd_project_list_interactive_empty_input_aborts(tmp_path, monkeypatch):
261314 _make_plugin_project (tmp_path , "repos/o--r/alpha" , "alpha-proj" )
262315 _link_project (tmp_path , "alpha-proj" , "repos/o--r/alpha" , "alpha-proj" )
263316 monkeypatch .setattr (status_mod , "_container_status_for" , lambda entry : None )
317+ monkeypatch .setattr (project_mod .sys .stdin , "isatty" , lambda : True )
318+ monkeypatch .setattr (project_mod .sys .stdout , "isatty" , lambda : True )
264319 monkeypatch .setattr ("builtins.input" , lambda * a , ** k : "" )
265320
266321 called = []
@@ -285,6 +340,8 @@ def test_cmd_project_list_interactive_non_tty_eof(tmp_path, monkeypatch):
285340 def raise_eof (* a , ** k ):
286341 raise EOFError
287342
343+ monkeypatch .setattr (project_mod .sys .stdin , "isatty" , lambda : True )
344+ monkeypatch .setattr (project_mod .sys .stdout , "isatty" , lambda : True )
288345 monkeypatch .setattr ("builtins.input" , raise_eof )
289346 called = []
290347 monkeypatch .setattr (container_mod , "cmd_project" , lambda args : called .append (1 ) or 0 )
@@ -308,6 +365,8 @@ def test_cmd_project_list_interactive_keyboard_interrupt_aborts(tmp_path, monkey
308365 def raise_interrupt (* a , ** k ):
309366 raise KeyboardInterrupt
310367
368+ monkeypatch .setattr (project_mod .sys .stdin , "isatty" , lambda : True )
369+ monkeypatch .setattr (project_mod .sys .stdout , "isatty" , lambda : True )
311370 monkeypatch .setattr ("builtins.input" , raise_interrupt )
312371 called = []
313372 monkeypatch .setattr (container_mod , "cmd_project" , lambda args : called .append (1 ) or 0 )
@@ -328,6 +387,8 @@ def test_cmd_project_list_interactive_out_of_range_reprompts(tmp_path, monkeypat
328387 _link_project (tmp_path , "alpha-proj" , "repos/o--r/alpha" , "alpha-proj" )
329388 monkeypatch .setattr (status_mod , "_container_status_for" , lambda entry : None )
330389
390+ monkeypatch .setattr (project_mod .sys .stdin , "isatty" , lambda : True )
391+ monkeypatch .setattr (project_mod .sys .stdout , "isatty" , lambda : True )
331392 # "99" (範囲外) → "1" (有効) の順に入力 → 再入力後に up が起動する
332393 inputs = iter (["99" , "1" ])
333394 monkeypatch .setattr ("builtins.input" , lambda * a , ** k : next (inputs ))
@@ -351,6 +412,8 @@ def test_cmd_project_list_interactive_non_numeric_reprompts(tmp_path, monkeypatc
351412 _link_project (tmp_path , "alpha-proj" , "repos/o--r/alpha" , "alpha-proj" )
352413 monkeypatch .setattr (status_mod , "_container_status_for" , lambda entry : None )
353414
415+ monkeypatch .setattr (project_mod .sys .stdin , "isatty" , lambda : True )
416+ monkeypatch .setattr (project_mod .sys .stdout , "isatty" , lambda : True )
354417 # "abc" (数値以外) → "1" (有効)
355418 inputs = iter (["abc" , "1" ])
356419 monkeypatch .setattr ("builtins.input" , lambda * a , ** k : next (inputs ))
@@ -369,20 +432,30 @@ def test_cmd_project_list_interactive_non_numeric_reprompts(tmp_path, monkeypatc
369432# ---------------------------------------------------------------------------
370433
371434def test_parser_project_list ():
435+ # 対話選択はデフォルト ON (フラグ無しで interactive=True)。
372436 parser = cli ._create_parser ()
373437 args = parser .parse_args (["project" , "list" ])
374438 assert args .command == "project"
375439 assert args .subcommand == "list"
376- assert args .interactive is False
440+ assert args .interactive is True
377441
378442
379443def test_parser_project_list_interactive_flag ():
444+ # `-i` / `--interactive` は後方互換で受け付ける (実質 no-op、True のまま)。
380445 parser = cli ._create_parser ()
381446 for flag in ("--interactive" , "-i" ):
382447 args = parser .parse_args (["project" , "list" , flag ])
383448 assert args .interactive is True
384449
385450
451+ def test_parser_project_list_no_interactive_flag ():
452+ # `--no-interactive` / `--plain` / `-P` で一覧表示のみ (interactive=False)。
453+ parser = cli ._create_parser ()
454+ for flag in ("--no-interactive" , "--plain" , "-P" ):
455+ args = parser .parse_args (["project" , "list" , flag ])
456+ assert args .interactive is False
457+
458+
386459def test_parser_top_level_list_synonym ():
387460 parser = cli ._create_parser ()
388461 args = parser .parse_args (["list" , "-i" ])
0 commit comments