diff --git a/scripts/python/setup_tasks.py b/scripts/python/setup_tasks.py index b3abb6dc1a..21b0018620 100644 --- a/scripts/python/setup_tasks.py +++ b/scripts/python/setup_tasks.py @@ -55,14 +55,31 @@ def _available_docs(paths: FeaturePaths) -> list[str]: return docs +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 mid-listing. + "[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 main(argv: list[str] | None = None) -> int: diff --git a/tests/test_setup_tasks_python_parity.py b/tests/test_setup_tasks_python_parity.py index 29d0e2b5aa..afa303b6bd 100644 --- a/tests/test_setup_tasks_python_parity.py +++ b/tests/test_setup_tasks_python_parity.py @@ -205,3 +205,28 @@ def test_missing_template_error_matches_all_variants(repo: Path) -> None: assert bash.returncode == ps.returncode == py.returncode == 1 assert bash.stdout == ps.stdout == py.stdout == "" assert bash.stderr == ps.stderr == py.stderr + + +def test_python_text_output_survives_a_legacy_stdout_code_page(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 document listing. The ASCII fallback is + the rendering these markers already have in-tree (Test-FileExists in + scripts/powershell/common.ps1, and normalize_status_text). + """ + feature = repo / "specs" / "001-my-feature" + (feature / "research.md").write_text("# research\n", encoding="utf-8") + (feature / "contracts").mkdir() + + env = clean_env() + env["PYTHONIOENCODING"] = "cp1252" + result = run(py_cmd(repo, SCRIPT), repo, env=env) + + assert result.returncode == 0, result.stderr + assert "UnicodeEncodeError" not in result.stderr + for doc in ("research.md", "data-model.md", "contracts/", "quickstart.md"): + assert doc in result.stdout, (doc, result.stdout) + assert "[OK] research.md" in normalize_status_text(result.stdout), result.stdout