From 685207dc1fcf06f2c9571be29d3d12fc82df3296 Mon Sep 17 00:00:00 2001 From: jawwad-ali Date: Sat, 15 Aug 2026 19:09:15 +0500 Subject: [PATCH] fix(powershell): stop create-new-feature crashing on a non-Latin description MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Get-BranchName's fallback assumed the pipeline yields at least one element: $fallbackWords = ($result -split '-') | Where-Object { $_ } | ... return [string]::Join('-', $fallbackWords) ConvertTo-CleanBranchName blanks every non-[a-z0-9] character, so a description written in any non-Latin script leaves nothing for the pipeline to emit. It yields $null, [string]::Join throws ArgumentNullException, and $ErrorActionPreference = 'Stop' makes that terminating — the script dies with a .NET stack trace, empty stdout, exit 1. Measured with all three twins installed in one project: desc='добавить авторизацию' bash rc=0 BRANCH_NAME='001-' py rc=0 BRANCH_NAME='001-' ps rc=1 Exception calling "Join" ... Identical for 添加用户认证 and '!!! ??? ***'. This hits every PowerShell user who phrases a feature in their own language. Wrap the pipeline in @() so it stays an array; Join on an empty array returns "", matching the twins. Verified the normal case is unchanged ('a-to-the-of' -> 'a-to-the', 'add user authentication' -> '001-user-authentication'), and the file stays ASCII-only (0 non-ASCII bytes) for tests/test_ps1_encoding.py. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/powershell/create-new-feature.ps1 | 9 +++- .../test_create_new_feature_python_parity.py | 53 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 index e7a68c4076..216cca5e20 100644 --- a/scripts/powershell/create-new-feature.ps1 +++ b/scripts/powershell/create-new-feature.ps1 @@ -162,7 +162,14 @@ function Get-BranchName { } else { # Fallback to original logic if no meaningful words found $result = ConvertTo-CleanBranchName -Name $Description - $fallbackWords = ($result -split '-') | Where-Object { $_ } | Select-Object -First 3 + # @() keeps this an array. ConvertTo-CleanBranchName blanks every + # non-[a-z0-9] character, so a description written in a non-Latin script + # (or made only of punctuation) leaves nothing for the pipeline to + # emit -- it yields $null, and [string]::Join on $null throws + # ArgumentNullException. With $ErrorActionPreference = 'Stop' that is + # terminating, so the script died with a .NET stack trace and exit 1 + # where the bash and Python twins both return an empty suffix. + $fallbackWords = @(($result -split '-') | Where-Object { $_ } | Select-Object -First 3) return [string]::Join('-', $fallbackWords) } } diff --git a/tests/test_create_new_feature_python_parity.py b/tests/test_create_new_feature_python_parity.py index 41122b1f5f..74d071ad5f 100644 --- a/tests/test_create_new_feature_python_parity.py +++ b/tests/test_create_new_feature_python_parity.py @@ -1065,3 +1065,56 @@ def test_all_variants_corrected_prefix_skips_timestamp_collision(repo: Path) -> assert json_stdout(py)["FEATURE_NUM"] == "20260320" for result in (bash, ps, py): assert "using 20260320 instead" in result.stderr + + +@pytest.mark.skipif(not HAS_POWERSHELL, reason="no PowerShell available") +@pytest.mark.parametrize( + "description", + ["!!! ??? ***", "добавить", "添加用户"], + ids=["punctuation_only", "cyrillic", "han"], +) +def test_powershell_survives_description_with_no_ascii_words( + tmp_path: Path, description: str +): + """A description with no [a-z0-9] characters must not crash the PS twin. + + ``ConvertTo-CleanBranchName`` blanks every non-ASCII character, so the + fallback pipeline yields nothing and ``[string]::Join`` received ``$null`` + — an ArgumentNullException, made terminating by + ``$ErrorActionPreference = 'Stop'``. The script died with a .NET stack + trace and exit 1 where the bash and Python twins both return an empty + suffix. This fires for any feature phrased in a non-Latin script. + """ + repo = _setup_repo(tmp_path) + + ps = run(ps_cmd(repo, SCRIPT, "-Json", "-DryRun", description), repo) + + assert ps.returncode == 0, ps.stderr + assert "ArgumentNullException" not in ps.stderr + assert "Join" not in ps.stderr + assert json_stdout(ps)["BRANCH_NAME"] == "001-" + + +@requires_bash +@pytest.mark.skipif(not HAS_POWERSHELL, reason="no PowerShell available") +def test_no_ascii_word_description_matches_across_twins(tmp_path: Path): + """All three twins agree on the branch name for such a description.""" + description = "добавить" + + bash_repo = _setup_repo(tmp_path, "b") + py_repo = _setup_repo(tmp_path, "p") + ps_repo = _setup_repo(tmp_path, "s") + + bash = run(bash_cmd(bash_repo, SCRIPT, "--json", "--dry-run", description), bash_repo) + py = run(py_cmd(py_repo, SCRIPT, "--json", "--dry-run", description), py_repo) + ps = run(ps_cmd(ps_repo, SCRIPT, "-Json", "-DryRun", description), ps_repo) + + assert bash.returncode == py.returncode == ps.returncode == 0, ( + bash.stderr, py.stderr, ps.stderr, + ) + names = { + json_stdout(bash)["BRANCH_NAME"], + json_stdout(py)["BRANCH_NAME"], + json_stdout(ps)["BRANCH_NAME"], + } + assert names == {"001-"}, names