This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 7bbd8a79283 Breeze: Show local reproduction commands in all breeze CI 
steps (#63901)
7bbd8a79283 is described below

commit 7bbd8a79283876db4d786291ea830efd38acd193
Author: Aaron Chen <[email protected]>
AuthorDate: Fri Apr 10 02:46:11 2026 -0700

    Breeze: Show local reproduction commands in all breeze CI steps (#63901)
    
    * Breeze: Show local reproduction commands for 
build-docs/core-tests/providers-tests/task-sdk-tests
    
    * Fix formatting in local reproduction command output
    
    * Refactor CI reproduction commands to derive from Click context 
automatically
    
    * Fix handling of excluded parameters and positional arguments in 
reproduction command context
    
    * Use long-form flags for flag pairs in reproduction commands
    
    Co-authored-by: Copilot <[email protected]>
    
    * Update reproduction commands to fetch current commit SHA and adjust 
comments for clarity
    
    * add ruler on the top and bottom of reproduce command
    
    * Automate CI reproduction commands for all Breeze commands via 
command_class
    
    * fix CI error - adjust position of type ignore
    
    ---------
    
    Co-authored-by: Copilot <[email protected]>
---
 dev/breeze/src/airflow_breeze/utils/click_utils.py |  28 +-
 .../src/airflow_breeze/utils/reproduce_ci.py       | 239 ++++++++++++
 dev/breeze/tests/test_reproduce_ci.py              | 432 +++++++++++++++++++++
 3 files changed, 697 insertions(+), 2 deletions(-)

diff --git a/dev/breeze/src/airflow_breeze/utils/click_utils.py 
b/dev/breeze/src/airflow_breeze/utils/click_utils.py
index 92d8d7b7cea..25a9de38868 100644
--- a/dev/breeze/src/airflow_breeze/utils/click_utils.py
+++ b/dev/breeze/src/airflow_breeze/utils/click_utils.py
@@ -16,7 +16,31 @@
 # under the License.
 from __future__ import annotations
 
+from typing import TYPE_CHECKING
+
 try:
-    from rich_click import RichGroup as BreezeGroup
+    from rich_click import RichCommand as _BaseCommand, RichGroup as _BaseGroup
 except ImportError:
-    from click import Group as BreezeGroup  # type: ignore[assignment]  # 
noqa: F401
+    from click import (  # type: ignore[assignment]
+        Command as _BaseCommand,
+        Group as _BaseGroup,
+    )
+
+if TYPE_CHECKING:
+    import click
+
+
+class BreezeCommand(_BaseCommand):
+    """Breeze CLI command that automatically prints reproduction instructions 
in CI."""
+
+    def invoke(self, ctx: click.Context) -> None:
+        try:
+            return super().invoke(ctx)
+        finally:
+            from airflow_breeze.utils.reproduce_ci import 
maybe_print_reproduction
+
+            maybe_print_reproduction(ctx)
+
+
+class BreezeGroup(_BaseGroup):
+    command_class = BreezeCommand
diff --git a/dev/breeze/src/airflow_breeze/utils/reproduce_ci.py 
b/dev/breeze/src/airflow_breeze/utils/reproduce_ci.py
new file mode 100644
index 00000000000..4a429f556ab
--- /dev/null
+++ b/dev/breeze/src/airflow_breeze/utils/reproduce_ci.py
@@ -0,0 +1,239 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Helpers for printing local reproduction instructions in CI logs."""
+
+from __future__ import annotations
+
+import os
+import shlex
+from dataclasses import dataclass
+
+import click
+from click.core import ParameterSource
+from rich.markup import escape
+
+from airflow_breeze.global_constants import APACHE_AIRFLOW_GITHUB_REPOSITORY
+from airflow_breeze.utils.console import get_console
+from airflow_breeze.utils.run_utils import commit_sha
+
+# Options that are side-effect-only or not meaningful for reproduction (safety 
net;
+# expose_value=False options like --verbose/--dry-run/--answer are already 
excluded
+# automatically because they don't appear in ctx.params).
+_EXCLUDED_PARAMS: frozenset[str] = frozenset(
+    {
+        "verbose",
+        "dry_run",
+        "answer",
+        "include_success_outputs",
+        "debug_resources",
+    }
+)
+
+# These sources represent values explicitly provided by the user or CI.
+_EXPLICIT_SOURCES: frozenset[ParameterSource] = frozenset(
+    {
+        ParameterSource.COMMANDLINE,
+        ParameterSource.ENVIRONMENT,
+        ParameterSource.PROMPT,
+    }
+)
+
+
+@dataclass
+class ReproductionCommand:
+    argv: list[str]
+    comment: str | None = None
+
+
+def build_reproduction_command_from_context(
+    ctx: click.Context,
+    *,
+    comment: str = "Run the same Breeze command locally",
+) -> ReproductionCommand:
+    """Reconstruct the CLI invocation from the current click Context.
+
+    Iterates over every parameter defined on the command, uses
+    ``ctx.get_parameter_source()`` to identify explicitly-provided values
+    (COMMANDLINE / ENVIRONMENT / PROMPT), and emits only those.  DEFAULT
+    and DEFAULT_MAP values are omitted to keep the output concise.
+
+    This removes the need for per-command builder functions.
+    """
+    argv: list[str] = ctx.command_path.split()
+
+    for param in ctx.command.params:
+        if not getattr(param, "expose_value", True):
+            continue
+        if param.name is None or param.name in _EXCLUDED_PARAMS:
+            continue
+
+        value = ctx.params.get(param.name)
+        source = ctx.get_parameter_source(param.name)
+
+        if isinstance(param, click.Argument):
+            continue  # collected after options
+
+        if not isinstance(param, click.Option):
+            continue
+
+        # Flag pair (e.g. --force-sa-warnings/--no-force-sa-warnings):
+        # emit the appropriate side only when explicitly provided.
+        if param.is_flag and param.secondary_opts:
+            if source in _EXPLICIT_SOURCES:
+                # Prefer long-form alias for both sides of the flag pair.
+                flag = param.opts[-1] if value else param.secondary_opts[-1]
+                argv.append(flag)
+            continue
+
+        # Simple boolean flag (no secondary_opts)
+        if param.is_flag:
+            if value and source in _EXPLICIT_SOURCES:
+                argv.append(param.opts[-1])
+            continue
+
+        # Non-flag option: only emit explicitly-provided values
+        if source not in _EXPLICIT_SOURCES:
+            continue
+        if value is None:
+            continue
+
+        flag = param.opts[-1]  # prefer long form
+
+        # Multiple option (e.g. --package-filter repeated)
+        if param.multiple:
+            for item in value:
+                argv.extend([flag, str(item)])
+            continue
+
+        argv.extend([flag, str(value)])
+
+    # Append positional arguments at the end
+    for param in ctx.command.params:
+        if isinstance(param, click.Argument) and param.name is not None:
+            value = ctx.params.get(param.name)
+            if value is not None:
+                if isinstance(value, (list, tuple)):
+                    argv.extend(str(v) for v in value)
+                else:
+                    argv.append(str(value))
+
+    return ReproductionCommand(argv=argv, comment=comment)
+
+
+def build_checkout_reproduction_commands(github_repository: str) -> 
list[ReproductionCommand]:
+    """Build git commands needed to reproduce the current CI checkout 
locally."""
+    current_commit_sha = os.environ.get("GITHUB_SHA") or 
os.environ.get("COMMIT_SHA") or commit_sha()
+    github_ref = os.environ.get("GITHUB_REF", "")
+    github_ref_parts = github_ref.split("/")
+    if len(github_ref_parts) == 4 and github_ref_parts[:2] == ["refs", "pull"]:
+        pull_request_number = github_ref_parts[2]
+        pull_request_ref_kind = github_ref_parts[3]
+        return [
+            ReproductionCommand(
+                argv=[
+                    "git",
+                    "fetch",
+                    f"https://github.com/{github_repository}.git";,
+                    github_ref,
+                ],
+                comment=f"Fetch the same code as CI (pull request 
{pull_request_ref_kind} ref)"
+                f" — or: gh pr checkout {pull_request_number}",
+            ),
+            ReproductionCommand(
+                argv=["git", "checkout", current_commit_sha],
+            ),
+        ]
+
+    if not current_commit_sha or current_commit_sha == "COMMIT_SHA_NOT_FOUND":
+        return []
+    return [
+        ReproductionCommand(
+            argv=["git", "checkout", current_commit_sha],
+            comment="Check out the same commit",
+        )
+    ]
+
+
+def build_ci_image_reproduction_command(
+    *,
+    github_repository: str = APACHE_AIRFLOW_GITHUB_REPOSITORY,
+    platform: str = "linux/amd64",
+    python: str = "",
+) -> ReproductionCommand:
+    """Build the CI image preparation command for local reproduction."""
+    if not python:
+        from airflow_breeze.global_constants import 
DEFAULT_PYTHON_MAJOR_MINOR_VERSION
+
+        python = DEFAULT_PYTHON_MAJOR_MINOR_VERSION
+    command = ["breeze", "ci-image", "build"]
+    if github_repository != APACHE_AIRFLOW_GITHUB_REPOSITORY:
+        command.extend(["--github-repository", github_repository])
+    command.extend(["--platform", platform, "--python", python])
+    return ReproductionCommand(
+        argv=command,
+        comment="Build the CI image locally",
+    )
+
+
+def should_print_local_reproduction() -> bool:
+    """Return True when local reproduction instructions should be printed."""
+    return (
+        os.environ.get("CI", "").lower() == "true" and 
os.environ.get("GITHUB_ACTIONS", "").lower() == "true"
+    )
+
+
+def print_local_reproduction(commands: list[ReproductionCommand]) -> None:
+    """Print local reproduction commands in CI logs."""
+    if not should_print_local_reproduction() or not commands:
+        return
+    lines: list[str] = []
+    step_number = 0
+    for command in commands:
+        if command.comment:
+            if lines:
+                lines.append("")
+            step_number += 1
+            lines.append(f"# {step_number}. {command.comment}")
+        lines.append(shlex.join(command.argv))
+    rendered = "\n".join(lines)
+    ruler = "─" * 80
+    console = get_console()
+    console.print(f"\n[warning]{ruler}[/]")
+    console.print("[warning]HOW TO REPRODUCE LOCALLY[/]\n")
+    console.print(f"[info]{escape(rendered)}[/]\n", soft_wrap=True)
+    console.print(f"[warning]{ruler}[/]\n")
+
+
+def maybe_print_reproduction(ctx: click.Context) -> None:
+    """Called by BreezeCommand.invoke() — prints reproduction instructions in 
CI."""
+    if not should_print_local_reproduction():
+        return
+
+    github_repository = ctx.params.get("github_repository", 
APACHE_AIRFLOW_GITHUB_REPOSITORY)
+    commands = build_checkout_reproduction_commands(github_repository)
+    # Skip CI image prelude for image build commands themselves — it would be 
redundant.
+    command_path = ctx.command_path
+    if not command_path.startswith(("breeze ci-image", "breeze prod-image")):
+        commands.append(
+            build_ci_image_reproduction_command(
+                github_repository=github_repository,
+                python=ctx.params.get("python", ""),
+                platform=ctx.params.get("platform", "linux/amd64"),
+            )
+        )
+    commands.append(build_reproduction_command_from_context(ctx))
+    print_local_reproduction(commands)
diff --git a/dev/breeze/tests/test_reproduce_ci.py 
b/dev/breeze/tests/test_reproduce_ci.py
new file mode 100644
index 00000000000..13d9c1b58e6
--- /dev/null
+++ b/dev/breeze/tests/test_reproduce_ci.py
@@ -0,0 +1,432 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from unittest import mock
+
+import click
+import click.testing
+import pytest
+
+from airflow_breeze.utils.click_utils import BreezeGroup
+from airflow_breeze.utils.reproduce_ci import (
+    ReproductionCommand,
+    build_checkout_reproduction_commands,
+    build_ci_image_reproduction_command,
+    build_reproduction_command_from_context,
+    print_local_reproduction,
+    should_print_local_reproduction,
+)
+
+
[email protected](
+    ("env_vars", "expected"),
+    [
+        ({"CI": "true", "GITHUB_ACTIONS": "true"}, True),
+        ({"CI": "true", "GITHUB_ACTIONS": "false"}, False),
+        ({"CI": "false", "GITHUB_ACTIONS": "true"}, False),
+        ({}, False),
+    ],
+)
+def test_should_print_local_reproduction_only_in_github_actions(env_vars, 
expected, monkeypatch):
+    monkeypatch.delenv("CI", raising=False)
+    monkeypatch.delenv("GITHUB_ACTIONS", raising=False)
+    for key, value in env_vars.items():
+        monkeypatch.setenv(key, value)
+    assert should_print_local_reproduction() is expected
+
+
+def test_build_ci_image_reproduction_command_with_custom_repo():
+    result = build_ci_image_reproduction_command(
+        github_repository="someone/airflow",
+        platform="linux/arm64",
+        python="3.11",
+    )
+    assert result.comment == "Build the CI image locally"
+    assert result.argv == [
+        "breeze",
+        "ci-image",
+        "build",
+        "--github-repository",
+        "someone/airflow",
+        "--platform",
+        "linux/arm64",
+        "--python",
+        "3.11",
+    ]
+
+
+def test_build_ci_image_reproduction_command_default_repo():
+    result = build_ci_image_reproduction_command(platform="linux/amd64", 
python="3.10")
+    assert result.argv == [
+        "breeze",
+        "ci-image",
+        "build",
+        "--platform",
+        "linux/amd64",
+        "--python",
+        "3.10",
+    ]
+
+
[email protected]("pr_ref_kind", ["merge", "head"])
+def 
test_build_checkout_reproduction_commands_fetches_pull_request_ref(pr_ref_kind, 
monkeypatch):
+    github_ref = f"refs/pull/42/{pr_ref_kind}"
+    monkeypatch.setenv("GITHUB_REF", github_ref)
+    monkeypatch.setenv("GITHUB_SHA", "merge-sha")
+
+    commands = build_checkout_reproduction_commands("someone/airflow")
+
+    assert [command.comment for command in commands] == [
+        f"Fetch the same code as CI (pull request {pr_ref_kind} ref) — or: gh 
pr checkout 42",
+        None,
+    ]
+    assert commands[0].argv == [
+        "git",
+        "fetch",
+        "https://github.com/someone/airflow.git";,
+        github_ref,
+    ]
+    assert commands[1].argv == ["git", "checkout", "merge-sha"]
+
+
+def test_build_checkout_reproduction_commands_plain_sha(monkeypatch):
+    monkeypatch.delenv("GITHUB_REF", raising=False)
+    monkeypatch.setenv("GITHUB_SHA", "def456")
+
+    commands = build_checkout_reproduction_commands("apache/airflow")
+
+    assert len(commands) == 1
+    assert commands[0].argv == ["git", "checkout", "def456"]
+
+
[email protected]("airflow_breeze.utils.reproduce_ci.get_console", autospec=True)
+def test_print_local_reproduction_renders_copyable_commands(mock_get_console, 
monkeypatch):
+    monkeypatch.setenv("CI", "true")
+    monkeypatch.setenv("GITHUB_ACTIONS", "true")
+
+    print_local_reproduction(
+        [
+            ReproductionCommand(argv=["git", "checkout", "abc123"], 
comment="Check out the same commit"),
+            ReproductionCommand(
+                argv=["breeze", "build-docs", "--docs-only"],
+                comment="Run the same Breeze command locally",
+            ),
+        ]
+    )
+
+    assert mock_get_console.return_value.print.call_count == 4
+    ruler_line = mock_get_console.return_value.print.call_args_list[0].args[0]
+    assert "─" * 80 in ruler_line
+    rendered_output = 
mock_get_console.return_value.print.call_args_list[2].args[0]
+    assert "# 1. Check out the same commit" in rendered_output
+    assert "git checkout abc123" in rendered_output
+    assert "breeze build-docs --docs-only" in rendered_output
+    bottom_ruler = 
mock_get_console.return_value.print.call_args_list[3].args[0]
+    assert "─" * 80 in bottom_ruler
+
+
+def _invoke_and_capture_reproduction(cli, args, monkeypatch, *, env: dict[str, 
str] | None = None):
+    captured_commands: list[list[ReproductionCommand]] = []
+
+    monkeypatch.setenv("CI", "true")
+    monkeypatch.setenv("GITHUB_ACTIONS", "true")
+    monkeypatch.setenv("GITHUB_SHA", "abc123")
+    monkeypatch.delenv("GITHUB_REF", raising=False)
+    if env:
+        for key, value in env.items():
+            monkeypatch.setenv(key, value)
+    monkeypatch.setattr(
+        "airflow_breeze.utils.reproduce_ci.print_local_reproduction",
+        lambda commands: captured_commands.append(commands),
+    )
+
+    result = click.testing.CliRunner().invoke(cli, args)
+
+    assert result.exit_code == 0, result.output
+    assert len(captured_commands) == 1
+    return captured_commands[0]
+
+
+def 
test_breeze_command_smoke_skip_cleanup_is_included_in_rendered_command(monkeypatch):
+    @click.group(cls=BreezeGroup, name="breeze")
+    def cli():
+        pass
+
+    @cli.command(name="parallel-task")
+    @click.option("--skip-cleanup", is_flag=True)
+    def parallel_task(skip_cleanup: bool):
+        del skip_cleanup
+
+    captured_commands = _invoke_and_capture_reproduction(
+        cli, ["parallel-task", "--skip-cleanup"], monkeypatch
+    )
+
+    assert captured_commands[-1].argv == ["breeze", "parallel-task", 
"--skip-cleanup"]
+
+
+# ---------------------------------------------------------------------------
+# Tests for build_reproduction_command_from_context
+# ---------------------------------------------------------------------------
+
+
+def _build_test_command(**options):
+    """Build a simple click command with the given options for testing."""
+
+    @click.command("test-cmd")
+    def cmd(**kwargs):
+        pass
+
+    for _name, opt in options.items():
+        cmd = opt(cmd)
+    return cmd
+
+
+def _invoke_and_get_context(cmd, args, env=None):
+    """Invoke a click command and capture the context."""
+    captured_ctx = {}
+
+    original_invoke = cmd.invoke
+
+    def patched_invoke(ctx):
+        captured_ctx["ctx"] = ctx
+        return original_invoke(ctx)
+
+    cmd.invoke = patched_invoke
+    runner = click.testing.CliRunner(env=env or {})
+    result = runner.invoke(cmd, args, catch_exceptions=False)
+    assert result.exit_code == 0, result.output
+    return captured_ctx["ctx"]
+
+
+class TestBuildReproductionCommandFromContext:
+    """Tests for the generic Click context-based command renderer."""
+
+    def test_simple_bool_flag_emitted_when_true(self):
+        @click.command("my-cmd")
+        @click.option("--verbose-output", is_flag=True, default=False)
+        def cmd(**kwargs):
+            pass
+
+        ctx = _invoke_and_get_context(cmd, ["--verbose-output"])
+        result = build_reproduction_command_from_context(ctx)
+        assert result.argv == ["my-cmd", "--verbose-output"]
+
+    def test_simple_bool_flag_omitted_when_default(self):
+        @click.command("my-cmd")
+        @click.option("--verbose-output", is_flag=True, default=False)
+        def cmd(**kwargs):
+            pass
+
+        ctx = _invoke_and_get_context(cmd, [])
+        result = build_reproduction_command_from_context(ctx)
+        assert result.argv == ["my-cmd"]
+
+    def test_flag_pair_emits_positive_side(self):
+        @click.command("my-cmd")
+        @click.option("--force/--no-force", default=False)
+        def cmd(**kwargs):
+            pass
+
+        ctx = _invoke_and_get_context(cmd, ["--force"])
+        result = build_reproduction_command_from_context(ctx)
+        assert "--force" in result.argv
+        assert "--no-force" not in result.argv
+
+    def test_flag_pair_emits_negative_side(self):
+        @click.command("my-cmd")
+        @click.option("--force/--no-force", default=True)
+        def cmd(**kwargs):
+            pass
+
+        ctx = _invoke_and_get_context(cmd, ["--no-force"])
+        result = build_reproduction_command_from_context(ctx)
+        assert "--no-force" in result.argv
+        assert result.argv.count("--force") == 0
+
+    def test_flag_pair_omitted_when_default(self):
+        @click.command("my-cmd")
+        @click.option("--force/--no-force", default=True)
+        def cmd(**kwargs):
+            pass
+
+        ctx = _invoke_and_get_context(cmd, [])
+        result = build_reproduction_command_from_context(ctx)
+        assert "--force" not in result.argv
+        assert "--no-force" not in result.argv
+
+    def test_flag_pair_prefers_long_form(self):
+        @click.command("my-cmd")
+        @click.option("-f", "--force/--no-force", default=False)
+        def cmd(**kwargs):
+            pass
+
+        ctx = _invoke_and_get_context(cmd, ["-f"])
+        result = build_reproduction_command_from_context(ctx)
+        assert "--force" in result.argv
+        assert "-f" not in result.argv
+
+    def test_string_option_emitted_when_explicit(self):
+        @click.command("my-cmd")
+        @click.option("--backend", default="sqlite")
+        def cmd(**kwargs):
+            pass
+
+        ctx = _invoke_and_get_context(cmd, ["--backend", "postgres"])
+        result = build_reproduction_command_from_context(ctx)
+        assert result.argv == ["my-cmd", "--backend", "postgres"]
+
+    def test_string_option_omitted_when_default(self):
+        @click.command("my-cmd")
+        @click.option("--backend", default="sqlite")
+        def cmd(**kwargs):
+            pass
+
+        ctx = _invoke_and_get_context(cmd, [])
+        result = build_reproduction_command_from_context(ctx)
+        assert result.argv == ["my-cmd"]
+
+    def test_multiple_option_repeats_flag(self):
+        @click.command("my-cmd")
+        @click.option("--package-filter", multiple=True)
+        def cmd(**kwargs):
+            pass
+
+        ctx = _invoke_and_get_context(cmd, ["--package-filter", "foo", 
"--package-filter", "bar"])
+        result = build_reproduction_command_from_context(ctx)
+        assert result.argv == ["my-cmd", "--package-filter", "foo", 
"--package-filter", "bar"]
+
+    def test_positional_arguments_appended_at_end(self):
+        @click.command("my-cmd")
+        @click.option("--flag", is_flag=True)
+        @click.argument("files", nargs=-1)
+        def cmd(**kwargs):
+            pass
+
+        ctx = _invoke_and_get_context(cmd, ["--flag", "file1.py", "file2.py"])
+        result = build_reproduction_command_from_context(ctx)
+        assert result.argv == ["my-cmd", "--flag", "file1.py", "file2.py"]
+
+    def test_expose_value_false_option_excluded(self):
+        @click.command("my-cmd")
+        @click.option("--verbose", is_flag=True, expose_value=False)
+        @click.option("--backend", default="sqlite")
+        def cmd(**kwargs):
+            pass
+
+        ctx = _invoke_and_get_context(cmd, ["--verbose", "--backend", 
"postgres"])
+        result = build_reproduction_command_from_context(ctx)
+        assert "--verbose" not in result.argv
+        assert "--backend" in result.argv
+
+    def test_excluded_params_filtered_out(self):
+        @click.command("my-cmd")
+        @click.option("--debug-resources", is_flag=True)
+        @click.option("--backend", default="sqlite")
+        def cmd(**kwargs):
+            pass
+
+        ctx = _invoke_and_get_context(cmd, ["--debug-resources", "--backend", 
"postgres"])
+        result = build_reproduction_command_from_context(ctx)
+        assert "--debug-resources" not in result.argv
+        assert "--backend" in result.argv
+
+    def test_envvar_source_included(self):
+        @click.command("my-cmd")
+        @click.option("--backend", default="sqlite", envvar="BACKEND")
+        def cmd(**kwargs):
+            pass
+
+        ctx = _invoke_and_get_context(cmd, [], env={"BACKEND": "postgres"})
+        result = build_reproduction_command_from_context(ctx)
+        assert result.argv == ["my-cmd", "--backend", "postgres"]
+
+    def test_envvar_same_as_default_still_included(self):
+        """When envvar explicitly sets the same value as default, it should 
still be emitted."""
+
+        @click.command("my-cmd")
+        @click.option("--backend", default="sqlite", envvar="BACKEND")
+        def cmd(**kwargs):
+            pass
+
+        ctx = _invoke_and_get_context(cmd, [], env={"BACKEND": "sqlite"})
+        result = build_reproduction_command_from_context(ctx)
+        assert result.argv == ["my-cmd", "--backend", "sqlite"]
+
+    def test_custom_comment(self):
+        @click.command("my-cmd")
+        def cmd(**kwargs):
+            pass
+
+        ctx = _invoke_and_get_context(cmd, [])
+        result = build_reproduction_command_from_context(ctx, comment="Custom 
comment")
+        assert result.comment == "Custom comment"
+
+    def test_default_comment(self):
+        @click.command("my-cmd")
+        def cmd(**kwargs):
+            pass
+
+        ctx = _invoke_and_get_context(cmd, [])
+        result = build_reproduction_command_from_context(ctx)
+        assert result.comment == "Run the same Breeze command locally"
+
+    def test_subcommand_path(self):
+        @click.group()
+        def grp():
+            pass
+
+        @grp.command("sub-cmd")
+        @click.option("--flag", is_flag=True)
+        def sub_cmd(**kwargs):
+            pass
+
+        captured_ctx = {}
+
+        original_invoke = sub_cmd.invoke
+
+        def patched_invoke(ctx):
+            captured_ctx["ctx"] = ctx
+            return original_invoke(ctx)
+
+        sub_cmd.invoke = patched_invoke
+        runner = click.testing.CliRunner()
+        result = runner.invoke(grp, ["sub-cmd", "--flag"], 
catch_exceptions=False)
+        assert result.exit_code == 0
+        ctx = captured_ctx["ctx"]
+        repro = build_reproduction_command_from_context(ctx)
+        assert repro.argv == ["grp", "sub-cmd", "--flag"]
+
+    def test_integer_option_converted_to_string(self):
+        @click.command("my-cmd")
+        @click.option("--timeout", type=int, default=60)
+        def cmd(**kwargs):
+            pass
+
+        ctx = _invoke_and_get_context(cmd, ["--timeout", "120"])
+        result = build_reproduction_command_from_context(ctx)
+        assert result.argv == ["my-cmd", "--timeout", "120"]
+
+    def test_prefers_long_option_form(self):
+        @click.command("my-cmd")
+        @click.option("-b", "--backend", default="sqlite")
+        def cmd(**kwargs):
+            pass
+
+        ctx = _invoke_and_get_context(cmd, ["-b", "postgres"])
+        result = build_reproduction_command_from_context(ctx)
+        assert result.argv == ["my-cmd", "--backend", "postgres"]

Reply via email to