# Copyright (c) Alibaba, Inc. and its affiliates.
"""Unit tests for agent strategy parsers.
Covers:
* ReAct / FunctionCalling submit interception, tools(), system prompt.
* SWE-bench toolcall + backticks strategies after the unified termination
refactor: sentinel detection in ``format_observation`` mutates
``ParsedAction.final_answer`` (no exceptions).
* The pure observation parser in
``evalscope.agent.strategies.swe_bench._observation`` (returncode
reverse-engineering, sentinel detection, mini-style envelope).
No API calls; all tests are deterministic.
"""
import unittest
from evalscope.api.agent import AgentContext, AgentLoopResult, ParsedAction
from evalscope.api.agent.trace import AgentTrace
from evalscope.api.messages import ChatMessageAssistant, ChatMessageTool, ChatMessageUser
from evalscope.api.model import ModelOutput
from evalscope.api.tool import ToolCall, ToolCallError, ToolFunction, ToolInfo
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_output(text: str = '', tool_calls: list | None = None) -> ModelOutput:
"""Build a minimal ModelOutput for parse_output tests."""
msg = ChatMessageAssistant(
content=text,
tool_calls=tool_calls,
model='test',
source='generate',
)
return ModelOutput(model='test', choices=[{'message': msg}])
def _make_ctx(messages: list | None = None, tools: list | None = None) -> AgentContext:
return AgentContext(
sample_id='test',
messages=messages or [],
tools=tools or [],
max_steps=10,
)
def _tool_call(name: str, args: dict | None = None, call_id: str = 'tc-1') -> ToolCall:
return ToolCall(id=call_id, function=ToolFunction(name=name, arguments=args or {}))
# ---------------------------------------------------------------------------
# SweBenchBackticksStrategy parse_output
# ---------------------------------------------------------------------------
class TestSweBenchBackticksParseOutput(unittest.TestCase):
"""Tests for SweBenchBackticksStrategy.parse_output."""
def setUp(self):
from evalscope.agent.strategies.swe_bench.swe_bench_backticks import SweBenchBackticksStrategy
self.strategy = SweBenchBackticksStrategy()
self.ctx = _make_ctx()
def test_single_bash_block(self):
output = _make_output(text='THOUGHT: I need to list files\n```mswea_bash_command\nls -la\n```')
parsed = self.strategy.parse_output(output, self.ctx)
self.assertEqual(len(parsed.tool_calls), 1)
self.assertEqual(parsed.tool_calls[0].function.name, 'bash')
self.assertIn('ls -la', parsed.tool_calls[0].function.arguments.get('command', ''))
def test_no_bash_block_returns_raw_text(self):
output = _make_output(text='The answer is 42')
parsed = self.strategy.parse_output(output, self.ctx)
self.assertIsNone(parsed.final_answer)
self.assertEqual(parsed.raw_text, 'The answer is 42')
def test_multiple_bash_blocks_error(self):
output = _make_output(
text='THOUGHT: try two\n```mswea_bash_command\necho first\n```\n```mswea_bash_command\necho second\n```'
)
parsed = self.strategy.parse_output(output, self.ctx)
self.assertIsNotNone(parsed.error)
self.assertIn('exactly one', parsed.error)
def test_bash_block_with_multiline_command(self):
cmd = 'for i in $(seq 1 5); do\n echo $i\ndone'
output = _make_output(text=f'THOUGHT: loop\n```mswea_bash_command\n{cmd}\n```')
parsed = self.strategy.parse_output(output, self.ctx)
self.assertEqual(len(parsed.tool_calls), 1)
self.assertIn('seq 1 5', parsed.tool_calls[0].function.arguments.get('command', ''))
# ---------------------------------------------------------------------------
# SweBenchBackticksStrategy format_observation
# ---------------------------------------------------------------------------
class TestSweBenchBackticksFormatObservation(unittest.TestCase):
"""Tests for SweBenchBackticksStrategy.format_observation."""
def setUp(self):
from evalscope.agent.strategies.swe_bench._observation import SUBMIT_SENTINEL
from evalscope.agent.strategies.swe_bench.swe_bench_backticks import SweBenchBackticksStrategy
self.strategy = SweBenchBackticksStrategy()
self.sentinel = SUBMIT_SENTINEL
def test_normal_observation_returns_user_message(self):
call = _tool_call('bash', {'command': 'ls'})
parsed = ParsedAction()
ctx = _make_ctx()
msg = self.strategy.format_observation(call, 'file1.txt\nfile2.txt', None, parsed, ctx)
self.assertIsInstance(msg, ChatMessageUser)
self.assertEqual(msg.role, 'user')
self.assertIn('file1.txt', msg.content)
self.assertIn('0', msg.content)
self.assertIn('``-style tags.
self.assertIsInstance(msg, ChatMessageUser)
self.assertNotIn('', msg.content)
self.assertNotIn('