Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
"""Subagent failures surface as one clean user-facing notice.
|
||||
|
||||
Covers the Aug 2026 community report: a delegate_task child that dies
|
||||
(provider 404, timeout, crash) previously vanished silently on platforms
|
||||
with tool_progress off — the parent model saw the error but the human never
|
||||
did. Now:
|
||||
|
||||
- ``tools.delegate_tool.format_subagent_failure_line`` renders one clean,
|
||||
human-readable line (no tracebacks/JSON walls).
|
||||
- ``TurnRunner.progress_callback`` intercepts ``subagent.complete`` events
|
||||
with a terminal failure status FIRST (before every progress-queue gate)
|
||||
and delivers the line via ``_deliver_platform_notice``.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from gateway.turn_context import TurnContext
|
||||
from tools.delegate_tool import (
|
||||
SUBAGENT_FAILURE_STATUSES,
|
||||
_clean_error_text,
|
||||
format_subagent_failure_line,
|
||||
)
|
||||
|
||||
|
||||
class TestCleanErrorText:
|
||||
def test_single_line_passthrough(self):
|
||||
assert _clean_error_text("Error code: 404 - model not found") == (
|
||||
"Error code: 404 - model not found"
|
||||
)
|
||||
|
||||
def test_traceback_takes_last_line(self):
|
||||
tb = (
|
||||
"Traceback (most recent call last):\n"
|
||||
' File "x.py", line 1, in <module>\n'
|
||||
" raise RuntimeError('boom')\n"
|
||||
"RuntimeError: boom"
|
||||
)
|
||||
assert _clean_error_text(tb) == "RuntimeError: boom"
|
||||
|
||||
def test_multiline_non_traceback_takes_first_line(self):
|
||||
assert _clean_error_text("first line\nsecond line") == "first line"
|
||||
|
||||
def test_caps_length(self):
|
||||
out = _clean_error_text("x" * 500, max_chars=100)
|
||||
assert len(out) == 100
|
||||
assert out.endswith("...")
|
||||
|
||||
def test_empty_and_none(self):
|
||||
assert _clean_error_text("") == ""
|
||||
assert _clean_error_text(None) == ""
|
||||
assert _clean_error_text(" \n ") == ""
|
||||
|
||||
|
||||
class TestFormatSubagentFailureLine:
|
||||
def test_failed_with_goal_error_duration(self):
|
||||
line = format_subagent_failure_line(
|
||||
"research competitor pricing",
|
||||
"failed",
|
||||
error="Error code: 404 - model not found",
|
||||
duration_seconds=12.4,
|
||||
)
|
||||
assert line.startswith("⚠️ Subagent failed")
|
||||
assert '"research competitor pricing"' in line
|
||||
assert "404" in line
|
||||
assert "(after 12s)" in line
|
||||
|
||||
def test_timeout_verb(self):
|
||||
line = format_subagent_failure_line("do a thing", "timeout")
|
||||
assert "timed out" in line
|
||||
|
||||
def test_long_goal_truncated(self):
|
||||
line = format_subagent_failure_line("g" * 200, "failed")
|
||||
assert "g" * 57 + "..." in line
|
||||
assert "g" * 61 not in line
|
||||
|
||||
def test_no_goal_no_error(self):
|
||||
line = format_subagent_failure_line(None, "error")
|
||||
assert line == "⚠️ Subagent failed"
|
||||
|
||||
def test_multiline_goal_flattened(self):
|
||||
line = format_subagent_failure_line("a\nb", "failed")
|
||||
assert "\n" not in line
|
||||
|
||||
def test_failure_statuses_frozen(self):
|
||||
assert SUBAGENT_FAILURE_STATUSES == {"failed", "error", "timeout"}
|
||||
|
||||
|
||||
def _make_runner_and_captured(monkeypatch, run_still_current=True):
|
||||
"""TurnRunner with a stub gateway runner; captures scheduled notices."""
|
||||
from gateway import run as run_mod
|
||||
|
||||
captured: list[str] = []
|
||||
|
||||
class _StubGatewayRunner:
|
||||
def _adapter_for_source(self, source):
|
||||
return None
|
||||
|
||||
async def _deliver_platform_notice(self, source, content):
|
||||
captured.append(content)
|
||||
|
||||
def _fake_schedule(coro, loop, logger=None, log_message=None):
|
||||
asyncio.run(coro)
|
||||
|
||||
monkeypatch.setattr(run_mod, "safe_schedule_threadsafe", _fake_schedule)
|
||||
|
||||
ctx = TurnContext(
|
||||
source=MagicMock(),
|
||||
_run_still_current=lambda: run_still_current,
|
||||
progress_queue=None,
|
||||
_loop_for_step=None,
|
||||
)
|
||||
return run_mod.TurnRunner(_StubGatewayRunner(), ctx), captured
|
||||
|
||||
|
||||
class TestGatewayFailureNotice:
|
||||
@pytest.mark.parametrize("status", sorted(SUBAGENT_FAILURE_STATUSES))
|
||||
def test_failure_statuses_deliver_notice(self, monkeypatch, status):
|
||||
runner, captured = _make_runner_and_captured(monkeypatch)
|
||||
runner.progress_callback(
|
||||
"subagent.complete",
|
||||
preview="Error code: 404 - model not found",
|
||||
status=status,
|
||||
goal="scan the repo",
|
||||
duration_seconds=8.0,
|
||||
)
|
||||
assert len(captured) == 1
|
||||
assert "Subagent" in captured[0]
|
||||
assert "404" in captured[0]
|
||||
assert '"scan the repo"' in captured[0]
|
||||
|
||||
@pytest.mark.parametrize("status", ["completed", "interrupted", None])
|
||||
def test_non_failure_statuses_stay_silent(self, monkeypatch, status):
|
||||
runner, captured = _make_runner_and_captured(monkeypatch)
|
||||
runner.progress_callback(
|
||||
"subagent.complete", preview="all done", status=status, goal="g"
|
||||
)
|
||||
assert captured == []
|
||||
|
||||
def test_stale_run_stays_silent(self, monkeypatch):
|
||||
runner, captured = _make_runner_and_captured(
|
||||
monkeypatch, run_still_current=False
|
||||
)
|
||||
runner.progress_callback(
|
||||
"subagent.complete", preview="boom", status="failed", goal="g"
|
||||
)
|
||||
assert captured == []
|
||||
|
||||
def test_fires_without_progress_queue(self, monkeypatch):
|
||||
"""The notice must not depend on tool_progress being enabled —
|
||||
progress_queue=None is exactly the Telegram/Slack default where the
|
||||
silent-failure report came from."""
|
||||
runner, captured = _make_runner_and_captured(monkeypatch)
|
||||
assert runner._ctx.progress_queue is None
|
||||
runner.progress_callback(
|
||||
"subagent.complete", preview="err", status="error", goal="g"
|
||||
)
|
||||
assert len(captured) == 1
|
||||
|
||||
def test_summary_preferred_over_preview(self, monkeypatch):
|
||||
runner, captured = _make_runner_and_captured(monkeypatch)
|
||||
runner.progress_callback(
|
||||
"subagent.complete",
|
||||
preview="short preview",
|
||||
status="failed",
|
||||
goal="g",
|
||||
summary="the real error detail",
|
||||
)
|
||||
assert "the real error detail" in captured[0]
|
||||
Reference in New Issue
Block a user