    60→        self._tracker = DirtyTracker()
    61→        self._sync_policy = sync_policy
    62→        self._consistency = consistency
    63→        self._ttl_seconds = ttl_seconds
    64→        self._locked_paths: set[str] = set()
    65→        self._transfer_stats: dict[str, TransferStats] = {}
    66→        self._created_at: float = time.monotonic()
    67→
    68→        for prefix, value in backends.items():
    69→            if isinstance(value, tuple) and len(value) == 3:
    70→                backend, mount_mode, mount_consistency = value
    71→            elif isinstance(value, tuple):
    72→                backend, mount_mode = value
    73→                mount_consistency = None
    74→            else:
    75→                backend = value
    76→                mount_mode = mode
    77→                mount_consistency = None
    78→            self._registry.mount(prefix, backend, mount_mode,
    79→                                 mount_consistency)
    80→
    81→    # ── mount management ────────────────────────────────────────────────────
    82→
    83→    def mount(self,
    84→              prefix: str,
    85→              backend: BaseBackend,
    86→              mode: MountMode = MountMode.READ,
    87→              consistency: ConsistencyPolicy | None = None) -> None:
    88→        self._registry.mount(prefix, backend, mode, consistency)
    89→
    90→    def backend(self, path: str) -> BaseBackend:
    91→        backend, _, _ = self._registry.resolve(path)
    92→        return backend
    93→
    94→    # ── read ops ────────────────────────────────────────────────────────────
    95→
    96→    def cat(self, path: str) -> bytes:
    97→        inode = self._tracker.get(path)
    98→        if inode and inode.dirty:
    99→            entry = self._cache.get(path)

        self._tracker = DirtyTracker()
        self._sync_policy = sync_policy
        self._consistency = consistency
        self._ttl_seconds = ttl_seconds
        self._locked_paths: set[str] = set()
        self._transfer_stats: dict[str, TransferStats] = {}
        self._created_at: float = time.monotonic()

        for prefix, value in backends.items():
            if isinstance(value, tuple) and len(value) == 3:
                backend, mount_mode, mount_consistency = value
            elif isinstance(value, tuple):
                backend, mount_mode = value
                mount_consistency = None
            else:
                backend = value
                mount_mode = mode
                mount_consistency = None
            self._registry.mount(prefix, backend, mount_mode,
                                 mount_consistency)

    # ── mount management ────────────────────────────────────────────────────

    def mount(self,
              prefix: str,
              backend: BaseBackend,
              mode: MountMode = MountMode.READ,
              consistency: ConsistencyPolicy | None = None) -> None:
        self._registry.mount(prefix, backend, mode, consistency)

    def backend(self, path: str) -> BaseBackend:
        backend, _, _ = self._registry.resolve(path)
        return backend

    # ── read ops ────────────────────────────────────────────────────────────

    def cat(self, path: str) -> bytes:
        inode = self._tracker.get(path)
        if inode and inode.dirty:
            entry = self._cache.get(path)


   330→        for m in self._registry.mounts():
   331→            if norm == m.prefix.rstrip("/") or norm.startswith(m.prefix):
   332→                return m.prefix
   333→        return "/"
   334→
   335→    def _effective_consistency(self, path: str) -> ConsistencyPolicy:
   336→        mount = self._registry.mount_for(path)
   337→        if mount.consistency is not None:
   338→            return mount.consistency
   339→        return self._consistency
   340→
   341→    def _assert_writable(self, path: str) -> None:
   342→        _, _, mode = self._registry.resolve(path)
   343→        if mode == MountMode.READ:
   344→            raise PermissionError(f"mount at {path!r} is read-only")
   345→
   346→    def _record_transfer(self,
   347→                         path: str,
   348→                         op: str,
   349→                         nbytes: int,
   350→                         duration_ms: int,
   351→                         cache_hit: bool | None = None) -> None:
   352→        prefix = self._mount_prefix(path)
   353→        if prefix not in self._transfer_stats:
   354→            self._transfer_stats[prefix] = TransferStats()
   355→        stats = self._transfer_stats[prefix]
   356→        if op == "read":
   357→            stats.bytes_read += nbytes
   358→            stats.read_ops += 1
   359→            stats.total_read_ms += duration_ms
   360→            if cache_hit is True:
   361→                stats.cache_hits += 1
   362→            elif cache_hit is False:
   363→                stats.cache_misses += 1
   364→        elif op == "write":
   365→            stats.bytes_written += nbytes
   366→            stats.write_ops += 1
   367→            stats.total_write_ms += duration_ms
   368→

        for m in self._registry.mounts():
            if norm == m.prefix.rstrip("/") or norm.startswith(m.prefix):
                return m.prefix
        return "/"

    def _effective_consistency(self, path: str) -> ConsistencyPolicy:
        mount = self._registry.mount_for(path)
        if mount.consistency is not None:
            return mount.consistency
        return self._consistency

    def _assert_writable(self, path: str) -> None:
        _, _, mode = self._registry.resolve(path)
        if mode == MountMode.READ:
            raise PermissionError(f"mount at {path!r} is read-only")

    def _record_transfer(self,
                         path: str,
                         op: str,
                         nbytes: int,
                         duration_ms: int,
                         cache_hit: bool | None = None) -> None:
        prefix = self._mount_prefix(path)
        if prefix not in self._transfer_stats:
            self._transfer_stats[prefix] = TransferStats()
        stats = self._transfer_stats[prefix]
        if op == "read":
            stats.bytes_read += nbytes
            stats.read_ops += 1
            stats.total_read_ms += duration_ms
            if cache_hit is True:
                stats.cache_hits += 1
            elif cache_hit is False:
                stats.cache_misses += 1
        elif op == "write":
            stats.bytes_written += nbytes
            stats.write_ops += 1
            stats.total_write_ms += duration_ms


Now I have the full picture. Let me write the plan.


Write

# Refactor workspace/__init__.py + ConsistencyPolicy cleanup

## Context

`mirage/workspace/__init__.py` contains the entire `Workspace` class (~340 lines) plus a `_PathLock` helper. Every other package in the codebase (`backend/`, `runtime/`, `fuse/`) uses `__init__.py` only for re-exports. This refactor moves the `Workspace` class to its own file and cleans up the confusing `ConsistencyPolicy.NONE` naming.

## Changes

### 1. Rename `ConsistencyPolicy.NONE` → `ConsistencyPolicy.CACHED`

**File: `mirage/types.py`**
- Rename the enum member `NONE = "none"` → `CACHED = "cached"`

**All references** (~14 occurrences across these files):
- `mirage/workspace/__init__.py` (will become `workspace.py`)
- `mirage/workspace/cache/policy.py`
- `mirage/cli/commands/workspace.py`
- `mirage/cli/commands/command.py` (4 occurrences)
- `mirage/cli/config.py`
- `mirage/server/fastapi/config.py`
- `mirage/server/service/local.py`
- `tests/workspace/test_init.py` (will become `test_workspace.py`)
- `tests/cli/test_app.py`
- `tests/cli/test_config.py`

### 2. Make `Mount.consistency` always explicit (never `None`)

**File: `mirage/workspace/mount.py`**
- `Mount.__init__`: Change `consistency: ConsistencyPolicy | None = None` → `consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED`
- `MountRegistry.mount()`: Same signature change

**File: `mirage/workspace/__init__.py` → `workspace.py`**
- In the `backends` dict parsing loop: when no consistency is provided in the tuple, use `self._consistency` (the workspace default) instead of `None`
- `Workspace.mount()`: Change `consistency: ConsistencyPolicy | None = None` → `consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED`
- Remove `_effective_consistency()` method entirely — just use `mount.consistency` directly
- In `cat()`: replace `self._effective_consistency(path)` with `self._registry.mount_for(path).consistency`

**File: `mirage/server/service/local.py`**
- Update mount consistency parsing to use `ConsistencyPolicy.CACHED` instead of `None`

**File: `mirage/server/fastapi/config.py`**
- `MountConfig.consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (no more `| None`)

**File: `mirage/cli/commands/workspace.py`**
- Update any `None` defaults for consistency to `ConsistencyPolicy.CACHED`

### 3. Move Workspace class to `workspace/workspace.py`

**Create: `mirage/workspace/workspace.py`**
- Move `_PathLock` class and `Workspace` class from `__init__.py`
- All existing imports stay the same

**Modify: `mirage/workspace/__init__.py`**
- Replace with re-exports only:
```python
from mirage.workspace.workspace import Workspace

__all__ = ["Workspace"]
```

### 4. Rename test file

- `git mv tests/workspace/test_init.py tests/workspace/test_workspace.py`
- No content changes needed (imports stay the same since they import from `mirage.workspace`)

## Execution Order

1. Rename `ConsistencyPolicy.NONE` → `CACHED` in `mirage/types.py`
2. Update all `ConsistencyPolicy.NONE` references across codebase
3. Make `Mount.consistency` non-optional in `mount.py`
4. Move `Workspace` + `_PathLock` to `mirage/workspace/workspace.py`
5. Rewrite `mirage/workspace/__init__.py` as re-exports
6. Remove `_effective_consistency` method, update `cat()` to use mount directly
7. Update workspace init parsing to pass explicit consistency instead of `None`
8. Rename `tests/workspace/test_init.py` → `test_workspace.py`
9. Update server/cli files that used `None` for consistency

## Key Files

- `mirage/types.py` — enum rename
- `mirage/workspace/__init__.py` → slim re-exports
- `mirage/workspace/workspace.py` — new home for Workspace class
- `mirage/workspace/mount.py` — Mount.consistency always explicit
- `mirage/workspace/cache/policy.py` — update NONE → CACHED
- `mirage/cli/commands/workspace.py`, `command.py`, `config.py` — update refs
- `mirage/server/fastapi/config.py`, `mirage/server/service/local.py` — update refs
- `tests/workspace/test_workspace.py` — renamed from test_init.py

## Verification

1. `uv run pytest` — all 641 tests pass
2. `pre-commit run --all-files` — lint/format clean


<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>

Error: File has not been read yet. Read it first before writing to it.


     1→# Test Directory Reorganization Plan
     2→
     3→## Context
     4→
     5→The `mirage/` source tree was recently reorganized into subpackages (`backend/local/`, `backend/network/`, `fuse/backend/`, etc.), but the `tests/` directory still has a flat structure that doesn't mirror the source layout. This plan reorganizes tests so each source file `mirage/a/b/c.py` has exactly one test file `tests/a/b/test_c.py`.
     6→
     7→## Summary of Changes
     8→
     9→- **15 pure moves** (git mv to new subdirectory)
    10→- **3 renames** (test file name doesn't match source file name)
    11→- **4 splits** (one test file covers multiple source files)
    12→- **2 merges** (multiple test files cover one source file)
    13→- **15 new `__init__.py`** files in new test subdirectories
    14→- **1 directory removal** (`tests/types/`)
    15→
    16→---
    17→
    18→## Step 1: Create New Directories + `__init__.py`
    19→
    20→Create empty `__init__.py` in each new directory:
    21→
    22→```
    23→tests/backend/local/__init__.py
    24→tests/backend/network/__init__.py
    25→tests/backend/object_storage/__init__.py
    26→tests/commands/builtin/__init__.py
    27→tests/commands/custom/__init__.py
    28→tests/commands/remote/__init__.py
    29→tests/fuse/backend/__init__.py
    30→tests/fuse/backend/local/__init__.py
    31→tests/fuse/filetype/__init__.py
    32→tests/fuse/remote/__init__.py
    33→tests/server/fastapi/__init__.py
    34→tests/server/fastapi/routers/__init__.py
    35→tests/server/service/__init__.py
    36→tests/runtime/accessors/__init__.py
    37→tests/observe/fastapi/__init__.py
    38→```
    39→
    40→## Step 2: Pure Moves (git mv)
    41→
    42→| From | To | Source file |
    43→|---|---|---|
    44→| `tests/backend/test_memory.py` | `tests/backend/local/test_memory.py` | `mirage/backend/local/memory.py` |
    45→| `tests/backend/test_local.py` | `tests/backend/local/test_local.py` | `mirage/backend/local/local.py` |
    46→| `tests/backend/test_proxy.py` | `tests/backend/network/test_proxy.py` | `mirage/backend/network/proxy.py` |
    47→| `tests/commands/test_handlers.py` | `tests/commands/builtin/test_handlers.py` | `mirage/commands/builtin/handlers.py` |
    48→| `tests/commands/test_nav.py` | `tests/commands/builtin/test_nav.py` | `mirage/commands/builtin/nav.py` |
    49→| `tests/commands/test_read.py` | `tests/commands/builtin/test_read.py` | `mirage/commands/builtin/read.py` |
    50→| `tests/commands/test_search.py` | `tests/commands/builtin/test_search.py` | `mirage/commands/builtin/search.py` |
    51→| `tests/commands/test_write.py` | `tests/commands/builtin/test_write.py` | `mirage/commands/builtin/write.py` |
    52→| `tests/commands/test_client.py` | `tests/commands/remote/test_client.py` | `mirage/commands/remote/client.py` |
    53→| `tests/fuse/test_remote.py` | `tests/fuse/remote/test_remote.py` | `mirage/fuse/remote/remote.py` |
    54→| `tests/server/test_app.py` | `tests/server/fastapi/test_app.py` | `mirage/server/fastapi/app.py` |
    55→| `tests/server/test_io_router.py` | `tests/server/fastapi/routers/test_io_router.py` | `mirage/server/fastapi/routers/io_router.py` |
    56→| `tests/server/test_file_cache.py` | `tests/server/service/test_cache.py` | `mirage/server/service/cache.py` |
    57→| `tests/server/conftest.py` | `tests/server/fastapi/conftest.py` | (serves test_app + test_io_router) |
    58→| `tests/server/test_arg_resolver.py` | `tests/commands/custom/test_arg_resolver.py` | `mirage/commands/custom/arg_resolver.py` |
    59→
    60→## Step 3: Renames (git mv, same directory or move + rename)
    61→
    62→| From | To | Reason |
    63→|---|---|---|
    64→| `tests/server/test_service.py` | `tests/server/service/test_local.py` | Source is `mirage/server/service/local.py` |
    65→| `tests/runtime/test_runtime.py` | `tests/runtime/test_base.py` | Source is `mirage/runtime/base.py` |
    66→| `tests/runtime/test_accessors.py` | `tests/runtime/accessors/test_local.py` | Source is `mirage/runtime/accessors/local.py` |
    67→
    68→## Step 4: Splits
    69→
    70→### 4a. `tests/backend/test_configs.py` → 3 files
    71→
    72→- **`tests/backend/object_storage/test_s3.py`**: `test_s3config_defaults`, `test_s3config_immutable`
    73→- **`tests/backend/object_storage/test_r2.py`**: `test_r2config_fields`, `test_r2config_immutable`
    74→- ExternalCommand tests (`test_external_command_fields`, `test_external_command_immutable`) → merge into `tests/commands/custom/test_registry.py` (Step 4d)
    75→- Delete `tests/backend/test_configs.py` after split
    76→
    77→### 4b. `tests/fuse/test_default.py` → 2 files
    78→
    79→- **`tests/fuse/backend/test_default.py`**: `test_default_eager_small_file`, `test_default_lazy_large_file`, `test_buffered_write_flush`, `test_buffered_write_new_file`, `test_flush_noop_without_writes`
    80→- **`tests/fuse/backend/local/test_memory.py`**: `test_memory_always_eager`
    81→- Delete `tests/fuse/test_default.py` after split
    82→
    83→### 4c. `tests/fuse/test_format_ops.py` → 2 files
    84→
    85→- **`tests/fuse/filetype/test_parquet.py`**: `_make_parquet_bytes` helper + `test_parquet_*` (4 tests)
    86→- **`tests/fuse/filetype/test_csv.py`**: `test_csv_*` (3 tests)
    87→- Delete `tests/fuse/test_format_ops.py` after split
    88→
    89→### 4d. `tests/server/test_registry.py` → split + move
    90→
    91→- **`tests/commands/custom/test_registry.py`**: All `CommandRegistry` and `ExternalCommand` tests (the bulk of the file: ~40 tests). Also merge in the 2 ExternalCommand pydantic tests from `test_configs.py` (Step 4a).
    92→- **`tests/server/test_constants.py`**: `test_parse_size_units` only (tests `mirage/server/constants._parse_size`)
    93→- Delete `tests/server/test_registry.py` after split
    94→
    95→Note: The `_CustomMethodBackend` helper class in `test_registry.py` is not a test class — it's a test fixture subclass. Keep it as a module-level helper in the new file.
    96→
    97→## Step 5: Merges
    98→
    99→### 5a. `tests/test_types.py` + `tests/types/test_types.py` → `tests/test_types.py`
   100→
   101→Merge all tests from `tests/types/test_types.py` into `tests/test_types.py`. Combine imports. Delete `tests/types/` directory entirely.
   102→
   103→### 5b. `tests/workspace/test_write_ops.py` + `test_lock.py` → merge into `tests/workspace/test_init.py`
   104→
   105→All three files test `mirage/workspace/__init__.py` (Workspace class). Merge `test_write_ops.py` and `test_lock.py` content into `test_init.py`. Keep both `_make_ws` helpers (rename to avoid collision if signatures differ). Delete the two merged files.
   106→
   107→## Step 6: Cleanup
   108→
   109→- Remove empty `tests/types/` directory
   110→- Remove stale `__init__.py` files from emptied directories (if any directory becomes empty)
   111→- Verify no duplicate test function names across merged files
   112→
   113→## Files That Stay In Place (no changes)
   114→
   115→- `tests/conftest.py`
   116→- `tests/backend/test_loader.py` → `mirage/backend/loader.py` ✓
   117→- `tests/commands/test_init.py` → `mirage/commands/__init__.py` ✓
   118→- `tests/fuse/test_fs.py` → `mirage/fuse/fs.py` ✓
   119→- `tests/fuse/test_loader.py` → `mirage/fuse/loader.py` ✓
   120→- `tests/fuse/test_registry.py` → `mirage/fuse/registry.py` ✓
   121→- `tests/observe/test_session_log.py`, `test_entries.py`, `test_inspector.py` ✓
   122→- `tests/runtime/test_memory.py`, `test_local.py`, `test_remote.py` ✓
   123→- `tests/workspace/test_mount.py`, `test_sync.py` ✓
   124→- `tests/workspace/cache/test_store.py`, `test_policy.py` ✓
   125→- `tests/cli/test_app.py`, `test_client.py`, `test_config.py` ✓
   126→- `tests/integration/test_remote_transfer.py` ✓ (cross-cutting)
   127→
   128→## Execution Order
   129→
   130→1. Create all new directories + `__init__.py` (Step 1)
   131→2. Do all pure `git mv` moves (Step 2)
   132→3. Do renames (Step 3)
   133→4. Write split files + delete originals (Step 4a-d)
   134→5. Write merged files + delete originals (Step 5a-b)
   135→6. Cleanup (Step 6)
   136→7. Verify (Step 7)
   137→
   138→## Verification
   139→
   140→1. `uv run pytest` — all 641 tests should pass
   141→2. `pre-commit run --all-files` — lint/format check
   142→3. Verify 1:1 mapping: every test file `tests/a/b/test_c.py` corresponds to `mirage/a/b/c.py`
   143→4. Verify no test classes remain (only `def test_*` functions)
   144→

# Test Directory Reorganization Plan

## Context

The `mirage/` source tree was recently reorganized into subpackages (`backend/local/`, `backend/network/`, `fuse/backend/`, etc.), but the `tests/` directory still has a flat structure that doesn't mirror the source layout. This plan reorganizes tests so each source file `mirage/a/b/c.py` has exactly one test file `tests/a/b/test_c.py`.

## Summary of Changes

- **15 pure moves** (git mv to new subdirectory)
- **3 renames** (test file name doesn't match source file name)
- **4 splits** (one test file covers multiple source files)
- **2 merges** (multiple test files cover one source file)
- **15 new `__init__.py`** files in new test subdirectories
- **1 directory removal** (`tests/types/`)

---

## Step 1: Create New Directories + `__init__.py`

Create empty `__init__.py` in each new directory:

```
tests/backend/local/__init__.py
tests/backend/network/__init__.py
tests/backend/object_storage/__init__.py
tests/commands/builtin/__init__.py
tests/commands/custom/__init__.py
tests/commands/remote/__init__.py
tests/fuse/backend/__init__.py
tests/fuse/backend/local/__init__.py
tests/fuse/filetype/__init__.py
tests/fuse/remote/__init__.py
tests/server/fastapi/__init__.py
tests/server/fastapi/routers/__init__.py
tests/server/service/__init__.py
tests/runtime/accessors/__init__.py
tests/observe/fastapi/__init__.py
```

## Step 2: Pure Moves (git mv)

| From | To | Source file |
|---|---|---|
| `tests/backend/test_memory.py` | `tests/backend/local/test_memory.py` | `mirage/backend/local/memory.py` |
| `tests/backend/test_local.py` | `tests/backend/local/test_local.py` | `mirage/backend/local/local.py` |
| `tests/backend/test_proxy.py` | `tests/backend/network/test_proxy.py` | `mirage/backend/network/proxy.py` |
| `tests/commands/test_handlers.py` | `tests/commands/builtin/test_handlers.py` | `mirage/commands/builtin/handlers.py` |
| `tests/commands/test_nav.py` | `tests/commands/builtin/test_nav.py` | `mirage/commands/builtin/nav.py` |
| `tests/commands/test_read.py` | `tests/commands/builtin/test_read.py` | `mirage/commands/builtin/read.py` |
| `tests/commands/test_search.py` | `tests/commands/builtin/test_search.py` | `mirage/commands/builtin/search.py` |
| `tests/commands/test_write.py` | `tests/commands/builtin/test_write.py` | `mirage/commands/builtin/write.py` |
| `tests/commands/test_client.py` | `tests/commands/remote/test_client.py` | `mirage/commands/remote/client.py` |
| `tests/fuse/test_remote.py` | `tests/fuse/remote/test_remote.py` | `mirage/fuse/remote/remote.py` |
| `tests/server/test_app.py` | `tests/server/fastapi/test_app.py` | `mirage/server/fastapi/app.py` |
| `tests/server/test_io_router.py` | `tests/server/fastapi/routers/test_io_router.py` | `mirage/server/fastapi/routers/io_router.py` |
| `tests/server/test_file_cache.py` | `tests/server/service/test_cache.py` | `mirage/server/service/cache.py` |
| `tests/server/conftest.py` | `tests/server/fastapi/conftest.py` | (serves test_app + test_io_router) |
| `tests/server/test_arg_resolver.py` | `tests/commands/custom/test_arg_resolver.py` | `mirage/commands/custom/arg_resolver.py` |

## Step 3: Renames (git mv, same directory or move + rename)

| From | To | Reason |
|---|---|---|
| `tests/server/test_service.py` | `tests/server/service/test_local.py` | Source is `mirage/server/service/local.py` |
| `tests/runtime/test_runtime.py` | `tests/runtime/test_base.py` | Source is `mirage/runtime/base.py` |
| `tests/runtime/test_accessors.py` | `tests/runtime/accessors/test_local.py` | Source is `mirage/runtime/accessors/local.py` |

## Step 4: Splits

### 4a. `tests/backend/test_configs.py` → 3 files

- **`tests/backend/object_storage/test_s3.py`**: `test_s3config_defaults`, `test_s3config_immutable`
- **`tests/backend/object_storage/test_r2.py`**: `test_r2config_fields`, `test_r2config_immutable`
- ExternalCommand tests (`test_external_command_fields`, `test_external_command_immutable`) → merge into `tests/commands/custom/test_registry.py` (Step 4d)
- Delete `tests/backend/test_configs.py` after split

### 4b. `tests/fuse/test_default.py` → 2 files

- **`tests/fuse/backend/test_default.py`**: `test_default_eager_small_file`, `test_default_lazy_large_file`, `test_buffered_write_flush`, `test_buffered_write_new_file`, `test_flush_noop_without_writes`
- **`tests/fuse/backend/local/test_memory.py`**: `test_memory_always_eager`
- Delete `tests/fuse/test_default.py` after split

### 4c. `tests/fuse/test_format_ops.py` → 2 files

- **`tests/fuse/filetype/test_parquet.py`**: `_make_parquet_bytes` helper + `test_parquet_*` (4 tests)
- **`tests/fuse/filetype/test_csv.py`**: `test_csv_*` (3 tests)
- Delete `tests/fuse/test_format_ops.py` after split

### 4d. `tests/server/test_registry.py` → split + move

- **`tests/commands/custom/test_registry.py`**: All `CommandRegistry` and `ExternalCommand` tests (the bulk of the file: ~40 tests). Also merge in the 2 ExternalCommand pydantic tests from `test_configs.py` (Step 4a).
- **`tests/server/test_constants.py`**: `test_parse_size_units` only (tests `mirage/server/constants._parse_size`)
- Delete `tests/server/test_registry.py` after split

Note: The `_CustomMethodBackend` helper class in `test_registry.py` is not a test class — it's a test fixture subclass. Keep it as a module-level helper in the new file.

## Step 5: Merges

### 5a. `tests/test_types.py` + `tests/types/test_types.py` → `tests/test_types.py`

Merge all tests from `tests/types/test_types.py` into `tests/test_types.py`. Combine imports. Delete `tests/types/` directory entirely.

### 5b. `tests/workspace/test_write_ops.py` + `test_lock.py` → merge into `tests/workspace/test_init.py`

All three files test `mirage/workspace/__init__.py` (Workspace class). Merge `test_write_ops.py` and `test_lock.py` content into `test_init.py`. Keep both `_make_ws` helpers (rename to avoid collision if signatures differ). Delete the two merged files.

## Step 6: Cleanup

- Remove empty `tests/types/` directory
- Remove stale `__init__.py` files from emptied directories (if any directory becomes empty)
- Verify no duplicate test function names across merged files

## Files That Stay In Place (no changes)

- `tests/conftest.py`
- `tests/backend/test_loader.py` → `mirage/backend/loader.py` ✓
- `tests/commands/test_init.py` → `mirage/commands/__init__.py` ✓
- `tests/fuse/test_fs.py` → `mirage/fuse/fs.py` ✓
- `tests/fuse/test_loader.py` → `mirage/fuse/loader.py` ✓
- `tests/fuse/test_registry.py` → `mirage/fuse/registry.py` ✓
- `tests/observe/test_session_log.py`, `test_entries.py`, `test_inspector.py` ✓
- `tests/runtime/test_memory.py`, `test_local.py`, `test_remote.py` ✓
- `tests/workspace/test_mount.py`, `test_sync.py` ✓
- `tests/workspace/cache/test_store.py`, `test_policy.py` ✓
- `tests/cli/test_app.py`, `test_client.py`, `test_config.py` ✓
- `tests/integration/test_remote_transfer.py` ✓ (cross-cutting)

## Execution Order

1. Create all new directories + `__init__.py` (Step 1)
2. Do all pure `git mv` moves (Step 2)
3. Do renames (Step 3)
4. Write split files + delete originals (Step 4a-d)
5. Write merged files + delete originals (Step 5a-b)
6. Cleanup (Step 6)
7. Verify (Step 7)

## Verification

1. `uv run pytest` — all 641 tests should pass
2. `pre-commit run --all-files` — lint/format check
3. Verify 1:1 mapping: every test file `tests/a/b/test_c.py` corresponds to `mirage/a/b/c.py`
4. Verify no test classes remain (only `def test_*` functions)


# Refactor workspace/__init__.py + ConsistencyPolicy cleanup

## Context

`mirage/workspace/__init__.py` contains the entire `Workspace` class (~340 lines) plus a `_PathLock` helper. Every other package (`backend/`, `runtime/`, `fuse/`) uses `__init__.py` only for re-exports. Additionally, `ConsistencyPolicy.NONE` is confusing alongside Python's `None` used for "inherit default". This refactor:
1. Moves the `Workspace` class to its own file
2. Renames `ConsistencyPolicy.NONE` → `ConsistencyPolicy.CACHED`
3. Makes `Mount.consistency` always explicit (no more `None` = inherit)

---

## Step 1: Rename `ConsistencyPolicy.NONE` → `CACHED`

**`mirage/types.py`**: Change enum member `NONE = "none"` → `CACHED = "cached"`

Update all references (~14 occurrences):
- `mirage/workspace/__init__.py` (line 55)
- `mirage/workspace/cache/policy.py` (line 15)
- `mirage/cli/commands/workspace.py` (line 107)
- `mirage/cli/commands/command.py` (lines 57, 69, 77, 78)
- `mirage/cli/config.py` (line 106)
- `mirage/server/fastapi/config.py` (line 18)
- `tests/workspace/test_init.py` (lines 10, 29, 94)
- `tests/cli/test_app.py` (line 36)
- `tests/cli/test_config.py` (line 133)

## Step 2: Make `Mount.consistency` always explicit

**`mirage/workspace/mount.py`**:
- `Mount.__init__`: `consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `ConsistencyPolicy | None = None`)
- `MountRegistry.mount()`: same signature change

**`mirage/workspace/__init__.py`** (soon to be `workspace.py`):
- In the `backends` dict parsing loop: when a tuple doesn't include consistency, use `self._consistency` (the workspace default) instead of `None`
- `Workspace.mount()`: `consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `| None = None`)
- Remove `_effective_consistency()` method entirely
- In `cat()`: replace `self._effective_consistency(path)` with `self._registry.mount_for(path).consistency`

**`mirage/server/service/local.py`**: Update mount consistency parsing — use `ConsistencyPolicy.CACHED` instead of `None`

**`mirage/server/fastapi/config.py`**: `MountConfig.consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `| None`)

**`mirage/cli/commands/workspace.py`**: Update any `None` consistency defaults

## Step 3: Move Workspace to `workspace/workspace.py`

**Create: `mirage/workspace/workspace.py`**
- Move `_PathLock` class and `Workspace` class from `__init__.py` (all imports stay the same)

**Rewrite: `mirage/workspace/__init__.py`**
```python
from mirage.workspace.workspace import Workspace

__all__ = ["Workspace"]
```

No other files need import changes — they all import `from mirage.workspace import Workspace` which continues to work.

## Step 4: Rename test file

- `git mv tests/workspace/test_init.py tests/workspace/test_workspace.py`
- No content changes (imports stay `from mirage.workspace import Workspace`)

## Key Files Modified

- `mirage/types.py`
- `mirage/workspace/workspace.py` (new)
- `mirage/workspace/__init__.py` (rewritten to re-exports)
- `mirage/workspace/mount.py`
- `mirage/workspace/cache/policy.py`
- `mirage/cli/commands/workspace.py`, `command.py`, `config.py`
- `mirage/server/fastapi/config.py`, `mirage/server/service/local.py`
- `tests/workspace/test_workspace.py` (renamed)
- `tests/cli/test_app.py`, `tests/cli/test_config.py`

## Verification

1. `uv run pytest` — all 641 tests pass
2. `pre-commit run --all-files` — lint/format clean


The file /Users/agent/.claude/plans/shimmering-leaping-wozniak.md has been updated successfully.

# Refactor workspace/__init__.py + ConsistencyPolicy cleanup

## Context

`mirage/workspace/__init__.py` contains the entire `Workspace` class (~340 lines) plus a `_PathLock` helper. Every other package (`backend/`, `runtime/`, `fuse/`) uses `__init__.py` only for re-exports. Additionally, `ConsistencyPolicy.NONE` is confusing alongside Python's `None` used for "inherit default". This refactor:
1. Moves the `Workspace` class to its own file
2. Renames `ConsistencyPolicy.NONE` → `ConsistencyPolicy.CACHED`
3. Makes `Mount.consistency` always explicit (no more `None` = inherit)

---

## Step 1: Rename `ConsistencyPolicy.NONE` → `CACHED`

**`mirage/types.py`**: Change enum member `NONE = "none"` → `CACHED = "cached"`

Update all references (~14 occurrences):
- `mirage/workspace/__init__.py` (line 55)
- `mirage/workspace/cache/policy.py` (line 15)
- `mirage/cli/commands/workspace.py` (line 107)
- `mirage/cli/commands/command.py` (lines 57, 69, 77, 78)
- `mirage/cli/config.py` (line 106)
- `mirage/server/fastapi/config.py` (line 18)
- `tests/workspace/test_init.py` (lines 10, 29, 94)
- `tests/cli/test_app.py` (line 36)
- `tests/cli/test_config.py` (line 133)

## Step 2: Make `Mount.consistency` always explicit

**`mirage/workspace/mount.py`**:
- `Mount.__init__`: `consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `ConsistencyPolicy | None = None`)
- `MountRegistry.mount()`: same signature change

**`mirage/workspace/__init__.py`** (soon to be `workspace.py`):
- In the `backends` dict parsing loop: when a tuple doesn't include consistency, use `self._consistency` (the workspace default) instead of `None`
- `Workspace.mount()`: `consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `| None = None`)
- Remove `_effective_consistency()` method entirely
- In `cat()`: replace `self._effective_consistency(path)` with `self._registry.mount_for(path).consistency`

**`mirage/server/service/local.py`**: Update mount consistency parsing — use `ConsistencyPolicy.CACHED` instead of `None`

**`mirage/server/fastapi/config.py`**: `MountConfig.consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `| None`)

**`mirage/cli/commands/workspace.py`**: Update any `None` consistency defaults

## Step 3: Move Workspace to `workspace/workspace.py`

**Create: `mirage/workspace/workspace.py`**
- Move `_PathLock` class and `Workspace` class from `__init__.py` (all imports stay the same)

**Rewrite: `mirage/workspace/__init__.py`**
```python
from mirage.workspace.workspace import Workspace

__all__ = ["Workspace"]
```

No other files need import changes — they all import `from mirage.workspace import Workspace` which continues to work.

## Step 4: Rename test file

- `git mv tests/workspace/test_init.py tests/workspace/test_workspace.py`
- No content changes (imports stay `from mirage.workspace import Workspace`)

## Key Files Modified

- `mirage/types.py`
- `mirage/workspace/workspace.py` (new)
- `mirage/workspace/__init__.py` (rewritten to re-exports)
- `mirage/workspace/mount.py`
- `mirage/workspace/cache/policy.py`
- `mirage/cli/commands/workspace.py`, `command.py`, `config.py`
- `mirage/server/fastapi/config.py`, `mirage/server/service/local.py`
- `tests/workspace/test_workspace.py` (renamed)
- `tests/cli/test_app.py`, `tests/cli/test_config.py`

## Verification

1. `uv run pytest` — all 641 tests pass
2. `pre-commit run --all-files` — lint/format clean

+# Refactor workspace/__init__.py + ConsistencyPolicy cleanup

-The `mirage/` source tree was recently reorganized into subpackages (`backend/local/`, `backend/network/`, `fuse/backend/`, etc.), but the `tests/` directory still has a flat structure that doesn't mirror the source layout. This plan reorganizes tests so each source file `mirage/a/b/c.py` has exactly one test file `tests/a/b/test_c.py`.

+`mirage/workspace/__init__.py` contains the entire `Workspace` class (~340 lines) plus a `_PathLock` helper. Every other package (`backend/`, `runtime/`, `fuse/`) uses `__init__.py` only for re-exports. Additionally, `ConsistencyPolicy.NONE` is confusing alongside Python's `None` used for "inherit default". This refactor:

+1. Moves the `Workspace` class to its own file

+2. Renames `ConsistencyPolicy.NONE` → `ConsistencyPolicy.CACHED`

+3. Makes `Mount.consistency` always explicit (no more `None` = inherit)

-- **15 pure moves** (git mv to new subdirectory)

-- **3 renames** (test file name doesn't match source file name)

-- **4 splits** (one test file covers multiple source files)

-- **2 merges** (multiple test files cover one source file)

-- **15 new `__init__.py`** files in new test subdirectories

-- **1 directory removal** (`tests/types/`)

-## Step 1: Create New Directories + `__init__.py`

+## Step 1: Rename `ConsistencyPolicy.NONE` → `CACHED`

-Create empty `__init__.py` in each new directory:

+**`mirage/types.py`**: Change enum member `NONE = "none"` → `CACHED = "cached"`

+Update all references (~14 occurrences):

+- `mirage/workspace/__init__.py` (line 55)

+- `mirage/workspace/cache/policy.py` (line 15)

+- `mirage/cli/commands/workspace.py` (line 107)

+- `mirage/cli/commands/command.py` (lines 57, 69, 77, 78)

+- `mirage/server/fastapi/config.py` (line 18)

+- `tests/workspace/test_init.py` (lines 10, 29, 94)

+- `tests/cli/test_config.py` (line 133)

+## Step 2: Make `Mount.consistency` always explicit

-| `tests/backend/test_memory.py` | `tests/backend/local/test_memory.py` | `mirage/backend/local/memory.py` |

-| `tests/backend/test_local.py` | `tests/backend/local/test_local.py` | `mirage/backend/local/local.py` |

-| `tests/backend/test_proxy.py` | `tests/backend/network/test_proxy.py` | `mirage/backend/network/proxy.py` |

-| `tests/commands/test_handlers.py` | `tests/commands/builtin/test_handlers.py` | `mirage/commands/builtin/handlers.py` |

-| `tests/commands/test_nav.py` | `tests/commands/builtin/test_nav.py` | `mirage/commands/builtin/nav.py` |

-| `tests/commands/test_read.py` | `tests/commands/builtin/test_read.py` | `mirage/commands/builtin/read.py` |

-| `tests/commands/test_search.py` | `tests/commands/builtin/test_search.py` | `mirage/commands/builtin/search.py` |

-| `tests/commands/test_write.py` | `tests/commands/builtin/test_write.py` | `mirage/commands/builtin/write.py` |

-| `tests/commands/test_client.py` | `tests/commands/remote/test_client.py` | `mirage/commands/remote/client.py` |

-| `tests/fuse/test_remote.py` | `tests/fuse/remote/test_remote.py` | `mirage/fuse/remote/remote.py` |

-| `tests/server/test_app.py` | `tests/server/fastapi/test_app.py` | `mirage/server/fastapi/app.py` |

-| `tests/server/test_io_router.py` | `tests/server/fastapi/routers/test_io_router.py` | `mirage/server/fastapi/routers/io_router.py` |

-| `tests/server/test_file_cache.py` | `tests/server/service/test_cache.py` | `mirage/server/service/cache.py` |

-| `tests/server/conftest.py` | `tests/server/fastapi/conftest.py` | (serves test_app + test_io_router) |

-| `tests/server/test_arg_resolver.py` | `tests/commands/custom/test_arg_resolver.py` | `mirage/commands/custom/arg_resolver.py` |

+- `Mount.__init__`: `consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `ConsistencyPolicy | None = None`)

+- `MountRegistry.mount()`: same signature change

-## Step 3: Renames (git mv, same directory or move + rename)

+**`mirage/workspace/__init__.py`** (soon to be `workspace.py`):

+- In the `backends` dict parsing loop: when a tuple doesn't include consistency, use `self._consistency` (the workspace default) instead of `None`

+- `Workspace.mount()`: `consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `| None = None`)

+- Remove `_effective_consistency()` method entirely

+- In `cat()`: replace `self._effective_consistency(path)` with `self._registry.mount_for(path).consistency`

-| `tests/server/test_service.py` | `tests/server/service/test_local.py` | Source is `mirage/server/service/local.py` |

-| `tests/runtime/test_runtime.py` | `tests/runtime/test_base.py` | Source is `mirage/runtime/base.py` |

-| `tests/runtime/test_accessors.py` | `tests/runtime/accessors/test_local.py` | Source is `mirage/runtime/accessors/local.py` |

+**`mirage/server/service/local.py`**: Update mount consistency parsing — use `ConsistencyPolicy.CACHED` instead of `None`

+**`mirage/server/fastapi/config.py`**: `MountConfig.consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `| None`)

-### 4a. `tests/backend/test_configs.py` → 3 files

+**`mirage/cli/commands/workspace.py`**: Update any `None` consistency defaults

-- **`tests/backend/object_storage/test_s3.py`**: `test_s3config_defaults`, `test_s3config_immutable`

-- **`tests/backend/object_storage/test_r2.py`**: `test_r2config_fields`, `test_r2config_immutable`

-- ExternalCommand tests (`test_external_command_fields`, `test_external_command_immutable`) → merge into `tests/commands/custom/test_registry.py` (Step 4d)

-- Delete `tests/backend/test_configs.py` after split

+## Step 3: Move Workspace to `workspace/workspace.py`

-### 4b. `tests/fuse/test_default.py` → 2 files

+- Move `_PathLock` class and `Workspace` class from `__init__.py` (all imports stay the same)

-- **`tests/fuse/backend/test_default.py`**: `test_default_eager_small_file`, `test_default_lazy_large_file`, `test_buffered_write_flush`, `test_buffered_write_new_file`, `test_flush_noop_without_writes`

-- Delete `tests/fuse/test_default.py` after split

+from mirage.workspace.workspace import Workspace

-### 4c. `tests/fuse/test_format_ops.py` → 2 files

-- **`tests/fuse/filetype/test_parquet.py`**: `_make_parquet_bytes` helper + `test_parquet_*` (4 tests)

-- **`tests/fuse/filetype/test_csv.py`**: `test_csv_*` (3 tests)

-- Delete `tests/fuse/test_format_ops.py` after split

+No other files need import changes — they all import `from mirage.workspace import Workspace` which continues to work.

-### 4d. `tests/server/test_registry.py` → split + move

-- **`tests/commands/custom/test_registry.py`**: All `CommandRegistry` and `ExternalCommand` tests (the bulk of the file: ~40 tests). Also merge in the 2 ExternalCommand pydantic tests from `test_configs.py` (Step 4a).

-- **`tests/server/test_constants.py`**: `test_parse_size_units` only (tests `mirage/server/constants._parse_size`)

-- Delete `tests/server/test_registry.py` after split

+- `git mv tests/workspace/test_init.py tests/workspace/test_workspace.py`

+- No content changes (imports stay `from mirage.workspace import Workspace`)

-Note: The `_CustomMethodBackend` helper class in `test_registry.py` is not a test class — it's a test fixture subclass. Keep it as a module-level helper in the new file.

+- `mirage/workspace/__init__.py` (rewritten to re-exports)

+- `mirage/cli/commands/workspace.py`, `command.py`, `config.py`

-### 5a. `tests/test_types.py` + `tests/types/test_types.py` → `tests/test_types.py`

-Merge all tests from `tests/types/test_types.py` into `tests/test_types.py`. Combine imports. Delete `tests/types/` directory entirely.

-### 5b. `tests/workspace/test_write_ops.py` + `test_lock.py` → merge into `tests/workspace/test_init.py`

-All three files test `mirage/workspace/__init__.py` (Workspace class). Merge `test_write_ops.py` and `test_lock.py` content into `test_init.py`. Keep both `_make_ws` helpers (rename to avoid collision if signatures differ). Delete the two merged files.

-- Remove empty `tests/types/` directory

-- Remove stale `__init__.py` files from emptied directories (if any directory becomes empty)

-- Verify no duplicate test function names across merged files

-## Files That Stay In Place (no changes)

-- `tests/backend/test_loader.py` → `mirage/backend/loader.py` ✓

-- `tests/commands/test_init.py` → `mirage/commands/__init__.py` ✓

-- `tests/fuse/test_fs.py` → `mirage/fuse/fs.py` ✓

-- `tests/fuse/test_loader.py` → `mirage/fuse/loader.py` ✓

-- `tests/fuse/test_registry.py` → `mirage/fuse/registry.py` ✓

-- `tests/observe/test_session_log.py`, `test_entries.py`, `test_inspector.py` ✓

-- `tests/runtime/test_memory.py`, `test_local.py`, `test_remote.py` ✓

-- `tests/workspace/test_mount.py`, `test_sync.py` ✓

-- `tests/workspace/cache/test_store.py`, `test_policy.py` ✓

-- `tests/cli/test_app.py`, `test_client.py`, `test_config.py` ✓

-- `tests/integration/test_remote_transfer.py` ✓ (cross-cutting)

-1. Create all new directories + `__init__.py` (Step 1)

-4. Write split files + delete originals (Step 4a-d)

-5. Write merged files + delete originals (Step 5a-b)

-1. `uv run pytest` — all 641 tests should pass

-2. `pre-commit run --all-files` — lint/format check

-3. Verify 1:1 mapping: every test file `tests/a/b/test_c.py` corresponds to `mirage/a/b/c.py`

-4. Verify no test classes remain (only `def test_*` functions)

+1. `uv run pytest` — all 641 tests pass

+2. `pre-commit run --all-files` — lint/format clean

# Test Directory Reorganization Plan

## Context

The `mirage/` source tree was recently reorganized into subpackages (`backend/local/`, `backend/network/`, `fuse/backend/`, etc.), but the `tests/` directory still has a flat structure that doesn't mirror the source layout. This plan reorganizes tests so each source file `mirage/a/b/c.py` has exactly one test file `tests/a/b/test_c.py`.

## Summary of Changes

- **15 pure moves** (git mv to new subdirectory)
- **3 renames** (test file name doesn't match source file name)
- **4 splits** (one test file covers multiple source files)
- **2 merges** (multiple test files cover one source file)
- **15 new `__init__.py`** files in new test subdirectories
- **1 directory removal** (`tests/types/`)

---

## Step 1: Create New Directories + `__init__.py`

Create empty `__init__.py` in each new directory:

```
tests/backend/local/__init__.py
tests/backend/network/__init__.py
tests/backend/object_storage/__init__.py
tests/commands/builtin/__init__.py
tests/commands/custom/__init__.py
tests/commands/remote/__init__.py
tests/fuse/backend/__init__.py
tests/fuse/backend/local/__init__.py
tests/fuse/filetype/__init__.py
tests/fuse/remote/__init__.py
tests/server/fastapi/__init__.py
tests/server/fastapi/routers/__init__.py
tests/server/service/__init__.py
tests/runtime/accessors/__init__.py
tests/observe/fastapi/__init__.py
```

## Step 2: Pure Moves (git mv)

| From | To | Source file |
|---|---|---|
| `tests/backend/test_memory.py` | `tests/backend/local/test_memory.py` | `mirage/backend/local/memory.py` |
| `tests/backend/test_local.py` | `tests/backend/local/test_local.py` | `mirage/backend/local/local.py` |
| `tests/backend/test_proxy.py` | `tests/backend/network/test_proxy.py` | `mirage/backend/network/proxy.py` |
| `tests/commands/test_handlers.py` | `tests/commands/builtin/test_handlers.py` | `mirage/commands/builtin/handlers.py` |
| `tests/commands/test_nav.py` | `tests/commands/builtin/test_nav.py` | `mirage/commands/builtin/nav.py` |
| `tests/commands/test_read.py` | `tests/commands/builtin/test_read.py` | `mirage/commands/builtin/read.py` |
| `tests/commands/test_search.py` | `tests/commands/builtin/test_search.py` | `mirage/commands/builtin/search.py` |
| `tests/commands/test_write.py` | `tests/commands/builtin/test_write.py` | `mirage/commands/builtin/write.py` |
| `tests/commands/test_client.py` | `tests/commands/remote/test_client.py` | `mirage/commands/remote/client.py` |
| `tests/fuse/test_remote.py` | `tests/fuse/remote/test_remote.py` | `mirage/fuse/remote/remote.py` |
| `tests/server/test_app.py` | `tests/server/fastapi/test_app.py` | `mirage/server/fastapi/app.py` |
| `tests/server/test_io_router.py` | `tests/server/fastapi/routers/test_io_router.py` | `mirage/server/fastapi/routers/io_router.py` |
| `tests/server/test_file_cache.py` | `tests/server/service/test_cache.py` | `mirage/server/service/cache.py` |
| `tests/server/conftest.py` | `tests/server/fastapi/conftest.py` | (serves test_app + test_io_router) |
| `tests/server/test_arg_resolver.py` | `tests/commands/custom/test_arg_resolver.py` | `mirage/commands/custom/arg_resolver.py` |

## Step 3: Renames (git mv, same directory or move + rename)

| From | To | Reason |
|---|---|---|
| `tests/server/test_service.py` | `tests/server/service/test_local.py` | Source is `mirage/server/service/local.py` |
| `tests/runtime/test_runtime.py` | `tests/runtime/test_base.py` | Source is `mirage/runtime/base.py` |
| `tests/runtime/test_accessors.py` | `tests/runtime/accessors/test_local.py` | Source is `mirage/runtime/accessors/local.py` |

## Step 4: Splits

### 4a. `tests/backend/test_configs.py` → 3 files

- **`tests/backend/object_storage/test_s3.py`**: `test_s3config_defaults`, `test_s3config_immutable`
- **`tests/backend/object_storage/test_r2.py`**: `test_r2config_fields`, `test_r2config_immutable`
- ExternalCommand tests (`test_external_command_fields`, `test_external_command_immutable`) → merge into `tests/commands/custom/test_registry.py` (Step 4d)
- Delete `tests/backend/test_configs.py` after split

### 4b. `tests/fuse/test_default.py` → 2 files

- **`tests/fuse/backend/test_default.py`**: `test_default_eager_small_file`, `test_default_lazy_large_file`, `test_buffered_write_flush`, `test_buffered_write_new_file`, `test_flush_noop_without_writes`
- **`tests/fuse/backend/local/test_memory.py`**: `test_memory_always_eager`
- Delete `tests/fuse/test_default.py` after split

### 4c. `tests/fuse/test_format_ops.py` → 2 files

- **`tests/fuse/filetype/test_parquet.py`**: `_make_parquet_bytes` helper + `test_parquet_*` (4 tests)
- **`tests/fuse/filetype/test_csv.py`**: `test_csv_*` (3 tests)
- Delete `tests/fuse/test_format_ops.py` after split

### 4d. `tests/server/test_registry.py` → split + move

- **`tests/commands/custom/test_registry.py`**: All `CommandRegistry` and `ExternalCommand` tests (the bulk of the file: ~40 tests). Also merge in the 2 ExternalCommand pydantic tests from `test_configs.py` (Step 4a).
- **`tests/server/test_constants.py`**: `test_parse_size_units` only (tests `mirage/server/constants._parse_size`)
- Delete `tests/server/test_registry.py` after split

Note: The `_CustomMethodBackend` helper class in `test_registry.py` is not a test class — it's a test fixture subclass. Keep it as a module-level helper in the new file.

## Step 5: Merges

### 5a. `tests/test_types.py` + `tests/types/test_types.py` → `tests/test_types.py`

Merge all tests from `tests/types/test_types.py` into `tests/test_types.py`. Combine imports. Delete `tests/types/` directory entirely.

### 5b. `tests/workspace/test_write_ops.py` + `test_lock.py` → merge into `tests/workspace/test_init.py`

All three files test `mirage/workspace/__init__.py` (Workspace class). Merge `test_write_ops.py` and `test_lock.py` content into `test_init.py`. Keep both `_make_ws` helpers (rename to avoid collision if signatures differ). Delete the two merged files.

## Step 6: Cleanup

- Remove empty `tests/types/` directory
- Remove stale `__init__.py` files from emptied directories (if any directory becomes empty)
- Verify no duplicate test function names across merged files

## Files That Stay In Place (no changes)

- `tests/conftest.py`
- `tests/backend/test_loader.py` → `mirage/backend/loader.py` ✓
- `tests/commands/test_init.py` → `mirage/commands/__init__.py` ✓
- `tests/fuse/test_fs.py` → `mirage/fuse/fs.py` ✓
- `tests/fuse/test_loader.py` → `mirage/fuse/loader.py` ✓
- `tests/fuse/test_registry.py` → `mirage/fuse/registry.py` ✓
- `tests/observe/test_session_log.py`, `test_entries.py`, `test_inspector.py` ✓
- `tests/runtime/test_memory.py`, `test_local.py`, `test_remote.py` ✓
- `tests/workspace/test_mount.py`, `test_sync.py` ✓
- `tests/workspace/cache/test_store.py`, `test_policy.py` ✓
- `tests/cli/test_app.py`, `test_client.py`, `test_config.py` ✓
- `tests/integration/test_remote_transfer.py` ✓ (cross-cutting)

## Execution Order

1. Create all new directories + `__init__.py` (Step 1)
2. Do all pure `git mv` moves (Step 2)
3. Do renames (Step 3)
4. Write split files + delete originals (Step 4a-d)
5. Write merged files + delete originals (Step 5a-b)
6. Cleanup (Step 6)
7. Verify (Step 7)

## Verification

1. `uv run pytest` — all 641 tests should pass
2. `pre-commit run --all-files` — lint/format check
3. Verify 1:1 mapping: every test file `tests/a/b/test_c.py` corresponds to `mirage/a/b/c.py`
4. Verify no test classes remain (only `def test_*` functions)


ExitPlanMode

# Refactor workspace/__init__.py + ConsistencyPolicy cleanup

## Context

`mirage/workspace/__init__.py` contains the entire `Workspace` class (~340 lines) plus a `_PathLock` helper. Every other package (`backend/`, `runtime/`, `fuse/`) uses `__init__.py` only for re-exports. Additionally, `ConsistencyPolicy.NONE` is confusing alongside Python's `None` used for "inherit default". This refactor:
1. Moves the `Workspace` class to its own file
2. Renames `ConsistencyPolicy.NONE` → `ConsistencyPolicy.CACHED`
3. Makes `Mount.consistency` always explicit (no more `None` = inherit)

---

## Step 1: Rename `ConsistencyPolicy.NONE` → `CACHED`

**`mirage/types.py`**: Change enum member `NONE = "none"` → `CACHED = "cached"`

Update all references (~14 occurrences):
- `mirage/workspace/__init__.py` (line 55)
- `mirage/workspace/cache/policy.py` (line 15)
- `mirage/cli/commands/workspace.py` (line 107)
- `mirage/cli/commands/command.py` (lines 57, 69, 77, 78)
- `mirage/cli/config.py` (line 106)
- `mirage/server/fastapi/config.py` (line 18)
- `tests/workspace/test_init.py` (lines 10, 29, 94)
- `tests/cli/test_app.py` (line 36)
- `tests/cli/test_config.py` (line 133)

## Step 2: Make `Mount.consistency` always explicit

**`mirage/workspace/mount.py`**:
- `Mount.__init__`: `consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `ConsistencyPolicy | None = None`)
- `MountRegistry.mount()`: same signature change

**`mirage/workspace/__init__.py`** (soon to be `workspace.py`):
- In the `backends` dict parsing loop: when a tuple doesn't include consistency, use `self._consistency` (the workspace default) instead of `None`
- `Workspace.mount()`: `consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `| None = None`)
- Remove `_effective_consistency()` method entirely
- In `cat()`: replace `self._effective_consistency(path)` with `self._registry.mount_for(path).consistency`

**`mirage/server/service/local.py`**: Update mount consistency parsing — use `ConsistencyPolicy.CACHED` instead of `None`

**`mirage/server/fastapi/config.py`**: `MountConfig.consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `| None`)

**`mirage/cli/commands/workspace.py`**: Update any `None` consistency defaults

## Step 3: Move Workspace to `workspace/workspace.py`

**Create: `mirage/workspace/workspace.py`**
- Move `_PathLock` class and `Workspace` class from `__init__.py` (all imports stay the same)

**Rewrite: `mirage/workspace/__init__.py`**
```python
from mirage.workspace.workspace import Workspace

__all__ = ["Workspace"]
```

No other files need import changes — they all import `from mirage.workspace import Workspace` which continues to work.

## Step 4: Rename test file

- `git mv tests/workspace/test_init.py tests/workspace/test_workspace.py`
- No content changes (imports stay `from mirage.workspace import Workspace`)

## Key Files Modified

- `mirage/types.py`
- `mirage/workspace/workspace.py` (new)
- `mirage/workspace/__init__.py` (rewritten to re-exports)
- `mirage/workspace/mount.py`
- `mirage/workspace/cache/policy.py`
- `mirage/cli/commands/workspace.py`, `command.py`, `config.py`
- `mirage/server/fastapi/config.py`, `mirage/server/service/local.py`
- `tests/workspace/test_workspace.py` (renamed)
- `tests/cli/test_app.py`, `tests/cli/test_config.py`

## Verification

1. `uv run pytest` — all 641 tests pass
2. `pre-commit run --all-files` — lint/format clean


User has approved your plan. You can now start coding. Start with updating your todo list if applicable

Your plan has been saved to: /Users/agent/.claude/plans/shimmering-leaping-wozniak.md
You can refer back to it if needed during implementation.

## Approved Plan:
# Refactor workspace/__init__.py + ConsistencyPolicy cleanup

## Context

`mirage/workspace/__init__.py` contains the entire `Workspace` class (~340 lines) plus a `_PathLock` helper. Every other package (`backend/`, `runtime/`, `fuse/`) uses `__init__.py` only for re-exports. Additionally, `ConsistencyPolicy.NONE` is confusing alongside Python's `None` used for "inherit default". This refactor:
1. Moves the `Workspace` class to its own file
2. Renames `ConsistencyPolicy.NONE` → `ConsistencyPolicy.CACHED`
3. Makes `Mount.consistency` always explicit (no more `None` = inherit)

---

## Step 1: Rename `ConsistencyPolicy.NONE` → `CACHED`

**`mirage/types.py`**: Change enum member `NONE = "none"` → `CACHED = "cached"`

Update all references (~14 occurrences):
- `mirage/workspace/__init__.py` (line 55)
- `mirage/workspace/cache/policy.py` (line 15)
- `mirage/cli/commands/workspace.py` (line 107)
- `mirage/cli/commands/command.py` (lines 57, 69, 77, 78)
- `mirage/cli/config.py` (line 106)
- `mirage/server/fastapi/config.py` (line 18)
- `tests/workspace/test_init.py` (lines 10, 29, 94)
- `tests/cli/test_app.py` (line 36)
- `tests/cli/test_config.py` (line 133)

## Step 2: Make `Mount.consistency` always explicit

**`mirage/workspace/mount.py`**:
- `Mount.__init__`: `consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `ConsistencyPolicy | None = None`)
- `MountRegistry.mount()`: same signature change

**`mirage/workspace/__init__.py`** (soon to be `workspace.py`):
- In the `backends` dict parsing loop: when a tuple doesn't include consistency, use `self._consistency` (the workspace default) instead of `None`
- `Workspace.mount()`: `consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `| None = None`)
- Remove `_effective_consistency()` method entirely
- In `cat()`: replace `self._effective_consistency(path)` with `self._registry.mount_for(path).consistency`

**`mirage/server/service/local.py`**: Update mount consistency parsing — use `ConsistencyPolicy.CACHED` instead of `None`

**`mirage/server/fastapi/config.py`**: `MountConfig.consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `| None`)

**`mirage/cli/commands/workspace.py`**: Update any `None` consistency defaults

## Step 3: Move Workspace to `workspace/workspace.py`

**Create: `mirage/workspace/workspace.py`**
- Move `_PathLock` class and `Workspace` class from `__init__.py` (all imports stay the same)

**Rewrite: `mirage/workspace/__init__.py`**
```python
from mirage.workspace.workspace import Workspace

__all__ = ["Workspace"]
```

No other files need import changes — they all import `from mirage.workspace import Workspace` which continues to work.

## Step 4: Rename test file

- `git mv tests/workspace/test_init.py tests/workspace/test_workspace.py`
- No content changes (imports stay `from mirage.workspace import Workspace`)

## Key Files Modified

- `mirage/types.py`
- `mirage/workspace/workspace.py` (new)
- `mirage/workspace/__init__.py` (rewritten to re-exports)
- `mirage/workspace/mount.py`
- `mirage/workspace/cache/policy.py`
- `mirage/cli/commands/workspace.py`, `command.py`, `config.py`
- `mirage/server/fastapi/config.py`, `mirage/server/service/local.py`
- `tests/workspace/test_workspace.py` (renamed)
- `tests/cli/test_app.py`, `tests/cli/test_config.py`

## Verification

1. `uv run pytest` — all 641 tests pass
2. `pre-commit run --all-files` — lint/format clean


TodoWrite

Rename ConsistencyPolicy.NONE → CACHED in types.py and all references

Renaming ConsistencyPolicy.NONE to CACHED

Make Mount.consistency always explicit (no None)

Making Mount.consistency always explicit

Move Workspace class to workspace/workspace.py

Rename test file test_init.py → test_workspace.py

Run tests and pre-commit to verify


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

Rename ConsistencyPolicy.NONE → CACHED in types.py and all references

Renaming ConsistencyPolicy.NONE to CACHED

Make Mount.consistency always explicit (no None)

Making Mount.consistency always explicit

Move Workspace class to workspace/workspace.py

Rename test file test_init.py → test_workspace.py

Run tests and pre-commit to verify


Step 1: Rename `ConsistencyPolicy.NONE` → `CACHED` in types.py:


mirage/types.py:49:class ConsistencyPolicy(str, Enum):
/Users/agent/strukto/mirage/mirage/types.py-50-    NONE = "none"
/Users/agent/strukto/mirage/mirage/types.py-51-    LAZY = "lazy"
/Users/agent/strukto/mirage/mirage/types.py-52-    TTL = "ttl"
/Users/agent/strukto/mirage/mirage/types.py-53-    ALWAYS = "always"
/Users/agent/strukto/mirage/mirage/types.py-54-
/Users/agent/strukto/mirage/mirage/types.py-55-


    49→class ConsistencyPolicy(str, Enum):
    50→    NONE = "none"
    51→    LAZY = "lazy"
    52→    TTL = "ttl"
    53→    ALWAYS = "always"

class ConsistencyPolicy(str, Enum):
    NONE = "none"
    LAZY = "lazy"
    TTL = "ttl"
    ALWAYS = "always"


The file /Users/agent/strukto/mirage/mirage/types.py has been updated successfully.

from enum import Enum

from pydantic import BaseModel, ConfigDict, Field

class FileType(str, Enum):
    DIRECTORY = "directory"
    TEXT = "text"
    BINARY = "binary"
    JSON = "json"
    CSV = "csv"
    IMAGE_PNG = "image/png"
    IMAGE_JPEG = "image/jpeg"
    IMAGE_GIF = "image/gif"
    ZIP = "application/zip"
    GZIP = "application/gzip"
    PDF = "application/pdf"

class CostLevel(str, Enum):
    CHEAP = "cheap"
    PARTIAL = "partial"
    EXPENSIVE = "expensive"
    NETWORK = "network"

class FileStat(BaseModel):
    model_config = ConfigDict(frozen=True)

    name: str
    size: int | None = None
    modified: str | None = None
    type: FileType | None = None
    extra: dict = Field(default_factory=dict)

class SyncPolicy(str, Enum):
    STAGED = "staged"
    IMMEDIATE = "immediate"
    MANUAL = "manual"

class MountMode(str, Enum):
    READ = "read"
    WRITE = "write"
    EXEC = "exec"

class ConsistencyPolicy(str, Enum):
    NONE = "none"
    LAZY = "lazy"
    TTL = "ttl"
    ALWAYS = "always"

class RuntimeType(str, Enum):
    MEMORY = "memory"
    LOCAL = "local"
    REMOTE = "remote"

class BackendType(str, Enum):
    MEMORY = "memory"
    LOCAL = "local"
    S3 = "s3"
    R2 = "r2"
    HTTP = "http"
    HTTPS = "https"

class Inode(BaseModel):
    virtual_path: str
    backend_mount: str
    checksum: str | None = None
    size: int | None = None
    remote_checksum: str | None = None
    dirty: bool = False

class SyncResult(BaseModel):
    model_config = ConfigDict(frozen=True)

    path: str
    op: str
    status: str
    remote: str | None = None
    bytes: int | None = None
    duration_ms: int = 0
    error: str | None = None

class TransferStats(BaseModel):
    bytes_read: int = 0
    bytes_written: int = 0
    read_ops: int = 0
    write_ops: int = 0
    cache_hits: int = 0
    cache_misses: int = 0
    total_read_ms: int = 0
    total_write_ms: int = 0

class CommandStats(BaseModel):
    total_commands: int = 0
    total_errors: int = 0
    last_command: str = ""
    last_timestamp: str = ""
    command_counts: dict[str, int] = Field(default_factory=dict)

class FilePayload(BaseModel):
    checksum: str
    data: bytes | None = None

class RemoteCommandOptions(BaseModel):
    max_file_size: str = "100MB"
    cache: str = "enabled"

class CommandResult(BaseModel):
    model_config = ConfigDict(arbitrary_types_allowed=True)

    stdout: str = ""
    output_files: dict[str, bytes] = {}

class ConsistencyPolicy(str, Enum):
-    NONE = "none"
+    CACHED = "cached"
LAZY = "lazy"
TTL = "ttl"
ALWAYS = "always"


Now update all references. Let me use an agent to handle the bulk replacements across all files in parallel with reading:


Bash

grep -rn "ConsistencyPolicy\.NONE" --include="*.py" | grep -v __pycache__

Find all ConsistencyPolicy.NONE references


./tests/workspace/test_init.py:10:def _make_consistency_ws(consistency=ConsistencyPolicy.NONE, ttl_seconds=300):
./tests/workspace/test_init.py:29:    ws, backend = _make_consistency_ws(ConsistencyPolicy.NONE)
./tests/workspace/test_init.py:94:        consistency=ConsistencyPolicy.NONE,
./tests/cli/test_config.py:133:    assert consistency == ConsistencyPolicy.NONE
./tests/cli/test_app.py:36:        consistency=ConsistencyPolicy.NONE,
./mirage/workspace/cache/policy.py:15:    if consistency == ConsistencyPolicy.NONE:
./mirage/workspace/__init__.py:55:        consistency: ConsistencyPolicy = ConsistencyPolicy.NONE,
./mirage/server/fastapi/config.py:18:    consistency: ConsistencyPolicy = ConsistencyPolicy.NONE
./mirage/cli/config.py:106:    default_consistency: ConsistencyPolicy = ConsistencyPolicy.NONE,
./mirage/cli/commands/command.py:57:                        value, default_mode, ConsistencyPolicy.NONE)
./mirage/cli/commands/command.py:69:                mounts[prefix] = (backend_obj, m, ConsistencyPolicy.NONE)
./mirage/cli/commands/command.py:77:                                                 ConsistencyPolicy.NONE)
./mirage/cli/commands/command.py:78:        mounts[prefix] = (backend_obj, m, ConsistencyPolicy.NONE)
./mirage/cli/commands/workspace.py:107:    consistency: ConsistencyPolicy = typer.Option(ConsistencyPolicy.NONE),


     1→import time
     2→
     3→from mirage.backend.base import BaseBackend
     4→from mirage.types import ConsistencyPolicy, FileStat
     5→from mirage.workspace.cache.entry import CacheEntry
     6→
     7→
     8→def is_fresh(
     9→    entry: CacheEntry,
    10→    consistency: ConsistencyPolicy,
    11→    ttl_seconds: int,
    12→    backend: BaseBackend,
    13→    backend_path: str,
    14→) -> tuple[bool, FileStat | None]:
    15→    if consistency == ConsistencyPolicy.NONE:
    16→        return True, None
    17→    if consistency == ConsistencyPolicy.ALWAYS:
    18→        return False, None
    19→    if consistency == ConsistencyPolicy.TTL:
    20→        if (time.monotonic() - entry.cached_at) < ttl_seconds:
    21→            return True, None
    22→    try:
    23→        remote = backend.stat(backend_path)
    24→    except FileNotFoundError:
    25→        return False, None
    26→    return metadata_matches(entry, remote), remote
    27→
    28→
    29→def metadata_matches(entry: CacheEntry, remote: FileStat) -> bool:
    30→    remote_etag = remote.extra.get("etag")
    31→    if entry.etag and remote_etag:
    32→        return entry.etag == remote_etag
    33→    if entry.modified and remote.modified:
    34→        return entry.modified == remote.modified
    35→    if entry.size is not None and remote.size is not None:
    36→        return entry.size == remote.size
    37→    return False
    38→

import time

from mirage.backend.base import BaseBackend
from mirage.types import ConsistencyPolicy, FileStat
from mirage.workspace.cache.entry import CacheEntry

def is_fresh(
    entry: CacheEntry,
    consistency: ConsistencyPolicy,
    ttl_seconds: int,
    backend: BaseBackend,
    backend_path: str,
) -> tuple[bool, FileStat | None]:
    if consistency == ConsistencyPolicy.NONE:
        return True, None
    if consistency == ConsistencyPolicy.ALWAYS:
        return False, None
    if consistency == ConsistencyPolicy.TTL:
        if (time.monotonic() - entry.cached_at) < ttl_seconds:
            return True, None
    try:
        remote = backend.stat(backend_path)
    except FileNotFoundError:
        return False, None
    return metadata_matches(entry, remote), remote

def metadata_matches(entry: CacheEntry, remote: FileStat) -> bool:
    remote_etag = remote.extra.get("etag")
    if entry.etag and remote_etag:
        return entry.etag == remote_etag
    if entry.modified and remote.modified:
        return entry.modified == remote.modified
    if entry.size is not None and remote.size is not None:
        return entry.size == remote.size
    return False


     1→import typer
     2→
     3→from mirage.cli.config import split_mount_spec
     4→from mirage.cli.helpers import _client
     5→from mirage.types import ConsistencyPolicy, MountMode, RuntimeType
     6→
     7→command_app = typer.Typer(no_args_is_help=True)
     8→command_server_app = typer.Typer(no_args_is_help=True)
     9→
    10→
    11→@command_app.command(name="add")
    12→def command_add(
    13→        ws_id: str = typer.Argument(),
    14→        name: str = typer.Argument(),
    15→        exec_str: str = typer.Argument(),
    16→        description: str = typer.Option(""),
    17→) -> None:
    18→    """Register a user CLI command in a workspace."""
    19→    _client().register_command(ws_id, name, exec_str, description)
    20→    print(f"registered: {name}")
    21→
    22→
    23→@command_server_app.command(name="serve")
    24→def command_server_serve(
    25→    mount_specs: list[str] | None = typer.Argument(default=None),
    26→    host: str = typer.Option("0.0.0.0"),
    27→    port: int = typer.Option(8765),
    28→    transport: str = typer.Option("http", help="http or grpc"),
    29→    auth: str = typer.Option("none", help="none, bearer, basic, or apikey"),
    30→    token: str | None = typer.Option(None,
    31→                                     help="Bearer token (--auth bearer)"),
    32→    api_key: str | None = typer.Option(None, help="API key (--auth apikey)"),
    33→    api_key_header: str = typer.Option("X-API-Key"),
    34→    username: str | None = typer.Option(None, help="Username (--auth basic)"),
    35→    password: str | None = typer.Option(None, help="Password (--auth basic)"),
    36→    mode: MountMode = typer.Option(MountMode.READ),
    37→    config: str | None = typer.Option(None),
    38→    runtime: RuntimeType = typer.Option(RuntimeType.MEMORY,
    39→                                        help="Runtime: memory or local"),
    40→) -> None:
    41→    """Start a standalone command server backed by the given mounts."""
    42→    from mirage.backend.loader import load_backend_class
    43→    from mirage.cli.config import (load_workspace_yaml, parse_mount_uri,
    44→                                   parse_mount_uri_full)
    45→    from mirage.commands.remote.auth import (APIKeyAuth, BasicAuth,
    46→                                             BearerTokenAuth, NoAuth)
    47→    from mirage.workspace import Workspace
    48→
    49→    default_mode = mode
    50→    mounts: dict = {}
    51→    if config:
    52→        cfg = load_workspace_yaml(config)
    53→        if "mounts" in cfg:
    54→            for prefix, value in cfg["mounts"].items():
    55→                if isinstance(value, str):
    56→                    backend_obj, m, _ = parse_mount_uri_full(
    57→                        value, default_mode, ConsistencyPolicy.NONE)
    58→                elif isinstance(value, dict) and "backend" in value:
    59→                    cls = load_backend_class(value["backend"])
    60→                    backend_obj = cls(**(value.get("args") or {}))
    61→                    m = MountMode(
    62→                        value["mode"]) if "mode" in value else default_mode
    63→                elif isinstance(value, dict):
    64→                    backend_obj = parse_mount_uri(value.get("uri", ""))
    65→                    m = MountMode(
    66→                        value["mode"]) if "mode" in value else default_mode
    67→                else:
    68→                    continue
    69→                mounts[prefix] = (backend_obj, m, ConsistencyPolicy.NONE)
    70→
    71→    for spec in (mount_specs or []):
    72→        try:
    73→            uri, prefix = split_mount_spec(spec)
    74→        except ValueError as e:
    75→            raise typer.BadParameter(str(e))
    76→        backend_obj, m, _ = parse_mount_uri_full(uri, default_mode,
    77→                                                 ConsistencyPolicy.NONE)
    78→        mounts[prefix] = (backend_obj, m, ConsistencyPolicy.NONE)
    79→
    80→    if not mounts:
    81→        raise typer.BadParameter("No mounts specified.")
    82→
    83→    ws = Workspace(mounts, mode=default_mode)
    84→
    85→    if auth == "bearer":
    86→        if not token:
    87→            raise typer.BadParameter("--token required for bearer auth")
    88→        server_auth = BearerTokenAuth(token)
    89→    elif auth == "basic":
    90→        if not username or not password:
    91→            raise typer.BadParameter(
    92→                "--username and --password required for basic auth")
    93→        server_auth = BasicAuth(username, password)
    94→    elif auth == "apikey":
    95→        if not api_key:
    96→            raise typer.BadParameter("--api-key required for apikey auth")
    97→        server_auth = APIKeyAuth(api_key, api_key_header)
    98→    else:
    99→        server_auth = NoAuth()
   100→
   101→    if transport == "grpc":
   102→        from mirage.commands.remote.grpc.server import serve
   103→        serve(ws, server_auth, host=host, port=port)
   104→    else:
   105→        import uvicorn
   106→
   107→        from mirage.commands.remote.fastapi.app import \
   108→            command_server_app as cs_app
   109→        from mirage.commands.remote.fastapi.app import configure
   110→        configure(ws, server_auth)
   111→        uvicorn.run(cs_app, host=host, port=port, log_level="warning")
   112→

import typer

from mirage.cli.config import split_mount_spec
from mirage.cli.helpers import _client
from mirage.types import ConsistencyPolicy, MountMode, RuntimeType

command_app = typer.Typer(no_args_is_help=True)
command_server_app = typer.Typer(no_args_is_help=True)

@command_app.command(name="add")
def command_add(
        ws_id: str = typer.Argument(),
        name: str = typer.Argument(),
        exec_str: str = typer.Argument(),
        description: str = typer.Option(""),
) -> None:
    """Register a user CLI command in a workspace."""
    _client().register_command(ws_id, name, exec_str, description)
    print(f"registered: {name}")

@command_server_app.command(name="serve")
def command_server_serve(
    mount_specs: list[str] | None = typer.Argument(default=None),
    host: str = typer.Option("0.0.0.0"),
    port: int = typer.Option(8765),
    transport: str = typer.Option("http", help="http or grpc"),
    auth: str = typer.Option("none", help="none, bearer, basic, or apikey"),
    token: str | None = typer.Option(None,
                                     help="Bearer token (--auth bearer)"),
    api_key: str | None = typer.Option(None, help="API key (--auth apikey)"),
    api_key_header: str = typer.Option("X-API-Key"),
    username: str | None = typer.Option(None, help="Username (--auth basic)"),
    password: str | None = typer.Option(None, help="Password (--auth basic)"),
    mode: MountMode = typer.Option(MountMode.READ),
    config: str | None = typer.Option(None),
    runtime: RuntimeType = typer.Option(RuntimeType.MEMORY,
                                        help="Runtime: memory or local"),
) -> None:
    """Start a standalone command server backed by the given mounts."""
    from mirage.backend.loader import load_backend_class
    from mirage.cli.config import (load_workspace_yaml, parse_mount_uri,
                                   parse_mount_uri_full)
    from mirage.commands.remote.auth import (APIKeyAuth, BasicAuth,
                                             BearerTokenAuth, NoAuth)
    from mirage.workspace import Workspace

    default_mode = mode
    mounts: dict = {}
    if config:
        cfg = load_workspace_yaml(config)
        if "mounts" in cfg:
            for prefix, value in cfg["mounts"].items():
                if isinstance(value, str):
                    backend_obj, m, _ = parse_mount_uri_full(
                        value, default_mode, ConsistencyPolicy.NONE)
                elif isinstance(value, dict) and "backend" in value:
                    cls = load_backend_class(value["backend"])
                    backend_obj = cls(**(value.get("args") or {}))
                    m = MountMode(
                        value["mode"]) if "mode" in value else default_mode
                elif isinstance(value, dict):
                    backend_obj = parse_mount_uri(value.get("uri", ""))
                    m = MountMode(
                        value["mode"]) if "mode" in value else default_mode
                else:
                    continue
                mounts[prefix] = (backend_obj, m, ConsistencyPolicy.NONE)

    for spec in (mount_specs or []):
        try:
            uri, prefix = split_mount_spec(spec)
        except ValueError as e:
            raise typer.BadParameter(str(e))
        backend_obj, m, _ = parse_mount_uri_full(uri, default_mode,
                                                 ConsistencyPolicy.NONE)
        mounts[prefix] = (backend_obj, m, ConsistencyPolicy.NONE)

    if not mounts:
        raise typer.BadParameter("No mounts specified.")

    ws = Workspace(mounts, mode=default_mode)

    if auth == "bearer":
        if not token:
            raise typer.BadParameter("--token required for bearer auth")
        server_auth = BearerTokenAuth(token)
    elif auth == "basic":
        if not username or not password:
            raise typer.BadParameter(
                "--username and --password required for basic auth")
        server_auth = BasicAuth(username, password)
    elif auth == "apikey":
        if not api_key:
            raise typer.BadParameter("--api-key required for apikey auth")
        server_auth = APIKeyAuth(api_key, api_key_header)
    else:
        server_auth = NoAuth()

    if transport == "grpc":
        from mirage.commands.remote.grpc.server import serve
        serve(ws, server_auth, host=host, port=port)
    else:
        import uvicorn

        from mirage.commands.remote.fastapi.app import \
            command_server_app as cs_app
        from mirage.commands.remote.fastapi.app import configure
        configure(ws, server_auth)
        uvicorn.run(cs_app, host=host, port=port, log_level="warning")


   100→        list[str] | None,
   101→        typer.Argument(help="Mount specs: backend:/path "
   102→                       "(e.g. memory:/data/, s3://bucket:/). "
   103→                       "Append #none|#lazy|#ttl|#always for "
   104→                       "consistency, @read|@write|@exec for mode.")] = None,
   105→    sync_policy: str = typer.Option("staged", help="staged or immediate"),
   106→    mode: MountMode = typer.Option(MountMode.WRITE, help="read or write"),
   107→    consistency: ConsistencyPolicy = typer.Option(ConsistencyPolicy.NONE),
   108→    ttl: int = typer.Option(300, help="TTL in seconds for ttl consistency"),
   109→    config: str | None = typer.Option(None,
   110→                                      help="Path to YAML workspace config"),
   111→    runtime: RuntimeType = typer.Option(RuntimeType.MEMORY,
   112→                                        help="Runtime: memory or local"),
   113→    fuse: bool = typer.Option(False, help="Also mount as FUSE filesystem"),
   114→    mountpoint: str | None = typer.Option(

        list[str] | None,
        typer.Argument(help="Mount specs: backend:/path "
                       "(e.g. memory:/data/, s3://bucket:/). "
                       "Append #none|#lazy|#ttl|#always for "
                       "consistency, @read|@write|@exec for mode.")] = None,
    sync_policy: str = typer.Option("staged", help="staged or immediate"),
    mode: MountMode = typer.Option(MountMode.WRITE, help="read or write"),
    consistency: ConsistencyPolicy = typer.Option(ConsistencyPolicy.NONE),
    ttl: int = typer.Option(300, help="TTL in seconds for ttl consistency"),
    config: str | None = typer.Option(None,
                                      help="Path to YAML workspace config"),
    runtime: RuntimeType = typer.Option(RuntimeType.MEMORY,
                                        help="Runtime: memory or local"),
    fuse: bool = typer.Option(False, help="Also mount as FUSE filesystem"),
    mountpoint: str | None = typer.Option(


   100→_CONSISTENCY_SUFFIXES = ("#none", "#lazy", "#ttl", "#always")
   101→
   102→
   103→def parse_mount_uri_full(
   104→    uri: str,
   105→    default_mode: MountMode = MountMode.READ,
   106→    default_consistency: ConsistencyPolicy = ConsistencyPolicy.NONE,
   107→) -> tuple[BaseBackend, MountMode, ConsistencyPolicy]:
   108→    consistency = default_consistency
   109→    for suffix in _CONSISTENCY_SUFFIXES:
   110→        if uri.endswith(suffix):
   111→            consistency = ConsistencyPolicy(suffix[1:])
   112→            uri = uri[:-len(suffix)]
   113→            break
   114→    backend, mode = parse_mount_uri_with_mode(uri, default_mode)

_CONSISTENCY_SUFFIXES = ("#none", "#lazy", "#ttl", "#always")

def parse_mount_uri_full(
    uri: str,
    default_mode: MountMode = MountMode.READ,
    default_consistency: ConsistencyPolicy = ConsistencyPolicy.NONE,
) -> tuple[BaseBackend, MountMode, ConsistencyPolicy]:
    consistency = default_consistency
    for suffix in _CONSISTENCY_SUFFIXES:
        if uri.endswith(suffix):
            consistency = ConsistencyPolicy(suffix[1:])
            uri = uri[:-len(suffix)]
            break
    backend, mode = parse_mount_uri_with_mode(uri, default_mode)


     1→from pydantic import BaseModel
     2→
     3→from mirage.types import ConsistencyPolicy, MountMode, SyncPolicy
     4→
     5→
     6→class MountConfig(BaseModel):
     7→    uri: str | None = None
     8→    backend: str | None = None
     9→    args: dict[str, str] | None = None
    10→    mode: MountMode | None = None
    11→    consistency: ConsistencyPolicy | None = None
    12→
    13→
    14→class CreateWorkspaceRequest(BaseModel):
    15→    mounts: dict[str, str | MountConfig]
    16→    sync_policy: SyncPolicy = SyncPolicy.STAGED
    17→    mode: MountMode = MountMode.WRITE
    18→    consistency: ConsistencyPolicy = ConsistencyPolicy.NONE
    19→    ttl_seconds: int = 300
    20→    runtime: str | None = None
    21→    fuse_ops: dict[str, str] | None = None
    22→
    23→
    24→class RegisterFuseRequest(BaseModel):
    25→    mountpoint: str
    26→    pid: int
    27→    mount_specs: list[str] = []
    28→    runtime: str | None = None
    29→
    30→
    31→class FilePayloadRequest(BaseModel):
    32→    checksum: str
    33→    data: str | None = None
    34→
    35→
    36→class ExecRequest(BaseModel):
    37→    command: str
    38→    cwd: str | None = None
    39→    stdin: str | None = None
    40→    input_files: dict[str, FilePayloadRequest] | None = None
    41→
    42→
    43→class FuseReportRequest(BaseModel):
    44→    mountpoint: str
    45→    status: dict
    46→    transfers: dict = {}
    47→    ops: list[dict] = []
    48→
    49→
    50→class RegisterCommandRequest(BaseModel):
    51→    name: str
    52→    exec: str
    53→    description: str = ""
    54→
    55→
    56→class RegisterFuseOpRequest(BaseModel):
    57→    pattern: str
    58→    handler: str
    59→
    60→
    61→class ReadRequest(BaseModel):
    62→    path: str
    63→    offset: int
    64→    size: int
    65→
    66→
    67→class WriteRequest(BaseModel):
    68→    path: str
    69→    offset: int
    70→    data: str
    71→
    72→
    73→class PathRequest(BaseModel):
    74→    path: str
    75→
    76→
    77→class ReaddirRequest(BaseModel):
    78→    path: str
    79→    cursor: str | None = None
    80→
    81→
    82→class RenameRequest(BaseModel):
    83→    src: str
    84→    dst: str
    85→
    86→
    87→class TruncateRequest(BaseModel):
    88→    path: str
    89→    size: int
    90→

from pydantic import BaseModel

from mirage.types import ConsistencyPolicy, MountMode, SyncPolicy

class MountConfig(BaseModel):
    uri: str | None = None
    backend: str | None = None
    args: dict[str, str] | None = None
    mode: MountMode | None = None
    consistency: ConsistencyPolicy | None = None

class CreateWorkspaceRequest(BaseModel):
    mounts: dict[str, str | MountConfig]
    sync_policy: SyncPolicy = SyncPolicy.STAGED
    mode: MountMode = MountMode.WRITE
    consistency: ConsistencyPolicy = ConsistencyPolicy.NONE
    ttl_seconds: int = 300
    runtime: str | None = None
    fuse_ops: dict[str, str] | None = None

class RegisterFuseRequest(BaseModel):
    mountpoint: str
    pid: int
    mount_specs: list[str] = []
    runtime: str | None = None

class FilePayloadRequest(BaseModel):
    checksum: str
    data: str | None = None

class ExecRequest(BaseModel):
    command: str
    cwd: str | None = None
    stdin: str | None = None
    input_files: dict[str, FilePayloadRequest] | None = None

class FuseReportRequest(BaseModel):
    mountpoint: str
    status: dict
    transfers: dict = {}
    ops: list[dict] = []

class RegisterCommandRequest(BaseModel):
    name: str
    exec: str
    description: str = ""

class RegisterFuseOpRequest(BaseModel):
    pattern: str
    handler: str

class ReadRequest(BaseModel):
    path: str
    offset: int
    size: int

class WriteRequest(BaseModel):
    path: str
    offset: int
    data: str

class PathRequest(BaseModel):
    path: str

class ReaddirRequest(BaseModel):
    path: str
    cursor: str | None = None

class RenameRequest(BaseModel):
    src: str
    dst: str

class TruncateRequest(BaseModel):
    path: str
    size: int


    90→                if backend_spec:
    91→                    cls = load_backend_class(backend_spec)
    92→                    backend = cls(**(value.get("args") or {}))
    93→                else:
    94→                    backend = parse_mount_uri(value.get("uri") or "")
    95→                mount_mode = (MountMode(value["mode"])
    96→                              if value.get("mode") else default_mode)
    97→                mount_consistency = (ConsistencyPolicy(value["consistency"])
    98→                                     if value.get("consistency") else None)
    99→                has_override = (mount_mode != default_mode
   100→                                or mount_consistency is not None)
   101→                if has_override and mount_consistency is not None:
   102→                    backends[prefix] = (backend, mount_mode, mount_consistency)
   103→                elif has_override:
   104→                    backends[prefix] = (backend, mount_mode)
   105→                else:
   106→                    backends[prefix] = backend
   107→
   108→        ws = Workspace(
   109→            backends,

                if backend_spec:
                    cls = load_backend_class(backend_spec)
                    backend = cls(**(value.get("args") or {}))
                else:
                    backend = parse_mount_uri(value.get("uri") or "")
                mount_mode = (MountMode(value["mode"])
                              if value.get("mode") else default_mode)
                mount_consistency = (ConsistencyPolicy(value["consistency"])
                                     if value.get("consistency") else None)
                has_override = (mount_mode != default_mode
                                or mount_consistency is not None)
                if has_override and mount_consistency is not None:
                    backends[prefix] = (backend, mount_mode, mount_consistency)
                elif has_override:
                    backends[prefix] = (backend, mount_mode)
                else:
                    backends[prefix] = backend

        ws = Workspace(
            backends,


Now I have all the files read. Let me do the bulk replacement of `ConsistencyPolicy.NONE` → `ConsistencyPolicy.CACHED`:


<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>


The file /Users/agent/strukto/mirage/mirage/workspace/cache/policy.py has been updated. All occurrences of 'ConsistencyPolicy.NONE' were successfully replaced with 'ConsistencyPolicy.CACHED'.

import time

from mirage.backend.base import BaseBackend
from mirage.types import ConsistencyPolicy, FileStat
from mirage.workspace.cache.entry import CacheEntry

def is_fresh(
    entry: CacheEntry,
    consistency: ConsistencyPolicy,
    ttl_seconds: int,
    backend: BaseBackend,
    backend_path: str,
) -> tuple[bool, FileStat | None]:
    if consistency == ConsistencyPolicy.NONE:
        return True, None
    if consistency == ConsistencyPolicy.ALWAYS:
        return False, None
    if consistency == ConsistencyPolicy.TTL:
        if (time.monotonic() - entry.cached_at) < ttl_seconds:
            return True, None
    try:
        remote = backend.stat(backend_path)
    except FileNotFoundError:
        return False, None
    return metadata_matches(entry, remote), remote

def metadata_matches(entry: CacheEntry, remote: FileStat) -> bool:
    remote_etag = remote.extra.get("etag")
    if entry.etag and remote_etag:
        return entry.etag == remote_etag
    if entry.modified and remote.modified:
        return entry.modified == remote.modified
    if entry.size is not None and remote.size is not None:
        return entry.size == remote.size
    return False

-    if consistency == ConsistencyPolicy.NONE:

+    if consistency == ConsistencyPolicy.CACHED:


The file /Users/agent/strukto/mirage/mirage/cli/commands/command.py has been updated. All occurrences of 'ConsistencyPolicy.NONE' were successfully replaced with 'ConsistencyPolicy.CACHED'.

import typer

from mirage.cli.config import split_mount_spec
from mirage.cli.helpers import _client
from mirage.types import ConsistencyPolicy, MountMode, RuntimeType

command_app = typer.Typer(no_args_is_help=True)
command_server_app = typer.Typer(no_args_is_help=True)

@command_app.command(name="add")
def command_add(
        ws_id: str = typer.Argument(),
        name: str = typer.Argument(),
        exec_str: str = typer.Argument(),
        description: str = typer.Option(""),
) -> None:
    """Register a user CLI command in a workspace."""
    _client().register_command(ws_id, name, exec_str, description)
    print(f"registered: {name}")

@command_server_app.command(name="serve")
def command_server_serve(
    mount_specs: list[str] | None = typer.Argument(default=None),
    host: str = typer.Option("0.0.0.0"),
    port: int = typer.Option(8765),
    transport: str = typer.Option("http", help="http or grpc"),
    auth: str = typer.Option("none", help="none, bearer, basic, or apikey"),
    token: str | None = typer.Option(None,
                                     help="Bearer token (--auth bearer)"),
    api_key: str | None = typer.Option(None, help="API key (--auth apikey)"),
    api_key_header: str = typer.Option("X-API-Key"),
    username: str | None = typer.Option(None, help="Username (--auth basic)"),
    password: str | None = typer.Option(None, help="Password (--auth basic)"),
    mode: MountMode = typer.Option(MountMode.READ),
    config: str | None = typer.Option(None),
    runtime: RuntimeType = typer.Option(RuntimeType.MEMORY,
                                        help="Runtime: memory or local"),
) -> None:
    """Start a standalone command server backed by the given mounts."""
    from mirage.backend.loader import load_backend_class
    from mirage.cli.config import (load_workspace_yaml, parse_mount_uri,
                                   parse_mount_uri_full)
    from mirage.commands.remote.auth import (APIKeyAuth, BasicAuth,
                                             BearerTokenAuth, NoAuth)
    from mirage.workspace import Workspace

    default_mode = mode
    mounts: dict = {}
    if config:
        cfg = load_workspace_yaml(config)
        if "mounts" in cfg:
            for prefix, value in cfg["mounts"].items():
                if isinstance(value, str):
                    backend_obj, m, _ = parse_mount_uri_full(
                        value, default_mode, ConsistencyPolicy.NONE)
                elif isinstance(value, dict) and "backend" in value:
                    cls = load_backend_class(value["backend"])
                    backend_obj = cls(**(value.get("args") or {}))
                    m = MountMode(
                        value["mode"]) if "mode" in value else default_mode
                elif isinstance(value, dict):
                    backend_obj = parse_mount_uri(value.get("uri", ""))
                    m = MountMode(
                        value["mode"]) if "mode" in value else default_mode
                else:
                    continue
                mounts[prefix] = (backend_obj, m, ConsistencyPolicy.NONE)

    for spec in (mount_specs or []):
        try:
            uri, prefix = split_mount_spec(spec)
        except ValueError as e:
            raise typer.BadParameter(str(e))
        backend_obj, m, _ = parse_mount_uri_full(uri, default_mode,
                                                 ConsistencyPolicy.NONE)
        mounts[prefix] = (backend_obj, m, ConsistencyPolicy.NONE)

    if not mounts:
        raise typer.BadParameter("No mounts specified.")

    ws = Workspace(mounts, mode=default_mode)

    if auth == "bearer":
        if not token:
            raise typer.BadParameter("--token required for bearer auth")
        server_auth = BearerTokenAuth(token)
    elif auth == "basic":
        if not username or not password:
            raise typer.BadParameter(
                "--username and --password required for basic auth")
        server_auth = BasicAuth(username, password)
    elif auth == "apikey":
        if not api_key:
            raise typer.BadParameter("--api-key required for apikey auth")
        server_auth = APIKeyAuth(api_key, api_key_header)
    else:
        server_auth = NoAuth()

    if transport == "grpc":
        from mirage.commands.remote.grpc.server import serve
        serve(ws, server_auth, host=host, port=port)
    else:
        import uvicorn

        from mirage.commands.remote.fastapi.app import \
            command_server_app as cs_app
        from mirage.commands.remote.fastapi.app import configure
        configure(ws, server_auth)
        uvicorn.run(cs_app, host=host, port=port, log_level="warning")

-                        value, default_mode, ConsistencyPolicy.NONE)

+                        value, default_mode, ConsistencyPolicy.CACHED)

-                mounts[prefix] = (backend_obj, m, ConsistencyPolicy.NONE)

+                mounts[prefix] = (backend_obj, m, ConsistencyPolicy.CACHED)

-                                                 ConsistencyPolicy.NONE)

+                                                 ConsistencyPolicy.CACHED)