Add tests for job-log excerpting

Covers the three call sites that show a job stream to a model: get_job_details,
_format_job_context (which re-slices an already-excerpted value down to 500
characters of head), and the get_job_errors tool result.
This commit is contained in:
Dannon Baker
2026-08-05 09:22:28 -04:00
parent 74f47f43f5
commit 5810c37ec6
2 changed files with 88 additions and 0 deletions
@@ -2,6 +2,7 @@ from unittest import mock
import pytest
from galaxy.agents.base import JOB_LOG_EXCERPT_CHARS
from galaxy.agents.operations import AgentOperationsManager
from galaxy.schema.fields import Security
from .base import BaseTestCase
@@ -157,6 +158,61 @@ class TestAgentOperationsManagerWithMockedServices(BaseTestCase):
assert result["collection"]["elements_truncated"] is True
assert result["collection"]["total_elements"] == 10
def test_get_job_errors_keeps_both_ends_of_a_long_log(self):
"""The failure is at the tail, so a head slice would drop the useful part."""
job = mock.MagicMock()
job.stderr = "HEAD_MARKER\n" + ("noise\n" * 5000) + "TAIL_MARKER: out of memory"
job.stdout = "short stdout"
job.info = "short info"
job.tool_id = "ngm"
job.tool_version = "1.0"
job.state = "error"
job.exit_code = 137
job.id = 42
hda = mock.MagicMock()
hda.creating_job = job
with (
mock.patch.object(self.agent_ops.hda_manager, "get_accessible", return_value=hda),
mock.patch.object(self.trans.security, "decode_id", return_value=123),
mock.patch.object(self.trans.security, "encode_id", return_value="enc42"),
):
result = self.agent_ops.get_job_errors("encoded_dataset_id")
assert len(result["stderr"]) <= JOB_LOG_EXCERPT_CHARS
assert "HEAD_MARKER" in result["stderr"]
assert "TAIL_MARKER" in result["stderr"]
assert result["truncated"] is True
# Streams that fit are passed through untouched.
assert result["stdout"] == "short stdout"
assert result["info"] == "short info"
def test_get_job_errors_counts_info_toward_the_truncated_flag(self):
"""Job.info is a TrimmedString(255), but that only trims on the way to the DB."""
job = mock.MagicMock()
job.stderr = "short stderr"
job.stdout = "short stdout"
job.info = "I" * 50000
job.tool_id = "ngm"
job.tool_version = "1.0"
job.state = "error"
job.exit_code = 1
job.id = 42
hda = mock.MagicMock()
hda.creating_job = job
with (
mock.patch.object(self.agent_ops.hda_manager, "get_accessible", return_value=hda),
mock.patch.object(self.trans.security, "decode_id", return_value=123),
mock.patch.object(self.trans.security, "encode_id", return_value="enc42"),
):
result = self.agent_ops.get_job_errors("encoded_dataset_id")
assert len(result["info"]) <= JOB_LOG_EXCERPT_CHARS
assert result["truncated"] is True
def test_get_workflow_details_with_version(self):
mock_workflow = mock.MagicMock()
+32
View File
@@ -551,6 +551,38 @@ class TestAgentUnitMocked:
assert response.metadata["query_truncated"] is True
assert response.metadata["original_query_length"] == 40000
def test_format_job_context_does_not_reslice_an_excerpted_log(self):
"""get_job_details already budgeted this stream; slicing again drops its tail.
The old 500-char head slice made that budget dead code -- only the first few
lines of a failing tool's banner ever reached the model.
"""
agent = ErrorAnalysisAgent(self.deps)
stderr = "HEAD_MARKER" + ("x" * 1500) + "TAIL_MARKER"
rendered = agent._format_job_context({"tool_id": "ngm", "state": "error", "stderr": stderr})
assert "HEAD_MARKER" in rendered
assert "TAIL_MARKER" in rendered
@pytest.mark.asyncio
async def test_get_job_details_keeps_the_tail_of_a_long_stderr(self):
"""A head slice here would drop the traceback before the prompt is ever built."""
agent = ErrorAnalysisAgent(self.deps)
job = mock.MagicMock()
job.stderr = "HEAD_MARKER\n" + ("noise\n" * 5000) + "TAIL_MARKER: killed"
job.stdout = ""
job.id = 42
self.deps.job_manager = mock.Mock()
self.deps.job_manager.get_accessible_job.return_value = job
details = await agent.get_job_details(42)
assert len(details["stderr"]) <= agents_base.JOB_LOG_EXCERPT_CHARS
assert "HEAD_MARKER" in details["stderr"]
assert "TAIL_MARKER" in details["stderr"]
@pytest.mark.asyncio
async def test_error_analysis_trims_oversized_stderr_instead_of_rejecting(self):
"""A huge stderr dump gets trimmed and analyzed rather than refused for length.