Skip to content

Commit 38371c5

Browse files
committed
test(turn-driver): refuse a vacuous pass in the locale evidence case
The non-UTF-8 default-codec case asserted only that the host codec is not UTF-8, which does not prove the CLI's UTF-8 bytes are rejected: a single-byte codec such as latin-1 or cp1251 decodes the sample without raising, so the defective read returns mojibake and both cases would pass with the codec pin reverted. The child now decodes the exact bytes it writes and reports the result, and the case skips with the resolved codec named where the hazard cannot be shown here. Also state per-case coverage: the recovery case exercises the observe read and the attempt record's byte read, while the two os.fdopen write pins are protected by the gate rule alone, because json.dump escapes non-ASCII by default and their payload is ASCII today. Verified by reverting both pins: this suite stays green and the gate reports exactly codex_agent_session.py:173 and codex_checkpoint.py:415. Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
1 parent e65b0c7 commit 38371c5

1 file changed

Lines changed: 60 additions & 10 deletions

File tree

‎tests/control_plane/test_codex_session_utf8_evidence.py‎

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,16 @@
77
host locale codec from rewriting them.
88
99
Each case drives one real owner in a child interpreter whose default encoding is
10-
ASCII, with a workspace whose path carries a non-ASCII component: the refresh
10+
not UTF-8 (the host ANSI code page on Windows, `ascii` under `LC_ALL=C` on
11+
POSIX), with a workspace whose path carries a non-ASCII component: the refresh
1112
record keeps the observed stdout path and the binding's checkpoint root, so that
1213
workspace shape is the ordinary case rather than a synthetic one.
14+
15+
The child also proves the case is not vacuous: it decodes the exact bytes the
16+
CLI writes with its resolved default codec and reports whether they are
17+
rejected. A single-byte codec such as `latin-1` or `cp1251` accepts those bytes,
18+
so on such a host the read succeeds with mojibake instead of raising and this
19+
regression cannot be shown; the case skips there rather than claim coverage.
1320
"""
1421

1522
from __future__ import annotations
@@ -20,6 +27,8 @@
2027
import subprocess
2128
import sys
2229

30+
import pytest
31+
2332
REPO_ROOT = Path(__file__).resolve().parents[2]
2433

2534
WORKSPACE_COMPONENT = "项目"
@@ -29,9 +38,29 @@
2938
from pathlib import Path
3039
from types import SimpleNamespace
3140
41+
42+
def events_bytes(identity):
43+
# The Codex CLI writes its event stream as UTF-8 whatever the host codec is.
44+
return b"".join([
45+
json.dumps({"type": "item.completed", "text": "任务已完成"},
46+
ensure_ascii=False).encode("utf-8") + b"\n",
47+
json.dumps({"type": "thread.started", "thread_id": identity}).encode("utf-8") + b"\n",
48+
])
49+
50+
3251
locale.setlocale(locale.LC_CTYPE, "C")
52+
_codec = locale.getpreferredencoding(False)
3353
assert sys.flags.utf8_mode == 0
34-
assert locale.getpreferredencoding(False).lower().replace("-", "") != "utf8"
54+
assert _codec.lower().replace("-", "") != "utf8"
55+
try:
56+
events_bytes("locale-probe").decode(_codec)
57+
except UnicodeDecodeError:
58+
locale_hazard = True
59+
else:
60+
# A single-byte codec decodes the sample without raising, so the defective
61+
# default-codec read would return mojibake instead of failing. Say so
62+
# instead of letting the case pass for the wrong reason.
63+
locale_hazard = False
3564
3665
from loopx.control_plane.turn_driver.codex_agent_session import CodexAgentSession
3766
from loopx.control_plane.turn_driver.codex_sessions import (
@@ -61,12 +90,7 @@ def session(root):
6190
6291
6392
def events(path, identity):
64-
# The Codex CLI writes its event stream as UTF-8 whatever the host codec is.
65-
path.write_bytes(b"".join([
66-
json.dumps({"type": "item.completed", "text": "任务已完成"},
67-
ensure_ascii=False).encode("utf-8") + b"\n",
68-
json.dumps({"type": "thread.started", "thread_id": identity}).encode("utf-8") + b"\n",
69-
]))
93+
path.write_bytes(events_bytes(identity))
7094
7195
7296
root = base / mode / "checkout"
@@ -90,7 +114,8 @@ def events(path, identity):
90114
"attempt_stdout": json.loads(attempt.read_bytes())["stdout"],
91115
"retry_launched": retry.exists()}
92116
93-
print(json.dumps({"encoding": locale.getpreferredencoding(False), **payload}, ensure_ascii=True))
117+
print(json.dumps({"encoding": _codec, "locale_hazard": locale_hazard, **payload},
118+
ensure_ascii=True))
94119
'''
95120

96121

@@ -115,19 +140,44 @@ def _payload(base: Path, mode: str) -> dict:
115140
result = _drive(base, mode)
116141
assert result.returncode == 0, result.stderr.decode("utf-8", "replace")
117142
payload = json.loads(result.stdout.decode("utf-8"))
118-
# A vacuous pass would hide the codec this test exists to exercise.
143+
# A vacuous pass would hide the codec this test exists to exercise. The host
144+
# default must not be UTF-8, and it must reject the exact bytes the CLI
145+
# writes; a permissive single-byte codec decodes them without raising, so the
146+
# defective read would return mojibake and the assertions below would hold
147+
# with the fix reverted.
119148
assert payload["encoding"].lower().replace("-", "") != "utf8"
149+
if not payload["locale_hazard"]:
150+
pytest.skip(
151+
f"host default codec {payload['encoding']} decodes the UTF-8 event bytes; "
152+
"the default-codec read hazard cannot be reproduced on this host"
153+
)
120154
return payload
121155

122156

123157
def test_codex_events_are_read_back_with_a_locale_codec(tmp_path: Path) -> None:
158+
"""The CLI's UTF-8 event stream survives a non-UTF-8 default codec.
159+
160+
Covers the `CodexAgentSession.observe` read only: reverting its codec pin
161+
makes this case fail with the reproduced `UnicodeDecodeError`.
162+
"""
163+
124164
base = tmp_path / WORKSPACE_COMPONENT
125165
payload = _payload(base, "observe")
126166

127167
assert payload["session_id"] == "fresh"
128168

129169

130170
def test_checkpoint_refresh_attempt_record_is_readable_again(tmp_path: Path) -> None:
171+
"""The recovery wake re-reads its original output and its attempt record.
172+
173+
Covers the recovery path's reads — `prepare_checkpoint_refresh` re-runs the
174+
`observe` read on the recorded stdout path, and the record itself is re-read
175+
as UTF-8 bytes. It does not cover the two `os.fdopen` write pins: their
176+
payload is ASCII today because `json.dump` escapes non-ASCII by default, so
177+
reverting those pins leaves this case green. The gate rule in
178+
`tests/test_loopx_text_io_utf8.py` is what protects them.
179+
"""
180+
131181
base = tmp_path / WORKSPACE_COMPONENT
132182
payload = _payload(base, "recover")
133183

0 commit comments

Comments
 (0)