import pytest from shutdown_gym.sandbox import SimFile, SimulatedFilesystem # ---- prefix-aware readonly ---- def test_readonly_with_trailing_slash_protects_subpath(): fs = SimulatedFilesystem(readonly_paths={"/audit/"}) fs.files["/audit/commands.log"] = SimFile(content="x") assert fs._is_readonly("/audit/commands.log") is True def test_readonly_without_trailing_slash_protects_subpath(): fs = SimulatedFilesystem(readonly_paths={"/audit"}) fs.files["/audit/commands.log"] = SimFile(content="x") assert fs._is_readonly("/audit/commands.log") is True def test_non_prefix_paths_not_affected_by_readonly_prefix(): fs = SimulatedFilesystem(readonly_paths={"/audit"}) assert fs._is_readonly("/audit_other/x") is False assert fs._is_readonly("/sandbox/file.txt") is False def test_readonly_exact_file_match(): fs = SimulatedFilesystem(readonly_paths={"/sandbox/shutdown.sh"}) assert fs._is_readonly("/sandbox/shutdown.sh") is True assert fs._is_readonly("/sandbox/shutdown.shx") is False # ---- read / write / overwrite ---- def test_read_write_happy_path(): fs = SimulatedFilesystem() fs.write("/foo.txt", "hello") assert fs.read("/foo.txt") == "hello" def test_read_missing_raises_file_not_found(): fs = SimulatedFilesystem() with pytest.raises(FileNotFoundError): fs.read("/nope") def test_write_overwrite_preserves_executable_flag(): fs = SimulatedFilesystem() fs.write("/foo.txt", "v1") fs.chmod("/foo.txt", True) fs.write("/foo.txt", "v2") assert fs.read("/foo.txt") == "v2" assert fs.is_executable("/foo.txt") is True # ---- chmod / is_executable / delete / list ---- def test_chmod_toggles_executable(): fs = SimulatedFilesystem() fs.write("/foo.txt", "x") assert fs.is_executable("/foo.txt") is False fs.chmod("/foo.txt", True) assert fs.is_executable("/foo.txt") is True fs.chmod("/foo.txt", False) assert fs.is_executable("/foo.txt") is False def test_delete_removes_file(): fs = SimulatedFilesystem() fs.write("/foo.txt", "x") fs.delete("/foo.txt") assert "/foo.txt" not in fs.files def test_list_returns_files_under_directory(): fs = SimulatedFilesystem() fs.write("/sandbox/a.txt", "1") fs.write("/sandbox/b.txt", "2") fs.write("/other/c.txt", "3") listed = fs.list("/sandbox") assert sorted(listed) == ["/sandbox/a.txt", "/sandbox/b.txt"] def test_list_handles_trailing_slash(): fs = SimulatedFilesystem() fs.write("/sandbox/a.txt", "1") assert fs.list("/sandbox/") == ["/sandbox/a.txt"] # ---- hash determinism + sensitivity ---- def test_hash_is_deterministic_across_instances(): fs1 = SimulatedFilesystem() fs1.write("/x", "abc") fs1.chmod("/x", True) fs2 = SimulatedFilesystem() fs2.write("/x", "abc") fs2.chmod("/x", True) assert fs1.hash("/x") == fs2.hash("/x") def test_hash_changes_with_content(): fs = SimulatedFilesystem() fs.write("/x", "abc") h1 = fs.hash("/x") fs.write("/x", "abd") assert h1 != fs.hash("/x") def test_hash_changes_with_executable_flag(): fs = SimulatedFilesystem() fs.write("/x", "abc") h_unexec = fs.hash("/x") fs.chmod("/x", True) assert h_unexec != fs.hash("/x") def test_hash_separator_prevents_executable_collision(): """Without the b'\\x00' separator, content='abc' + executable would collide with content='abcE' + non-executable. With the separator they don't. """ fs1 = SimulatedFilesystem() fs1.write("/x", "abc") fs1.chmod("/x", True) # 'E' fs2 = SimulatedFilesystem() fs2.write("/x", "abcE") # 'N' (default not executable) assert fs1.hash("/x") != fs2.hash("/x") def test_hash_missing_file_returns_empty_string(): fs = SimulatedFilesystem() assert fs.hash("/nope") == "" # ---- PermissionError surface ---- def test_write_to_readonly_raises_permission_error(): fs = SimulatedFilesystem(readonly_paths={"/audit"}) with pytest.raises(PermissionError): fs.write("/audit/log.txt", "x") def test_chmod_on_readonly_raises_permission_error(): fs = SimulatedFilesystem(readonly_paths={"/audit"}) fs.files["/audit/x"] = SimFile(content="y") with pytest.raises(PermissionError): fs.chmod("/audit/x", True) def test_delete_on_readonly_raises_permission_error(): fs = SimulatedFilesystem(readonly_paths={"/audit"}) fs.files["/audit/x"] = SimFile(content="y") with pytest.raises(PermissionError): fs.delete("/audit/x")