Skip to content

Commit 3dff6f1

Browse files
jawwad-aliclaude
andauthored
fix(scripts): stop check-prerequisites text mode crashing on a legacy stdout code page (#3890)
* fix(scripts): stop check-prerequisites text mode crashing on a legacy code page _check_file/_check_dir hard-code U+2713/U+2717 and print() them to sys.stdout. On Windows sys.stdout falls back to the ANSI code page whenever stdout is not a console — which is every time an agent or a workflow step captures the output — and U+2713 is unencodable in cp1252: stdout encoding: cp1252 UnicodeEncodeError: 'charmap' codec can't encode character '✓' So text mode aborted right after printing "AVAILABLE_DOCS:", losing every per-document line. Fall back to ASCII when stdout cannot encode the glyph. "[OK]"/"[FAIL]" is the rendering these markers already have in-tree: Test-FileExists in scripts/powershell/common.ps1 emits exactly those, and normalize_status_text in tests/parity_helpers.py maps the glyphs onto them, so the twins already treat the two forms as equivalent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(scripts): cover both status markers in the cp1252 regression Review catch: the fixture left every reported document absent (the empty contracts/ also reports missing), so the test only ever called _status_marker(False). The assertion was `"[OK]" in out or "[FAIL]" in out`, which "[FAIL]" alone satisfied. Proved the hole by mutation: replacing the fallback body with a bare `return "[FAIL]"` — deleting the success branch outright — left the test GREEN. Add research.md so one document is present, and assert both markers explicitly. The strengthened test now kills all three mutations: fallback always "[FAIL]" -> FAILS (was passing) fallback always "[OK]" -> FAILS no fallback at all -> FAILS (the original bug) unmutated -> 12 passed, 8 skipped Missing documents are still present in the fixture, so the failure path stays covered too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(scripts): restore the _status_marker ASCII fallback The previous commit on this branch unintentionally reverted the source fix while adding the strengthened test, so the branch carried the test without the implementation it tests. Cause: my local verification script reverted the file for its red run with `git checkout upstream/main -- <file>`, which writes the INDEX as well as the working tree. Restoring the working-tree copy afterwards left main's version staged, and the next commit captured it. Restores the fix from 275663b. Verified: 12 passed / 8 skipped, and the red run (source reverted) produces 1 new-vs-baseline failure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent fe3732e commit 3dff6f1

2 files changed

Lines changed: 67 additions & 4 deletions

File tree

scripts/python/check_prerequisites.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,14 +130,31 @@ def _print_paths_only(paths: FeaturePaths, json_mode: bool) -> None:
130130
print(f"TASKS: {paths.tasks}")
131131

132132

133+
def _status_marker(ok: bool) -> str:
134+
"""Return the status glyph, downgraded to ASCII when stdout cannot encode it.
135+
136+
On Windows sys.stdout falls back to the ANSI code page whenever it is not a
137+
console - a pipe or a file redirect, which is how agents and workflow steps
138+
invoke these scripts - and U+2713 is unencodable in cp1252, so printing it
139+
raised UnicodeEncodeError and aborted the report right after
140+
"AVAILABLE_DOCS:". "[OK]"/"[FAIL]" is the ASCII rendering these markers
141+
already have in-tree: see Test-FileExists in scripts/powershell/common.ps1
142+
and normalize_status_text in tests/parity_helpers.py.
143+
"""
144+
glyph = "✓" if ok else "✗"
145+
try:
146+
glyph.encode(getattr(sys.stdout, "encoding", None) or "utf-8")
147+
except (LookupError, UnicodeEncodeError):
148+
return "[OK]" if ok else "[FAIL]"
149+
return glyph
150+
151+
133152
def _check_file(path: Path, description: str) -> None:
134-
marker = "✓" if path.is_file() else "✗"
135-
print(f" {marker} {description}")
153+
print(f" {_status_marker(path.is_file())} {description}")
136154

137155

138156
def _check_dir(path: Path, description: str) -> None:
139-
marker = "✓" if _dir_has_entries(path) else "✗"
140-
print(f" {marker} {description}")
157+
print(f" {_status_marker(_dir_has_entries(path))} {description}")
141158

142159

143160
def _print_text_results(paths: FeaturePaths, include_tasks: bool) -> None:

tests/test_check_prerequisites_python_parity.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,52 @@ def test_python_text_output_matches_bash(prereq_repo: Path) -> None:
181181
assert _normalize_status_text(py.stdout) == _normalize_status_text(bash.stdout)
182182

183183

184+
def test_python_text_output_survives_a_legacy_stdout_code_page(
185+
prereq_repo: Path,
186+
) -> None:
187+
"""Text mode must not crash when stdout cannot encode the status glyphs.
188+
189+
On Windows sys.stdout falls back to the ANSI code page whenever it is not a
190+
console — which is every time an agent or a workflow step captures the
191+
output. U+2713 is unencodable in cp1252, so printing it raised
192+
UnicodeEncodeError and truncated the report right after "AVAILABLE_DOCS:".
193+
The ASCII fallback is the rendering these markers already have in-tree
194+
(Test-FileExists in scripts/powershell/common.ps1, and
195+
normalize_status_text here).
196+
"""
197+
feat = prereq_repo / "specs" / "001-my-feature"
198+
feat.mkdir(parents=True)
199+
(feat / "plan.md").write_text("# plan\n", encoding="utf-8")
200+
# research.md is present and the rest are not, so BOTH status markers are
201+
# produced in the same cp1252 subprocess: U+2713 for the available document
202+
# and U+2717 for the missing ones. Asserting only one of them would let a
203+
# fallback that always returned "[FAIL]" pass.
204+
(feat / "research.md").write_text("# research\n", encoding="utf-8")
205+
(feat / "contracts").mkdir() # present but empty -> reported missing
206+
_write_feature_json(prereq_repo)
207+
208+
env = _clean_env()
209+
env["PYTHONIOENCODING"] = "cp1252"
210+
result = _run(_py_cmd(prereq_repo, "--include-tasks"), prereq_repo, env=env)
211+
212+
assert result.returncode == 0, result.stderr
213+
assert "UnicodeEncodeError" not in result.stderr
214+
assert "AVAILABLE_DOCS:" in result.stdout
215+
# Every per-document line must still be there, not truncated away by the
216+
# encode error.
217+
for doc in (
218+
"research.md",
219+
"data-model.md",
220+
"contracts/",
221+
"quickstart.md",
222+
"tasks.md",
223+
):
224+
assert doc in result.stdout, (doc, result.stdout)
225+
# Both fallback markers, so neither branch of _status_marker can regress.
226+
assert "[OK] research.md" in result.stdout, result.stdout
227+
assert "[FAIL] quickstart.md" in result.stdout, result.stdout
228+
229+
184230
@requires_bash
185231
def test_python_help_output_matches_bash(prereq_repo: Path) -> None:
186232
bash = _run(_bash_cmd(prereq_repo, "--help"), prereq_repo)

0 commit comments

Comments
 (0)