diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 index 2f23283fc4..2835c3ee4e 100644 --- a/scripts/powershell/create-new-feature.ps1 +++ b/scripts/powershell/create-new-feature.ps1 @@ -350,7 +350,10 @@ if (-not $DryRun) { if (-not (Test-Path -PathType Leaf $specFile)) { $template = Resolve-Template -TemplateName 'spec-template' -RepoRoot $repoRoot if ($template -and (Test-Path $template)) { - Copy-Item $template $specFile -Force + # Read the template content and write it to the spec file with UTF-8 encoding without BOM + $content = [System.IO.File]::ReadAllText($template) + $utf8NoBom = New-Object System.Text.UTF8Encoding($false) + [System.IO.File]::WriteAllText($specFile, $content, $utf8NoBom) } else { New-Item -ItemType File -Path $specFile -Force | Out-Null } diff --git a/scripts/powershell/setup-plan.ps1 b/scripts/powershell/setup-plan.ps1 index ee09094bf7..2881c9fc8a 100644 --- a/scripts/powershell/setup-plan.ps1 +++ b/scripts/powershell/setup-plan.ps1 @@ -34,8 +34,10 @@ New-Item -ItemType Directory -Path $paths.FEATURE_DIR -Force | Out-Null # Copy plan template if it exists, otherwise note it or create empty file $template = Resolve-Template -TemplateName 'plan-template' -RepoRoot $paths.REPO_ROOT if ($template -and (Test-Path $template)) { - Copy-Item $template $paths.IMPL_PLAN -Force - Write-Output "Copied plan template to $($paths.IMPL_PLAN)" + # Read the template content and write it to the implementation plan file with UTF-8 encoding without BOM + $content = [System.IO.File]::ReadAllText($template) + $utf8NoBom = New-Object System.Text.UTF8Encoding($false) + [System.IO.File]::WriteAllText($paths.IMPL_PLAN, $content, $utf8NoBom) } else { Write-Warning "Plan template not found" # Create a basic plan file if template doesn't exist diff --git a/tests/test_timestamp_branches.py b/tests/test_timestamp_branches.py index 39228d9455..1562c7bcd2 100644 --- a/tests/test_timestamp_branches.py +++ b/tests/test_timestamp_branches.py @@ -381,29 +381,6 @@ def test_bash_specify_feature_prefixed_resolves_by_prefix(self, tmp_path: Path): assert result.returncode == 0, result.stderr assert result.stdout.strip() == str(tmp_path / "specs" / "001-target-spec") - @pytest.mark.skipif(not _has_pwsh(), reason="pwsh not installed") - def test_ps_specify_feature_prefixed_resolves_by_prefix(self, git_repo: Path): - """PowerShell Get-FeaturePathsEnv: same prefix stripping as bash.""" - common_ps = PROJECT_ROOT / "scripts" / "powershell" / "common.ps1" - spec_dir = git_repo / "specs" / "001-ps-prefix-spec" - spec_dir.mkdir(parents=True) - ps_cmd = f'. "{common_ps}"; $r = Get-FeaturePathsEnv; Write-Output "FEATURE_DIR=$($r.FEATURE_DIR)"' - result = subprocess.run( - ["pwsh", "-NoProfile", "-Command", ps_cmd], - cwd=git_repo, - capture_output=True, - text=True, - env={**os.environ, "SPECIFY_FEATURE": "feat/001-other"}, - ) - assert result.returncode == 0, result.stderr - for line in result.stdout.splitlines(): - if line.startswith("FEATURE_DIR="): - val = line.split("=", 1)[1].strip() - assert val == str(spec_dir) - break - else: - pytest.fail("FEATURE_DIR not found in PowerShell output") - # ── get_current_branch Tests ───────────────────────────────────────────────── @@ -650,6 +627,25 @@ def test_powershell_surfaces_checkout_errors(self): assert "$switchBranchError = git checkout -q $branchName 2>&1 | Out-String" in contents assert "exists but could not be checked out.`n$($switchBranchError.Trim())" in contents + @pytest.mark.skipif(not _has_pwsh(), reason="pwsh not installed") + def test_ps_spec_file_written_without_bom(self, ps_git_repo: Path): + """spec.md generated by create-new-feature.ps1 must not contain a UTF-8 BOM.""" + result = run_ps_script( + ps_git_repo, "-ShortName", "bom-check", "BOM check feature" + ) + assert result.returncode == 0, result.stderr + + spec_file = next((ps_git_repo / "specs").rglob("spec.md"), None) + assert spec_file is not None, ( + f"spec.md was not created.\nstdout: {result.stdout}\nstderr: {result.stderr}" + ) + + with open(spec_file, "rb") as f: + raw_bytes = f.read(3) + assert raw_bytes != b"\xef\xbb\xbf", ( + f"spec.md must not start with a UTF-8 BOM — got first 3 bytes: {raw_bytes!r}" + ) + class TestGitExtensionParity: def test_bash_extension_surfaces_checkout_errors(self): @@ -1256,4 +1252,4 @@ def test_ps_feature_json_overrides_branch_lookup(self, git_repo: Path): assert val == str(custom_dir) break else: - pytest.fail("FEATURE_DIR not found in PowerShell output") + pytest.fail("FEATURE_DIR not found in PowerShell output") \ No newline at end of file