     1→import os
     2→import resource
     3→import time
     4→from typing import TYPE_CHECKING, Any
     5→
     6→from mirage.backends.local.memory import MemoryBackend
     7→from mirage.commands import COMMAND_COST, COMMANDS
     8→from mirage.types import CostLevel
     9→
    10→if TYPE_CHECKING:
    11→    from mirage.workspace import Workspace
    12→
    13→_server_start_time: float = 0.0
    14→
    15→
    16→def _count_dirty(ws: "Workspace") -> int:
    17→    memory_mounts = {
    18→        m.prefix
    19→        for m in ws._registry.mounts() if isinstance(m.backend, MemoryBackend)
    20→    }
    21→    return len([
    22→        i for i in ws._tracker.dirty() if i.backend_mount not in memory_mounts
    23→    ])
    24→
    25→
    26→def set_server_start_time(t: float) -> None:
    27→    global _server_start_time
    28→    _server_start_time = t
    29→
    30→
    31→def global_status(sessions: dict[str, Any]) -> dict:
    32→    rss = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
    33→    if os.uname().sysname == "Darwin":
    34→        rss_bytes = rss
    35→    else:
    36→        rss_bytes = rss * 1024
    37→    total_cache = 0
    38→    for s in sessions.values():
    39→        if s.ws is not None:
    40→            total_cache += s.ws._cache.size
    41→    uptime_s = int(time.monotonic() - _server_start_time)
    42→    return {
    43→        "rss_bytes": rss_bytes,
    44→        "total_sessions": len(sessions),
    45→        "total_cache_bytes": total_cache,
    46→        "uptime_seconds": uptime_s,
    47→    }
    48→
    49→
    50→def list_sessions(sessions: dict[str, Any]) -> list[dict]:
    51→    results = []
    52→    for s in sessions.values():
    53→        entry: dict[str, Any] = {
    54→            "id": s.id,
    55→            "fuse_mounted": s.fuse_mounted,
    56→            "mode": s.mode,
    57→            "mounts": s.mounts,
    58→        }
    59→        if s.ws is not None:
    60→            entry["cache_used"] = s.ws._cache.size
    61→            entry["cache_limit"] = s.ws._cache.limit
    62→            entry["dirty_count"] = len(s.ws._tracker.dirty())
    63→            entry["total_commands"] = s.command_stats.total_commands
    64→        results.append(entry)
    65→    return results
    66→
    67→
    68→def workspace_status(session: Any) -> dict:
    69→    ws: Workspace = session.ws
    70→    backends_health = []
    71→    for mount in ws._registry.mounts():
    72→        backends_health.append({
    73→            "mount": mount.prefix,
    74→            "backend": type(mount.backend).__name__,
    75→        })
    76→    return {
    77→        "cache_used": ws._cache.size,
    78→        "cache_limit": ws._cache.limit,
    79→        "dirty_inodes": _count_dirty(ws),
    80→        "backends": backends_health,
    81→        "last_command": session.command_stats.last_command,
    82→        "total_commands": session.command_stats.total_commands,
    83→        "total_errors": session.command_stats.total_errors,
    84→    }
    85→
    86→
    87→def workspace_commands(session: Any) -> list[dict]:
    88→    result = []
    89→    for name in sorted(COMMANDS.keys()):
    90→        cost = COMMAND_COST.get(name, CostLevel.CHEAP)
    91→        result.append({
    92→            "name": name,
    93→            "source": "builtin",
    94→            "cost_level": cost.value,
    95→        })
    96→    for name, ext in session.registry._external.items():
    97→        result.append({
    98→            "name": name,
    99→            "source": "external",
   100→            "cost_level": "unknown",
   101→            "description": ext.description,
   102→            "exec": ext.exec,
   103→        })
   104→    return result
   105→
   106→
   107→def workspace_memory(session: Any) -> dict:
   108→    ws: Workspace = session.ws
   109→    cache_entries = {}
   110→    for path, entry in ws._cache._store.items():
   111→        cache_entries[path] = len(entry.data)
   112→    dirty_paths = [i.virtual_path for i in ws._tracker.dirty()]
   113→    backend_memory: dict[str, dict] = {}
   114→    for mount in ws._registry.mounts():
   115→        if isinstance(mount.backend, MemoryBackend):
   116→            total = sum(len(v) for v in mount.backend._files.values())
   117→            backend_memory[mount.prefix] = {
   118→                "type": "MemoryBackend",
   119→                "file_count": len(mount.backend._files),
   120→                "total_bytes": total,
   121→            }
   122→    return {
   123→        "cache": {
   124→            "used": ws._cache.size,
   125→            "limit": ws._cache.limit,
   126→            "entry_count": len(ws._cache._store),
   127→            "entries": cache_entries,
   128→        },
   129→        "dirty_tracker": {
   130→            "total_inodes": len(ws._tracker._inodes),
   131→            "dirty_paths": dirty_paths,
   132→        },
   133→        "backend_memory": backend_memory,
   134→    }
   135→
   136→
   137→def workspace_transfers(session: Any) -> dict:
   138→    ws: Workspace = session.ws
   139→    return workspace_transfers_from_ws(ws)
   140→
   141→
   142→def workspace_status_from_ws(ws: "Workspace") -> dict:
   143→    backends_health = []
   144→    for mount in ws._registry.mounts():
   145→        backends_health.append({
   146→            "mount": mount.prefix,
   147→            "backend": type(mount.backend).__name__,
   148→        })
   149→    return {
   150→        "cache_used": ws._cache.size,
   151→        "cache_limit": ws._cache.limit,
   152→        "dirty_inodes": _count_dirty(ws),
   153→        "backends": backends_health,
   154→        "last_command": None,
   155→        "total_commands": 0,
   156→        "total_errors": 0,
   157→    }
   158→
   159→
   160→def workspace_transfers_from_ws(ws: "Workspace") -> dict:
   161→    per_backend: dict[str, dict] = {}
   162→    for prefix, stats in ws._transfer_stats.items():
   163→        total_reads = stats.read_ops
   164→        avg_read_ms = (stats.total_read_ms /
   165→                       total_reads if total_reads > 0 else 0)
   166→        total_cache_ops = stats.cache_hits + stats.cache_misses
   167→        hit_rate = (stats.cache_hits /
   168→                    total_cache_ops if total_cache_ops > 0 else 0.0)
   169→        per_backend[prefix] = {
   170→            "bytes_read": stats.bytes_read,
   171→            "bytes_written": stats.bytes_written,
   172→            "read_ops": stats.read_ops,
   173→            "write_ops": stats.write_ops,
   174→            "avg_read_latency_ms": round(avg_read_ms, 1),
   175→            "cache_hit_rate": round(hit_rate, 3),
   176→        }
   177→    return per_backend
   178→


     1→import pytest
     2→
     3→from mirage.backends.local.memory import MemoryBackend
     4→from mirage.types import MountMode, SyncPolicy
     5→from mirage.workspace import Workspace
     6→
     7→
     8→def test_lock_paths_blocks_write():
     9→    ws = Workspace({"/data/": MemoryBackend()},
    10→                   mode=MountMode.WRITE,
    11→                   sync_policy=SyncPolicy.IMMEDIATE)
    12→    ws.write("/data/file.txt", b"hello")
    13→    with ws.lock_paths(["/data/file.txt"]):
    14→        with pytest.raises(PermissionError, match="locked"):
    15→            ws.write("/data/file.txt", b"world")
    16→
    17→
    18→def test_lock_paths_unlocks_on_exit():
    19→    ws = Workspace({"/data/": MemoryBackend()},
    20→                   mode=MountMode.WRITE,
    21→                   sync_policy=SyncPolicy.IMMEDIATE)
    22→    ws.write("/data/file.txt", b"hello")
    23→    with ws.lock_paths(["/data/file.txt"]):
    24→        pass
    25→    ws.write("/data/file.txt", b"world")
    26→    assert ws.cat("/data/file.txt") == b"world"
    27→
    28→
    29→def test_lock_paths_unlocks_on_exception():
    30→    ws = Workspace({"/data/": MemoryBackend()},
    31→                   mode=MountMode.WRITE,
    32→                   sync_policy=SyncPolicy.IMMEDIATE)
    33→    ws.write("/data/file.txt", b"hello")
    34→    with pytest.raises(RuntimeError):
    35→        with ws.lock_paths(["/data/file.txt"]):
    36→            raise RuntimeError("boom")
    37→    ws.write("/data/file.txt", b"world")
    38→    assert ws.cat("/data/file.txt") == b"world"
    39→
    40→
    41→def test_lock_allows_read():
    42→    ws = Workspace({"/data/": MemoryBackend()},
    43→                   mode=MountMode.WRITE,
    44→                   sync_policy=SyncPolicy.IMMEDIATE)
    45→    ws.write("/data/file.txt", b"hello")
    46→    with ws.lock_paths(["/data/file.txt"]):
    47→        assert ws.cat("/data/file.txt") == b"hello"
    48→
    49→
    50→def test_lock_multiple_paths():
    51→    ws = Workspace({"/data/": MemoryBackend()},
    52→                   mode=MountMode.WRITE,
    53→                   sync_policy=SyncPolicy.IMMEDIATE)
    54→    ws.write("/data/a.txt", b"a")
    55→    ws.write("/data/b.txt", b"b")
    56→    with ws.lock_paths(["/data/a.txt", "/data/b.txt"]):
    57→        with pytest.raises(PermissionError, match="locked"):
    58→            ws.write("/data/a.txt", b"x")
    59→        with pytest.raises(PermissionError, match="locked"):
    60→            ws.write("/data/b.txt", b"x")
    61→
    62→
    63→def test_unlock_paths_force():
    64→    ws = Workspace({"/data/": MemoryBackend()},
    65→                   mode=MountMode.WRITE,
    66→                   sync_policy=SyncPolicy.IMMEDIATE)
    67→    ws.write("/data/file.txt", b"hello")
    68→    ws._lock_path("/data/file.txt")
    69→    with pytest.raises(PermissionError, match="locked"):
    70→        ws.write("/data/file.txt", b"world")
    71→    ws.unlock_paths(["/data/file.txt"])
    72→    ws.write("/data/file.txt", b"world")
    73→


     1→import os
     2→import signal
     3→import subprocess
     4→import sys
     5→import time
     6→import uuid
     7→from datetime import datetime, timezone
     8→
     9→from pydantic import BaseModel, ConfigDict, Field
    10→
    11→from mirage.backends.loader import load_backend_class
    12→from mirage.cli.config import parse_mount_uri, split_mount_spec
    13→from mirage.commands import COMMANDS, _resolve
    14→from mirage.commands.builtin import nav
    15→from mirage.commands.custom.registry import CommandRegistry, ExternalCommand
    16→from mirage.fuse_ops import FUSE_OPS
    17→from mirage.fuse_ops.loader import (build_registry_from_config,
    18→                                    load_fuse_op_class)
    19→from mirage.fuse_ops.registry import FuseOpRegistry
    20→from mirage.observe.entries import (CommandEntry, CommandResult, DispatchInfo,
    21→                                    ParsedCommand, SessionEntry, WorkspaceInfo)
    22→from mirage.observe.session_log import get_or_create_log
    23→from mirage.runtime.base import _BLOCKED, CommandNotAllowedError
    24→from mirage.server.service.base import MirageService
    25→from mirage.server.service.cache import FileCache
    26→from mirage.types import (CommandStats, ConsistencyPolicy, FileStat, MountMode,
    27→                          RuntimeType, SyncPolicy)
    28→from mirage.workspace import Workspace
    29→
    30→
    31→class WorkspaceSession(BaseModel):
    32→    model_config = ConfigDict(arbitrary_types_allowed=True)
    33→
    34→    id: str
    35→    ws: Workspace | None
    36→    registry: CommandRegistry | None
    37→    cwd: str = "/"
    38→    fuse_mounted: bool = False
    39→    mode: MountMode = MountMode.WRITE
    40→    mountpoint: str | None = None
    41→    pid: int | None = None
    42→    mounts: list[dict] = Field(default_factory=list)
    43→    runtime_type: RuntimeType | None = None
    44→    fuse_registry: FuseOpRegistry = Field(default_factory=FuseOpRegistry)
    45→    command_stats: CommandStats = Field(default_factory=CommandStats)
    46→    created_at: float = Field(default_factory=time.monotonic)
    47→    reported_status: dict | None = None
    48→    reported_transfers: dict | None = None
    49→
    50→
    51→def _unmount_fuse(mountpoint: str, pid: int | None) -> None:
    52→    if sys.platform == "darwin":
    53→        subprocess.run(["diskutil", "unmount", "force", mountpoint],
    54→                       capture_output=True)
    55→    else:
    56→        subprocess.run(["fusermount", "-u", mountpoint], capture_output=True)
    57→    if pid:
    58→        try:
    59→            os.kill(pid, signal.SIGTERM)
    60→        except ProcessLookupError:
    61→            pass
    62→
    63→
    64→class LocalService(MirageService):
    65→
    66→    def __init__(self) -> None:
    67→        self._sessions: dict[str, WorkspaceSession] = {}
    68→        self._file_cache = FileCache()
    69→
    70→    def _get_session(self, ws_id: str) -> WorkspaceSession:
    71→        session = self._sessions.get(ws_id)
    72→        if session is None:
    73→            raise KeyError(f"workspace {ws_id!r} not found")
    74→        if session.fuse_mounted:
    75→            raise ValueError("not supported for fuse workspaces")
    76→        return session
    77→
    78→    def create_workspace(self, config: dict) -> str:
    79→        mode = config.get("mode", "write")
    80→        consistency = config.get("consistency", "none")
    81→        default_mode = MountMode(mode)
    82→        default_consistency = ConsistencyPolicy(consistency)
    83→        mounts = config.get("mounts", {})
    84→
    85→        backends: dict = {}
    86→        for prefix, value in mounts.items():
    87→            if isinstance(value, str):
    88→                backends[prefix] = parse_mount_uri(value)
    89→            else:
    90→                backend_spec = value.get("backend")
    91→                if backend_spec:
    92→                    cls = load_backend_class(backend_spec)
    93→                    backend = cls(**(value.get("args") or {}))
    94→                else:
    95→                    backend = parse_mount_uri(value.get("uri") or "")
    96→                mount_mode = (MountMode(value["mode"])
    97→                              if value.get("mode") else default_mode)
    98→                mount_consistency = (ConsistencyPolicy(value["consistency"])
    99→                                     if value.get("consistency") else None)
   100→                has_override = (mount_mode != default_mode
   101→                                or mount_consistency is not None)
   102→                if has_override and mount_consistency is not None:
   103→                    backends[prefix] = (backend, mount_mode, mount_consistency)
   104→                elif has_override:
   105→                    backends[prefix] = (backend, mount_mode)
   106→                else:
   107→                    backends[prefix] = backend
   108→
   109→        ws = Workspace(
   110→            backends,
   111→            sync_policy=SyncPolicy(config.get("sync_policy", "staged")),
   112→            mode=default_mode,
   113→            consistency=default_consistency,
   114→            ttl_seconds=config.get("ttl_seconds", 300),
   115→        )
   116→
   117→        mounts_info: list[dict] = []
   118→        for prefix, value in mounts.items():
   119→            if isinstance(value, str):
   120→                uri = value
   121→            else:
   122→                uri = value.get("uri") or ""
   123→                if not uri and value.get("backend"):
   124→                    uri = f"{value['backend']}://"
   125→            mounts_info.append({"prefix": prefix, "uri": uri})
   126→
   127→        ws_id = str(uuid.uuid4())[:8]
   128→        registry = CommandRegistry()
   129→        fuse_ops_config = config.get("fuse_ops") or {}
   130→        fuse_registry = build_registry_from_config(fuse_ops_config)
   131→        runtime = config.get("runtime")
   132→        rt_type = RuntimeType(runtime) if runtime else None
   133→
   134→        self._sessions[ws_id] = WorkspaceSession(
   135→            id=ws_id,
   136→            ws=ws,
   137→            registry=registry,
   138→            mode=mode,
   139→            mounts=mounts_info,
   140→            runtime_type=rt_type,
   141→            fuse_registry=fuse_registry,
   142→        )
   143→
   144→        log = get_or_create_log(ws_id)
   145→        mount_info = {}
   146→        for m in ws._registry.mounts():
   147→            mount_info[m.prefix] = {
   148→                "type": type(m.backend).__name__,
   149→                "mode": m.mode.value,
   150→            }
   151→        session_entry = SessionEntry(
   152→            timestamp=datetime.now(timezone.utc).isoformat(),
   153→            workspace=WorkspaceInfo(
   154→                mounts=mount_info,
   155→                cache_limit=str(ws._cache.limit),
   156→                runtime_type=rt_type.value if rt_type else None,
   157→            ),
   158→            commands=registry.list_commands(),
   159→        )
   160→        log.write_session(session_entry)
   161→        return ws_id
   162→
   163→    def delete_workspace(self, ws_id: str) -> None:
   164→        session = self._sessions.get(ws_id)
   165→        if session is None:
   166→            raise KeyError(f"workspace {ws_id!r} not found")
   167→        if session.fuse_mounted and session.mountpoint:
   168→            _unmount_fuse(session.mountpoint, session.pid)
   169→        self._file_cache.clear(ws_id)
   170→        del self._sessions[ws_id]
   171→
   172→    def list_workspaces(self) -> list[dict]:
   173→        return [{
   174→            "id": s.id,
   175→            "fuse_mounted": s.fuse_mounted,
   176→            "mode": s.mode,
   177→            "mountpoint": s.mountpoint,
   178→            "mounts": s.mounts,
   179→            "runtime": s.runtime_type.value if s.runtime_type else None,
   180→        } for s in self._sessions.values()]
   181→
   182→    def exec(
   183→        self,
   184→        ws_id: str,
   185→        command: str,
   186→        cwd: str | None = None,
   187→        stdin: bytes | None = None,
   188→        input_files: dict[str, tuple[str, bytes | None]] | None = None,
   189→    ) -> tuple[bool, str, dict[str, bytes] | None, dict[str, str] | None]:
   190→        session = self._get_session(ws_id)
   191→        if cwd is not None:
   192→            session.cwd = cwd
   193→
   194→        cache_status: dict[str, str] = {}
   195→        if input_files:
   196→            for path, (checksum, data) in input_files.items():
   197→                hit, _ = self._file_cache.get(ws_id, path, checksum)
   198→                if hit:
   199→                    cache_status[path] = "hit"
   200→                elif data is not None:
   201→                    self._file_cache.put(ws_id, path, checksum, data)
   202→                    session.ws.write(path, data)
   203→                    cache_status[path] = "miss"
   204→                else:
   205→                    return (False, f"cache miss: {path}", None, None)
   206→
   207→        cmd_name = command.strip().split()[0] if command.strip() else ""
   208→        session.command_stats.total_commands += 1
   209→        session.command_stats.command_counts[cmd_name] = (
   210→            session.command_stats.command_counts.get(cmd_name, 0) + 1)
   211→        session.command_stats.last_command = command
   212→        session.command_stats.last_timestamp = datetime.now(
   213→            timezone.utc).isoformat()
   214→        t0 = time.monotonic()
   215→        try:
   216→            if session.runtime_type is not None and cmd_name in _BLOCKED:
   217→                raise CommandNotAllowedError(
   218→                    f"{cmd_name!r} is not allowed in this runtime")
   219→            output = session.registry.run(session.ws,
   220→                                          command,
   221→                                          cwd=session.cwd,
   222→                                          stdin=stdin)
   223→            duration_ms = int((time.monotonic() - t0) * 1000)
   224→            if command.strip().startswith("cd "):
   225→                parts = command.strip().split(None, 1)
   226→                if len(parts) > 1:
   227→                    session.cwd = nav.cd(session.ws, session.cwd,
   228→                                         _resolve(session.cwd, parts[1]))
   229→            args = command.strip().split()[1:] if command.strip() else []
   230→            source = "builtin" if cmd_name in COMMANDS else "external"
   231→            log = get_or_create_log(ws_id)
   232→            entry = CommandEntry(
   233→                timestamp=datetime.now(timezone.utc).isoformat(),
   234→                raw=command,
   235→                parsed=ParsedCommand(
   236→                    pipeline=[command],
   237→                    args={cmd_name: args},
   238→                ),
   239→                dispatch=[
   240→                    DispatchInfo(
   241→                        command=cmd_name,
   242→                        handler=f"_{cmd_name}"
   243→                        if source == "builtin" else cmd_name,
   244→                        source=source,
   245→                    )
   246→                ],
   247→                result=CommandResult(
   248→                    exit_code=0,
   249→                    stdout_bytes=len(output.encode()) if output else 0,
   250→                    duration_ms=duration_ms,
   251→                ),
   252→            )
   253→            log.write_command(entry)
   254→
   255→            return (True, output, None, cache_status or None)
   256→        except Exception as e:
   257→            duration_ms = int((time.monotonic() - t0) * 1000)
   258→            session.command_stats.total_errors += 1
   259→            log = get_or_create_log(ws_id)
   260→            entry = CommandEntry(
   261→                timestamp=datetime.now(timezone.utc).isoformat(),
   262→                raw=command,
   263→                result=CommandResult(exit_code=1,
   264→                                     stdout_bytes=0,
   265→                                     duration_ms=duration_ms),
   266→            )
   267→            log.write_command(entry)
   268→            return (False, str(e), None, None)
   269→
   270→    def list_commands(self, ws_id: str) -> list[str]:
   271→        session = self._get_session(ws_id)
   272→        return session.registry.list_commands()
   273→
   274→    def register_command(self,
   275→                         ws_id: str,
   276→                         name: str,
   277→                         exec_str: str,
   278→                         description: str = "") -> None:
   279→        session = self._get_session(ws_id)
   280→        session.registry.register(
   281→            ExternalCommand(name=name, exec=exec_str, description=description))
   282→
   283→    def sync(self, ws_id: str) -> list[dict]:
   284→        session = self._get_session(ws_id)
   285→        results = session.ws.sync()
   286→        return [{"path": r.path, "status": r.status} for r in results]
   287→
   288→    def status(self, ws_id: str) -> list[dict]:
   289→        session = self._get_session(ws_id)
   290→        inodes = session.ws.status()
   291→        return [{
   292→            "path": i.virtual_path,
   293→            "dirty": i.dirty,
   294→            "size": i.size
   295→        } for i in inodes]
   296→
   297→    # ── File I/O (backend-level) ─────────────────────────────────────────
   298→
   299→    def io_read(self, ws_id: str, path: str, offset: int, size: int) -> bytes:
   300→        session = self._get_session(ws_id)
   301→        backend, backend_path, _ = session.ws._registry.resolve(path)
   302→        return backend.read(backend_path, offset, size)
   303→
   304→    def io_write(self, ws_id: str, path: str, offset: int, data: bytes) -> int:
   305→        session = self._get_session(ws_id)
   306→        backend, backend_path, _ = session.ws._registry.resolve(path)
   307→        return backend.write(backend_path, offset, data)
   308→
   309→    def io_stat(self, ws_id: str, path: str) -> FileStat:
   310→        session = self._get_session(ws_id)
   311→        backend, backend_path, _ = session.ws._registry.resolve(path)
   312→        return backend.stat(backend_path)
   313→
   314→    def io_readdir(self,
   315→                   ws_id: str,
   316→                   path: str,
   317→                   cursor: str | None = None) -> tuple[list[str], str | None]:
   318→        session = self._get_session(ws_id)
   319→        backend, backend_path, _ = session.ws._registry.resolve(path)
   320→        return backend.readdir(backend_path, cursor)
   321→
   322→    def io_create(self, ws_id: str, path: str) -> None:
   323→        session = self._get_session(ws_id)
   324→        backend, backend_path, _ = session.ws._registry.resolve(path)
   325→        backend.create(backend_path)
   326→
   327→    def io_unlink(self, ws_id: str, path: str) -> None:
   328→        session = self._get_session(ws_id)
   329→        backend, backend_path, _ = session.ws._registry.resolve(path)
   330→        backend.unlink(backend_path)
   331→
   332→    def io_mkdir(self, ws_id: str, path: str) -> None:
   333→        session = self._get_session(ws_id)
   334→        backend, backend_path, _ = session.ws._registry.resolve(path)
   335→        backend.mkdir(backend_path)
   336→
   337→    def io_rmdir(self, ws_id: str, path: str) -> None:
   338→        session = self._get_session(ws_id)
   339→        backend, backend_path, _ = session.ws._registry.resolve(path)
   340→        backend.rmdir(backend_path)
   341→
   342→    def io_rename(self, ws_id: str, src: str, dst: str) -> None:
   343→        session = self._get_session(ws_id)
   344→        backend, src_path, _ = session.ws._registry.resolve(src)
   345→        _, dst_path, _ = session.ws._registry.resolve(dst)
   346→        backend.rename(src_path, dst_path)
   347→
   348→    def io_truncate(self, ws_id: str, path: str, size: int) -> None:
   349→        session = self._get_session(ws_id)
   350→        backend, backend_path, _ = session.ws._registry.resolve(path)
   351→        backend.truncate(backend_path, size)
   352→
   353→    def io_fuse_cat(self, ws_id: str, path: str) -> bytes:
   354→        session = self._get_session(ws_id)
   355→        st = session.ws.stat(path)
   356→        op = session.fuse_registry.resolve(path, st.size or 0, session.ws)
   357→        ctx = op.on_open(session.ws, path, st.size or 0)
   358→        return op.read(session.ws, path, 0, st.size or 0, ctx)
   359→
   360→    # ── FuseOp management ────────────────────────────────────────────────
   361→
   362→    def list_fuse_ops(self, ws_id: str) -> list[dict]:
   363→        session = self._get_session(ws_id)
   364→        return session.fuse_registry.list_ops()
   365→
   366→    def register_fuse_op(self, ws_id: str, pattern: str, handler: str) -> None:
   367→        session = self._get_session(ws_id)
   368→        op = FUSE_OPS.get(handler)
   369→        if op is None:
   370→            try:
   371→                cls = load_fuse_op_class(handler)
   372→                op = cls()
   373→            except (ValueError, Exception) as e:
   374→                raise ValueError(f"unknown handler: {handler} ({e})")
   375→        session.fuse_registry.register(pattern, op)
   376→
   377→    # ── FUSE-specific (not part of MirageService ABC) ────────────────────
   378→
   379→    def register_fuse(self,
   380→                      mountpoint: str,
   381→                      pid: int,
   382→                      mount_specs: list[str] | None = None,
   383→                      runtime: str | None = None) -> str:
   384→        fuse_mounts: list[dict] = []
   385→        for spec in (mount_specs or []):
   386→            try:
   387→                uri, prefix = split_mount_spec(spec)
   388→                fuse_mounts.append({"prefix": prefix, "uri": uri})
   389→            except ValueError:
   390→                pass
   391→        ws_id = str(uuid.uuid4())[:8]
   392→        rt_type = RuntimeType(runtime) if runtime else None
   393→        self._sessions[ws_id] = WorkspaceSession(
   394→            id=ws_id,
   395→            ws=None,
   396→            registry=None,
   397→            fuse_mounted=True,
   398→            mountpoint=mountpoint,
   399→            pid=pid,
   400→            mounts=fuse_mounts,
   401→            runtime_type=rt_type,
   402→        )
   403→        mount_info = {m["prefix"]: {"type": m["uri"]} for m in fuse_mounts}
   404→        log = get_or_create_log(ws_id)
   405→        session_entry = SessionEntry(
   406→            timestamp=datetime.now(timezone.utc).isoformat(),
   407→            workspace=WorkspaceInfo(
   408→                mounts=mount_info,
   409→                runtime_type=rt_type.value if rt_type else None,
   410→            ),
   411→        )
   412→        log.write_session(session_entry)
   413→        return ws_id
   414→
   415→    def report_fuse_stats(self,
   416→                          mountpoint: str,
   417→                          status: dict,
   418→                          transfers: dict | None = None,
   419→                          ops: list[dict] | None = None) -> bool:
   420→        for s in self._sessions.values():
   421→            if s.fuse_mounted and s.mountpoint == mountpoint:
   422→                s.reported_status = status
   423→                s.reported_transfers = transfers or {}
   424→                if ops:
   425→                    log = get_or_create_log(s.id)
   426→                    for op in ops:
   427→                        log._append(op)
   428→                return True
   429→        return False
   430→


     1→from datetime import datetime, timezone
     2→
     3→from mirage.backends.local.memory import MemoryBackend
     4→from mirage.types import ConsistencyPolicy, MountMode
     5→from mirage.workspace import Workspace
     6→
     7→
     8→def _make_ws(consistency=ConsistencyPolicy.NONE, ttl_seconds=300):
     9→    backend = MemoryBackend()
    10→    backend.put("/file.txt", b"original")
    11→    ws = Workspace(
    12→        {"/data/": backend},
    13→        mode=MountMode.WRITE,
    14→        consistency=consistency,
    15→        ttl_seconds=ttl_seconds,
    16→    )
    17→    return ws, backend
    18→
    19→
    20→def test_consistency_none_caches_forever():
    21→    ws, backend = _make_ws(ConsistencyPolicy.NONE)
    22→    assert ws.cat("/data/file.txt") == b"original"
    23→    backend._files["/file.txt"] = b"updated"
    24→    backend._modified["/file.txt"] = datetime.now(timezone.utc).isoformat()
    25→    assert ws.cat("/data/file.txt") == b"original"
    26→
    27→
    28→def test_consistency_always_refetches():
    29→    ws, backend = _make_ws(ConsistencyPolicy.ALWAYS)
    30→    assert ws.cat("/data/file.txt") == b"original"
    31→    backend._files["/file.txt"] = b"updated"
    32→    backend._modified["/file.txt"] = datetime.now(timezone.utc).isoformat()
    33→    assert ws.cat("/data/file.txt") == b"updated"
    34→
    35→
    36→def test_consistency_ttl_fresh_within_window():
    37→    ws, backend = _make_ws(ConsistencyPolicy.TTL, ttl_seconds=300)
    38→    assert ws.cat("/data/file.txt") == b"original"
    39→    backend._files["/file.txt"] = b"updated"
    40→    backend._modified["/file.txt"] = datetime.now(timezone.utc).isoformat()
    41→    assert ws.cat("/data/file.txt") == b"original"
    42→
    43→
    44→def test_consistency_ttl_stale_after_expiry():
    45→    ws, backend = _make_ws(ConsistencyPolicy.TTL, ttl_seconds=10)
    46→    assert ws.cat("/data/file.txt") == b"original"
    47→    backend._files["/file.txt"] = b"updated"
    48→    backend._modified["/file.txt"] = datetime.now(timezone.utc).isoformat()
    49→    entry = ws._cache.get("/data/file.txt")
    50→    entry.cached_at -= 20
    51→    assert ws.cat("/data/file.txt") == b"updated"
    52→
    53→
    54→def test_consistency_lazy_unchanged():
    55→    ws, backend = _make_ws(ConsistencyPolicy.LAZY)
    56→    assert ws.cat("/data/file.txt") == b"original"
    57→    assert ws.cat("/data/file.txt") == b"original"
    58→
    59→
    60→def test_consistency_lazy_changed():
    61→    ws, backend = _make_ws(ConsistencyPolicy.LAZY)
    62→    assert ws.cat("/data/file.txt") == b"original"
    63→    backend._files["/file.txt"] = b"updated-content"
    64→    backend._modified["/file.txt"] = datetime.now(timezone.utc).isoformat()
    65→    assert ws.cat("/data/file.txt") == b"updated-content"
    66→
    67→
    68→def test_dirty_file_skips_revalidation():
    69→    ws, backend = _make_ws(ConsistencyPolicy.ALWAYS)
    70→    ws.write("/data/file.txt", b"local-edit")
    71→    backend._files["/file.txt"] = b"remote-change"
    72→    assert ws.cat("/data/file.txt") == b"local-edit"
    73→
    74→
    75→def test_per_mount_consistency():
    76→    be_none = MemoryBackend()
    77→    be_none.put("/f.txt", b"v1")
    78→    be_always = MemoryBackend()
    79→    be_always.put("/f.txt", b"v1")
    80→    ws = Workspace(
    81→        {
    82→            "/a/": be_none,
    83→            "/b/": (be_always, MountMode.READ, ConsistencyPolicy.ALWAYS),
    84→        },
    85→        mode=MountMode.READ,
    86→        consistency=ConsistencyPolicy.NONE,
    87→    )
    88→    assert ws.cat("/a/f.txt") == b"v1"
    89→    assert ws.cat("/b/f.txt") == b"v1"
    90→    be_none._files["/f.txt"] = b"v2"
    91→    be_none._modified["/f.txt"] = datetime.now(timezone.utc).isoformat()
    92→    be_always._files["/f.txt"] = b"v2"
    93→    be_always._modified["/f.txt"] = datetime.now(timezone.utc).isoformat()
    94→    assert ws.cat("/a/f.txt") == b"v1"
    95→    assert ws.cat("/b/f.txt") == b"v2"
    96→


     1→"""
     2→Example: R2Backend + MemoryRuntime
     3→
     4→Demonstrates read commands against a real Cloudflare R2 bucket.
     5→Requires: uv sync --extra r2
     6→
     7→Credentials are loaded from .env.development (see .env.example).
     8→"""
     9→
    10→import os
    11→
    12→from dotenv import load_dotenv
    13→
    14→from mirage import MountMode, SyncPolicy, Workspace
    15→from mirage.backends.object_storage.r2 import R2Backend, R2Config
    16→from mirage.runtime.memory import MemoryRuntime
    17→
    18→load_dotenv(".env.development")
    19→
    20→CHOSEN_PDF = os.environ.get("CHOSEN_PDF",
    21→                            "")  # set to a filename within Review/
    22→
    23→config = R2Config(
    24→    account_id=os.environ["R2_ACCOUNT_ID"],
    25→    bucket=os.environ["R2_BUCKET"],
    26→    access_key_id=os.environ["R2_ACCESS_KEY_ID"],
    27→    secret_access_key=os.environ["R2_SECRET_ACCESS_KEY"],
    28→    region=os.environ.get("R2_REGION", "auto"),
    29→)
    30→
    31→ws = Workspace(
    32→    {"/r2/": R2Backend(config)},
    33→    sync_policy=SyncPolicy.IMMEDIATE,
    34→    mode=MountMode.READ,
    35→)
    36→
    37→rt = MemoryRuntime(ws)
    38→
    39→print("=== ls /r2/Review ===")
    40→print(rt.run("ls /r2/Review"))
    41→
    42→print("\n=== tree /r2/Review ===")
    43→print(rt.run("tree /r2/Review"))
    44→
    45→print("\n=== find /r2/Review ===")
    46→print(rt.run("find /r2/Review"))
    47→
    48→print("\n=== stat first file ===")
    49→files = ws.ls("/r2/Review")
    50→if files:
    51→    first = files[0]
    52→    s = ws.stat(first)
    53→    print(f"{first}")
    54→    print(f"  size: {s.size}  modified: {s.modified}")
    55→    if first.lower().endswith(".pdf"):
    56→        print("  (PDF — use a PDF parser for text extraction)")
    57→    else:
    58→        print(ws.cat(first).decode(errors="replace"))
    59→
    60→print("\n=== stat /r2/Review ===")
    61→print(rt.run("stat /r2/Review"))
    62→
    63→print("\n=== du /r2/Review ===")
    64→print(rt.run("du /r2/Review"), "bytes")
    65→


     1→import pytest
     2→
     3→from mirage.backends.local.memory import MemoryBackend
     4→from mirage.workspace.sync import DirtyTracker
     5→
     6→
     7→@pytest.fixture
     8→def tracker():
     9→    return DirtyTracker()
    10→
    11→
    12→@pytest.fixture
    13→def backend():
    14→    return MemoryBackend()
    15→
    16→
    17→def test_on_read_records_clean_inode(tracker):
    18→    tracker.on_read("/f.txt", b"data", "/tmp/")
    19→    inode = tracker.get("/f.txt")
    20→    assert inode is not None
    21→    assert inode.dirty is False
    22→    assert inode.checksum is not None
    23→
    24→
    25→def test_on_read_updates_existing(tracker):
    26→    tracker.on_read("/f.txt", b"old", "/tmp/")
    27→    tracker.on_read("/f.txt", b"new", "/tmp/")
    28→    inode = tracker.get("/f.txt")
    29→    assert inode.size == 3
    30→
    31→
    32→def test_on_write_new_file_marks_dirty(tracker):
    33→    tracker.on_write("/f.txt", b"data", "/tmp/")
    34→    inode = tracker.get("/f.txt")
    35→    assert inode.dirty is True
    36→    assert inode.remote_checksum is None
    37→
    38→
    39→def test_on_write_existing_file_updates_checksum(tracker):
    40→    tracker.on_read("/f.txt", b"old", "/tmp/")
    41→    old_checksum = tracker.get("/f.txt").checksum
    42→    tracker.on_write("/f.txt", b"new data", "/tmp/")
    43→    inode = tracker.get("/f.txt")
    44→    assert inode.dirty is True
    45→    assert inode.checksum != old_checksum
    46→    assert inode.size == 8
    47→
    48→
    49→def test_on_delete_marks_dirty_null_checksum(tracker):
    50→    tracker.on_read("/f.txt", b"data", "/tmp/")
    51→    tracker.on_delete("/f.txt")
    52→    inode = tracker.get("/f.txt")
    53→    assert inode.dirty is True
    54→    assert inode.checksum is None
    55→    assert inode.size is None
    56→
    57→
    58→def test_on_sync_uploads_data(tracker, backend):
    59→    tracker.on_write("/f.txt", b"hello", "/tmp/")
    60→    backend.create("/f.txt")
    61→    result = tracker.on_sync("/f.txt", "/f.txt", b"hello", backend)
    62→    assert result.status == "ok"
    63→    assert result.bytes == 5
    64→
    65→
    66→def test_on_sync_clears_dirty_flag(tracker, backend):
    67→    tracker.on_write("/f.txt", b"hello", "/tmp/")
    68→    backend.create("/f.txt")
    69→    tracker.on_sync("/f.txt", "/f.txt", b"hello", backend)
    70→    inode = tracker.get("/f.txt")
    71→    assert inode.dirty is False
    72→
    73→
    74→def test_on_sync_delete_calls_unlink(tracker, backend):
    75→    backend.create("/f.txt")
    76→    tracker.on_read("/f.txt", b"data", "/tmp/")
    77→    tracker.on_delete("/f.txt")
    78→    result = tracker.on_sync("/f.txt", "/f.txt", None, backend)
    79→    assert result.status == "ok"
    80→    assert result.op == "delete"
    81→
    82→
    83→def test_on_sync_error_returns_error_result(tracker):
    84→
    85→    class FailingBackend:
    86→
    87→        def put(self, path, data):
    88→            raise RuntimeError("upload failed")
    89→
    90→        def unlink(self, path):
    91→            raise RuntimeError("delete failed")
    92→
    93→    tracker.on_write("/f.txt", b"data", "/tmp/")
    94→    result = tracker.on_sync("/f.txt", "/f.txt", b"data", FailingBackend())
    95→    assert result.status == "error"
    96→    assert result.error is not None
    97→
    98→
    99→def test_dirty_returns_only_dirty_inodes(tracker):
   100→    tracker.on_read("/clean.txt", b"data", "/tmp/")
   101→    tracker.on_write("/dirty.txt", b"data", "/tmp/")
   102→    dirty = tracker.dirty()
   103→    assert len(dirty) == 1
   104→    assert dirty[0].virtual_path == "/dirty.txt"
   105→
   106→
   107→def test_status_returns_all_inodes(tracker):
   108→    tracker.on_read("/a.txt", b"a", "/tmp/")
   109→    tracker.on_write("/b.txt", b"b", "/tmp/")
   110→    assert len(tracker.status()) == 2
   111→
   112→
   113→def test_get_existing_inode(tracker):
   114→    tracker.on_write("/f.txt", b"x", "/tmp/")
   115→    assert tracker.get("/f.txt") is not None
   116→
   117→
   118→def test_get_missing_returns_none(tracker):
   119→    assert tracker.get("/nope.txt") is None
   120→
   121→
   122→def test_reset_clears_all(tracker):
   123→    tracker.on_write("/f.txt", b"data", "/tmp/")
   124→    tracker.reset()
   125→    assert tracker.status() == []
   126→


     1→"""
     2→Example: Use the Claude Agent SDK with a Mirage FUSE-backed workspace.
     3→
     4→The agent reads, writes, and edits files — all intercepted by Mirage's
     5→FUSE layer.  Every file operation is recorded and can be inspected after
     6→the agent finishes.
     7→
     8→Usage:
     9→    pip install claude-agent-sdk
    10→    python examples/claude-sdk.py
    11→    python examples/claude-sdk.py --agent-id code-bot \
    12→        --prompt "create a python calculator module with tests"
    13→"""
    14→
    15→import argparse
    16→import subprocess
    17→import sys
    18→import tempfile
    19→
    20→import anyio
    21→from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
    22→from mirage_fuse import MirageFS, mount_background
    23→
    24→from mirage.backends.local.memory import MemoryBackend
    25→from mirage.types import MountMode
    26→from mirage.workspace import Workspace
    27→
    28→
    29→def unmount(mountpoint: str) -> None:
    30→    if sys.platform == "darwin":
    31→        subprocess.run(["diskutil", "unmount", "force", mountpoint],
    32→                       capture_output=True)
    33→    else:
    34→        subprocess.run(["fusermount", "-u", mountpoint], capture_output=True)
    35→
    36→
    37→def print_workspace(ws: Workspace, prefix: str = "/") -> None:
    38→    for path in ws.ls(prefix):
    39→        try:
    40→            content = ws.cat(path)
    41→            print(f"\n--- {path} ({len(content)} bytes) ---")
    42→            print(content.decode(errors="replace"))
    43→        except (IsADirectoryError, ValueError):
    44→            print(f"\n--- {path}/ (directory) ---")
    45→
    46→
    47→def print_ops(ops: list[dict]) -> None:
    48→    for op in ops:
    49→        ts = op["timestamp"]
    50→        print(f"  [{ts}] {op['op']:6s}  {op['path']}")
    51→
    52→
    53→async def run_agent(mountpoint: str, prompt: str, agent_id: str) -> None:
    54→    options = ClaudeAgentOptions(
    55→        cwd=mountpoint,
    56→        allowed_tools=["Read", "Write", "Edit", "Glob", "Grep", "Bash"],
    57→        permission_mode="bypassPermissions",
    58→        allow_dangerously_skip_permissions=True,
    59→        env={"MIRAGE_AGENT_ID": agent_id},
    60→    )
    61→    async for message in query(prompt=prompt, options=options):
    62→        if isinstance(message, ResultMessage):
    63→            print(f"\n=== Agent result ===\n{message.result}")
    64→
    65→
    66→def main() -> None:
    67→    parser = argparse.ArgumentParser()
    68→    parser.add_argument("--agent-id", default="claude-sdk", dest="agent_id")
    69→    parser.add_argument(
    70→        "--prompt",
    71→        default=(
    72→            "Create a file called /hello.py that prints 'Hello from Mirage!' "
    73→            "and a file called /utils.py with a function"
    74→            " that reverses a string."),
    75→    )
    76→    args = parser.parse_args()
    77→
    78→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    79→
    80→    with tempfile.TemporaryDirectory() as mountpoint:
    81→        fs = MirageFS(ws, agent_id=args.agent_id)
    82→        t = mount_background(ws, mountpoint, agent_id=args.agent_id)
    83→        print(f"Mounted memory workspace at {mountpoint}")
    84→        print(f"Agent: {args.agent_id}")
    85→        print(f"Prompt: {args.prompt}\n")
    86→
    87→        try:
    88→            anyio.run(run_agent, mountpoint, args.prompt, args.agent_id)
    89→        finally:
    90→            ops = fs.drain_ops()
    91→            unmount(mountpoint)
    92→            t.join(timeout=3)
    93→
    94→    print("\n=== FUSE operations captured ===")
    95→    if ops:
    96→        print_ops(ops)
    97→    else:
    98→        print("  (no operations recorded)")
    99→
   100→    print("\n=== Files in workspace (all in-memory) ===")
   101→    try:
   102→        print_workspace(ws)
   103→    except FileNotFoundError:
   104→        print("  (workspace is empty)")
   105→
   106→
   107→if __name__ == "__main__":
   108→    main()
   109→


     1→import pytest
     2→
     3→from mirage.backends.local.memory import MemoryBackend
     4→from mirage.types import MountMode, SyncPolicy
     5→from mirage.workspace import Workspace
     6→
     7→
     8→def _make_ws(mode=MountMode.WRITE, sync_policy=SyncPolicy.STAGED):
     9→    backend = MemoryBackend()
    10→    return Workspace({"/data/": backend}, mode=mode,
    11→                     sync_policy=sync_policy), backend
    12→
    13→
    14→def test_write_and_cat():
    15→    ws, _ = _make_ws()
    16→    ws.write("/data/hello.txt", b"hello world")
    17→    assert ws.cat("/data/hello.txt") == b"hello world"
    18→
    19→
    20→def test_write_marks_dirty():
    21→    ws, _ = _make_ws()
    22→    ws.write("/data/file.txt", b"x")
    23→    assert any(i.virtual_path == "/data/file.txt" and i.dirty
    24→               for i in ws.status())
    25→
    26→
    27→def test_rm_staged_only():
    28→    ws, _ = _make_ws()
    29→    ws.write("/data/new.txt", b"staged")
    30→    ws.rm("/data/new.txt")
    31→    assert not ws.exists("/data/new.txt")
    32→
    33→
    34→def test_rm_persisted():
    35→    ws, backend = _make_ws()
    36→    backend.put("/remote.txt", b"remote")
    37→    ws.rm("/data/remote.txt")
    38→    assert not ws.exists("/data/remote.txt")
    39→
    40→
    41→def test_mv_staged_only():
    42→    ws, _ = _make_ws()
    43→    ws.write("/data/a.txt", b"content")
    44→    ws.mv("/data/a.txt", "/data/b.txt")
    45→    assert ws.cat("/data/b.txt") == b"content"
    46→    assert not ws.exists("/data/a.txt")
    47→
    48→
    49→def test_cp_creates_copy():
    50→    ws, backend = _make_ws()
    51→    backend.put("/src.txt", b"original")
    52→    ws.cp("/data/src.txt", "/data/copy.txt")
    53→    assert ws.cat("/data/copy.txt") == b"original"
    54→
    55→
    56→def test_write_read_only_raises():
    57→    ws, _ = _make_ws(mode=MountMode.READ)
    58→    with pytest.raises(PermissionError):
    59→        ws.write("/data/x.txt", b"x")
    60→
    61→
    62→def test_sync_staged_writes_to_backend():
    63→    ws, backend = _make_ws(sync_policy=SyncPolicy.STAGED)
    64→    ws.write("/data/out.txt", b"synced")
    65→    results = ws.sync()
    66→    assert any(r.path == "/data/out.txt" and r.status == "ok" for r in results)
    67→    assert backend.cat("/out.txt") == b"synced"
    68→
    69→
    70→def test_sync_immediate_writes_through():
    71→    ws, backend = _make_ws(sync_policy=SyncPolicy.IMMEDIATE)
    72→    ws.write("/data/live.txt", b"live data")
    73→    assert backend.cat("/live.txt") == b"live data"
    74→    assert not any(i.dirty for i in ws.status())
    75→
    76→
    77→def test_reset_clears_dirty():
    78→    ws, _ = _make_ws()
    79→    ws.write("/data/x.txt", b"x")
    80→    ws.reset()
    81→    assert ws.status() == []
    82→
    83→
    84→def test_stat_staged_file():
    85→    ws, _ = _make_ws()
    86→    ws.write("/data/stat.txt", b"hello")
    87→    stat = ws.stat("/data/stat.txt")
    88→    assert stat.size == 5
    89→
    90→
    91→def test_exists_staged_file():
    92→    ws, _ = _make_ws()
    93→    ws.write("/data/exists.txt", b"y")
    94→    assert ws.exists("/data/exists.txt")
    95→


     1→"""
     2→Example: Run Claude Code inside a memory-backed FUSE filesystem.
     3→
     4→All file operations from Claude happen in-memory — nothing touches disk.
     5→After Claude exits, the workspace still holds everything Claude created.
     6→
     7→Usage:
     8→    python examples/claude-memory.py
     9→    python examples/claude-memory.py --agent-id my-bot \
    10→        --prompt "write a fibonacci.py"
    11→"""
    12→
    13→import argparse
    14→import os
    15→import subprocess
    16→import sys
    17→import tempfile
    18→
    19→from mirage_fuse import mount_background
    20→
    21→from mirage.backends.local.memory import MemoryBackend
    22→from mirage.types import MountMode
    23→from mirage.workspace import Workspace
    24→
    25→
    26→def unmount(mountpoint: str) -> None:
    27→    if sys.platform == "darwin":
    28→        subprocess.run(["diskutil", "unmount", "force", mountpoint],
    29→                       capture_output=True)
    30→    else:
    31→        subprocess.run(["fusermount", "-u", mountpoint], capture_output=True)
    32→
    33→
    34→def main() -> None:
    35→    parser = argparse.ArgumentParser()
    36→    parser.add_argument("--agent-id", default="claude", dest="agent_id")
    37→    parser.add_argument("--prompt",
    38→                        default="write a hello world script in /hello.py")
    39→    args = parser.parse_args()
    40→
    41→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    42→
    43→    with tempfile.TemporaryDirectory() as mountpoint:
    44→        t = mount_background(ws, mountpoint, agent_id=args.agent_id)
    45→        print(f"Mounted memory filesystem at {mountpoint}")
    46→        print(f"Agent: {args.agent_id}")
    47→
    48→        try:
    49→            subprocess.run(
    50→                ["claude", "-p", args.prompt],
    51→                cwd=mountpoint,
    52→                env={
    53→                    **os.environ, "MIRAGE_AGENT_ID": args.agent_id
    54→                },
    55→            )
    56→        finally:
    57→            unmount(mountpoint)
    58→            t.join(timeout=3)
    59→
    60→    print("\n=== Files Claude created (in memory) ===")
    61→    for path in ws.ls("/"):
    62→        try:
    63→            content = ws.cat(path)
    64→            print(f"\n--- {path} ({len(content)} bytes) ---")
    65→            print(content.decode(errors="replace"))
    66→        except (IsADirectoryError, ValueError):
    67→            print(f"\n--- {path}/ (directory) ---")
    68→
    69→
    70→if __name__ == "__main__":
    71→    main()
    72→


     1→# mirage-fuse
     2→
     3→Expose any mirage Workspace (memory, S3, R2, HTTP) as a native macOS directory so Claude Code and any CLI tool can read and write remote files transparently.
     4→
     5→## Prerequisites
     6→
     7→```bash
     8→brew install --cask macfuse
     9→uv add mirage-fuse
    10→```
    11→
    12→### macOS Security Setup
    13→
    14→1. Open **System Settings → Privacy & Security**. Scroll to the bottom — you'll see:
    15→
    16→> "System software from developer 'Benjamin Fleischer' was blocked from loading."
    17→> Click **Allow**.
    18→
    19→2. **Restart your Mac.** macOS requires a reboot to load the kernel extension.
    20→1. If you don't see the Allow button (Apple Silicon M1/M2/M3/M4):
    21→
    22→- Shut down completely
    23→- Hold power button until "Loading startup options" appears
    24→- Click **Options → Continue**
    25→- In Recovery: **Utilities → Startup Security Utility**
    26→- Set to **Reduced Security** and check **"Allow user management of kernel extensions"**
    27→- Restart, then retry step 1
    28→
    29→## Usage
    30→
    31→### CLI
    32→
    33→```bash
    34→# Mount an in-memory workspace as a real directory (writable)
    35→mirage mount-fuse /data/:memory:// --mountpoint /tmp/ws/ --mode write --foreground
    36→# (runs in foreground — open a new terminal for the next commands)
    37→
    38→# In another terminal: any OS tool works natively
    39→ls /tmp/ws/data/
    40→echo "hello" > /tmp/ws/data/notes.txt
    41→cat /tmp/ws/data/notes.txt
    42→
    43→# Use Claude Code directly on the mounted path
    44→cd /tmp/ws/data && claude -p "write a hello.py script here"
    45→
    46→# Ctrl+C in the first terminal to unmount
    47→```
    48→
    49→For read-only S3/R2 access:
    50→
    51→```bash
    52→mirage mount-fuse /data/:s3://my-bucket --mountpoint /tmp/s3/ --mode read
    53→claude -p "summarize the files in /tmp/s3/data/reports/"
    54→# Ctrl+C to unmount
    55→```
    56→
    57→### Python API
    58→
    59→```python
    60→from mirage.backends.local.memory import MemoryBackend
    61→from mirage.workspace import Workspace
    62→from mirage_fuse import MirageFS, mount
    63→
    64→ws = Workspace({"/data/": MemoryBackend()}, mode="write")
    65→mount(ws, "/tmp/ws/", foreground=True)
    66→```
    67→
    68→## Supported FUSE Operations
    69→
    70→| Operation  | Description             |
    71→| ---------- | ----------------------- |
    72→| `getattr`  | File/directory metadata |
    73→| `readdir`  | List directory contents |
    74→| `read`     | Read file contents      |
    75→| `write`    | Write file contents     |
    76→| `create`   | Create new files        |
    77→| `mkdir`    | Create directories      |
    78→| `unlink`   | Delete files            |
    79→| `rename`   | Move/rename files       |
    80→| `truncate` | Resize files            |
    81→| `open`     | Open existing files     |
    82→| `release`  | Close file handles      |
    83→


     1→import errno
     2→import os
     3→import stat
     4→import subprocess
     5→import sys
     6→import tempfile
     7→
     8→import pytest
     9→from mirage_fuse.fs import _MIRAGE_DIR, _MIRAGE_WHOAMI, MirageFS
    10→
    11→import fuse
    12→from mirage.backends.local.memory import MemoryBackend
    13→from mirage.types import MountMode
    14→from mirage.workspace import Workspace
    15→
    16→_fuse_available = sys.platform in ("linux", "darwin")
    17→
    18→
    19→@pytest.fixture
    20→def seed_ws():
    21→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    22→    ws.write("/a.txt", b"hello world")
    23→    ws.write("/sub/b.txt", b"nested")
    24→    return ws
    25→
    26→
    27→@pytest.fixture
    28→def rw_ws():
    29→    return Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    30→
    31→
    32→def test_getattr_root(seed_ws):
    33→    fs = MirageFS(seed_ws)
    34→    attrs = fs.getattr("/")
    35→    assert attrs["st_mode"] & stat.S_IFDIR
    36→
    37→
    38→def test_getattr_file(seed_ws):
    39→    fs = MirageFS(seed_ws)
    40→    attrs = fs.getattr("/a.txt")
    41→    assert attrs["st_mode"] & stat.S_IFREG
    42→    assert attrs["st_size"] == len(b"hello world")
    43→
    44→
    45→def test_getattr_dir(seed_ws):
    46→    fs = MirageFS(seed_ws)
    47→    attrs = fs.getattr("/sub")
    48→    assert attrs["st_mode"] & stat.S_IFDIR
    49→
    50→
    51→def test_getattr_missing(seed_ws):
    52→    fs = MirageFS(seed_ws)
    53→    with pytest.raises(fuse.FuseOSError) as exc:
    54→        fs.getattr("/no_such_file.txt")
    55→    assert exc.value.errno == errno.ENOENT
    56→
    57→
    58→def test_readdir_root(seed_ws):
    59→    fs = MirageFS(seed_ws)
    60→    entries = fs.readdir("/", None)
    61→    assert "." in entries
    62→    assert ".." in entries
    63→    assert "a.txt" in entries
    64→    assert "sub" in entries
    65→
    66→
    67→def test_readdir_subdir(seed_ws):
    68→    fs = MirageFS(seed_ws)
    69→    entries = fs.readdir("/sub", None)
    70→    assert "b.txt" in entries
    71→
    72→
    73→def test_read_full(seed_ws):
    74→    fs = MirageFS(seed_ws)
    75→    fh = fs.open("/a.txt", os.O_RDONLY)
    76→    data = fs.read("/a.txt", 1024, 0, fh)
    77→    assert data == b"hello world"
    78→
    79→
    80→def test_read_offset(seed_ws):
    81→    fs = MirageFS(seed_ws)
    82→    fh = fs.open("/a.txt", os.O_RDONLY)
    83→    data = fs.read("/a.txt", 5, 6, fh)
    84→    assert data == b"world"
    85→
    86→
    87→def test_create_and_write(rw_ws):
    88→    fs = MirageFS(rw_ws)
    89→    fh = fs.create("/new.txt", 0o644)
    90→    fs.write("/new.txt", b"data", 0, fh)
    91→    fs.flush("/new.txt", fh)
    92→    assert rw_ws.cat("/new.txt") == b"data"
    93→
    94→
    95→def test_mkdir(rw_ws):
    96→    fs = MirageFS(rw_ws)
    97→    fs.mkdir("/newdir", 0o755)
    98→    entries = fs.readdir("/newdir", None)
    99→    assert "." in entries
   100→
   101→
   102→def test_unlink(rw_ws):
   103→    rw_ws.write("/todel.txt", b"bye")
   104→    fs = MirageFS(rw_ws)
   105→    fs.unlink("/todel.txt")
   106→    with pytest.raises(fuse.FuseOSError) as exc:
   107→        fs.getattr("/todel.txt")
   108→    assert exc.value.errno == errno.ENOENT
   109→
   110→
   111→def test_rename(rw_ws):
   112→    rw_ws.write("/old.txt", b"content")
   113→    fs = MirageFS(rw_ws)
   114→    fs.rename("/old.txt", "/new.txt")
   115→    assert rw_ws.cat("/new.txt") == b"content"
   116→
   117→
   118→def test_truncate(rw_ws):
   119→    rw_ws.write("/f.txt", b"hello world")
   120→    fs = MirageFS(rw_ws)
   121→    fs.truncate("/f.txt", 5)
   122→    assert rw_ws.cat("/f.txt") == b"hello"
   123→
   124→
   125→def test_write_at_offset(rw_ws):
   126→    rw_ws.write("/f.txt", b"hello world")
   127→    fs = MirageFS(rw_ws)
   128→    fh = fs.open("/f.txt", os.O_RDWR)
   129→    fs.write("/f.txt", b"WORLD", 6, fh)
   130→    fs.flush("/f.txt", fh)
   131→    assert rw_ws.cat("/f.txt") == b"hello WORLD"
   132→
   133→
   134→def test_truncate_extend(rw_ws):
   135→    rw_ws.write("/f.txt", b"hi")
   136→    fs = MirageFS(rw_ws)
   137→    fs.truncate("/f.txt", 5)
   138→    assert rw_ws.cat("/f.txt") == b"hi\x00\x00\x00"
   139→
   140→
   141→def test_mirage_dir_getattr(seed_ws):
   142→    fs = MirageFS(seed_ws)
   143→    attrs = fs.getattr(_MIRAGE_DIR)
   144→    assert attrs["st_mode"] & stat.S_IFDIR
   145→
   146→
   147→def test_mirage_whoami_getattr(seed_ws):
   148→    fs = MirageFS(seed_ws, agent_id="test-agent")
   149→    attrs = fs.getattr(_MIRAGE_WHOAMI)
   150→    assert attrs["st_mode"] & stat.S_IFREG
   151→    assert attrs["st_size"] > 0
   152→
   153→
   154→def test_mirage_dir_in_root_readdir(seed_ws):
   155→    fs = MirageFS(seed_ws)
   156→    entries = fs.readdir("/", None)
   157→    assert ".mirage" in entries
   158→
   159→
   160→def test_mirage_dir_readdir(seed_ws):
   161→    fs = MirageFS(seed_ws)
   162→    entries = fs.readdir(_MIRAGE_DIR, None)
   163→    assert "whoami" in entries
   164→    assert "." in entries
   165→    assert ".." in entries
   166→
   167→
   168→def test_whoami_content_has_agent(seed_ws):
   169→    fs = MirageFS(seed_ws, agent_id="claude")
   170→    fh = fs.open(_MIRAGE_WHOAMI, os.O_RDONLY)
   171→    data = fs.read(_MIRAGE_WHOAMI, 4096, 0, fh)
   172→    text = data.decode()
   173→    assert "agent: claude" in text
   174→    assert "cwd: /" in text
   175→    assert "mounts:" in text
   176→
   177→
   178→def test_whoami_content_offset(seed_ws):
   179→    fs = MirageFS(seed_ws, agent_id="x")
   180→    fh = fs.open(_MIRAGE_WHOAMI, os.O_RDONLY)
   181→    full = fs.read(_MIRAGE_WHOAMI, 4096, 0, fh)
   182→    partial = fs.read(_MIRAGE_WHOAMI, 5, 0, fh)
   183→    assert partial == full[:5]
   184→
   185→
   186→def test_whoami_open(seed_ws):
   187→    fs = MirageFS(seed_ws)
   188→    fh = fs.open(_MIRAGE_WHOAMI, os.O_RDONLY)
   189→    assert isinstance(fh, int)
   190→    assert fh > 0
   191→
   192→
   193→def test_agent_id_auto_generated(seed_ws):
   194→    fs = MirageFS(seed_ws)
   195→    assert fs.agent_id.startswith("agent-")
   196→
   197→
   198→def test_agent_id_from_param(seed_ws):
   199→    fs = MirageFS(seed_ws, agent_id="my-bot")
   200→    assert fs.agent_id == "my-bot"
   201→
   202→
   203→def test_agent_id_from_env(seed_ws, monkeypatch):
   204→    monkeypatch.setenv("MIRAGE_AGENT_ID", "env-agent")
   205→    fs = MirageFS(seed_ws)
   206→    assert fs.agent_id == "env-agent"
   207→
   208→
   209→def test_whoami_size_matches_content(seed_ws):
   210→    fs = MirageFS(seed_ws, agent_id="claude")
   211→    attrs = fs.getattr(_MIRAGE_WHOAMI)
   212→    fh = fs.open(_MIRAGE_WHOAMI, os.O_RDONLY)
   213→    content = fs.read(_MIRAGE_WHOAMI, 4096, 0, fh)
   214→    assert attrs["st_size"] == len(content)
   215→
   216→
   217→def test_fuse_dispatches_to_registry(seed_ws):
   218→    from mirage.fuse_ops.registry import FuseOpRegistry
   219→    registry = FuseOpRegistry()
   220→    fs = MirageFS(seed_ws, fuse_registry=registry)
   221→    fh = fs.open("/a.txt", os.O_RDONLY)
   222→    data = fs.read("/a.txt", 1024, 0, fh)
   223→    assert data == b"hello world"
   224→
   225→
   226→def test_fuse_write_buffered_flush(rw_ws):
   227→    from mirage.fuse_ops.registry import FuseOpRegistry
   228→    registry = FuseOpRegistry()
   229→    fs = MirageFS(rw_ws, fuse_registry=registry)
   230→    rw_ws.write("/f.txt", b"hello world")
   231→    fh = fs.open("/f.txt", os.O_RDWR)
   232→    fs.write("/f.txt", b"HELLO", 0, fh)
   233→    fs.flush("/f.txt", fh)
   234→    assert rw_ws.cat("/f.txt") == b"HELLO world"
   235→
   236→
   237→def test_fuse_release_cleans_handles(seed_ws):
   238→    fs = MirageFS(seed_ws)
   239→    fh = fs.open("/a.txt", os.O_RDONLY)
   240→    assert fh in fs._handles
   241→    fs.release("/a.txt", fh)
   242→    assert fh not in fs._handles
   243→
   244→
   245→def test_open_returns_unique_handles(seed_ws):
   246→    fs = MirageFS(seed_ws)
   247→    fh1 = fs.open("/a.txt", os.O_RDONLY)
   248→    fh2 = fs.open("/a.txt", os.O_RDONLY)
   249→    assert fh1 != fh2
   250→
   251→
   252→@pytest.mark.skipif(not _fuse_available,
   253→                    reason="FUSE not available on this platform")
   254→def test_mount_background_readable():
   255→    from mirage_fuse import mount_background
   256→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
   257→    ws.write("/hello.txt", b"hi from memory")
   258→    with tempfile.TemporaryDirectory() as mountpoint:
   259→        t = mount_background(ws, mountpoint, agent_id="test-agent")
   260→        try:
   261→            path = os.path.join(mountpoint, "hello.txt")
   262→            assert os.path.exists(path)
   263→            with open(path, "rb") as f:
   264→                assert f.read() == b"hi from memory"
   265→        finally:
   266→            if sys.platform == "darwin":
   267→                subprocess.run(["diskutil", "unmount", "force", mountpoint],
   268→                               capture_output=True)
   269→            else:
   270→                subprocess.run(["fusermount", "-u", mountpoint],
   271→                               capture_output=True)
   272→            t.join(timeout=3)
   273→
   274→
   275→@pytest.mark.skipif(not _fuse_available,
   276→                    reason="FUSE not available on this platform")
   277→def test_mount_background_whoami_file():
   278→    from mirage_fuse import mount_background
   279→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
   280→    with tempfile.TemporaryDirectory() as mountpoint:
   281→        t = mount_background(ws, mountpoint, agent_id="claude")
   282→        try:
   283→            whoami_path = os.path.join(mountpoint, ".mirage", "whoami")
   284→            assert os.path.exists(whoami_path)
   285→            content = open(whoami_path).read()
   286→            assert "agent: claude" in content
   287→        finally:
   288→            if sys.platform == "darwin":
   289→                subprocess.run(["diskutil", "unmount", "force", mountpoint],
   290→                               capture_output=True)
   291→            else:
   292→                subprocess.run(["fusermount", "-u", mountpoint],
   293→                               capture_output=True)
   294→            t.join(timeout=3)
   295→


     1→import pytest
     2→
     3→from mirage.backends.local.memory import MemoryBackend
     4→from mirage.types import MountMode
     5→from mirage.workspace import Workspace
     6→from mirage.workspace.mount import MountRegistry
     7→
     8→
     9→def test_duplicate_prefix_raises():
    10→    reg = MountRegistry()
    11→    reg.mount("/data/", MemoryBackend())
    12→    with pytest.raises(ValueError, match="duplicate mount prefix"):
    13→        reg.mount("/data/", MemoryBackend())
    14→
    15→
    16→def test_duplicate_different_normalization():
    17→    reg = MountRegistry()
    18→    reg.mount("/data", MemoryBackend())
    19→    with pytest.raises(ValueError, match="duplicate mount prefix"):
    20→        reg.mount("/data/", MemoryBackend())
    21→
    22→
    23→def test_nested_prefixes_allowed():
    24→    reg = MountRegistry()
    25→    reg.mount("/data/", MemoryBackend())
    26→    reg.mount("/data/sub/", MemoryBackend())
    27→    assert len(reg.mounts()) == 2
    28→
    29→
    30→def test_per_mount_mode_resolve():
    31→    reg = MountRegistry()
    32→    reg.mount("/ro/", MemoryBackend(), mode=MountMode.READ)
    33→    reg.mount("/rw/", MemoryBackend(), mode=MountMode.WRITE)
    34→    _, _, mode_ro = reg.resolve("/ro/file.txt")
    35→    _, _, mode_rw = reg.resolve("/rw/file.txt")
    36→    assert mode_ro == MountMode.READ
    37→    assert mode_rw == MountMode.WRITE
    38→
    39→
    40→def test_workspace_per_mount_mode():
    41→    ro_backend = MemoryBackend()
    42→    rw_backend = MemoryBackend()
    43→    ws = Workspace(
    44→        {
    45→            "/ro/": ro_backend,
    46→            "/rw/": (rw_backend, MountMode.WRITE),
    47→        },
    48→        mode=MountMode.READ,
    49→    )
    50→    ws.write("/rw/test.txt", b"hello")
    51→    assert ws.cat("/rw/test.txt") == b"hello"
    52→    with pytest.raises(PermissionError):
    53→        ws.write("/ro/test.txt", b"fail")
    54→
    55→
    56→def test_workspace_tuple_mode_override():
    57→    ws = Workspace(
    58→        {"/a/": (MemoryBackend(), MountMode.WRITE)},
    59→        mode=MountMode.READ,
    60→    )
    61→    ws.write("/a/test.txt", b"ok")
    62→    assert ws.cat("/a/test.txt") == b"ok"
    63→
    64→
    65→def test_workspace_duplicate_prefix_raises():
    66→    with pytest.raises(ValueError, match="duplicate mount prefix"):
    67→        Workspace({
    68→            "/data/": MemoryBackend(),
    69→            "/data": MemoryBackend(),
    70→        })
    71→


     1→from mirage.backends.local.memory import MemoryBackend
     2→from mirage.commands.custom.arg_resolver import classify_args
     3→from mirage.types import MountMode, SyncPolicy
     4→from mirage.workspace import Workspace
     5→
     6→
     7→def test_classify_input_file():
     8→    ws = Workspace({"/data/": MemoryBackend()},
     9→                   mode=MountMode.WRITE,
    10→                   sync_policy=SyncPolicy.IMMEDIATE)
    11→    ws.write("/data/input.json", b'{"key": "value"}')
    12→    result = classify_args(ws, ["input.json"], cwd="/data/")
    13→    assert "/data/input.json" in result.input_files
    14→    assert result.input_files["/data/input.json"] == b'{"key": "value"}'
    15→
    16→
    17→def test_classify_potential_output():
    18→    ws = Workspace({"/data/": MemoryBackend()}, mode=MountMode.WRITE)
    19→    result = classify_args(ws, ["/data/output.csv"], cwd="/")
    20→    assert "/data/output.csv" in result.potential_outputs
    21→
    22→
    23→def test_classify_directory():
    24→    ws = Workspace({"/data/": MemoryBackend()},
    25→                   mode=MountMode.WRITE,
    26→                   sync_policy=SyncPolicy.IMMEDIATE)
    27→    ws.mkdir("/data/dir/")
    28→    ws.write("/data/dir/a.txt", b"aaa")
    29→    ws.write("/data/dir/b.txt", b"bbb")
    30→    result = classify_args(ws, ["/data/dir/"], cwd="/")
    31→    assert "/data/dir/" in result.input_dirs
    32→    assert "/data/dir/a.txt" in result.input_dirs["/data/dir/"]
    33→    assert "/data/dir/b.txt" in result.input_dirs["/data/dir/"]
    34→
    35→
    36→def test_classify_regular_args():
    37→    ws = Workspace({"/data/": MemoryBackend()})
    38→    result = classify_args(ws, ["--verbose", "-o", "100"], cwd="/")
    39→    assert result.regular_args == ["--verbose", "-o", "100"]
    40→    assert result.input_files == {}
    41→    assert result.potential_outputs == []
    42→
    43→
    44→def test_classify_mixed_args():
    45→    ws = Workspace({"/data/": MemoryBackend()},
    46→                   mode=MountMode.WRITE,
    47→                   sync_policy=SyncPolicy.IMMEDIATE)
    48→    ws.write("/data/in.json", b"data")
    49→    result = classify_args(ws,
    50→                           ["/data/in.json", "-o", "/data/out.csv", "--flag"],
    51→                           cwd="/")
    52→    assert "/data/in.json" in result.input_files
    53→    assert "/data/out.csv" in result.potential_outputs
    54→    assert "--flag" in result.regular_args
    55→    assert "-o" in result.regular_args
    56→
    57→
    58→def test_classify_no_mount_match():
    59→    ws = Workspace({"/data/": MemoryBackend()})
    60→    result = classify_args(ws, ["/other/file.txt"], cwd="/")
    61→    assert result.input_files == {}
    62→    assert result.potential_outputs == []
    63→    assert "/other/file.txt" in result.regular_args
    64→
    65→
    66→def test_classify_relative_path():
    67→    ws = Workspace({"/data/": MemoryBackend()},
    68→                   mode=MountMode.WRITE,
    69→                   sync_policy=SyncPolicy.IMMEDIATE)
    70→    ws.mkdir("/data/sub/")
    71→    ws.write("/data/sub/file.txt", b"hello")
    72→    result = classify_args(ws, ["sub/file.txt"], cwd="/data/")
    73→    assert "/data/sub/file.txt" in result.input_files
    74→
    75→
    76→def test_classify_arg_map():
    77→    ws = Workspace({"/data/": MemoryBackend()},
    78→                   mode=MountMode.WRITE,
    79→                   sync_policy=SyncPolicy.IMMEDIATE)
    80→    ws.write("/data/file.txt", b"hello")
    81→    result = classify_args(ws, ["file.txt", "--flag"], cwd="/data/")
    82→    assert result.arg_map["file.txt"] == "/data/file.txt"
    83→    assert "file.txt" not in result.regular_args
    84→


     1→import sys
     2→from unittest.mock import MagicMock
     3→
     4→import pytest
     5→
     6→from mirage.backends.local.memory import MemoryBackend
     7→from mirage.commands.custom.registry import CommandRegistry, ExternalCommand
     8→from mirage.commands.remote.client import CommandClient
     9→from mirage.types import (CommandResult, MountMode, RemoteCommandOptions,
    10→                          SyncPolicy)
    11→from mirage.workspace import Workspace
    12→
    13→
    14→def test_builtin_ls():
    15→    ws = Workspace({"/tmp/": MemoryBackend()},
    16→                   mode=MountMode.WRITE,
    17→                   sync_policy=SyncPolicy.IMMEDIATE)
    18→    ws.write("/tmp/a.txt", b"hi")
    19→    reg = CommandRegistry()
    20→    result = reg.run(ws, "ls /tmp/", cwd="/")
    21→    assert "a.txt" in result
    22→
    23→
    24→def test_register_external():
    25→    reg = CommandRegistry()
    26→    reg.register(ExternalCommand(name="echo-test", exec="echo hello"))
    27→    assert "echo-test" in reg.list_commands()
    28→
    29→
    30→def test_run_external_no_placeholder(tmp_path):
    31→    reg = CommandRegistry()
    32→    reg.register(ExternalCommand(name="greet", exec="echo hello"))
    33→    ws = Workspace({"/tmp/": MemoryBackend()})
    34→    result = reg.run(ws, "greet", cwd="/")
    35→    assert "hello" in result
    36→
    37→
    38→def test_run_external_file_placeholder(tmp_path):
    39→    ws = Workspace({"/tmp/": MemoryBackend()}, mode=MountMode.WRITE)
    40→    ws.write("/tmp/data.txt", b"line1\nline2")
    41→    script = tmp_path / "count.py"
    42→    script.write_text(
    43→        "import sys; lines = open(sys.argv[1]).readlines(); print(len(lines))")
    44→    reg = CommandRegistry()
    45→    reg.register(
    46→        ExternalCommand(name="linecount", exec=f"{sys.executable} {script}"))
    47→    result = reg.run(ws, "linecount /tmp/data.txt", cwd="/")
    48→    assert result.strip() == "2"
    49→
    50→
    51→def test_unknown_command():
    52→    reg = CommandRegistry()
    53→    ws = Workspace({"/tmp/": MemoryBackend()})
    54→    with pytest.raises(ValueError, match="unknown command"):
    55→        reg.run(ws, "nonexistent", cwd="/")
    56→
    57→
    58→def test_run_external_file_placeholder_multi_arg(tmp_path):
    59→    ws = Workspace({"/tmp/": MemoryBackend()}, mode=MountMode.WRITE)
    60→    ws.write("/tmp/a.txt", b"aaa")
    61→    ws.write("/tmp/b.txt", b"bbb")
    62→    script = tmp_path / "concat.py"
    63→    script.write_text(
    64→        "import sys; [print(open(f).read(), end='') for f in sys.argv[1:]]")
    65→    reg = CommandRegistry()
    66→    reg.register(
    67→        ExternalCommand(name="concat", exec=f"{sys.executable} {script}"))
    68→    result = reg.run(ws, "concat /tmp/a.txt /tmp/b.txt", cwd="/")
    69→    assert "aaa" in result
    70→    assert "bbb" in result
    71→
    72→
    73→def test_external_overrides_builtin():
    74→    ws = Workspace({"/tmp/": MemoryBackend()},
    75→                   mode=MountMode.WRITE,
    76→                   sync_policy=SyncPolicy.IMMEDIATE)
    77→    ws.write("/tmp/file.txt", b"data")
    78→    reg = CommandRegistry()
    79→    reg.register(ExternalCommand(name="ls", exec="echo overridden"))
    80→    result = reg.run(ws, "ls /tmp/", cwd="/")
    81→    assert "overridden" in result
    82→    assert "file.txt" not in result
    83→
    84→
    85→class _CustomMethodBackend(MemoryBackend):
    86→
    87→    def lllll(self, path: str) -> str:
    88→        return f"custom-result-for:{path}"
    89→
    90→    def bytes_method(self, path: str) -> bytes:
    91→        return b"raw-bytes"
    92→
    93→    def list_method(self, path: str) -> list:
    94→        return ["item1", "item2", "item3"]
    95→
    96→
    97→def test_backend_method_fallback():
    98→    backend = _CustomMethodBackend()
    99→    ws = Workspace({"/data/": backend}, mode=MountMode.WRITE)
   100→    reg = CommandRegistry()
   101→    result = reg.run(ws, "lllll /data/", cwd="/")
   102→    assert "custom-result-for:" in result
   103→
   104→
   105→def test_backend_method_fallback_bytes_coercion():
   106→    backend = _CustomMethodBackend()
   107→    ws = Workspace({"/data/": backend}, mode=MountMode.WRITE)
   108→    reg = CommandRegistry()
   109→    result = reg.run(ws, "bytes_method /data/", cwd="/")
   110→    assert result == "raw-bytes"
   111→
   112→
   113→def test_backend_method_fallback_list_coercion():
   114→    backend = _CustomMethodBackend()
   115→    ws = Workspace({"/data/": backend}, mode=MountMode.WRITE)
   116→    reg = CommandRegistry()
   117→    result = reg.run(ws, "list_method /data/", cwd="/")
   118→    assert "item1\nitem2\nitem3" == result
   119→
   120→
   121→def test_backend_private_method_rejected():
   122→    backend = _CustomMethodBackend()
   123→    ws = Workspace({"/data/": backend}, mode=MountMode.WRITE)
   124→    reg = CommandRegistry()
   125→    with pytest.raises(ValueError, match="unknown command"):
   126→        reg.run(ws, "_files /data/", cwd="/")
   127→
   128→
   129→def test_list_commands_includes_builtins():
   130→    registry = CommandRegistry()
   131→    cmds = registry.list_commands()
   132→    assert "ls" in cmds
   133→    assert "cat" in cmds
   134→
   135→
   136→def test_run_builtin_cat():
   137→    backend = MemoryBackend()
   138→    backend.put("/hello.txt", b"hello world")
   139→    ws = Workspace({"/data/": backend}, mode=MountMode.READ)
   140→    result = CommandRegistry().run(ws, "cat /data/hello.txt", cwd="/")
   141→    assert "hello world" in result
   142→
   143→
   144→def test_external_command_with_options():
   145→    cmd = ExternalCommand(
   146→        name="transform",
   147→        exec="transform",
   148→        options=RemoteCommandOptions(max_file_size="500MB", cache="disabled"),
   149→    )
   150→    assert cmd.options.max_file_size == "500MB"
   151→    assert cmd.options.cache == "disabled"
   152→
   153→
   154→def test_external_command_default_options():
   155→    cmd = ExternalCommand(name="echo-test", exec="echo hello")
   156→    assert cmd.options.max_file_size == "100MB"
   157→
   158→
   159→def test_external_command_default_options_not_none():
   160→    cmd = ExternalCommand(name="echo-test", exec="echo hello")
   161→    assert cmd.options is not None
   162→    assert cmd.options.max_file_size == "100MB"
   163→
   164→
   165→def test_run_external_out_placeholder(tmp_path):
   166→    ws = Workspace({"/tmp/": MemoryBackend()},
   167→                   mode=MountMode.WRITE,
   168→                   sync_policy=SyncPolicy.IMMEDIATE)
   169→    ws.write("/tmp/input.txt", b"hello world")
   170→    script = tmp_path / "upper.py"
   171→    script.write_text("import sys\n"
   172→                      "data = open(sys.argv[1]).read()\n"
   173→                      "open(sys.argv[2], 'w').write(data.upper())\n"
   174→                      "print('done')\n")
   175→    reg = CommandRegistry()
   176→    reg.register(
   177→        ExternalCommand(
   178→            name="upper",
   179→            exec=f"{sys.executable} {script}",
   180→        ))
   181→    result = reg.run(ws, "upper /tmp/input.txt /tmp/output.txt", cwd="/")
   182→    assert "done" in result
   183→    output = ws.cat("/tmp/output.txt")
   184→    assert output == b"HELLO WORLD"
   185→
   186→
   187→def test_registry_dispatches_to_client_with_files():
   188→    mock_client = MagicMock(spec=CommandClient)
   189→    mock_client.run.return_value = CommandResult(
   190→        stdout="done",
   191→        output_files={"/tmp/out.csv": b"result"},
   192→    )
   193→
   194→    ws = Workspace({"/tmp/": MemoryBackend()},
   195→                   mode=MountMode.WRITE,
   196→                   sync_policy=SyncPolicy.IMMEDIATE)
   197→    ws.write("/tmp/input.json", b'{"data": true}')
   198→
   199→    reg = CommandRegistry(client=mock_client)
   200→    reg.register(ExternalCommand(
   201→        name="transform",
   202→        exec="transform",
   203→    ))
   204→    result = reg.run(ws, "transform /tmp/input.json /tmp/out.csv", cwd="/")
   205→    assert "done" in result
   206→
   207→    call_kwargs = mock_client.run.call_args
   208→    assert call_kwargs.kwargs.get("files") is not None
   209→    files = call_kwargs.kwargs["files"]
   210→    assert "/tmp/input.json" in files
   211→    assert files["/tmp/input.json"].data == b'{"data": true}'
   212→    assert files["/tmp/input.json"].checksum is not None
   213→
   214→    output = ws.cat("/tmp/out.csv")
   215→    assert output == b"result"
   216→
   217→
   218→def test_registry_dispatches_to_client_with_stdin():
   219→    mock_client = MagicMock(spec=CommandClient)
   220→    mock_client.run.return_value = CommandResult(stdout="processed")
   221→
   222→    ws = Workspace({"/tmp/": MemoryBackend()})
   223→    reg = CommandRegistry(client=mock_client)
   224→    reg.register(ExternalCommand(name="process", exec="process"))
   225→    result = reg.run(ws, "process", cwd="/", stdin=b"pipe input")
   226→    assert "processed" in result
   227→    call_kwargs = mock_client.run.call_args
   228→    assert call_kwargs.kwargs.get("stdin") == b"pipe input"
   229→
   230→
   231→def test_registry_client_no_placeholders_still_uses_client():
   232→    mock_client = MagicMock(spec=CommandClient)
   233→    mock_client.run.return_value = CommandResult(stdout="remote echo")
   234→
   235→    ws = Workspace({"/tmp/": MemoryBackend()})
   236→    reg = CommandRegistry(client=mock_client)
   237→    reg.register(ExternalCommand(name="remote-echo", exec="echo hello"))
   238→    result = reg.run(ws, "remote-echo", cwd="/")
   239→    assert "remote echo" in result
   240→    mock_client.run.assert_called_once()
   241→
   242→
   243→def test_registry_out_to_readonly_mount_fails():
   244→    mock_client = MagicMock(spec=CommandClient)
   245→    ws = Workspace({"/data/": MemoryBackend()}, mode=MountMode.READ)
   246→    reg = CommandRegistry(client=mock_client)
   247→    reg.register(ExternalCommand(name="transform", exec="transform"))
   248→    with pytest.raises(PermissionError, match="read-only"):
   249→        reg.run(ws, "transform /data/in.json /data/out.csv", cwd="/")
   250→
   251→
   252→def test_registry_rejects_oversized_file():
   253→    mock_client = MagicMock(spec=CommandClient)
   254→    ws = Workspace({"/tmp/": MemoryBackend()},
   255→                   mode=MountMode.WRITE,
   256→                   sync_policy=SyncPolicy.IMMEDIATE)
   257→    ws.write("/tmp/big.bin", b"x" * 200)
   258→
   259→    reg = CommandRegistry(client=mock_client)
   260→    reg.register(
   261→        ExternalCommand(
   262→            name="process",
   263→            exec="process",
   264→            options=RemoteCommandOptions(max_file_size="100B"),
   265→        ))
   266→    with pytest.raises(ValueError, match="exceeds max"):
   267→        reg.run(ws, "process /tmp/big.bin", cwd="/")
   268→
   269→
   270→def test_registry_allows_file_within_size_limit():
   271→    mock_client = MagicMock(spec=CommandClient)
   272→    mock_client.run.return_value = CommandResult(stdout="ok")
   273→
   274→    ws = Workspace({"/tmp/": MemoryBackend()},
   275→                   mode=MountMode.WRITE,
   276→                   sync_policy=SyncPolicy.IMMEDIATE)
   277→    ws.write("/tmp/small.txt", b"small")
   278→
   279→    reg = CommandRegistry(client=mock_client)
   280→    reg.register(
   281→        ExternalCommand(
   282→            name="process",
   283→            exec="process",
   284→            options=RemoteCommandOptions(max_file_size="100B"),
   285→        ))
   286→    result = reg.run(ws, "process /tmp/small.txt", cwd="/")
   287→    assert "ok" in result
   288→
   289→
   290→def test_parse_size_units():
   291→    from mirage.server.constants import _parse_size
   292→    assert _parse_size("100B") == 100
   293→    assert _parse_size("10KB") == 10240
   294→    assert _parse_size("1MB") == 1048576
   295→    assert _parse_size("2GB") == 2147483648
   296→    assert _parse_size("100") == 100
   297→
   298→
   299→def test_run_external_no_template_input_output(tmp_path):
   300→    ws = Workspace({"/tmp/": MemoryBackend()},
   301→                   mode=MountMode.WRITE,
   302→                   sync_policy=SyncPolicy.IMMEDIATE)
   303→    ws.write("/tmp/input.txt", b"hello world")
   304→    script = tmp_path / "upper.py"
   305→    script.write_text("import sys\n"
   306→                      "data = open(sys.argv[1]).read()\n"
   307→                      "open(sys.argv[2], 'w').write(data.upper())\n"
   308→                      "print('done')\n")
   309→    reg = CommandRegistry()
   310→    reg.register(
   311→        ExternalCommand(name="upper", exec=f"{sys.executable} {script}"))
   312→    result = reg.run(ws, "upper /tmp/input.txt /tmp/output.txt", cwd="/")
   313→    assert "done" in result
   314→    output = ws.cat("/tmp/output.txt")
   315→    assert output == b"HELLO WORLD"
   316→
   317→
   318→def test_run_external_no_template_inplace(tmp_path):
   319→    ws = Workspace({"/tmp/": MemoryBackend()},
   320→                   mode=MountMode.WRITE,
   321→                   sync_policy=SyncPolicy.IMMEDIATE)
   322→    ws.write("/tmp/data.txt", b"hello")
   323→    script = tmp_path / "inplace.py"
   324→    script.write_text("import sys\n"
   325→                      "path = sys.argv[1]\n"
   326→                      "data = open(path).read()\n"
   327→                      "open(path, 'w').write(data.upper())\n"
   328→                      "print('done')\n")
   329→    reg = CommandRegistry()
   330→    reg.register(
   331→        ExternalCommand(name="format", exec=f"{sys.executable} {script}"))
   332→    result = reg.run(ws, "format /tmp/data.txt", cwd="/")
   333→    assert "done" in result
   334→    assert ws.cat("/tmp/data.txt") == b"HELLO"
   335→
   336→
   337→def test_run_external_no_template_no_files():
   338→    ws = Workspace({"/tmp/": MemoryBackend()})
   339→    reg = CommandRegistry()
   340→    reg.register(ExternalCommand(name="greet", exec="echo"))
   341→    result = reg.run(ws, "greet hello world", cwd="/")
   342→    assert "hello world" in result
   343→
   344→
   345→def test_dispatch_auto_detect_input_and_output():
   346→    mock_client = MagicMock(spec=CommandClient)
   347→    mock_client.run.return_value = CommandResult(
   348→        stdout="done",
   349→        output_files={"/tmp/out.csv": b"result"},
   350→    )
   351→    ws = Workspace({"/tmp/": MemoryBackend()},
   352→                   mode=MountMode.WRITE,
   353→                   sync_policy=SyncPolicy.IMMEDIATE)
   354→    ws.write("/tmp/input.json", b'{"data": true}')
   355→
   356→    reg = CommandRegistry(client=mock_client)
   357→    reg.register(ExternalCommand(name="transform", exec="transform"))
   358→    result = reg.run(ws, "transform /tmp/input.json /tmp/out.csv", cwd="/")
   359→    assert "done" in result
   360→
   361→    call_kwargs = mock_client.run.call_args
   362→    files = call_kwargs.kwargs.get("files")
   363→    assert files is not None
   364→    assert "/tmp/input.json" in files
   365→    assert files["/tmp/input.json"].data == b'{"data": true}'
   366→
   367→    output = ws.cat("/tmp/out.csv")
   368→    assert output == b"result"
   369→
   370→
   371→def test_dispatch_auto_detect_inplace():
   372→    mock_client = MagicMock(spec=CommandClient)
   373→    mock_client.run.return_value = CommandResult(
   374→        stdout="formatted",
   375→        output_files={"/tmp/data.json": b'{"formatted": true}'},
   376→    )
   377→    ws = Workspace({"/tmp/": MemoryBackend()},
   378→                   mode=MountMode.WRITE,
   379→                   sync_policy=SyncPolicy.IMMEDIATE)
   380→    ws.write("/tmp/data.json", b'{"data": true}')
   381→
   382→    reg = CommandRegistry(client=mock_client)
   383→    reg.register(ExternalCommand(name="format", exec="format"))
   384→    result = reg.run(ws, "format /tmp/data.json", cwd="/")
   385→    assert "formatted" in result
   386→    assert ws.cat("/tmp/data.json") == b'{"formatted": true}'
   387→
   388→
   389→def test_dispatch_auto_detect_no_files():
   390→    mock_client = MagicMock(spec=CommandClient)
   391→    mock_client.run.return_value = CommandResult(stdout="hello")
   392→    ws = Workspace({"/tmp/": MemoryBackend()})
   393→    reg = CommandRegistry(client=mock_client)
   394→    reg.register(ExternalCommand(name="echo", exec="echo"))
   395→    result = reg.run(ws, "echo hello", cwd="/")
   396→    assert "hello" in result
   397→
   398→
   399→def test_dispatch_locks_paths_during_execution():
   400→    mock_client = MagicMock(spec=CommandClient)
   401→    locked_during_run = []
   402→
   403→    def capture_locks(*args, **kwargs):
   404→        locked_during_run.extend(list(ws._locked_paths))
   405→        return CommandResult(stdout="ok")
   406→
   407→    mock_client.run.side_effect = capture_locks
   408→    ws = Workspace({"/tmp/": MemoryBackend()},
   409→                   mode=MountMode.WRITE,
   410→                   sync_policy=SyncPolicy.IMMEDIATE)
   411→    ws.write("/tmp/file.txt", b"data")
   412→    reg = CommandRegistry(client=mock_client)
   413→    reg.register(ExternalCommand(name="process", exec="process"))
   414→    reg.run(ws, "process /tmp/file.txt", cwd="/")
   415→    assert "/tmp/file.txt" in locked_during_run
   416→    assert len(ws._locked_paths) == 0
   417→
   418→
   419→def test_dispatch_size_limit_enforced():
   420→    mock_client = MagicMock(spec=CommandClient)
   421→    ws = Workspace({"/tmp/": MemoryBackend()},
   422→                   mode=MountMode.WRITE,
   423→                   sync_policy=SyncPolicy.IMMEDIATE)
   424→    ws.write("/tmp/big.bin", b"x" * 200)
   425→    reg = CommandRegistry(client=mock_client)
   426→    reg.register(
   427→        ExternalCommand(
   428→            name="process",
   429→            exec="process",
   430→            options=RemoteCommandOptions(max_file_size="100B"),
   431→        ))
   432→    with pytest.raises(ValueError, match="exceeds max"):
   433→        reg.run(ws, "process /tmp/big.bin", cwd="/")
   434→
   435→
   436→def test_dispatch_output_to_readonly_fails():
   437→    mock_client = MagicMock(spec=CommandClient)
   438→    ws = Workspace({"/data/": MemoryBackend()}, mode=MountMode.READ)
   439→    reg = CommandRegistry(client=mock_client)
   440→    reg.register(ExternalCommand(name="transform", exec="transform"))
   441→    with pytest.raises(PermissionError, match="read-only"):
   442→        reg.run(ws, "transform /data/in.json /data/out.csv", cwd="/")
   443→


     1→import pytest
     2→
     3→from mirage.backends.local.memory import MemoryBackend
     4→from mirage.runtime.base import CommandNotAllowedError, Runtime
     5→from mirage.types import MountMode, SyncPolicy
     6→from mirage.workspace import Workspace
     7→
     8→
     9→@pytest.fixture
    10→def runtime():
    11→    ws = Workspace(
    12→        {"/tmp/": MemoryBackend()},
    13→        mode=MountMode.WRITE,
    14→        sync_policy=SyncPolicy.IMMEDIATE,
    15→    )
    16→    return Runtime(ws)
    17→
    18→
    19→def test_empty_command_returns_empty_string(runtime):
    20→    assert runtime.run("") == ""
    21→
    22→
    23→def test_blocked_python_raises(runtime):
    24→    with pytest.raises(CommandNotAllowedError):
    25→        runtime.run("python script.py")
    26→
    27→
    28→def test_blocked_bash_raises(runtime):
    29→    with pytest.raises(CommandNotAllowedError):
    30→        runtime.run("bash -c 'echo hi'")
    31→
    32→
    33→def test_blocked_sudo_raises(runtime):
    34→    with pytest.raises(CommandNotAllowedError):
    35→        runtime.run("sudo ls")
    36→
    37→
    38→def test_unknown_command_raises(runtime):
    39→    with pytest.raises(ValueError, match="unknown command"):
    40→        runtime.run("nonexistent_cmd")
    41→
    42→
    43→def test_cd_changes_cwd(runtime):
    44→    runtime.ws.mkdir("/tmp/sub/")
    45→    runtime.run("cd /tmp/sub/")
    46→    assert runtime._cwd == "/tmp/sub/"
    47→
    48→
    49→def test_cd_no_args_goes_to_root(runtime):
    50→    runtime.ws.mkdir("/tmp/sub/")
    51→    runtime.run("cd /tmp/sub/")
    52→    runtime.run("cd")
    53→    assert runtime._cwd == "/"
    54→
    55→
    56→def test_cd_resets_cwd(runtime):
    57→    runtime.ws.mkdir("/tmp/sub/")
    58→    runtime.run("cd /tmp/sub/")
    59→    runtime.run("cd /tmp/")
    60→    assert runtime._cwd == "/tmp/"
    61→
    62→
    63→def test_whoami_always_has_agent_id(runtime):
    64→    result = runtime.run("whoami")
    65→    assert "agent:" in result
    66→    assert "cwd:" in result
    67→
    68→
    69→def test_whoami_with_agent_id():
    70→    ws = Workspace(
    71→        {"/tmp/": MemoryBackend()},
    72→        mode=MountMode.WRITE,
    73→        sync_policy=SyncPolicy.IMMEDIATE,
    74→    )
    75→    rt = Runtime(ws, agent_id="claude-code")
    76→    result = rt.run("whoami")
    77→    assert "agent: claude-code" in result
    78→    assert "cwd: /" in result
    79→
    80→
    81→def test_identify_sets_agent_id(runtime):
    82→    runtime.run("identify my-agent")
    83→    assert runtime.agent_id == "my-agent"
    84→
    85→
    86→def test_identify_returns_agent_id(runtime):
    87→    result = runtime.run("identify claude-session-42")
    88→    assert result == "claude-session-42"
    89→
    90→
    91→def test_whoami_shows_agent_after_identify(runtime):
    92→    runtime.run("identify test-agent")
    93→    result = runtime.run("whoami")
    94→    assert "agent: test-agent" in result
    95→
    96→
    97→def test_identify_with_spaces(runtime):
    98→    runtime.run("identify Claude Code Agent")
    99→    assert runtime.agent_id == "Claude Code Agent"
   100→


     1→import pytest
     2→
     3→from mirage.backends.local.memory import MemoryBackend
     4→from mirage.commands.custom.registry import CommandRegistry
     5→from mirage.fuse_ops.registry import FuseOpRegistry
     6→from mirage.runtime import MemoryRuntime
     7→from mirage.runtime.accessors.local import (LocalCommandsAccessor,
     8→                                            LocalFuseOpsAccessor,
     9→                                            LocalWorkspaceOpsAccessor)
    10→from mirage.types import MountMode
    11→from mirage.workspace import Workspace
    12→
    13→
    14→def _make_ws():
    15→    return Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    16→
    17→
    18→def test_local_commands_list():
    19→    reg = CommandRegistry()
    20→    acc = LocalCommandsAccessor(reg)
    21→    cmds = acc.list()
    22→    assert "ls" in cmds
    23→    assert "cat" in cmds
    24→
    25→
    26→def test_local_commands_register():
    27→    reg = CommandRegistry()
    28→    acc = LocalCommandsAccessor(reg)
    29→    acc.register("myjq", "jq .")
    30→    assert "myjq" in acc.list()
    31→
    32→
    33→def test_local_fuse_ops_list():
    34→    reg = FuseOpRegistry()
    35→    acc = LocalFuseOpsAccessor(reg)
    36→    ops = acc.list()
    37→    assert any(o["pattern"] == "*" for o in ops)
    38→
    39→
    40→def test_local_fuse_ops_register():
    41→    reg = FuseOpRegistry()
    42→    acc = LocalFuseOpsAccessor(reg)
    43→    acc.register("*.parquet", "parquet")
    44→    ops = acc.list()
    45→    assert any(o["pattern"] == "*.parquet" for o in ops)
    46→
    47→
    48→def test_local_fuse_ops_register_unknown():
    49→    reg = FuseOpRegistry()
    50→    acc = LocalFuseOpsAccessor(reg)
    51→    with pytest.raises(ValueError, match="unknown handler"):
    52→        acc.register("*.xyz", "nonexistent_handler_xyz")
    53→
    54→
    55→def test_local_workspace_ops_status():
    56→    ws = _make_ws()
    57→    acc = LocalWorkspaceOpsAccessor(ws)
    58→    result = acc.status()
    59→    assert isinstance(result, list)
    60→
    61→
    62→def test_local_workspace_ops_sync():
    63→    ws = _make_ws()
    64→    acc = LocalWorkspaceOpsAccessor(ws)
    65→    result = acc.sync()
    66→    assert isinstance(result, list)
    67→
    68→
    69→def test_local_workspace_ops_status_after_write():
    70→    ws = _make_ws()
    71→    ws.write("/f.txt", b"hello")
    72→    acc = LocalWorkspaceOpsAccessor(ws)
    73→    result = acc.status()
    74→    paths = [s["path"] for s in result]
    75→    assert "/f.txt" in paths
    76→
    77→
    78→def test_runtime_has_accessors():
    79→    ws = _make_ws()
    80→    rt = MemoryRuntime(ws)
    81→    assert hasattr(rt, "commands")
    82→    assert hasattr(rt, "fuse_ops")
    83→    assert hasattr(rt, "workspace")
    84→    assert "ls" in rt.commands.list()
    85→    assert isinstance(rt.fuse_ops.list(), list)
    86→    assert isinstance(rt.workspace.status(), list)
    87→
    88→
    89→def test_runtime_register_command_then_run():
    90→    ws = _make_ws()
    91→    rt = MemoryRuntime(ws)
    92→    rt.commands.register("hello", "echo hello")
    93→    assert "hello" in rt.commands.list()
    94→


     1→import pytest
     2→
     3→from mirage.backends.local.memory import MemoryBackend
     4→from mirage.runtime.memory import MemoryRuntime
     5→from mirage.types import MountMode, SyncPolicy
     6→from mirage.workspace import Workspace
     7→
     8→
     9→@pytest.fixture
    10→def runtime():
    11→    ws = Workspace(
    12→        {"/tmp/": MemoryBackend()},
    13→        mode=MountMode.WRITE,
    14→        sync_policy=SyncPolicy.IMMEDIATE,
    15→    )
    16→    return MemoryRuntime(ws)
    17→
    18→
    19→def test_run_ls_empty(runtime):
    20→    result = runtime.run("ls /tmp/")
    21→    assert isinstance(result, str)
    22→
    23→
    24→def test_run_ls_with_files(runtime):
    25→    runtime.ws.write("/tmp/a.txt", b"hello")
    26→    result = runtime.run("ls /tmp/")
    27→    assert "a.txt" in result
    28→
    29→
    30→def test_run_cat(runtime):
    31→    runtime.ws.write("/tmp/f.txt", b"content")
    32→    result = runtime.run("cat /tmp/f.txt")
    33→    assert "content" in result
    34→
    35→
    36→def test_run_pwd(runtime):
    37→    assert runtime.run("pwd") == "/"
    38→


     1→from mirage.backends.local.memory import MemoryBackend
     2→from mirage.commands import COMMAND_COST, COMMANDS
     3→from mirage.types import CostLevel
     4→from mirage.workspace import Workspace
     5→
     6→
     7→def test_commands_has_ls():
     8→    assert "ls" in COMMANDS
     9→
    10→
    11→def test_ls_handler(write_ws):
    12→    write_ws.write("/tmp/a.txt", b"hello")
    13→    result = COMMANDS["ls"](write_ws, [], "/tmp/")
    14→    assert "a.txt" in result
    15→
    16→
    17→def test_cat_handler(write_ws):
    18→    write_ws.write("/tmp/f.txt", b"world")
    19→    result = COMMANDS["cat"](write_ws, ["/tmp/f.txt"], "/")
    20→    assert result == "world"
    21→
    22→
    23→def test_pwd_handler():
    24→    ws = Workspace({"/tmp/": MemoryBackend()})
    25→    result = COMMANDS["pwd"](ws, [], "/tmp/")
    26→    assert result == "/tmp/"
    27→
    28→
    29→def test_head_handler(write_ws):
    30→    lines = "\n".join(f"line{i}" for i in range(20))
    31→    write_ws.write("/tmp/f.txt", lines.encode())
    32→    result = COMMANDS["head"](write_ws, ["-n", "3", "/tmp/f.txt"], "/")
    33→    assert result == "line0\nline1\nline2"
    34→
    35→
    36→def test_tail_handler(write_ws):
    37→    lines = "\n".join(f"line{i}" for i in range(20))
    38→    write_ws.write("/tmp/f.txt", lines.encode())
    39→    result = COMMANDS["tail"](write_ws, ["-n", "3", "/tmp/f.txt"], "/")
    40→    assert result == "line17\nline18\nline19"
    41→
    42→
    43→def test_command_cost_cheap_commands():
    44→    for cmd in ("ls", "stat", "find", "mkdir", "rm"):
    45→        assert COMMAND_COST[cmd] == CostLevel.CHEAP
    46→
    47→
    48→def test_command_cost_partial_commands():
    49→    for cmd in ("head", "tail", "grep"):
    50→        assert COMMAND_COST[cmd] == CostLevel.PARTIAL
    51→
    52→
    53→def test_command_cost_expensive_commands():
    54→    for cmd in ("cat", "wc", "md5", "sort", "uniq"):
    55→        assert COMMAND_COST[cmd] == CostLevel.EXPENSIVE
    56→
    57→
    58→def test_command_cost_network_commands():
    59→    for cmd in ("curl", "wget"):
    60→        assert COMMAND_COST[cmd] == CostLevel.NETWORK
    61→
    62→
    63→def test_all_commands_have_cost():
    64→    for cmd in COMMANDS:
    65→        assert cmd in COMMAND_COST, f"missing cost for {cmd!r}"
    66→