From 6b84ae69a0d08c0f7d0ca9910a0d23fcd9137fbd Mon Sep 17 00:00:00 2001 From: gdibben Date: Thu, 8 Oct 2026 08:26:40 +0100 Subject: [PATCH] Prevent --json from prompting interactively on a 403 Per review: --json still went through _create_host_backend_client and _call_host_backend_with_optional_tenant, both of which prompt interactively (writing to stdout, reading stdin) when the module- level _no_input flag is left at its default False. deploy_listeners_ list never set it, unlike the main deploy command, so a missing API key or an org-scoped key without LANGSMITH_TENANT_ID would corrupt the JSON output with warning text and could hang waiting for input that a scripted invocation never provides. Sets _no_input = json_output at the top of the command, same pattern _deploy_cmd already uses, so both paths raise a ClickException to stderr instead. Added a regression test that fails without the fix (click.prompt is genuinely invoked) and passes with it. --- libs/cli/langgraph_cli/deploy.py | 2 + libs/cli/tests/unit_tests/cli/test_cli.py | 53 +++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/libs/cli/langgraph_cli/deploy.py b/libs/cli/langgraph_cli/deploy.py index 7507482c1..f83946e32 100644 --- a/libs/cli/langgraph_cli/deploy.py +++ b/libs/cli/langgraph_cli/deploy.py @@ -2536,6 +2536,8 @@ def deploy_listeners() -> None: def deploy_listeners_list( api_key: str | None, host_url: str | None, json_output: bool ) -> None: + global _no_input + _no_input = json_output client = _create_host_backend_client(host_url, api_key) listeners = _call_host_backend_with_optional_tenant( client, lambda c: c.list_listeners() diff --git a/libs/cli/tests/unit_tests/cli/test_cli.py b/libs/cli/tests/unit_tests/cli/test_cli.py index 01838dec2..5b621b8ad 100644 --- a/libs/cli/tests/unit_tests/cli/test_cli.py +++ b/libs/cli/tests/unit_tests/cli/test_cli.py @@ -14,6 +14,7 @@ import langgraph_cli.deploy as deploy_module from langgraph_cli.cli import cli, prepare_args_and_stdin from langgraph_cli.config import Config, _get_pip_cleanup_lines, validate_config from langgraph_cli.docker import DEFAULT_POSTGRES_URI, DockerCapabilities, Version +from langgraph_cli.host_backend import HostBackendError from langgraph_cli.util import clean_empty_lines FORMATTED_CLEANUP_LINES = _get_pip_cleanup_lines( @@ -536,6 +537,9 @@ def test_deploy_listeners_list_command_no_results(monkeypatch) -> None: def test_deploy_listeners_list_command_json(monkeypatch) -> None: + # --json sets the module-level _no_input global as a side effect; pin + # the "before" value so monkeypatch restores it after this test. + monkeypatch.setattr(deploy_module, "_no_input", False) runner = CliRunner() listeners = [ @@ -579,6 +583,9 @@ def test_deploy_listeners_list_command_json(monkeypatch) -> None: def test_deploy_listeners_list_command_json_no_results(monkeypatch) -> None: + # --json sets the module-level _no_input global as a side effect; pin + # the "before" value so monkeypatch restores it after this test. + monkeypatch.setattr(deploy_module, "_no_input", False) runner = CliRunner() class FakeClient: @@ -608,6 +615,52 @@ def test_deploy_listeners_list_command_json_no_results(monkeypatch) -> None: assert json.loads(result.output) == [] +def test_deploy_listeners_list_command_json_org_scoped_403(monkeypatch) -> None: + """With --json, an org-scoped 403 must raise cleanly instead of + prompting for a workspace ID, which would write to stdout and hang + waiting for input that a scripted invocation never provides.""" + # deploy_listeners_list sets the module-level _no_input global as a + # side effect; pin its "before" value so monkeypatch restores it after + # this test instead of leaking True into whichever test runs next. + monkeypatch.setattr(deploy_module, "_no_input", False) + runner = CliRunner() + + class FakeClient: + def __init__(self, host_url: str, api_key: str, tenant_id: str | None = None): + pass + + def list_listeners(self): + raise HostBackendError("requires workspace specification", status_code=403) + + def set_tenant(self, tenant_id: str) -> None: + raise AssertionError("must not retry with a prompted tenant with --json") + + monkeypatch.setattr(deploy_module, "HostBackendClient", FakeClient) + + prompt_calls = [] + monkeypatch.setattr( + click, "prompt", lambda *a, **k: prompt_calls.append((a, k)) or "unreachable" + ) + + result = runner.invoke( + cli, + [ + "deploy", + "listeners", + "list", + "--json", + "--api-key", + "test-key", + "--host-url", + "https://api.example.com", + ], + ) + + assert prompt_calls == [], "must not prompt interactively with --json" + assert result.exit_code != 0 + assert "LANGSMITH_TENANT_ID" in result.output + + def test_deploy_revisions_list_command(monkeypatch) -> None: runner = CliRunner() captured: dict[str, str] = {}