From 2bbde6597b2f8ad290318513f0b78467b9356e7a Mon Sep 17 00:00:00 2001 From: Quratulain-bilal Date: Sat, 20 Jun 2026 01:36:27 +0500 Subject: [PATCH 1/2] fix(scripts): send check-prerequisites.ps1 errors to stderr The validation errors and run-hints in check-prerequisites.ps1 were written with Write-Output, so they went to stdout. This script is usually run with -Json and its stdout parsed by the agent, so an error (e.g. missing plan.md) leaves the parser with an error string instead of JSON. The bash counterpart already writes these to stderr (>&2), as do the sibling PowerShell scripts (setup-tasks.ps1, common.ps1's Get-FeaturePathsEnv). Switch the six error/hint lines to [Console]::Error.WriteLine so stdout stays clean and the two shells match. --- scripts/powershell/check-prerequisites.ps1 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/powershell/check-prerequisites.ps1 b/scripts/powershell/check-prerequisites.ps1 index bb60e52c85..52469aa19a 100644 --- a/scripts/powershell/check-prerequisites.ps1 +++ b/scripts/powershell/check-prerequisites.ps1 @@ -83,24 +83,24 @@ if ($PathsOnly) { # Validate required directories and files if (-not (Test-Path $paths.FEATURE_DIR -PathType Container)) { - Write-Output "ERROR: Feature directory not found: $($paths.FEATURE_DIR)" + [Console]::Error.WriteLine("ERROR: Feature directory not found: $($paths.FEATURE_DIR)") $specifyCommand = Format-SpecKitCommand -CommandName 'specify' -RepoRoot $paths.REPO_ROOT - Write-Output "Run $specifyCommand first to create the feature structure." + [Console]::Error.WriteLine("Run $specifyCommand first to create the feature structure.") exit 1 } if (-not (Test-Path $paths.IMPL_PLAN -PathType Leaf)) { - Write-Output "ERROR: plan.md not found in $($paths.FEATURE_DIR)" + [Console]::Error.WriteLine("ERROR: plan.md not found in $($paths.FEATURE_DIR)") $planCommand = Format-SpecKitCommand -CommandName 'plan' -RepoRoot $paths.REPO_ROOT - Write-Output "Run $planCommand first to create the implementation plan." + [Console]::Error.WriteLine("Run $planCommand first to create the implementation plan.") exit 1 } # Check for tasks.md if required if ($RequireTasks -and -not (Test-Path $paths.TASKS -PathType Leaf)) { - Write-Output "ERROR: tasks.md not found in $($paths.FEATURE_DIR)" + [Console]::Error.WriteLine("ERROR: tasks.md not found in $($paths.FEATURE_DIR)") $tasksCommand = Format-SpecKitCommand -CommandName 'tasks' -RepoRoot $paths.REPO_ROOT - Write-Output "Run $tasksCommand first to create the task list." + [Console]::Error.WriteLine("Run $tasksCommand first to create the task list.") exit 1 } From 26e1d80ecc26be681df22e4fe0e2f9b5cfe4c709 Mon Sep 17 00:00:00 2001 From: Quratulain-bilal Date: Tue, 23 Jun 2026 23:30:26 +0500 Subject: [PATCH 2/2] test(scripts): assert check-prerequisites errors stay on stderr Per the #3122 bug assessment, tighten the failure-path tests so they verify stdout stays clean (empty / valid JSON) and the error text only appears on stderr, instead of checking the combined stdout+stderr string. Covers all three PowerShell validation paths (missing feature dir, missing plan.md, missing tasks.md with -RequireTasks) and the bash counterpart. The two new error-routing tests fail on the pre-fix script (errors on stdout) and pass after it. --- tests/test_check_prerequisites_paths_only.py | 64 ++++++++++++++++++-- 1 file changed, 60 insertions(+), 4 deletions(-) diff --git a/tests/test_check_prerequisites_paths_only.py b/tests/test_check_prerequisites_paths_only.py index 03e2fc6e8b..c8c2926abc 100644 --- a/tests/test_check_prerequisites_paths_only.py +++ b/tests/test_check_prerequisites_paths_only.py @@ -143,7 +143,11 @@ def test_paths_only_text_mode_on_non_spec_branch(prereq_repo: Path) -> None: @requires_bash def test_normal_mode_still_validates_branch(prereq_repo: Path) -> None: - """Without --paths-only, feature directory validation must still fail on main.""" + """Without --paths-only, feature directory validation must still fail on main. + + The error must go to stderr and stdout must stay clean, so a caller that + parses stdout as JSON is not handed the error string instead (#3122). + """ script = prereq_repo / ".specify" / "scripts" / "bash" / "check-prerequisites.sh" result = subprocess.run( ["bash", str(script), "--json"], @@ -155,6 +159,8 @@ def test_normal_mode_still_validates_branch(prereq_repo: Path) -> None: ) assert result.returncode != 0 assert "Feature directory not found" in result.stderr + assert "Feature directory not found" not in result.stdout + assert result.stdout.strip() == "" # ── PowerShell tests ────────────────────────────────────────────────────── @@ -213,7 +219,33 @@ def test_ps_paths_only_succeeds_on_spec_branch(prereq_repo: Path) -> None: @pytest.mark.skipif(not (HAS_PWSH or _WINDOWS_POWERSHELL), reason="no PowerShell available") def test_ps_normal_mode_still_validates_branch(prereq_repo: Path) -> None: - """Without -PathsOnly, feature directory validation must still fail on main.""" + """Without -PathsOnly, feature directory validation must still fail on main. + + The error must land on stderr only, leaving stdout clean for -Json + callers that parse it as JSON (#3122). + """ + script = prereq_repo / ".specify" / "scripts" / "powershell" / "check-prerequisites.ps1" + exe = "pwsh" if HAS_PWSH else _WINDOWS_POWERSHELL + result = subprocess.run( + [exe, "-NoProfile", "-File", str(script), "-Json"], + cwd=prereq_repo, + capture_output=True, + text=True, + check=False, + env=_clean_env(), + ) + assert result.returncode != 0 + assert "Feature directory not found" in result.stderr + assert "Feature directory not found" not in result.stdout + assert result.stdout.strip() == "" + + +@pytest.mark.skipif(not (HAS_PWSH or _WINDOWS_POWERSHELL), reason="no PowerShell available") +def test_ps_missing_plan_error_goes_to_stderr(prereq_repo: Path) -> None: + """A missing plan.md must report on stderr, not stdout (#3122).""" + feat = prereq_repo / "specs" / "001-my-feature" + feat.mkdir(parents=True, exist_ok=True) + _write_feature_json(prereq_repo) script = prereq_repo / ".specify" / "scripts" / "powershell" / "check-prerequisites.ps1" exe = "pwsh" if HAS_PWSH else _WINDOWS_POWERSHELL result = subprocess.run( @@ -225,5 +257,29 @@ def test_ps_normal_mode_still_validates_branch(prereq_repo: Path) -> None: env=_clean_env(), ) assert result.returncode != 0 - combined = result.stdout + result.stderr - assert "Feature directory not found" in combined + assert "plan.md not found" in result.stderr + assert "plan.md not found" not in result.stdout + assert result.stdout.strip() == "" + + +@pytest.mark.skipif(not (HAS_PWSH or _WINDOWS_POWERSHELL), reason="no PowerShell available") +def test_ps_missing_tasks_error_goes_to_stderr(prereq_repo: Path) -> None: + """With -RequireTasks, a missing tasks.md must report on stderr only (#3122).""" + feat = prereq_repo / "specs" / "001-my-feature" + feat.mkdir(parents=True, exist_ok=True) + (feat / "plan.md").write_text("# plan\n", encoding="utf-8") + _write_feature_json(prereq_repo) + script = prereq_repo / ".specify" / "scripts" / "powershell" / "check-prerequisites.ps1" + exe = "pwsh" if HAS_PWSH else _WINDOWS_POWERSHELL + result = subprocess.run( + [exe, "-NoProfile", "-File", str(script), "-Json", "-RequireTasks"], + cwd=prereq_repo, + capture_output=True, + text=True, + check=False, + env=_clean_env(), + ) + assert result.returncode != 0 + assert "tasks.md not found" in result.stderr + assert "tasks.md not found" not in result.stdout + assert result.stdout.strip() == ""