Skip to content

Commit d3c15ab

Browse files
committed
fix: report why a render failed instead of a raw payload
A/B arm ab-no-analyze-plus-tty-no-prompts gave up on python_tui at FRID 1 inside the unit-test fix loop and told the user only: ERROR codeplain: None ExitWithError printed whatever payload the failing action handed over, and actions reach that state by three routes: an encoded RenderError dict, a plain string, or nothing at all. The last route printed "None"; the first printed a raw dict repr, which is what the conformance-fix-exhausted path has been showing all along: ERROR codeplain: {'error': {'message': "The renderer was unable to ...", 'type': None, 'details': None}} Unwraps the reason at the log site and falls back to last_error_message, the same message the returned payload already carried — so the line a user reads is never less informative than the error the renderer returns.
1 parent 4ed6ae6 commit d3c15ab

2 files changed

Lines changed: 85 additions & 1 deletion

File tree

‎render_machine/actions/exit_with_error.py‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class ExitWithError(BaseAction):
1010
SUCCESSFUL_OUTCOME = "error_handled"
1111

1212
def execute(self, render_context: RenderContext, previous_action_payload: Any | None):
13-
console.error(previous_action_payload)
13+
console.error(self._error_message(render_context, previous_action_payload))
1414

1515
render_context.codeplain_api.fail_functional_requirement(
1616
render_context.frid_context.frid,
@@ -33,3 +33,22 @@ def execute(self, render_context: RenderContext, previous_action_payload: Any |
3333
message=render_context.last_error_message or "Unknown error",
3434
).to_payload(),
3535
)
36+
37+
@staticmethod
38+
def _error_message(render_context: RenderContext, previous_action_payload: Any | None) -> str:
39+
"""What the user is told the render stopped for.
40+
41+
Actions reach this state by three routes: some hand over an encoded RenderError
42+
payload, some a plain string, and some nothing at all. Printing the payload as it
43+
arrives showed a raw dict for the first and the word "None" for the last, so the
44+
reason is unwrapped here and falls back to the same message the returned payload
45+
carries.
46+
"""
47+
if isinstance(previous_action_payload, dict):
48+
error = previous_action_payload.get("error")
49+
if isinstance(error, dict) and error.get("message"):
50+
return error["message"]
51+
elif previous_action_payload:
52+
return str(previous_action_payload)
53+
54+
return render_context.last_error_message or "Unknown error"

‎tests/test_exit_with_error.py‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""The operator-facing message on a failed render.
2+
3+
`ExitWithError` prints the payload the failing action handed over, but not every path
4+
into it supplies one — a render that gave up inside the unit-test fix loop arrives with
5+
`None`, and the user was shown a bare `ERROR codeplain: None` with no reason. The encoded
6+
payload already falls back to `last_error_message`; the console line must agree, so the
7+
message a user sees is never less informative than the one the renderer returns.
8+
"""
9+
10+
from unittest.mock import MagicMock, patch
11+
12+
from render_machine.actions.exit_with_error import ExitWithError
13+
from render_machine.render_types import RenderError
14+
15+
16+
def render_context(last_error_message=None):
17+
context = MagicMock()
18+
context.last_error_message = last_error_message
19+
context.frid_context.frid = "2"
20+
context.run_state.render_id = "render-id"
21+
return context
22+
23+
24+
def executed_with(payload, last_error_message=None):
25+
context = render_context(last_error_message)
26+
with patch("render_machine.actions.exit_with_error.console") as console:
27+
outcome, encoded = ExitWithError().execute(context, payload)
28+
return console.error.call_args[0][0], outcome, encoded
29+
30+
31+
def test_the_failing_action_s_own_message_is_shown():
32+
shown, outcome, _ = executed_with("Conformance tests could not be fixed.")
33+
34+
assert shown == "Conformance tests could not be fixed."
35+
assert outcome == ExitWithError.SUCCESSFUL_OUTCOME
36+
37+
38+
def test_a_missing_payload_falls_back_to_the_last_error_message():
39+
shown, _, _ = executed_with(None, last_error_message="The Unit Tests script has failed.")
40+
41+
assert shown == "The Unit Tests script has failed."
42+
43+
44+
def test_with_nothing_to_report_the_user_still_gets_words():
45+
shown, _, _ = executed_with(None)
46+
47+
assert shown == "Unknown error"
48+
49+
50+
def test_an_encoded_error_payload_is_unwrapped_to_its_reason():
51+
"""The conformance-fix-exhausted path arrives as an encoded RenderError, which used
52+
to reach the user as a raw dict repr."""
53+
payload = RenderError.encode(message="Could not produce an implementation that passes.").to_payload()
54+
55+
shown, _, _ = executed_with(payload)
56+
57+
assert shown == "Could not produce an implementation that passes."
58+
59+
60+
def test_the_shown_message_matches_the_encoded_one():
61+
"""Two sources of truth for the same failure would let the log and the returned
62+
error disagree about why a render stopped."""
63+
shown, _, encoded = executed_with(None, last_error_message="The Unit Tests script has failed.")
64+
65+
assert shown == encoded["error"]["message"]

0 commit comments

Comments
 (0)