|
import json
|
|
|
|
from openhands.events.observation.commands import (
|
|
CMD_OUTPUT_METADATA_PS1_REGEX,
|
|
CMD_OUTPUT_PS1_BEGIN,
|
|
CMD_OUTPUT_PS1_END,
|
|
CmdOutputMetadata,
|
|
CmdOutputObservation,
|
|
)
|
|
|
|
|
|
def test_ps1_metadata_format():
|
|
"""Test that PS1 prompt has correct format markers"""
|
|
prompt = CmdOutputMetadata.to_ps1_prompt()
|
|
print(prompt)
|
|
assert prompt.startswith('\n###PS1JSON###\n')
|
|
assert prompt.endswith('\n###PS1END###\n')
|
|
assert r'\"exit_code\"' in prompt, 'PS1 prompt should contain escaped double quotes'
|
|
|
|
|
|
def test_ps1_metadata_json_structure():
|
|
"""Test that PS1 prompt contains valid JSON with expected fields"""
|
|
prompt = CmdOutputMetadata.to_ps1_prompt()
|
|
|
|
json_str = prompt.replace('###PS1JSON###\n', '').replace('\n###PS1END###\n', '')
|
|
|
|
json_str = json_str.replace(r'\"', '"')
|
|
|
|
json_str = json_str.split('###PS1END###')[0].strip()
|
|
data = json.loads(json_str)
|
|
|
|
|
|
expected_fields = {
|
|
'pid',
|
|
'exit_code',
|
|
'username',
|
|
'hostname',
|
|
'working_dir',
|
|
'py_interpreter_path',
|
|
}
|
|
assert set(data.keys()) == expected_fields
|
|
|
|
|
|
def test_ps1_metadata_parsing():
|
|
"""Test parsing PS1 output into CmdOutputMetadata"""
|
|
test_data = {
|
|
'exit_code': 0,
|
|
'username': 'testuser',
|
|
'hostname': 'localhost',
|
|
'working_dir': '/home/testuser',
|
|
'py_interpreter_path': '/usr/bin/python',
|
|
}
|
|
|
|
ps1_str = f"""###PS1JSON###
|
|
{json.dumps(test_data, indent=2)}
|
|
###PS1END###
|
|
"""
|
|
matches = CmdOutputMetadata.matches_ps1_metadata(ps1_str)
|
|
assert len(matches) == 1
|
|
metadata = CmdOutputMetadata.from_ps1_match(matches[0])
|
|
assert metadata.exit_code == test_data['exit_code']
|
|
assert metadata.username == test_data['username']
|
|
assert metadata.hostname == test_data['hostname']
|
|
assert metadata.working_dir == test_data['working_dir']
|
|
assert metadata.py_interpreter_path == test_data['py_interpreter_path']
|
|
|
|
|
|
def test_ps1_metadata_parsing_string():
|
|
"""Test parsing PS1 output into CmdOutputMetadata"""
|
|
ps1_str = r"""###PS1JSON###
|
|
{
|
|
"exit_code": "0",
|
|
"username": "myname",
|
|
"hostname": "myhostname",
|
|
"working_dir": "~/mydir",
|
|
"py_interpreter_path": "/my/python/path"
|
|
}
|
|
###PS1END###
|
|
"""
|
|
matches = CmdOutputMetadata.matches_ps1_metadata(ps1_str)
|
|
assert len(matches) == 1
|
|
metadata = CmdOutputMetadata.from_ps1_match(matches[0])
|
|
assert metadata.exit_code == 0
|
|
assert metadata.username == 'myname'
|
|
assert metadata.hostname == 'myhostname'
|
|
assert metadata.working_dir == '~/mydir'
|
|
assert metadata.py_interpreter_path == '/my/python/path'
|
|
|
|
|
|
def test_ps1_metadata_parsing_string_real_example():
|
|
"""Test parsing PS1 output into CmdOutputMetadata"""
|
|
ps1_str = r"""
|
|
###PS1JSON###
|
|
{
|
|
"pid": "",
|
|
"exit_code": "0",
|
|
"username": "runner",
|
|
"hostname": "fv-az1055-610",
|
|
"working_dir": "/home/runner/work/OpenHands/OpenHands",
|
|
"py_interpreter_path": "/home/runner/.cache/pypoetry/virtualenvs/openhands-ai-ULPBlkAi-py3.12/bin/python"
|
|
}
|
|
###PS1END###
|
|
"""
|
|
matches = CmdOutputMetadata.matches_ps1_metadata(ps1_str)
|
|
assert len(matches) == 1
|
|
metadata = CmdOutputMetadata.from_ps1_match(matches[0])
|
|
assert metadata.exit_code == 0
|
|
assert metadata.username == 'runner'
|
|
assert metadata.hostname == 'fv-az1055-610'
|
|
assert metadata.working_dir == '/home/runner/work/OpenHands/OpenHands'
|
|
assert (
|
|
metadata.py_interpreter_path
|
|
== '/home/runner/.cache/pypoetry/virtualenvs/openhands-ai-ULPBlkAi-py3.12/bin/python'
|
|
)
|
|
|
|
|
|
def test_ps1_metadata_parsing_additional_prefix():
|
|
"""Test parsing PS1 output into CmdOutputMetadata"""
|
|
test_data = {
|
|
'exit_code': 0,
|
|
'username': 'testuser',
|
|
'hostname': 'localhost',
|
|
'working_dir': '/home/testuser',
|
|
'py_interpreter_path': '/usr/bin/python',
|
|
}
|
|
|
|
ps1_str = f"""
|
|
This is something that not part of the PS1 prompt
|
|
|
|
###PS1JSON###
|
|
{json.dumps(test_data, indent=2)}
|
|
###PS1END###
|
|
"""
|
|
|
|
matches = CmdOutputMetadata.matches_ps1_metadata(ps1_str)
|
|
assert len(matches) == 1
|
|
metadata = CmdOutputMetadata.from_ps1_match(matches[0])
|
|
assert metadata.exit_code == test_data['exit_code']
|
|
assert metadata.username == test_data['username']
|
|
assert metadata.hostname == test_data['hostname']
|
|
assert metadata.working_dir == test_data['working_dir']
|
|
assert metadata.py_interpreter_path == test_data['py_interpreter_path']
|
|
|
|
|
|
def test_ps1_metadata_parsing_invalid():
|
|
"""Test parsing invalid PS1 output returns default metadata"""
|
|
|
|
invalid_json = """###PS1JSON###
|
|
{invalid json}
|
|
###PS1END###
|
|
"""
|
|
matches = CmdOutputMetadata.matches_ps1_metadata(invalid_json)
|
|
assert len(matches) == 0
|
|
|
|
|
|
invalid_format = """NOT A VALID PS1 PROMPT"""
|
|
matches = CmdOutputMetadata.matches_ps1_metadata(invalid_format)
|
|
assert len(matches) == 0
|
|
|
|
|
|
empty_metadata = """###PS1JSON###
|
|
|
|
###PS1END###
|
|
"""
|
|
matches = CmdOutputMetadata.matches_ps1_metadata(empty_metadata)
|
|
assert len(matches) == 0
|
|
|
|
|
|
whitespace_metadata = """###PS1JSON###
|
|
|
|
{
|
|
"exit_code": "0",
|
|
"pid": "123",
|
|
"username": "test",
|
|
"hostname": "localhost",
|
|
"working_dir": "/home/test",
|
|
"py_interpreter_path": "/usr/bin/python"
|
|
}
|
|
|
|
###PS1END###
|
|
"""
|
|
matches = CmdOutputMetadata.matches_ps1_metadata(whitespace_metadata)
|
|
assert len(matches) == 1
|
|
metadata = CmdOutputMetadata.from_ps1_match(matches[0])
|
|
assert metadata.exit_code == 0
|
|
assert metadata.pid == 123
|
|
|
|
|
|
def test_ps1_metadata_missing_fields():
|
|
"""Test handling of missing fields in PS1 metadata"""
|
|
|
|
minimal_data = {'exit_code': 0, 'pid': 123}
|
|
ps1_str = f"""###PS1JSON###
|
|
{json.dumps(minimal_data)}
|
|
###PS1END###
|
|
"""
|
|
matches = CmdOutputMetadata.matches_ps1_metadata(ps1_str)
|
|
assert len(matches) == 1
|
|
metadata = CmdOutputMetadata.from_ps1_match(matches[0])
|
|
assert metadata.exit_code == 0
|
|
assert metadata.pid == 123
|
|
assert metadata.username is None
|
|
assert metadata.hostname is None
|
|
assert metadata.working_dir is None
|
|
assert metadata.py_interpreter_path is None
|
|
|
|
|
|
no_exit_code = {'pid': 123, 'username': 'test'}
|
|
ps1_str = f"""###PS1JSON###
|
|
{json.dumps(no_exit_code)}
|
|
###PS1END###
|
|
"""
|
|
matches = CmdOutputMetadata.matches_ps1_metadata(ps1_str)
|
|
assert len(matches) == 1
|
|
metadata = CmdOutputMetadata.from_ps1_match(matches[0])
|
|
assert metadata.exit_code == -1
|
|
assert metadata.pid == 123
|
|
assert metadata.username == 'test'
|
|
|
|
|
|
def test_ps1_metadata_multiple_blocks():
|
|
"""Test handling multiple PS1 metadata blocks"""
|
|
test_data = {
|
|
'exit_code': 0,
|
|
'username': 'testuser',
|
|
'hostname': 'localhost',
|
|
'working_dir': '/home/testuser',
|
|
'py_interpreter_path': '/usr/bin/python',
|
|
}
|
|
|
|
ps1_str = f"""###PS1JSON###
|
|
{json.dumps(test_data, indent=2)}
|
|
###PS1END###
|
|
Some other content
|
|
###PS1JSON###
|
|
{json.dumps(test_data, indent=2)}
|
|
###PS1END###
|
|
"""
|
|
matches = CmdOutputMetadata.matches_ps1_metadata(ps1_str)
|
|
assert len(matches) == 2
|
|
|
|
metadata1 = CmdOutputMetadata.from_ps1_match(matches[0])
|
|
metadata2 = CmdOutputMetadata.from_ps1_match(matches[1])
|
|
assert metadata1.exit_code == test_data['exit_code']
|
|
assert metadata2.exit_code == test_data['exit_code']
|
|
|
|
|
|
def test_ps1_metadata_regex_pattern():
|
|
"""Test the regex pattern used to extract PS1 metadata"""
|
|
|
|
test_str = f'{CMD_OUTPUT_PS1_BEGIN}test\n{CMD_OUTPUT_PS1_END}'
|
|
matches = CMD_OUTPUT_METADATA_PS1_REGEX.finditer(test_str)
|
|
match = next(matches)
|
|
assert match.group(1).strip() == 'test'
|
|
|
|
|
|
test_str = f'prefix\n{CMD_OUTPUT_PS1_BEGIN}test\n{CMD_OUTPUT_PS1_END}suffix'
|
|
matches = CMD_OUTPUT_METADATA_PS1_REGEX.finditer(test_str)
|
|
match = next(matches)
|
|
assert match.group(1).strip() == 'test'
|
|
|
|
|
|
test_str = f'{CMD_OUTPUT_PS1_BEGIN}line1\nline2\nline3\n{CMD_OUTPUT_PS1_END}'
|
|
matches = CMD_OUTPUT_METADATA_PS1_REGEX.finditer(test_str)
|
|
match = next(matches)
|
|
assert match.group(1).strip() == 'line1\nline2\nline3'
|
|
|
|
|
|
def test_cmd_output_observation_properties():
|
|
"""Test CmdOutputObservation class properties"""
|
|
|
|
metadata = CmdOutputMetadata(exit_code=0, pid=123)
|
|
obs = CmdOutputObservation(command='ls', content='file1\nfile2', metadata=metadata)
|
|
assert obs.command_id == 123
|
|
assert obs.exit_code == 0
|
|
assert not obs.error
|
|
assert 'exit code 0' in obs.message
|
|
assert 'ls' in obs.message
|
|
assert 'file1' in str(obs)
|
|
assert 'file2' in str(obs)
|
|
assert 'metadata' in str(obs)
|
|
|
|
|
|
metadata = CmdOutputMetadata(exit_code=1, pid=456)
|
|
obs = CmdOutputObservation(command='invalid', content='error', metadata=metadata)
|
|
assert obs.command_id == 456
|
|
assert obs.exit_code == 1
|
|
assert obs.error
|
|
assert 'exit code 1' in obs.message
|
|
assert 'invalid' in obs.message
|
|
assert 'error' in str(obs)
|
|
|
|
|
|
def test_ps1_metadata_empty_fields():
|
|
"""Test handling of empty fields in PS1 metadata"""
|
|
|
|
empty_data = {
|
|
'exit_code': 0,
|
|
'pid': 123,
|
|
'username': '',
|
|
'hostname': '',
|
|
'working_dir': '',
|
|
'py_interpreter_path': '',
|
|
}
|
|
ps1_str = f"""###PS1JSON###
|
|
{json.dumps(empty_data)}
|
|
###PS1END###
|
|
"""
|
|
matches = CmdOutputMetadata.matches_ps1_metadata(ps1_str)
|
|
assert len(matches) == 1
|
|
metadata = CmdOutputMetadata.from_ps1_match(matches[0])
|
|
assert metadata.exit_code == 0
|
|
assert metadata.pid == 123
|
|
assert metadata.username == ''
|
|
assert metadata.hostname == ''
|
|
assert metadata.working_dir == ''
|
|
assert metadata.py_interpreter_path == ''
|
|
|
|
|
|
malformed_json = """###PS1JSON###
|
|
{
|
|
"exit_code":0,
|
|
"pid" : 123,
|
|
"username": "test" ,
|
|
"hostname": "host",
|
|
"working_dir" :"dir",
|
|
"py_interpreter_path":"path"
|
|
}
|
|
###PS1END###
|
|
"""
|
|
matches = CmdOutputMetadata.matches_ps1_metadata(malformed_json)
|
|
assert len(matches) == 1
|
|
metadata = CmdOutputMetadata.from_ps1_match(matches[0])
|
|
assert metadata.exit_code == 0
|
|
assert metadata.pid == 123
|
|
assert metadata.username == 'test'
|
|
assert metadata.hostname == 'host'
|
|
assert metadata.working_dir == 'dir'
|
|
assert metadata.py_interpreter_path == 'path'
|
|
|