from __future__ import annotations import subprocess from pathlib import Path from types import SimpleNamespace from typing import Any, cast import pytest import pyro_mcp.workspace_disk as workspace_disk_module from pyro_mcp.workspace_disk import ( _artifact_type_from_mode, _debugfs_ls_entries, _debugfs_stat, _run_debugfs, export_workspace_disk_image, list_workspace_disk, read_workspace_disk_file, scrub_workspace_runtime_paths, ) def _run_debugfs_write(rootfs_image: Path, command: str) -> None: proc = subprocess.run( # noqa: S603 ["debugfs", "-w", "-R", command, str(rootfs_image)], text=True, capture_output=True, check=False, ) if proc.returncode != 0: message = proc.stderr.strip() or proc.stdout.strip() or command raise RuntimeError(message) def _create_rootfs_image(tmp_path: Path) -> Path: rootfs_image = tmp_path / "workspace-rootfs.ext4" with rootfs_image.open("wb") as handle: handle.truncate(16 * 1024 * 1024) proc = subprocess.run( # noqa: S603 ["mkfs.ext4", "-F", str(rootfs_image)], text=True, capture_output=True, check=False, ) if proc.returncode != 0: message = proc.stderr.strip() or proc.stdout.strip() or "mkfs.ext4 failed" raise RuntimeError(message) for directory in ( "/workspace", "/workspace/src", "/run", "/run/pyro-secrets", "/run/pyro-services", ): _run_debugfs_write(rootfs_image, f"mkdir {directory}") note_path = tmp_path / "note.txt" note_path.write_text("hello from disk\n", encoding="utf-8") child_path = tmp_path / "child.txt" child_path.write_text("nested child\n", encoding="utf-8") secret_path = tmp_path / "secret.txt" secret_path.write_text("super-secret\n", encoding="utf-8") service_path = tmp_path / "service.log" service_path.write_text("service runtime\n", encoding="utf-8") _run_debugfs_write(rootfs_image, f"write {note_path} /workspace/note.txt") _run_debugfs_write(rootfs_image, f"write {child_path} /workspace/src/child.txt") _run_debugfs_write(rootfs_image, "symlink /workspace/link note.txt") _run_debugfs_write(rootfs_image, f"write {secret_path} /run/pyro-secrets/TOKEN") _run_debugfs_write(rootfs_image, f"write {service_path} /run/pyro-services/app.log") return rootfs_image def test_workspace_disk_list_read_export_and_scrub(tmp_path: Path) -> None: rootfs_image = _create_rootfs_image(tmp_path) listing = list_workspace_disk(rootfs_image, guest_path="/workspace", recursive=True) assert listing == [ { "path": "/workspace/link", "artifact_type": "symlink", "size_bytes": 8, "link_target": "note.txt", }, { "path": "/workspace/note.txt", "artifact_type": "file", "size_bytes": 16, "link_target": None, }, { "path": "/workspace/src", "artifact_type": "directory", "size_bytes": 0, "link_target": None, }, { "path": "/workspace/src/child.txt", "artifact_type": "file", "size_bytes": 13, "link_target": None, }, ] single = list_workspace_disk(rootfs_image, guest_path="/workspace/note.txt", recursive=False) assert single == [ { "path": "/workspace/note.txt", "artifact_type": "file", "size_bytes": 16, "link_target": None, } ] read_payload = read_workspace_disk_file( rootfs_image, guest_path="/workspace/note.txt", max_bytes=5, ) assert read_payload == { "path": "/workspace/note.txt", "size_bytes": 16, "max_bytes": 5, "content": "hello", "truncated": True, } output_path = tmp_path / "workspace.ext4" exported = export_workspace_disk_image(rootfs_image, output_path=output_path) assert exported["output_path"] == str(output_path) assert exported["disk_format"] == "ext4" assert int(exported["bytes_written"]) == output_path.stat().st_size scrub_workspace_runtime_paths(rootfs_image) run_listing = list_workspace_disk(rootfs_image, guest_path="/run", recursive=True) assert run_listing == [] def test_workspace_disk_rejects_invalid_inputs(tmp_path: Path) -> None: rootfs_image = _create_rootfs_image(tmp_path) with pytest.raises(RuntimeError, match="workspace disk path does not exist"): list_workspace_disk(rootfs_image, guest_path="/missing", recursive=False) with pytest.raises(RuntimeError, match="workspace disk path does not exist"): read_workspace_disk_file( rootfs_image, guest_path="/missing.txt", max_bytes=4096, ) with pytest.raises(RuntimeError, match="regular files"): read_workspace_disk_file( rootfs_image, guest_path="/workspace/src", max_bytes=4096, ) with pytest.raises(ValueError, match="max_bytes must be positive"): read_workspace_disk_file( rootfs_image, guest_path="/workspace/note.txt", max_bytes=0, ) output_path = tmp_path / "existing.ext4" output_path.write_text("present\n", encoding="utf-8") with pytest.raises(RuntimeError, match="output_path already exists"): export_workspace_disk_image(rootfs_image, output_path=output_path) def test_workspace_disk_internal_error_paths( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: rootfs_image = tmp_path / "dummy.ext4" rootfs_image.write_bytes(b"rootfs") monkeypatch.setattr(cast(Any, workspace_disk_module).shutil, "which", lambda _name: None) with pytest.raises(RuntimeError, match="debugfs is required"): _run_debugfs(rootfs_image, "stat /workspace") monkeypatch.setattr( cast(Any, workspace_disk_module).shutil, "which", lambda _name: "/usr/bin/debugfs", ) monkeypatch.setattr( cast(Any, workspace_disk_module).subprocess, "run", lambda *args, **kwargs: SimpleNamespace( # noqa: ARG005 returncode=1, stdout="", stderr="", ), ) with pytest.raises(RuntimeError, match="debugfs command failed: stat /workspace"): _run_debugfs(rootfs_image, "stat /workspace") assert _artifact_type_from_mode("00000") is None monkeypatch.setattr(workspace_disk_module, "_run_debugfs", lambda *_args, **_kwargs: "noise") with pytest.raises(RuntimeError, match="failed to inspect workspace disk path"): _debugfs_stat(rootfs_image, "/workspace/bad") monkeypatch.setattr( workspace_disk_module, "_run_debugfs", lambda *_args, **_kwargs: "Type: fifo\nSize: 1\n", ) with pytest.raises(RuntimeError, match="unsupported workspace disk path type"): _debugfs_stat(rootfs_image, "/workspace/fifo") monkeypatch.setattr( workspace_disk_module, "_run_debugfs", lambda *_args, **_kwargs: "File not found by ext2_lookup", ) with pytest.raises(RuntimeError, match="workspace disk path does not exist"): _debugfs_ls_entries(rootfs_image, "/workspace/missing") monkeypatch.setattr( workspace_disk_module, "_debugfs_stat", lambda *_args, **_kwargs: workspace_disk_module._DebugfsStat( # noqa: SLF001 path="/workspace", artifact_type="directory", size_bytes=0, ), ) monkeypatch.setattr( workspace_disk_module, "_debugfs_ls_entries", lambda *_args, **_kwargs: [ workspace_disk_module._DebugfsDirEntry( # noqa: SLF001 name="special", path="/workspace/special", artifact_type=None, size_bytes=0, ) ], ) assert list_workspace_disk(rootfs_image, guest_path="/workspace", recursive=True) == [] monkeypatch.setattr( workspace_disk_module, "_debugfs_stat", lambda *_args, **_kwargs: workspace_disk_module._DebugfsStat( # noqa: SLF001 path="/workspace/note.txt", artifact_type="file", size_bytes=12, ), ) monkeypatch.setattr(workspace_disk_module, "_run_debugfs", lambda *_args, **_kwargs: "") with pytest.raises(RuntimeError, match="failed to dump workspace disk file"): read_workspace_disk_file( rootfs_image, guest_path="/workspace/note.txt", max_bytes=16, )