diff --git a/scripts/bash/common.sh b/scripts/bash/common.sh index 416fcadfc..5e45e8708 100644 --- a/scripts/bash/common.sh +++ b/scripts/bash/common.sh @@ -78,7 +78,7 @@ get_current_branch() { latest_timestamp="$ts" latest_feature=$dirname fi - elif [[ "$dirname" =~ ^([0-9]{3})- ]]; then + elif [[ "$dirname" =~ ^([0-9]{3,})- ]]; then local number=${BASH_REMATCH[1]} number=$((10#$number)) if [[ "$number" -gt "$highest" ]]; then @@ -124,9 +124,15 @@ check_feature_branch() { return 0 fi - if [[ ! "$branch" =~ ^[0-9]{3}- ]] && [[ ! "$branch" =~ ^[0-9]{8}-[0-9]{6}- ]]; then + # Accept sequential prefix (3+ digits) but exclude malformed timestamps + # Malformed: 7-or-8 digit date + 6-digit time with no trailing slug (e.g. "2026031-143022" or "20260319-143022") + local is_sequential=false + if [[ "$branch" =~ ^[0-9]{3,}- ]] && [[ ! "$branch" =~ ^[0-9]{7}-[0-9]{6}- ]] && [[ ! "$branch" =~ ^[0-9]{7,8}-[0-9]{6}$ ]]; then + is_sequential=true + fi + if [[ "$is_sequential" != "true" ]] && [[ ! "$branch" =~ ^[0-9]{8}-[0-9]{6}- ]]; then echo "ERROR: Not on a feature branch. Current branch: $branch" >&2 - echo "Feature branches should be named like: 001-feature-name or 20260319-143022-feature-name" >&2 + echo "Feature branches should be named like: 001-feature-name, 1234-feature-name, or 20260319-143022-feature-name" >&2 return 1 fi @@ -146,7 +152,7 @@ find_feature_dir_by_prefix() { local prefix="" if [[ "$branch_name" =~ ^([0-9]{8}-[0-9]{6})- ]]; then prefix="${BASH_REMATCH[1]}" - elif [[ "$branch_name" =~ ^([0-9]{3})- ]]; then + elif [[ "$branch_name" =~ ^([0-9]{3,})- ]]; then prefix="${BASH_REMATCH[1]}" else # If branch doesn't have a recognized prefix, fall back to exact match diff --git a/scripts/powershell/common.ps1 b/scripts/powershell/common.ps1 index 7a96d3fac..8c8c801ee 100644 --- a/scripts/powershell/common.ps1 +++ b/scripts/powershell/common.ps1 @@ -83,8 +83,8 @@ function Get-CurrentBranch { $latestTimestamp = $ts $latestFeature = $_.Name } - } elseif ($_.Name -match '^(\d{3})-') { - $num = [int]$matches[1] + } elseif ($_.Name -match '^(\d{3,})-') { + $num = [long]$matches[1] if ($num -gt $highest) { $highest = $num # Only update if no timestamp branch found yet @@ -139,9 +139,13 @@ function Test-FeatureBranch { return $true } - if ($Branch -notmatch '^[0-9]{3}-' -and $Branch -notmatch '^\d{8}-\d{6}-') { + # Accept sequential prefix (3+ digits) but exclude malformed timestamps + # Malformed: 7-or-8 digit date + 6-digit time with no trailing slug (e.g. "2026031-143022" or "20260319-143022") + $hasMalformedTimestamp = ($Branch -match '^[0-9]{7}-[0-9]{6}-') -or ($Branch -match '^(?:\d{7}|\d{8})-\d{6}$') + $isSequential = ($Branch -match '^[0-9]{3,}-') -and (-not $hasMalformedTimestamp) + if (-not $isSequential -and $Branch -notmatch '^\d{8}-\d{6}-') { Write-Output "ERROR: Not on a feature branch. Current branch: $Branch" - Write-Output "Feature branches should be named like: 001-feature-name or 20260319-143022-feature-name" + Write-Output "Feature branches should be named like: 001-feature-name, 1234-feature-name, or 20260319-143022-feature-name" return $false } return $true diff --git a/tests/test_timestamp_branches.py b/tests/test_timestamp_branches.py index 0c9eb07b4..9e36f1756 100644 --- a/tests/test_timestamp_branches.py +++ b/tests/test_timestamp_branches.py @@ -186,11 +186,26 @@ class TestCheckFeatureBranch: result = source_and_call('check_feature_branch "main" "true"') assert result.returncode != 0 + def test_accepts_four_digit_sequential_branch(self): + """check_feature_branch accepts 4+ digit sequential branch.""" + result = source_and_call('check_feature_branch "1234-feat" "true"') + assert result.returncode == 0 + def test_rejects_partial_timestamp(self): """Test 9: check_feature_branch rejects 7-digit date.""" result = source_and_call('check_feature_branch "2026031-143022-feat" "true"') assert result.returncode != 0 + def test_rejects_timestamp_without_slug(self): + """check_feature_branch rejects timestamp-like branch missing trailing slug.""" + result = source_and_call('check_feature_branch "20260319-143022" "true"') + assert result.returncode != 0 + + def test_rejects_7digit_timestamp_without_slug(self): + """check_feature_branch rejects 7-digit date + 6-digit time without slug.""" + result = source_and_call('check_feature_branch "2026031-143022" "true"') + assert result.returncode != 0 + # ── find_feature_dir_by_prefix Tests ───────────────────────────────────────── @@ -214,6 +229,15 @@ class TestFindFeatureDirByPrefix: assert result.returncode == 0 assert result.stdout.strip() == f"{tmp_path}/specs/20260319-143022-original-feat" + def test_four_digit_sequential_prefix(self, tmp_path: Path): + """find_feature_dir_by_prefix resolves 4+ digit sequential prefix.""" + (tmp_path / "specs" / "1000-original-feat").mkdir(parents=True) + result = source_and_call( + f'find_feature_dir_by_prefix "{tmp_path}" "1000-different-name"' + ) + assert result.returncode == 0 + assert result.stdout.strip() == f"{tmp_path}/specs/1000-original-feat" + # ── get_current_branch Tests ─────────────────────────────────────────────────