Now I have a comprehensive picture. Let me write the final plan. The key mapping is:

### Current → Target test file mapping

The rule: `mirage/a/b/c.py` → `tests/a/b/test_c.py`

Let me map each current test file to where it should go:

**tests/backend/**
- `test_memory.py` → `tests/backend/local/test_memory.py` (move, source: `mirage/backend/local/memory.py`)
- `test_local.py` → `tests/backend/local/test_local.py` (move, source: `mirage/backend/local/local.py`)

- `test_proxy.py` → `tests/backend/network/test_proxy.py` (move, source: `mirage/backend/network/proxy.py`)
- `test_configs.py` needs splitting: the S3 and R2 config tests go to `tests/backend/object_storage/` while the ExternalCommand tests move to `tests/commands/custom/test_registry.py`
- `test_loader.py` stays in place since it tests the backend loader itself

For the FUSE tests, I'm splitting them by functionality: DefaultFuseOp and its write/flush operations go to `tests/fuse/backend/test_default.py`, while MemoryFuseOp gets its own file at `tests/fuse/backend/local/test_memory.py`. The format operation tests are being separated by file type—CsvFuseOp and ParquetFuseOp each get dedicated test files under `tests/fuse/filetype/`.

For the workspace tests, I'm consolidating the three files that all test the Workspace class (`test_write_ops.py`, `test_init.py`, and `test_lock.py`) into a single `tests/workspace/test_init.py` file, while keeping the mount and sync tests in their own files since they correspond to separate source modules.

In the cache subdirectory, I'm renaming `test_cache_entry.py` to `test_entry.py` to match the source file name, and the store and policy tests stay as-is.

For observe tests, the API test is moving to `tests/observe/fastapi/test_router.py` since it tests the FastAPI router, while the session log, entries, and inspector tests remain in their current locations.

For the server tests, `test_app.py` is staying as an integration test at `tests/server/fastapi/test_app.py` since it covers the full FastAPI app and workspace endpoints, the file cache test is moving to `tests/server/service/test_cache.py`, and the IO router test is moving to `tests/server/fastapi/routers/test_io_router.py`.

Now looking at the remaining test files: `test_service.py` needs to be renamed and moved to `tests/server/service/test_local.py` since it tests the local service module, `test_arg_resolver.py` should move to `tests/commands/custom/test_arg_resolver.py` under the commands directory, and `test_registry.py` needs to be split since it covers both the registry and constants modules—registry tests go to `tests/commands/custom/test_registry.py` while the size parsing tests go to `tests/server/test_constants.py`. The `conftest.py` file will either stay in place or move to `tests/server/fastapi/conftest.py` depending on where the app tests end up.

For the runtime tests, `test_runtime.py` gets renamed to `test_base.py` to match its source module, while the memory, local, and remote test files stay put. The accessors test needs to move into its own subdirectory at `tests/runtime/accessors/test_local.py`.

The command tests are getting reorganized by moving handler, navigation, read, search, and write tests into a `builtin` subdirectory, and the client test moves to `tests/commands/remote/test_client.py`. The CLI tests all stay in their current location, and finally the type tests scattered across the root and types directory get consolidated into a single `tests/test_types.py` file.

Integration tests are staying as-is since they cover cross-cutting concerns across multiple modules. Now I'm reconsidering the backend configs test—it's testing three different Pydantic models from different source files, so it needs to be split across three separate test files corresponding to where each model lives. Let me check what's actually in that test file before finalizing the split.


Let me read the key test files that need splitting to write an accurate plan.


     1→import pytest
     2→from pydantic import ValidationError
     3→
     4→from mirage.backend.object_storage.r2 import R2Config
     5→from mirage.backend.object_storage.s3 import S3Config
     6→from mirage.commands.custom.registry import ExternalCommand
     7→
     8→
     9→def test_s3config_defaults():
    10→    c = S3Config(bucket="my-bucket")
    11→    assert c.region is None
    12→    assert c.timeout == 30
    13→    assert c.select_threshold_bytes == 1024 * 1024
    14→
    15→
    16→def test_s3config_immutable():
    17→    c = S3Config(bucket="x")
    18→    with pytest.raises(ValidationError):
    19→        c.bucket = "y"
    20→
    21→
    22→def test_r2config_fields():
    23→    c = R2Config(account_id="acc",
    24→                 bucket="bkt",
    25→                 access_key_id="k",
    26→                 secret_access_key="s")
    27→    assert c.region == "auto"
    28→    assert c.timeout == 30
    29→
    30→
    31→def test_r2config_immutable():
    32→    c = R2Config(account_id="a",
    33→                 bucket="b",
    34→                 access_key_id="k",
    35→                 secret_access_key="s")
    36→    with pytest.raises(ValidationError):
    37→        c.bucket = "other"
    38→
    39→
    40→def test_external_command_fields():
    41→    cmd = ExternalCommand(name="linecount", exec="python lc.py {file}")
    42→    assert cmd.description == ""
    43→
    44→
    45→def test_external_command_immutable():
    46→    cmd = ExternalCommand(name="x", exec="echo {file}")
    47→    with pytest.raises(ValidationError):
    48→        cmd.name = "y"
    49→

import pytest
from pydantic import ValidationError

from mirage.backend.object_storage.r2 import R2Config
from mirage.backend.object_storage.s3 import S3Config
from mirage.commands.custom.registry import ExternalCommand

def test_s3config_defaults():
    c = S3Config(bucket="my-bucket")
    assert c.region is None
    assert c.timeout == 30
    assert c.select_threshold_bytes == 1024 * 1024

def test_s3config_immutable():
    c = S3Config(bucket="x")
    with pytest.raises(ValidationError):
        c.bucket = "y"

def test_r2config_fields():
    c = R2Config(account_id="acc",
                 bucket="bkt",
                 access_key_id="k",
                 secret_access_key="s")
    assert c.region == "auto"
    assert c.timeout == 30

def test_r2config_immutable():
    c = R2Config(account_id="a",
                 bucket="b",
                 access_key_id="k",
                 secret_access_key="s")
    with pytest.raises(ValidationError):
        c.bucket = "other"

def test_external_command_fields():
    cmd = ExternalCommand(name="linecount", exec="python lc.py {file}")
    assert cmd.description == ""

def test_external_command_immutable():
    cmd = ExternalCommand(name="x", exec="echo {file}")
    with pytest.raises(ValidationError):
        cmd.name = "y"


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

import io

import pyarrow as pa
import pyarrow.parquet as pq

from mirage.backend.local.memory import MemoryBackend
from mirage.fuse.filetype.csv import CsvFuseOp
from mirage.fuse.filetype.parquet import ParquetFuseOp
from mirage.types import MountMode
from mirage.workspace import Workspace

def _make_parquet_bytes() -> bytes:
    table = pa.table({
        "name": ["alice", "bob", "charlie"],
        "score": [95, 87, 72],
    })
    buf = io.BytesIO()
    pq.write_table(table, buf)
    return buf.getvalue()

def test_parquet_read_returns_text():
    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    ws.write("/data.parquet", _make_parquet_bytes())
    op = ParquetFuseOp()
    ctx = op.on_open(ws, "/data.parquet", 1000)
    data = op.read(ws, "/data.parquet", 0, 4096, ctx)
    text = data.decode()
    assert "name" in text
    assert "score" in text
    assert "alice" in text

def test_parquet_schema_in_output():
    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    ws.write("/data.parquet", _make_parquet_bytes())
    op = ParquetFuseOp()
    ctx = op.on_open(ws, "/data.parquet", 1000)
    data = op.read(ws, "/data.parquet", 0, 8192, ctx)
    text = data.decode()
    assert "int" in text.lower()

def test_parquet_row_count():
    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    ws.write("/data.parquet", _make_parquet_bytes())
    op = ParquetFuseOp()
    ctx = op.on_open(ws, "/data.parquet", 1000)
    data = op.read(ws, "/data.parquet", 0, 8192, ctx)
    text = data.decode()
    assert "Rows: 3" in text

def test_parquet_error_handling():
    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    ws.write("/bad.parquet", b"not a parquet file")
    op = ParquetFuseOp()
    ctx = op.on_open(ws, "/bad.parquet", 18)
    data = op.read(ws, "/bad.parquet", 0, 4096, ctx)
    text = data.decode()
    assert "Error" in text

def test_csv_small_passthrough():
    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    small = b"name,score\nalice,95\nbob,87\n"
    ws.write("/small.csv", small)
    op = CsvFuseOp()
    ctx = op.on_open(ws, "/small.csv", len(small))
    data = op.read(ws, "/small.csv", 0, 4096, ctx)
    assert data == small

def test_csv_large_preview():
    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    header = "name,score\n"
    rows = "".join(f"user{i},{i}\n" for i in range(5000))
    big = (header + rows).encode()
    ws.write("/big.csv", big)
    op = CsvFuseOp()
    ctx = op.on_open(ws, "/big.csv", len(big))
    data = op.read(ws, "/big.csv", 0, 8192, ctx)
    text = data.decode()
    assert "name" in text
    assert "score" in text
    assert "5001 rows" in text
    assert "2 columns" in text

def test_csv_large_has_preview_marker():
    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    header = "a,b,c\n"
    rows = "".join(f"{i},{i+1},{i+2}\n" for i in range(5000))
    big = (header + rows).encode()
    ws.write("/big.csv", big)
    op = CsvFuseOp()
    ctx = op.on_open(ws, "/big.csv", len(big))
    data = op.read(ws, "/big.csv", 0, 8192, ctx)
    text = data.decode()
    assert "more rows" in text


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

import sys
from unittest.mock import MagicMock

import pytest

from mirage.backend.local.memory import MemoryBackend
from mirage.commands.custom.registry import CommandRegistry, ExternalCommand
from mirage.commands.remote.client import CommandClient
from mirage.types import (CommandResult, MountMode, RemoteCommandOptions,
                          SyncPolicy)
from mirage.workspace import Workspace

def test_builtin_ls():
    ws = Workspace({"/tmp/": MemoryBackend()},
                   mode=MountMode.WRITE,
                   sync_policy=SyncPolicy.IMMEDIATE)
    ws.write("/tmp/a.txt", b"hi")
    reg = CommandRegistry()
    result = reg.run(ws, "ls /tmp/", cwd="/")
    assert "a.txt" in result

def test_register_external():
    reg = CommandRegistry()
    reg.register(ExternalCommand(name="echo-test", exec="echo hello"))
    assert "echo-test" in reg.list_commands()

def test_run_external_no_placeholder(tmp_path):
    reg = CommandRegistry()
    reg.register(ExternalCommand(name="greet", exec="echo hello"))
    ws = Workspace({"/tmp/": MemoryBackend()})
    result = reg.run(ws, "greet", cwd="/")
    assert "hello" in result

def test_run_external_file_placeholder(tmp_path):
    ws = Workspace({"/tmp/": MemoryBackend()}, mode=MountMode.WRITE)
    ws.write("/tmp/data.txt", b"line1\nline2")
    script = tmp_path / "count.py"
    script.write_text(
        "import sys; lines = open(sys.argv[1]).readlines(); print(len(lines))")
    reg = CommandRegistry()
    reg.register(
        ExternalCommand(name="linecount", exec=f"{sys.executable} {script}"))
    result = reg.run(ws, "linecount /tmp/data.txt", cwd="/")
    assert result.strip() == "2"

def test_unknown_command():
    reg = CommandRegistry()
    ws = Workspace({"/tmp/": MemoryBackend()})
    with pytest.raises(ValueError, match="unknown command"):
        reg.run(ws, "nonexistent", cwd="/")

def test_run_external_file_placeholder_multi_arg(tmp_path):
    ws = Workspace({"/tmp/": MemoryBackend()}, mode=MountMode.WRITE)
    ws.write("/tmp/a.txt", b"aaa")
    ws.write("/tmp/b.txt", b"bbb")
    script = tmp_path / "concat.py"
    script.write_text(
        "import sys; [print(open(f).read(), end='') for f in sys.argv[1:]]")
    reg = CommandRegistry()
    reg.register(
        ExternalCommand(name="concat", exec=f"{sys.executable} {script}"))
    result = reg.run(ws, "concat /tmp/a.txt /tmp/b.txt", cwd="/")
    assert "aaa" in result
    assert "bbb" in result

def test_external_overrides_builtin():
    ws = Workspace({"/tmp/": MemoryBackend()},
                   mode=MountMode.WRITE,
                   sync_policy=SyncPolicy.IMMEDIATE)
    ws.write("/tmp/file.txt", b"data")
    reg = CommandRegistry()
    reg.register(ExternalCommand(name="ls", exec="echo overridden"))
    result = reg.run(ws, "ls /tmp/", cwd="/")
    assert "overridden" in result
    assert "file.txt" not in result

class _CustomMethodBackend(MemoryBackend):

    def lllll(self, path: str) -> str:
        return f"custom-result-for:{path}"

    def bytes_method(self, path: str) -> bytes:
        return b"raw-bytes"

    def list_method(self, path: str) -> list:
        return ["item1", "item2", "item3"]

def test_backend_method_fallback():
    backend = _CustomMethodBackend()
    ws = Workspace({"/data/": backend}, mode=MountMode.WRITE)
    reg = CommandRegistry()
    result = reg.run(ws, "lllll /data/", cwd="/")
    assert "custom-result-for:" in result

def test_backend_method_fallback_bytes_coercion():
    backend = _CustomMethodBackend()
    ws = Workspace({"/data/": backend}, mode=MountMode.WRITE)
    reg = CommandRegistry()
    result = reg.run(ws, "bytes_method /data/", cwd="/")
    assert result == "raw-bytes"

def test_backend_method_fallback_list_coercion():
    backend = _CustomMethodBackend()
    ws = Workspace({"/data/": backend}, mode=MountMode.WRITE)
    reg = CommandRegistry()
    result = reg.run(ws, "list_method /data/", cwd="/")
    assert "item1\nitem2\nitem3" == result

def test_backend_private_method_rejected():
    backend = _CustomMethodBackend()
    ws = Workspace({"/data/": backend}, mode=MountMode.WRITE)
    reg = CommandRegistry()
    with pytest.raises(ValueError, match="unknown command"):
        reg.run(ws, "_files /data/", cwd="/")

def test_list_commands_includes_builtins():
    registry = CommandRegistry()
    cmds = registry.list_commands()
    assert "ls" in cmds
    assert "cat" in cmds

def test_run_builtin_cat():
    backend = MemoryBackend()
    backend.put("/hello.txt", b"hello world")
    ws = Workspace({"/data/": backend}, mode=MountMode.READ)
    result = CommandRegistry().run(ws, "cat /data/hello.txt", cwd="/")
    assert "hello world" in result

def test_external_command_with_options():
    cmd = ExternalCommand(
        name="transform",
        exec="transform",
        options=RemoteCommandOptions(max_file_size="500MB", cache="disabled"),
    )
    assert cmd.options.max_file_size == "500MB"
    assert cmd.options.cache == "disabled"

def test_external_command_default_options():
    cmd = ExternalCommand(name="echo-test", exec="echo hello")
    assert cmd.options.max_file_size == "100MB"

def test_external_command_default_options_not_none():
    cmd = ExternalCommand(name="echo-test", exec="echo hello")
    assert cmd.options is not None
    assert cmd.options.max_file_size == "100MB"

def test_run_external_out_placeholder(tmp_path):
    ws = Workspace({"/tmp/": MemoryBackend()},
                   mode=MountMode.WRITE,
                   sync_policy=SyncPolicy.IMMEDIATE)
    ws.write("/tmp/input.txt", b"hello world")
    script = tmp_path / "upper.py"
    script.write_text("import sys\n"
                      "data = open(sys.argv[1]).read()\n"
                      "open(sys.argv[2], 'w').write(data.upper())\n"
                      "print('done')\n")
    reg = CommandRegistry()
    reg.register(
        ExternalCommand(
            name="upper",
            exec=f"{sys.executable} {script}",
        ))
    result = reg.run(ws, "upper /tmp/input.txt /tmp/output.txt", cwd="/")
    assert "done" in result
    output = ws.cat("/tmp/output.txt")
    assert output == b"HELLO WORLD"

def test_registry_dispatches_to_client_with_files():
    mock_client = MagicMock(spec=CommandClient)
    mock_client.run.return_value = CommandResult(
        stdout="done",
        output_files={"/tmp/out.csv": b"result"},
    )

    ws = Workspace({"/tmp/": MemoryBackend()},
                   mode=MountMode.WRITE,
                   sync_policy=SyncPolicy.IMMEDIATE)
    ws.write("/tmp/input.json", b'{"data": true}')

    reg = CommandRegistry(client=mock_client)
    reg.register(ExternalCommand(
        name="transform",
        exec="transform",
    ))
    result = reg.run(ws, "transform /tmp/input.json /tmp/out.csv", cwd="/")
    assert "done" in result

    call_kwargs = mock_client.run.call_args
    assert call_kwargs.kwargs.get("files") is not None
    files = call_kwargs.kwargs["files"]
    assert "/tmp/input.json" in files
    assert files["/tmp/input.json"].data == b'{"data": true}'
    assert files["/tmp/input.json"].checksum is not None

    output = ws.cat("/tmp/out.csv")
    assert output == b"result"

def test_registry_dispatches_to_client_with_stdin():
    mock_client = MagicMock(spec=CommandClient)
    mock_client.run.return_value = CommandResult(stdout="processed")

    ws = Workspace({"/tmp/": MemoryBackend()})
    reg = CommandRegistry(client=mock_client)
    reg.register(ExternalCommand(name="process", exec="process"))
    result = reg.run(ws, "process", cwd="/", stdin=b"pipe input")
    assert "processed" in result
    call_kwargs = mock_client.run.call_args
    assert call_kwargs.kwargs.get("stdin") == b"pipe input"

def test_registry_client_no_placeholders_still_uses_client():
    mock_client = MagicMock(spec=CommandClient)
    mock_client.run.return_value = CommandResult(stdout="remote echo")

    ws = Workspace({"/tmp/": MemoryBackend()})
    reg = CommandRegistry(client=mock_client)
    reg.register(ExternalCommand(name="remote-echo", exec="echo hello"))
    result = reg.run(ws, "remote-echo", cwd="/")
    assert "remote echo" in result
    mock_client.run.assert_called_once()

def test_registry_out_to_readonly_mount_fails():
    mock_client = MagicMock(spec=CommandClient)
    ws = Workspace({"/data/": MemoryBackend()}, mode=MountMode.READ)
    reg = CommandRegistry(client=mock_client)
    reg.register(ExternalCommand(name="transform", exec="transform"))
    with pytest.raises(PermissionError, match="read-only"):
        reg.run(ws, "transform /data/in.json /data/out.csv", cwd="/")

def test_registry_rejects_oversized_file():
    mock_client = MagicMock(spec=CommandClient)
    ws = Workspace({"/tmp/": MemoryBackend()},
                   mode=MountMode.WRITE,
                   sync_policy=SyncPolicy.IMMEDIATE)
    ws.write("/tmp/big.bin", b"x" * 200)

    reg = CommandRegistry(client=mock_client)
    reg.register(
        ExternalCommand(
            name="process",
            exec="process",
            options=RemoteCommandOptions(max_file_size="100B"),
        ))
    with pytest.raises(ValueError, match="exceeds max"):
        reg.run(ws, "process /tmp/big.bin", cwd="/")

def test_registry_allows_file_within_size_limit():
    mock_client = MagicMock(spec=CommandClient)
    mock_client.run.return_value = CommandResult(stdout="ok")

    ws = Workspace({"/tmp/": MemoryBackend()},
                   mode=MountMode.WRITE,
                   sync_policy=SyncPolicy.IMMEDIATE)
    ws.write("/tmp/small.txt", b"small")

    reg = CommandRegistry(client=mock_client)
    reg.register(
        ExternalCommand(
            name="process",
            exec="process",
            options=RemoteCommandOptions(max_file_size="100B"),
        ))
    result = reg.run(ws, "process /tmp/small.txt", cwd="/")
    assert "ok" in result

def test_parse_size_units():
    from mirage.server.constants import _parse_size
    assert _parse_size("100B") == 100
    assert _parse_size("10KB") == 10240
    assert _parse_size("1MB") == 1048576
    assert _parse_size("2GB") == 2147483648
    assert _parse_size("100") == 100

def test_run_external_no_template_input_output(tmp_path):
    ws = Workspace({"/tmp/": MemoryBackend()},
                   mode=MountMode.WRITE,
                   sync_policy=SyncPolicy.IMMEDIATE)
    ws.write("/tmp/input.txt", b"hello world")
    script = tmp_path / "upper.py"
    script.write_text("import sys\n"
                      "data = open(sys.argv[1]).read()\n"
                      "open(sys.argv[2], 'w').write(data.upper())\n"
                      "print('done')\n")
    reg = CommandRegistry()
    reg.register(
        ExternalCommand(name="upper", exec=f"{sys.executable} {script}"))
    result = reg.run(ws, "upper /tmp/input.txt /tmp/output.txt", cwd="/")
    assert "done" in result
    output = ws.cat("/tmp/output.txt")
    assert output == b"HELLO WORLD"

def test_run_external_no_template_inplace(tmp_path):
    ws = Workspace({"/tmp/": MemoryBackend()},
                   mode=MountMode.WRITE,
                   sync_policy=SyncPolicy.IMMEDIATE)
    ws.write("/tmp/data.txt", b"hello")
    script = tmp_path / "inplace.py"
    script.write_text("import sys\n"
                      "path = sys.argv[1]\n"
                      "data = open(path).read()\n"
                      "open(path, 'w').write(data.upper())\n"
                      "print('done')\n")
    reg = CommandRegistry()
    reg.register(
        ExternalCommand(name="format", exec=f"{sys.executable} {script}"))
    result = reg.run(ws, "format /tmp/data.txt", cwd="/")
    assert "done" in result
    assert ws.cat("/tmp/data.txt") == b"HELLO"

def test_run_external_no_template_no_files():
    ws = Workspace({"/tmp/": MemoryBackend()})
    reg = CommandRegistry()
    reg.register(ExternalCommand(name="greet", exec="echo"))
    result = reg.run(ws, "greet hello world", cwd="/")
    assert "hello world" in result

def test_dispatch_auto_detect_input_and_output():
    mock_client = MagicMock(spec=CommandClient)
    mock_client.run.return_value = CommandResult(
        stdout="done",
        output_files={"/tmp/out.csv": b"result"},
    )
    ws = Workspace({"/tmp/": MemoryBackend()},
                   mode=MountMode.WRITE,
                   sync_policy=SyncPolicy.IMMEDIATE)
    ws.write("/tmp/input.json", b'{"data": true}')

    reg = CommandRegistry(client=mock_client)
    reg.register(ExternalCommand(name="transform", exec="transform"))
    result = reg.run(ws, "transform /tmp/input.json /tmp/out.csv", cwd="/")
    assert "done" in result

    call_kwargs = mock_client.run.call_args
    files = call_kwargs.kwargs.get("files")
    assert files is not None
    assert "/tmp/input.json" in files
    assert files["/tmp/input.json"].data == b'{"data": true}'

    output = ws.cat("/tmp/out.csv")
    assert output == b"result"

def test_dispatch_auto_detect_inplace():
    mock_client = MagicMock(spec=CommandClient)
    mock_client.run.return_value = CommandResult(
        stdout="formatted",
        output_files={"/tmp/data.json": b'{"formatted": true}'},
    )
    ws = Workspace({"/tmp/": MemoryBackend()},
                   mode=MountMode.WRITE,
                   sync_policy=SyncPolicy.IMMEDIATE)
    ws.write("/tmp/data.json", b'{"data": true}')

    reg = CommandRegistry(client=mock_client)
    reg.register(ExternalCommand(name="format", exec="format"))
    result = reg.run(ws, "format /tmp/data.json", cwd="/")
    assert "formatted" in result
    assert ws.cat("/tmp/data.json") == b'{"formatted": true}'

def test_dispatch_auto_detect_no_files():
    mock_client = MagicMock(spec=CommandClient)
    mock_client.run.return_value = CommandResult(stdout="hello")
    ws = Workspace({"/tmp/": MemoryBackend()})
    reg = CommandRegistry(client=mock_client)
    reg.register(ExternalCommand(name="echo", exec="echo"))
    result = reg.run(ws, "echo hello", cwd="/")
    assert "hello" in result

def test_dispatch_locks_paths_during_execution():
    mock_client = MagicMock(spec=CommandClient)
    locked_during_run = []

    def capture_locks(*args, **kwargs):
        locked_during_run.extend(list(ws._locked_paths))
        return CommandResult(stdout="ok")

    mock_client.run.side_effect = capture_locks
    ws = Workspace({"/tmp/": MemoryBackend()},
                   mode=MountMode.WRITE,
                   sync_policy=SyncPolicy.IMMEDIATE)
    ws.write("/tmp/file.txt", b"data")
    reg = CommandRegistry(client=mock_client)
    reg.register(ExternalCommand(name="process", exec="process"))
    reg.run(ws, "process /tmp/file.txt", cwd="/")
    assert "/tmp/file.txt" in locked_during_run
    assert len(ws._locked_paths) == 0

def test_dispatch_size_limit_enforced():
    mock_client = MagicMock(spec=CommandClient)
    ws = Workspace({"/tmp/": MemoryBackend()},
                   mode=MountMode.WRITE,
                   sync_policy=SyncPolicy.IMMEDIATE)
    ws.write("/tmp/big.bin", b"x" * 200)
    reg = CommandRegistry(client=mock_client)
    reg.register(
        ExternalCommand(
            name="process",
            exec="process",
            options=RemoteCommandOptions(max_file_size="100B"),
        ))
    with pytest.raises(ValueError, match="exceeds max"):
        reg.run(ws, "process /tmp/big.bin", cwd="/")

def test_dispatch_output_to_readonly_fails():
    mock_client = MagicMock(spec=CommandClient)
    ws = Workspace({"/data/": MemoryBackend()}, mode=MountMode.READ)
    reg = CommandRegistry(client=mock_client)
    reg.register(ExternalCommand(name="transform", exec="transform"))
    with pytest.raises(PermissionError, match="read-only"):
        reg.run(ws, "transform /data/in.json /data/out.csv", cwd="/")


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

import pytest

from mirage.backend.local.memory import MemoryBackend
from mirage.commands.custom.registry import CommandRegistry
from mirage.fuse.registry import FuseOpRegistry
from mirage.runtime import MemoryRuntime
from mirage.runtime.accessors.local import (LocalCommandsAccessor,
                                            LocalFuseOpsAccessor,
                                            LocalWorkspaceOpsAccessor)
from mirage.types import MountMode
from mirage.workspace import Workspace

def _make_ws():
    return Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)

def test_local_commands_list():
    reg = CommandRegistry()
    acc = LocalCommandsAccessor(reg)
    cmds = acc.list()
    assert "ls" in cmds
    assert "cat" in cmds

def test_local_commands_register():
    reg = CommandRegistry()
    acc = LocalCommandsAccessor(reg)
    acc.register("myjq", "jq .")
    assert "myjq" in acc.list()

def test_local_fuse_ops_list():
    reg = FuseOpRegistry()
    acc = LocalFuseOpsAccessor(reg)
    ops = acc.list()
    assert any(o["pattern"] == "*" for o in ops)

def test_local_fuse_ops_register():
    reg = FuseOpRegistry()
    acc = LocalFuseOpsAccessor(reg)
    acc.register("*.parquet", "parquet")
    ops = acc.list()
    assert any(o["pattern"] == "*.parquet" for o in ops)

def test_local_fuse_ops_register_unknown():
    reg = FuseOpRegistry()
    acc = LocalFuseOpsAccessor(reg)
    with pytest.raises(ValueError, match="unknown handler"):
        acc.register("*.xyz", "nonexistent_handler_xyz")

def test_local_workspace_ops_status():
    ws = _make_ws()
    acc = LocalWorkspaceOpsAccessor(ws)
    result = acc.status()
    assert isinstance(result, list)

def test_local_workspace_ops_sync():
    ws = _make_ws()
    acc = LocalWorkspaceOpsAccessor(ws)
    result = acc.sync()
    assert isinstance(result, list)

def test_local_workspace_ops_status_after_write():
    ws = _make_ws()
    ws.write("/f.txt", b"hello")
    acc = LocalWorkspaceOpsAccessor(ws)
    result = acc.status()
    paths = [s["path"] for s in result]
    assert "/f.txt" in paths

def test_runtime_has_accessors():
    ws = _make_ws()
    rt = MemoryRuntime(ws)
    assert hasattr(rt, "commands")
    assert hasattr(rt, "fuse_ops")
    assert hasattr(rt, "workspace")
    assert "ls" in rt.commands.list()
    assert isinstance(rt.fuse_ops.list(), list)
    assert isinstance(rt.workspace.status(), list)

def test_runtime_register_command_then_run():
    ws = _make_ws()
    rt = MemoryRuntime(ws)
    rt.commands.register("hello", "echo hello")
    assert "hello" in rt.commands.list()


Now let me read a few more files that need attention:

1. `tests/workspace/test_write_ops.py`, `test_init.py`, `test_lock.py` - these all test `mirage/workspace/__init__.py` and need merging
2. `tests/test_types.py` and `tests/types/test_types.py` - need merging
3. `tests/server/test_arg_resolver.py` - needs to move to commands/custom/
4. `tests/observe/test_api.py` - needs to move

Let me read a couple more to understand the splits/merges needed.


     1→import pytest
     2→
     3→from mirage.backend.local.memory import MemoryBackend
     4→from mirage.types import MountMode, SyncPolicy
     5→from mirage.workspace import Workspace
     6→
     7→
     8→def _make_ws(mode=MountMode.WRITE, sync_policy=SyncPolicy.STAGED):
     9→    backend = MemoryBackend()
    10→    return Workspace({"/data/": backend}, mode=mode,

import pytest

from mirage.backend.local.memory import MemoryBackend
from mirage.types import MountMode, SyncPolicy
from mirage.workspace import Workspace

def _make_ws(mode=MountMode.WRITE, sync_policy=SyncPolicy.STAGED):
    backend = MemoryBackend()
    return Workspace({"/data/": backend}, mode=mode,


     1→from datetime import datetime, timezone
     2→
     3→from mirage.backend.local.memory import MemoryBackend
     4→from mirage.types import ConsistencyPolicy, MountMode
     5→from mirage.workspace import Workspace
     6→
     7→
     8→def _make_ws(consistency=ConsistencyPolicy.NONE, ttl_seconds=300):
     9→    backend = MemoryBackend()
    10→    backend.put("/file.txt", b"original")

from datetime import datetime, timezone

from mirage.backend.local.memory import MemoryBackend
from mirage.types import ConsistencyPolicy, MountMode
from mirage.workspace import Workspace

def _make_ws(consistency=ConsistencyPolicy.NONE, ttl_seconds=300):
    backend = MemoryBackend()
    backend.put("/file.txt", b"original")


     1→import pytest
     2→
     3→from mirage.backend.local.memory import MemoryBackend
     4→from mirage.types import MountMode, SyncPolicy
     5→from mirage.workspace import Workspace
     6→
     7→
     8→def test_lock_paths_blocks_write():
     9→    ws = Workspace({"/data/": MemoryBackend()},
    10→                   mode=MountMode.WRITE,

import pytest

from mirage.backend.local.memory import MemoryBackend
from mirage.types import MountMode, SyncPolicy
from mirage.workspace import Workspace

def test_lock_paths_blocks_write():
    ws = Workspace({"/data/": MemoryBackend()},
                   mode=MountMode.WRITE,


     1→import pytest
     2→from pydantic import ValidationError
     3→
     4→from mirage.types import (CommandStats, FileStat, Inode, SyncResult,
     5→                          TransferStats)
     6→
     7→
     8→def test_filestat_defaults():
     9→    fs = FileStat(name="foo.txt")
    10→    assert fs.name == "foo.txt"
    11→    assert fs.size is None
    12→    assert fs.extra == {}
    13→
    14→
    15→def test_filestat_immutable():
    16→    fs = FileStat(name="foo.txt")
    17→    with pytest.raises(ValidationError):
    18→        fs.name = "bar.txt"
    19→
    20→
    21→def test_inode_defaults():
    22→    inode = Inode(virtual_path="/a/b.txt", backend_mount="/a/")
    23→    assert inode.dirty is False
    24→    assert inode.checksum is None
    25→
    26→
    27→def test_inode_mutable():
    28→    inode = Inode(virtual_path="/a/b.txt", backend_mount="/a/")
    29→    inode.dirty = True
    30→    assert inode.dirty is True
    31→
    32→
    33→def test_syncresult_fields():
    34→    r = SyncResult(path="/a/b.txt", op="create", status="ok")
    35→    assert r.op == "create"
    36→    assert r.duration_ms == 0
    37→
    38→
    39→def test_syncresult_immutable():
    40→    r = SyncResult(path="/a/b.txt", op="create", status="ok")
    41→    with pytest.raises(ValidationError):
    42→        r.status = "error"
    43→
    44→
    45→def test_transferstats_mutable():
    46→    ts = TransferStats()
    47→    ts.bytes_read += 100
    48→    assert ts.bytes_read == 100
    49→
    50→
    51→def test_commandstats_defaults():
    52→    cs = CommandStats()
    53→    assert cs.total_commands == 0
    54→    assert cs.command_counts == {}
    55→

import pytest
from pydantic import ValidationError

from mirage.types import (CommandStats, FileStat, Inode, SyncResult,
                          TransferStats)

def test_filestat_defaults():
    fs = FileStat(name="foo.txt")
    assert fs.name == "foo.txt"
    assert fs.size is None
    assert fs.extra == {}

def test_filestat_immutable():
    fs = FileStat(name="foo.txt")
    with pytest.raises(ValidationError):
        fs.name = "bar.txt"

def test_inode_defaults():
    inode = Inode(virtual_path="/a/b.txt", backend_mount="/a/")
    assert inode.dirty is False
    assert inode.checksum is None

def test_inode_mutable():
    inode = Inode(virtual_path="/a/b.txt", backend_mount="/a/")
    inode.dirty = True
    assert inode.dirty is True

def test_syncresult_fields():
    r = SyncResult(path="/a/b.txt", op="create", status="ok")
    assert r.op == "create"
    assert r.duration_ms == 0

def test_syncresult_immutable():
    r = SyncResult(path="/a/b.txt", op="create", status="ok")
    with pytest.raises(ValidationError):
        r.status = "error"

def test_transferstats_mutable():
    ts = TransferStats()
    ts.bytes_read += 100
    assert ts.bytes_read == 100

def test_commandstats_defaults():
    cs = CommandStats()
    assert cs.total_commands == 0
    assert cs.command_counts == {}


     1→from mirage.types import (BackendType, CommandResult, FilePayload,
     2→                          RemoteCommandOptions)
     3→
     4→
     5→def test_command_result_defaults():
     6→    r = CommandResult(stdout="hello")
     7→    assert r.stdout == "hello"
     8→    assert r.output_files == {}
     9→
    10→
    11→def test_command_result_with_files():
    12→    r = CommandResult(stdout="done", output_files={"/out.csv": b"a,b"})
    13→    assert r.output_files["/out.csv"] == b"a,b"
    14→
    15→
    16→def test_file_payload_with_data():
    17→    p = FilePayload(checksum="abc123", data=b"hello")
    18→    assert p.checksum == "abc123"
    19→    assert p.data == b"hello"
    20→
    21→
    22→def test_file_payload_checksum_only():
    23→    p = FilePayload(checksum="abc123")
    24→    assert p.data is None
    25→
    26→
    27→def test_remote_command_options_defaults():
    28→    opts = RemoteCommandOptions()
    29→    assert opts.max_file_size == "100MB"
    30→    assert opts.cache == "enabled"
    31→
    32→
    33→def test_remote_command_options_custom():
    34→    opts = RemoteCommandOptions(max_file_size="500MB", cache="disabled")
    35→    assert opts.max_file_size == "500MB"
    36→    assert opts.cache == "disabled"
    37→
    38→
    39→def test_remote_command_options_only_two_fields():
    40→    opts = RemoteCommandOptions()
    41→    assert opts.max_file_size == "100MB"
    42→    assert opts.cache == "enabled"
    43→    assert set(RemoteCommandOptions.model_fields.keys()) == {
    44→        "max_file_size", "cache"
    45→    }
    46→
    47→
    48→def test_backend_type_values():
    49→    assert BackendType.MEMORY == "memory"
    50→    assert BackendType.LOCAL == "local"
    51→    assert BackendType.S3 == "s3"
    52→    assert BackendType.R2 == "r2"
    53→    assert BackendType.HTTP == "http"
    54→    assert BackendType.HTTPS == "https"
    55→
    56→
    57→def test_backend_type_string_equality():
    58→    assert BackendType.MEMORY.value == "memory"
    59→    assert BackendType.LOCAL.value == "local"
    60→    assert BackendType.S3.value == "s3"
    61→    assert BackendType.R2.value == "r2"
    62→    assert BackendType.HTTP.value == "http"
    63→    assert BackendType.HTTPS.value == "https"
    64→    assert BackendType("memory") is BackendType.MEMORY
    65→    assert BackendType("s3") is BackendType.S3
    66→
    67→
    68→def test_backend_type_members():
    69→    assert set(BackendType) == {
    70→        BackendType.MEMORY,
    71→        BackendType.LOCAL,
    72→        BackendType.S3,
    73→        BackendType.R2,
    74→        BackendType.HTTP,
    75→        BackendType.HTTPS,
    76→    }
    77→

from mirage.types import (BackendType, CommandResult, FilePayload,
                          RemoteCommandOptions)

def test_command_result_defaults():
    r = CommandResult(stdout="hello")
    assert r.stdout == "hello"
    assert r.output_files == {}

def test_command_result_with_files():
    r = CommandResult(stdout="done", output_files={"/out.csv": b"a,b"})
    assert r.output_files["/out.csv"] == b"a,b"

def test_file_payload_with_data():
    p = FilePayload(checksum="abc123", data=b"hello")
    assert p.checksum == "abc123"
    assert p.data == b"hello"

def test_file_payload_checksum_only():
    p = FilePayload(checksum="abc123")
    assert p.data is None

def test_remote_command_options_defaults():
    opts = RemoteCommandOptions()
    assert opts.max_file_size == "100MB"
    assert opts.cache == "enabled"

def test_remote_command_options_custom():
    opts = RemoteCommandOptions(max_file_size="500MB", cache="disabled")
    assert opts.max_file_size == "500MB"
    assert opts.cache == "disabled"

def test_remote_command_options_only_two_fields():
    opts = RemoteCommandOptions()
    assert opts.max_file_size == "100MB"
    assert opts.cache == "enabled"
    assert set(RemoteCommandOptions.model_fields.keys()) == {
        "max_file_size", "cache"
    }

def test_backend_type_values():
    assert BackendType.MEMORY == "memory"
    assert BackendType.LOCAL == "local"
    assert BackendType.S3 == "s3"
    assert BackendType.R2 == "r2"
    assert BackendType.HTTP == "http"
    assert BackendType.HTTPS == "https"

def test_backend_type_string_equality():
    assert BackendType.MEMORY.value == "memory"
    assert BackendType.LOCAL.value == "local"
    assert BackendType.S3.value == "s3"
    assert BackendType.R2.value == "r2"
    assert BackendType.HTTP.value == "http"
    assert BackendType.HTTPS.value == "https"
    assert BackendType("memory") is BackendType.MEMORY
    assert BackendType("s3") is BackendType.S3

def test_backend_type_members():
    assert set(BackendType) == {
        BackendType.MEMORY,
        BackendType.LOCAL,
        BackendType.S3,
        BackendType.R2,
        BackendType.HTTP,
        BackendType.HTTPS,
    }


     1→from mirage.backend.local.memory import MemoryBackend
     2→from mirage.commands.custom.arg_resolver import classify_args
     3→from mirage.types import MountMode, SyncPolicy
     4→from mirage.workspace import Workspace
     5→

from mirage.backend.local.memory import MemoryBackend
from mirage.commands.custom.arg_resolver import classify_args
from mirage.types import MountMode, SyncPolicy
from mirage.workspace import Workspace


Now I have a comprehensive understanding. Let me write the final plan file. Let me organize everything clearly.

The rule: `mirage/a/b/c.py` → `tests/a/b/test_c.py`

Here's my complete mapping:

## Moves (simple relocations, no content changes needed)

### tests/backend/
1. `tests/backend/test_memory.py` → `tests/backend/local/test_memory.py`
2. `tests/backend/test_local.py` → `tests/backend/local/test_local.py`
3. `tests/backend/test_proxy.py` → `tests/backend/network/test_proxy.py`

4. `tests/backend/test_loader.py` → stays ✓

### tests/fuse/
5. `tests/fuse/test_remote.py` → `tests/fuse/remote/test_remote.py`
6. `tests/fuse/test_fs.py` → stays ✓
7. `tests/fuse/test_loader.py` → stays ✓
8. `tests/fuse/test_registry.py` → stays ✓

### tests/commands/
9. `tests/commands/test_handlers.py` → `tests/commands/builtin/test_handlers.py`

The remaining command tests follow the same pattern—the builtin command tests move into a `builtin/` subdirectory while the remote client test goes into `remote/`, and the init test stays in place. The server tests also need reorganization, with the FastAPI app test moving into a `fastapi/` subdirectory.

Now I'm mapping out the rest of the server tests: the IO router test moves into `fastapi/routers/`, the file cache test becomes `service/test_cache.py`, and the service test becomes `service/test_local.py` to match the source module name. The conftest fixtures move to `fastapi/` since they support the tests that are relocating there, and the arg resolver test moves into the commands structure under `custom/`.

For the runtime tests, most stay in place except the runtime test itself gets renamed to `test_base.py` and the accessors test moves into its own subdirectory. The observe tests are mostly stable with just the API test moving into `fastapi/` and getting renamed to `test_router.py`.

Now I'm looking at how to split `tests/backend/test_configs.py` — the S3 and R2 config tests need to move into their own files under `object_storage/`, while the external command tests should go to the commands registry tests, though there's some overlap to consider there.

For the FUSE tests, I'm splitting `test_default.py` so the DefaultFuseOp and write/flush tests stay in `backend/test_default.py` while the MemoryFuseOp test moves to `backend/local/test_memory.py`. The format ops tests are getting split by filetype — Parquet tests to their own file and CSV tests to theirs.

The server registry tests need some reorganization too, with the size units test moving to a constants file since that's where the source code lives, and there are a couple more splits to handle there.

Actually, I'm realizing the CommandRegistry and ExternalCommand tests should all go together in `tests/commands/custom/test_registry.py` since they're testing the same module, and the size units test is the only one that should move to the constants file.

Now looking at the merges needed: I'm consolidating the types tests into a single file and removing the types subdirectory, merging the three workspace-related test files since they all test the Workspace class, and creating the necessary `__init__.py` files for the new backend and commands subdirectories.

I also need to create `__init__.py` files for the fuse, server, runtime, and observe test directories and their subdirectories, then clean up the old test files once everything's been consolidated.


Now I have all the information needed. Let me write the final plan.


Write

# 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)


File created successfully at: /Users/agent/.claude/plans/shimmering-leaping-wozniak.md

# 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

# 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)


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:
# 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)


The plan is approved. Let me start implementing. I'll set up the todo list and begin execution.


Plan approved. Let me start implementing.


TodoWrite

Create 15 new directories with __init__.py files

Execute 15 pure git mv moves

Execute 3 renames (git mv)

Split test_configs.py into object_storage tests

Split test_default.py into fuse/backend tests

Split test_format_ops.py into csv and parquet tests

Split server/test_registry.py into commands/custom + server/constants

Merge types test files into tests/test_types.py

Merge workspace test files into tests/workspace/test_init.py

Cleanup empty directories and stale files

Run tests and pre-commit to verify