From e2d15e7b59e35240d0ea848c57103685dee379ea Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Sat, 6 Jun 2026 22:03:54 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat(list):=20devbase=20list=20=E3=82=92?= =?UTF-8?q?=E5=AF=BE=E8=A9=B1=E9=81=B8=E6=8A=9E=E3=83=87=E3=83=95=E3=82=A9?= =?UTF-8?q?=E3=83=AB=E3=83=88=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `devbase list` / `devbase project list` を TTY ではデフォルトで対話選択 (番号入力 → project up 起動) にする。一覧表示のみは `--no-interactive` (`--plain` / `-P`)。非 TTY (パイプ/CI 等) では自動的に一覧表示へ フォールバックする。`--interactive` / `-i` は後方互換として維持。 - cli.py: --interactive を default=True 化、--no-interactive 追加 - project.py: sys.stdin.isatty() ゲートで非 TTY 自動フォールバック - bash/zsh 補完、ドキュメント、CHANGELOG を更新 - 非 TTY フォールバックの新規テスト追加 Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 2 +- docs/user/cli-reference.md | 19 +++++++++----- docs/user/container-operations.md | 10 ++++--- etc/_devbase | 14 +++++++--- etc/devbase-completion.bash | 8 +++--- lib/devbase/cli.py | 13 +++++++--- lib/devbase/commands/project.py | 5 +++- tests/cli/test_completion.py | 2 +- tests/cli/test_project_list.py | 43 ++++++++++++++++++++++++++++++- 9 files changed, 91 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17e571e..b85adc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ ### Added - **`devbase project` サブコマンド群を新設**しました (PLAN06)。CWD に依存せずプロジェクト名でコンテナ操作ができます。 - `devbase project up/down/ps/logs/scale [name]` で、任意のディレクトリから `$DEVBASE_ROOT/projects/` を対象に操作できます。名前解決はラッパー (`bin/devbase`) が対象ディレクトリへ `cd` してから実行するため、シェル実装の `build` を含む全操作が名前指定で成立します(呼び出し元シェルの作業ディレクトリは変わりません)。存在しない名前はエラーになり候補が提示されます。 - - `devbase project list [--interactive|-i]` で `$DEVBASE_ROOT/projects/` 配下を `NAME` / `PLUGIN` / `STATUS` の一覧表示します。`PLUGIN` 列はシンボリックリンク先から解決するため、PLAN04 の同名衝突 suffix(例 `carmo.takemi`)が付いていても正しいプラグイン名を表示します。`--interactive` では一覧から番号で選択して起動でき、非対話環境では番号入力にフォールバックします。 + - `devbase project list` で `$DEVBASE_ROOT/projects/` 配下を `NAME` / `PLUGIN` / `STATUS` の一覧表示します。`PLUGIN` 列はシンボリックリンク先から解決するため、PLAN04 の同名衝突 suffix(例 `carmo.takemi`)が付いていても正しいプラグイン名を表示します。**TTY ではデフォルトで対話選択**になり、一覧から番号で選んだプロジェクトを `project up` で起動します。`--no-interactive`(`--plain` / `-P`)で一覧表示のみに切り替えられ、パイプ・リダイレクト・CI などの非 TTY 環境では自動的に一覧表示へフォールバックします(`--interactive` / `-i` は後方互換として引き続き受け付けます)。 - トップレベルシノニム `devbase up/down/ps/scale [name]` / `devbase build [image]` / `devbase login [index]` / `devbase list` を整備しました(`logs` はシノニムを持たず `devbase project logs` のみ)。 - bash / zsh のシェル補完に `project` グループとプロジェクト名補完(`$DEVBASE_ROOT/projects/` 配下を列挙)を追加しました。 - 利用者向けドキュメント [`docs/user/cli-reference.md`](docs/user/cli-reference.md) / [`docs/user/container-operations.md`](docs/user/container-operations.md) を `project` 体系に更新しました。 diff --git a/docs/user/cli-reference.md b/docs/user/cli-reference.md index 6398069..9a0f388 100644 --- a/docs/user/cli-reference.md +++ b/docs/user/cli-reference.md @@ -18,7 +18,7 @@ graph TD D --> D1["up / down / ps / logs / scale [name]"] D --> D3["login [index]"] D --> D4["build [image]"] - D --> D2["list [--interactive]"] + D --> D2["list [--no-interactive]"] E --> E1[init / sync / list / set / get / delete / edit / project] F --> F1[list / install / uninstall / update / info / sync] F --> F2[repo add / repo remove / repo list / repo refresh] @@ -292,21 +292,26 @@ devbase build [image] `$DEVBASE_ROOT/projects/` 配下のプロジェクトを `NAME` / `PLUGIN` / `STATUS` の一覧で 表示します。 +TTY(端末)では**デフォルトで対話選択**になり、番号入力で選んだプロジェクトを +`project up` で起動します。パイプ・リダイレクト・CI などの非 TTY 環境では自動的に +一覧表示のみへフォールバックします。 + ``` -devbase project list [--interactive|-i] -devbase list [--interactive|-i] +devbase project list [--no-interactive|--plain|-P] +devbase list [--no-interactive|--plain|-P] ``` | オプション | 説明 | |-----------|------| -| `--interactive` / `-i` | 一覧から番号で選択し、そのプロジェクトを `project up` で起動 | +| `--no-interactive` / `--plain` / `-P` | 対話選択せず一覧表示のみ | +| `--interactive` / `-i` | (後方互換)対話選択。デフォルトのため通常は不要 | ```bash -# 一覧表示 +# 一覧を表示して番号で選択・起動(TTY デフォルト) devbase list -# 一覧から選んで起動(非対話環境では番号入力にフォールバック) -devbase list -i +# 一覧表示のみ(選択しない) +devbase list --no-interactive ``` 出力例: diff --git a/docs/user/container-operations.md b/docs/user/container-operations.md index d15889d..6ec3bb1 100644 --- a/docs/user/container-operations.md +++ b/docs/user/container-operations.md @@ -238,13 +238,17 @@ devbase project logs -f --tail 100 ### プロジェクト一覧 ```bash -# 全プロジェクトを NAME / PLUGIN / STATUS で一覧表示 +# 一覧を表示し、番号で選択して起動(TTY ではこれがデフォルト) devbase list -# 一覧から選択して起動(非対話環境では番号入力にフォールバック) -devbase list -i +# 選択せず NAME / PLUGIN / STATUS の一覧表示のみ +devbase list --no-interactive # --plain / -P も同義 ``` +> TTY(端末)では `devbase list` はデフォルトで対話選択になり、番号入力で +> そのプロジェクトを起動します。パイプ・リダイレクト・CI などの非 TTY 環境では +> 自動的に一覧表示のみにフォールバックします。 + `devbase project ps` が「対象プロジェクト 1 つのコンテナ状態」を表示するのに対し、 `devbase list` は「全プロジェクトの横断一覧」を表示します。 diff --git a/etc/_devbase b/etc/_devbase index 6d9c53e..3f50002 100644 --- a/etc/_devbase +++ b/etc/_devbase @@ -150,8 +150,11 @@ _devbase() { ;; list) _arguments \ - '--interactive[Select a project interactively and start it]' \ - '-i[Select a project interactively and start it]' + '--no-interactive[Just print the table without interactive selection]' \ + '--plain[Just print the table without interactive selection]' \ + '-P[Just print the table without interactive selection]' \ + '--interactive[(compat) interactive selection, default]' \ + '-i[(compat) interactive selection, default]' ;; project) case "$words[3]" in @@ -185,8 +188,11 @@ _devbase() { ;; list) _arguments \ - '--interactive[Select a project interactively and start it]' \ - '-i[Select a project interactively and start it]' + '--no-interactive[Just print the table without interactive selection]' \ + '--plain[Just print the table without interactive selection]' \ + '-P[Just print the table without interactive selection]' \ + '--interactive[(compat) interactive selection, default]' \ + '-i[(compat) interactive selection, default]' ;; *) _describe -t project-commands 'project command' project_subcommands diff --git a/etc/devbase-completion.bash b/etc/devbase-completion.bash index bf45245..b76a2a0 100644 --- a/etc/devbase-completion.bash +++ b/etc/devbase-completion.bash @@ -62,10 +62,10 @@ _devbase_completions() { COMPREPLY=($(compgen -W "$(_devbase_project_names)" -- "$cur")) fi ;; - # list は位置引数を取らず --interactive のみ。`-*` ガードを外し + # list は位置引数を取らず対話制御フラグのみ。`-*` ガードを外し # 常にフラグ候補を出す (zsh 側 _arguments と挙動を揃える)。 list) - COMPREPLY=($(compgen -W "--interactive -i" -- "$cur")) + COMPREPLY=($(compgen -W "--no-interactive --plain -P --interactive -i" -- "$cur")) ;; project) COMPREPLY=($(compgen -W "$project_subcommands" -- "$cur")) @@ -121,10 +121,10 @@ _devbase_completions() { COMPREPLY=($(compgen -W "$(_devbase_project_names)" -- "$cur")) fi ;; - # list は位置引数を取らず --interactive のみ。`-*` ガードを外し + # list は位置引数を取らず対話制御フラグのみ。`-*` ガードを外し # 常にフラグ候補を出す (zsh 側 _arguments と挙動を揃える)。 list) - COMPREPLY=($(compgen -W "--interactive -i" -- "$cur")) + COMPREPLY=($(compgen -W "--no-interactive --plain -P --interactive -i" -- "$cur")) ;; esac fi diff --git a/lib/devbase/cli.py b/lib/devbase/cli.py index d22773e..8dc9182 100644 --- a/lib/devbase/cli.py +++ b/lib/devbase/cli.py @@ -183,11 +183,18 @@ def _add_project_parser(subparsers): def _add_list_subparser(sub): """`list` サブコマンドを登録する (project list / top-level list 共通)。 - NAME / PLUGIN / STATUS の一覧表示。`--interactive` で選択 → `project up` 起動。 + NAME / PLUGIN / STATUS の一覧表示。デフォルトで対話選択 → `project up` 起動。 + `--no-interactive` (`--plain`) で一覧表示のみ。非 TTY では自動的に一覧のみ。 """ p = sub.add_parser('list', help='List projects (NAME / PLUGIN / STATUS)') - p.add_argument('--interactive', '-i', action='store_true', - help='Select a project interactively and start it') + # 対話選択をデフォルト ON にする。`-i` / `--interactive` は後方互換のため + # 引き続き受け付ける (既に default=True なので実質 no-op)。 + p.add_argument('--interactive', '-i', dest='interactive', + action='store_true', default=True, + help='Select a project interactively and start it (default)') + p.add_argument('--no-interactive', '--plain', '-P', dest='interactive', + action='store_false', + help='Just print the table without interactive selection') def _add_env_parser(subparsers): diff --git a/lib/devbase/commands/project.py b/lib/devbase/commands/project.py index b0a4368..759c6d0 100644 --- a/lib/devbase/commands/project.py +++ b/lib/devbase/commands/project.py @@ -11,6 +11,7 @@ from __future__ import annotations import os +import sys from pathlib import Path from devbase.log import get_logger @@ -174,7 +175,9 @@ def cmd_project_list(devbase_root: Path, args) -> int: logger.info("プロジェクトがありません (%s)。", projects_dir) return 0 - if getattr(args, "interactive", False): + # 対話選択はデフォルト ON。ただし非 TTY (パイプ / CI / リダイレクト) では + # input() が EOFError になり実用にならないため、自動的に一覧表示へフォールバック。 + if getattr(args, "interactive", True) and sys.stdin.isatty(): return _interactive_select_and_up(rows) _print_table(rows) diff --git a/tests/cli/test_completion.py b/tests/cli/test_completion.py index 0f52b14..b6c73a9 100644 --- a/tests/cli/test_completion.py +++ b/tests/cli/test_completion.py @@ -135,7 +135,7 @@ def test_bash_top_level_ps_flag_after_name(fake_root): def test_bash_project_list_flags(fake_root): out = _bash_complete("devbase project list '-'", 3, fake_root) - assert set(out) == {"--interactive", "-i"} + assert set(out) == {"--no-interactive", "--plain", "-P", "--interactive", "-i"} def test_bash_top_level_commands_include_project_and_list(fake_root): diff --git a/tests/cli/test_project_list.py b/tests/cli/test_project_list.py index f0e264c..50ccae8 100644 --- a/tests/cli/test_project_list.py +++ b/tests/cli/test_project_list.py @@ -222,6 +222,30 @@ def test_cmd_project_list_empty(tmp_path, capsys): assert rc == 0 +def test_cmd_project_list_non_tty_falls_back_to_table(tmp_path, monkeypatch, capsys): + """interactive=True (デフォルト) でも非 TTY では一覧表示にフォールバックする。""" + from devbase.commands import project as project_mod + from devbase.commands import status as status_mod + from devbase.commands import container as container_mod + + _make_plugin_project(tmp_path, "repos/o--r/alpha", "alpha-proj") + _link_project(tmp_path, "alpha-proj", "repos/o--r/alpha", "alpha-proj") + monkeypatch.setattr(status_mod, "_container_status_for", + lambda entry: {"name": entry.name, "status": "stopped", "count": 0}) + monkeypatch.setattr(project_mod.sys.stdin, "isatty", lambda: False) + + called = [] + monkeypatch.setattr(container_mod, "cmd_project", lambda args: called.append(1) or 0) + + args = types.SimpleNamespace(interactive=True) + rc = project_mod.cmd_project_list(tmp_path, args) + out = capsys.readouterr().out + + assert rc == 0 + assert called == [], "非 TTY では対話起動しない" + assert "alpha-proj" in out + + # --------------------------------------------------------------------------- # cmd_project_list: --interactive # --------------------------------------------------------------------------- @@ -237,6 +261,8 @@ def test_cmd_project_list_interactive_selects_and_ups(tmp_path, monkeypatch): _link_project(tmp_path, "beta-proj", "plugins/beta", "beta-proj") monkeypatch.setattr(status_mod, "_container_status_for", lambda entry: None) + # 対話選択は TTY 環境でのみ起動するため isatty を True に固定する。 + monkeypatch.setattr(project_mod.sys.stdin, "isatty", lambda: True) # 番号 "2" を選択 (sorted: alpha-proj=1, beta-proj=2) monkeypatch.setattr("builtins.input", lambda *a, **k: "2") @@ -261,6 +287,7 @@ def test_cmd_project_list_interactive_empty_input_aborts(tmp_path, monkeypatch): _make_plugin_project(tmp_path, "repos/o--r/alpha", "alpha-proj") _link_project(tmp_path, "alpha-proj", "repos/o--r/alpha", "alpha-proj") monkeypatch.setattr(status_mod, "_container_status_for", lambda entry: None) + monkeypatch.setattr(project_mod.sys.stdin, "isatty", lambda: True) monkeypatch.setattr("builtins.input", lambda *a, **k: "") called = [] @@ -285,6 +312,7 @@ def test_cmd_project_list_interactive_non_tty_eof(tmp_path, monkeypatch): def raise_eof(*a, **k): raise EOFError + monkeypatch.setattr(project_mod.sys.stdin, "isatty", lambda: True) monkeypatch.setattr("builtins.input", raise_eof) called = [] monkeypatch.setattr(container_mod, "cmd_project", lambda args: called.append(1) or 0) @@ -308,6 +336,7 @@ def test_cmd_project_list_interactive_keyboard_interrupt_aborts(tmp_path, monkey def raise_interrupt(*a, **k): raise KeyboardInterrupt + monkeypatch.setattr(project_mod.sys.stdin, "isatty", lambda: True) monkeypatch.setattr("builtins.input", raise_interrupt) called = [] monkeypatch.setattr(container_mod, "cmd_project", lambda args: called.append(1) or 0) @@ -328,6 +357,7 @@ def test_cmd_project_list_interactive_out_of_range_reprompts(tmp_path, monkeypat _link_project(tmp_path, "alpha-proj", "repos/o--r/alpha", "alpha-proj") monkeypatch.setattr(status_mod, "_container_status_for", lambda entry: None) + monkeypatch.setattr(project_mod.sys.stdin, "isatty", lambda: True) # "99" (範囲外) → "1" (有効) の順に入力 → 再入力後に up が起動する inputs = iter(["99", "1"]) monkeypatch.setattr("builtins.input", lambda *a, **k: next(inputs)) @@ -351,6 +381,7 @@ def test_cmd_project_list_interactive_non_numeric_reprompts(tmp_path, monkeypatc _link_project(tmp_path, "alpha-proj", "repos/o--r/alpha", "alpha-proj") monkeypatch.setattr(status_mod, "_container_status_for", lambda entry: None) + monkeypatch.setattr(project_mod.sys.stdin, "isatty", lambda: True) # "abc" (数値以外) → "1" (有効) inputs = iter(["abc", "1"]) monkeypatch.setattr("builtins.input", lambda *a, **k: next(inputs)) @@ -369,20 +400,30 @@ def test_cmd_project_list_interactive_non_numeric_reprompts(tmp_path, monkeypatc # --------------------------------------------------------------------------- def test_parser_project_list(): + # 対話選択はデフォルト ON (フラグ無しで interactive=True)。 parser = cli._create_parser() args = parser.parse_args(["project", "list"]) assert args.command == "project" assert args.subcommand == "list" - assert args.interactive is False + assert args.interactive is True def test_parser_project_list_interactive_flag(): + # `-i` / `--interactive` は後方互換で受け付ける (実質 no-op、True のまま)。 parser = cli._create_parser() for flag in ("--interactive", "-i"): args = parser.parse_args(["project", "list", flag]) assert args.interactive is True +def test_parser_project_list_no_interactive_flag(): + # `--no-interactive` / `--plain` / `-P` で一覧表示のみ (interactive=False)。 + parser = cli._create_parser() + for flag in ("--no-interactive", "--plain", "-P"): + args = parser.parse_args(["project", "list", flag]) + assert args.interactive is False + + def test_parser_top_level_list_synonym(): parser = cli._create_parser() args = parser.parse_args(["list", "-i"]) From a07cb24022bc54198fbc9d6c74fd7a13490eaa25 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Sat, 6 Jun 2026 22:53:57 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix(list):=20=E9=9D=9E=20TTY=20=E3=83=95?= =?UTF-8?q?=E3=82=A9=E3=83=BC=E3=83=AB=E3=83=90=E3=83=83=E3=82=AF=E5=88=A4?= =?UTF-8?q?=E5=AE=9A=E3=81=AB=20stdout.isatty()=20=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `devbase list | cat` や `devbase list > out.txt` のように stdout だけが 非 TTY のケースでも対話選択が起動してしまう問題を修正。stdin / stdout の いずれかが非 TTY なら確実に一覧表示へフォールバックするよう判定を拡張した。 stdout 非 TTY フォールバックのテストも追加。 Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/devbase/commands/project.py | 4 +++- tests/cli/test_project_list.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/lib/devbase/commands/project.py b/lib/devbase/commands/project.py index 759c6d0..8ce9242 100644 --- a/lib/devbase/commands/project.py +++ b/lib/devbase/commands/project.py @@ -177,7 +177,9 @@ def cmd_project_list(devbase_root: Path, args) -> int: # 対話選択はデフォルト ON。ただし非 TTY (パイプ / CI / リダイレクト) では # input() が EOFError になり実用にならないため、自動的に一覧表示へフォールバック。 - if getattr(args, "interactive", True) and sys.stdin.isatty(): + # stdin / stdout のいずれかが非 TTY (`devbase list | cat`, `> out.txt` 等) なら + # 対話プロンプトが表示できない / 読めないため、確実に一覧表示へフォールバックする。 + if getattr(args, "interactive", True) and sys.stdin.isatty() and sys.stdout.isatty(): return _interactive_select_and_up(rows) _print_table(rows) diff --git a/tests/cli/test_project_list.py b/tests/cli/test_project_list.py index 50ccae8..881eb6f 100644 --- a/tests/cli/test_project_list.py +++ b/tests/cli/test_project_list.py @@ -246,6 +246,32 @@ def test_cmd_project_list_non_tty_falls_back_to_table(tmp_path, monkeypatch, cap assert "alpha-proj" in out +def test_cmd_project_list_stdout_non_tty_falls_back_to_table(tmp_path, monkeypatch, capsys): + """stdin が TTY でも stdout が非 TTY (`devbase list | cat` / `> out.txt`) なら + 対話起動せず一覧表示へフォールバックする。""" + from devbase.commands import project as project_mod + from devbase.commands import status as status_mod + from devbase.commands import container as container_mod + + _make_plugin_project(tmp_path, "repos/o--r/alpha", "alpha-proj") + _link_project(tmp_path, "alpha-proj", "repos/o--r/alpha", "alpha-proj") + monkeypatch.setattr(status_mod, "_container_status_for", + lambda entry: {"name": entry.name, "status": "stopped", "count": 0}) + monkeypatch.setattr(project_mod.sys.stdin, "isatty", lambda: True) + monkeypatch.setattr(project_mod.sys.stdout, "isatty", lambda: False) + + called = [] + monkeypatch.setattr(container_mod, "cmd_project", lambda args: called.append(1) or 0) + + args = types.SimpleNamespace(interactive=True) + rc = project_mod.cmd_project_list(tmp_path, args) + out = capsys.readouterr().out + + assert rc == 0 + assert called == [], "stdout 非 TTY では対話起動しない" + assert "alpha-proj" in out + + # --------------------------------------------------------------------------- # cmd_project_list: --interactive # --------------------------------------------------------------------------- @@ -263,6 +289,7 @@ def test_cmd_project_list_interactive_selects_and_ups(tmp_path, monkeypatch): # 対話選択は TTY 環境でのみ起動するため isatty を True に固定する。 monkeypatch.setattr(project_mod.sys.stdin, "isatty", lambda: True) + monkeypatch.setattr(project_mod.sys.stdout, "isatty", lambda: True) # 番号 "2" を選択 (sorted: alpha-proj=1, beta-proj=2) monkeypatch.setattr("builtins.input", lambda *a, **k: "2") @@ -288,6 +315,7 @@ def test_cmd_project_list_interactive_empty_input_aborts(tmp_path, monkeypatch): _link_project(tmp_path, "alpha-proj", "repos/o--r/alpha", "alpha-proj") monkeypatch.setattr(status_mod, "_container_status_for", lambda entry: None) monkeypatch.setattr(project_mod.sys.stdin, "isatty", lambda: True) + monkeypatch.setattr(project_mod.sys.stdout, "isatty", lambda: True) monkeypatch.setattr("builtins.input", lambda *a, **k: "") called = [] @@ -313,6 +341,7 @@ def raise_eof(*a, **k): raise EOFError monkeypatch.setattr(project_mod.sys.stdin, "isatty", lambda: True) + monkeypatch.setattr(project_mod.sys.stdout, "isatty", lambda: True) monkeypatch.setattr("builtins.input", raise_eof) called = [] monkeypatch.setattr(container_mod, "cmd_project", lambda args: called.append(1) or 0) @@ -337,6 +366,7 @@ def raise_interrupt(*a, **k): raise KeyboardInterrupt monkeypatch.setattr(project_mod.sys.stdin, "isatty", lambda: True) + monkeypatch.setattr(project_mod.sys.stdout, "isatty", lambda: True) monkeypatch.setattr("builtins.input", raise_interrupt) called = [] monkeypatch.setattr(container_mod, "cmd_project", lambda args: called.append(1) or 0) @@ -358,6 +388,7 @@ def test_cmd_project_list_interactive_out_of_range_reprompts(tmp_path, monkeypat monkeypatch.setattr(status_mod, "_container_status_for", lambda entry: None) monkeypatch.setattr(project_mod.sys.stdin, "isatty", lambda: True) + monkeypatch.setattr(project_mod.sys.stdout, "isatty", lambda: True) # "99" (範囲外) → "1" (有効) の順に入力 → 再入力後に up が起動する inputs = iter(["99", "1"]) monkeypatch.setattr("builtins.input", lambda *a, **k: next(inputs)) @@ -382,6 +413,7 @@ def test_cmd_project_list_interactive_non_numeric_reprompts(tmp_path, monkeypatc monkeypatch.setattr(status_mod, "_container_status_for", lambda entry: None) monkeypatch.setattr(project_mod.sys.stdin, "isatty", lambda: True) + monkeypatch.setattr(project_mod.sys.stdout, "isatty", lambda: True) # "abc" (数値以外) → "1" (有効) inputs = iter(["abc", "1"]) monkeypatch.setattr("builtins.input", lambda *a, **k: next(inputs))