     1→import pytest
     2→from fastapi.testclient import TestClient
     3→
     4→from mirage.backends.proxy import ProxyBackend
     5→from mirage.observe import session_log as observe_session_module
     6→from mirage.server.fastapi import app as server_app_module
     7→from mirage.server.fastapi.app import app
     8→
     9→
    10→@pytest.fixture
    11→def proxy():
    12→    server_app_module._sessions.clear()
    13→    observe_session_module._session_logs.clear()
    14→    client = TestClient(app)
    15→    resp = client.post("/workspaces", json={"mounts": {"/": "memory"}})
    16→    ws_id = resp.json()["id"]
    17→    backend = ProxyBackend("http://testserver", ws_id, http_client=client)
    18→    yield backend
    19→    server_app_module._sessions.clear()
    20→    observe_session_module._session_logs.clear()
    21→
    22→
    23→def test_create_stat(proxy):
    24→    proxy.create("/f.txt")
    25→    st = proxy.stat("/f.txt")
    26→    assert st.name is not None
    27→    assert st.size == 0
    28→
    29→
    30→def test_write_read(proxy):
    31→    proxy.create("/f.txt")
    32→    written = proxy.write("/f.txt", 0, b"hello world")
    33→    assert written == 11
    34→    data = proxy.read("/f.txt", 0, 11)
    35→    assert data == b"hello world"
    36→
    37→
    38→def test_cat(proxy):
    39→    proxy.create("/f.txt")
    40→    proxy.write("/f.txt", 0, b"hello")
    41→    data = proxy.cat("/f.txt")
    42→    assert data == b"hello"
    43→
    44→
    45→def test_mkdir_readdir(proxy):
    46→    proxy.mkdir("/sub")
    47→    entries = proxy.ls("/")
    48→    names = [e.rsplit("/", 1)[-1] for e in entries]
    49→    assert "sub" in names
    50→
    51→
    52→def test_unlink(proxy):
    53→    proxy.create("/f.txt")
    54→    proxy.unlink("/f.txt")
    55→    with pytest.raises(FileNotFoundError):
    56→        proxy.stat("/f.txt")
    57→
    58→
    59→def test_rename(proxy):
    60→    proxy.create("/a.txt")
    61→    proxy.write("/a.txt", 0, b"data")
    62→    proxy.rename("/a.txt", "/b.txt")
    63→    with pytest.raises(FileNotFoundError):
    64→        proxy.stat("/a.txt")
    65→    st = proxy.stat("/b.txt")
    66→    assert st.size == 4
    67→
    68→
    69→def test_truncate(proxy):
    70→    proxy.create("/f.txt")
    71→    proxy.write("/f.txt", 0, b"hello world")
    72→    proxy.truncate("/f.txt", 5)
    73→    st = proxy.stat("/f.txt")
    74→    assert st.size == 5
    75→
    76→
    77→def test_stat_missing(proxy):
    78→    with pytest.raises(FileNotFoundError):
    79→        proxy.stat("/missing.txt")
    80→
    81→
    82→def test_ls(proxy):
    83→    proxy.create("/a.txt")
    84→    proxy.create("/b.txt")
    85→    entries = proxy.ls("/")
    86→    names = [e.rsplit("/", 1)[-1] for e in entries]
    87→    assert "a.txt" in names
    88→    assert "b.txt" in names
    89→
    90→
    91→def test_fuse_cat(proxy):
    92→    proxy.create("/f.txt")
    93→    proxy.write("/f.txt", 0, b"content")
    94→    data = proxy.fuse_cat("/f.txt")
    95→    assert data == b"content"
    96→
    97→
    98→def test_put(proxy):
    99→    proxy.put("/f.txt", b"hello")
   100→    data = proxy.cat("/f.txt")
   101→    assert data == b"hello"
   102→


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

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


     1→from mirage.backends.local import LocalBackend
     2→from mirage.commands.custom.registry import CommandRegistry
     3→from mirage.runtime.base import CommandNotAllowedError, Runtime
     4→from mirage.types import MountMode, SyncPolicy
     5→from mirage.workspace import Workspace
     6→
     7→
     8→class LocalRuntime(Runtime):
     9→
    10→    def __init__(self,
    11→                 root: str,
    12→                 registry: CommandRegistry | None = None,
    13→                 mode: MountMode = MountMode.WRITE) -> None:
    14→        backend = LocalBackend(root)
    15→        ws = Workspace({"/": backend},
    16→                       mode=mode,
    17→                       sync_policy=SyncPolicy.IMMEDIATE)
    18→        super().__init__(ws, registry)
    19→
    20→
    21→__all__ = ["CommandNotAllowedError", "LocalRuntime"]
    22→


     1→import hashlib
     2→
     3→import pytest
     4→
     5→from mirage.backends.local import LocalBackend
     6→from mirage.commands.builtin.read import cat, diff, file, head, md5, tail, wc
     7→from mirage.types import FileType, MountMode, SyncPolicy
     8→from mirage.workspace import Workspace
     9→
    10→
    11→@pytest.fixture
    12→def local_ws(tmp_path):
    13→    backend = LocalBackend(str(tmp_path))
    14→    return Workspace({"/": backend},
    15→                     mode=MountMode.WRITE,
    16→                     sync_policy=SyncPolicy.IMMEDIATE)
    17→
    18→
    19→# --- head ---
    20→
    21→
    22→def test_head_returns_first_n_lines(write_ws):
    23→    lines = b"\n".join(f"line{i}".encode() for i in range(20))
    24→    write_ws.write("/tmp/test.txt", lines)
    25→    result = head(write_ws, "/tmp/test.txt", lines=3)
    26→    assert result == b"line0\nline1\nline2"
    27→
    28→
    29→def test_head_default_10_lines(write_ws):
    30→    lines = b"\n".join(f"line{i}".encode() for i in range(20))
    31→    write_ws.write("/tmp/test.txt", lines)
    32→    result = head(write_ws, "/tmp/test.txt")
    33→    expected = b"\n".join(f"line{i}".encode() for i in range(10))
    34→    assert result == expected
    35→
    36→
    37→def test_head_empty_file(write_ws):
    38→    write_ws.write("/tmp/test.txt", b"")
    39→    assert head(write_ws, "/tmp/test.txt") == b""
    40→
    41→
    42→def test_head_fewer_lines_than_requested(write_ws):
    43→    write_ws.write("/tmp/test.txt", b"one\ntwo")
    44→    assert head(write_ws, "/tmp/test.txt", lines=10) == b"one\ntwo"
    45→
    46→
    47→def test_head_local_backend(local_ws):
    48→    local_ws.write("/hello.txt", b"a\nb\nc\nd")
    49→    assert head(local_ws, "/hello.txt", lines=2) == b"a\nb"
    50→
    51→
    52→# --- tail ---
    53→
    54→
    55→def test_tail_returns_last_n_lines(write_ws):
    56→    lines = b"\n".join(f"line{i}".encode() for i in range(20))
    57→    write_ws.write("/tmp/test.txt", lines)
    58→    result = tail(write_ws, "/tmp/test.txt", lines=3)
    59→    assert result == b"line17\nline18\nline19"
    60→
    61→
    62→def test_tail_default_10_lines(write_ws):
    63→    lines = b"\n".join(f"line{i}".encode() for i in range(20))
    64→    write_ws.write("/tmp/test.txt", lines)
    65→    result = tail(write_ws, "/tmp/test.txt")
    66→    expected = b"\n".join(f"line{i}".encode() for i in range(10, 20))
    67→    assert result == expected
    68→
    69→
    70→def test_tail_empty_file(write_ws):
    71→    write_ws.write("/tmp/test.txt", b"")
    72→    assert tail(write_ws, "/tmp/test.txt") == b""
    73→
    74→
    75→def test_tail_fewer_lines_than_requested(write_ws):
    76→    write_ws.write("/tmp/test.txt", b"one\ntwo")
    77→    assert tail(write_ws, "/tmp/test.txt", lines=10) == b"one\ntwo"
    78→
    79→
    80→def test_tail_local_backend(local_ws):
    81→    local_ws.write("/hello.txt", b"a\nb\nc\nd")
    82→    assert tail(local_ws, "/hello.txt", lines=2) == b"c\nd"
    83→
    84→
    85→# --- cat ---
    86→
    87→
    88→def test_cat_returns_bytes(write_ws):
    89→    write_ws.write("/tmp/f.txt", b"hello world")
    90→    assert cat(write_ws, "/tmp/f.txt") == b"hello world"
    91→
    92→
    93→def test_cat_empty_file(write_ws):
    94→    write_ws.write("/tmp/empty.txt", b"")
    95→    assert cat(write_ws, "/tmp/empty.txt") == b""
    96→
    97→
    98→def test_cat_local_backend(local_ws):
    99→    local_ws.write("/data.txt", b"from disk")
   100→    assert cat(local_ws, "/data.txt") == b"from disk"
   101→
   102→
   103→# --- wc ---
   104→
   105→
   106→def test_wc_counts(write_ws):
   107→    write_ws.write("/tmp/f.txt", b"one two\nthree\n")
   108→    result = wc(write_ws, "/tmp/f.txt")
   109→    assert result["lines"] == 2
   110→    assert result["words"] == 3
   111→    assert result["bytes"] == 14
   112→
   113→
   114→def test_wc_empty_file(write_ws):
   115→    write_ws.write("/tmp/empty.txt", b"")
   116→    result = wc(write_ws, "/tmp/empty.txt")
   117→    assert result == {"lines": 0, "words": 0, "bytes": 0}
   118→
   119→
   120→def test_wc_local_backend(local_ws):
   121→    local_ws.write("/f.txt", b"a b\nc\n")
   122→    result = wc(local_ws, "/f.txt")
   123→    assert result["lines"] == 2
   124→    assert result["words"] == 3
   125→
   126→
   127→# --- md5 ---
   128→
   129→
   130→def test_md5_matches_hashlib(write_ws):
   131→    data = b"hello"
   132→    write_ws.write("/tmp/f.txt", data)
   133→    assert md5(write_ws, "/tmp/f.txt") == hashlib.md5(data).hexdigest()
   134→
   135→
   136→def test_md5_empty_file(write_ws):
   137→    write_ws.write("/tmp/empty.txt", b"")
   138→    assert md5(write_ws, "/tmp/empty.txt") == hashlib.md5(b"").hexdigest()
   139→
   140→
   141→def test_md5_local_backend(local_ws):
   142→    data = b"disk content"
   143→    local_ws.write("/f.txt", data)
   144→    assert md5(local_ws, "/f.txt") == hashlib.md5(data).hexdigest()
   145→
   146→
   147→# --- diff ---
   148→
   149→
   150→def test_diff_identical_files(write_ws):
   151→    write_ws.write("/tmp/a.txt", b"same\n")
   152→    write_ws.write("/tmp/b.txt", b"same\n")
   153→    assert diff(write_ws, "/tmp/a.txt", "/tmp/b.txt") == []
   154→
   155→
   156→def test_diff_different_files(write_ws):
   157→    write_ws.write("/tmp/a.txt", b"hello\n")
   158→    write_ws.write("/tmp/b.txt", b"world\n")
   159→    result = diff(write_ws, "/tmp/a.txt", "/tmp/b.txt")
   160→    assert any("-hello" in line for line in result)
   161→    assert any("+world" in line for line in result)
   162→
   163→
   164→def test_diff_local_backend(local_ws):
   165→    local_ws.write("/a.txt", b"foo\n")
   166→    local_ws.write("/b.txt", b"bar\n")
   167→    result = diff(local_ws, "/a.txt", "/b.txt")
   168→    assert len(result) > 0
   169→
   170→
   171→# --- file / _detect ---
   172→
   173→
   174→def test_file_text(write_ws):
   175→    write_ws.write("/tmp/f.txt", b"hello world, plain text")
   176→    assert file(write_ws, "/tmp/f.txt") == FileType.TEXT
   177→
   178→
   179→def test_file_png(write_ws):
   180→    write_ws.write("/tmp/img.png", b"\x89PNGrest")
   181→    assert file(write_ws, "/tmp/img.png") == FileType.IMAGE_PNG
   182→
   183→
   184→def test_file_pdf(write_ws):
   185→    write_ws.write("/tmp/doc.pdf", b"%PDFrest")
   186→    assert file(write_ws, "/tmp/doc.pdf") == FileType.PDF
   187→
   188→
   189→def test_file_gzip(write_ws):
   190→    write_ws.write("/tmp/f.gz", b"\x1f\x8brest")
   191→    assert file(write_ws, "/tmp/f.gz") == FileType.GZIP
   192→
   193→
   194→def test_file_directory(write_ws):
   195→    write_ws.mkdir("/tmp/subdir/")
   196→    assert file(write_ws, "/tmp/subdir/") == FileType.DIRECTORY
   197→
   198→
   199→def test_filetype_is_str_compatible(write_ws):
   200→    write_ws.write("/tmp/f.txt", b"plain text here")
   201→    result = file(write_ws, "/tmp/f.txt")
   202→    assert result == "text"  # FileType.TEXT == "text" via str inheritance
   203→
   204→
   205→def test_file_local_backend(local_ws):
   206→    local_ws.write("/f.txt", b"plain text content here")
   207→    assert file(local_ws, "/f.txt") == "text"
   208→
   209→
   210→def test_presign_not_implemented_on_memory(write_ws):
   211→    write_ws.write("/tmp/test.txt", b"data")
   212→    with pytest.raises(NotImplementedError):
   213→        write_ws.backend("/tmp/test.txt").presign("/test.txt")
   214→


     1→import pytest
     2→
     3→from mirage.backends.local import LocalBackend
     4→from mirage.types import FileType
     5→
     6→
     7→@pytest.fixture
     8→def local_backend(tmp_path):
     9→    return LocalBackend(str(tmp_path))
    10→
    11→
    12→def test_create_and_cat(local_backend):
    13→    local_backend.create("/hello.txt")
    14→    local_backend.write("/hello.txt", 0, b"hello")
    15→    assert local_backend.cat("/hello.txt") == b"hello"
    16→
    17→
    18→def test_put_and_cat(local_backend):
    19→    local_backend.put("/f.txt", b"data")
    20→    assert local_backend.cat("/f.txt") == b"data"
    21→
    22→
    23→def test_read_partial(local_backend):
    24→    local_backend.put("/f.txt", b"abcdef")
    25→    assert local_backend.read("/f.txt", 2, 3) == b"cde"
    26→
    27→
    28→def test_mkdir_and_readdir(local_backend):
    29→    local_backend.mkdir("/mydir")
    30→    entries, _ = local_backend.readdir("/mydir")
    31→    assert entries == []
    32→
    33→
    34→def test_rmdir_empty(local_backend):
    35→    local_backend.mkdir("/emptydir")
    36→    local_backend.rmdir("/emptydir")
    37→    with pytest.raises(FileNotFoundError):
    38→        local_backend.stat("/emptydir")
    39→
    40→
    41→def test_unlink(local_backend):
    42→    local_backend.put("/del.txt", b"x")
    43→    local_backend.unlink("/del.txt")
    44→    with pytest.raises(FileNotFoundError):
    45→        local_backend.stat("/del.txt")
    46→
    47→
    48→def test_rename(local_backend):
    49→    local_backend.put("/old.txt", b"content")
    50→    local_backend.rename("/old.txt", "/new.txt")
    51→    assert local_backend.cat("/new.txt") == b"content"
    52→    with pytest.raises(FileNotFoundError):
    53→        local_backend.stat("/old.txt")
    54→
    55→
    56→def test_stat_file(local_backend):
    57→    local_backend.put("/f.txt", b"hello")
    58→    s = local_backend.stat("/f.txt")
    59→    assert s.name == "f.txt"
    60→    assert s.size == 5
    61→    assert s.type == "text"
    62→
    63→
    64→def test_stat_directory(local_backend):
    65→    local_backend.mkdir("/mydir")
    66→    s = local_backend.stat("/mydir")
    67→    assert s.type == FileType.DIRECTORY
    68→    assert s.size is None
    69→
    70→
    71→def test_stat_missing_raises(local_backend):
    72→    with pytest.raises(FileNotFoundError):
    73→        local_backend.stat("/missing.txt")
    74→
    75→
    76→def test_write_at_offset(local_backend):
    77→    local_backend.put("/f.txt", b"hello world")
    78→    local_backend.write("/f.txt", 6, b"earth")
    79→    assert local_backend.cat("/f.txt") == b"hello earth"
    80→
    81→
    82→def test_truncate_shrink(local_backend):
    83→    local_backend.put("/f.txt", b"abcdef")
    84→    local_backend.truncate("/f.txt", 3)
    85→    assert local_backend.cat("/f.txt") == b"abc"
    86→
    87→
    88→def test_truncate_grow(local_backend):
    89→    local_backend.put("/f.txt", b"ab")
    90→    local_backend.truncate("/f.txt", 5)
    91→    assert len(local_backend.cat("/f.txt")) == 5
    92→
    93→
    94→def test_path_traversal_raises(local_backend):
    95→    with pytest.raises(ValueError):
    96→        local_backend.stat("/../etc/passwd")
    97→


     1→import pytest
     2→
     3→from mirage.backends.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→


     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 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→


Edit

from mirage.backends.local import LocalBackend

from mirage.backends.local.local import LocalBackend


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


from mirage.backends.r2 import R2Backend, R2Config

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


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


from mirage.backends.r2 import R2Config
from mirage.backends.s3 import S3Config

from mirage.backends.object_storage.r2 import R2Config
from mirage.backends.object_storage.s3 import S3Config


from mirage.backends.proxy import ProxyBackend

from mirage.backends.network.proxy import ProxyBackend


The file /Users/agent/strukto/mirage/tests/backends/test_configs.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/runtime/remote.py has been updated successfully.


from mirage.backends.local import LocalBackend

from mirage.backends.local.local import LocalBackend


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


from mirage.backends.local import LocalBackend

from mirage.backends.local.local import LocalBackend


     1→from __future__ import annotations
     2→
     3→import time
     4→
     5→from mirage.backends.base import BaseBackend
     6→from mirage.backends.memory import _guess_type
     7→from mirage.types import (ConsistencyPolicy, FileStat, Inode, MountMode,
     8→                          SyncPolicy, SyncResult, TransferStats)
     9→from mirage.workspace.cache import LRUCache
    10→from mirage.workspace.cache.policy import is_fresh
    11→from mirage.workspace.mount import MountRegistry
    12→from mirage.workspace.sync import DirtyTracker
    13→
    14→
    15→class _PathLock:
    16→
    17→    def __init__(self, ws: Workspace, paths: list[str]) -> None:
    18→        self._ws = ws
    19→        self._paths = paths
    20→
    21→    def __enter__(self) -> _PathLock:
    22→        return self
    23→
    24→    def __exit__(self, *_: object) -> None:
    25→        self._ws.unlock_paths(self._paths)
    26→
    27→
    28→class Workspace:
    29→    """
    30→    Unified virtual filesystem over heterogeneous backends.
    31→
    32→    Mounts backends at virtual path prefixes and provides a command-level API
    33→    (cat, ls, write, cp, mv, rm, ...) with transparent caching
    34→    and write staging.
    35→
    36→    Example::
    37→
    38→        ws = Workspace(
    39→            {"/data/": S3Backend(S3Config(bucket="my-bucket")),
    40→             "/tmp/":  MemoryBackend()},
    41→            cache_limit="256MB",
    42→            sync_policy=SyncPolicy.STAGED,
    43→        )
    44→        content = ws.cat("/data/reports/q1.json")
    45→        ws.write("/tmp/summary.txt", b"hello")
    46→        ws.sync()
    47→    """
    48→
    49→    def __init__(
    50→        self,
    51→        backends: dict[str, BaseBackend | tuple],
    52→        cache_limit: str | int = "512MB",
    53→        sync_policy: SyncPolicy = SyncPolicy.STAGED,
    54→        mode: MountMode = MountMode.READ,
    55→        consistency: ConsistencyPolicy = ConsistencyPolicy.NONE,
    56→        ttl_seconds: int = 300,
    57→    ) -> None:
    58→        self._registry = MountRegistry()
    59→        self._cache = LRUCache(cache_limit)
    60→        self._tracker = DirtyTracker()
    61→        self._sync_policy = sync_policy
    62→        self._consistency = consistency
    63→        self._ttl_seconds = ttl_seconds
    64→        self._locked_paths: set[str] = set()
    65→        self._transfer_stats: dict[str, TransferStats] = {}
    66→        self._created_at: float = time.monotonic()
    67→
    68→        for prefix, value in backends.items():
    69→            if isinstance(value, tuple) and len(value) == 3:
    70→                backend, mount_mode, mount_consistency = value
    71→            elif isinstance(value, tuple):
    72→                backend, mount_mode = value
    73→                mount_consistency = None
    74→            else:
    75→                backend = value
    76→                mount_mode = mode
    77→                mount_consistency = None
    78→            self._registry.mount(prefix, backend, mount_mode,
    79→                                 mount_consistency)
    80→
    81→    # ── mount management ────────────────────────────────────────────────────
    82→
    83→    def mount(self,
    84→              prefix: str,
    85→              backend: BaseBackend,
    86→              mode: MountMode = MountMode.READ,
    87→              consistency: ConsistencyPolicy | None = None) -> None:
    88→        self._registry.mount(prefix, backend, mode, consistency)
    89→
    90→    def backend(self, path: str) -> BaseBackend:
    91→        backend, _, _ = self._registry.resolve(path)
    92→        return backend
    93→
    94→    # ── read ops ────────────────────────────────────────────────────────────
    95→
    96→    def cat(self, path: str) -> bytes:
    97→        inode = self._tracker.get(path)
    98→        if inode and inode.dirty:
    99→            entry = self._cache.get(path)
   100→            if entry is not None:
   101→                self._record_transfer(path,
   102→                                      "read",
   103→                                      len(entry.data),
   104→                                      0,
   105→                                      cache_hit=True)
   106→                return entry.data
   107→
   108→        entry = self._cache.get(path)
   109→        remote_stat = None
   110→        if entry is not None:
   111→            consistency = self._effective_consistency(path)
   112→            backend, bp, _ = self._registry.resolve(path)
   113→            fresh, remote_stat = is_fresh(entry, consistency,
   114→                                          self._ttl_seconds, backend, bp)
   115→            if fresh:
   116→                self._record_transfer(path,
   117→                                      "read",
   118→                                      len(entry.data),
   119→                                      0,
   120→                                      cache_hit=True)
   121→                return entry.data
   122→        else:
   123→            backend, bp, _ = self._registry.resolve(path)
   124→
   125→        t0 = time.monotonic()
   126→        data = backend.cat(bp)
   127→        duration_ms = int((time.monotonic() - t0) * 1000)
   128→        self._record_transfer(path,
   129→                              "read",
   130→                              len(data),
   131→                              duration_ms,
   132→                              cache_hit=False)
   133→        if remote_stat is None:
   134→            remote_stat = backend.stat(bp)
   135→        self._cache.put(path,
   136→                        data,
   137→                        etag=remote_stat.extra.get("etag"),
   138→                        modified=remote_stat.modified,
   139→                        size=remote_stat.size)
   140→        self._tracker.on_read(path, data, self._mount_prefix(path))
   141→        return data
   142→
   143→    def ls(self, path: str) -> list[str]:
   144→        backend, bp, _ = self._registry.resolve(path)
   145→        prefix = self._mount_prefix(path).rstrip("/")
   146→        backend_ok = False
   147→        try:
   148→            backend_entries = backend.ls(bp)
   149→            backend_ok = True
   150→        except FileNotFoundError:
   151→            backend_entries = []
   152→        result: set[str] = {
   153→            prefix + "/" + e.lstrip("/")
   154→            for e in backend_entries
   155→        }
   156→        norm_path = "/" + path.strip("/")
   157→        dir_prefix = norm_path.rstrip("/") + "/"
   158→        has_staged_children = False
   159→        for inode in self._tracker.dirty():
   160→            vp = inode.virtual_path
   161→            if not vp.startswith(dir_prefix):
   162→                continue
   163→            if inode.checksum is None:
   164→                result.discard(vp)
   165→                continue
   166→            has_staged_children = True
   167→            remainder = vp[len(dir_prefix):]
   168→            first = remainder.split("/")[0]
   169→            if not first:
   170→                continue
   171→            if "/" in remainder:
   172→                result.add(dir_prefix + first + "/")
   173→            else:
   174→                result.add(dir_prefix + first)
   175→        if not backend_ok and not has_staged_children:
   176→            raise FileNotFoundError(path)
   177→        return sorted(result)
   178→
   179→    def ls_l(self, path: str) -> list[FileStat]:
   180→        backend, bp, _ = self._registry.resolve(path)
   181→        return backend.ls_l(bp)
   182→
   183→    def stat(self, path: str) -> FileStat:
   184→        # For staged writes, build FileStat from the tracker rather than
   185→        # hitting the backend (which hasn't received the write yet).
   186→        inode = self._tracker.get(path)
   187→        if inode and inode.dirty and inode.checksum is not None:
   188→            entry = self._cache.get(path)
   189→            size = len(entry.data) if entry is not None else inode.size
   190→            return FileStat(
   191→                name=path.rstrip("/").rsplit("/", 1)[-1],
   192→                size=size,
   193→                type=_guess_type(path),
   194→            )
   195→        backend, bp, _ = self._registry.resolve(path)
   196→        return backend.stat(bp)
   197→
   198→    def exists(self, path: str) -> bool:
   199→        try:
   200→            self.stat(path)
   201→            return True
   202→        except (FileNotFoundError, ValueError):
   203→            return False
   204→
   205→    def head(self, path: str, lines: int = 10) -> bytes:
   206→        backend, bp, _ = self._registry.resolve(path)
   207→        return backend.head(bp, lines)
   208→
   209→    def tail(self, path: str, lines: int = 10) -> bytes:
   210→        backend, bp, _ = self._registry.resolve(path)
   211→        return backend.tail(bp, lines)
   212→
   213→    def read(self, path: str, offset: int, size: int) -> bytes:
   214→        inode = self._tracker.get(path)
   215→        if inode and inode.dirty:
   216→            entry = self._cache.get(path)
   217→            if entry is not None:
   218→                return entry.data[offset:offset + size]
   219→        backend, bp, _ = self._registry.resolve(path)
   220→        return backend.read(bp, offset, size)
   221→
   222→    def grep(self, pattern: str, path: str) -> list[str]:
   223→        backend, bp, _ = self._registry.resolve(path)
   224→        return backend.grep(pattern, bp)
   225→
   226→    # ── write ops ───────────────────────────────────────────────────────────
   227→
   228→    def write(self, path: str, data: bytes) -> None:
   229→        self._assert_not_locked(path)
   230→        self._assert_writable(path)
   231→        backend, bp, _ = self._registry.resolve(path)
   232→        self._cache.put(path, data)
   233→        self._tracker.on_write(path, data, self._mount_prefix(path))
   234→        self._record_transfer(path, "write", len(data), 0)
   235→        if self._sync_policy == SyncPolicy.IMMEDIATE:
   236→            backend.put(bp, data)
   237→            self._tracker.on_read(path, data, self._mount_prefix(path))
   238→
   239→    def mkdir(self, path: str) -> None:
   240→        self._assert_writable(path)
   241→        backend, bp, _ = self._registry.resolve(path)
   242→        backend.mkdir(bp)
   243→
   244→    def rm(self, path: str) -> None:
   245→        self._assert_writable(path)
   246→        backend, bp, _ = self._registry.resolve(path)
   247→        inode = self._tracker.get(path)
   248→        staged_only = inode and inode.dirty and inode.remote_checksum is None
   249→        if not staged_only:
   250→            backend.unlink(bp)
   251→        self._cache.invalidate(path)
   252→        self._tracker.on_delete(path)
   253→
   254→    def mv(self, src: str, dst: str) -> None:
   255→        self._assert_writable(src)
   256→        sb, sbp, _ = self._registry.resolve(src)
   257→        db, dbp, _ = self._registry.resolve(dst)
   258→        src_inode = self._tracker.get(src)
   259→        staged_only = (src_inode and src_inode.dirty
   260→                       and src_inode.remote_checksum is None)
   261→        if staged_only:
   262→            entry = self._cache.get(src)
   263→            data = entry.data if entry else b""
   264→            self._cache.put(dst, data)
   265→            self._tracker.on_write(dst, data, self._mount_prefix(dst))
   266→            self._cache.invalidate(src)
   267→            self._tracker.on_delete(src)
   268→        elif sb is db:
   269→            sb.rename(sbp, dbp)
   270→            self._cache.invalidate(src)
   271→        else:
   272→            data = sb.cat(sbp)
   273→            db.put(dbp, data)
   274→            sb.unlink(sbp)
   275→            self._cache.invalidate(src)
   276→
   277→    def cp(self, src: str, dst: str) -> None:
   278→        self._assert_writable(dst)
   279→        data = self.cat(src)
   280→        db, dbp, _ = self._registry.resolve(dst)
   281→        db.put(dbp, data)
   282→
   283→    # ── sync lifecycle ──────────────────────────────────────────────────────
   284→
   285→    def sync(self, path: str | None = None) -> list[SyncResult]:
   286→        results: list[SyncResult] = []
   287→        targets = [
   288→            i for i in self._tracker.dirty()
   289→            if path is None or i.virtual_path == path
   290→        ]
   291→        for inode in targets:
   292→            backend, bp, _ = self._registry.resolve(inode.virtual_path)
   293→            entry = self._cache.get(inode.virtual_path)
   294→            data = entry.data if entry else None
   295→            result = self._tracker.on_sync(inode.virtual_path, bp, data,
   296→                                           backend)
   297→            results.append(result)
   298→        return results
   299→
   300→    def status(self) -> list[Inode]:
   301→        return self._tracker.status()
   302→
   303→    def reset(self) -> None:
   304→        self._tracker.reset()
   305→        self._cache.clear()
   306→
   307→    # ── path locking ─────────────────────────────────────────────────────────
   308→
   309→    def _lock_path(self, path: str) -> None:
   310→        self._locked_paths.add(path)
   311→
   312→    def unlock_paths(self, paths: list[str]) -> None:
   313→        for p in paths:
   314→            self._locked_paths.discard(p)
   315→
   316→    def lock_paths(self, paths: list[str]) -> _PathLock:
   317→        for p in paths:
   318→            self._lock_path(p)
   319→        return _PathLock(self, paths)
   320→
   321→    def _assert_not_locked(self, path: str) -> None:
   322→        if path in self._locked_paths:
   323→            raise PermissionError(
   324→                f"path {path!r} is locked by remote execution")
   325→
   326→    # ── helpers ─────────────────────────────────────────────────────────────
   327→
   328→    def _mount_prefix(self, path: str) -> str:
   329→        norm = "/" + path.strip("/")
   330→        for m in self._registry.mounts():
   331→            if norm == m.prefix.rstrip("/") or norm.startswith(m.prefix):
   332→                return m.prefix
   333→        return "/"
   334→
   335→    def _effective_consistency(self, path: str) -> ConsistencyPolicy:
   336→        mount = self._registry.mount_for(path)
   337→        if mount.consistency is not None:
   338→            return mount.consistency
   339→        return self._consistency
   340→
   341→    def _assert_writable(self, path: str) -> None:
   342→        _, _, mode = self._registry.resolve(path)
   343→        if mode == MountMode.READ:
   344→            raise PermissionError(f"mount at {path!r} is read-only")
   345→
   346→    def _record_transfer(self,
   347→                         path: str,
   348→                         op: str,
   349→                         nbytes: int,
   350→                         duration_ms: int,
   351→                         cache_hit: bool | None = None) -> None:
   352→        prefix = self._mount_prefix(path)
   353→        if prefix not in self._transfer_stats:
   354→            self._transfer_stats[prefix] = TransferStats()
   355→        stats = self._transfer_stats[prefix]
   356→        if op == "read":
   357→            stats.bytes_read += nbytes
   358→            stats.read_ops += 1
   359→            stats.total_read_ms += duration_ms
   360→            if cache_hit is True:
   361→                stats.cache_hits += 1
   362→            elif cache_hit is False:
   363→                stats.cache_misses += 1
   364→        elif op == "write":
   365→            stats.bytes_written += nbytes
   366→            stats.write_ops += 1
   367→            stats.total_write_ms += duration_ms
   368→


     1→from mirage.backends.local import LocalBackend
     2→from mirage.backends.memory import MemoryBackend
     3→from mirage.types import (ConsistencyPolicy, FileStat, Inode, MountMode,
     4→                          SyncPolicy, SyncResult)
     5→from mirage.workspace import Workspace
     6→
     7→__all__ = [
     8→    "Workspace",
     9→    "MemoryBackend",
    10→    "LocalBackend",
    11→    "ConsistencyPolicy",
    12→    "FileStat",
    13→    "Inode",
    14→    "MountMode",
    15→    "SyncPolicy",
    16→    "SyncResult",
    17→]
    18→


from mirage.backends.local import LocalBackend

from mirage.backends.local.local import LocalBackend


The file /Users/agent/strukto/mirage/mirage/runtime/local.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→
    11→def _build_backend(type_name: str, connection: str) -> BaseBackend:
    12→    if type_name == BackendType.MEMORY:
    13→        return MemoryBackend()
    14→    if type_name == BackendType.LOCAL:
    15→        return LocalBackend(root=os.path.abspath(connection))
    16→    if type_name == BackendType.S3:
    17→        from mirage.backends.s3 import S3Backend, S3Config
    18→        rest = connection[len("s3://"):]
    19→        bucket = rest.split("/", 1)[0]
    20→        return S3Backend(S3Config(bucket=bucket))
    21→    if type_name == BackendType.R2:
    22→        from mirage.backends.r2 import R2Backend, R2Config
    23→        rest = connection[len("r2://"):]
    24→        parts = rest.split("/", 1)
    25→        return R2Backend(
    26→            R2Config(
    27→                account_id=parts[0],
    28→                bucket=parts[1] if len(parts) > 1 else "",
    29→                access_key_id=os.environ.get("R2_ACCESS_KEY_ID", ""),
    30→                secret_access_key=os.environ.get("R2_SECRET_ACCESS_KEY", ""),
    31→            ))
    32→    if type_name in (BackendType.HTTP, BackendType.HTTPS):
    33→        from mirage.backends.http import HTTPBackend, HTTPConfig
    34→        return HTTPBackend(HTTPConfig(base_url=connection))
    35→    raise ValueError(f"Unknown backend type: {type_name!r}. "
    36→                     "Use built-ins: memory, local, s3, r2, http")
    37→
    38→
    39→def parse_mount_uri(uri: str) -> BaseBackend:
    40→    if uri == BackendType.MEMORY:
    41→        return MemoryBackend()
    42→    if uri == BackendType.LOCAL:
    43→        return LocalBackend(root=os.path.abspath("."))
    44→
    45→    colon = uri.find(":")
    46→    if colon > 0 and "//" not in uri[:colon + 3]:
    47→        return _build_backend(uri[:colon], uri[colon + 1:])
    48→
    49→    if uri.startswith("s3://"):
    50→        return _build_backend("s3", uri)
    51→    if uri.startswith("r2://"):
    52→        return _build_backend("r2", uri)
    53→    if uri.startswith("http://") or uri.startswith("https://"):
    54→        return _build_backend("http", uri)
    55→
    56→    if "://" in uri:
    57→        scheme = uri.split("://")[0]
    58→        raise ValueError(
    59→            f"Unknown URI scheme {scheme!r}. "
    60→            "Accepted: s3://, r2://, https://. "
    61→            "For simple backends use: memory, local, local:./path")
    62→    return LocalBackend(root=os.path.abspath(uri))
    63→
    64→
    65→def split_mount_spec(spec: str) -> tuple[str, str]:
    66→    """Split 'backend:/path' into (backend, path).
    67→
    68→    Args:
    69→        spec (str): Mount spec like 'memory:/data/' or 's3://bucket:/data/'.
    70→
    71→    Returns:
    72→        tuple[str, str]: (backend, mount_path) pair.
    73→    """
    74→    i = len(spec) - 1
    75→    while i >= 0:
    76→        if spec[i] == ":" and i + 1 < len(spec) and spec[i + 1] == "/":
    77→            if i >= 2 and spec[i - 1:i + 2] == "://":
    78→                i -= 1
    79→                continue
    80→            return spec[:i], spec[i + 1:]
    81→        i -= 1
    82→    raise ValueError(
    83→        f"Invalid mount spec: {spec!r}. "
    84→        "Use format: backend:/path (e.g. memory:/data/, s3://bucket:/)")
    85→
    86→
    87→_MODE_SUFFIXES = ("@read", "@write", "@exec")
    88→
    89→
    90→def parse_mount_uri_with_mode(
    91→    uri: str,
    92→    default_mode: MountMode = MountMode.READ,
    93→) -> tuple[BaseBackend, MountMode]:
    94→    for suffix in _MODE_SUFFIXES:
    95→        if uri.endswith(suffix):
    96→            return parse_mount_uri(uri[:-len(suffix)]), MountMode(suffix[1:])
    97→    return parse_mount_uri(uri), default_mode
    98→
    99→
   100→_CONSISTENCY_SUFFIXES = ("#none", "#lazy", "#ttl", "#always")
   101→
   102→
   103→def parse_mount_uri_full(
   104→    uri: str,
   105→    default_mode: MountMode = MountMode.READ,
   106→    default_consistency: ConsistencyPolicy = ConsistencyPolicy.NONE,
   107→) -> tuple[BaseBackend, MountMode, ConsistencyPolicy]:
   108→    consistency = default_consistency
   109→    for suffix in _CONSISTENCY_SUFFIXES:
   110→        if uri.endswith(suffix):
   111→            consistency = ConsistencyPolicy(suffix[1:])
   112→            uri = uri[:-len(suffix)]
   113→            break
   114→    backend, mode = parse_mount_uri_with_mode(uri, default_mode)
   115→    return backend, mode, consistency
   116→
   117→
   118→def parse_mounts_from_cli(
   119→    mount_specs: list[str],
   120→    default_mode: MountMode = MountMode.READ,
   121→) -> dict[str, BaseBackend | tuple[BaseBackend, MountMode]]:
   122→    mounts: dict[str, BaseBackend | tuple[BaseBackend, MountMode]] = {}
   123→    for spec in mount_specs:
   124→        uri, prefix = split_mount_spec(spec)
   125→        backend, mode = parse_mount_uri_with_mode(uri, default_mode)
   126→        if mode != default_mode:
   127→            mounts[prefix] = (backend, mode)
   128→        else:
   129→            mounts[prefix] = backend
   130→    return mounts
   131→
   132→
   133→def load_yaml_config(path: str) -> dict:
   134→    with open(path) as f:
   135→        return yaml.safe_load(f)
   136→
   137→
   138→def load_workspace_yaml(path: str) -> dict:
   139→    with open(path) as f:
   140→        data = yaml.safe_load(f)
   141→    mounts_raw = data.get("mounts", {})
   142→    mounts: dict[str, str | dict] = {}
   143→    for prefix, value in mounts_raw.items():
   144→        if isinstance(value, str):
   145→            mounts[prefix] = value
   146→        elif isinstance(value, dict):
   147→            backend_spec = value.get("backend")
   148→            if backend_spec:
   149→                entry: dict[str, str | dict] = {"backend": backend_spec}
   150→                if value.get("args"):
   151→                    entry["args"] = value["args"]
   152→                if value.get("mode"):
   153→                    entry["mode"] = value["mode"]
   154→                if value.get("consistency"):
   155→                    entry["consistency"] = value["consistency"]
   156→                mounts[prefix] = entry
   157→            else:
   158→                uri = value.get("uri", "")
   159→                mount_mode = value.get("mode")
   160→                mount_consistency = value.get("consistency")
   161→                entry_uri: dict[str, str] = {"uri": uri}
   162→                if mount_mode:
   163→                    entry_uri["mode"] = mount_mode
   164→                if mount_consistency:
   165→                    entry_uri["consistency"] = mount_consistency
   166→                if len(entry_uri) == 1:
   167→                    mounts[prefix] = uri
   168→                else:
   169→                    mounts[prefix] = entry_uri
   170→    result: dict = {"mounts": mounts, "commands": data.get("commands", {})}
   171→    if "consistency" in data:
   172→        result["consistency"] = data["consistency"]
   173→    if "ttl" in data:
   174→        result["ttl"] = data["ttl"]
   175→    if "agent_id" in data:
   176→        result["agent_id"] = data["agent_id"]
   177→    fuse_ops = data.get("fuse_ops", {})
   178→    if fuse_ops:
   179→        result["fuse_ops"] = fuse_ops
   180→    for key in ("runtime", "url", "ws_id", "command_url", "fuse_ops_url"):
   181→        if key in data:
   182→            result[key] = data[key]
   183→    return result
   184→


from mirage.backends.local import LocalBackend

from mirage.backends.local.local import LocalBackend


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


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


     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→@pytest.fixture
     9→def memory_backend():
    10→    return MemoryBackend()
    11→
    12→
    13→@pytest.fixture
    14→def write_ws():
    15→    return Workspace(
    16→        {"/tmp/": MemoryBackend()},
    17→        mode=MountMode.WRITE,
    18→        sync_policy=SyncPolicy.IMMEDIATE,
    19→    )
    20→
    21→
    22→@pytest.fixture
    23→def staged_ws():
    24→    return Workspace(
    25→        {"/tmp/": MemoryBackend()},
    26→        mode=MountMode.WRITE,
    27→        sync_policy=SyncPolicy.STAGED,
    28→    )
    29→


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


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


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


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


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


     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 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→


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


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


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


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