Surface the real failure cause in the error dialog (roadmap A3)

On failure the error window showed a generic message with the whole log
buried in an expander. Lead instead with a focused summary: which step
failed (the process label), its command, and the last stderr lines, while
keeping the full log below. The runner passes the failing process to
error_window, which reads the executor's bounded stderr_data.

build_summary/_stderr_tail are static and unit-tested (tests/
test_error_summary.py): tail extraction, CR handling, blank-line skipping,
stdout fallback, long-line truncation, and Pango-markup escaping of paths/
messages containing & < ' so set_markup can't choke.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Luna 2026-06-05 12:22:55 -07:00
parent 00fdbd50df
commit 8cb6e1aa52
3 changed files with 127 additions and 3 deletions

View file

@ -0,0 +1,74 @@
"""Tests for the focused failure summary shown in the error window."""
from devedeng.error import error_window
class FakeProc:
def __init__(self, text="", command_var=None, stderr_data="", stdout_data=""):
self.text = text
self.command_var = command_var or []
self.stderr_data = stderr_data
self.stdout_data = stdout_data
def test_stderr_tail_takes_last_lines():
raw = "\n".join("line %d" % i for i in range(100))
tail = error_window._stderr_tail(FakeProc(stderr_data=raw), max_lines=5)
assert tail.splitlines() == ["line 95", "line 96", "line 97",
"line 98", "line 99"]
def test_stderr_tail_handles_carriage_returns():
raw = "p 1\rp 2\rp 3\ndone\n"
tail = error_window._stderr_tail(FakeProc(stderr_data=raw))
assert tail.splitlines()[-2:] == ["p 3", "done"]
def test_stderr_tail_skips_blank_lines():
raw = "real\n\n \n\nlast\n"
assert error_window._stderr_tail(FakeProc(stderr_data=raw)) == "real\nlast"
def test_stderr_tail_falls_back_to_stdout():
p = FakeProc(stderr_data="", stdout_data="only on stdout\n")
assert error_window._stderr_tail(p) == "only on stdout"
def test_stderr_tail_truncates_long_lines():
raw = "x" * 500
tail = error_window._stderr_tail(FakeProc(stderr_data=raw), max_line_len=200)
assert tail.endswith(" ...")
assert len(tail) <= 210
def test_build_summary_includes_step_command_and_tail():
p = FakeProc(
text="Converting My Movie",
command_var=["nice", "-n", "10", "ffmpeg", "-i", "in.mkv", "out.mpg"],
stderr_data="some warning\nfatal: invalid data found\n",
)
s = error_window.build_summary(p)
assert "Converting My Movie" in s
assert "ffmpeg" in s
assert "invalid data found" in s
# headline is present and markup is well-formed (bold tag closed)
assert "<b>" in s and "</b>" in s
def test_build_summary_escapes_markup_special_chars():
# a path with & and < must be escaped so set_markup doesn't choke
p = FakeProc(text="Converting A & B <x>",
command_var=["ffmpeg", "-i", "a&b<x>.mkv"],
stderr_data="error: <bad> & worse\n")
s = error_window.build_summary(p)
assert "&amp;" in s
assert "&lt;" in s
# the raw unescaped sequences should not appear in dynamic content
assert "a&b<x>.mkv" not in s
def test_build_summary_without_stderr_still_works():
p = FakeProc(text="Creating ISO image", command_var=["mkisofs", "-o", "x"])
s = error_window.build_summary(p)
assert "Creating ISO image" in s
assert "mkisofs" in s