diff --git a/scripts/python/check_prerequisites.py b/scripts/python/check_prerequisites.py index 50c31cb513..e909ffb507 100644 --- a/scripts/python/check_prerequisites.py +++ b/scripts/python/check_prerequisites.py @@ -130,14 +130,31 @@ def _print_paths_only(paths: FeaturePaths, json_mode: bool) -> None: print(f"TASKS: {paths.tasks}") +def _status_marker(ok: bool) -> str: + """Return the status glyph, downgraded to ASCII when stdout cannot encode it. + + On Windows sys.stdout falls back to the ANSI code page whenever it is not a + console - a pipe or a file redirect, which is how agents and workflow steps + invoke these scripts - and U+2713 is unencodable in cp1252, so printing it + raised UnicodeEncodeError and aborted the report right after + "AVAILABLE_DOCS:". "[OK]"/"[FAIL]" is the ASCII rendering these markers + already have in-tree: see Test-FileExists in scripts/powershell/common.ps1 + and normalize_status_text in tests/parity_helpers.py. + """ + glyph = "✓" if ok else "✗" + try: + glyph.encode(getattr(sys.stdout, "encoding", None) or "utf-8") + except (LookupError, UnicodeEncodeError): + return "[OK]" if ok else "[FAIL]" + return glyph + + def _check_file(path: Path, description: str) -> None: - marker = "✓" if path.is_file() else "✗" - print(f" {marker} {description}") + print(f" {_status_marker(path.is_file())} {description}") def _check_dir(path: Path, description: str) -> None: - marker = "✓" if _dir_has_entries(path) else "✗" - print(f" {marker} {description}") + print(f" {_status_marker(_dir_has_entries(path))} {description}") def _print_text_results(paths: FeaturePaths, include_tasks: bool) -> None: diff --git a/tests/test_check_prerequisites_python_parity.py b/tests/test_check_prerequisites_python_parity.py index 9868e99c27..0ecefa7d9b 100644 --- a/tests/test_check_prerequisites_python_parity.py +++ b/tests/test_check_prerequisites_python_parity.py @@ -181,6 +181,45 @@ def test_python_text_output_matches_bash(prereq_repo: Path) -> None: assert _normalize_status_text(py.stdout) == _normalize_status_text(bash.stdout) +def test_python_text_output_survives_a_legacy_stdout_code_page( + prereq_repo: Path, +) -> None: + """Text mode must not crash when stdout cannot encode the status glyphs. + + On Windows sys.stdout falls back to the ANSI code page whenever it is not a + console — which is every time an agent or a workflow step captures the + output. U+2713 is unencodable in cp1252, so printing it raised + UnicodeEncodeError and truncated the report right after "AVAILABLE_DOCS:". + The ASCII fallback is the rendering these markers already have in-tree + (Test-FileExists in scripts/powershell/common.ps1, and + normalize_status_text here). + """ + feat = prereq_repo / "specs" / "001-my-feature" + feat.mkdir(parents=True) + (feat / "plan.md").write_text("# plan\n", encoding="utf-8") + (feat / "contracts").mkdir() + _write_feature_json(prereq_repo) + + env = _clean_env() + env["PYTHONIOENCODING"] = "cp1252" + result = _run(_py_cmd(prereq_repo, "--include-tasks"), prereq_repo, env=env) + + assert result.returncode == 0, result.stderr + assert "UnicodeEncodeError" not in result.stderr + assert "AVAILABLE_DOCS:" in result.stdout + # Every per-document line must still be there, not truncated away by the + # encode error, and rendered with the ASCII markers. + for doc in ( + "research.md", + "data-model.md", + "contracts/", + "quickstart.md", + "tasks.md", + ): + assert doc in result.stdout, (doc, result.stdout) + assert "[OK]" in result.stdout or "[FAIL]" in result.stdout, result.stdout + + @requires_bash def test_python_help_output_matches_bash(prereq_repo: Path) -> None: bash = _run(_bash_cmd(prereq_repo, "--help"), prereq_repo)