From 21e18cfeae6e0e5313c4d0e07fd99660cc590fd4 Mon Sep 17 00:00:00 2001 From: jawwad-ali Date: Wed, 24 Jun 2026 01:12:43 +0500 Subject: [PATCH 1/2] fix(scripts): keep PowerShell branch-name acronym match case-sensitive Get-BranchName keeps a sub-3-character word only when it appears as an UPPERCASE acronym in the description. The bash twin checks this case-sensitively (grep "\b${word^^}\b" / grep -qw -- "${word^^}"), but the PowerShell twin used -match, which is case-INSENSITIVE, so it kept EVERY short word regardless of case -- contradicting its own comment and diverging from bash. The same description then produced different spec-directory and branch names on Windows/PowerShell vs macOS/Linux (e.g. "Add go support" -> 001-go-support instead of 001-support), desyncing specs/, feature.json, and git branches across a mixed-OS team. Use the case-sensitive -cmatch so a short word is kept only for a genuine uppercase acronym, matching bash. Applied to both the core scripts/powershell/create-new-feature.ps1 and the git extension's create-new-feature-branch.ps1. Add bash + PowerShell regression tests (core and git-extension) asserting a lowercase short word is dropped while an uppercase acronym is kept. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../powershell/create-new-feature-branch.ps1 | 5 ++- scripts/powershell/create-new-feature.ps1 | 7 ++-- tests/extensions/git/test_git_extension.py | 33 +++++++++++++++++++ tests/test_timestamp_branches.py | 30 +++++++++++++++++ 4 files changed, 72 insertions(+), 3 deletions(-) diff --git a/extensions/git/scripts/powershell/create-new-feature-branch.ps1 b/extensions/git/scripts/powershell/create-new-feature-branch.ps1 index 65358df0ba..6a4417f8b9 100644 --- a/extensions/git/scripts/powershell/create-new-feature-branch.ps1 +++ b/extensions/git/scripts/powershell/create-new-feature-branch.ps1 @@ -252,7 +252,10 @@ function Get-BranchName { if ($stopWords -contains $word) { continue } if ($word.Length -ge 3) { $meaningfulWords += $word - } elseif ($Description -match "\b$($word.ToUpper())\b") { + } elseif ($Description -cmatch "\b$($word.ToUpper())\b") { + # Case-sensitive (-cmatch) to mirror the bash twin's `grep -qw -- "${word^^}"`: + # keep a short word only when its UPPERCASE form appears in the original + # (an acronym). -match is case-insensitive and would keep every short word. $meaningfulWords += $word } } diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 index 8627caa6e7..8ad0ed822a 100644 --- a/scripts/powershell/create-new-feature.ps1 +++ b/scripts/powershell/create-new-feature.ps1 @@ -111,8 +111,11 @@ function Get-BranchName { # Keep words that are length >= 3 OR appear as uppercase in original (likely acronyms) if ($word.Length -ge 3) { $meaningfulWords += $word - } elseif ($Description -match "\b$($word.ToUpper())\b") { - # Keep short words if they appear as uppercase in original (likely acronyms) + } elseif ($Description -cmatch "\b$($word.ToUpper())\b") { + # Keep short words if they appear as uppercase in original (likely acronyms). + # Use -cmatch (case-sensitive) so this matches the bash twin's + # case-sensitive `grep "\b${word^^}\b"`; -match would also match the + # lowercased word and keep every short word regardless of case. $meaningfulWords += $word } } diff --git a/tests/extensions/git/test_git_extension.py b/tests/extensions/git/test_git_extension.py index 3d40aef4ee..a7167f3179 100644 --- a/tests/extensions/git/test_git_extension.py +++ b/tests/extensions/git/test_git_extension.py @@ -298,6 +298,24 @@ def test_creates_branch_sequential(self, tmp_path: Path): assert data["BRANCH_NAME"] == "001-user-auth" assert data["FEATURE_NUM"] == "001" + def test_branch_name_short_word_case_sensitivity(self, tmp_path: Path): + """A short word is dropped from the derived branch name unless it appears + as an UPPERCASE acronym in the description (case-sensitive, must match the + PowerShell twin).""" + project = _setup_project(tmp_path) + # lowercase "go" (<3 chars, not an uppercase acronym) is dropped + r1 = _run_bash( + "create-new-feature-branch.sh", project, "--json", "--dry-run", "Add go support", + ) + assert r1.returncode == 0, r1.stderr + assert json.loads(r1.stdout)["BRANCH_NAME"] == "001-support" + # uppercase "GO" is kept as an acronym + r2 = _run_bash( + "create-new-feature-branch.sh", project, "--json", "--dry-run", "Use GO now", + ) + assert r2.returncode == 0, r2.stderr + assert json.loads(r2.stdout)["BRANCH_NAME"] == "001-use-go-now" + def test_creates_branch_timestamp(self, tmp_path: Path): """Extension create-new-feature-branch.sh creates timestamp branch.""" project = _setup_project(tmp_path) @@ -426,6 +444,21 @@ def test_creates_branch_sequential(self, tmp_path: Path): data = json.loads(result.stdout) assert data["BRANCH_NAME"] == "001-user-auth" + def test_branch_name_short_word_case_sensitivity(self, tmp_path: Path): + """PowerShell must match the bash twin: a short word is dropped unless it + appears as an UPPERCASE acronym (case-sensitive -cmatch, not -match).""" + project = _setup_project(tmp_path) + r1 = _run_pwsh( + "create-new-feature-branch.ps1", project, "-Json", "-DryRun", "Add go support", + ) + assert r1.returncode == 0, r1.stderr + assert json.loads(r1.stdout)["BRANCH_NAME"] == "001-support" + r2 = _run_pwsh( + "create-new-feature-branch.ps1", project, "-Json", "-DryRun", "Use GO now", + ) + assert r2.returncode == 0, r2.stderr + assert json.loads(r2.stdout)["BRANCH_NAME"] == "001-use-go-now" + def test_dry_run_counts_branches_checked_out_in_worktrees(self, tmp_path: Path): """Branches checked out in sibling worktrees still reserve their prefix.""" project = _setup_project(tmp_path / "project") diff --git a/tests/test_timestamp_branches.py b/tests/test_timestamp_branches.py index 1856afb972..7a7d334c19 100644 --- a/tests/test_timestamp_branches.py +++ b/tests/test_timestamp_branches.py @@ -240,6 +240,17 @@ def test_sequential_default_with_existing_specs(self, git_repo: Path): assert branch is not None assert re.match(r"^\d{3,}-new-feat$", branch), f"unexpected branch: {branch}" + def test_branch_name_short_word_case_sensitivity(self, git_repo: Path): + """A short word is dropped from the derived branch name unless it appears + as an UPPERCASE acronym in the description. The PowerShell twin must use + case-sensitive -cmatch to produce the same result.""" + r1 = run_script(git_repo, "--json", "--dry-run", "Add go support") + assert r1.returncode == 0, r1.stderr + assert json.loads(r1.stdout)["BRANCH_NAME"] == "001-support" + r2 = run_script(git_repo, "--json", "--dry-run", "Use GO now") + assert r2.returncode == 0, r2.stderr + assert json.loads(r2.stdout)["BRANCH_NAME"] == "001-use-go-now" + def test_sequential_ignores_timestamp_dirs(self, git_repo: Path): """Sequential numbering skips timestamp dirs when computing next number.""" (git_repo / "specs" / "002-first-feat").mkdir(parents=True) @@ -272,6 +283,25 @@ def test_powershell_scanner_uses_long_tryparse_for_large_prefixes(self): assert "[long]::TryParse($matches[1], [ref]$num)" in content assert "$num = [int]$matches[1]" not in content + @pytest.mark.skipif(not _has_pwsh(), reason="pwsh not installed") + def test_branch_name_short_word_case_sensitivity(self, ps_git_repo: Path): + """Core create-new-feature.ps1 must drop a short word unless it appears as + an UPPERCASE acronym (case-sensitive -cmatch), matching the bash twin.""" + script = ps_git_repo / "scripts" / "powershell" / "create-new-feature.ps1" + + def _run(desc: str) -> subprocess.CompletedProcess: + return subprocess.run( + ["pwsh", "-NoProfile", "-File", str(script), "-Json", "-DryRun", desc], + cwd=ps_git_repo, capture_output=True, text=True, + ) + + r1 = _run("Add go support") + assert r1.returncode == 0, r1.stderr + assert json.loads(r1.stdout)["BRANCH_NAME"] == "001-support" + r2 = _run("Use GO now") + assert r2.returncode == 0, r2.stderr + assert json.loads(r2.stdout)["BRANCH_NAME"] == "001-use-go-now" + # ── check_feature_branch Tests ─────────────────────────────────────────────── From 1c450059d4418c141d2899d732864b7e4541389e Mon Sep 17 00:00:00 2001 From: jawwad-ali Date: Wed, 24 Jun 2026 18:20:02 +0500 Subject: [PATCH 2/2] test: fix article grammar in branch-name docstrings Address review: 'an UPPERCASE acronym' -> 'an acronym in UPPERCASE' across the four branch-name case-sensitivity test docstrings (the indefinite article reads cleanly before 'acronym'). Docstring-only; no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/extensions/git/test_git_extension.py | 4 ++-- tests/test_timestamp_branches.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/extensions/git/test_git_extension.py b/tests/extensions/git/test_git_extension.py index a7167f3179..2017dae627 100644 --- a/tests/extensions/git/test_git_extension.py +++ b/tests/extensions/git/test_git_extension.py @@ -300,7 +300,7 @@ def test_creates_branch_sequential(self, tmp_path: Path): def test_branch_name_short_word_case_sensitivity(self, tmp_path: Path): """A short word is dropped from the derived branch name unless it appears - as an UPPERCASE acronym in the description (case-sensitive, must match the + as an acronym in UPPERCASE in the description (case-sensitive, must match the PowerShell twin).""" project = _setup_project(tmp_path) # lowercase "go" (<3 chars, not an uppercase acronym) is dropped @@ -446,7 +446,7 @@ def test_creates_branch_sequential(self, tmp_path: Path): def test_branch_name_short_word_case_sensitivity(self, tmp_path: Path): """PowerShell must match the bash twin: a short word is dropped unless it - appears as an UPPERCASE acronym (case-sensitive -cmatch, not -match).""" + appears as an acronym in UPPERCASE (case-sensitive -cmatch, not -match).""" project = _setup_project(tmp_path) r1 = _run_pwsh( "create-new-feature-branch.ps1", project, "-Json", "-DryRun", "Add go support", diff --git a/tests/test_timestamp_branches.py b/tests/test_timestamp_branches.py index 7a7d334c19..14032030ed 100644 --- a/tests/test_timestamp_branches.py +++ b/tests/test_timestamp_branches.py @@ -242,7 +242,7 @@ def test_sequential_default_with_existing_specs(self, git_repo: Path): def test_branch_name_short_word_case_sensitivity(self, git_repo: Path): """A short word is dropped from the derived branch name unless it appears - as an UPPERCASE acronym in the description. The PowerShell twin must use + as an acronym in UPPERCASE in the description. The PowerShell twin must use case-sensitive -cmatch to produce the same result.""" r1 = run_script(git_repo, "--json", "--dry-run", "Add go support") assert r1.returncode == 0, r1.stderr @@ -286,7 +286,7 @@ def test_powershell_scanner_uses_long_tryparse_for_large_prefixes(self): @pytest.mark.skipif(not _has_pwsh(), reason="pwsh not installed") def test_branch_name_short_word_case_sensitivity(self, ps_git_repo: Path): """Core create-new-feature.ps1 must drop a short word unless it appears as - an UPPERCASE acronym (case-sensitive -cmatch), matching the bash twin.""" + an acronym in UPPERCASE (case-sensitive -cmatch), matching the bash twin.""" script = ps_git_repo / "scripts" / "powershell" / "create-new-feature.ps1" def _run(desc: str) -> subprocess.CompletedProcess: