Spaces:
Build error
Build error
| 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' | |