From d3fca90fa6c8c75fc288e2e08c2b11d278d90161 Mon Sep 17 00:00:00 2001 From: fangliquanflq Date: Sat, 8 Aug 2026 04:24:35 +0000 Subject: [PATCH] test(tests): stabilize write_json concurrent serialization flake --- tests/test_tui_gateway_server.py | 63 ++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 11 deletions(-) diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index c3d8e5d313..8a64537c93 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -896,24 +896,65 @@ class _BrokenStdout: def test_write_json_serializes_concurrent_writes(monkeypatch): - out = _ChunkyStdout() - monkeypatch.setattr(server, "_real_stdout", out) + """Assert StdioTransport holds _stdout_lock across the full stream.write. - threads = [ - threading.Thread(target=server.write_json, args=({"seq": i, "text": "x" * 24},)) - for i in range(8) - ] + The old char-by-char sleep mock made this test take long enough that + leftover background write_json calls from earlier cases in this file + could append an extra line (intermittent ``assert 9 == 8`` on CI/main). + Match the WS concurrent-send check: count in-flight writes, and only + assert on frames that carry this test's marker payload. + """ + marker = "x" * 24 + active = 0 + max_active = 0 + gate = threading.Lock() + frames: list[str] = [] + class RecordingStdout: + def write(self, text: str) -> int: + nonlocal active, max_active + with gate: + active += 1 + max_active = max(max_active, active) + try: + # Release the GIL while "in write" so a missing outer lock + # would let another thread bump max_active above 1. + time.sleep(0.01) + frames.append(text) + finally: + with gate: + active -= 1 + return len(text) + + def flush(self) -> None: + return None + + monkeypatch.setattr(server, "_real_stdout", RecordingStdout()) + + barrier = threading.Barrier(8) + + def _worker(seq: int) -> None: + barrier.wait(timeout=5) + server.write_json({"seq": seq, "text": marker}) + + threads = [threading.Thread(target=_worker, args=(i,)) for i in range(8)] for t in threads: t.start() - for t in threads: - t.join() + t.join(timeout=10) + assert not t.is_alive() - lines = "".join(out.parts).splitlines() + assert max_active == 1 - assert len(lines) == 8 - assert {json.loads(line)["seq"] for line in lines} == set(range(8)) + ours = [] + for frame in frames: + assert frame.endswith("\n"), frame + obj = json.loads(frame) + if obj.get("text") == marker and "seq" in obj: + ours.append(obj) + + assert {obj["seq"] for obj in ours} == set(range(8)) + assert len(ours) == 8 def test_write_json_returns_false_on_broken_pipe(monkeypatch):