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() # Extract JSON content between markers json_str = prompt.replace('###PS1JSON###\n', '').replace('\n###PS1END###\n', '') # Remove escaping before parsing json_str = json_str.replace(r'\"', '"') # Remove any trailing content after the JSON json_str = json_str.split('###PS1END###')[0].strip() data = json.loads(json_str) # Check required fields 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""" # Test with invalid JSON invalid_json = """###PS1JSON### {invalid json} ###PS1END### """ matches = CmdOutputMetadata.matches_ps1_metadata(invalid_json) assert len(matches) == 0 # No matches should be found for invalid JSON # Test with missing markers invalid_format = """NOT A VALID PS1 PROMPT""" matches = CmdOutputMetadata.matches_ps1_metadata(invalid_format) assert len(matches) == 0 # Test with empty PS1 metadata empty_metadata = """###PS1JSON### ###PS1END### """ matches = CmdOutputMetadata.matches_ps1_metadata(empty_metadata) assert len(matches) == 0 # No matches should be found for empty metadata # Test with whitespace in PS1 metadata 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""" # Test with only required fields 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 # Test with missing exit_code but valid pid 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 # default value 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 # Should find both blocks # Both blocks should parse successfully 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 basic pattern matching 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 with content before and after 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 with multiline content 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""" # Test with successful command 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) # Test with failed command 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""" # Test with empty strings 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 == '' # Test with malformed but valid JSON 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'