     1→import pytest
     2→
     3→from mirage.commands.builtin.write import (cp, echo, mkdir, mv, rm, sed, tee,
     4→                                           touch)
     5→
     6→
     7→def test_mkdir_creates_directory(write_ws):
     8→    mkdir(write_ws, "/tmp/newdir/")
     9→    assert write_ws.exists("/tmp/newdir/")
    10→
    11→
    12→def test_touch_creates_empty_file(write_ws):
    13→    touch(write_ws, "/tmp/new.txt")
    14→    assert write_ws.cat("/tmp/new.txt") == b""
    15→
    16→
    17→def test_touch_does_not_overwrite_existing(write_ws):
    18→    write_ws.write("/tmp/existing.txt", b"original")
    19→    touch(write_ws, "/tmp/existing.txt")
    20→    assert write_ws.cat("/tmp/existing.txt") == b"original"
    21→
    22→
    23→def test_cp_copies_file(write_ws):
    24→    write_ws.write("/tmp/src.txt", b"hello")
    25→    cp(write_ws, "/tmp/src.txt", "/tmp/dst.txt")
    26→    assert write_ws.cat("/tmp/dst.txt") == b"hello"
    27→    assert write_ws.cat("/tmp/src.txt") == b"hello"
    28→
    29→
    30→def test_mv_moves_file(write_ws):
    31→    write_ws.write("/tmp/a.txt", b"data")
    32→    mv(write_ws, "/tmp/a.txt", "/tmp/b.txt")
    33→    assert write_ws.cat("/tmp/b.txt") == b"data"
    34→    with pytest.raises(FileNotFoundError):
    35→        write_ws.cat("/tmp/a.txt")
    36→
    37→
    38→def test_mv_source_no_longer_readable(write_ws):
    39→    write_ws.write("/tmp/x.txt", b"x")
    40→    mv(write_ws, "/tmp/x.txt", "/tmp/y.txt")
    41→    with pytest.raises(FileNotFoundError):
    42→        write_ws.cat("/tmp/x.txt")
    43→
    44→
    45→def test_rm_removes_file(write_ws):
    46→    write_ws.write("/tmp/f.txt", b"bye")
    47→    rm(write_ws, "/tmp/f.txt")
    48→    assert not write_ws.exists("/tmp/f.txt")
    49→
    50→
    51→def test_rm_directory_without_recursive_raises(write_ws):
    52→    write_ws.mkdir("/tmp/sub/")
    53→    write_ws.write("/tmp/sub/f.txt", b"x")
    54→    with pytest.raises(IsADirectoryError):
    55→        rm(write_ws, "/tmp/sub/")
    56→
    57→
    58→def test_rm_directory_recursive(write_ws):
    59→    write_ws.mkdir("/tmp/sub/")
    60→    write_ws.write("/tmp/sub/a.txt", b"a")
    61→    write_ws.write("/tmp/sub/b.txt", b"b")
    62→    rm(write_ws, "/tmp/sub/", recursive=True)
    63→    assert not write_ws.exists("/tmp/sub/a.txt")
    64→    assert not write_ws.exists("/tmp/sub/b.txt")
    65→
    66→
    67→def test_sed_replaces_pattern(write_ws):
    68→    write_ws.write("/tmp/f.txt", b"hello world")
    69→    sed(write_ws, "/tmp/f.txt", "world", "earth")
    70→    assert write_ws.cat("/tmp/f.txt") == b"hello earth"
    71→
    72→
    73→def test_sed_with_regex(write_ws):
    74→    write_ws.write("/tmp/f.txt", b"foo123bar")
    75→    sed(write_ws, "/tmp/f.txt", r"\d+", "NUM")
    76→    assert write_ws.cat("/tmp/f.txt") == b"fooNUMbar"
    77→
    78→
    79→def test_sed_no_match_unchanged(write_ws):
    80→    write_ws.write("/tmp/f.txt", b"hello")
    81→    sed(write_ws, "/tmp/f.txt", "xyz", "abc")
    82→    assert write_ws.cat("/tmp/f.txt") == b"hello"
    83→
    84→
    85→def test_echo_writes_content(write_ws):
    86→    echo(write_ws, "/tmp/out.txt", "hello\n")
    87→    assert write_ws.cat("/tmp/out.txt") == b"hello\n"
    88→
    89→
    90→def test_echo_append_mode(write_ws):
    91→    write_ws.write("/tmp/out.txt", b"first\n")
    92→    echo(write_ws, "/tmp/out.txt", "second\n", append=True)
    93→    assert write_ws.cat("/tmp/out.txt") == b"first\nsecond\n"
    94→
    95→
    96→def test_echo_creates_if_missing(write_ws):
    97→    echo(write_ws, "/tmp/new.txt", "content", append=True)
    98→    assert write_ws.cat("/tmp/new.txt") == b"content"
    99→
   100→
   101→def test_tee_writes_and_returns_data(write_ws):
   102→    result = tee(write_ws, "/tmp/out.txt", b"tee data")
   103→    assert result == b"tee data"
   104→    assert write_ws.cat("/tmp/out.txt") == b"tee data"
   105→
   106→
   107→def test_tee_creates_file(write_ws):
   108→    tee(write_ws, "/tmp/created.txt", b"new")
   109→    assert write_ws.exists("/tmp/created.txt")
   110→
   111→
   112→def test_tee_replaces_existing(write_ws):
   113→    write_ws.write("/tmp/existing.txt", b"old")
   114→    tee(write_ws, "/tmp/existing.txt", b"new")
   115→    assert write_ws.cat("/tmp/existing.txt") == b"new"
   116→


     1→import pytest
     2→
     3→from mirage.commands.builtin.handlers import (COMMANDS, _cat, _cp, _cut, _diff,
     4→                                              _du, _echo, _file, _find, _grep,
     5→                                              _head, _ls, _md5, _mkdir, _mv,
     6→                                              _pwd, _resolve, _rm, _sed, _sort,
     7→                                              _stat, _tail, _tee, _touch, _tr,
     8→                                              _tree, _uniq, _wc)
     9→
    10→# --- _resolve ---
    11→
    12→
    13→def test_resolve_absolute_path():
    14→    assert _resolve("/some/cwd", "/abs/path") == "/abs/path"
    15→
    16→
    17→def test_resolve_relative_path():
    18→    assert _resolve("/some/cwd", "rel") == "/some/cwd/rel"
    19→
    20→
    21→def test_resolve_relative_with_dotdot():
    22→    assert _resolve("/some/cwd", "../other") == "/some/other"
    23→
    24→
    25→def test_resolve_from_trailing_slash_cwd():
    26→    assert _resolve("/some/cwd/", "rel") == "/some/cwd/rel"
    27→
    28→
    29→# --- ls ---
    30→
    31→
    32→def test_ls_handler(write_ws):
    33→    write_ws.write("/tmp/a.txt", b"hello")
    34→    result = _ls(write_ws, [], "/tmp/")
    35→    assert "a.txt" in result
    36→
    37→
    38→def test_ls_long_format(write_ws):
    39→    write_ws.write("/tmp/a.txt", b"hello")
    40→    result = _ls(write_ws, ["-l"], "/tmp/")
    41→    assert "a.txt" in result
    42→
    43→
    44→def test_ls_combined_flags(write_ws):
    45→    write_ws.write("/tmp/a.txt", b"hello")
    46→    result = _ls(write_ws, ["-la"], "/tmp/")
    47→    assert "a.txt" in result
    48→
    49→
    50→def test_ls_all_files_flag(write_ws):
    51→    write_ws.write("/tmp/a.txt", b"hello")
    52→    result = _ls(write_ws, ["-a"], "/tmp/")
    53→    assert "a.txt" in result
    54→
    55→
    56→def test_ls_sort_by_size(write_ws):
    57→    write_ws.write("/tmp/small.txt", b"x")
    58→    write_ws.write("/tmp/large.txt", b"x" * 100)
    59→    result = _ls(write_ws, ["-S"], "/tmp/")
    60→    assert "large.txt" in result and "small.txt" in result
    61→
    62→
    63→def test_ls_reverse_sort(write_ws):
    64→    write_ws.write("/tmp/a.txt", b"x")
    65→    write_ws.write("/tmp/z.txt", b"x")
    66→    result = _ls(write_ws, ["-r"], "/tmp/")
    67→    lines = result.split("\n")
    68→    assert lines[0] == "z.txt"
    69→
    70→
    71→def test_ls_recursive(write_ws):
    72→    write_ws.mkdir("/tmp/sub/")
    73→    write_ws.write("/tmp/sub/f.txt", b"x")
    74→    result = _ls(write_ws, ["-R"], "/tmp/")
    75→    assert "f.txt" in result
    76→
    77→
    78→def test_ls_list_dir(write_ws):
    79→    write_ws.mkdir("/tmp/mydir/")
    80→    result = _ls(write_ws, ["-d"], "/tmp/mydir/")
    81→    assert "mydir" in result
    82→
    83→
    84→def test_ls_human_readable(write_ws):
    85→    write_ws.write("/tmp/a.txt", b"x" * 2048)
    86→    result = _ls(write_ws, ["-lh"], "/tmp/")
    87→    assert "a.txt" in result
    88→
    89→
    90→def test_ls_unknown_flag_raises(write_ws):
    91→    with pytest.raises(ValueError, match="invalid option"):
    92→        _ls(write_ws, ["-z"], "/tmp/")
    93→
    94→
    95→# --- stat ---
    96→
    97→
    98→def test_stat_handler(write_ws):
    99→    write_ws.write("/tmp/f.txt", b"x")
   100→    result = _stat(write_ws, ["/tmp/f.txt"], "/")
   101→    assert "name=" in result
   102→
   103→
   104→def test_stat_missing_operand(write_ws):
   105→    with pytest.raises(ValueError, match="missing operand"):
   106→        _stat(write_ws, [], "/")
   107→
   108→
   109→def test_stat_unknown_flag_raises(write_ws):
   110→    with pytest.raises(ValueError, match="invalid option"):
   111→        _stat(write_ws, ["-z"], "/")
   112→
   113→
   114→# --- pwd ---
   115→
   116→
   117→def test_pwd_handler(write_ws):
   118→    assert _pwd(write_ws, [], "/tmp/") == "/tmp/"
   119→
   120→
   121→# --- find ---
   122→
   123→
   124→def test_find_handler(write_ws):
   125→    write_ws.write("/tmp/a.txt", b"")
   126→    result = _find(write_ws, [], "/tmp/")
   127→    assert "/tmp/a.txt" in result
   128→
   129→
   130→def test_find_with_name_filter(write_ws):
   131→    write_ws.write("/tmp/a.txt", b"")
   132→    write_ws.write("/tmp/b.log", b"")
   133→    result = _find(write_ws, ["/tmp/", "-name", "*.txt"], "/")
   134→    assert "/tmp/a.txt" in result
   135→    assert "/tmp/b.log" not in result
   136→
   137→
   138→def test_find_type_file(write_ws):
   139→    write_ws.mkdir("/tmp/sub/")
   140→    write_ws.write("/tmp/f.txt", b"x")
   141→    result = _find(write_ws, ["/tmp/", "-type", "f"], "/")
   142→    assert "/tmp/f.txt" in result
   143→    assert "/tmp/sub/" not in result
   144→
   145→
   146→def test_find_type_directory(write_ws):
   147→    write_ws.mkdir("/tmp/sub/")
   148→    write_ws.write("/tmp/f.txt", b"x")
   149→    result = _find(write_ws, ["/tmp/", "-type", "d"], "/")
   150→    assert "sub" in result
   151→    assert "/tmp/f.txt" not in result
   152→
   153→
   154→def test_find_maxdepth(write_ws):
   155→    write_ws.mkdir("/tmp/sub/")
   156→    write_ws.write("/tmp/f.txt", b"x")
   157→    write_ws.write("/tmp/sub/deep.txt", b"y")
   158→    result = _find(write_ws, ["/tmp/", "-maxdepth", "0"], "/")
   159→    assert "/tmp/f.txt" in result
   160→    assert "/tmp/sub/deep.txt" not in result
   161→
   162→
   163→def test_find_unknown_flag_raises(write_ws):
   164→    with pytest.raises(ValueError, match="invalid option"):
   165→        _find(write_ws, ["-z"], "/")
   166→
   167→
   168→# --- tree / du ---
   169→
   170→
   171→def test_tree_handler(write_ws):
   172→    write_ws.write("/tmp/f.txt", b"")
   173→    result = _tree(write_ws, [], "/tmp/")
   174→    assert isinstance(result, str)
   175→
   176→
   177→def test_du_handler(write_ws):
   178→    write_ws.write("/tmp/f.txt", b"hello")
   179→    result = _du(write_ws, [], "/tmp/")
   180→    assert result.isdigit()
   181→
   182→
   183→def test_du_human_readable(write_ws):
   184→    write_ws.write("/tmp/f.txt", b"x" * 2048)
   185→    result = _du(write_ws, ["-h"], "/tmp/")
   186→    assert any(u in result for u in ("B", "K", "M", "G"))
   187→
   188→
   189→def test_du_unknown_flag_raises(write_ws):
   190→    with pytest.raises(ValueError, match="invalid option"):
   191→        _du(write_ws, ["-z"], "/tmp/")
   192→
   193→
   194→# --- cat ---
   195→
   196→
   197→def test_cat_handler(write_ws):
   198→    write_ws.write("/tmp/f.txt", b"world")
   199→    assert _cat(write_ws, ["/tmp/f.txt"], "/") == "world"
   200→
   201→
   202→def test_cat_missing_operand(write_ws):
   203→    with pytest.raises(ValueError, match="missing operand"):
   204→        _cat(write_ws, [], "/")
   205→
   206→
   207→# --- head ---
   208→
   209→
   210→def test_head_handler(write_ws):
   211→    lines = "\n".join(f"line{i}" for i in range(20))
   212→    write_ws.write("/tmp/f.txt", lines.encode())
   213→    result = _head(write_ws, ["-n", "3", "/tmp/f.txt"], "/")
   214→    assert result == "line0\nline1\nline2"
   215→
   216→
   217→def test_head_embedded_n_flag(write_ws):
   218→    lines = "\n".join(f"line{i}" for i in range(20))
   219→    write_ws.write("/tmp/f.txt", lines.encode())
   220→    result = _head(write_ws, ["-n3", "/tmp/f.txt"], "/")
   221→    assert result == "line0\nline1\nline2"
   222→
   223→
   224→def test_head_bytes_mode(write_ws):
   225→    write_ws.write("/tmp/f.txt", b"hello world")
   226→    result = _head(write_ws, ["-c", "5", "/tmp/f.txt"], "/")
   227→    assert result == "hello"
   228→
   229→
   230→def test_head_bytes_mode_embedded(write_ws):
   231→    write_ws.write("/tmp/f.txt", b"hello world")
   232→    result = _head(write_ws, ["-c5", "/tmp/f.txt"], "/")
   233→    assert result == "hello"
   234→
   235→
   236→def test_head_unknown_flag_raises(write_ws):
   237→    with pytest.raises(ValueError, match="invalid option"):
   238→        _head(write_ws, ["-z"], "/")
   239→
   240→
   241→def test_head_missing_operand(write_ws):
   242→    with pytest.raises(ValueError, match="missing operand"):
   243→        _head(write_ws, [], "/")
   244→
   245→
   246→# --- tail ---
   247→
   248→
   249→def test_tail_handler(write_ws):
   250→    lines = "\n".join(f"line{i}" for i in range(20))
   251→    write_ws.write("/tmp/f.txt", lines.encode())
   252→    result = _tail(write_ws, ["-n", "3", "/tmp/f.txt"], "/")
   253→    assert result == "line17\nline18\nline19"
   254→
   255→
   256→def test_tail_embedded_n_flag(write_ws):
   257→    lines = "\n".join(f"line{i}" for i in range(20))
   258→    write_ws.write("/tmp/f.txt", lines.encode())
   259→    result = _tail(write_ws, ["-n3", "/tmp/f.txt"], "/")
   260→    assert result == "line17\nline18\nline19"
   261→
   262→
   263→def test_tail_bytes_mode(write_ws):
   264→    write_ws.write("/tmp/f.txt", b"hello world")
   265→    result = _tail(write_ws, ["-c", "5", "/tmp/f.txt"], "/")
   266→    assert result == "world"
   267→
   268→
   269→def test_tail_unknown_flag_raises(write_ws):
   270→    with pytest.raises(ValueError, match="invalid option"):
   271→        _tail(write_ws, ["-z"], "/")
   272→
   273→
   274→def test_tail_missing_operand(write_ws):
   275→    with pytest.raises(ValueError, match="missing operand"):
   276→        _tail(write_ws, [], "/")
   277→
   278→
   279→# --- wc ---
   280→
   281→
   282→def test_wc_handler(write_ws):
   283→    write_ws.write("/tmp/f.txt", b"one two\nthree\n")
   284→    result = _wc(write_ws, ["/tmp/f.txt"], "/")
   285→    assert result == "2\t3\t14"
   286→
   287→
   288→def test_wc_lines_only(write_ws):
   289→    write_ws.write("/tmp/f.txt", b"one\ntwo\nthree\n")
   290→    result = _wc(write_ws, ["-l", "/tmp/f.txt"], "/")
   291→    assert result == "3"
   292→
   293→
   294→def test_wc_words_only(write_ws):
   295→    write_ws.write("/tmp/f.txt", b"one two three\n")
   296→    result = _wc(write_ws, ["-w", "/tmp/f.txt"], "/")
   297→    assert result == "3"
   298→
   299→
   300→def test_wc_bytes_only(write_ws):
   301→    write_ws.write("/tmp/f.txt", b"hello\n")
   302→    result = _wc(write_ws, ["-c", "/tmp/f.txt"], "/")
   303→    assert result == "6"
   304→
   305→
   306→def test_wc_unknown_flag_raises(write_ws):
   307→    with pytest.raises(ValueError, match="invalid option"):
   308→        _wc(write_ws, ["-z", "/tmp/f.txt"], "/")
   309→
   310→
   311→def test_wc_missing_operand(write_ws):
   312→    with pytest.raises(ValueError, match="missing operand"):
   313→        _wc(write_ws, [], "/")
   314→
   315→
   316→# --- md5 ---
   317→
   318→
   319→def test_md5_handler(write_ws):
   320→    write_ws.write("/tmp/f.txt", b"data")
   321→    result = _md5(write_ws, ["/tmp/f.txt"], "/")
   322→    assert len(result) == 32
   323→
   324→
   325→def test_md5_missing_operand(write_ws):
   326→    with pytest.raises(ValueError, match="missing operand"):
   327→        _md5(write_ws, [], "/")
   328→
   329→
   330→# --- diff ---
   331→
   332→
   333→def test_diff_handler(write_ws):
   334→    write_ws.write("/tmp/a.txt", b"hello\n")
   335→    write_ws.write("/tmp/b.txt", b"world\n")
   336→    result = _diff(write_ws, ["/tmp/a.txt", "/tmp/b.txt"], "/")
   337→    assert "-hello" in result
   338→
   339→
   340→def test_diff_missing_paths(write_ws):
   341→    with pytest.raises(ValueError, match="requires two paths"):
   342→        _diff(write_ws, ["/tmp/a.txt"], "/")
   343→
   344→
   345→# --- file ---
   346→
   347→
   348→def test_file_handler(write_ws):
   349→    write_ws.write("/tmp/f.txt", b"plain text here")
   350→    assert _file(write_ws, ["/tmp/f.txt"], "/") == "text"
   351→
   352→
   353→def test_file_missing_operand(write_ws):
   354→    with pytest.raises(ValueError, match="missing operand"):
   355→        _file(write_ws, [], "/")
   356→
   357→
   358→# --- grep ---
   359→
   360→
   361→def test_grep_handler(write_ws):
   362→    write_ws.write("/tmp/f.txt", b"ERROR line\nINFO line\n")
   363→    result = _grep(write_ws, ["ERROR", "/tmp/f.txt"], "/")
   364→    assert "ERROR" in result
   365→    assert "INFO" not in result
   366→
   367→
   368→def test_grep_combined_flags(write_ws):
   369→    write_ws.write("/tmp/f.txt", b"Error line\nINFO line\n")
   370→    result = _grep(write_ws, ["-iv", "error", "/tmp/f.txt"], "/")
   371→    assert "INFO" in result
   372→    assert "Error" not in result
   373→
   374→
   375→def test_grep_line_numbers(write_ws):
   376→    write_ws.write("/tmp/f.txt", b"foo\nbar\nfoo\n")
   377→    result = _grep(write_ws, ["-n", "foo", "/tmp/f.txt"], "/")
   378→    assert "1:foo" in result
   379→    assert "3:foo" in result
   380→
   381→
   382→def test_grep_count_only(write_ws):
   383→    write_ws.write("/tmp/f.txt", b"foo\nbar\nfoo\n")
   384→    result = _grep(write_ws, ["-c", "foo", "/tmp/f.txt"], "/")
   385→    assert result == "2"
   386→
   387→
   388→def test_grep_files_only(write_ws):
   389→    write_ws.write("/tmp/f.txt", b"hello world\n")
   390→    result = _grep(write_ws, ["-l", "hello", "/tmp/f.txt"], "/")
   391→    assert "/tmp/f.txt" in result
   392→
   393→
   394→def test_grep_whole_word(write_ws):
   395→    write_ws.write("/tmp/f.txt", b"foo\nfoobar\n")
   396→    result = _grep(write_ws, ["-w", "foo", "/tmp/f.txt"], "/")
   397→    assert "foo" in result
   398→    assert "foobar" not in result
   399→
   400→
   401→def test_grep_fixed_string(write_ws):
   402→    write_ws.write("/tmp/f.txt", b"a.b\naXb\n")
   403→    result = _grep(write_ws, ["-F", "a.b", "/tmp/f.txt"], "/")
   404→    assert "a.b" in result
   405→    assert "aXb" not in result
   406→
   407→
   408→def test_grep_only_matching(write_ws):
   409→    write_ws.write("/tmp/f.txt", b"hello world\n")
   410→    result = _grep(write_ws, ["-o", "hello", "/tmp/f.txt"], "/")
   411→    assert result == "hello"
   412→
   413→
   414→def test_grep_max_count(write_ws):
   415→    write_ws.write("/tmp/f.txt", b"a\na\na\na\n")
   416→    result = _grep(write_ws, ["-m", "2", "a", "/tmp/f.txt"], "/")
   417→    assert result.count("a") == 2
   418→
   419→
   420→def test_grep_unknown_flag_raises(write_ws):
   421→    with pytest.raises(ValueError, match="invalid option"):
   422→        _grep(write_ws, ["-z", "pat", "/tmp/f.txt"], "/")
   423→
   424→
   425→def test_grep_missing_args(write_ws):
   426→    with pytest.raises(ValueError, match="usage"):
   427→        _grep(write_ws, ["ERROR"], "/")
   428→
   429→
   430→# --- sort ---
   431→
   432→
   433→def test_sort_handler(write_ws):
   434→    write_ws.write("/tmp/f.txt", b"b\na\nc\n")
   435→    result = _sort(write_ws, ["/tmp/f.txt"], "/")
   436→    assert result == "a\nb\nc"
   437→
   438→
   439→def test_sort_reverse(write_ws):
   440→    write_ws.write("/tmp/f.txt", b"b\na\nc\n")
   441→    result = _sort(write_ws, ["-r", "/tmp/f.txt"], "/")
   442→    assert result == "c\nb\na"
   443→
   444→
   445→def test_sort_numeric(write_ws):
   446→    write_ws.write("/tmp/f.txt", b"10\n2\n1\n20\n")
   447→    result = _sort(write_ws, ["-n", "/tmp/f.txt"], "/")
   448→    assert result == "1\n2\n10\n20"
   449→
   450→
   451→def test_sort_key_field(write_ws):
   452→    write_ws.write("/tmp/f.txt", b"b 2\na 1\nc 3\n")
   453→    result = _sort(write_ws, ["-k", "2", "/tmp/f.txt"], "/")
   454→    assert result.split("\n")[0] == "a 1"
   455→
   456→
   457→def test_sort_unique(write_ws):
   458→    write_ws.write("/tmp/f.txt", b"a\nb\na\n")
   459→    result = _sort(write_ws, ["-u", "/tmp/f.txt"], "/")
   460→    assert result.count("a") == 1
   461→
   462→
   463→def test_sort_ignore_case(write_ws):
   464→    write_ws.write("/tmp/f.txt", b"B\na\nC\n")
   465→    result = _sort(write_ws, ["-f", "/tmp/f.txt"], "/")
   466→    lines = result.split("\n")
   467→    assert lines[0].lower() == "a"
   468→
   469→
   470→def test_sort_unknown_flag_raises(write_ws):
   471→    with pytest.raises(ValueError, match="invalid option"):
   472→        _sort(write_ws, ["-z", "/tmp/f.txt"], "/")
   473→
   474→
   475→def test_sort_missing_operand(write_ws):
   476→    with pytest.raises(ValueError, match="missing operand"):
   477→        _sort(write_ws, [], "/")
   478→
   479→
   480→# --- uniq ---
   481→
   482→
   483→def test_uniq_handler(write_ws):
   484→    write_ws.write("/tmp/f.txt", b"a\na\nb\n")
   485→    result = _uniq(write_ws, ["/tmp/f.txt"], "/")
   486→    assert result == "a\nb"
   487→
   488→
   489→def test_uniq_count(write_ws):
   490→    write_ws.write("/tmp/f.txt", b"a\na\nb\n")
   491→    result = _uniq(write_ws, ["-c", "/tmp/f.txt"], "/")
   492→    assert "2" in result
   493→    assert "1" in result
   494→
   495→
   496→def test_uniq_duplicates_only(write_ws):
   497→    write_ws.write("/tmp/f.txt", b"a\na\nb\n")
   498→    result = _uniq(write_ws, ["-d", "/tmp/f.txt"], "/")
   499→    assert "a" in result
   500→    assert "b" not in result
   501→
   502→
   503→def test_uniq_unique_only(write_ws):
   504→    write_ws.write("/tmp/f.txt", b"a\na\nb\n")
   505→    result = _uniq(write_ws, ["-u", "/tmp/f.txt"], "/")
   506→    assert "b" in result
   507→    assert result.count("a") == 0
   508→
   509→
   510→def test_uniq_unknown_flag_raises(write_ws):
   511→    with pytest.raises(ValueError, match="invalid option"):
   512→        _uniq(write_ws, ["-z", "/tmp/f.txt"], "/")
   513→
   514→
   515→def test_uniq_missing_operand(write_ws):
   516→    with pytest.raises(ValueError, match="missing operand"):
   517→        _uniq(write_ws, [], "/")
   518→
   519→
   520→# --- cut ---
   521→
   522→
   523→def test_cut_handler(write_ws):
   524→    write_ws.write("/tmp/f.txt", b"a\tb\tc\n")
   525→    result = _cut(write_ws, ["-f", "2", "/tmp/f.txt"], "/")
   526→    assert result == "b"
   527→
   528→
   529→def test_cut_embedded_f_flag(write_ws):
   530→    write_ws.write("/tmp/f.txt", b"a\tb\tc\n")
   531→    result = _cut(write_ws, ["-f2", "/tmp/f.txt"], "/")
   532→    assert result == "b"
   533→
   534→
   535→def test_cut_custom_delimiter(write_ws):
   536→    write_ws.write("/tmp/f.txt", b"a,b,c\n")
   537→    result = _cut(write_ws, ["-d", ",", "-f", "2", "/tmp/f.txt"], "/")
   538→    assert result == "b"
   539→
   540→
   541→def test_cut_chars_mode(write_ws):
   542→    write_ws.write("/tmp/f.txt", b"hello world\n")
   543→    result = _cut(write_ws, ["-c", "1-5", "/tmp/f.txt"], "/")
   544→    assert result == "hello"
   545→
   546→
   547→def test_cut_unknown_flag_raises(write_ws):
   548→    with pytest.raises(ValueError, match="invalid option"):
   549→        _cut(write_ws, ["-z", "/tmp/f.txt"], "/")
   550→
   551→
   552→def test_cut_missing_operand(write_ws):
   553→    with pytest.raises(ValueError, match="missing operand"):
   554→        _cut(write_ws, [], "/")
   555→
   556→
   557→# --- tr ---
   558→
   559→
   560→def test_tr_handler(write_ws):
   561→    write_ws.write("/tmp/f.txt", b"hello")
   562→    result = _tr(write_ws, ["/tmp/f.txt", "h", "H"], "/")
   563→    assert result == "Hello"
   564→
   565→
   566→def test_tr_missing_args(write_ws):
   567→    with pytest.raises(ValueError, match="usage"):
   568→        _tr(write_ws, ["/tmp/f.txt", "h"], "/")
   569→
   570→
   571→# --- write handlers ---
   572→
   573→
   574→def test_mkdir_handler(write_ws):
   575→    _mkdir(write_ws, ["/tmp/newdir/"], "/")
   576→    result = _ls(write_ws, [], "/tmp/")
   577→    assert "newdir" in result
   578→
   579→
   580→def test_mkdir_missing_operand(write_ws):
   581→    with pytest.raises(ValueError, match="missing operand"):
   582→        _mkdir(write_ws, [], "/")
   583→
   584→
   585→def test_touch_handler(write_ws):
   586→    _touch(write_ws, ["/tmp/new.txt"], "/")
   587→    assert _cat(write_ws, ["/tmp/new.txt"], "/") == ""
   588→
   589→
   590→def test_touch_missing_operand(write_ws):
   591→    with pytest.raises(ValueError, match="missing operand"):
   592→        _touch(write_ws, [], "/")
   593→
   594→
   595→def test_cp_handler(write_ws):
   596→    write_ws.write("/tmp/src.txt", b"copy me")
   597→    _cp(write_ws, ["/tmp/src.txt", "/tmp/dst.txt"], "/")
   598→    assert _cat(write_ws, ["/tmp/dst.txt"], "/") == "copy me"
   599→
   600→
   601→def test_cp_missing_args(write_ws):
   602→    with pytest.raises(ValueError, match="requires src and dst"):
   603→        _cp(write_ws, ["/tmp/src.txt"], "/")
   604→
   605→
   606→def test_mv_handler(write_ws):
   607→    write_ws.write("/tmp/old.txt", b"move me")
   608→    _mv(write_ws, ["/tmp/old.txt", "/tmp/new.txt"], "/")
   609→    assert _cat(write_ws, ["/tmp/new.txt"], "/") == "move me"
   610→
   611→
   612→def test_mv_missing_args(write_ws):
   613→    with pytest.raises(ValueError, match="requires src and dst"):
   614→        _mv(write_ws, ["/tmp/src.txt"], "/")
   615→
   616→
   617→def test_rm_handler(write_ws):
   618→    write_ws.write("/tmp/f.txt", b"bye")
   619→    _rm(write_ws, ["/tmp/f.txt"], "/")
   620→    result = _ls(write_ws, [], "/tmp/")
   621→    assert "f.txt" not in result
   622→
   623→
   624→def test_rm_recursive(write_ws):
   625→    write_ws.mkdir("/tmp/subdir/")
   626→    write_ws.write("/tmp/subdir/f.txt", b"x")
   627→    _rm(write_ws, ["-r", "/tmp/subdir/"], "/")
   628→    result = _ls(write_ws, [], "/tmp/")
   629→    assert "subdir" not in result
   630→
   631→
   632→def test_rm_force_no_error_if_missing(write_ws):
   633→    _rm(write_ws, ["-f", "/tmp/nonexistent.txt"], "/")
   634→
   635→
   636→def test_rm_verbose(write_ws):
   637→    write_ws.write("/tmp/f.txt", b"x")
   638→    result = _rm(write_ws, ["-v", "/tmp/f.txt"], "/")
   639→    assert "removed" in result
   640→
   641→
   642→def test_rm_unknown_flag_raises(write_ws):
   643→    with pytest.raises(ValueError, match="invalid option"):
   644→        _rm(write_ws, ["-z", "/tmp/f.txt"], "/")
   645→
   646→
   647→def test_rm_missing_operand(write_ws):
   648→    with pytest.raises(ValueError, match="missing operand"):
   649→        _rm(write_ws, [], "/")
   650→
   651→
   652→def test_sed_handler(write_ws):
   653→    write_ws.write("/tmp/f.txt", b"hello world")
   654→    _sed(write_ws, ["s/hello/hi/", "/tmp/f.txt"], "/")
   655→    assert _cat(write_ws, ["/tmp/f.txt"], "/") == "hi world"
   656→
   657→
   658→def test_sed_missing_args(write_ws):
   659→    with pytest.raises(ValueError, match="usage"):
   660→        _sed(write_ws, ["s/a/b/"], "/")
   661→
   662→
   663→def test_sed_bad_expression(write_ws):
   664→    write_ws.write("/tmp/f.txt", b"x")
   665→    with pytest.raises(ValueError, match="unsupported expression"):
   666→        _sed(write_ws, ["bad/expr/here/x", "/tmp/f.txt"], "/")
   667→
   668→
   669→def test_echo_no_redirect(write_ws):
   670→    assert _echo(write_ws, ["hello", "world"], "/") == "hello world"
   671→
   672→
   673→def test_echo_redirect(write_ws):
   674→    _echo(write_ws, ["hello", ">", "/tmp/out.txt"], "/")
   675→    assert _cat(write_ws, ["/tmp/out.txt"], "/") == "hello\n"
   676→
   677→
   678→def test_echo_append(write_ws):
   679→    write_ws.write("/tmp/f.txt", b"first\n")
   680→    _echo(write_ws, ["second", ">>", "/tmp/f.txt"], "/")
   681→    result = _cat(write_ws, ["/tmp/f.txt"], "/")
   682→    assert "first" in result and "second" in result
   683→
   684→
   685→def test_tee_handler(write_ws):
   686→    result = _tee(write_ws, ["hello", "/tmp/out.txt"], "/")
   687→    assert result == "hello"
   688→    assert _cat(write_ws, ["/tmp/out.txt"], "/") == "hello"
   689→
   690→
   691→def test_tee_missing_operand(write_ws):
   692→    with pytest.raises(ValueError, match="missing operand"):
   693→        _tee(write_ws, [], "/")
   694→
   695→
   696→# --- COMMANDS dict ---
   697→
   698→
   699→def test_commands_contains_all_keys():
   700→    expected = {
   701→        "ls", "stat", "pwd", "find", "tree", "du", "cat", "head", "tail", "wc",
   702→        "md5", "diff", "file", "grep", "sort", "uniq", "cut", "tr", "mkdir",
   703→        "touch", "cp", "mv", "rm", "sed", "echo", "tee", "curl", "wget"
   704→    }
   705→    assert expected.issubset(set(COMMANDS.keys()))
   706→


     1→import base64
     2→import inspect
     3→import json
     4→from unittest.mock import MagicMock, patch
     5→
     6→import httpx
     7→import pytest
     8→
     9→from mirage.commands.remote.client import (CommandClient, CommandClientError,
    10→                                           GRPCCommandClient,
    11→                                           HTTPCommandClient)
    12→from mirage.types import CommandResult, FilePayload
    13→
    14→
    15→def test_command_client_has_run_with_files():
    16→    sig = inspect.signature(CommandClient.run)
    17→    params = list(sig.parameters.keys())
    18→    assert "stdin" in params
    19→    assert "files" in params
    20→
    21→
    22→def test_command_client_run_returns_command_result():
    23→    sig = inspect.signature(CommandClient.run)
    24→    assert sig.return_annotation is CommandResult
    25→
    26→
    27→def test_http_client_run_simple(httpx_mock):
    28→
    29→    def handler(request):
    30→        body = json.loads(request.content)
    31→        assert body["command"] == "echo hi"
    32→        assert body["cwd"] == "/"
    33→        assert body.get("stdin") is None
    34→        assert body.get("input_files") is None
    35→        return httpx.Response(200,
    36→                              json={
    37→                                  "ok": True,
    38→                                  "output": "hi\n",
    39→                                  "output_files": {},
    40→                              })
    41→
    42→    httpx_mock.add_callback(handler)
    43→    client = HTTPCommandClient("http://localhost:9999")
    44→    result = client.run("echo hi")
    45→    assert isinstance(result, CommandResult)
    46→    assert result.stdout == "hi\n"
    47→    assert result.output_files == {}
    48→
    49→
    50→def test_http_client_run_with_files(httpx_mock):
    51→
    52→    def handler(request):
    53→        body = json.loads(request.content)
    54→        assert "/data/in.json" in body["input_files"]
    55→        payload = body["input_files"]["/data/in.json"]
    56→        assert payload["checksum"] == "abc123"
    57→        decoded = base64.b64decode(payload["data"])
    58→        assert decoded == b'{"key": "value"}'
    59→        return httpx.Response(
    60→            200,
    61→            json={
    62→                "ok": True,
    63→                "output": "done",
    64→                "output_files": {
    65→                    "/data/out.csv": {
    66→                        "checksum": "def456",
    67→                        "data": base64.b64encode(b"a,b\n1,2").decode(),
    68→                    }
    69→                },
    70→            })
    71→
    72→    httpx_mock.add_callback(handler)
    73→    client = HTTPCommandClient("http://localhost:9999")
    74→    result = client.run(
    75→        "transform /data/in.json -o /data/out.csv",
    76→        files={
    77→            "/data/in.json":
    78→            FilePayload(checksum="abc123", data=b'{"key": "value"}'),
    79→        },
    80→    )
    81→    assert result.stdout == "done"
    82→    assert result.output_files["/data/out.csv"] == b"a,b\n1,2"
    83→
    84→
    85→def test_http_client_run_with_stdin(httpx_mock):
    86→
    87→    def handler(request):
    88→        body = json.loads(request.content)
    89→        stdin_data = base64.b64decode(body["stdin"])
    90→        assert stdin_data == b"piped data"
    91→        return httpx.Response(200,
    92→                              json={
    93→                                  "ok": True,
    94→                                  "output": "received",
    95→                                  "output_files": {},
    96→                              })
    97→
    98→    httpx_mock.add_callback(handler)
    99→    client = HTTPCommandClient("http://localhost:9999")
   100→    result = client.run("process", stdin=b"piped data")
   101→    assert result.stdout == "received"
   102→
   103→
   104→def test_http_client_run_checksum_only(httpx_mock):
   105→
   106→    def handler(request):
   107→        body = json.loads(request.content)
   108→        payload = body["input_files"]["/data/in.json"]
   109→        assert payload["checksum"] == "abc123"
   110→        assert payload["data"] is None
   111→        return httpx.Response(200,
   112→                              json={
   113→                                  "ok": True,
   114→                                  "output": "cached",
   115→                                  "output_files": {},
   116→                              })
   117→
   118→    httpx_mock.add_callback(handler)
   119→    client = HTTPCommandClient("http://localhost:9999")
   120→    result = client.run(
   121→        "transform /data/in.json",
   122→        files={"/data/in.json": FilePayload(checksum="abc123", data=None)},
   123→    )
   124→    assert result.stdout == "cached"
   125→
   126→
   127→def test_http_client_run_412_cache_miss(httpx_mock):
   128→    httpx_mock.add_response(status_code=412,
   129→                            json={
   130→                                "missing": ["/data/in.json"],
   131→                            })
   132→    client = HTTPCommandClient("http://localhost:9999")
   133→    with pytest.raises(CommandClientError, match="cache miss"):
   134→        client.run(
   135→            "transform /data/in.json",
   136→            files={"/data/in.json": FilePayload(checksum="abc123", data=None)},
   137→        )
   138→
   139→
   140→def test_grpc_client_run_returns_command_result():
   141→    mock_stub = MagicMock()
   142→    mock_resp = MagicMock()
   143→    mock_resp.ok = True
   144→    mock_resp.output = "hello"
   145→    mock_resp.output_files = {}
   146→    mock_stub.RunCommand.return_value = mock_resp
   147→
   148→    with patch("mirage.commands.remote.client.grpc") as mock_grpc:
   149→        mock_grpc.insecure_channel.return_value = MagicMock()
   150→        client = GRPCCommandClient("localhost", port=50051)
   151→        client._stub = mock_stub
   152→        result = client.run("echo hello")
   153→        assert isinstance(result, CommandResult)
   154→        assert result.stdout == "hello"
   155→


     1→from mirage.backend.local.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→from unittest.mock import MagicMock
     2→
     3→from mirage.backend.local.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→


wait why init in tests remove them


Good point — test directories don't need `__init__.py` files. pytest discovers tests without them. Let me update the plan and skip creating those files.


TodoWrite

Create 15 new test directories (no __init__.py)

Execute 15 pure git mv moves

Execute 3 renames (git mv)

Split test_configs.py into object_storage tests

Split test_default.py into fuse/backend tests

Split test_format_ops.py into csv and parquet tests

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

Merge types test files into tests/test_types.py

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

Remove existing __init__.py files from tests/ and cleanup

Removing __init__.py files and cleaning up

Run tests and pre-commit to verify


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

Create 15 new directories with __init__.py files

Execute 15 pure git mv moves

Execute 3 renames (git mv)

Split test_configs.py into object_storage tests

Split test_default.py into fuse/backend tests

Split test_format_ops.py into csv and parquet tests

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

Merge types test files into tests/test_types.py

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

Cleanup empty directories and stale files

Run tests and pre-commit to verify

Create 15 new test directories (no __init__.py)

Execute 15 pure git mv moves

Execute 3 renames (git mv)

Remove existing __init__.py files from tests/ and cleanup

Removing __init__.py files and cleaning up

Run tests and pre-commit to verify


Let me first check which `__init__.py` files currently exist in tests/.


/Users/agent/strukto/mirage/tests/__init__.py
/Users/agent/strukto/mirage/tests/cli/__init__.py
/Users/agent/strukto/mirage/tests/commands/__init__.py
/Users/agent/strukto/mirage/tests/server/__init__.py
/Users/agent/strukto/mirage/tests/workspace/__init__.py
/Users/agent/strukto/mirage/tests/workspace/cache/__init__.py
/Users/agent/strukto/mirage/tests/backend/__init__.py
/Users/agent/strukto/mirage/tests/runtime/__init__.py
/Users/agent/strukto/mirage/tests/observe/__init__.py
/Users/agent/strukto/mirage/tests/fuse/__init__.py
/Users/agent/strukto/mirage/tests/types/__init__.py
/Users/agent/strukto/mirage/tests/integration/__init__.py


Now let me create the new directories (without `__init__.py`) and start the moves.


Bash

mkdir -p tests/backend/local tests/backend/network tests/backend/object_storage tests/commands/builtin tests/commands/custom tests/commands/remote tests/fuse/backend/local tests/fuse/filetype tests/fuse/remote tests/server/fastapi/routers tests/server/service tests/runtime/accessors tests/observe/fastapi

Create new test subdirectories


TodoWrite

Create 15 new test directories (no __init__.py)

Execute 15 pure git mv moves

Execute 3 renames (git mv)

Split test_configs.py into object_storage tests

Split test_default.py into fuse/backend tests

Split test_format_ops.py into csv and parquet tests

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

Merge types test files into tests/test_types.py

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

Remove existing __init__.py files from tests/ and cleanup

Removing __init__.py files and cleaning up

Run tests and pre-commit to verify


Execute 15 pure git mv moves

Execute 3 renames (git mv)

Run tests and pre-commit to verify

Execute 15 pure git mv moves

Execute 3 renames (git mv)

Run tests and pre-commit to verify


Bash

git mv tests/backend/test_memory.py tests/backend/local/test_memory.py && git mv tests/backend/test_local.py tests/backend/local/test_local.py && git mv tests/backend/test_proxy.py tests/backend/network/test_proxy.py && git mv tests/commands/test_handlers.py tests/commands/builtin/test_handlers.py && git mv tests/commands/test_nav.py tests/commands/builtin/test_nav.py && git mv tests/commands/test_read.py tests/commands/builtin/test_read.py && git mv tests/commands/test_search.py tests/commands/builtin/test_search.py && git mv tests/commands/test_write.py tests/commands/builtin/test_write.py && git mv tests/commands/test_client.py tests/commands/remote/test_client.py && git mv tests/fuse/test_remote.py tests/fuse/remote/test_remote.py && git mv tests/server/test_app.py tests/server/fastapi/test_app.py && git mv tests/server/test_io_router.py tests/server/fastapi/routers/test_io_router.py && git mv tests/server/test_file_cache.py tests/server/service/test_cache.py && git mv tests/server/conftest.py tests/server/fastapi/conftest.py && git mv tests/server/test_arg_resolver.py tests/commands/custom/test_arg_resolver.py

Execute all 15 pure git mv moves


TodoWrite

Create 15 new test directories (no __init__.py)

Execute 15 pure git mv moves

Execute 3 renames (git mv)

Split test_configs.py into object_storage tests

Split test_default.py into fuse/backend tests

Split test_format_ops.py into csv and parquet tests

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

Merge types test files into tests/test_types.py

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

Remove existing __init__.py files from tests/ and cleanup

Removing __init__.py files and cleaning up

Run tests and pre-commit to verify


Bash

git mv tests/server/test_service.py tests/server/service/test_local.py && git mv tests/runtime/test_runtime.py tests/runtime/test_base.py && git mv tests/runtime/test_accessors.py tests/runtime/accessors/test_local.py

Execute 3 renames via git mv


TodoWrite

Create 15 new test directories (no __init__.py)

Execute 15 pure git mv moves

Execute 3 renames (git mv)

Split test_configs.py into object_storage tests

Split test_default.py into fuse/backend tests

Split test_format_ops.py into csv and parquet tests

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

Merge types test files into tests/test_types.py

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

Remove existing __init__.py files from tests/ and cleanup

Removing __init__.py files and cleaning up

Run tests and pre-commit to verify


Now the splits. First, split `test_configs.py`:


Write

import pytest
from pydantic import ValidationError

from mirage.backend.object_storage.s3 import S3Config

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

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


File created successfully at: /Users/agent/strukto/mirage/tests/backend/object_storage/test_s3.py

import pytest
from pydantic import ValidationError

from mirage.backend.object_storage.s3 import S3Config

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

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


import pytest
from pydantic import ValidationError

from mirage.backend.object_storage.r2 import R2Config

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

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


File created successfully at: /Users/agent/strukto/mirage/tests/backend/object_storage/test_r2.py

import pytest
from pydantic import ValidationError

from mirage.backend.object_storage.r2 import R2Config

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

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


The ExternalCommand tests from `test_configs.py` will be merged into `tests/commands/custom/test_registry.py` in step 4d. Delete the original:


Bash

Remove original test_configs.py after split


rm 'tests/backend/test_configs.py'


TodoWrite

Create 15 new test directories (no __init__.py)

Execute 15 pure git mv moves

Execute 3 renames (git mv)

Split test_configs.py into object_storage tests

Split test_default.py into fuse/backend tests

Split test_format_ops.py into csv and parquet tests

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

Merge types test files into tests/test_types.py

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

Remove existing __init__.py files from tests/ and cleanup

Removing __init__.py files and cleaning up

Run tests and pre-commit to verify


Split `test_default.py` into fuse/backend tests:


Write

from mirage.backend.local.memory import MemoryBackend
from mirage.fuse.backend.default import DefaultFuseOp
from mirage.types import MountMode
from mirage.workspace import Workspace

def test_default_eager_small_file():
    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    ws.write("/f.txt", b"hello world")
    op = DefaultFuseOp()
    ctx = op.on_open(ws, "/f.txt", 11)
    assert ctx["eager"] is True
    data = op.read(ws, "/f.txt", 0, 5, ctx)
    assert data == b"hello"

def test_default_lazy_large_file():
    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    big = b"x" * (2 * 1024 * 1024)
    ws.write("/big.bin", big)
    op = DefaultFuseOp()
    ctx = op.on_open(ws, "/big.bin", len(big))
    assert ctx["eager"] is False
    chunk = op.read(ws, "/big.bin", 0, 100, ctx)
    assert chunk == b"x" * 100

def test_buffered_write_flush():
    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    ws.write("/f.txt", b"hello world")
    op = DefaultFuseOp()
    ctx = op.on_open(ws, "/f.txt", 11)
    op.write(ws, "/f.txt", 0, b"HELLO", ctx)
    op.write(ws, "/f.txt", 6, b"WORLD", ctx)
    op.on_flush(ws, "/f.txt", ctx)
    assert ws.cat("/f.txt") == b"HELLO WORLD"

def test_buffered_write_new_file():
    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    op = DefaultFuseOp()
    ctx = op.on_open(ws, "/new.txt", 0)
    op.write(ws, "/new.txt", 0, b"brand new", ctx)
    op.on_flush(ws, "/new.txt", ctx)
    assert ws.cat("/new.txt") == b"brand new"

def test_flush_noop_without_writes():
    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    ws.write("/f.txt", b"original")
    op = DefaultFuseOp()
    ctx = op.on_open(ws, "/f.txt", 8)
    op.on_flush(ws, "/f.txt", ctx)
    assert ws.cat("/f.txt") == b"original"


File created successfully at: /Users/agent/strukto/mirage/tests/fuse/backend/test_default.py

from mirage.backend.local.memory import MemoryBackend
from mirage.fuse.backend.default import DefaultFuseOp
from mirage.types import MountMode
from mirage.workspace import Workspace

def test_default_eager_small_file():
    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    ws.write("/f.txt", b"hello world")
    op = DefaultFuseOp()
    ctx = op.on_open(ws, "/f.txt", 11)
    assert ctx["eager"] is True
    data = op.read(ws, "/f.txt", 0, 5, ctx)
    assert data == b"hello"

def test_default_lazy_large_file():
    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    big = b"x" * (2 * 1024 * 1024)
    ws.write("/big.bin", big)
    op = DefaultFuseOp()
    ctx = op.on_open(ws, "/big.bin", len(big))
    assert ctx["eager"] is False
    chunk = op.read(ws, "/big.bin", 0, 100, ctx)
    assert chunk == b"x" * 100

def test_buffered_write_flush():
    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    ws.write("/f.txt", b"hello world")
    op = DefaultFuseOp()
    ctx = op.on_open(ws, "/f.txt", 11)
    op.write(ws, "/f.txt", 0, b"HELLO", ctx)
    op.write(ws, "/f.txt", 6, b"WORLD", ctx)
    op.on_flush(ws, "/f.txt", ctx)
    assert ws.cat("/f.txt") == b"HELLO WORLD"

def test_buffered_write_new_file():
    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    op = DefaultFuseOp()
    ctx = op.on_open(ws, "/new.txt", 0)
    op.write(ws, "/new.txt", 0, b"brand new", ctx)
    op.on_flush(ws, "/new.txt", ctx)
    assert ws.cat("/new.txt") == b"brand new"

def test_flush_noop_without_writes():
    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    ws.write("/f.txt", b"original")
    op = DefaultFuseOp()
    ctx = op.on_open(ws, "/f.txt", 8)
    op.on_flush(ws, "/f.txt", ctx)
    assert ws.cat("/f.txt") == b"original"


from mirage.backend.local.memory import MemoryBackend
from mirage.fuse.backend.local.memory import MemoryFuseOp
from mirage.types import MountMode
from mirage.workspace import Workspace

def test_memory_always_eager():
    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    big = b"y" * (2 * 1024 * 1024)
    ws.write("/big.bin", big)
    op = MemoryFuseOp()
    ctx = op.on_open(ws, "/big.bin", len(big))
    assert ctx["eager"] is True


File created successfully at: /Users/agent/strukto/mirage/tests/fuse/backend/local/test_memory.py

from mirage.backend.local.memory import MemoryBackend
from mirage.fuse.backend.local.memory import MemoryFuseOp
from mirage.types import MountMode
from mirage.workspace import Workspace

def test_memory_always_eager():
    ws = Workspace({"/": MemoryBackend()}, mode=MountMode.WRITE)
    big = b"y" * (2 * 1024 * 1024)
    ws.write("/big.bin", big)
    op = MemoryFuseOp()
    ctx = op.on_open(ws, "/big.bin", len(big))
    assert ctx["eager"] is True


Bash

Remove original fuse/test_default.py after split


rm 'tests/fuse/test_default.py'


TodoWrite

Create 15 new test directories (no __init__.py)

Execute 15 pure git mv moves

Execute 3 renames (git mv)

Split test_configs.py into object_storage tests

Split test_default.py into fuse/backend tests

Split test_format_ops.py into csv and parquet tests

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

Merge types test files into tests/test_types.py

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

Remove existing __init__.py files from tests/ and cleanup

Removing __init__.py files and cleaning up

Run tests and pre-commit to verify


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

Execute 15 pure git mv moves

Execute 3 renames (git mv)

Split test_configs.py into object_storage tests

Split test_default.py into fuse/backend tests

Split test_format_ops.py into csv and parquet tests

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

Merge types test files into tests/test_types.py

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

Remove existing __init__.py files from tests/ and cleanup

Run tests and pre-commit to verify

Create 15 new test directories (no __init__.py)

Execute 15 pure git mv moves

Execute 3 renames (git mv)

Run tests and pre-commit to verify


Split `test_format_ops.py`:


Write

import io

import pyarrow as pa
import pyarrow.parquet as pq

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

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

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

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

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

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


File created successfully at: /Users/agent/strukto/mirage/tests/fuse/filetype/test_parquet.py

import io

import pyarrow as pa
import pyarrow.parquet as pq

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

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

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

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

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

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


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

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

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

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


File created successfully at: /Users/agent/strukto/mirage/tests/fuse/filetype/test_csv.py

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

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

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

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


Bash

Remove original test_format_ops.py after split


rm 'tests/fuse/test_format_ops.py'


TodoWrite

Create 15 new test directories (no __init__.py)

Execute 15 pure git mv moves

Execute 3 renames (git mv)

Split test_configs.py into object_storage tests

Split test_default.py into fuse/backend tests

Split test_format_ops.py into csv and parquet tests

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

Merge types test files into tests/test_types.py

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

Remove existing __init__.py files from tests/ and cleanup

Removing __init__.py files and cleaning up

Run tests and pre-commit to verify


Now the big split: `tests/server/test_registry.py`. All CommandRegistry + ExternalCommand tests go to `tests/commands/custom/test_registry.py` (plus the 2 ExternalCommand pydantic tests from the now-deleted test_configs.py). The `test_parse_size_units` goes to `tests/server/test_constants.py`.


Write

import sys
from unittest.mock import MagicMock

import pytest
from pydantic import ValidationError

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

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

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

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

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

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

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

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

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

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

class _CustomMethodBackend(MemoryBackend):

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


from mirage.server.constants import _parse_size

def test_parse_size_units():
    assert _parse_size("100B") == 100
    assert _parse_size("10KB") == 10240
    assert _parse_size("1MB") == 1048576
    assert _parse_size("2GB") == 2147483648
    assert _parse_size("100") == 100


File created successfully at: /Users/agent/strukto/mirage/tests/commands/custom/test_registry.py

import sys
from unittest.mock import MagicMock

import pytest
from pydantic import ValidationError

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

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

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

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

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

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

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

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

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

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

class _CustomMethodBackend(MemoryBackend):

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


File created successfully at: /Users/agent/strukto/mirage/tests/server/test_constants.py

from mirage.server.constants import _parse_size

def test_parse_size_units():
    assert _parse_size("100B") == 100
    assert _parse_size("10KB") == 10240
    assert _parse_size("1MB") == 1048576
    assert _parse_size("2GB") == 2147483648
    assert _parse_size("100") == 100


Bash

Remove original server/test_registry.py after split


rm 'tests/server/test_registry.py'


TodoWrite

Create 15 new test directories (no __init__.py)

Execute 15 pure git mv moves

Execute 3 renames (git mv)

Split test_configs.py into object_storage tests

Split test_default.py into fuse/backend tests

Split test_format_ops.py into csv and parquet tests

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

Merge types test files into tests/test_types.py

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

Remove existing __init__.py files from tests/ and cleanup

Removing __init__.py files and cleaning up

Run tests and pre-commit to verify


Now merge types test files. I already read both - merge `tests/types/test_types.py` into `tests/test_types.py`: