The file /Users/agent/strukto/mirage/mirage/fuse/fs.py has been updated. All occurrences of 'mirage.fuse_ops' were successfully replaced with 'mirage.fuse'.

import errno
import os
import stat
import subprocess
import sys
import threading
import time
import uuid
from collections import deque
from datetime import datetime, timezone

import fuse
from mirage.fuse_ops.base import FuseOp
from mirage.fuse_ops.registry import FuseOpRegistry
from mirage.workspace import Workspace

_ENV_AGENT_ID = "MIRAGE_AGENT_ID"
_MIRAGE_DIR = "/.mirage"
_MIRAGE_WHOAMI = "/.mirage/whoami"

_MAX_OPS = 200

class MirageFS(fuse.Operations):

    def __init__(self,
                 ws: Workspace,
                 agent_id: str | None = None,
                 fuse_registry: FuseOpRegistry | None = None) -> None:
        self.ws = ws
        self.agent_id = (agent_id or os.environ.get(_ENV_AGENT_ID)
                         or f"agent-{uuid.uuid4().hex[:8]}")
        self._now = time.time()
        self._prefixes = [m.prefix for m in ws._registry._mounts]
        self._ops: deque[dict] = deque(maxlen=_MAX_OPS)
        self._ops_lock = threading.Lock()
        self._read_seen: set[str] = set()
        self._fuse_registry = fuse_registry or FuseOpRegistry()
        self._handles: dict[int, dict] = {}
        self._handle_ops: dict[int, FuseOp] = {}
        self._next_fh = 1

    def _whoami_content(self) -> bytes:
        mounts = [m.prefix for m in self.ws._registry._mounts]
        lines = [
            f"agent: {self.agent_id}",
            "cwd: /",
            f"mounts: {', '.join(mounts)}",
        ]
        return ("\n".join(lines) + "\n").encode()

    def _dir_stat(self) -> dict:
        return {
            "st_mode": stat.S_IFDIR | 0o755,
            "st_nlink": 2,
            "st_uid": os.getuid(),
            "st_gid": os.getgid(),
            "st_size": 0,
            "st_atime": self._now,
            "st_mtime": self._now,
            "st_ctime": self._now,
        }

    def _file_stat(self, size: int) -> dict:
        return {
            "st_mode": stat.S_IFREG | 0o644,
            "st_nlink": 1,
            "st_uid": os.getuid(),
            "st_gid": os.getgid(),
            "st_size": size,
            "st_atime": self._now,
            "st_mtime": self._now,
            "st_ctime": self._now,
        }

    def _is_virtual_dir(self, path: str) -> bool:
        normalized = path.rstrip("/") + "/"
        for p in self._prefixes:
            if p.startswith(normalized) or p.rstrip("/") == path.rstrip("/"):
                return True
        return False

    def getattr(self, path: str, fh=None) -> dict:
        if path == "/":
            return self._dir_stat()
        if path == _MIRAGE_DIR:
            return self._dir_stat()
        if path == _MIRAGE_WHOAMI:
            return self._file_stat(len(self._whoami_content()))
        if self._is_virtual_dir(path):
            return self._dir_stat()
        try:
            s = self.ws.stat(path)
            return self._file_stat(s.size or 0)
        except (FileNotFoundError, ValueError):
            pass
        try:
            self.ws.ls(path)
            return self._dir_stat()
        except (FileNotFoundError, ValueError):
            raise fuse.FuseOSError(errno.ENOENT)

    def _virtual_children(self, path: str) -> list[str]:
        normalized = path.rstrip("/") + "/" if path != "/" else "/"
        children = set()
        for p in self._prefixes:
            if p.startswith(normalized) and p != normalized:
                rest = p[len(normalized):]
                child = rest.split("/")[0]
                if child:
                    children.add(child)
        return sorted(children)

    def _clean_path(self, path: str) -> str:
        basename = os.path.basename(path)
        if basename.startswith("._"):
            basename = basename[2:]
            return os.path.join(os.path.dirname(path), basename)
        return path

    def _record_op(self, op: str, path: str, raw: str) -> None:
        path = self._clean_path(path)
        with self._ops_lock:
            self._ops.append({
                "type":
                "fuse_op",
                "op":
                op,
                "path":
                path,
                "timestamp":
                datetime.now(timezone.utc).isoformat(),
                "raw":
                raw,
            })

    def drain_ops(self) -> list[dict]:
        with self._ops_lock:
            ops = list(self._ops)
            self._ops.clear()
            self._read_seen.clear()
            return ops

    def readdir(self, path: str, fh) -> list:
        if path == _MIRAGE_DIR:
            return [".", "..", "whoami"]
        names = set(self._virtual_children(path))
        if path == "/":
            names.add(".mirage")
        try:
            entries = self.ws.ls(path)
            for e in entries:
                part = e.rstrip("/").rsplit("/", 1)[-1]
                if part:
                    names.add(part)
        except (FileNotFoundError, ValueError):
            if not names:
                raise fuse.FuseOSError(errno.ENOENT)
        return [".", ".."] + sorted(names)

    def _record_read(self, path: str) -> None:
        clean = self._clean_path(path)
        with self._ops_lock:
            if clean not in self._read_seen:
                self._read_seen.add(clean)
                self._ops.append({
                    "type":
                    "fuse_op",
                    "op":
                    "read",
                    "path":
                    clean,
                    "timestamp":
                    datetime.now(timezone.utc).isoformat(),
                    "raw":
                    f"read {clean}",
                })

    def read(self, path: str, size: int, offset: int, fh) -> bytes:
        if path == _MIRAGE_WHOAMI:
            data = self._whoami_content()
            return data[offset:offset + size]
        op = self._handle_ops.get(fh)
        ctx = self._handles.get(fh)
        if op is None or ctx is None:
            try:
                data = self.ws.cat(path)
                self._record_read(path)
                return data[offset:offset + size]
            except (FileNotFoundError, ValueError):
                raise fuse.FuseOSError(errno.ENOENT)
        try:
            data = op.read(self.ws, path, offset, size, ctx)
            self._record_read(path)
            return data
        except (FileNotFoundError, ValueError):
            raise fuse.FuseOSError(errno.ENOENT)

    def write(self, path: str, data: bytes, offset: int, fh) -> int:
        op = self._handle_ops.get(fh)
        ctx = self._handles.get(fh)
        if op is not None and ctx is not None:
            try:
                result = op.write(self.ws, path, offset, data, ctx)
                self._record_op("write", path, f"write {path}")
                return result
            except PermissionError:
                raise fuse.FuseOSError(errno.EACCES)
        try:
            existing = b""
            try:
                existing = self.ws.cat(path)
            except FileNotFoundError:
                pass
            if offset > len(existing):
                existing = existing + b"\0" * (offset - len(existing))
            new_data = existing[:offset] + data + existing[offset + len(data):]
            self.ws.write(path, new_data)
            self._record_op("write", path, f"write {path}")
            return len(data)
        except PermissionError:
            raise fuse.FuseOSError(errno.EACCES)
        except ValueError:
            raise fuse.FuseOSError(errno.ENOENT)

    def create(self, path: str, mode, fi=None) -> int:
        try:
            self.ws.write(path, b"")
            self._record_op("create", path, f"create {path}")
            op = self._fuse_registry.resolve(path, 0, self.ws)
            ctx = op.on_open(self.ws, path, 0)
            ctx["path"] = path
            fh = self._next_fh
            self._next_fh += 1
            self._handles[fh] = ctx
            self._handle_ops[fh] = op
            return fh
        except PermissionError:
            raise fuse.FuseOSError(errno.EACCES)
        except ValueError:
            raise fuse.FuseOSError(errno.ENOENT)

    def mkdir(self, path: str, mode) -> None:
        try:
            self.ws.mkdir(path)
            self._record_op("mkdir", path, f"mkdir {path}")
        except PermissionError:
            raise fuse.FuseOSError(errno.EACCES)
        except ValueError:
            raise fuse.FuseOSError(errno.ENOENT)

    def unlink(self, path: str) -> None:
        try:
            self.ws.rm(path)
            self._record_op("rm", path, f"rm {path}")
        except PermissionError:
            raise fuse.FuseOSError(errno.EACCES)
        except FileNotFoundError:
            raise fuse.FuseOSError(errno.ENOENT)

    def rename(self, old: str, new: str, flags: int = 0) -> None:
        try:
            self.ws.mv(old, new)
            self._record_op("mv", old, f"mv {old} → {new}")
        except PermissionError:
            raise fuse.FuseOSError(errno.EACCES)
        except (FileNotFoundError, ValueError):
            raise fuse.FuseOSError(errno.ENOENT)

    def rmdir(self, path: str) -> None:
        try:
            entries = self.ws.ls(path)
            if entries:
                raise fuse.FuseOSError(errno.ENOTEMPTY)
            self._record_op("rmdir", path, f"rmdir {path}")
        except (FileNotFoundError, ValueError):
            raise fuse.FuseOSError(errno.ENOENT)

    def statfs(self, path: str) -> dict:
        return {
            "f_bsize": 4096,
            "f_frsize": 4096,
            "f_blocks": 1024 * 1024,
            "f_bfree": 1024 * 1024,
            "f_bavail": 1024 * 1024,
            "f_files": 1000000,
            "f_ffree": 1000000,
            "f_favail": 1000000,
            "f_namemax": 255,
        }

    def chmod(self, path: str, mode) -> None:
        self.getattr(path)

    def chown(self, path: str, uid: int, gid: int) -> None:
        self.getattr(path)

    def utimens(self, path: str, times=None) -> None:
        self.getattr(path)

    def access(self, path: str, amode: int) -> None:
        self.getattr(path)

    def flush(self, path: str, fh) -> None:
        op = self._handle_ops.get(fh)
        ctx = self._handles.get(fh)
        if op is not None and ctx is not None:
            try:
                op.on_flush(self.ws, path, ctx)
            except PermissionError:
                raise fuse.FuseOSError(errno.EACCES)
            return
        self.ws.sync(path)

    def fsync(self, path: str, datasync: int, fh) -> None:
        self.flush(path, fh)

    def open(self, path: str, flags) -> int:
        if path == _MIRAGE_WHOAMI:
            fh = self._next_fh
            self._next_fh += 1
            self._handles[fh] = {"path": path}
            return fh
        try:
            s = self.ws.stat(path)
            size = s.size or 0
        except (FileNotFoundError, ValueError):
            raise fuse.FuseOSError(errno.ENOENT)
        op = self._fuse_registry.resolve(path, size, self.ws)
        ctx = op.on_open(self.ws, path, size)
        ctx["path"] = path
        fh = self._next_fh
        self._next_fh += 1
        self._handles[fh] = ctx
        self._handle_ops[fh] = op
        return fh

    def release(self, path: str, fh) -> int:
        self._handles.pop(fh, None)
        self._handle_ops.pop(fh, None)
        return 0

    def truncate(self, path: str, length: int, fh=None) -> None:
        try:
            data = b""
            try:
                data = self.ws.cat(path)
            except FileNotFoundError:
                pass
            padded = data[:length].ljust(length, b"\0")
            self.ws.write(path, padded)
        except PermissionError:
            raise fuse.FuseOSError(errno.EACCES)
        except ValueError:
            raise fuse.FuseOSError(errno.ENOENT)

def _run_fuse(fs: MirageFS, mountpoint: str, foreground: bool) -> None:
    fuse.FUSE(fs, mountpoint, nothreads=True, foreground=foreground)

def mount_background(ws: Workspace,
                     mountpoint: str,
                     agent_id: str | None = None) -> threading.Thread:
    """Start FUSE in a daemon thread and return immediately."""
    fs = MirageFS(ws, agent_id=agent_id)
    t = threading.Thread(target=_run_fuse,
                         args=(fs, mountpoint, True),
                         daemon=True)
    t.start()
    time.sleep(0.3)
    return t

def mount(ws: Workspace | None = None,
          mountpoint: str = "",
          foreground: bool = True,
          agent_id: str | None = None,
          fs: MirageFS | None = None) -> None:
    if fs is None:
        fs = MirageFS(ws, agent_id=agent_id)
    t = threading.Thread(
        target=_run_fuse,
        args=(fs, mountpoint, foreground),
        daemon=True,
    )
    t.start()
    try:
        while t.is_alive():
            t.join(timeout=0.5)
    except KeyboardInterrupt:
        print("\nUnmounting...", flush=True)
        if sys.platform == "darwin":
            subprocess.run(
                ["diskutil", "unmount", "force", mountpoint],
                capture_output=True,
            )
        else:
            subprocess.run(["fusermount", "-u", mountpoint],
                           capture_output=True)
        t.join(timeout=5)


Now update all external imports. Two patterns to replace:
1. `mirage.fuse_ops` → `mirage.fuse` (with subpath adjustments)
2. `mirage_fuse` → `mirage.fuse`


TodoWrite

Create directory structure and git mv all files

Creating directory structure and moving files

Write sub-package __init__.py files

Update internal fuse imports

Update external imports (fuse_ops → fuse, mirage_fuse → mirage.fuse)

Update pyproject.toml (add fusepy optional dep)

Clean up old directories

Run tests


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable

Create directory structure and git mv all files

Creating directory structure and moving files

Write sub-package __init__.py files

Update internal fuse imports

Update external imports (fuse_ops → fuse, mirage_fuse → mirage.fuse)

Update pyproject.toml (add fusepy optional dep)

Clean up old directories

Run tests

Write sub-package __init__.py files

Update internal fuse imports

Clean up old directories

Run tests


The remaining external files need two kinds of changes. Let me handle them with parallel agents.


Agent

Update fuse_ops and mirage_fuse imports

Prompt:
Update all remaining imports across the codebase for the fuse_ops → fuse refactoring. There are two patterns to fix:

**Pattern 1: `mirage.fuse_ops` → `mirage.fuse`**
Files (outside of mirage/fuse/ which is already done):
- tests/fuse/test_remote.py: `from mirage.fuse_ops.remote import RemoteFuseOp` → `from mirage.fuse.remote.remote import RemoteFuseOp`
- tests/fuse/test_default.py: `from mirage.fuse_ops.default import DefaultFuseOp` → `from mirage.fuse.backend.default import DefaultFuseOp` AND `from mirage.fuse_ops.memory import MemoryFuseOp` → `from mirage.fuse.backend.memory import MemoryFuseOp`
- tests/fuse/test_loader.py: `from mirage.fuse_ops.loader import ...` → `from mirage.fuse.loader import ...` AND string literal `"from mirage.fuse_ops import FuseOp\n"` → `"from mirage.fuse import FuseOp\n"`
- tests/fuse/test_format_ops.py: `from mirage.fuse_ops.csv_op import CsvFuseOp` → `from mirage.fuse.filetype.csv import CsvFuseOp` AND `from mirage.fuse_ops.parquet import ParquetFuseOp` → `from mirage.fuse.filetype.parquet import ParquetFuseOp`
- tests/fuse/test_registry.py: `from mirage.fuse_ops import FUSE_OPS, FuseOp` → `from mirage.fuse import FUSE_OPS, FuseOp` AND `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry`
- tests/fuse/test_fs.py: `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry` (appears multiple times)
- tests/runtime/test_accessors.py: `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry`
- mirage/runtime/base.py: `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry`
- mirage/runtime/accessors/local.py: `from mirage.fuse_ops import FUSE_OPS, FuseOp` → `from mirage.fuse import FUSE_OPS, FuseOp` AND `from mirage.fuse_ops.loader import load_fuse_op_class` → `from mirage.fuse.loader import load_fuse_op_class` AND `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry`
- mirage/server/service/local.py: `from mirage.fuse_ops import FUSE_OPS` → `from mirage.fuse import FUSE_OPS` AND `from mirage.fuse_ops.loader import ...` → `from mirage.fuse.loader import ...` AND `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry`
- mirage/cli/commands/workspace.py: `from mirage.fuse_ops.loader import build_registry_from_config` → `from mirage.fuse.loader import build_registry_from_config` AND `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry` AND `from mirage.fuse_ops.remote import RemoteFuseOp` → `from mirage.fuse.remote.remote import RemoteFuseOp`

**Pattern 2: `mirage_fuse` → `mirage.fuse`**
These imports reference the old separate package `mirage_fuse`:
- mirage/cli/commands/workspace.py: `from mirage_fuse import MirageFS` → `from mirage.fuse.fs import MirageFS` AND `from mirage_fuse import mount as fuse_mount_fn` → `from mirage.fuse.fs import mount as fuse_mount_fn`
- examples/claude-memory.py: `from mirage_fuse import mount_background` → `from mirage.fuse.fs import mount_background`
- examples/claude-sdk.py: `from mirage_fuse import MirageFS, mount_background` → `from mirage.fuse.fs import MirageFS, mount_background`
- tests/fuse/test_fs.py: `from mirage_fuse.fs import _MIRAGE_DIR, _MIRAGE_WHOAMI, MirageFS` → `from mirage.fuse.fs import _MIRAGE_DIR, _MIRAGE_WHOAMI, MirageFS` AND `from mirage_fuse import mount_background` → `from mirage.fuse.fs import mount_background` (2 occurrences)
- fuse/README.md: `from mirage_fuse import MirageFS, mount` → `from mirage.fuse.fs import MirageFS, mount` (this file is now at mirage/fuse/README.md)

For each file: read it first, then use Edit tool to make the changes. Use replace_all where the same pattern appears multiple times.


All updates are complete and correct. Here is a summary of every change made:

---

## Summary of Changes

### Pattern 1: `mirage.fuse_ops` → `mirage.fuse`

**`/Users/agent/strukto/mirage/tests/fuse/test_remote.py`**
- `from mirage.fuse_ops.remote import RemoteFuseOp` → `from mirage.fuse.remote.remote import RemoteFuseOp`

**`/Users/agent/strukto/mirage/tests/fuse/test_default.py`**
- `from mirage.fuse_ops.default import DefaultFuseOp` → `from mirage.fuse.backend.default import DefaultFuseOp`
- `from mirage.fuse_ops.memory import MemoryFuseOp` → `from mirage.fuse.backend.memory import MemoryFuseOp`

**`/Users/agent/strukto/mirage/tests/fuse/test_loader.py`**
- `from mirage.fuse_ops.loader import (...)` → `from mirage.fuse.loader import (...)`
- String literal `"from mirage.fuse_ops import FuseOp\n"` → `"from mirage.fuse import FuseOp\n"`
- String literal `"mirage.fuse_ops.default:DefaultFuseOp"` → `"mirage.fuse.backend.default:DefaultFuseOp"`
- String literal `"mirage.fuse_ops.memory:MemoryFuseOp"` → `"mirage.fuse.backend.memory:MemoryFuseOp"`

**`/Users/agent/strukto/mirage/tests/fuse/test_format_ops.py`**
- `from mirage.fuse_ops.csv_op import CsvFuseOp` → `from mirage.fuse.filetype.csv import CsvFuseOp`
- `from mirage.fuse_ops.parquet import ParquetFuseOp` → `from mirage.fuse.filetype.parquet import ParquetFuseOp`

**`/Users/agent/strukto/mirage/tests/fuse/test_registry.py`**
- `from mirage.fuse_ops import FUSE_OPS, FuseOp` → `from mirage.fuse import FUSE_OPS, FuseOp`
- `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry`

**`/Users/agent/strukto/mirage/tests/fuse/test_fs.py`**
- `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry` (2 occurrences via `replace_all`)

**`/Users/agent/strukto/mirage/tests/runtime/test_accessors.py`**
- `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry`

**`/Users/agent/strukto/mirage/mirage/runtime/base.py`**
- `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry`

**`/Users/agent/strukto/mirage/mirage/runtime/accessors/local.py`**
- `from mirage.fuse_ops import FUSE_OPS, FuseOp` → `from mirage.fuse import FUSE_OPS, FuseOp`
- `from mirage.fuse_ops.loader import load_fuse_op_class` → `from mirage.fuse.loader import load_fuse_op_class`
- `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry`

**`/Users/agent/strukto/mirage/mirage/server/service/local.py`**
- `from mirage.fuse_ops import FUSE_OPS` → `from mirage.fuse import FUSE_OPS`
- `from mirage.fuse_ops.loader import (...)` → `from mirage.fuse.loader import (...)`
- `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry`

**`/Users/agent/strukto/mirage/mirage/cli/commands/workspace.py`**
- `from mirage.fuse_ops.loader import build_registry_from_config` → `from mirage.fuse.loader import build_registry_from_config`
- `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry`
- `from mirage.fuse_ops.remote import RemoteFuseOp` → `from mirage.fuse.remote.remote import RemoteFuseOp`

### Pattern 2: `mirage_fuse` → `mirage.fuse`

**`/Users/agent/strukto/mirage/mirage/cli/commands/workspace.py`**
- TYPE_CHECKING: `from mirage_fuse import MirageFS` → `from mirage.fuse.fs import MirageFS`
- `from mirage_fuse import MirageFS` → `from mirage.fuse.fs import MirageFS`
- `from mirage_fuse import mount as fuse_mount_fn` → `from mirage.fuse.fs import mount as fuse_mount_fn`

**`/Users/agent/strukto/mirage/examples/claude-memory.py`**
- `from mirage_fuse import mount_background` → `from mirage.fuse.fs import mount_background`

**`/Users/agent/strukto/mirage/examples/claude-sdk.py`**
- `from mirage_fuse import MirageFS, mount_background` → `from mirage.fuse.fs import MirageFS, mount_background`

**`/Users/agent/strukto/mirage/tests/fuse/test_fs.py`**
- `from mirage_fuse.fs import _MIRAGE_DIR, _MIRAGE_WHOAMI, MirageFS` → `from mirage.fuse.fs import _MIRAGE_DIR, _MIRAGE_WHOAMI, MirageFS`
- `from mirage_fuse import mount_background` → `from mirage.fuse.fs import mount_background` (2 occurrences via `replace_all`)

**`/Users/agent/strukto/mirage/mirage/fuse/README.md`**
- `from mirage_fuse import MirageFS, mount` → `from mirage.fuse.fs import MirageFS, mount`

### Bonus Fix

**`/Users/agent/strukto/mirage/tests/cli/test_app.py`**
- `patch("mirage_fuse.mount")` → `patch("mirage.cli.commands.workspace.fuse_mount_fn")` (2 occurrences) — necessary because `workspace.py` now imports `mount` locally as `fuse_mount_fn`, so the patch target must match the name in the importing module's namespace.

agentId: ad5b9a9e44f326029 (for resuming to continue this agent's work if needed)
<usage>total_tokens: 56203
tool_uses: 45
duration_ms: 207562</usage>

Prompt:
Update all remaining imports across the codebase for the fuse_ops → fuse refactoring. There are two patterns to fix:

**Pattern 1: `mirage.fuse_ops` → `mirage.fuse`**
Files (outside of mirage/fuse/ which is already done):
- tests/fuse/test_remote.py: `from mirage.fuse_ops.remote import RemoteFuseOp` → `from mirage.fuse.remote.remote import RemoteFuseOp`
- tests/fuse/test_default.py: `from mirage.fuse_ops.default import DefaultFuseOp` → `from mirage.fuse.backend.default import DefaultFuseOp` AND `from mirage.fuse_ops.memory import MemoryFuseOp` → `from mirage.fuse.backend.memory import MemoryFuseOp`
- tests/fuse/test_loader.py: `from mirage.fuse_ops.loader import ...` → `from mirage.fuse.loader import ...` AND string literal `"from mirage.fuse_ops import FuseOp\n"` → `"from mirage.fuse import FuseOp\n"`
- tests/fuse/test_format_ops.py: `from mirage.fuse_ops.csv_op import CsvFuseOp` → `from mirage.fuse.filetype.csv import CsvFuseOp` AND `from mirage.fuse_ops.parquet import ParquetFuseOp` → `from mirage.fuse.filetype.parquet import ParquetFuseOp`
- tests/fuse/test_registry.py: `from mirage.fuse_ops import FUSE_OPS, FuseOp` → `from mirage.fuse import FUSE_OPS, FuseOp` AND `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry`
- tests/fuse/test_fs.py: `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry` (appears multiple times)
- tests/runtime/test_accessors.py: `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry`
- mirage/runtime/base.py: `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry`
- mirage/runtime/accessors/local.py: `from mirage.fuse_ops import FUSE_OPS, FuseOp` → `from mirage.fuse import FUSE_OPS, FuseOp` AND `from mirage.fuse_ops.loader import load_fuse_op_class` → `from mirage.fuse.loader import load_fuse_op_class` AND `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry`
- mirage/server/service/local.py: `from mirage.fuse_ops import FUSE_OPS` → `from mirage.fuse import FUSE_OPS` AND `from mirage.fuse_ops.loader import ...` → `from mirage.fuse.loader import ...` AND `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry`
- mirage/cli/commands/workspace.py: `from mirage.fuse_ops.loader import build_registry_from_config` → `from mirage.fuse.loader import build_registry_from_config` AND `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry` AND `from mirage.fuse_ops.remote import RemoteFuseOp` → `from mirage.fuse.remote.remote import RemoteFuseOp`

**Pattern 2: `mirage_fuse` → `mirage.fuse`**
These imports reference the old separate package `mirage_fuse`:
- mirage/cli/commands/workspace.py: `from mirage_fuse import MirageFS` → `from mirage.fuse.fs import MirageFS` AND `from mirage_fuse import mount as fuse_mount_fn` → `from mirage.fuse.fs import mount as fuse_mount_fn`
- examples/claude-memory.py: `from mirage_fuse import mount_background` → `from mirage.fuse.fs import mount_background`
- examples/claude-sdk.py: `from mirage_fuse import MirageFS, mount_background` → `from mirage.fuse.fs import MirageFS, mount_background`
- tests/fuse/test_fs.py: `from mirage_fuse.fs import _MIRAGE_DIR, _MIRAGE_WHOAMI, MirageFS` → `from mirage.fuse.fs import _MIRAGE_DIR, _MIRAGE_WHOAMI, MirageFS` AND `from mirage_fuse import mount_background` → `from mirage.fuse.fs import mount_background` (2 occurrences)
- fuse/README.md: `from mirage_fuse import MirageFS, mount` → `from mirage.fuse.fs import MirageFS, mount` (this file is now at mirage/fuse/README.md)

For each file: read it first, then use Edit tool to make the changes. Use replace_all where the same pattern appears multiple times.


Good. Now let me verify no stale references remain, update pyproject.toml, and clean up.


fuse/pyproject.toml:16:include = ["mirage_fuse*"]
fuse/mirage_fuse/__init__.py:1:from mirage_fuse.fs import MirageFS, mount, mount_background


Those are in the old `fuse/` directory that we'll remove. Let me also check for the `fuse_ops` test directory reference.


README.md:15:        RT["Runtime
.commands · .fuse_ops · .workspace"]
README.md:23:        RRT["RemoteRuntime
.commands · .fuse_ops · .workspace"]
README.md:64:- **Runtime** is how an agent executes commands. All runtimes expose the same accessor API: `.commands` (list/register), `.fuse_ops` (list/register), `.workspace` (sync/status). `MemoryRuntime` and `LocalRuntime` use local implementations; `RemoteRuntime` forwards to the server via HTTP.
tests/runtime/test_remote.py:94:def test_remote_fuse_ops_list(rt):
tests/runtime/test_remote.py:95:    ops = rt.fuse_ops.list()
tests/runtime/test_remote.py:100:def test_remote_fuse_ops_register(rt):
tests/runtime/test_remote.py:101:    rt.fuse_ops.register("*.parquet", "parquet")
tests/runtime/test_remote.py:102:    ops = rt.fuse_ops.list()
mirage/runtime/base.py:69:        self.fuse_ops: FuseOpsAccessor = LocalFuseOpsAccessor(FuseOpRegistry())
tests/observe/test_api.py:243:    fuse_ops = [e for e in history if e["type"] == "fuse_op"]
tests/observe/test_api.py:244:    assert len(fuse_ops) == 2
tests/observe/test_api.py:245:    assert fuse_ops[0]["op"] == "create"
tests/observe/test_api.py:246:    assert fuse_ops[1]["raw"] == "write /data/a.py"
mirage/runtime/remote.py:33:        self.fuse_ops = RemoteFuseOpsAccessor(self._http, ws_id)
mirage/server/service/local.py:129:        fuse_ops_config = config.get("fuse_ops") or {}
mirage/server/service/local.py:130:        fuse_registry = build_registry_from_config(fuse_ops_config)
mirage/server/service/local.py:362:    def list_fuse_ops(self, ws_id: str) -> list[dict]:
mirage/server/service/base.py:99:    def list_fuse_ops(self, ws_id: str) -> list[dict]:
tests/cli/test_config.py:182:def test_load_workspace_yaml_with_fuse_ops(tmp_path):
tests/cli/test_config.py:186:                        "fuse_ops:\n"
tests/cli/test_config.py:190:    assert "fuse_ops" in cfg
tests/cli/test_config.py:191:    assert cfg["fuse_ops"]["*.parquet"] == "parquet"
tests/cli/test_config.py:192:    assert cfg["fuse_ops"]["*.csv"] == "csv"
tests/cli/test_config.py:195:def test_load_workspace_yaml_no_fuse_ops(tmp_path):
tests/cli/test_config.py:199:    assert "fuse_ops" not in cfg
tests/cli/test_config.py:220:                        "fuse_ops_url: http://ops:8002\n"
tests/cli/test_config.py:226:    assert cfg["fuse_ops_url"] == "http://ops:8002"
mirage/server/fastapi/config.py:21:    fuse_ops: dict[str, str] | None = None
tests/runtime/test_accessors.py:33:def test_local_fuse_ops_list():
tests/runtime/test_accessors.py:40:def test_local_fuse_ops_register():
tests/runtime/test_accessors.py:48:def test_local_fuse_ops_register_unknown():
tests/runtime/test_accessors.py:82:    assert hasattr(rt, "fuse_ops")
tests/runtime/test_accessors.py:85:    assert isinstance(rt.fuse_ops.list(), list)
mirage/server/fastapi/routers/command_router.py:90:def list_fuse_ops(ws_id: str, request: Request) -> list[dict]:
mirage/server/fastapi/routers/command_router.py:92:    return svc.list_fuse_ops(ws_id)
tests/server/test_app.py:245:def test_list_fuse_ops(client):
mirage/fuse/loader.py:35:def build_registry_from_config(fuse_ops: dict) -> FuseOpRegistry:
mirage/fuse/loader.py:37:    for pattern, value in fuse_ops.items():
mirage/cli/commands/workspace.py:239:        fuse_ops_config = cfg.get("fuse_ops", {}) if cfg else {}
mirage/cli/commands/workspace.py:240:        fuse_registry = build_registry_from_config(fuse_ops_config)
tests/server/test_service.py:176:def test_list_fuse_ops(svc):
tests/server/test_service.py:178:    ops = svc.list_fuse_ops(ws_id)
tests/server/test_service.py:186:    ops = svc.list_fuse_ops(ws_id)
tests/fuse/test_registry.py:9:def test_fuse_ops_dict_has_default():
tests/fuse/test_registry.py:13:def test_fuse_ops_dict_has_all_backends():
tests/fuse/test_registry.py:57:def test_fuse_ops_dict_has_format_ops():
mirage/cli/config.py:177:    fuse_ops = data.get("fuse_ops", {})
mirage/cli/config.py:178:    if fuse_ops:
mirage/cli/config.py:179:        result["fuse_ops"] = fuse_ops
mirage/cli/config.py:180:    for key in ("runtime", "url", "ws_id", "command_url", "fuse_ops_url"):


The remaining `fuse_ops` references are:
1. README.md - mentions `fuse_ops` as an API accessor name (like `rt.fuse_ops`)
2. Runtime code - `self.fuse_ops` is an accessor attribute name
3. Config/YAML keys - `fuse_ops` is a config key name
4. Test function names and variables
5. Server API - `list_fuse_ops` method names

These are NOT import paths - they're variable names, config keys, API method names, etc. These are a different concern from the file/module rename. The user asked to rename the folder, not necessarily all the API-level names. These config keys and accessor names are part of the public API.

I should leave these alone since they're part of the public API surface, not the folder structure. Let me focus on cleaning up the old directories and verifying the tests still pass.


The remaining `fuse_ops` references are variable names, config keys, and API method names (like `rt.fuse_ops`, `list_fuse_ops`, YAML `fuse_ops:` key) — not import paths. Those are fine to leave as-is since they're part of the public API, not module paths.

Now let me clean up and update pyproject.toml.


TodoWrite

Create directory structure and git mv all files

Creating directory structure and moving files

Write sub-package __init__.py files

Update internal fuse imports

Update external imports (fuse_ops → fuse, mirage_fuse → mirage.fuse)

Update pyproject.toml (add fusepy optional dep)

Clean up old directories

Run tests


Write sub-package __init__.py files

Update internal fuse imports

Clean up old directories

Run tests

Write sub-package __init__.py files

Update internal fuse imports

Clean up old directories

Run tests


     1→[project]
     2→name = "mirage"
     3→version = "0.0.1"
     4→description = "Add your description here"
     5→readme = "README.md"
     6→requires-python = ">=3.12"
     7→dependencies = [
     8→    "fastapi>=0.135.1",
     9→    "grpcio>=1.62.0",
    10→    "httpx>=0.28.1",
    11→    "mirage-fuse",
    12→    "pre-commit",
    13→    "pytest",
    14→    "python-dotenv>=1.2.2",
    15→    "pyyaml>=6.0.3",
    16→    "typer>=0.12.0",
    17→    "uvicorn[standard]>=0.41.0",
    18→]
    19→
    20→[build-system]
    21→requires = ["setuptools>=68"]
    22→build-backend = "setuptools.build_meta"
    23→
    24→[project.scripts]
    25→mirage = "mirage.cli.app:main"
    26→
    27→[tool.setuptools.packages.find]
    28→include = ["mirage*"]
    29→
    30→[project.optional-dependencies]
    31→s3      = ["boto3>=1.34"]
    32→r2      = ["boto3>=1.34"]
    33→http    = []
    34→parquet = [
    35→    "pandas>=3.0.1",
    36→    "pyarrow>=15.0",
    37→]
    38→all     = ["boto3>=1.34", "pyarrow>=15.0"]
    39→
    40→[tool.uv.workspace]
    41→members = ["fuse"]
    42→
    43→[tool.uv.sources]
    44→mirage-fuse = { workspace = true }
    45→
    46→[dependency-groups]
    47→dev = [
    48→    "grpcio>=1.78.0",
    49→    "grpcio-tools>=1.78.0",
    50→    "pytest-cov>=7.0.0",
    51→    "pytest-httpx>=0.36.0",
    52→]
    53→
    54→[tool.flake8]
    55→exclude = ["mirage/commands/remote/grpc/proto/command_pb2.py", "mirage/commands/remote/grpc/proto/command_pb2_grpc.py"]
    56→
    57→[tool.pytest.ini_options]
    58→testpaths = ["tests"]
    59→addopts = "--cov=mirage --cov-report=term-missing -q"
    60→

[project]
name = "mirage"
version = "0.0.1"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "fastapi>=0.135.1",
    "grpcio>=1.62.0",
    "httpx>=0.28.1",
    "mirage-fuse",
    "pre-commit",
    "pytest",
    "python-dotenv>=1.2.2",
    "pyyaml>=6.0.3",
    "typer>=0.12.0",
    "uvicorn[standard]>=0.41.0",
]

[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"

[project.scripts]
mirage = "mirage.cli.app:main"

[tool.setuptools.packages.find]
include = ["mirage*"]

[project.optional-dependencies]
s3      = ["boto3>=1.34"]
r2      = ["boto3>=1.34"]
http    = []
parquet = [
    "pandas>=3.0.1",
    "pyarrow>=15.0",
]
all     = ["boto3>=1.34", "pyarrow>=15.0"]

[tool.uv.workspace]
members = ["fuse"]

[tool.uv.sources]
mirage-fuse = { workspace = true }

[dependency-groups]
dev = [
    "grpcio>=1.78.0",
    "grpcio-tools>=1.78.0",
    "pytest-cov>=7.0.0",
    "pytest-httpx>=0.36.0",
]

[tool.flake8]
exclude = ["mirage/commands/remote/grpc/proto/command_pb2.py", "mirage/commands/remote/grpc/proto/command_pb2_grpc.py"]

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "--cov=mirage --cov-report=term-missing -q"


The file /Users/agent/strukto/mirage/pyproject.toml has been updated successfully.

[project]
name = "mirage"
version = "0.0.1"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "fastapi>=0.135.1",
    "grpcio>=1.62.0",
    "httpx>=0.28.1",
    "mirage-fuse",
    "pre-commit",
    "pytest",
    "python-dotenv>=1.2.2",
    "pyyaml>=6.0.3",
    "typer>=0.12.0",
    "uvicorn[standard]>=0.41.0",
]

[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"

[project.scripts]
mirage = "mirage.cli.app:main"

[tool.setuptools.packages.find]
include = ["mirage*"]

[project.optional-dependencies]
s3      = ["boto3>=1.34"]
r2      = ["boto3>=1.34"]
http    = []
parquet = [
    "pandas>=3.0.1",
    "pyarrow>=15.0",
]
all     = ["boto3>=1.34", "pyarrow>=15.0"]

[tool.uv.workspace]
members = ["fuse"]

[tool.uv.sources]
mirage-fuse = { workspace = true }

[dependency-groups]
dev = [
    "grpcio>=1.78.0",
    "grpcio-tools>=1.78.0",
    "pytest-cov>=7.0.0",
    "pytest-httpx>=0.36.0",
]

[tool.flake8]
exclude = ["mirage/commands/remote/grpc/proto/command_pb2.py", "mirage/commands/remote/grpc/proto/command_pb2_grpc.py"]

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "--cov=mirage --cov-report=term-missing -q"


Edit

http    = []
fuse    = ["fusepy>=3.0.1"]
parquet = [


http    = []
fuse    = ["fusepy>=3.0.1"]
parquet = [

[project]
name = "mirage"
version = "0.0.1"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "fastapi>=0.135.1",
    "grpcio>=1.62.0",
    "httpx>=0.28.1",
    "pre-commit",
    "pytest",
    "python-dotenv>=1.2.2",
    "pyyaml>=6.0.3",
    "typer>=0.12.0",
    "uvicorn[standard]>=0.41.0",
]

[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"

[project.scripts]
mirage = "mirage.cli.app:main"

[tool.setuptools.packages.find]
include = ["mirage*"]

[project.optional-dependencies]
s3      = ["boto3>=1.34"]
r2      = ["boto3>=1.34"]
http    = []
parquet = [
    "pandas>=3.0.1",
    "pyarrow>=15.0",
]
all     = ["boto3>=1.34", "pyarrow>=15.0"]

[tool.uv.workspace]
members = ["fuse"]

[tool.uv.sources]
mirage-fuse = { workspace = true }

[dependency-groups]
dev = [
    "grpcio>=1.78.0",
    "grpcio-tools>=1.78.0",
    "pytest-cov>=7.0.0",
    "pytest-httpx>=0.36.0",
]

[tool.flake8]
exclude = ["mirage/commands/remote/grpc/proto/command_pb2.py", "mirage/commands/remote/grpc/proto/command_pb2_grpc.py"]

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "--cov=mirage --cov-report=term-missing -q"


[project]
name = "mirage"
version = "0.0.1"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "fastapi>=0.135.1",
    "grpcio>=1.62.0",
    "httpx>=0.28.1",
    "pre-commit",
    "pytest",
    "python-dotenv>=1.2.2",
    "pyyaml>=6.0.3",
    "typer>=0.12.0",
    "uvicorn[standard]>=0.41.0",
]

[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"

[project.scripts]
mirage = "mirage.cli.app:main"

[tool.setuptools.packages.find]
include = ["mirage*"]

[project.optional-dependencies]
s3      = ["boto3>=1.34"]
r2      = ["boto3>=1.34"]
http    = []
fuse    = ["fusepy>=3.0.1"]
parquet = [
    "pandas>=3.0.1",
    "pyarrow>=15.0",
]
all     = ["boto3>=1.34", "pyarrow>=15.0"]

[tool.uv.workspace]
members = ["fuse"]

[tool.uv.sources]
mirage-fuse = { workspace = true }

[dependency-groups]
dev = [
    "grpcio>=1.78.0",
    "grpcio-tools>=1.78.0",
    "pytest-cov>=7.0.0",
    "pytest-httpx>=0.36.0",
]

[tool.flake8]
exclude = ["mirage/commands/remote/grpc/proto/command_pb2.py", "mirage/commands/remote/grpc/proto/command_pb2_grpc.py"]

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "--cov=mirage --cov-report=term-missing -q"


[tool.uv.workspace]
members = ["fuse"]

[tool.uv.sources]
mirage-fuse = { workspace = true }

[dependency-groups]


[project]
name = "mirage"
version = "0.0.1"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "fastapi>=0.135.1",
    "grpcio>=1.62.0",
    "httpx>=0.28.1",
    "pre-commit",
    "pytest",
    "python-dotenv>=1.2.2",
    "pyyaml>=6.0.3",
    "typer>=0.12.0",
    "uvicorn[standard]>=0.41.0",
]

[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"

[project.scripts]
mirage = "mirage.cli.app:main"

[tool.setuptools.packages.find]
include = ["mirage*"]

[project.optional-dependencies]
s3      = ["boto3>=1.34"]
r2      = ["boto3>=1.34"]
http    = []
fuse    = ["fusepy>=3.0.1"]
parquet = [
    "pandas>=3.0.1",
    "pyarrow>=15.0",
]
all     = ["boto3>=1.34", "fusepy>=3.0.1", "pyarrow>=15.0"]

[tool.uv.workspace]
members = ["fuse"]

[tool.uv.sources]
mirage-fuse = { workspace = true }

[dependency-groups]
dev = [
    "grpcio>=1.78.0",
    "grpcio-tools>=1.78.0",
    "pytest-cov>=7.0.0",
    "pytest-httpx>=0.36.0",
]

[tool.flake8]
exclude = ["mirage/commands/remote/grpc/proto/command_pb2.py", "mirage/commands/remote/grpc/proto/command_pb2_grpc.py"]

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "--cov=mirage --cov-report=term-missing -q"


Now clean up old directories.


TodoWrite

Create directory structure and git mv all files

Creating directory structure and moving files

Write sub-package __init__.py files

Update internal fuse imports

Update external imports (fuse_ops → fuse, mirage_fuse → mirage.fuse)

Update pyproject.toml (add fusepy optional dep)

Clean up old directories

Run tests


Bash

git rm mirage/fuse_ops/__init__.py && rm -rf mirage/fuse_ops/__pycache__ && rmdir mirage/fuse_ops 2>/dev/null; git rm fuse/mirage_fuse/__init__.py && rm -rf fuse/mirage_fuse/__pycache__ && rmdir fuse/mirage_fuse 2>/dev/null; git rm fuse/tests/test_fs.py 2>/dev/null; git rm fuse/tests/__init__.py 2>/dev/null; rm -rf fuse/tests/__pycache__ && rmdir fuse/tests 2>/dev/null; echo "Done cleaning"

Remove old fuse_ops/ and fuse/mirage_fuse/ directories


rm 'mirage/fuse_ops/__init__.py'
rm 'fuse/mirage_fuse/__init__.py'
Done cleaning


Check what's left in fuse/


mirage_fuse.egg-info
pyproject.toml


git rm fuse/pyproject.toml && rm -rf fuse/mirage_fuse.egg-info fuse/.pytest_cache fuse/.coverage && rmdir fuse/tests 2>/dev/null; rmdir fuse/mirage_fuse 2>/dev/null; rmdir fuse 2>/dev/null || echo "fuse dir has untracked files left"; ls fuse/ 2>/dev/null || echo "fuse/ removed"

Remove remaining fuse/ files


rm 'fuse/pyproject.toml'
fuse/ removed


Now sync deps and run tests.


TodoWrite

Create directory structure and git mv all files

Creating directory structure and moving files

Write sub-package __init__.py files

Update internal fuse imports

Update external imports (fuse_ops → fuse, mirage_fuse → mirage.fuse)

Update pyproject.toml (add fusepy optional dep)

Clean up old directories

Run tests


Bash

Sync dependencies


 + jmespath==1.1.0
 ~ mirage==0.0.1 (from file:///Users/agent/strukto/mirage)
 - mirage-fuse==0.0.1 (from file:///Users/agent/strukto/mirage/fuse)
 + s3transfer==0.16.0
 + urllib3==2.6.3


Run full test suite


mirage/server/service/local.py                            260     15    94%   56, 92-93, 107, 124, 205, 225-227, 338-340, 372, 389-390
mirage/types.py                                            91      0   100%
mirage/workspace/__init__.py                              241     11    95%   88, 170, 219-220, 223-224, 272-275, 333
mirage/workspace/cache/__init__.py                          3      0   100%
mirage/workspace/cache/entry.py                             7      0   100%
mirage/workspace/cache/policy.py                           26      3    88%   24-25, 37
mirage/workspace/cache/store.py                            45      1    98%   14
mirage/workspace/mount.py                                  34      1    97%   63
mirage/workspace/sync.py                                   56      0   100%
-------------------------------------------------------------------------------------
TOTAL                                                    4704   1236    74%
=========================== short test summary info ============================
FAILED tests/cli/test_app.py::test_mount_fuse_backend_mounts_at_root - Attrib...
FAILED tests/cli/test_app.py::test_mount_fuse_backend_with_extra_spec - Attri...
2 failed, 639 passed, 1 skipped, 2 warnings in 3.12s


Two failures in `test_app.py`. Let me check.


uv run pytest tests/cli/test_app.py::test_mount_fuse_backend_mounts_at_root -v 2>&1 | tail -20

Check test failure details


mirage/server/fastapi/routers/io_router.py                110    110     0%   1-139
mirage/server/fastapi/routers/workspace_router.py          60     60     0%   1-95
mirage/server/routes.py                                    27      0   100%
mirage/server/service/__init__.py                           0      0   100%
mirage/server/service/base.py                               7      7     0%   1-64
mirage/server/service/cache.py                             24     24     0%   1-33
mirage/server/service/local.py                            260    260     0%   1-428
mirage/types.py                                            91      0   100%
mirage/workspace/__init__.py                              241    199    17%   18-19, 22, 25, 58-78, 88, 91-92, 97-141, 144-177, 180-181, 186-196, 199-203, 206-207, 210-211, 214-220, 223-224, 229-237, 240-242, 245-252, 255-275, 278-281, 286-298, 301, 304-305, 310, 313-314, 317-319, 322-323, 329-333, 336-339, 342-344, 352-367
mirage/workspace/cache/__init__.py                          3      0   100%
mirage/workspace/cache/entry.py                             7      0   100%
mirage/workspace/cache/policy.py                           26     20    23%   15-26, 30-37
mirage/workspace/cache/store.py                            45     30    33%   8-14, 20-22, 25-28, 36-47, 50-52, 55-56, 60, 64, 67-69
mirage/workspace/mount.py                                  34     24    29%   12-16, 29, 36-41, 50-56, 59-63, 66
mirage/workspace/sync.py                                   56     41    27%   20, 25-33, 43-50, 60-63, 72-93, 102, 105, 108, 111, 115
-------------------------------------------------------------------------------------
TOTAL                                                    4704   4048    14%
=========================== short test summary info ============================
FAILED tests/cli/test_app.py::test_mount_fuse_backend_mounts_at_root - Attrib...
============================== 1 failed in 0.24s ===============================


uv run pytest tests/cli/test_app.py::test_mount_fuse_backend_mounts_at_root -v --tb=short 2>&1 | grep -A 20 "FAILED\|AttributeError\|Error"

Get error details


    raise AttributeError(
E   AttributeError: <module 'mirage.cli.commands.workspace' from '/Users/agent/strukto/mirage/mirage/cli/commands/workspace.py'> does not have the attribute 'fuse_mount_fn'
================================ tests coverage ================================
______________ coverage: platform darwin, python 3.12.10-final-0 _______________

Name                                                    Stmts   Miss  Cover   Missing
-------------------------------------------------------------------------------------
mirage/__init__.py                                          4      0   100%
mirage/backend/__init__.py                                  4      0   100%
mirage/backend/base.py                                     81     57    30%   16, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 56-60, 63-67, 70-77, 81, 87-89, 96-103, 107-115, 118, 123-127
mirage/backend/filetype.py                                  6      2    67%   37-38
mirage/backend/loader.py                                   20     16    20%   20-42
mirage/backend/local/__init__.py                            3      0   100%
mirage/backend/local/local.py                              69     47    32%   20, 26-30, 33-41, 49-52, 57-65, 68-71, 74, 77, 80-84, 87-89, 92-93, 96-97, 100-101, 104-106, 109-110
mirage/backend/local/memory.py                            119     96    19%   20-22, 28-29, 32, 35-36, 41-44, 49-67, 70-84, 87, 90, 93-103, 106-112, 115-119, 122-132, 135-141, 144-159, 162-170, 175-176
mirage/backend/network/__init__.py                          2      0   100%
mirage/backend/network/http.py                             62     62     0%   1-119
mirage/backend/network/proxy.py                            51     31    39%   16-18, 21-29, 32-33, 38-39, 42-43, 46, 49, 52-58, 61, 64, 67, 70, 73, 76, 79-80
mirage/backend/object_storage/__init__.py                   1      1     0%   1
mirage/backend/object_storage/r2.py                        16     16     0%   1-41
mirage/backend/object_storage/s3.py                       184    184     0%   1-341
mirage/cli/__init__.py                                      0      0   100%
--
FAILED tests/cli/test_app.py::test_mount_fuse_backend_mounts_at_root - Attrib...
============================== 1 failed in 0.23s ===============================


The subagent patched `mirage.cli.commands.workspace.fuse_mount_fn` but `fuse_mount_fn` is imported inside a function (lazy import), so it doesn't exist as a module attribute. Let me check the test and the workspace code.


    35→        mode=MountMode.READ,
    36→        consistency=ConsistencyPolicy.NONE,
    37→        ttl=300,
    38→        config=None,
    39→        runtime=RuntimeType.MEMORY,
    40→        fuse=True,
    41→        agent_id=None,
    42→    )
    43→
    44→
    45→def test_mount_fuse_backend_mounts_at_root():
    46→    """--backend memory with --fuse mounts the backend at / prefix."""
    47→    from mirage.cli.commands.workspace import mount
    48→    with tempfile.TemporaryDirectory() as mp:
    49→        with patch("mirage.cli.commands.workspace.fuse_mount_fn") as mock_fuse, \
    50→             patch("mirage.cli.commands.workspace._ensure_server"), \
    51→             patch("mirage.cli.commands.workspace._client"):
    52→            mount(backend="memory", mountpoint=mp, **_fuse_defaults())
    53→            ws = mock_fuse.call_args.kwargs["fs"].ws
    54→            prefixes = [m.prefix for m in ws._registry._mounts]
    55→            assert "/" in prefixes
    56→
    57→
    58→def test_mount_fuse_backend_with_extra_spec():
    59→    """--backend combined with positional prefix:uri spec and --fuse."""
    60→    from mirage.cli.commands.workspace import mount
    61→    with tempfile.TemporaryDirectory() as mp:
    62→        with patch("mirage.cli.commands.workspace.fuse_mount_fn") as mock_fuse, \
    63→             patch("mirage.cli.commands.workspace._ensure_server"), \
    64→             patch("mirage.cli.commands.workspace._client"):
    65→            mount(["memory:/data/"],
    66→                  backend="memory",
    67→                  mountpoint=mp,
    68→                  **_fuse_defaults())
    69→            ws = mock_fuse.call_args.kwargs["fs"].ws

        mode=MountMode.READ,
        consistency=ConsistencyPolicy.NONE,
        ttl=300,
        config=None,
        runtime=RuntimeType.MEMORY,
        fuse=True,
        agent_id=None,
    )

def test_mount_fuse_backend_mounts_at_root():
    """--backend memory with --fuse mounts the backend at / prefix."""
    from mirage.cli.commands.workspace import mount
    with tempfile.TemporaryDirectory() as mp:
        with patch("mirage.cli.commands.workspace.fuse_mount_fn") as mock_fuse, \
             patch("mirage.cli.commands.workspace._ensure_server"), \
             patch("mirage.cli.commands.workspace._client"):
            mount(backend="memory", mountpoint=mp, **_fuse_defaults())
            ws = mock_fuse.call_args.kwargs["fs"].ws
            prefixes = [m.prefix for m in ws._registry._mounts]
            assert "/" in prefixes

def test_mount_fuse_backend_with_extra_spec():
    """--backend combined with positional prefix:uri spec and --fuse."""
    from mirage.cli.commands.workspace import mount
    with tempfile.TemporaryDirectory() as mp:
        with patch("mirage.cli.commands.workspace.fuse_mount_fn") as mock_fuse, \
             patch("mirage.cli.commands.workspace._ensure_server"), \
             patch("mirage.cli.commands.workspace._client"):
            mount(["memory:/data/"],
                  backend="memory",
                  mountpoint=mp,
                  **_fuse_defaults())
            ws = mock_fuse.call_args.kwargs["fs"].ws


15:from mirage.fuse.loader import build_registry_from_config
16:from mirage.fuse.registry import FuseOpRegistry
23:    from mirage.fuse.fs import MirageFS
205:    from mirage.fuse.fs import MirageFS
206:    from mirage.fuse.fs import mount as fuse_mount_fn
207:    from mirage.fuse.remote.remote import RemoteFuseOp
250:    fuse_mount_fn(mountpoint=mountpoint, foreground=True, fs=fs)


The `fuse_mount_fn` is a lazy import inside a function (line 206), so patching it on the module doesn't work. The correct patch target is `mirage.fuse.fs.mount`. Let me fix the test.


     1→import os
     2→import subprocess
     3→import sys
     4→import tempfile
     5→import time
     6→from unittest.mock import patch
     7→
     8→import pytest
     9→
    10→
    11→def _run(args: list[str],
    12→         env: dict | None = None,
    13→         check: bool = True) -> subprocess.CompletedProcess:
    14→    e = os.environ.copy()
    15→    if env:
    16→        e.update(env)
    17→    return subprocess.run(
    18→        [sys.executable, "-m", "mirage.cli.app"] + args,
    19→        capture_output=True,
    20→        text=True,
    21→        env=e,
    22→        check=check,
    23→    )
    24→
    25→
    26→def test_help():
    27→    result = _run(["--help"], check=False)
    28→    assert result.returncode == 0
    29→
    30→
    31→def _fuse_defaults() -> dict:
    32→    from mirage.types import ConsistencyPolicy, MountMode, RuntimeType
    33→    return dict(
    34→        sync_policy="staged",
    35→        mode=MountMode.READ,
    36→        consistency=ConsistencyPolicy.NONE,
    37→        ttl=300,
    38→        config=None,
    39→        runtime=RuntimeType.MEMORY,
    40→        fuse=True,
    41→        agent_id=None,
    42→    )
    43→
    44→
    45→def test_mount_fuse_backend_mounts_at_root():
    46→    """--backend memory with --fuse mounts the backend at / prefix."""
    47→    from mirage.cli.commands.workspace import mount
    48→    with tempfile.TemporaryDirectory() as mp:
    49→        with patch("mirage.cli.commands.workspace.fuse_mount_fn") as mock_fuse, \
    50→             patch("mirage.cli.commands.workspace._ensure_server"), \
    51→             patch("mirage.cli.commands.workspace._client"):
    52→            mount(backend="memory", mountpoint=mp, **_fuse_defaults())
    53→            ws = mock_fuse.call_args.kwargs["fs"].ws
    54→            prefixes = [m.prefix for m in ws._registry._mounts]
    55→            assert "/" in prefixes
    56→
    57→
    58→def test_mount_fuse_backend_with_extra_spec():
    59→    """--backend combined with positional prefix:uri spec and --fuse."""
    60→    from mirage.cli.commands.workspace import mount
    61→    with tempfile.TemporaryDirectory() as mp:
    62→        with patch("mirage.cli.commands.workspace.fuse_mount_fn") as mock_fuse, \
    63→             patch("mirage.cli.commands.workspace._ensure_server"), \
    64→             patch("mirage.cli.commands.workspace._client"):
    65→            mount(["memory:/data/"],
    66→                  backend="memory",
    67→                  mountpoint=mp,
    68→                  **_fuse_defaults())
    69→            ws = mock_fuse.call_args.kwargs["fs"].ws
    70→            prefixes = [m.prefix for m in ws._registry._mounts]
    71→            assert "/" in prefixes
    72→            assert any(p.rstrip("/") == "/data" for p in prefixes)
    73→
    74→
    75→def test_mount_fuse_no_backend_no_spec_raises():
    76→    """mount --fuse with no --backend and no specs exits with error."""
    77→    result = _run(["mount", "--fuse", "--mountpoint", "/tmp/x"], check=False)
    78→    assert result.returncode != 0
    79→
    80→
    81→@pytest.mark.skip(reason="requires live server")
    82→def test_mount_exec_kill():
    83→    tmpdir = tempfile.mkdtemp(dir="/tmp")
    84→    env = {"MIRAGE_HOME": tmpdir}
    85→
    86→    result = _run(["mount", "memory:/data/"], env=env)
    87→    ws_id = result.stdout.strip()
    88→    assert len(ws_id) == 8
    89→
    90→    time.sleep(0.3)
    91→
    92→    result = _run(["exec", ws_id, "ls", "/data"], env=env)
    93→    assert result.returncode == 0
    94→
    95→    result = _run(["workspace", "ls"], env=env)
    96→    assert ws_id in result.stdout
    97→
    98→    _run(["kill", ws_id], env=env)
    99→    time.sleep(0.3)
   100→
   101→    result = _run(["workspace", "ls"], env=env, check=False)
   102→    assert ws_id not in result.stdout
   103→

import os
import subprocess
import sys
import tempfile
import time
from unittest.mock import patch

import pytest

def _run(args: list[str],
         env: dict | None = None,
         check: bool = True) -> subprocess.CompletedProcess:
    e = os.environ.copy()
    if env:
        e.update(env)
    return subprocess.run(
        [sys.executable, "-m", "mirage.cli.app"] + args,
        capture_output=True,
        text=True,
        env=e,
        check=check,
    )

def test_help():
    result = _run(["--help"], check=False)
    assert result.returncode == 0

def _fuse_defaults() -> dict:
    from mirage.types import ConsistencyPolicy, MountMode, RuntimeType
    return dict(
        sync_policy="staged",
        mode=MountMode.READ,
        consistency=ConsistencyPolicy.NONE,
        ttl=300,
        config=None,
        runtime=RuntimeType.MEMORY,
        fuse=True,
        agent_id=None,
    )

def test_mount_fuse_backend_mounts_at_root():
    """--backend memory with --fuse mounts the backend at / prefix."""
    from mirage.cli.commands.workspace import mount
    with tempfile.TemporaryDirectory() as mp:
        with patch("mirage.cli.commands.workspace.fuse_mount_fn") as mock_fuse, \
             patch("mirage.cli.commands.workspace._ensure_server"), \
             patch("mirage.cli.commands.workspace._client"):
            mount(backend="memory", mountpoint=mp, **_fuse_defaults())
            ws = mock_fuse.call_args.kwargs["fs"].ws
            prefixes = [m.prefix for m in ws._registry._mounts]
            assert "/" in prefixes

def test_mount_fuse_backend_with_extra_spec():
    """--backend combined with positional prefix:uri spec and --fuse."""
    from mirage.cli.commands.workspace import mount
    with tempfile.TemporaryDirectory() as mp:
        with patch("mirage.cli.commands.workspace.fuse_mount_fn") as mock_fuse, \
             patch("mirage.cli.commands.workspace._ensure_server"), \
             patch("mirage.cli.commands.workspace._client"):
            mount(["memory:/data/"],
                  backend="memory",
                  mountpoint=mp,
                  **_fuse_defaults())
            ws = mock_fuse.call_args.kwargs["fs"].ws
            prefixes = [m.prefix for m in ws._registry._mounts]
            assert "/" in prefixes
            assert any(p.rstrip("/") == "/data" for p in prefixes)

def test_mount_fuse_no_backend_no_spec_raises():
    """mount --fuse with no --backend and no specs exits with error."""
    result = _run(["mount", "--fuse", "--mountpoint", "/tmp/x"], check=False)
    assert result.returncode != 0

@pytest.mark.skip(reason="requires live server")
def test_mount_exec_kill():
    tmpdir = tempfile.mkdtemp(dir="/tmp")
    env = {"MIRAGE_HOME": tmpdir}

    result = _run(["mount", "memory:/data/"], env=env)
    ws_id = result.stdout.strip()
    assert len(ws_id) == 8

    time.sleep(0.3)

    result = _run(["exec", ws_id, "ls", "/data"], env=env)
    assert result.returncode == 0

    result = _run(["workspace", "ls"], env=env)
    assert ws_id in result.stdout

    _run(["kill", ws_id], env=env)
    time.sleep(0.3)

    result = _run(["workspace", "ls"], env=env, check=False)
    assert ws_id not in result.stdout


The patch target needs to be `mirage.fuse.fs.mount` since `fuse_mount_fn` is a local variable inside the function. Let me fix this.


The file /Users/agent/strukto/mirage/tests/cli/test_app.py has been updated. All occurrences of 'patch("mirage.cli.commands.workspace.fuse_mount_fn")' were successfully replaced with 'patch("mirage.fuse.fs.mount")'.

import os
import subprocess
import sys
import tempfile
import time
from unittest.mock import patch

import pytest

def _run(args: list[str],
         env: dict | None = None,
         check: bool = True) -> subprocess.CompletedProcess:
    e = os.environ.copy()
    if env:
        e.update(env)
    return subprocess.run(
        [sys.executable, "-m", "mirage.cli.app"] + args,
        capture_output=True,
        text=True,
        env=e,
        check=check,
    )

def test_help():
    result = _run(["--help"], check=False)
    assert result.returncode == 0

def _fuse_defaults() -> dict:
    from mirage.types import ConsistencyPolicy, MountMode, RuntimeType
    return dict(
        sync_policy="staged",
        mode=MountMode.READ,
        consistency=ConsistencyPolicy.NONE,
        ttl=300,
        config=None,
        runtime=RuntimeType.MEMORY,
        fuse=True,
        agent_id=None,
    )

def test_mount_fuse_backend_mounts_at_root():
    """--backend memory with --fuse mounts the backend at / prefix."""
    from mirage.cli.commands.workspace import mount
    with tempfile.TemporaryDirectory() as mp:
        with patch("mirage.cli.commands.workspace.fuse_mount_fn") as mock_fuse, \
             patch("mirage.cli.commands.workspace._ensure_server"), \
             patch("mirage.cli.commands.workspace._client"):
            mount(backend="memory", mountpoint=mp, **_fuse_defaults())
            ws = mock_fuse.call_args.kwargs["fs"].ws
            prefixes = [m.prefix for m in ws._registry._mounts]
            assert "/" in prefixes

def test_mount_fuse_backend_with_extra_spec():
    """--backend combined with positional prefix:uri spec and --fuse."""
    from mirage.cli.commands.workspace import mount
    with tempfile.TemporaryDirectory() as mp:
        with patch("mirage.cli.commands.workspace.fuse_mount_fn") as mock_fuse, \
             patch("mirage.cli.commands.workspace._ensure_server"), \
             patch("mirage.cli.commands.workspace._client"):
            mount(["memory:/data/"],
                  backend="memory",
                  mountpoint=mp,
                  **_fuse_defaults())
            ws = mock_fuse.call_args.kwargs["fs"].ws
            prefixes = [m.prefix for m in ws._registry._mounts]
            assert "/" in prefixes
            assert any(p.rstrip("/") == "/data" for p in prefixes)

def test_mount_fuse_no_backend_no_spec_raises():
    """mount --fuse with no --backend and no specs exits with error."""
    result = _run(["mount", "--fuse", "--mountpoint", "/tmp/x"], check=False)
    assert result.returncode != 0

@pytest.mark.skip(reason="requires live server")
def test_mount_exec_kill():
    tmpdir = tempfile.mkdtemp(dir="/tmp")
    env = {"MIRAGE_HOME": tmpdir}

    result = _run(["mount", "memory:/data/"], env=env)
    ws_id = result.stdout.strip()
    assert len(ws_id) == 8

    time.sleep(0.3)

    result = _run(["exec", ws_id, "ls", "/data"], env=env)
    assert result.returncode == 0

    result = _run(["workspace", "ls"], env=env)
    assert ws_id in result.stdout

    _run(["kill", ws_id], env=env)
    time.sleep(0.3)

    result = _run(["workspace", "ls"], env=env, check=False)
    assert ws_id not in result.stdout

-        with patch("mirage.cli.commands.workspace.fuse_mount_fn") as mock_fuse, \

+        with patch("mirage.fuse.fs.mount") as mock_fuse, \


uv run pytest tests/cli/test_app.py -v --tb=short 2>&1 | grep -E "PASSED|FAILED|ERROR"

Rerun test_app.py tests


uv run pytest tests/cli/test_app.py -v 2>&1 | tail -20

Rerun test_app.py with full output


mirage/server/fastapi/routers/__init__.py                   0      0   100%
mirage/server/fastapi/routers/command_router.py            65     65     0%   1-101
mirage/server/fastapi/routers/io_router.py                110    110     0%   1-139
mirage/server/fastapi/routers/workspace_router.py          60     60     0%   1-95
mirage/server/routes.py                                    27      0   100%
mirage/server/service/__init__.py                           0      0   100%
mirage/server/service/base.py                               7      7     0%   1-64
mirage/server/service/cache.py                             24     24     0%   1-33
mirage/server/service/local.py                            260    260     0%   1-428
mirage/types.py                                            91      0   100%
mirage/workspace/__init__.py                              241    183    24%   18-19, 22, 25, 70, 72-73, 88, 91-92, 97-141, 144-177, 180-181, 186-196, 199-203, 206-207, 210-211, 214-220, 223-224, 229-237, 240-242, 245-252, 255-275, 278-281, 286-298, 301, 304-305, 310, 313-314, 317-319, 322-323, 329-333, 336-339, 342-344, 352-367
mirage/workspace/cache/__init__.py                          3      0   100%
mirage/workspace/cache/entry.py                             7      0   100%
mirage/workspace/cache/policy.py                           26     20    23%   15-26, 30-37
mirage/workspace/cache/store.py                            45     22    51%   9, 14, 25-28, 36-47, 50-52, 55-56, 60, 64, 67-69
mirage/workspace/mount.py                                  34     13    62%   39, 50-56, 59-63, 66
mirage/workspace/sync.py                                   56     40    29%   25-33, 43-50, 60-63, 72-93, 102, 105, 108, 111, 115
-------------------------------------------------------------------------------------
TOTAL                                                    4704   3872    18%
========================= 4 passed, 1 skipped in 0.51s =========================


All passing. Now run the full suite.


Run full test suite


mirage/workspace/mount.py                                  34      1    97%   63
mirage/workspace/sync.py                                   56      0   100%
-------------------------------------------------------------------------------------
TOTAL                                                    4704   1193    75%
641 passed, 1 skipped, 2 warnings in 2.77s


All 641 tests pass (more tests now because `fuse/tests/test_fs.py` was merged into the main test suite).


TodoWrite

Create directory structure and git mv all files

Creating directory structure and moving files

Write sub-package __init__.py files

Update internal fuse imports

Update external imports (fuse_ops → fuse, mirage_fuse → mirage.fuse)

Update pyproject.toml (add fusepy optional dep)

Clean up old directories

Run tests


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable

Create directory structure and git mv all files

Write sub-package __init__.py files

Update internal fuse imports

Update external imports (fuse_ops → fuse, mirage_fuse → mirage.fuse)

Update pyproject.toml (add fusepy optional dep)

Clean up old directories

Run tests

Write sub-package __init__.py files

Update internal fuse imports

Clean up old directories

Run tests


Now let me address the line length warnings you mentioned.


    45→def test_mount_fuse_backend_mounts_at_root():
    46→    """--backend memory with --fuse mounts the backend at / prefix."""
    47→    from mirage.cli.commands.workspace import mount
    48→    with tempfile.TemporaryDirectory() as mp:
    49→        with patch("mirage.fuse.fs.mount") as mock_fuse, \
    50→             patch("mirage.cli.commands.workspace._ensure_server"), \
    51→             patch("mirage.cli.commands.workspace._client"):
    52→            mount(backend="memory", mountpoint=mp, **_fuse_defaults())
    53→            ws = mock_fuse.call_args.kwargs["fs"].ws
    54→            prefixes = [m.prefix for m in ws._registry._mounts]
    55→            assert "/" in prefixes
    56→
    57→
    58→def test_mount_fuse_backend_with_extra_spec():
    59→    """--backend combined with positional prefix:uri spec and --fuse."""
    60→    from mirage.cli.commands.workspace import mount
    61→    with tempfile.TemporaryDirectory() as mp:
    62→        with patch("mirage.fuse.fs.mount") as mock_fuse, \
    63→             patch("mirage.cli.commands.workspace._ensure_server"), \
    64→             patch("mirage.cli.commands.workspace._client"):
    65→            mount(["memory:/data/"],
    66→                  backend="memory",
    67→                  mountpoint=mp,
    68→                  **_fuse_defaults())
    69→            ws = mock_fuse.call_args.kwargs["fs"].ws
    70→            prefixes = [m.prefix for m in ws._registry._mounts]
    71→            assert "/" in prefixes
    72→            assert any(p.rstrip("/") == "/data" for p in prefixes)
    73→
    74→

def test_mount_fuse_backend_mounts_at_root():
    """--backend memory with --fuse mounts the backend at / prefix."""
    from mirage.cli.commands.workspace import mount
    with tempfile.TemporaryDirectory() as mp:
        with patch("mirage.fuse.fs.mount") as mock_fuse, \
             patch("mirage.cli.commands.workspace._ensure_server"), \
             patch("mirage.cli.commands.workspace._client"):
            mount(backend="memory", mountpoint=mp, **_fuse_defaults())
            ws = mock_fuse.call_args.kwargs["fs"].ws
            prefixes = [m.prefix for m in ws._registry._mounts]
            assert "/" in prefixes

def test_mount_fuse_backend_with_extra_spec():
    """--backend combined with positional prefix:uri spec and --fuse."""
    from mirage.cli.commands.workspace import mount
    with tempfile.TemporaryDirectory() as mp:
        with patch("mirage.fuse.fs.mount") as mock_fuse, \
             patch("mirage.cli.commands.workspace._ensure_server"), \
             patch("mirage.cli.commands.workspace._client"):
            mount(["memory:/data/"],
                  backend="memory",
                  mountpoint=mp,
                  **_fuse_defaults())
            ws = mock_fuse.call_args.kwargs["fs"].ws
            prefixes = [m.prefix for m in ws._registry._mounts]
            assert "/" in prefixes
            assert any(p.rstrip("/") == "/data" for p in prefixes)


The line length warnings are on lines 49 and 62. Let me check - `patch("mirage.fuse.fs.mount") as mock_fuse, \` is shorter than before. Let me count:
Line 49: `        with patch("mirage.fuse.fs.mount") as mock_fuse, \` - that's 59 chars. But looking at the original error message: "tests/cli/test_app.py:49:80: E501 line too long (81 > 79 characters)" - this seems to be from before my edit? Let me check the current line lengths.

Actually, the user's error message might have been from when the subagent used the longer `"mirage.cli.commands.workspace.fuse_mount_fn"` patch path. Let me verify if the current lines are actually within the limit now.


The line length issue was from the subagent's earlier version with the longer patch path. The current lines with `"mirage.fuse.fs.mount"` are well within 79 chars. All done!

**Summary of the fuse refactoring:**

```
mirage/fuse/                     (was: mirage/fuse_ops/ + fuse/mirage_fuse/)
├── __init__.py                  FUSE_OPS dict + re-exports
├── base.py                      FuseOp base class
├── fs.py                        MirageFS, mount, mount_background (was separate package)
├── registry.py                  FuseOpRegistry
├── loader.py                    load_fuse_op_class, build_registry_from_config
├── README.md                    (moved from fuse/)
├── backend/                     ← read/write strategy per storage type
│   ├── default.py
│   ├── local.py
│   ├── memory.py
│   └── s3.py
├── filetype/                    ← virtual rendering per FileType
│   ├── csv.py                   (was csv_op.py)
│   └── parquet.py
└── remote/
    └── remote.py
```

Changes:
- Merged the separate `fuse/` package (mirage-fuse) into `mirage/fuse/`
- `fusepy` is now an optional dependency (`[fuse]`) instead of a separate package
- Grouped ops into `backend/` and `filetype/` subfolders
- Moved `fuse/tests/` into the main `tests/fuse/` suite (641 tests passing, up from 609)


Update all remaining imports across the codebase for the fuse_ops → fuse refactoring. There are two patterns to fix:

**Pattern 1: `mirage.fuse_ops` → `mirage.fuse`**
Files (outside of mirage/fuse/ which is already done):
- tests/fuse/test_remote.py: `from mirage.fuse_ops.remote import RemoteFuseOp` → `from mirage.fuse.remote.remote import RemoteFuseOp`
- tests/fuse/test_default.py: `from mirage.fuse_ops.default import DefaultFuseOp` → `from mirage.fuse.backend.default import DefaultFuseOp` AND `from mirage.fuse_ops.memory import MemoryFuseOp` → `from mirage.fuse.backend.memory import MemoryFuseOp`
- tests/fuse/test_loader.py: `from mirage.fuse_ops.loader import ...` → `from mirage.fuse.loader import ...` AND string literal `"from mirage.fuse_ops import FuseOp\n"` → `"from mirage.fuse import FuseOp\n"`
- tests/fuse/test_format_ops.py: `from mirage.fuse_ops.csv_op import CsvFuseOp` → `from mirage.fuse.filetype.csv import CsvFuseOp` AND `from mirage.fuse_ops.parquet import ParquetFuseOp` → `from mirage.fuse.filetype.parquet import ParquetFuseOp`
- tests/fuse/test_registry.py: `from mirage.fuse_ops import FUSE_OPS, FuseOp` → `from mirage.fuse import FUSE_OPS, FuseOp` AND `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry`
- tests/fuse/test_fs.py: `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry` (appears multiple times)
- tests/runtime/test_accessors.py: `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry`
- mirage/runtime/base.py: `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry`
- mirage/runtime/accessors/local.py: `from mirage.fuse_ops import FUSE_OPS, FuseOp` → `from mirage.fuse import FUSE_OPS, FuseOp` AND `from mirage.fuse_ops.loader import load_fuse_op_class` → `from mirage.fuse.loader import load_fuse_op_class` AND `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry`
- mirage/server/service/local.py: `from mirage.fuse_ops import FUSE_OPS` → `from mirage.fuse import FUSE_OPS` AND `from mirage.fuse_ops.loader import ...` → `from mirage.fuse.loader import ...` AND `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry`
- mirage/cli/commands/workspace.py: `from mirage.fuse_ops.loader import build_registry_from_config` → `from mirage.fuse.loader import build_registry_from_config` AND `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry` AND `from mirage.fuse_ops.remote import RemoteFuseOp` → `from mirage.fuse.remote.remote import RemoteFuseOp`

**Pattern 2: `mirage_fuse` → `mirage.fuse`**
These imports reference the old separate package `mirage_fuse`:
- mirage/cli/commands/workspace.py: `from mirage_fuse import MirageFS` → `from mirage.fuse.fs import MirageFS` AND `from mirage_fuse import mount as fuse_mount_fn` → `from mirage.fuse.fs import mount as fuse_mount_fn`
- examples/claude-memory.py: `from mirage_fuse import mount_background` → `from mirage.fuse.fs import mount_background`
- examples/claude-sdk.py: `from mirage_fuse import MirageFS, mount_background` → `from mirage.fuse.fs import MirageFS, mount_background`
- tests/fuse/test_fs.py: `from mirage_fuse.fs import _MIRAGE_DIR, _MIRAGE_WHOAMI, MirageFS` → `from mirage.fuse.fs import _MIRAGE_DIR, _MIRAGE_WHOAMI, MirageFS` AND `from mirage_fuse import mount_background` → `from mirage.fuse.fs import mount_background` (2 occurrences)
- fuse/README.md: `from mirage_fuse import MirageFS, mount` → `from mirage.fuse.fs import MirageFS, mount` (this file is now at mirage/fuse/README.md)

For each file: read it first, then use Edit tool to make the changes. Use replace_all where the same pattern appears multiple times.

Prompt:
Update all remaining imports across the codebase for the fuse_ops → fuse refactoring. There are two patterns to fix:

**Pattern 1: `mirage.fuse_ops` → `mirage.fuse`**
Files (outside of mirage/fuse/ which is already done):
- tests/fuse/test_remote.py: `from mirage.fuse_ops.remote import RemoteFuseOp` → `from mirage.fuse.remote.remote import RemoteFuseOp`
- tests/fuse/test_default.py: `from mirage.fuse_ops.default import DefaultFuseOp` → `from mirage.fuse.backend.default import DefaultFuseOp` AND `from mirage.fuse_ops.memory import MemoryFuseOp` → `from mirage.fuse.backend.memory import MemoryFuseOp`
- tests/fuse/test_loader.py: `from mirage.fuse_ops.loader import ...` → `from mirage.fuse.loader import ...` AND string literal `"from mirage.fuse_ops import FuseOp\n"` → `"from mirage.fuse import FuseOp\n"`
- tests/fuse/test_format_ops.py: `from mirage.fuse_ops.csv_op import CsvFuseOp` → `from mirage.fuse.filetype.csv import CsvFuseOp` AND `from mirage.fuse_ops.parquet import ParquetFuseOp` → `from mirage.fuse.filetype.parquet import ParquetFuseOp`
- tests/fuse/test_registry.py: `from mirage.fuse_ops import FUSE_OPS, FuseOp` → `from mirage.fuse import FUSE_OPS, FuseOp` AND `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry`
- tests/fuse/test_fs.py: `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry` (appears multiple times)
- tests/runtime/test_accessors.py: `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry`
- mirage/runtime/base.py: `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry`
- mirage/runtime/accessors/local.py: `from mirage.fuse_ops import FUSE_OPS, FuseOp` → `from mirage.fuse import FUSE_OPS, FuseOp` AND `from mirage.fuse_ops.loader import load_fuse_op_class` → `from mirage.fuse.loader import load_fuse_op_class` AND `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry`
- mirage/server/service/local.py: `from mirage.fuse_ops import FUSE_OPS` → `from mirage.fuse import FUSE_OPS` AND `from mirage.fuse_ops.loader import ...` → `from mirage.fuse.loader import ...` AND `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry`
- mirage/cli/commands/workspace.py: `from mirage.fuse_ops.loader import build_registry_from_config` → `from mirage.fuse.loader import build_registry_from_config` AND `from mirage.fuse_ops.registry import FuseOpRegistry` → `from mirage.fuse.registry import FuseOpRegistry` AND `from mirage.fuse_ops.remote import RemoteFuseOp` → `from mirage.fuse.remote.remote import RemoteFuseOp`

**Pattern 2: `mirage_fuse` → `mirage.fuse`**
These imports reference the old separate package `mirage_fuse`:
- mirage/cli/commands/workspace.py: `from mirage_fuse import MirageFS` → `from mirage.fuse.fs import MirageFS` AND `from mirage_fuse import mount as fuse_mount_fn` → `from mirage.fuse.fs import mount as fuse_mount_fn`
- examples/claude-memory.py: `from mirage_fuse import mount_background` → `from mirage.fuse.fs import mount_background`
- examples/claude-sdk.py: `from mirage_fuse import MirageFS, mount_background` → `from mirage.fuse.fs import MirageFS, mount_background`
- tests/fuse/test_fs.py: `from mirage_fuse.fs import _MIRAGE_DIR, _MIRAGE_WHOAMI, MirageFS` → `from mirage.fuse.fs import _MIRAGE_DIR, _MIRAGE_WHOAMI, MirageFS` AND `from mirage_fuse import mount_background` → `from mirage.fuse.fs import mount_background` (2 occurrences)
- fuse/README.md: `from mirage_fuse import MirageFS, mount` → `from mirage.fuse.fs import MirageFS, mount` (this file is now at mirage/fuse/README.md)

For each file: read it first, then use Edit tool to make the changes. Use replace_all where the same pattern appears multiple times.


     1→from unittest.mock import MagicMock
     2→
     3→from mirage.backend.local.memory import MemoryBackend
     4→from mirage.fuse_ops.remote import RemoteFuseOp
     5→from mirage.types import MountMode, SyncPolicy
     6→from mirage.workspace import Workspace
     7→
     8→
     9→def test_remote_on_open_sends_file():
    10→    mock_client = MagicMock()
    11→    mock_client.on_open.return_value = {"eager": True, "buffer": b"processed"}
    12→
    13→    ws = Workspace({"/data/": MemoryBackend()},
    14→                   mode=MountMode.WRITE,
    15→                   sync_policy=SyncPolicy.IMMEDIATE)
    16→    ws.write("/data/test.txt", b"hello world")
    17→
    18→    op = RemoteFuseOp(client=mock_client)
    19→    ctx = op.on_open(ws, "/data/test.txt", 11)
    20→    assert ctx.get("eager") is True
    21→
    22→    call_kwargs = mock_client.on_open.call_args
    23→    assert call_kwargs.kwargs.get("data") == b"hello world"
    24→    assert call_kwargs.kwargs.get("checksum") is not None
    25→
    26→
    27→def test_remote_read_delegates():
    28→    mock_client = MagicMock()
    29→    mock_client.read.return_value = b"partial"
    30→
    31→    ws = Workspace({"/data/": MemoryBackend()})
    32→    op = RemoteFuseOp(client=mock_client)
    33→    result = op.read(ws, "/data/test.txt", 0, 7, {"eager": False})
    34→    assert result == b"partial"
    35→    mock_client.read.assert_called_once()
    36→
    37→
    38→def test_remote_on_flush_writes_back():
    39→    mock_client = MagicMock()
    40→    mock_client.on_flush.return_value = b"final output"
    41→
    42→    ws = Workspace({"/data/": MemoryBackend()},
    43→                   mode=MountMode.WRITE,
    44→                   sync_policy=SyncPolicy.IMMEDIATE)
    45→    ws.write("/data/test.txt", b"initial")
    46→
    47→    op = RemoteFuseOp(client=mock_client)
    48→    ctx = {"writes": [(0, b"some data")]}
    49→    op.on_flush(ws, "/data/test.txt", ctx)
    50→    mock_client.on_flush.assert_called_once()
    51→    result = ws.cat("/data/test.txt")
    52→    assert result == b"final output"
    53→
    54→
    55→def test_remote_write_delegates():
    56→    mock_client = MagicMock()
    57→    mock_client.write.return_value = 5
    58→
    59→    ws = Workspace({"/data/": MemoryBackend()})
    60→    op = RemoteFuseOp(client=mock_client)
    61→    result = op.write(ws, "/data/test.txt", 0, b"hello", {})
    62→    assert result == 5
    63→    mock_client.write.assert_called_once()
    64→
    65→
    66→def test_remote_on_open_empty_file():
    67→    mock_client = MagicMock()
    68→    mock_client.on_open.return_value = {"eager": True}
    69→
    70→    ws = Workspace({"/data/": MemoryBackend()})
    71→    op = RemoteFuseOp(client=mock_client)
    72→    ctx = op.on_open(ws, "/data/empty.txt", 0)
    73→    assert ctx.get("eager") is True
    74→    call_kwargs = mock_client.on_open.call_args
    75→    assert call_kwargs.kwargs.get("data") == b""
    76→


     1→from mirage.backend.local.memory import MemoryBackend
     2→from mirage.fuse_ops.default import DefaultFuseOp
     3→from mirage.fuse_ops.memory import MemoryFuseOp
     4→from mirage.types import MountMode
     5→from mirage.workspace import Workspace
     6→
     7→
     8→def test_default_eager_small_file():
     9→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    10→    ws.write("/f.txt", b"hello world")
    11→    op = DefaultFuseOp()
    12→    ctx = op.on_open(ws, "/f.txt", 11)
    13→    assert ctx["eager"] is True
    14→    data = op.read(ws, "/f.txt", 0, 5, ctx)
    15→    assert data == b"hello"
    16→
    17→
    18→def test_default_lazy_large_file():
    19→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    20→    big = b"x" * (2 * 1024 * 1024)
    21→    ws.write("/big.bin", big)
    22→    op = DefaultFuseOp()
    23→    ctx = op.on_open(ws, "/big.bin", len(big))
    24→    assert ctx["eager"] is False
    25→    chunk = op.read(ws, "/big.bin", 0, 100, ctx)
    26→    assert chunk == b"x" * 100
    27→
    28→
    29→def test_memory_always_eager():
    30→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    31→    big = b"y" * (2 * 1024 * 1024)
    32→    ws.write("/big.bin", big)
    33→    op = MemoryFuseOp()
    34→    ctx = op.on_open(ws, "/big.bin", len(big))
    35→    assert ctx["eager"] is True
    36→
    37→
    38→def test_buffered_write_flush():
    39→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    40→    ws.write("/f.txt", b"hello world")
    41→    op = DefaultFuseOp()
    42→    ctx = op.on_open(ws, "/f.txt", 11)
    43→    op.write(ws, "/f.txt", 0, b"HELLO", ctx)
    44→    op.write(ws, "/f.txt", 6, b"WORLD", ctx)
    45→    op.on_flush(ws, "/f.txt", ctx)
    46→    assert ws.cat("/f.txt") == b"HELLO WORLD"
    47→
    48→
    49→def test_buffered_write_new_file():
    50→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    51→    op = DefaultFuseOp()
    52→    ctx = op.on_open(ws, "/new.txt", 0)
    53→    op.write(ws, "/new.txt", 0, b"brand new", ctx)
    54→    op.on_flush(ws, "/new.txt", ctx)
    55→    assert ws.cat("/new.txt") == b"brand new"
    56→
    57→
    58→def test_flush_noop_without_writes():
    59→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    60→    ws.write("/f.txt", b"original")
    61→    op = DefaultFuseOp()
    62→    ctx = op.on_open(ws, "/f.txt", 8)
    63→    op.on_flush(ws, "/f.txt", ctx)
    64→    assert ws.cat("/f.txt") == b"original"
    65→


     1→import os
     2→import tempfile
     3→
     4→import pytest
     5→
     6→from mirage.fuse_ops.loader import (build_registry_from_config,
     7→                                    load_fuse_op_class)
     8→
     9→
    10→def test_load_builtin_module():
    11→    cls = load_fuse_op_class("mirage.fuse_ops.default:DefaultFuseOp")
    12→    op = cls()
    13→    assert op.name == "default"
    14→
    15→
    16→def test_load_from_file():
    17→    code = ("from mirage.fuse_ops import FuseOp\n"
    18→            "class CustomOp(FuseOp):\n"
    19→            "    name = 'custom_test'\n")
    20→    with tempfile.NamedTemporaryFile(suffix=".py", mode="w",
    21→                                     delete=False) as f:
    22→        f.write(code)
    23→        f.flush()
    24→        cls = load_fuse_op_class(f"{f.name}:CustomOp")
    25→    op = cls()
    26→    assert op.name == "custom_test"
    27→    os.unlink(f.name)
    28→
    29→
    30→def test_invalid_spec():
    31→    with pytest.raises(ValueError, match="invalid"):
    32→        load_fuse_op_class("no_colon_here")
    33→
    34→
    35→def test_build_registry_builtin():
    36→    registry = build_registry_from_config({"*.parquet": "default"})
    37→    op = registry.resolve("/data/file.parquet", 100)
    38→    assert op.name == "default"
    39→
    40→
    41→def test_build_registry_custom_class():
    42→    registry = build_registry_from_config({
    43→        "*.txt":
    44→        "mirage.fuse_ops.memory:MemoryFuseOp",
    45→    })
    46→    op = registry.resolve("/data/file.txt", 100)
    47→    assert op.name == "memory"
    48→
    49→
    50→def test_build_registry_dict_form():
    51→    registry = build_registry_from_config({
    52→        "*.bin": {
    53→            "handler": "s3"
    54→        },
    55→    })
    56→    op = registry.resolve("/data/file.bin", 100)
    57→    assert op.name == "s3"
    58→
    59→
    60→def test_build_registry_empty():
    61→    registry = build_registry_from_config({})
    62→    op = registry.resolve("/data/file.txt", 100)
    63→    assert op.name == "default"
    64→


     1→import io
     2→
     3→import pyarrow as pa
     4→import pyarrow.parquet as pq
     5→
     6→from mirage.backend.local.memory import MemoryBackend
     7→from mirage.fuse_ops.csv_op import CsvFuseOp
     8→from mirage.fuse_ops.parquet import ParquetFuseOp
     9→from mirage.types import MountMode
    10→from mirage.workspace import Workspace
    11→
    12→
    13→def _make_parquet_bytes() -> bytes:
    14→    table = pa.table({
    15→        "name": ["alice", "bob", "charlie"],
    16→        "score": [95, 87, 72],
    17→    })
    18→    buf = io.BytesIO()
    19→    pq.write_table(table, buf)
    20→    return buf.getvalue()
    21→
    22→
    23→def test_parquet_read_returns_text():
    24→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    25→    ws.write("/data.parquet", _make_parquet_bytes())
    26→    op = ParquetFuseOp()
    27→    ctx = op.on_open(ws, "/data.parquet", 1000)
    28→    data = op.read(ws, "/data.parquet", 0, 4096, ctx)
    29→    text = data.decode()
    30→    assert "name" in text
    31→    assert "score" in text
    32→    assert "alice" in text
    33→
    34→
    35→def test_parquet_schema_in_output():
    36→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    37→    ws.write("/data.parquet", _make_parquet_bytes())
    38→    op = ParquetFuseOp()
    39→    ctx = op.on_open(ws, "/data.parquet", 1000)
    40→    data = op.read(ws, "/data.parquet", 0, 8192, ctx)
    41→    text = data.decode()
    42→    assert "int" in text.lower()
    43→
    44→
    45→def test_parquet_row_count():
    46→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    47→    ws.write("/data.parquet", _make_parquet_bytes())
    48→    op = ParquetFuseOp()
    49→    ctx = op.on_open(ws, "/data.parquet", 1000)
    50→    data = op.read(ws, "/data.parquet", 0, 8192, ctx)
    51→    text = data.decode()
    52→    assert "Rows: 3" in text
    53→
    54→
    55→def test_parquet_error_handling():
    56→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    57→    ws.write("/bad.parquet", b"not a parquet file")
    58→    op = ParquetFuseOp()
    59→    ctx = op.on_open(ws, "/bad.parquet", 18)
    60→    data = op.read(ws, "/bad.parquet", 0, 4096, ctx)
    61→    text = data.decode()
    62→    assert "Error" in text
    63→
    64→
    65→def test_csv_small_passthrough():
    66→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    67→    small = b"name,score\nalice,95\nbob,87\n"
    68→    ws.write("/small.csv", small)
    69→    op = CsvFuseOp()
    70→    ctx = op.on_open(ws, "/small.csv", len(small))
    71→    data = op.read(ws, "/small.csv", 0, 4096, ctx)
    72→    assert data == small
    73→
    74→
    75→def test_csv_large_preview():
    76→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    77→    header = "name,score\n"
    78→    rows = "".join(f"user{i},{i}\n" for i in range(5000))
    79→    big = (header + rows).encode()
    80→    ws.write("/big.csv", big)
    81→    op = CsvFuseOp()
    82→    ctx = op.on_open(ws, "/big.csv", len(big))
    83→    data = op.read(ws, "/big.csv", 0, 8192, ctx)
    84→    text = data.decode()
    85→    assert "name" in text
    86→    assert "score" in text
    87→    assert "5001 rows" in text
    88→    assert "2 columns" in text
    89→
    90→
    91→def test_csv_large_has_preview_marker():
    92→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    93→    header = "a,b,c\n"
    94→    rows = "".join(f"{i},{i+1},{i+2}\n" for i in range(5000))
    95→    big = (header + rows).encode()
    96→    ws.write("/big.csv", big)
    97→    op = CsvFuseOp()
    98→    ctx = op.on_open(ws, "/big.csv", len(big))
    99→    data = op.read(ws, "/big.csv", 0, 8192, ctx)
   100→    text = data.decode()
   101→    assert "more rows" in text
   102→


     1→from mirage.fuse_ops import FUSE_OPS, FuseOp
     2→from mirage.fuse_ops.registry import FuseOpRegistry
     3→
     4→
     5→class ParquetOp(FuseOp):
     6→    name = "parquet"
     7→
     8→
     9→def test_fuse_ops_dict_has_default():
    10→    assert "default" in FUSE_OPS
    11→
    12→
    13→def test_fuse_ops_dict_has_all_backends():
    14→    assert "s3" in FUSE_OPS
    15→    assert "memory" in FUSE_OPS
    16→    assert "local" in FUSE_OPS
    17→
    18→
    19→def test_registry_resolve_default():
    20→    reg = FuseOpRegistry()
    21→    op = reg.resolve("/data/file.txt", 100)
    22→    assert op.name == "default"
    23→
    24→
    25→def test_registry_resolve_custom():
    26→    reg = FuseOpRegistry()
    27→    reg.register("*.parquet", ParquetOp())
    28→    op = reg.resolve("/data/file.parquet", 100)
    29→    assert op.name == "parquet"
    30→
    31→
    32→def test_registry_first_match_wins():
    33→    reg = FuseOpRegistry()
    34→    reg.register("*.parquet", ParquetOp())
    35→    reg.register("*", FuseOp())
    36→    op = reg.resolve("/data/file.parquet", 100)
    37→    assert op.name == "parquet"
    38→
    39→
    40→def test_registry_list_ops():
    41→    reg = FuseOpRegistry()
    42→    reg.register("*.csv", FuseOp())
    43→    ops = reg.list_ops()
    44→    assert len(ops) >= 2
    45→    patterns = [o["pattern"] for o in ops]
    46→    assert "*.csv" in patterns
    47→    assert "*" in patterns
    48→
    49→
    50→def test_registry_set_default():
    51→    reg = FuseOpRegistry()
    52→    reg.set_default(ParquetOp())
    53→    op = reg.resolve("/anything.txt", 100)
    54→    assert op.name == "parquet"
    55→
    56→
    57→def test_fuse_ops_dict_has_format_ops():
    58→    assert "parquet" in FUSE_OPS
    59→    assert "csv" in FUSE_OPS
    60→
    61→
    62→def test_registry_backend_aware_memory():
    63→    from mirage.backend.local.memory import MemoryBackend
    64→    from mirage.types import MountMode
    65→    from mirage.workspace import Workspace
    66→
    67→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    68→    reg = FuseOpRegistry()
    69→    op = reg.resolve("/file.txt", 100, ws)
    70→    assert op.name == "memory"
    71→
    72→
    73→def test_registry_backend_aware_local():
    74→    import tempfile
    75→
    76→    from mirage.backend.local.local import LocalBackend
    77→    from mirage.types import MountMode
    78→    from mirage.workspace import Workspace
    79→
    80→    with tempfile.TemporaryDirectory() as d:
    81→        ws = Workspace({"/": LocalBackend(root=d)}, mode=MountMode.READ)
    82→        reg = FuseOpRegistry()
    83→        op = reg.resolve("/file.txt", 100, ws)
    84→        assert op.name == "local"
    85→
    86→
    87→def test_registry_pattern_overrides_backend():
    88→    from mirage.backend.local.memory import MemoryBackend
    89→    from mirage.types import MountMode
    90→    from mirage.workspace import Workspace
    91→
    92→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    93→    reg = FuseOpRegistry()
    94→    reg.register("*.txt", FUSE_OPS["s3"])
    95→    op = reg.resolve("/file.txt", 100, ws)
    96→    assert op.name == "s3"
    97→
    98→
    99→def test_workspace_read_partial():
   100→    from mirage.backend.local.memory import MemoryBackend
   101→    from mirage.types import MountMode
   102→    from mirage.workspace import Workspace
   103→
   104→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
   105→    ws.write("/f.txt", b"hello world")
   106→    chunk = ws.read("/f.txt", 0, 5)
   107→    assert chunk == b"hello"
   108→
   109→
   110→def test_workspace_read_full():
   111→    from mirage.backend.local.memory import MemoryBackend
   112→    from mirage.types import MountMode
   113→    from mirage.workspace import Workspace
   114→
   115→    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
   116→    ws.write("/f.txt", b"hello")
   117→    data = ws.read("/f.txt", 0, 5)
   118→    assert data == b"hello"
   119→