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


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


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


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


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


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


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


     1→from unittest.mock import MagicMock
     2→
     3→from mirage.backends.memory import MemoryBackend
     4→from mirage.commands.custom.registry import CommandRegistry, ExternalCommand
     5→from mirage.commands.remote.client import CommandClient
     6→from mirage.types import CommandResult, MountMode, SyncPolicy
     7→from mirage.workspace import Workspace
     8→
     9→
    10→def _fake_run_transform(command, cwd="/", stdin=None, files=None):
    11→    assert files is not None
    12→    assert "/data/input.json" in files
    13→    input_data = files["/data/input.json"].data
    14→    processed = input_data.upper()
    15→    return CommandResult(
    16→        stdout="processed 1 file",
    17→        output_files={"/data/output.csv": processed},
    18→    )
    19→
    20→
    21→def test_full_round_trip_file_transfer():
    22→    mock_client = MagicMock(spec=CommandClient)
    23→    mock_client.run.side_effect = _fake_run_transform
    24→
    25→    ws = Workspace(
    26→        {"/data/": MemoryBackend()},
    27→        mode=MountMode.WRITE,
    28→        sync_policy=SyncPolicy.IMMEDIATE,
    29→    )
    30→
    31→    ws.write("/data/input.json", b'{"name": "test"}')
    32→
    33→    reg = CommandRegistry(client=mock_client)
    34→    reg.register(
    35→        ExternalCommand(
    36→            name="transform",
    37→            exec="transform {file} -o {out}",
    38→        ))
    39→
    40→    result = reg.run(ws,
    41→                     "transform /data/input.json /data/output.csv",
    42→                     cwd="/")
    43→    assert "processed" in result
    44→
    45→    output = ws.cat("/data/output.csv")
    46→    assert output == b'{"NAME": "TEST"}'
    47→
    48→
    49→def test_full_round_trip_pipe_mode():
    50→    mock_client = MagicMock(spec=CommandClient)
    51→    mock_client.run.return_value = CommandResult(stdout="HELLO WORLD")
    52→
    53→    ws = Workspace({"/data/": MemoryBackend()})
    54→    reg = CommandRegistry(client=mock_client)
    55→    reg.register(ExternalCommand(name="remote-upper", exec="upper"))
    56→
    57→    result = reg.run(ws, "remote-upper", cwd="/", stdin=b"hello world")
    58→    assert result == "HELLO WORLD"
    59→
    60→    call_kwargs = mock_client.run.call_args
    61→    assert call_kwargs.kwargs["stdin"] == b"hello world"
    62→
    63→
    64→def test_mixed_local_and_remote_commands():
    65→    mock_client = MagicMock(spec=CommandClient)
    66→    mock_client.run.return_value = CommandResult(
    67→        stdout="ok",
    68→        output_files={"/data/summary.txt": b"summary content"},
    69→    )
    70→
    71→    ws = Workspace(
    72→        {"/data/": MemoryBackend()},
    73→        mode=MountMode.WRITE,
    74→        sync_policy=SyncPolicy.IMMEDIATE,
    75→    )
    76→
    77→    reg = CommandRegistry(client=mock_client)
    78→    reg.register(
    79→        ExternalCommand(name="summarize", exec="summarize {file} -o {out}"))
    80→
    81→    ws.write("/data/raw.txt", b"hello")
    82→
    83→    reg.run(ws, "summarize /data/raw.txt /data/summary.txt", cwd="/")
    84→
    85→    content = ws.cat("/data/summary.txt")
    86→    assert content == b"summary content"
    87→
    88→
    89→def test_checksum_sent_with_files():
    90→    mock_client = MagicMock(spec=CommandClient)
    91→    mock_client.run.return_value = CommandResult(stdout="ok")
    92→
    93→    ws = Workspace(
    94→        {"/data/": MemoryBackend()},
    95→        mode=MountMode.WRITE,
    96→        sync_policy=SyncPolicy.IMMEDIATE,
    97→    )
    98→    ws.write("/data/input.txt", b"test data")
    99→
   100→    reg = CommandRegistry(client=mock_client)
   101→    reg.register(ExternalCommand(name="process", exec="process {file}"))
   102→
   103→    reg.run(ws, "process /data/input.txt", cwd="/")
   104→
   105→    call_kwargs = mock_client.run.call_args
   106→    files = call_kwargs.kwargs["files"]
   107→    payload = files["/data/input.txt"]
   108→    assert payload.checksum is not None
   109→    assert len(payload.checksum) == 32
   110→    assert payload.data == b"test data"
   111→
   112→
   113→def test_relative_path_resolution():
   114→    mock_client = MagicMock(spec=CommandClient)
   115→    mock_client.run.return_value = CommandResult(stdout="ok")
   116→
   117→    ws = Workspace(
   118→        {"/data/": MemoryBackend()},
   119→        mode=MountMode.WRITE,
   120→        sync_policy=SyncPolicy.IMMEDIATE,
   121→    )
   122→    ws.write("/data/input.txt", b"content")
   123→
   124→    reg = CommandRegistry(client=mock_client)
   125→    reg.register(ExternalCommand(name="process", exec="process {file}"))
   126→
   127→    reg.run(ws, "process /data/input.txt", cwd="/data")
   128→
   129→    call_kwargs = mock_client.run.call_args
   130→    files = call_kwargs.kwargs["files"]
   131→    assert "/data/input.txt" in files
   132→


     1→import pytest
     2→
     3→from mirage.backends.loader import load_backend_class
     4→
     5→
     6→def test_load_from_module():
     7→    cls = load_backend_class("mirage.backends.memory:MemoryBackend")
     8→    from mirage.backends.memory import MemoryBackend
     9→    assert cls is MemoryBackend
    10→
    11→
    12→def test_load_from_script_file(tmp_path):
    13→    script = tmp_path / "custom_backend.py"
    14→    script.write_text("from mirage.backends.memory import MemoryBackend\n"
    15→                      "class CustomBackend(MemoryBackend):\n"
    16→                      "    pass\n")
    17→    cls = load_backend_class(f"{script}:CustomBackend")
    18→    assert cls.__name__ == "CustomBackend"
    19→    instance = cls()
    20→    instance.put("/test.txt", b"hello")
    21→    assert instance.cat("/test.txt") == b"hello"
    22→
    23→
    24→def test_load_invalid_spec_no_colon():
    25→    with pytest.raises(ValueError, match="invalid backend spec"):
    26→        load_backend_class("mirage.backends.memory")
    27→
    28→
    29→def test_load_missing_file():
    30→    with pytest.raises(ValueError, match="cannot load script"):
    31→        load_backend_class("/nonexistent/path.py:Cls")
    32→
    33→
    34→def test_load_missing_class():
    35→    with pytest.raises(ValueError, match="not found"):
    36→        load_backend_class("mirage.backends.memory:DoesNotExist")
    37→


Edit

from mirage.backends.s3 import S3Backend, S3Config

from mirage.backends.object_storage.s3 import S3Backend, S3Config


     1→import pytest
     2→
     3→from mirage.backends.local.local import LocalBackend
     4→from mirage.backends.memory import MemoryBackend
     5→from mirage.cli.config import (load_workspace_yaml, load_yaml_config,
     6→                               parse_mount_uri, parse_mount_uri_full,
     7→                               parse_mount_uri_with_mode,
     8→                               parse_mounts_from_cli, split_mount_spec)
     9→from mirage.types import ConsistencyPolicy, MountMode
    10→
    11→
    12→def test_parse_memory_uri():
    13→    backend = parse_mount_uri("memory")
    14→    assert isinstance(backend, MemoryBackend)
    15→
    16→
    17→def test_parse_local_uri():
    18→    backend = parse_mount_uri("local:./scratch")
    19→    assert isinstance(backend, LocalBackend)
    20→
    21→
    22→def test_parse_local_uri_relative_path():
    23→    backend = parse_mount_uri("./scratch")
    24→    assert isinstance(backend, LocalBackend)
    25→
    26→
    27→def test_parse_local_uri_absolute_path():
    28→    backend = parse_mount_uri("/tmp/mydir")
    29→    assert isinstance(backend, LocalBackend)
    30→
    31→
    32→def test_parse_unknown_scheme_raises():
    33→    with pytest.raises(ValueError, match="Unknown URI scheme"):
    34→        parse_mount_uri("memory://")
    35→
    36→
    37→def test_parse_mount_cli_args():
    38→    mounts = parse_mounts_from_cli(
    39→        ["memory:/data/", "local:./scratch:/local/"])
    40→    assert "/data/" in mounts
    41→    assert "/local/" in mounts
    42→    assert isinstance(mounts["/data/"], MemoryBackend)
    43→    assert isinstance(mounts["/local/"], LocalBackend)
    44→
    45→
    46→def test_load_yaml_config(tmp_path):
    47→    cfg_file = tmp_path / "ws.yaml"
    48→    cfg_file.write_text(
    49→        "mounts:\n  /data/: memory\n  /local/: local:./scratch\n"
    50→        "sync_policy: immediate\nmode: write\n")
    51→    cfg = load_yaml_config(str(cfg_file))
    52→    assert "/data/" in cfg["mounts"]
    53→    assert cfg["sync_policy"] == "immediate"
    54→    assert cfg["mode"] == "write"
    55→
    56→
    57→def test_parse_mount_uri_with_mode_read():
    58→    backend, mode = parse_mount_uri_with_mode("memory@read")
    59→    assert isinstance(backend, MemoryBackend)
    60→    assert mode == MountMode.READ
    61→
    62→
    63→def test_parse_mount_uri_with_mode_write():
    64→    backend, mode = parse_mount_uri_with_mode("memory@write")
    65→    assert isinstance(backend, MemoryBackend)
    66→    assert mode == MountMode.WRITE
    67→
    68→
    69→def test_parse_mount_uri_with_mode_default():
    70→    backend, mode = parse_mount_uri_with_mode("memory")
    71→    assert isinstance(backend, MemoryBackend)
    72→    assert mode == MountMode.READ
    73→
    74→
    75→def test_parse_mounts_from_cli_per_mount_mode():
    76→    mounts = parse_mounts_from_cli(
    77→        ["memory@write:/a/", "memory:/b/"],
    78→        default_mode=MountMode.READ,
    79→    )
    80→    assert isinstance(mounts["/a/"], tuple)
    81→    backend, mode = mounts["/a/"]
    82→    assert isinstance(backend, MemoryBackend)
    83→    assert mode == MountMode.WRITE
    84→    assert isinstance(mounts["/b/"], MemoryBackend)
    85→
    86→
    87→def test_split_mount_spec_memory():
    88→    backend, path = split_mount_spec("memory:/data/")
    89→    assert backend == "memory"
    90→    assert path == "/data/"
    91→
    92→
    93→def test_split_mount_spec_s3():
    94→    backend, path = split_mount_spec("s3://bucket:/")
    95→    assert backend == "s3://bucket"
    96→    assert path == "/"
    97→
    98→
    99→def test_split_mount_spec_with_mode():
   100→    backend, path = split_mount_spec("memory@write:/a/")
   101→    assert backend == "memory@write"
   102→    assert path == "/a/"
   103→
   104→
   105→def test_split_mount_spec_invalid():
   106→    with pytest.raises(ValueError, match="Invalid mount spec"):
   107→        split_mount_spec("nocolon")
   108→
   109→
   110→def test_load_workspace_yaml_per_mount_mode(tmp_path):
   111→    cfg_file = tmp_path / "ws.yaml"
   112→    cfg_file.write_text("mounts:\n"
   113→                        "  /data/: memory\n"
   114→                        "  /scratch/:\n"
   115→                        "    uri: memory\n"
   116→                        "    mode: write\n")
   117→    cfg = load_workspace_yaml(str(cfg_file))
   118→    assert cfg["mounts"]["/data/"] == "memory"
   119→    assert cfg["mounts"]["/scratch/"] == {"uri": "memory", "mode": "write"}
   120→
   121→
   122→def test_parse_mount_uri_full_lazy():
   123→    backend, mode, consistency = parse_mount_uri_full("memory@write#lazy")
   124→    assert isinstance(backend, MemoryBackend)
   125→    assert mode == MountMode.WRITE
   126→    assert consistency == ConsistencyPolicy.LAZY
   127→
   128→
   129→def test_parse_mount_uri_full_default():
   130→    backend, mode, consistency = parse_mount_uri_full("memory")
   131→    assert isinstance(backend, MemoryBackend)
   132→    assert mode == MountMode.READ
   133→    assert consistency == ConsistencyPolicy.NONE
   134→
   135→
   136→def test_load_workspace_yaml_consistency(tmp_path):
   137→    cfg_file = tmp_path / "ws.yaml"
   138→    cfg_file.write_text("consistency: lazy\n"
   139→                        "ttl: 60\n"
   140→                        "mounts:\n"
   141→                        "  /data/: memory\n"
   142→                        "  /scratch/:\n"
   143→                        "    uri: memory\n"
   144→                        "    consistency: always\n")
   145→    cfg = load_workspace_yaml(str(cfg_file))
   146→    assert cfg["consistency"] == "lazy"
   147→    assert cfg["ttl"] == 60
   148→    assert cfg["mounts"]["/data/"] == "memory"
   149→    assert cfg["mounts"]["/scratch/"] == {
   150→        "uri": "memory",
   151→        "consistency": "always",
   152→    }
   153→
   154→
   155→def test_load_workspace_yaml_custom_backend(tmp_path):
   156→    cfg_file = tmp_path / "ws.yaml"
   157→    cfg_file.write_text("mounts:\n"
   158→                        "  /data/:\n"
   159→                        "    backend: './my_backend.py:MyBackend'\n"
   160→                        "    args:\n"
   161→                        "      bucket: my-bucket\n"
   162→                        "    mode: write\n"
   163→                        "    consistency: lazy\n")
   164→    cfg = load_workspace_yaml(str(cfg_file))
   165→    mount = cfg["mounts"]["/data/"]
   166→    assert mount["backend"] == "./my_backend.py:MyBackend"
   167→    assert mount["args"] == {"bucket": "my-bucket"}
   168→    assert mount["mode"] == "write"
   169→    assert mount["consistency"] == "lazy"
   170→
   171→
   172→def test_load_workspace_yaml_custom_backend_minimal(tmp_path):
   173→    cfg_file = tmp_path / "ws.yaml"
   174→    cfg_file.write_text("mounts:\n"
   175→                        "  /data/:\n"
   176→                        "    backend: 'mypackage:MyBackend'\n")
   177→    cfg = load_workspace_yaml(str(cfg_file))
   178→    mount = cfg["mounts"]["/data/"]
   179→    assert mount == {"backend": "mypackage:MyBackend"}
   180→
   181→
   182→def test_load_workspace_yaml_with_fuse_ops(tmp_path):
   183→    cfg_file = tmp_path / "ws.yaml"
   184→    cfg_file.write_text("mounts:\n"
   185→                        "  /data/: memory\n"
   186→                        "fuse_ops:\n"
   187→                        "  '*.parquet': parquet\n"
   188→                        "  '*.csv': csv\n")
   189→    cfg = load_workspace_yaml(str(cfg_file))
   190→    assert "fuse_ops" in cfg
   191→    assert cfg["fuse_ops"]["*.parquet"] == "parquet"
   192→    assert cfg["fuse_ops"]["*.csv"] == "csv"
   193→
   194→
   195→def test_load_workspace_yaml_no_fuse_ops(tmp_path):
   196→    cfg_file = tmp_path / "ws.yaml"
   197→    cfg_file.write_text("mounts:\n  /data/: memory\n")
   198→    cfg = load_workspace_yaml(str(cfg_file))
   199→    assert "fuse_ops" not in cfg
   200→
   201→
   202→def test_load_workspace_yaml_remote(tmp_path):
   203→    cfg_file = tmp_path / "ws.yaml"
   204→    cfg_file.write_text("runtime: remote\n"
   205→                        "url: http://server:8000\n"
   206→                        "ws_id: abc12345\n"
   207→                        "mounts: {}\n")
   208→    cfg = load_workspace_yaml(str(cfg_file))
   209→    assert cfg["runtime"] == "remote"
   210→    assert cfg["url"] == "http://server:8000"
   211→    assert cfg["ws_id"] == "abc12345"
   212→
   213→
   214→def test_load_workspace_yaml_remote_with_command_url(tmp_path):
   215→    cfg_file = tmp_path / "ws.yaml"
   216→    cfg_file.write_text("runtime: remote\n"
   217→                        "url: http://data:8000\n"
   218→                        "ws_id: abc12345\n"
   219→                        "command_url: http://cmd:8001\n"
   220→                        "fuse_ops_url: http://ops:8002\n"
   221→                        "mounts: {}\n")
   222→    cfg = load_workspace_yaml(str(cfg_file))
   223→    assert cfg["runtime"] == "remote"
   224→    assert cfg["url"] == "http://data:8000"
   225→    assert cfg["command_url"] == "http://cmd:8001"
   226→    assert cfg["fuse_ops_url"] == "http://ops:8002"
   227→


from mirage.backends.r2 import R2Backend, R2Config

from mirage.backends.object_storage.r2 import R2Backend, R2Config


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


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


from mirage.backends.http import HTTPBackend, HTTPConfig

from mirage.backends.network.http import HTTPBackend, HTTPConfig


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


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


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


from mirage.backends.proxy import ProxyBackend

from mirage.backends.network.proxy import ProxyBackend


The file /Users/agent/strukto/mirage/mirage/cli/commands/workspace.py has been updated successfully.


from mirage.backends.memory import _guess_type

from mirage.backends.local.memory import _guess_type


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


from mirage.backends.local import LocalBackend

from mirage.backends.local.local import LocalBackend


from mirage.backends.memory import MemoryBackend

from mirage.backends.local.memory import MemoryBackend


The file /Users/agent/strukto/mirage/tests/fuse_ops/test_registry.py has been updated. All occurrences of '    from mirage.backends.local import LocalBackend' were successfully replaced with '    from mirage.backends.local.local import LocalBackend'.


from mirage.backends.memory import MemoryBackend

from mirage.backends.local.memory import MemoryBackend


from mirage.backends.memory import MemoryBackend

from mirage.backends.local.memory import MemoryBackend


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


<tool_use_error>File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.</tool_use_error>

Error: File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.


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


from mirage.backends.memory import MemoryBackend

from mirage.backends.local.memory import MemoryBackend


from mirage.backends.memory import MemoryBackend

from mirage.backends.local.memory import MemoryBackend


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


from mirage.backends.memory import MemoryBackend

from mirage.backends.local.memory import MemoryBackend


The file /Users/agent/strukto/mirage/tests/fuse_ops/test_remote.py has been updated successfully.


from mirage.backends.memory import MemoryBackend

from mirage.backends.local.memory import MemoryBackend


The file /Users/agent/strukto/mirage/tests/fuse_ops/test_default.py has been updated successfully.


from mirage.backends.memory import MemoryBackend

from mirage.backends.local.memory import MemoryBackend


The file /Users/agent/strukto/mirage/tests/fuse_ops/test_format_ops.py has been updated successfully.


from mirage.backends.memory import MemoryBackend

from mirage.backends.local.memory import MemoryBackend


The file /Users/agent/strukto/mirage/tests/commands/test_init.py has been updated successfully.


from mirage.backends.memory import MemoryBackend

from mirage.backends.local.memory import MemoryBackend


from mirage.backends.memory import MemoryBackend

from mirage.backends.local.memory import MemoryBackend


The file /Users/agent/strukto/mirage/tests/runtime/test_memory.py has been updated successfully.


from mirage.backends.memory import MemoryBackend

from mirage.backends.local.memory import MemoryBackend


The file /Users/agent/strukto/mirage/tests/runtime/test_accessors.py has been updated successfully.


from mirage.backends.memory import MemoryBackend

from mirage.backends.local.memory import MemoryBackend


The file /Users/agent/strukto/mirage/tests/runtime/test_runtime.py has been updated successfully.


from mirage.backends.memory import MemoryBackend

from mirage.backends.local.memory import MemoryBackend


The file /Users/agent/strukto/mirage/tests/server/test_registry.py has been updated successfully.


from mirage.backends.memory import MemoryBackend

from mirage.backends.local.memory import MemoryBackend


The file /Users/agent/strukto/mirage/tests/server/test_arg_resolver.py has been updated successfully.


from mirage.backends.memory import MemoryBackend

from mirage.backends.local.memory import MemoryBackend


The file /Users/agent/strukto/mirage/tests/workspace/test_mount.py has been updated successfully.


from mirage.backends.memory import MemoryBackend

from mirage.backends.local.memory import MemoryBackend


The file /Users/agent/strukto/mirage/tests/workspace/test_write_ops.py has been updated successfully.


from mirage.backends.memory import MemoryBackend

from mirage.backends.local.memory import MemoryBackend


The file /Users/agent/strukto/mirage/tests/workspace/test_sync.py has been updated successfully.


from mirage.backends.memory import MemoryBackend

from mirage.backends.local.memory import MemoryBackend


The file /Users/agent/strukto/mirage/tests/workspace/test_init.py has been updated successfully.


from mirage.backends.memory import MemoryBackend

from mirage.backends.local.memory import MemoryBackend


The file /Users/agent/strukto/mirage/tests/workspace/test_lock.py has been updated successfully.


from mirage.backends.memory import MemoryBackend

from mirage.backends.local.memory import MemoryBackend


The file /Users/agent/strukto/mirage/tests/integration/test_remote_transfer.py has been updated successfully.


from mirage.backends.memory import MemoryBackend

from mirage.backends.local.memory import MemoryBackend


from mirage.backends.memory import MemoryBackend

from mirage.backends.local.memory import MemoryBackend


The file /Users/agent/strukto/mirage/tests/cli/test_config.py has been updated successfully.


The file /Users/agent/strukto/mirage/fuse/tests/test_fs.py has been updated successfully.


The file /Users/agent/strukto/mirage/examples/claude-memory.py has been updated successfully.


The file /Users/agent/strukto/mirage/examples/claude-sdk.py has been updated successfully.


     1→import os
     2→
     3→import yaml
     4→
     5→from mirage.backends.base import BaseBackend
     6→from mirage.backends.local.local import LocalBackend
     7→from mirage.backends.memory import MemoryBackend
     8→from mirage.types import BackendType, ConsistencyPolicy, MountMode
     9→
    10→


from mirage.backends.memory import MemoryBackend

from mirage.backends.local.memory import MemoryBackend


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


from mirage.backends.memory import MemoryBackend

from mirage.backends.local.memory import MemoryBackend


<tool_use_error>File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.</tool_use_error>


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


from mirage.backends.memory import MemoryBackend

from mirage.backends.local.memory import MemoryBackend


The file /Users/agent/strukto/mirage/tests/fuse_ops/test_registry.py has been updated. All occurrences of 'from mirage.backends.memory import MemoryBackend' were successfully replaced with 'from mirage.backends.local.memory import MemoryBackend'.


The file /Users/agent/strukto/mirage/tests/backends/test_loader.py has been updated. All occurrences of 'from mirage.backends.memory import' were successfully replaced with 'from mirage.backends.local.memory import'.


from mirage.backends.memory import MemoryBackend

from mirage.backends.local.memory import MemoryBackend


The file /Users/agent/strukto/mirage/fuse/README.md has been updated successfully.


Found 2 files
tests/observe/test_inspector.py
mirage/backends/__init__.py


     1→from dataclasses import dataclass, field
     2→
     3→from mirage.backends.memory import MemoryBackend
     4→from mirage.commands import COMMANDS
     5→from mirage.commands.custom.registry import CommandRegistry
     6→from mirage.observe import inspector
     7→from mirage.types import CommandStats, MountMode, SyncPolicy
     8→from mirage.workspace import Workspace
     9→
    10→
    11→@dataclass
    12→class _FakeSession:
    13→    id: str = "test-ws"
    14→    ws: Workspace | None = None
    15→    registry: CommandRegistry | None = None
    16→    cwd: str = "/"
    17→    fuse_mounted: bool = False
    18→    mode: str = "write"
    19→    mountpoint: str | None = None
    20→    pid: int | None = None
    21→    mounts: list[dict] = field(default_factory=list)
    22→    command_stats: CommandStats = field(default_factory=CommandStats)
    23→
    24→
    25→def _make_session() -> _FakeSession:
    26→    ws = Workspace(
    27→        {"/tmp/": MemoryBackend()},
    28→        mode=MountMode.WRITE,
    29→        sync_policy=SyncPolicy.STAGED,
    30→    )
    31→    return _FakeSession(ws=ws, registry=CommandRegistry())
    32→
    33→
    34→def test_global_status():
    35→    session = _make_session()
    36→    sessions = {"ws1": session}
    37→    result = inspector.global_status(sessions)
    38→    assert "rss_bytes" in result
    39→    assert result["total_sessions"] == 1
    40→    assert result["total_cache_bytes"] == 0
    41→    assert "uptime_seconds" in result
    42→
    43→
    44→def test_list_sessions():
    45→    session = _make_session()
    46→    sessions = {"ws1": session}
    47→    result = inspector.list_sessions(sessions)
    48→    assert len(result) == 1
    49→    assert result[0]["id"] == "test-ws"
    50→    assert result[0]["cache_used"] == 0
    51→    assert result[0]["total_commands"] == 0
    52→
    53→
    54→def test_workspace_status():
    55→    session = _make_session()
    56→    session.command_stats.total_commands = 5
    57→    session.command_stats.total_errors = 1
    58→    session.command_stats.last_command = "cat /tmp/test"
    59→    result = inspector.workspace_status(session)
    60→    assert result["cache_used"] == 0
    61→    assert result["dirty_inodes"] == 0
    62→    assert result["total_commands"] == 5
    63→    assert result["total_errors"] == 1
    64→    assert result["last_command"] == "cat /tmp/test"
    65→    assert len(result["backends"]) == 1
    66→    assert result["backends"][0]["backend"] == "MemoryBackend"
    67→
    68→
    69→def test_workspace_commands():
    70→    session = _make_session()
    71→    result = inspector.workspace_commands(session)
    72→    names = {c["name"] for c in result}
    73→    assert "cat" in names
    74→    assert "ls" in names
    75→    assert "grep" in names
    76→    assert len(result) >= len(COMMANDS)
    77→    for cmd in result:
    78→        assert "source" in cmd
    79→        assert "cost_level" in cmd
    80→
    81→
    82→def test_workspace_memory():
    83→    session = _make_session()
    84→    session.ws.write("/tmp/hello.txt", b"hello world")
    85→    result = inspector.workspace_memory(session)
    86→    assert result["cache"]["used"] > 0
    87→    assert result["cache"]["entry_count"] == 1
    88→    assert "/tmp/hello.txt" in result["cache"]["entries"]
    89→    assert result["dirty_tracker"]["total_inodes"] == 1
    90→    assert "/tmp/hello.txt" in result["dirty_tracker"]["dirty_paths"]
    91→    assert "/tmp/" in result["backend_memory"]
    92→    assert result["backend_memory"]["/tmp/"]["file_count"] == 0
    93→
    94→
    95→def test_workspace_memory_with_synced_file():
    96→    ws = Workspace(
    97→        {"/tmp/": MemoryBackend()},
    98→        mode=MountMode.WRITE,
    99→        sync_policy=SyncPolicy.IMMEDIATE,
   100→    )
   101→    session = _FakeSession(ws=ws, registry=CommandRegistry())
   102→    ws.write("/tmp/hello.txt", b"hello")
   103→    result = inspector.workspace_memory(session)
   104→    assert result["backend_memory"]["/tmp/"]["file_count"] == 1
   105→
   106→
   107→def test_workspace_transfers():
   108→    session = _make_session()
   109→    session.ws.write("/tmp/a.txt", b"data")
   110→    session.ws.cat("/tmp/a.txt")
   111→    session.ws.cat("/tmp/a.txt")
   112→    result = inspector.workspace_transfers(session)
   113→    assert "/tmp/" in result
   114→    stats = result["/tmp/"]
   115→    assert stats["write_ops"] >= 1
   116→    assert stats["read_ops"] >= 2
   117→    assert stats["bytes_read"] > 0
   118→    assert stats["cache_hit_rate"] > 0
   119→


from mirage.backends.memory import MemoryBackend

from mirage.backends.local.memory import MemoryBackend


The file /Users/agent/strukto/mirage/tests/observe/test_inspector.py has been updated successfully.


Found 1 file
mirage/backends/__init__.py


Why we use _guess_type? What's that for? any improvement for that?

because in the future we may need to handle unmet file extension


The user is asking about `_guess_type` - what it does, and whether we can improve it. Let me read the implementation.


mirage/workspace/__init__.py:6:from mirage.backends.local.memory import _guess_type
mirage/workspace/__init__.py:193:                type=_guess_type(path),
mirage/backends/object_storage/s3.py:7:from mirage.backends.local.memory import _guess_type
mirage/backends/object_storage/s3.py:140:                type=_guess_type(path),
mirage/backends/object_storage/s3.py:252:                                 type=_guess_type(name)))
mirage/backends/network/http.py:4:from mirage.backends.local.memory import _guess_type
mirage/backends/network/http.py:88:            type=_guess_type(path),
mirage/backends/local/memory.py:81:                type=_guess_type(p),
mirage/backends/local/memory.py:178:def _guess_type(path: str) -> str | None:
mirage/backends/local/local.py:6:from mirage.backends.local.memory import _guess_type
mirage/backends/local/local.py:44:                        type=_guess_type(p.name))
mirage/backends/local/__init__.py:2:from mirage.backends.local.memory import MemoryBackend, _guess_type
mirage/backends/local/__init__.py:4:__all__ = ["LocalBackend", "MemoryBackend", "_guess_type"]


   175→        return hashlib.md5(self._files[p]).hexdigest()
   176→
   177→
   178→def _guess_type(path: str) -> str | None:
   179→    ext = path.rsplit(".", 1)[-1].lower() if "." in path else ""
   180→    return {
   181→        "json": "json",
   182→        "jsonl": "json",
   183→        "csv": "csv",
   184→        "tsv": "csv",
   185→        "txt": "text",
   186→        "md": "text",
   187→        "py": "text",
   188→        "js": "text",
   189→        "ts": "text",
   190→        "yaml": "text",
   191→        "yml": "text",
   192→        "toml": "text",
   193→    }.get(ext)
   194→

        return hashlib.md5(self._files[p]).hexdigest()

def _guess_type(path: str) -> str | None:
    ext = path.rsplit(".", 1)[-1].lower() if "." in path else ""
    return {
        "json": "json",
        "jsonl": "json",
        "csv": "csv",
        "tsv": "csv",
        "txt": "text",
        "md": "text",
        "py": "text",
        "js": "text",
        "ts": "text",
        "yaml": "text",
        "yml": "text",
        "toml": "text",
    }.get(ext)


It's a hardcoded extension → type mapping used by every backend when building `FileStat`. Two problems:

1. **It lives in `memory.py`** — unrelated to that backend, every other backend imports it from there
2. **It's a closed dict** — unrecognized extensions return `None`, no way to extend it

Since you want to handle unknown extensions in the future, I'd suggest moving it out into a shared utility (e.g. `mirage/backends/filetype.py` at the top level alongside `base.py`) and making the mapping a `dict` that users/backends can register new types into.

Want me to go ahead and:
1. Move `_guess_type` to a new `mirage/backends/filetype.py` module
2. Make the mapping a module-level `EXTENSION_MAP` dict so it's extensible (e.g. `EXTENSION_MAP["parquet"] = "parquet"`)
3. Optionally add a fallback strategy (like returning `"binary"` instead of `None` for unknown extensions)?


do this

and if unmet file type, should we use text by default or binary?

also rename backends to backend?


AskUserQuestion

Question: For unknown file extensions, should the default be 'text' or 'binary'?

Choices:
Safer default — treats unknown files as opaque binary data
More permissive — assumes unknown files are human-readable


The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.

Note: The user's next message may contain a correction or preference. Pay close attention — if they explain what went wrong or how they'd prefer you to work, consider saving that to memory for future sessions.


[Request interrupted by user for tool use]


What's the default filetype for system like linux?


On Linux (and POSIX systems), the default when a file type can't be determined is `application/octet-stream` — which is essentially **binary**. Tools like `file(1)` and `libmagic` fall back to this. The web (HTTP `Content-Type` header) follows the same convention.

So `"binary"` is the standard safe default. Want me to go ahead with that?


ok let's follow them


The user wants me to:
1. Move `_guess_type` to a new `mirage/backends/filetype.py` module with an extensible `EXTENSION_MAP` dict
2. Default to `"binary"` for unknown extensions
3. Rename `backends` to `backend` across the entire codebase

This is a big refactoring. Let me plan this out:

1. Create `mirage/backend/filetype.py` with extensible EXTENSION_MAP and `guess_type` function (dropping the underscore since it's now a public module)
2. Rename `mirage/backends/` to `mirage/backend/`

3. Update ALL imports from `mirage.backends` to `mirage.backend`

I'm debating the best order to minimize the number of passes through the codebase. The rename affects everything, so I should probably do both the module creation and the directory rename together in one coordinated refactoring rather than doing them separately.

Let me map out the actual steps: create the new filetype module in the current backends directory, update all the imports to use it, then do a git move of the entire backends directory to backend, followed by a mass find-and-replace across the codebase to update all import paths. I'll also need to handle the test directory structure the same way. I'm thinking through the execution order more carefully—I should handle the mass replacements with agents since there are so many files involved, which would be more efficient than processing them one by one. My plan is to create the new filetype module first, update all the imports across the codebase, remove the old function definitions, then rename the directories and do a final pass on any remaining references to the old module name.


Three changes to make:
1. Extract `_guess_type` → `filetype.py` with extensible map, default `"binary"`
2. Rename `backends/` → `backend/`
3. Update all imports


TodoWrite

Create filetype.py with extensible EXTENSION_MAP and guess_type()

Update all _guess_type imports to use filetype module

Remove _guess_type from memory.py and local/__init__.py

Rename backends/ to backend/ everywhere

Mass replace mirage.backends → mirage.backend in all files

Run tests