diff --git a/lib/python/base_cli/app.py b/lib/python/base_cli/app.py index 8cb595a..6bf303a 100644 --- a/lib/python/base_cli/app.py +++ b/lib/python/base_cli/app.py @@ -2876,7 +2876,16 @@ def run_app( invocation_token = _INVOCATION_ARGV.set(invocation_argv) try: bypass_token = _INVOCATION_MAIN_BYPASS.set(command) - output_capture = io.StringIO() if state.json_output else None + # A configured JSON option may be enabled by any Click-supported + # source (for example ``default_map`` or a combined short flag), + # so raw argv cannot determine capture eligibility. Buffer the + # command whenever JSON mode exists and let the parsed lifecycle + # value decide whether to emit an envelope or replay human text. + output_capture = ( + io.StringIO() + if app.lifecycle_options.json is not None + else None + ) try: if output_capture is None: result = command.main( diff --git a/tests/test_json_contracts.py b/tests/test_json_contracts.py index 712de65..729c31d 100644 --- a/tests/test_json_contracts.py +++ b/tests/test_json_contracts.py @@ -189,6 +189,54 @@ def main(ctx: base_cli.Context) -> None: self.assertEqual(envelope["type"], "success") self.assertEqual(envelope["details"]["stdout"], "hello from env\n") + def test_json_mode_captures_combined_short_flags(self) -> None: + app = base_cli.App( + name="json-combined-short", + log_to_file=False, + lifecycle_options=base_cli.LifecycleOptions( + json=base_cli.LifecycleOption("--json", "-j"), + ), + ) + + @app.command() + @base_cli.option("-x", is_flag=True) + def main(ctx: base_cli.Context, x: bool) -> None: + del ctx, x + print("hello from combined flags") + + with tempfile.TemporaryDirectory() as home: + result = base_cli.testing.invoke(app, ["-xj"], home=Path(home)) + + self.assertEqual(result.exit_code, 0, result.output) + envelope = json.loads(result.stdout) + self.assertEqual(envelope["type"], "success") + self.assertEqual( + envelope["details"]["stdout"], + "hello from combined flags\n", + ) + + def test_json_mode_captures_default_map_values(self) -> None: + app = base_cli.App( + name="json-default-map", + log_to_file=False, + lifecycle_options=base_cli.LifecycleOptions( + json=base_cli.LifecycleOption("--json"), + ), + ) + + @app.command(context_settings={"default_map": {"json": True}}) + def main(ctx: base_cli.Context) -> None: + del ctx + print("hello from default map") + + with tempfile.TemporaryDirectory() as home: + result = base_cli.testing.invoke(app, [], home=Path(home)) + + self.assertEqual(result.exit_code, 0, result.output) + envelope = json.loads(result.stdout) + self.assertEqual(envelope["type"], "success") + self.assertEqual(envelope["details"]["stdout"], "hello from default map\n") + def test_json_mode_is_opt_in_and_human_output_remains_unchanged(self) -> None: app = base_cli.App(name="human-default", log_to_file=False)