From 3728b31420e6417fc546fea5fa8c2f3b8de41a98 Mon Sep 17 00:00:00 2001 From: NIU-123370 <191000457+NIU-123370@users.noreply.github.com> Date: Fri, 25 Sep 2026 21:33:04 +0800 Subject: [PATCH 1/2] fix(effect-runtime): keep one startup envelope one record `_startup_diagnostic` framed the managed runtime's stderr with `str.splitlines()`, which honours U+0085, U+2028 and U+2029 as line breaks. A rejected `LOOPX_EFFECT_RUNTIME_IDLE_MS` echoes the offending value back inside the typed envelope, and `JSON.stringify` leaves those three characters unescaped, so the single documented record was torn into fragments that no longer parsed. The caller then fell back to `runtime_exited_before_ready (exit_code=2)` and hid the actionable `invalid_idle_timeout` guidance. Frame the diagnostic stream on `\n`, the separator the server actually writes, matching the response reader in this module. Values padded with U+001C or ASCII whitespace already reported correctly because those are escaped or are not record breaks; they stay pinned so a future framing change cannot silently widen or narrow the accepted set. Signed-off-by: NIU-123370 <191000457+NIU-123370@users.noreply.github.com> --- loopx/control_plane/effect_runtime.py | 4 +- .../test_effect_runtime_integration.py | 67 +++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/loopx/control_plane/effect_runtime.py b/loopx/control_plane/effect_runtime.py index d4c7be6188..ab727111b4 100644 --- a/loopx/control_plane/effect_runtime.py +++ b/loopx/control_plane/effect_runtime.py @@ -662,7 +662,9 @@ def _startup_diagnostic(raw: bytes) -> tuple[str, str] | None: if not raw: return None - for line in reversed(raw.decode("utf-8", errors="replace").splitlines()): + # Not splitlines(): it also breaks on U+0085/U+2028/U+2029, which a rejected + # setting can echo back unescaped inside the envelope and tear the record. + for line in reversed(raw.decode("utf-8", errors="replace").split("\n")): candidate = line.strip() if not candidate.startswith("{"): continue diff --git a/tests/control_plane/test_effect_runtime_integration.py b/tests/control_plane/test_effect_runtime_integration.py index a0a83b30ab..917814296f 100644 --- a/tests/control_plane/test_effect_runtime_integration.py +++ b/tests/control_plane/test_effect_runtime_integration.py @@ -912,6 +912,67 @@ def test_managed_runtime_releases_memory_after_idle_timeout( ) +def _envelope(code: str, received: str) -> bytes: + """Frame one startup rejection the way the managed server publishes it. + + ``ensure_ascii=False`` mirrors ``JSON.stringify``, which leaves U+0085 and + the two separators raw while escaping everything below U+0020. + """ + + return ( + json.dumps( + { + "schema_version": ( + effect_runtime.EFFECT_RUNTIME_STARTUP_ERROR_SCHEMA_VERSION + ), + "code": code, + "message": 'idle timeout guidance (received "' + received + '")', + }, + ensure_ascii=False, + ) + + "\n" + ).encode("utf-8") + + +@pytest.mark.parametrize( + ("received", "label"), + [ + ("\u0085150\u0085", "NEL"), + ("\u2028150\u2028", "LINE SEPARATOR"), + ("\u2029150\u2029", "PARAGRAPH SEPARATOR"), + ("\u001c150\u001c", "FILE SEPARATOR"), + (" 150 ", "ASCII space"), + ], +) +def test_startup_diagnostic_survives_a_padding_value_it_quotes_back( + received: str, + label: str, +) -> None: + """One envelope is one record, whatever the rejected value contains.""" + + envelope = _envelope("invalid_idle_timeout", received) + + recovered = effect_runtime._startup_diagnostic(envelope) + + assert recovered is not None, label + code, message = recovered + assert code == "invalid_idle_timeout" + assert "150" in message + + +def test_startup_diagnostic_ignores_stderr_without_an_envelope() -> None: + """A crash trace is not a typed configuration diagnostic.""" + + stderr = ( + "node:internal/process/promises:391\n" + " triggerUncaughtException(err, true);\n" + " ^\n" + "Error: listen EACCES\n" + ).encode("utf-8") + + assert effect_runtime._startup_diagnostic(stderr) is None + + @pytest.mark.parametrize( "raw_idle_ms", [ @@ -924,6 +985,12 @@ def test_managed_runtime_releases_memory_after_idle_timeout( "0x10", " 150", "150 ", + # Echoed back by the runtime without escaping, and honoured as a + # newline by str.splitlines(): the two together used to mask the + # typed diagnostic behind runtime_exited_before_ready. + "\u0085150\u0085", + "\u2028150\u2028", + "\u2029150\u2029", "2147483648", "9007199254740993", ], From 962a4a9ea328e9a8950a84e672a647e812f7b12e Mon Sep 17 00:00:00 2001 From: NIU-123370 <191000457+NIU-123370@users.noreply.github.com> Date: Fri, 25 Sep 2026 21:39:57 +0800 Subject: [PATCH 2/2] test(effect-runtime): pin the diagnostic record separator itself Framing on `\n` is only correct because the server terminates the envelope with `\n`. The earlier test set could not tell that separator from `\r\n`, `\r` or the ASCII record separator, because each rejection publishes one record and a single-record stream survives any of them. Add a stderr capture that mixes the envelope with unrelated Node output, so the reader has to frame records rather than treat the whole capture as one line. This is a regression pin for the framing rule, not a second fix. Signed-off-by: NIU-123370 <191000457+NIU-123370@users.noreply.github.com> --- .../test_effect_runtime_integration.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/control_plane/test_effect_runtime_integration.py b/tests/control_plane/test_effect_runtime_integration.py index 917814296f..e5b4c668f1 100644 --- a/tests/control_plane/test_effect_runtime_integration.py +++ b/tests/control_plane/test_effect_runtime_integration.py @@ -960,6 +960,21 @@ def test_startup_diagnostic_survives_a_padding_value_it_quotes_back( assert "150" in message +def test_startup_diagnostic_recovers_the_envelope_beside_other_output() -> None: + """Framing is per record, so earlier noise does not bury the diagnostic.""" + + envelope = _envelope("invalid_idle_timeout", " 150 ").decode("utf-8") + stderr = ( + "npm warn ignoring empty lockfile\n" + "node:events:496\n" + envelope + "Warning: fsync() failed\n" + ).encode("utf-8") + + recovered = effect_runtime._startup_diagnostic(stderr) + + assert recovered is not None + assert recovered[0] == "invalid_idle_timeout" + + def test_startup_diagnostic_ignores_stderr_without_an_envelope() -> None: """A crash trace is not a typed configuration diagnostic."""