chore: import upstream snapshot with attribution
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:30:44 +08:00
commit bcbd1bdb22
5748 changed files with 562488 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
@@ -0,0 +1,85 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage import MountMode, RAMResource, Workspace
async def _workspace() -> Workspace:
ws = Workspace({"/": RAMResource()}, mode=MountMode.WRITE)
await ws.execute("mkdir -p /data/sub")
await ws.execute("tee /data/plain.txt > /dev/null", stdin=b"y\n")
await ws.execute("cd /data")
return ws
async def _rc(ws: Workspace, cmd: str) -> int:
io = await ws.execute(cmd)
return io.exit_code
@pytest.mark.asyncio
async def test_f_relative_resolves_against_cwd():
ws = await _workspace()
assert await _rc(ws, "test -f plain.txt") == 0
@pytest.mark.asyncio
async def test_f_relative_missing():
ws = await _workspace()
assert await _rc(ws, "test -f missing.txt") == 1
@pytest.mark.asyncio
async def test_f_relative_with_dotdot():
ws = await _workspace()
await ws.execute("cd /data/sub")
assert await _rc(ws, "test -f ../plain.txt") == 0
@pytest.mark.asyncio
async def test_d_relative_resolves_against_cwd():
ws = await _workspace()
assert await _rc(ws, "test -d sub") == 0
@pytest.mark.asyncio
async def test_d_relative_missing():
ws = await _workspace()
assert await _rc(ws, "test -d nosuch") == 1
@pytest.mark.asyncio
async def test_f_absolute_unchanged():
ws = await _workspace()
assert await _rc(ws, "test -f /data/plain.txt") == 0
@pytest.mark.asyncio
async def test_f_empty_operand_false():
ws = await _workspace()
assert await _rc(ws, 'test -f ""') == 1
@pytest.mark.asyncio
async def test_bracket_form_relative():
ws = await _workspace()
assert await _rc(ws, "[ -f plain.txt ]") == 0
@pytest.mark.asyncio
async def test_negation_relative():
ws = await _workspace()
assert await _rc(ws, "test ! -f missing.txt") == 0
@@ -0,0 +1,55 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import time
import pytest
from mirage.workspace.executor.builtins.script import handle_sleep
@pytest.mark.asyncio
async def test_sleep_missing_operand_exits_1():
_, io, node = await handle_sleep([])
assert io.exit_code == 1
assert io.stderr == b"sleep: missing operand\n"
assert node.exit_code == 1
@pytest.mark.asyncio
@pytest.mark.parametrize(
"raw",
["abc", "-1", "inf", "Infinity", "nan", "NaN", "0x10", "1_0", "1e309", ""])
async def test_sleep_invalid_interval_exits_1(raw):
_, io, node = await handle_sleep([raw])
assert io.exit_code == 1
assert io.stderr == f"sleep: invalid time interval '{raw}'\n".encode()
assert node.exit_code == 1
@pytest.mark.asyncio
@pytest.mark.parametrize("raw", ["0", "0.", ".01", "+0.01", "1e-3"])
async def test_sleep_valid_interval_exits_0(raw):
_, io, node = await handle_sleep([raw])
assert io.exit_code == 0
assert not io.stderr
assert node.exit_code == 0
@pytest.mark.asyncio
async def test_sleep_zero_returns_promptly():
start = time.monotonic()
_, io, _ = await handle_sleep(["0"])
assert io.exit_code == 0
assert time.monotonic() - start < 0.05
@@ -0,0 +1,56 @@
import pytest
from mirage.workspace.executor.builtins.text import handle_echo
async def echo_bytes(args: list[str]) -> bytes:
out, io, _ = await handle_echo(args)
assert io.exit_code == 0
assert isinstance(out, bytes)
return out
@pytest.mark.asyncio
async def test_plain_words_join_with_newline():
assert await echo_bytes(["hi", "there"]) == b"hi there\n"
@pytest.mark.asyncio
async def test_leading_n_suppresses_newline():
assert await echo_bytes(["-n", "hi"]) == b"hi"
@pytest.mark.asyncio
async def test_trailing_n_prints_literally():
assert await echo_bytes(["hi", "-n"]) == b"hi -n\n"
@pytest.mark.asyncio
async def test_cluster_ne():
assert await echo_bytes(["-ne", "a\\tb"]) == b"a\tb"
@pytest.mark.asyncio
async def test_capital_e_disables_escapes():
assert await echo_bytes(["-e", "-E", "a\\tb"]) == b"a\\tb\n"
@pytest.mark.asyncio
async def test_last_of_e_and_E_wins_within_cluster():
assert await echo_bytes(["-eE", "a\\tb"]) == b"a\\tb\n"
assert await echo_bytes(["-Ee", "a\\tb"]) == b"a\tb\n"
@pytest.mark.asyncio
async def test_unknown_char_makes_word_literal():
assert await echo_bytes(["-nq", "hi"]) == b"-nq hi\n"
@pytest.mark.asyncio
async def test_option_after_operand_is_literal():
assert await echo_bytes(["hi", "-e", "a\\tb"]) == b"hi -e a\\tb\n"
@pytest.mark.asyncio
async def test_lone_dash_is_literal():
assert await echo_bytes(["-"]) == b"-\n"
@@ -0,0 +1,99 @@
import asyncio
import pytest
from mirage.io import IOResult
from mirage.io.stream import materialize
from mirage.workspace.executor.builtins.timeout import (handle_timeout,
parse_duration)
from mirage.workspace.session.session import Session
class FakeShell:
def __init__(self, delay: float = 0.0, exit_code: int = 0):
self.lines: list[str] = []
self.delay = delay
self.exit_code = exit_code
async def __call__(self, line: str, session_id: str) -> IOResult:
self.lines.append(line)
if self.delay:
await asyncio.sleep(self.delay)
return IOResult(stdout=b"done\n", exit_code=self.exit_code)
def make_session() -> Session:
return Session(session_id="s1")
def test_parse_duration_units():
assert parse_duration("1") == 1.0
assert parse_duration("0.5") == 0.5
assert parse_duration("2s") == 2.0
assert parse_duration("2m") == 120.0
assert parse_duration("1h") == 3600.0
assert parse_duration("1d") == 86400.0
assert parse_duration(".5") == 0.5
def test_parse_duration_rejects_garbage():
assert parse_duration("xx") is None
assert parse_duration("-1") is None
assert parse_duration("1x") is None
assert parse_duration("") is None
@pytest.mark.asyncio
async def test_command_finishing_in_time_passes_through():
shell = FakeShell(exit_code=3)
stdout, io, _ = await handle_timeout(shell, ["5", "wc", "-l"],
make_session())
assert shell.lines == ["wc -l"]
assert io.exit_code == 3
assert stdout == b"done\n"
@pytest.mark.asyncio
async def test_overrun_exits_124():
shell = FakeShell(delay=1.0)
_, io, node = await handle_timeout(shell, ["0.05", "sleep", "1"],
make_session())
assert io.exit_code == 124
assert node.exit_code == 124
@pytest.mark.asyncio
async def test_invalid_duration_exits_125():
shell = FakeShell()
_, io, _ = await handle_timeout(shell, ["xx", "sleep", "1"],
make_session())
assert io.exit_code == 125
assert (await
materialize(io.stderr)) == b"timeout: invalid time interval 'xx'\n"
assert shell.lines == []
@pytest.mark.asyncio
async def test_missing_operand_exits_125():
shell = FakeShell()
_, io, _ = await handle_timeout(shell, ["5"], make_session())
assert io.exit_code == 125
assert await materialize(io.stderr) == b"timeout: missing operand\n"
@pytest.mark.asyncio
async def test_signal_option_rejected():
shell = FakeShell()
_, io, _ = await handle_timeout(shell, ["-s", "KILL", "1", "sleep", "3"],
make_session())
assert io.exit_code == 125
assert (await
materialize(io.stderr)) == b"timeout: unsupported option -- '-s'\n"
@pytest.mark.asyncio
async def test_quoting_survives_rejoin():
shell = FakeShell()
await handle_timeout(shell, ["1", "grep", "a b", "f.txt"], make_session())
assert shell.lines == ["grep 'a b' f.txt"]
@@ -0,0 +1,76 @@
import pytest
from mirage.io.stream import materialize
from mirage.workspace.executor.builtins.vars import (handle_read,
handle_return,
handle_shift)
from mirage.workspace.executor.control import ReturnSignal
from mirage.workspace.session.session import Session
def make_session() -> Session:
return Session(session_id="s1")
@pytest.mark.asyncio
async def test_shift_non_numeric_errors_like_bash():
_, io, _ = await handle_shift(["x"], None, session=make_session())
assert io.exit_code == 1
assert (await
materialize(io.stderr)) == b"shift: x: numeric argument required\n"
@pytest.mark.asyncio
async def test_shift_too_many_arguments():
_, io, _ = await handle_shift(["1", "2"], None, session=make_session())
assert io.exit_code == 1
assert await materialize(io.stderr) == b"shift: too many arguments\n"
@pytest.mark.asyncio
async def test_shift_default_one():
session = make_session()
session.positional_args = ["a", "b"]
_, io, _ = await handle_shift([], None, session=session)
assert io.exit_code == 0
assert session.positional_args == ["b"]
@pytest.mark.asyncio
async def test_return_non_numeric_raises_2_with_message():
with pytest.raises(ReturnSignal) as exc:
await handle_return(["x"])
assert exc.value.exit_code == 2
assert exc.value.stderr == b"return: x: numeric argument required\n"
@pytest.mark.asyncio
async def test_return_numeric():
with pytest.raises(ReturnSignal) as exc:
await handle_return(["7"])
assert exc.value.exit_code == 7
assert exc.value.stderr == b""
@pytest.mark.asyncio
async def test_read_invalid_option_exits_2():
_, io, _ = await handle_read(["-q", "v"], make_session(), b"line\n")
assert io.exit_code == 2
assert await materialize(io.stderr) == b"read: -q: invalid option\n"
@pytest.mark.asyncio
async def test_read_dash_r_consumed_not_a_variable():
session = make_session()
_, io, _ = await handle_read(["-r", "v"], session, b"hello world\n")
assert io.exit_code == 0
assert session.env["v"] == "hello world"
assert "-r" not in session.env
@pytest.mark.asyncio
async def test_read_defaults_to_reply():
session = make_session()
_, io, _ = await handle_read([], session, b"hi\n")
assert io.exit_code == 0
assert session.env["REPLY"] == "hi"
@@ -0,0 +1,124 @@
import pytest
from mirage.io import IOResult
from mirage.io.stream import materialize
from mirage.workspace.executor.builtins.xargs import handle_xargs
from mirage.workspace.session.session import Session
class FakeShell:
def __init__(self, exit_codes: list[int] | None = None):
self.lines: list[str] = []
self.exit_codes = exit_codes or []
async def __call__(self, line: str, session_id: str) -> IOResult:
self.lines.append(line)
code = (self.exit_codes[len(self.lines) - 1]
if len(self.lines) <= len(self.exit_codes) else 0)
return IOResult(stdout=f"ran:{line}\n".encode(), exit_code=code)
def make_session() -> Session:
return Session(session_id="s1")
@pytest.mark.asyncio
async def test_batches_one_arg_per_run_with_n1():
shell = FakeShell()
_, io, _ = await handle_xargs(shell, ["-n1", "echo"], make_session(),
b"a b c")
assert shell.lines == ["echo a", "echo b", "echo c"]
assert io.exit_code == 0
@pytest.mark.asyncio
async def test_single_run_without_n():
shell = FakeShell()
_, io, _ = await handle_xargs(shell, ["echo"], make_session(), b"a b c")
assert shell.lines == ["echo a b c"]
assert io.exit_code == 0
@pytest.mark.asyncio
async def test_failing_invocation_exits_123_but_continues():
shell = FakeShell(exit_codes=[1, 0])
_, io, _ = await handle_xargs(shell, ["-n1", "wc"], make_session(), b"a b")
assert shell.lines == ["wc a", "wc b"]
assert io.exit_code == 123
@pytest.mark.asyncio
async def test_command_not_found_stops_with_127():
shell = FakeShell(exit_codes=[127, 0])
_, io, _ = await handle_xargs(shell, ["-n1", "nope"], make_session(),
b"a b")
assert shell.lines == ["nope a"]
assert io.exit_code == 127
@pytest.mark.asyncio
async def test_no_run_if_empty():
shell = FakeShell()
_, io, _ = await handle_xargs(shell, ["-r", "echo", "hi"], make_session(),
b"")
assert shell.lines == []
assert io.exit_code == 0
@pytest.mark.asyncio
async def test_empty_input_without_r_runs_once():
shell = FakeShell()
_, io, _ = await handle_xargs(shell, ["echo", "hi"], make_session(), b"")
assert shell.lines == ["echo hi"]
assert io.exit_code == 0
@pytest.mark.asyncio
async def test_null_delimited_input():
shell = FakeShell()
await handle_xargs(shell, ["-0", "echo"], make_session(), b"a b\0c\0")
assert shell.lines == ["echo 'a b' c"]
@pytest.mark.asyncio
async def test_custom_delimiter():
shell = FakeShell()
await handle_xargs(shell, ["-d,", "echo"], make_session(), b"a,b,c")
assert shell.lines == ["echo a b c"]
@pytest.mark.asyncio
async def test_invalid_option_exits_1():
shell = FakeShell()
_, io, _ = await handle_xargs(shell, ["-q", "echo"], make_session(), b"x")
assert io.exit_code == 1
assert await materialize(io.stderr) == b"xargs: invalid option -- 'q'\n"
assert shell.lines == []
@pytest.mark.asyncio
async def test_unsupported_option_exits_1():
shell = FakeShell()
_, io, _ = await handle_xargs(shell, ["-I", "{}", "echo"], make_session(),
b"x")
assert io.exit_code == 1
assert await materialize(io.stderr
) == b"xargs: unsupported option -- 'I'\n"
@pytest.mark.asyncio
async def test_n_zero_rejected():
shell = FakeShell()
_, io, _ = await handle_xargs(shell, ["-n0", "echo"], make_session(), b"x")
assert io.exit_code == 1
assert (await
materialize(io.stderr
)) == b"xargs: value 0 for -n option should be >= 1\n"
@pytest.mark.asyncio
async def test_input_words_stay_single_tokens():
shell = FakeShell()
await handle_xargs(shell, ["echo"], make_session(), b"don't $(reboot)")
assert shell.lines == ["echo 'don'\"'\"'t' '$(reboot)'"]
@@ -0,0 +1,99 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from mirage.workspace.executor.builtins import _interpret_escapes
def test_newline():
assert _interpret_escapes("a\\nb") == "a\nb"
def test_tab():
assert _interpret_escapes("a\\tb") == "a\tb"
def test_carriage_return():
assert _interpret_escapes("\\r") == "\r"
def test_bell():
assert _interpret_escapes("\\a") == "\a"
def test_backspace():
assert _interpret_escapes("\\b") == "\b"
def test_form_feed():
assert _interpret_escapes("\\f") == "\f"
def test_vertical_tab():
assert _interpret_escapes("\\v") == "\v"
def test_literal_backslash():
assert _interpret_escapes("a\\\\b") == "a\\b"
def test_double_backslash_before_n():
assert _interpret_escapes("\\\\n") == "\\n"
def test_double_backslash_before_b():
assert _interpret_escapes("a\\\\b") == "a\\b"
def test_hex_escape():
assert _interpret_escapes("\\x41") == "A"
def test_hex_single_digit():
assert _interpret_escapes("\\x9") == "\t"
def test_hex_no_digits():
assert _interpret_escapes("\\x") == "\\x"
def test_octal_escape():
assert _interpret_escapes("\\0101") == "A"
def test_octal_null():
assert _interpret_escapes("\\0") == "\0"
def test_stop_output():
assert _interpret_escapes("hello\\cworld") == "hello"
def test_unknown_escape_passthrough():
assert _interpret_escapes("\\z") == "\\z"
def test_no_escapes():
assert _interpret_escapes("hello world") == "hello world"
def test_empty():
assert _interpret_escapes("") == ""
def test_trailing_backslash():
assert _interpret_escapes("end\\") == "end\\"
def test_mixed():
assert _interpret_escapes("a\\tb\\nc\\\\d") == "a\tb\nc\\d"
@@ -0,0 +1,89 @@
import asyncio
from types import SimpleNamespace
from mirage.resource.ram import RAMResource
from mirage.types import MountMode
from mirage.workspace import Workspace
from mirage.workspace.executor.fanout import (_adjust_depth_texts,
_synthesize_find_mount_entries)
def _mounts(*prefixes):
return [SimpleNamespace(prefix=p) for p in prefixes]
def test_synthesize_no_expression_emits_all():
desc = _mounts("/ram/", "/disk/")
assert _synthesize_find_mount_entries("/", desc, []) == "/ram\n/disk"
def test_synthesize_positive_name():
desc = _mounts("/ram/", "/disk/")
assert _synthesize_find_mount_entries("/", desc,
["-name", "ram"]) == "/ram"
def test_synthesize_honors_not():
desc = _mounts("/ram/", "/disk/", "/notes/")
out = _synthesize_find_mount_entries("/", desc, ["-not", "-name", "ram"])
assert out == "/disk\n/notes"
def test_synthesize_honors_or():
desc = _mounts("/ram/", "/disk/", "/notes/")
out = _synthesize_find_mount_entries(
"/", desc, ["-name", "ram", "-o", "-name", "disk"])
assert out == "/ram\n/disk"
def test_synthesize_type_file_excludes_mount_dirs():
desc = _mounts("/ram/", "/disk/")
assert _synthesize_find_mount_entries("/", desc, ["-type", "f"]) == ""
def test_synthesize_type_dir_includes_mount_dirs():
desc = _mounts("/ram/", "/disk/")
assert _synthesize_find_mount_entries("/", desc,
["-type", "d"]) == "/ram\n/disk"
def test_synthesize_maxdepth_window():
desc = _mounts("/ram/", "/a/b/")
assert _synthesize_find_mount_entries("/", desc,
["-maxdepth", "1"]) == "/ram"
def test_adjust_depth_texts_reduces_maxdepth_by_delta():
out = _adjust_depth_texts(["-maxdepth", "3", "-name", "x"], "/",
"/data/sub")
assert out == ["-maxdepth", "1", "-name", "x"]
def test_adjust_depth_texts_clamps_mindepth_at_zero():
out = _adjust_depth_texts(["-mindepth", "1"], "/", "/data")
assert out == ["-mindepth", "0"]
def test_adjust_depth_texts_no_depth_tokens_unchanged():
out = _adjust_depth_texts(["-name", "x", "-o", "-name", "y"], "/", "/data")
assert out == ["-name", "x", "-o", "-name", "y"]
def test_adjust_depth_texts_same_mount_unchanged():
assert _adjust_depth_texts(["-maxdepth", "3"], "/data",
"/data") == ["-maxdepth", "3"]
def test_maxdepth_applies_to_child_mount_depth_end_to_end():
parent = RAMResource()
child = RAMResource()
child._store.dirs.add("/a")
child._store.files["/a/b.txt"] = b"deep\n"
ws = Workspace(resources={
"/": (parent, MountMode.EXEC),
"/data/": (child, MountMode.EXEC),
}, )
io = asyncio.run(ws.execute("find / -maxdepth 2"))
out = (io.stdout if isinstance(io.stdout, bytes) else b"").decode()
assert "/data/a" in out
assert "/data/a/b.txt" not in out
+190
View File
@@ -0,0 +1,190 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
from unittest.mock import MagicMock
from mirage.commands.config import RegisteredCommand
from mirage.commands.spec.types import CommandSpec, OperandKind, Option
from mirage.workspace.executor.builtins import (_collect_man_hits,
_render_man_entry,
_render_man_index, handle_man)
from mirage.workspace.session import Session
def _mk_cmd(name, spec, filetype=None, resource="ram"):
return RegisteredCommand(
name=name,
spec=spec,
resource=resource,
filetype=filetype,
fn=lambda *a, **kw: None,
)
def _mk_mount(prefix, kind, cmds=None, general=None):
mount = MagicMock()
mount.prefix = prefix
mount.resource = MagicMock()
mount.resource.name = kind
cmds = cmds or {}
general = general or {}
def _resolve(name, extension=None):
if name in cmds:
return cmds[name]
if name in general:
return general[name]
return None
def _is_general(name):
return name in general and name not in cmds
def _all():
seen = set()
out = []
for rc in cmds.values():
if rc.name in seen:
continue
seen.add(rc.name)
out.append(rc)
for rc in general.values():
if rc.name in seen:
continue
seen.add(rc.name)
out.append(rc)
return out
mount.resolve_command = MagicMock(side_effect=_resolve)
mount.is_general_command = MagicMock(side_effect=_is_general)
mount.all_commands = MagicMock(side_effect=_all)
return mount
def _mk_registry(mounts, cwd_mount=None):
reg = MagicMock()
reg.mounts = MagicMock(return_value=mounts)
def _mount_for(path):
if cwd_mount is not None:
return cwd_mount
raise ValueError(f"no mount: {path}")
reg.mount_for = MagicMock(side_effect=_mount_for)
return reg
def test_collect_man_hits_skips_dev():
spec = CommandSpec(description="x")
cat_cmd = _mk_cmd("cat", spec)
mount_dev = _mk_mount("/dev/", "dev", cmds={"cat": cat_cmd})
mount_ram = _mk_mount("/ram/", "ram", cmds={"cat": cat_cmd})
reg = _mk_registry([mount_dev, mount_ram])
hits = _collect_man_hits("cat", reg)
assert len(hits) == 1
assert hits[0].mount is mount_ram
def test_render_man_entry_no_options():
spec = CommandSpec(description="Concatenate files.")
cat_cmd = _mk_cmd("cat", spec)
mount = _mk_mount("/ram/", "ram", cmds={"cat": cat_cmd})
hits = _collect_man_hits("cat", _mk_registry([mount]))
out = _render_man_entry("cat", hits)
assert out.startswith("# cat\n")
assert "Concatenate files." in out
assert "## OPTIONS" not in out
assert "## RESOURCES\n\n- ram\n" in out
def test_render_man_entry_with_options():
spec = CommandSpec(
description="Print a sequence.",
options=(
Option(short="-s",
value_kind=OperandKind.TEXT,
description="separator"),
Option(short="-w", description="zero-pad"),
),
)
cmd = _mk_cmd("seq", spec)
mount = _mk_mount("/ram/", "ram", cmds={"seq": cmd})
hits = _collect_man_hits("seq", _mk_registry([mount]))
out = _render_man_entry("seq", hits)
assert "## OPTIONS" in out
assert "| short | long | value | description |" in out
assert "| -s | | text | separator |" in out
assert "| -w | | none | zero-pad |" in out
def test_render_man_entry_dedupes_by_kind_and_filetype():
spec = CommandSpec(description="cat")
plain = _mk_cmd("cat", spec)
parquet = _mk_cmd("cat", spec, filetype=".parquet")
m1 = _mk_mount("/a/", "ram", cmds={"cat": plain})
m2 = _mk_mount("/b/", "ram", cmds={"cat": plain})
m3 = _mk_mount("/c/", "ram", cmds={"cat": parquet})
reg = _mk_registry([m1, m2, m3])
hits = _collect_man_hits("cat", reg)
out = _render_man_entry("cat", hits)
assert out.count("- ram\n") == 1
assert "- ram (filetype: .parquet)" in out
def test_render_man_entry_general_first():
spec = CommandSpec(description="x")
cmd = _mk_cmd("bc", spec, resource=None)
mount = _mk_mount("/ram/", "ram", general={"bc": cmd})
hits = _collect_man_hits("bc", _mk_registry([mount]))
out = _render_man_entry("bc", hits)
assert out.endswith("- general\n")
def test_handle_man_missing_entry():
reg = _mk_registry([])
out, io, node = asyncio.run(
handle_man(["nope"], Session(session_id="t"), reg))
assert out is None
assert io.exit_code == 1
assert io.stderr == b"man: no entry for nope\n"
assert node.exit_code == 1
def test_handle_man_index_cwd_first():
spec_a = CommandSpec(description="ls files")
spec_b = CommandSpec(description="cat files")
ls = _mk_cmd("ls", spec_a)
cat = _mk_cmd("cat", spec_b)
mount_z = _mk_mount("/z/", "zfs", cmds={"ls": ls})
mount_r = _mk_mount("/ram/", "ram", cmds={"cat": cat})
reg = _mk_registry([mount_z, mount_r], cwd_mount=mount_r)
out, io, node = asyncio.run(
handle_man([], Session(session_id="t", cwd="/ram/x"), reg))
assert io.exit_code == 0
text = out.decode()
ram_pos = text.index("# ram")
zfs_pos = text.index("# zfs")
assert ram_pos < zfs_pos
def test_render_man_index_dedupes_general_across_mounts():
spec_g = CommandSpec(description="bc desc")
bc = _mk_cmd("bc", spec_g, resource=None)
spec_a = CommandSpec(description="ls files")
ls = _mk_cmd("ls", spec_a)
m1 = _mk_mount("/a/", "ram", cmds={"ls": ls}, general={"bc": bc})
m2 = _mk_mount("/b/", "s3", cmds={"ls": ls}, general={"bc": bc})
reg = _mk_registry([m1, m2])
text = _render_man_index(Session(session_id="t"), reg)
assert text.count("- bc \u2014 bc desc") == 1
@@ -0,0 +1,77 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage.io import IOResult
from mirage.io.types import materialize
from mirage.workspace.executor.pipes import handle_pipe
from mirage.workspace.session import Session
from mirage.workspace.types import ExecutionNode
class FakeNode:
def __init__(self, text: str):
self.text = text
@pytest.mark.asyncio
async def test_handle_pipe_passes_empty_stdin_when_left_returns_none():
calls: list[dict] = []
async def execute_node(nd, _session, stdin, _call_stack=None):
stdin_was_none = stdin is None
materialized = await materialize(stdin)
calls.append({
"text": nd.text,
"stdin_was_none": stdin_was_none,
"stdin_bytes": materialized,
})
if nd.text == "left":
return (None, IOResult(stderr=b"boom", exit_code=1),
ExecutionNode(command=nd.text, exit_code=1))
return (b"right-out", IOResult(exit_code=0),
ExecutionNode(command=nd.text, exit_code=0))
await handle_pipe(
execute_node,
[FakeNode("left"), FakeNode("right")],
[False],
Session(session_id="t"),
None,
)
right = next(c for c in calls if c["text"] == "right")
assert right["stdin_was_none"] is False
assert right["stdin_bytes"] == b""
@pytest.mark.asyncio
async def test_handle_pipe_threads_stdout_to_next_stdin():
seen: list[bytes] = []
async def execute_node(nd, _session, stdin, _call_stack=None):
seen.append(await materialize(stdin))
return (f"{nd.text}-out".encode(), IOResult(exit_code=0),
ExecutionNode(command=nd.text, exit_code=0))
await handle_pipe(
execute_node,
[FakeNode("a"), FakeNode("b")],
[False],
Session(session_id="t"),
None,
)
assert seen[0] == b""
assert seen[1] == b"a-out"
@@ -0,0 +1,103 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage import MountMode, RAMResource, Workspace
async def _workspace() -> Workspace:
ws = Workspace({"/": RAMResource()}, mode=MountMode.WRITE)
await ws.execute("mkdir -p /data")
return ws
async def _out(ws: Workspace, cmd: str) -> str:
io = await ws.execute(cmd)
return (io.stdout or b"").decode()
@pytest.mark.asyncio
async def test_redirect_target_expands_after_cd_in_list():
# tree-sitter hoists the trailing redirect over the && list; the
# target must still expand with the cwd the last command sees.
ws = await _workspace()
await ws.execute("cd /data && echo hi > OUT")
assert await _out(ws, "cat /data/OUT") == "hi\n"
@pytest.mark.asyncio
async def test_redirect_captures_only_last_command():
ws = await _workspace()
out = await _out(ws, "echo one && echo two > /data/f")
assert out == "one\n"
assert await _out(ws, "cat /data/f") == "two\n"
@pytest.mark.asyncio
async def test_redirect_short_circuit_and():
ws = await _workspace()
io = await ws.execute("false && echo never > /data/f3")
assert io.exit_code == 1
io = await ws.execute("test -f /data/f3")
assert io.exit_code == 1
@pytest.mark.asyncio
async def test_redirect_short_circuit_or():
ws = await _workspace()
await ws.execute("false || echo fallback > /data/f4")
assert await _out(ws, "cat /data/f4") == "fallback\n"
@pytest.mark.asyncio
async def test_redirect_chain_compounds():
# Each redirect re-associates independently, so a multi-redirect
# chain executes left to right instead of hoisting.
ws = await _workspace()
out = await _out(
ws, "echo a > /data/c && echo b >> /data/c && cat /data/c"
" && wc -l < /data/c")
assert out == "a\nb\n2\n"
@pytest.mark.asyncio
async def test_redirect_group_keeps_whole_body():
# Compound bodies are real bash group redirects, not hoists.
ws = await _workspace()
await ws.execute("{ echo g1; echo g2; } > /data/grp")
assert await _out(ws, "cat /data/grp") == "g1\ng2\n"
@pytest.mark.asyncio
async def test_redirect_subshell_keeps_whole_body():
ws = await _workspace()
await ws.execute("(echo s1; echo s2) > /data/subq")
assert await _out(ws, "cat /data/subq") == "s1\ns2\n"
@pytest.mark.asyncio
async def test_redirect_pipeline_right_side():
ws = await _workspace()
out = await _out(
ws, "echo x && echo y | tr a-z A-Z > /data/up && cat /data/up")
assert out == "x\nY\n"
@pytest.mark.asyncio
async def test_stdin_redirect_binds_last_command():
ws = await _workspace()
await ws.execute("printf 'l1\\nl2\\n' | tee /data/seed > /dev/null")
out = await _out(ws, "echo lead && wc -l < /data/seed")
assert out == "lead\n2\n"
+13
View File
@@ -0,0 +1,13 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
@@ -0,0 +1,80 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from mirage.resource.ram import RAMResource
from mirage.types import MountMode, PathSpec
from mirage.workspace.expand.classify.heuristic import (_unescape_path,
classify_word)
from mirage.workspace.mount import MountRegistry
def test_unescape_backslash_apostrophe():
assert _unescape_path(r"Zecheng\'s\ Server") == "Zecheng's Server"
def test_unescape_no_backslash():
assert _unescape_path("normal") == "normal"
def test_unescape_only_space():
assert _unescape_path(r"hello\ world") == "hello world"
def test_classify_backslash_escaped_absolute():
registry = MountRegistry()
resource = RAMResource()
resource._store.dirs.add("/")
registry.mount("/ram/", resource, MountMode.WRITE)
result = classify_word(r"/ram/Zecheng\'s\ Server/", registry, "/")
assert isinstance(result, PathSpec)
assert result.virtual == "/ram/Zecheng's Server"
def test_classify_quoted_path():
registry = MountRegistry()
resource = RAMResource()
resource._store.dirs.add("/")
registry.mount("/ram/", resource, MountMode.WRITE)
result = classify_word("/ram/Zecheng's Server/", registry, "/")
assert isinstance(result, PathSpec)
assert result.virtual == "/ram/Zecheng's Server"
def test_bare_filename_stays_text():
registry = MountRegistry()
registry.mount("/ram/", RAMResource(), MountMode.WRITE)
assert classify_word("file.txt", registry, "/ram") == "file.txt"
def test_relative_subdir_path_resolves():
registry = MountRegistry()
registry.mount("/ram/", RAMResource(), MountMode.WRITE)
result = classify_word("sub/file.txt", registry, "/ram")
assert isinstance(result, PathSpec)
assert result.virtual == "/ram/sub/file.txt"
def test_relative_glob_resolves_against_cwd():
registry = MountRegistry()
registry.mount("/ram/", RAMResource(), MountMode.WRITE)
result = classify_word("*.txt", registry, "/ram")
assert isinstance(result, PathSpec)
assert result.pattern == "*.txt"
assert result.directory == "/ram/"
def test_bare_glob_operator_stays_text():
registry = MountRegistry()
registry.mount("/ram/", RAMResource(), MountMode.WRITE)
assert classify_word("*", registry, "/ram") == "*"
@@ -0,0 +1,71 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from mirage.commands.spec.types import OperandKind
from mirage.resource.ram import RAMResource
from mirage.types import MountMode, PathSpec
from mirage.workspace.expand.classify.parts import classify_parts
from mirage.workspace.mount import MountRegistry
def _registry() -> MountRegistry:
registry = MountRegistry()
registry.mount("/ram/", RAMResource(), MountMode.WRITE)
return registry
def test_name_never_classified():
result = classify_parts(["/ram/cat", "/ram/x"], _registry(), "/")
assert result[0] == "/ram/cat"
assert isinstance(result[1], PathSpec)
def test_text_kind_keeps_string():
result = classify_parts(["cat", "/ram/x"],
_registry(),
"/",
word_kinds=[OperandKind.TEXT])
assert result[1] == "/ram/x"
def test_path_kind_classifies_bare_filename():
result = classify_parts(["cat", "file.txt"],
_registry(),
"/ram",
word_kinds=[OperandKind.PATH])
assert isinstance(result[1], PathSpec)
assert result[1].virtual == "/ram/file.txt"
def test_duplicate_word_kinds_per_slot():
result = classify_parts(["grep", "*.txt", "*.txt"],
_registry(),
"/ram",
word_kinds=[OperandKind.TEXT, OperandKind.PATH])
assert result[1] == "*.txt"
assert isinstance(result[2], PathSpec)
assert result[2].pattern == "*.txt"
def test_none_kind_falls_back_to_heuristic():
result = classify_parts(["cat", "/ram/x", "plain"],
_registry(),
"/",
word_kinds=[None, None])
assert isinstance(result[1], PathSpec)
assert result[2] == "plain"
def test_empty_parts():
assert classify_parts([], _registry(), "/") == []
@@ -0,0 +1,45 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from mirage.resource.ram import RAMResource
from mirage.types import MountMode, PathSpec
from mirage.workspace.expand.classify.path import classify_bare_path
from mirage.workspace.mount import MountRegistry
def _registry() -> MountRegistry:
registry = MountRegistry()
registry.mount("/ram/", RAMResource(), MountMode.WRITE)
return registry
def test_bare_filename_resolves_against_cwd():
result = classify_bare_path("file.txt", _registry(), "/ram")
assert isinstance(result, PathSpec)
assert result.virtual == "/ram/file.txt"
assert result.resolved
assert result.raw_path == "file.txt"
def test_bare_glob_becomes_pattern():
result = classify_bare_path("f?.txt", _registry(), "/ram")
assert isinstance(result, PathSpec)
assert result.pattern == "f?.txt"
assert result.directory == "/ram/"
def test_absolute_path_delegates_to_heuristic():
result = classify_bare_path("/ram/file.txt", _registry(), "/")
assert isinstance(result, PathSpec)
assert result.virtual == "/ram/file.txt"
@@ -0,0 +1,62 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from mirage.resource.ram import RAMResource
from mirage.types import MountMode, PathSpec
from mirage.workspace.expand.classify.relative import relative_spec
from mirage.workspace.mount import MountRegistry
def _registry() -> MountRegistry:
registry = MountRegistry()
registry.mount("/ram/", RAMResource(), MountMode.WRITE)
return registry
def test_plain_word_resolves_and_keeps_raw():
result = relative_spec("sub/a.txt", _registry(), "/ram")
assert isinstance(result, PathSpec)
assert result.virtual == "/ram/sub/a.txt"
assert result.raw_path == "sub/a.txt"
assert result.resolved
assert result.pattern is None
def test_glob_word_becomes_pattern():
result = relative_spec("sub/*.txt", _registry(), "/ram")
assert isinstance(result, PathSpec)
assert result.directory == "/ram/sub/"
assert result.pattern == "*.txt"
assert not result.resolved
assert result.raw_path == "sub/*.txt"
def test_dotdot_normalizes():
result = relative_spec("../x.txt", _registry(), "/ram/sub")
assert isinstance(result, PathSpec)
assert result.virtual == "/ram/x.txt"
assert result.raw_path == "../x.txt"
def test_unmounted_stays_text():
registry = MountRegistry()
registry.mount("/ram/", RAMResource(), MountMode.WRITE)
assert relative_spec("a.txt", registry, "/elsewhere") == "a.txt"
def test_raw_path_round_trip():
result = relative_spec("./sub/a.txt", _registry(), "/ram")
assert isinstance(result, PathSpec)
assert result.raw_path == "./sub/a.txt"
assert result.virtual == "/ram/sub/a.txt"
@@ -0,0 +1,58 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import dataclasses
import pytest
from mirage.types import PathSpec
from mirage.workspace.expand.argv import Argv
def _ps(virtual: str) -> PathSpec:
return PathSpec(virtual=virtual,
directory=virtual[:virtual.rfind("/") + 1],
resource_path="",
resolved=True)
def test_words_includes_name():
argv = Argv(name="cat", args=("f.txt", ), operands=(_ps("/ram/f.txt"), ))
assert argv.words == ["cat", _ps("/ram/f.txt")]
def test_words_empty_command():
assert Argv(name="", args=(), operands=()).words == []
def test_views_differ_only_in_type():
pattern = _ps("/ram/*.txt")
argv = Argv(name="ls", args=("/ram/*.txt", ), operands=(pattern, ))
assert len(argv.args) == len(argv.operands)
assert argv.args[0] == argv.operands[0].virtual
def test_with_operands_replaces_only_operands():
argv = Argv(name="rm", args=("link", ), operands=(_ps("/ram/link"), ))
rewritten = argv.with_operands([_ps("/ram/target")])
assert rewritten.operands == (_ps("/ram/target"), )
assert rewritten.name == "rm"
assert rewritten.args == ("link", )
assert argv.operands == (_ps("/ram/link"), )
def test_frozen():
argv = Argv(name="cat", args=(), operands=())
with pytest.raises(dataclasses.FrozenInstanceError):
argv.name = "dog"
+296
View File
@@ -0,0 +1,296 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
from unittest.mock import MagicMock, patch
from mirage.cache.index import RAMIndexCacheStore
from mirage.resource.ram import RAMResource
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
from mirage.workspace.expand.globs import resolve_globs
def _mock_registry(resolve_result=None):
mount = MagicMock()
mount.prefix = "/data/"
async def _resolve_glob(scopes, prefix=""):
if resolve_result is not None:
return resolve_result
return scopes
mount.resource = MagicMock()
mount.resource.resolve_glob = _resolve_glob
reg = MagicMock()
reg.mount_for = MagicMock(return_value=mount)
return reg
def _run(coro):
return asyncio.run(coro)
def test_text_passes_through():
reg = _mock_registry()
classified = ["grep", "pattern"]
result = _run(resolve_globs(classified, reg))
assert result == ["grep", "pattern"]
def test_pathspec_without_pattern_preserved():
reg = _mock_registry()
ps = PathSpec(resource_path="data/file.txt",
virtual="/data/file.txt",
directory="/data/",
resolved=True)
classified = ["cat", ps]
result = _run(resolve_globs(classified, reg))
assert len(result) == 2
assert result[0] == "cat"
assert isinstance(result[1], PathSpec)
assert result[1] is ps
def test_glob_pathspec_resolved_to_pathspec():
resolved_ps = PathSpec(
resource_path=mount_key("/data/a.txt", "/data"),
virtual="/data/a.txt",
directory="/data/",
resolved=True,
)
reg = _mock_registry(resolve_result=[resolved_ps])
glob_ps = PathSpec(
resource_path="data/*.txt",
virtual="/data/*.txt",
directory="/data/",
pattern="*.txt",
resolved=False,
)
classified = ["cat", glob_ps]
result = _run(resolve_globs(classified, reg))
assert len(result) == 2
assert result[0] == "cat"
assert isinstance(result[1], PathSpec)
assert result[1] is resolved_ps
def test_glob_multiple_matches_expand():
matches = [
PathSpec(resource_path="data/a.txt",
virtual="/data/a.txt",
directory="/data/",
resolved=True),
PathSpec(resource_path="data/b.txt",
virtual="/data/b.txt",
directory="/data/",
resolved=True),
]
reg = _mock_registry(resolve_result=matches)
glob_ps = PathSpec(
resource_path="data/*.txt",
virtual="/data/*.txt",
directory="/data/",
pattern="*.txt",
resolved=False,
)
classified = ["ls", glob_ps]
result = _run(resolve_globs(classified, reg))
assert len(result) == 3
assert result[0] == "ls"
assert all(isinstance(r, PathSpec) for r in result[1:])
assert result[1].virtual == "/data/a.txt"
assert result[2].virtual == "/data/b.txt"
def test_glob_string_result_wrapped_in_pathspec():
reg = _mock_registry(resolve_result=["/a.txt"])
glob_ps = PathSpec(
resource_path="data/*.txt",
virtual="/data/*.txt",
directory="/data/",
pattern="*.txt",
resolved=False,
)
classified = ["cat", glob_ps]
result = _run(resolve_globs(classified, reg))
assert len(result) == 2
assert isinstance(result[1], PathSpec)
assert result[1].virtual == "/data/a.txt"
def test_glob_no_match_keeps_literal_word():
reg = _mock_registry(resolve_result=[])
glob_ps = PathSpec(
resource_path="data/*.xyz",
virtual="/data/*.xyz",
directory="/data/",
pattern="*.xyz",
resolved=False,
)
classified = ["cat", glob_ps]
result = _run(resolve_globs(classified, reg))
assert len(result) == 2
assert result[0] == "cat"
assert isinstance(result[1], PathSpec)
assert result[1].virtual == "/data/*.xyz"
assert result[1].pattern
def test_mixed_text_and_pathspec():
reg = _mock_registry()
ps = PathSpec(resource_path="data/file.txt",
virtual="/data/file.txt",
directory="/data/",
resolved=True)
classified = ["grep", "-i", "pattern", ps]
result = _run(resolve_globs(classified, reg))
assert result[0] == "grep"
assert result[1] == "-i"
assert result[2] == "pattern"
assert isinstance(result[3], PathSpec)
assert result[3] is ps
def test_resolve_error_returns_original_pathspec():
reg = _mock_registry()
reg.mount_for = MagicMock(side_effect=ValueError("no mount"))
glob_ps = PathSpec(
resource_path="unknown/*.txt",
virtual="/unknown/*.txt",
directory="/unknown/",
pattern="*.txt",
resolved=False,
)
classified = ["cat", glob_ps]
result = _run(resolve_globs(classified, reg))
assert len(result) == 2
assert isinstance(result[1], PathSpec)
def test_pathspec_dir_carries_pattern():
ps = PathSpec(
resource_path=mount_key("/data/*.txt", "/data"),
virtual="/data/*.txt",
directory="/data/",
pattern="*.txt",
resolved=False,
)
d = ps.dir
assert d.virtual == "/data/"
assert d.pattern == "*.txt"
assert d.resource_path == ""
def test_pathspec_dir_no_pattern():
ps = PathSpec(
resource_path="data/file.txt",
virtual="/data/file.txt",
directory="/data/",
resolved=True,
)
d = ps.dir
assert d.virtual == "/data/"
assert d.pattern is None
def test_scope_error_truncates_instead_of_crash():
from mirage.core.ram.glob import resolve_glob as ram_resolve_glob
resource = RAMResource()
for i in range(20):
resource._store.files[f"/f{i:02d}.txt"] = b""
resource._store.dirs.add("/")
index = RAMIndexCacheStore()
glob_ps = PathSpec(
resource_path="*.txt",
virtual="/*.txt",
directory="/",
pattern="*.txt",
resolved=False,
)
async def _run():
with patch("mirage.core.ram.glob.SCOPE_ERROR", 5):
result = await ram_resolve_glob(resource.accessor, [glob_ps],
index)
return result
result = asyncio.run(_run())
assert len(result) == 5
def test_relative_glob_matches_spelled_as_typed():
matches = [
PathSpec(resource_path="data/sub/a.txt",
virtual="/data/sub/a.txt",
directory="/data/sub/",
resolved=True),
]
reg = _mock_registry(resolve_result=matches)
glob_ps = PathSpec(
resource_path="data/sub/*.txt",
virtual="/data/sub/*.txt",
directory="/data/sub/",
pattern="*.txt",
resolved=False,
raw_path="sub/*.txt",
)
result = _run(resolve_globs(["ls", glob_ps], reg))
assert isinstance(result[1], PathSpec)
assert result[1].raw_path == "sub/a.txt"
assert result[1].virtual == "/data/sub/a.txt"
def test_absolute_glob_matches_keep_virtual():
matches = [
PathSpec(resource_path="data/a.txt",
virtual="/data/a.txt",
directory="/data/",
resolved=True),
]
reg = _mock_registry(resolve_result=matches)
glob_ps = PathSpec(
resource_path="data/*.txt",
virtual="/data/*.txt",
directory="/data/",
pattern="*.txt",
resolved=False,
)
result = _run(resolve_globs(["ls", glob_ps], reg))
assert isinstance(result[1], PathSpec)
assert result[1].raw_path == result[1].virtual
assert result[1].raw_path == "/data/a.txt"
def test_bare_relative_glob_raw_has_no_dir_prefix():
matches = [
PathSpec(resource_path="data/a.txt",
virtual="/data/a.txt",
directory="/data/",
resolved=True),
]
reg = _mock_registry(resolve_result=matches)
glob_ps = PathSpec(
resource_path="data/*.txt",
virtual="/data/*.txt",
directory="/data/",
pattern="*.txt",
resolved=False,
raw_path="*.txt",
)
result = _run(resolve_globs(["ls", glob_ps], reg))
assert isinstance(result[1], PathSpec)
assert result[1].raw_path == "a.txt"
@@ -0,0 +1,68 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage import MountMode, RAMResource, Workspace
async def _workspace_at(cwd: str) -> Workspace:
ws = Workspace({"/": RAMResource()}, mode=MountMode.WRITE)
await ws.execute(f"mkdir -p {cwd}")
await ws.execute(f"cd {cwd}")
return ws
@pytest.mark.asyncio
async def test_redirect_bare_target_resolves_against_cwd():
# A redirect target is a path by definition: `> BARE` must write
# cwd/BARE even though the bare word would classify as text.
ws = await _workspace_at("/data")
await ws.execute("echo hi > BARE")
io = await ws.execute("cat /data/BARE")
assert (io.stdout or b"") == b"hi\n"
@pytest.mark.asyncio
async def test_redirect_extensionless_relative_target():
ws = await _workspace_at("/data")
await ws.execute("mkdir -p /data/sub")
await ws.execute("echo hi > sub/OUT")
io = await ws.execute("cat /data/sub/OUT")
assert (io.stdout or b"") == b"hi\n"
@pytest.mark.asyncio
async def test_redirect_append_relative_target():
ws = await _workspace_at("/data")
await ws.execute("echo one > LOG")
await ws.execute("echo two >> LOG")
io = await ws.execute("cat /data/LOG")
assert (io.stdout or b"") == b"one\ntwo\n"
@pytest.mark.asyncio
async def test_redirect_stdin_relative_source():
ws = await _workspace_at("/data")
await ws.execute("echo hi > IN")
io = await ws.execute("wc -l < IN")
assert (io.stdout or b"").strip() == b"1"
@pytest.mark.asyncio
async def test_redirect_absolute_target_unchanged():
ws = await _workspace_at("/data")
await ws.execute("echo hi > /data/ABS")
io = await ws.execute("cat /data/ABS")
assert (io.stdout or b"") == b"hi\n"
@@ -0,0 +1,101 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage import MountMode, RAMResource, Workspace
from mirage.commands.spec import SPECS
from mirage.commands.spec.types import OperandKind
from mirage.workspace.expand.spec_hints import (spec_for_command,
spec_word_kinds)
PATH = OperandKind.PATH
TEXT = OperandKind.TEXT
def test_spec_for_command_prefers_cwd_mount():
ws = Workspace({"/": RAMResource()}, mode=MountMode.WRITE)
mount = ws._registry.mount_for("/")
spec = spec_for_command("grep", ws._registry, "/")
assert spec is mount.spec_for("grep")
def test_spec_for_command_falls_back_to_shared_specs():
ws = Workspace({"/": RAMResource()}, mode=MountMode.WRITE)
mount = ws._registry.mount_for("/")
name = next(n for n in SPECS if mount.spec_for(n) is None)
assert spec_for_command(name, ws._registry, "/") is SPECS[name]
def test_spec_for_command_unknown_name_is_none():
ws = Workspace({"/": RAMResource()}, mode=MountMode.WRITE)
assert spec_for_command("no-such-command", ws._registry, "/") is None
def test_basic_grep_pattern_and_path():
kinds = spec_word_kinds(SPECS["grep"], ["pattern", "file.txt"])
assert kinds == [TEXT, PATH]
def test_text_flag_values_positional():
kinds = spec_word_kinds(SPECS["find"], ["/data", "-name", "*.txt"])
assert kinds == [PATH, None, TEXT]
def test_long_value_flag_equals_not_classified():
kinds = spec_word_kinds(SPECS["du"], ["--max-depth=1", "/data"])
assert kinds == [None, PATH]
def test_mixed_cluster_value_is_text():
kinds = spec_word_kinds(SPECS["grep"], ["-ne", "pat", "/a.txt"])
assert kinds == [None, TEXT, PATH]
def test_repeated_dash_e_values_are_text():
kinds = spec_word_kinds(SPECS["grep"],
["-e", "foo", "-e", "bar", "/a.txt"])
assert kinds == [None, TEXT, None, TEXT, PATH]
def test_numeric_shorthand_not_a_path():
kinds = spec_word_kinds(SPECS["head"], ["-5", "file.txt"])
assert kinds == [None, PATH]
def test_find_ignore_tokens_not_classified():
kinds = spec_word_kinds(SPECS["find"],
["/data", "(", "-name", "*.txt", ")"])
assert kinds[0] == PATH
assert kinds[1] is None
assert kinds[4] is None
def test_duplicate_word_text_and_path_slots():
# F8: the same word is the pattern (TEXT) and a file glob (PATH);
# value sets could not tell the two slots apart.
kinds = spec_word_kinds(SPECS["grep"], ["*.txt", "*.txt"])
assert kinds == [TEXT, PATH]
@pytest.mark.asyncio
async def test_du_max_depth_equals_at_root_mount():
ws = Workspace({"/": RAMResource()}, mode=MountMode.WRITE)
await ws.execute("mkdir -p /data/sub")
await ws.execute("tee /data/sub/n.txt > /dev/null", stdin=b"x\n")
io = await ws.execute("du --max-depth=1 /data/sub")
out = (io.stdout or b"").decode()
assert "--max-depth" not in out
assert "/data/sub" in out
+13
View File
@@ -0,0 +1,13 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
+113
View File
@@ -0,0 +1,113 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import boto3
import pytest
from moto import mock_aws
from mirage.resource.disk import DiskResource
from mirage.resource.ram import RAMResource
from mirage.resource.s3.s3 import S3Config, S3Resource
from mirage.types import MountMode
from mirage.workspace.mount import MountRegistry
def _ram_write(p: RAMResource, path: str, data: bytes) -> None:
"""Write file to RAMResource store directly (sync, for test setup)."""
key = "/" + path.strip("/")
parts = key.strip("/").split("/")
for i in range(len(parts) - 1):
p._store.dirs.add("/" + "/".join(parts[:i + 1]))
p._store.files[key] = data
@pytest.fixture
def ram_resource():
"""A RAMResource with test data."""
p = RAMResource()
_ram_write(p, "/hello.txt", b"hello world\n")
_ram_write(p, "/nums.txt", b"3\n1\n2\n")
_ram_write(p, "/sub/nested.txt", b"nested\n")
return p
@pytest.fixture
def empty_resource():
"""An empty RAMResource."""
return RAMResource()
@pytest.fixture
def disk_resource(tmp_path):
"""A DiskResource backed by a temporary directory."""
data_dir = tmp_path / "disk_data"
data_dir.mkdir()
(data_dir / "readme.txt").write_bytes(b"disk file\n")
sub = data_dir / "sub"
sub.mkdir()
(sub / "deep.txt").write_bytes(b"deep content\n")
return DiskResource(root=str(data_dir))
@pytest.fixture
def s3_resource():
"""An S3Resource backed by moto mock."""
with mock_aws():
conn = boto3.client("s3", region_name="us-east-1")
conn.create_bucket(Bucket="test-bucket")
conn.put_object(Bucket="test-bucket",
Key="data/report.csv",
Body=b"col1,col2\n1,2\n")
conn.put_object(Bucket="test-bucket",
Key="data/summary.txt",
Body=b"summary\n")
config = S3Config(
bucket="test-bucket",
region="us-east-1",
endpoint_url=None,
)
yield S3Resource(config)
@pytest.fixture
def registry(ram_resource):
"""MountRegistry with /data/ mounted to RAMResource."""
reg = MountRegistry()
reg.mount("/data/", ram_resource, MountMode.WRITE)
return reg
@pytest.fixture
def multi_registry(s3_resource, disk_resource, ram_resource):
"""MountRegistry with S3, disk, and RAM mounts."""
reg = MountRegistry()
reg.mount("/s3/", s3_resource, MountMode.READ)
reg.mount("/disk/", disk_resource, MountMode.WRITE)
reg.mount("/ram/", ram_resource, MountMode.WRITE)
return reg
@pytest.fixture
def nested_registry():
"""MountRegistry with nested prefixes."""
p1 = RAMResource()
p1._store.files["/file.txt"] = b"outer\n"
p2 = RAMResource()
p2._store.files["/deep.txt"] = b"inner\n"
reg = MountRegistry()
reg.mount("/data/", p1, MountMode.WRITE)
reg.mount("/data/sub/", p2, MountMode.WRITE)
return reg
+199
View File
@@ -0,0 +1,199 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
import pytest
from mirage.resource.ram import RAMResource
from mirage.types import MountMode, PathSpec
from mirage.workspace.mount import MountRegistry
from mirage.workspace.mount.mount import MountEntry
def _run(coro):
return asyncio.run(coro)
# ── prefix validation ──────────────────────────
def test_mount_accepts_root_prefix():
m = MountEntry("/", RAMResource())
assert m.prefix == "/"
def test_mount_rejects_no_leading_slash():
with pytest.raises(ValueError, match="must start with /"):
MountEntry("data/", RAMResource())
def test_mount_rejects_no_trailing_slash():
with pytest.raises(ValueError, match="must end with /"):
MountEntry("/data", RAMResource())
def test_mount_rejects_double_slash():
with pytest.raises(ValueError, match="must not contain //"):
MountEntry("/data//sub/", RAMResource())
def test_mount_valid_prefix():
m = MountEntry("/data/", RAMResource())
assert m.prefix == "/data/"
# ── read-only enforcement ──────────────────────
def test_read_only_blocks_write_ops():
reg = MountRegistry()
reg.mount("/ro/", RAMResource(), MountMode.READ)
mount = reg.mount_for("/ro/file.txt")
with pytest.raises(PermissionError, match="read-only"):
_run(mount.execute_op("write", "/file.txt", data=b"x"))
def test_write_mode_allows_write_ops():
reg = MountRegistry()
reg.mount("/rw/", RAMResource(), MountMode.WRITE)
mount = reg.mount_for("/rw/file.txt")
_run(mount.execute_op("write", "/new.txt", data=b"hello"))
def test_read_only_blocks_write_cmd():
reg = MountRegistry()
reg.mount("/ro/", RAMResource(), MountMode.READ)
mount = reg.mount_for("/ro/file.txt")
scope = PathSpec(resource_path="ro/newdir",
virtual="/ro/newdir",
directory="/ro/",
resolved=True)
stdout, io = _run(mount.execute_cmd("mkdir", [scope], [], {}))
assert io.exit_code != 0
assert b"read-only" in io.stderr
def test_write_mode_allows_write_cmd():
reg = MountRegistry()
reg.mount("/rw/", RAMResource(), MountMode.WRITE)
mount = reg.mount_for("/rw/file.txt")
scope = PathSpec(resource_path="rw/newdir",
virtual="/rw/newdir",
directory="/rw/",
resolved=True)
stdout, io = _run(mount.execute_cmd("mkdir", [scope], [], {}))
assert io.exit_code == 0
def test_read_only_allows_read_cmd():
reg = MountRegistry()
reg.mount("/ro/", RAMResource(), MountMode.READ)
mount = reg.mount_for("/ro/")
scope = PathSpec(resource_path="ro",
virtual="/ro/",
directory="/ro/",
resolved=False)
stdout, io = _run(mount.execute_cmd("ls", [scope], [], {}))
assert io.exit_code == 0
# ── execute_cmd ────────────────────────────────
def test_execute_cmd_cat(registry):
mount = registry.mount_for("/data/hello.txt")
scope = PathSpec(resource_path="data/hello.txt",
virtual="/data/hello.txt",
directory="/data/",
resolved=True)
stdout, io = _run(mount.execute_cmd("cat", [scope], [], {}))
assert io.exit_code == 0
assert stdout is not None
def test_execute_cmd_not_found(registry):
mount = registry.mount_for("/data/hello.txt")
stdout, io = _run(mount.execute_cmd("nonexistent_cmd", [], [], {}))
assert io.exit_code == 127
assert b"command not found" in io.stderr
def test_execute_cmd_ls(registry):
mount = registry.mount_for("/data/hello.txt")
scope = PathSpec(resource_path="data",
virtual="/data/",
directory="/data/",
resolved=False)
stdout, io = _run(mount.execute_cmd("ls", [scope], [], {}))
assert io.exit_code == 0
def test_execute_cmd_with_flag_kwargs(registry):
mount = registry.mount_for("/data/hello.txt")
scope = PathSpec(resource_path="data/hello.txt",
virtual="/data/hello.txt",
directory="/data/",
resolved=True)
stdout, io = _run(mount.execute_cmd("cat", [scope], [], {"n": True}))
assert io.exit_code == 0
def test_execute_cmd_with_texts(registry):
mount = registry.mount_for("/data/hello.txt")
scope = PathSpec(resource_path="data/hello.txt",
virtual="/data/hello.txt",
directory="/data/",
resolved=True)
stdout, io = _run(mount.execute_cmd("grep", [scope], ["hello"], {}))
assert io.exit_code == 0
# ── execute_op ─────────────────────────────────
def test_execute_op_stat(registry):
mount = registry.mount_for("/data/hello.txt")
result = _run(mount.execute_op("stat", "/hello.txt"))
assert result is not None
assert result.size > 0
def test_execute_op_readdir(registry):
mount = registry.mount_for("/data/")
result = _run(mount.execute_op("readdir", "/"))
assert isinstance(result, list)
assert len(result) > 0
def test_execute_op_no_such_op(registry):
mount = registry.mount_for("/data/hello.txt")
with pytest.raises(AttributeError, match="no op"):
_run(mount.execute_op("nonexistent_op", "/file.txt"))
# ── command resolution ─────────────────────────
def test_resolve_command_exists(registry):
mount = registry.mount_for("/data/hello.txt")
cmd = mount.resolve_command("cat")
assert cmd is not None
assert cmd.name == "cat"
def test_resolve_command_missing(registry):
mount = registry.mount_for("/data/hello.txt")
cmd = mount.resolve_command("nonexistent")
assert cmd is None
@@ -0,0 +1,119 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage.workspace.mount.namespace import Namespace
@pytest.fixture
def namespace(registry):
return Namespace(registry)
def test_resolve_delegates_to_registry(namespace, registry):
assert namespace.resolve("/data/hello.txt") == registry.resolve(
"/data/hello.txt")
def test_resolve_follow_noop_without_links(namespace):
assert namespace.resolve(
"/data/hello.txt", follow=True) == namespace.resolve("/data/hello.txt",
follow=False)
def test_resolve_unknown_path_raises(namespace):
with pytest.raises(ValueError, match="no mount"):
namespace.resolve("/unknown/x.txt")
def test_mount_for_delegates_to_registry(namespace, registry):
assert namespace.mount_for("/data/hello.txt") is registry.mount_for(
"/data/hello.txt")
def test_symlink_readlink_roundtrip_verbatim(namespace):
namespace.symlink("/data/link", "/data/hello.txt", 1.0)
assert namespace.is_link("/data/link")
assert namespace.readlink("/data/link") == "/data/hello.txt"
def test_readlink_missing_returns_none(namespace):
assert namespace.readlink("/data/nope") is None
def test_symlink_stores_relative_target_verbatim(namespace):
namespace.symlink("/data/link", "hello.txt", 1.0)
assert namespace.readlink("/data/link") == "hello.txt"
def test_unlink_removes_link(namespace):
namespace.symlink("/data/link", "/data/hello.txt", 1.0)
assert namespace.unlink("/data/link") is True
assert namespace.is_link("/data/link") is False
assert namespace.unlink("/data/link") is False
def test_rename_moves_link(namespace):
namespace.symlink("/data/a", "/data/hello.txt", 1.0)
assert namespace.rename("/data/a", "/data/b") is True
assert namespace.is_link("/data/a") is False
assert namespace.readlink("/data/b") == "/data/hello.txt"
def test_resolve_follows_link_to_target_mount(namespace):
namespace.symlink("/data/link", "/data/hello.txt", 1.0)
assert namespace.resolve(
"/data/link", follow=True) == namespace.resolve("/data/hello.txt")
def test_resolve_no_follow_keeps_link_path(namespace, registry):
namespace.symlink("/data/link", "/data/hello.txt", 1.0)
assert namespace.resolve("/data/link",
follow=False) == registry.resolve("/data/link")
def test_resolve_cycle_raises(namespace):
from mirage.utils.path import CycleError
namespace.symlink("/data/a", "/data/b", 1.0)
namespace.symlink("/data/b", "/data/a", 1.0)
with pytest.raises(CycleError):
namespace.resolve("/data/a", follow=True)
def test_follow_resolves_prefix_links(namespace):
namespace.symlink("/data/link", "/data/real", 1.0)
assert namespace.follow("/data/link/f.txt") == "/data/real/f.txt"
assert namespace.follow("/data/other") == "/data/other"
def test_follow_identity_without_links(namespace):
assert namespace.follow("/data/x") == "/data/x"
def test_links_under_returns_direct_children_only(namespace):
namespace.symlink("/data/a", "/t1", 1.0)
namespace.symlink("/data/sub/b", "/t2", 1.0)
namespace.symlink("/other/c", "/t3", 1.0)
assert namespace.links_under("/data") == {"a": "/t1"}
assert namespace.links_under("/data/sub") == {"b": "/t2"}
def test_purge_under_drops_nested_entries(namespace):
namespace.symlink("/data/sub/a", "/t1", 1.0)
namespace.symlink("/data/sub/deep/b", "/t2", 1.0)
namespace.symlink("/data/keep", "/t3", 1.0)
assert namespace.purge_under("/data/sub") == 2
assert namespace.is_link("/data/keep") is True
assert namespace.is_link("/data/sub/a") is False
@@ -0,0 +1,89 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage import MountMode, Workspace
from mirage.commands.builtin.utils.safeguard import SafeguardExceededError
from mirage.resource.ram import RAMResource
from mirage.types import CommandSafeguard, OnExceed
async def _read_long(accessor, scope, *args, **kwargs):
return b"hello world"
async def _read_lines(accessor, scope, *args, **kwargs):
return b"a\nb\nc\nd\n"
async def _read_short(accessor, scope, *args, **kwargs):
return b"hi"
async def _ws_mount():
ws = Workspace({"/data": RAMResource()}, mode=MountMode.WRITE)
await ws.execute("echo hi > /data/f.txt")
mount = next(m for m in ws._registry._mounts if m.prefix == "/data/")
return mount
@pytest.mark.asyncio
async def test_vfs_read_truncates_to_max_bytes(monkeypatch):
mount = await _ws_mount()
mount.command_safeguards["read"] = CommandSafeguard(max_bytes=5)
monkeypatch.setattr(mount._ops[("read", None)], "fn", _read_long)
assert await mount.execute_op("read", "/data/f.txt") == b"hello"
@pytest.mark.asyncio
async def test_vfs_read_truncates_to_max_lines(monkeypatch):
mount = await _ws_mount()
mount.command_safeguards["read"] = CommandSafeguard(max_lines=2)
monkeypatch.setattr(mount._ops[("read", None)], "fn", _read_lines)
assert await mount.execute_op("read", "/data/f.txt") == b"a\nb\n"
@pytest.mark.asyncio
async def test_vfs_read_on_exceed_error_raises(monkeypatch):
mount = await _ws_mount()
mount.command_safeguards["read"] = CommandSafeguard(
max_bytes=5, on_exceed=OnExceed.ERROR)
monkeypatch.setattr(mount._ops[("read", None)], "fn", _read_long)
with pytest.raises(SafeguardExceededError):
await mount.execute_op("read", "/data/f.txt")
@pytest.mark.asyncio
async def test_vfs_read_within_limit_untouched(monkeypatch):
mount = await _ws_mount()
mount.command_safeguards["read"] = CommandSafeguard(max_bytes=100)
monkeypatch.setattr(mount._ops[("read", None)], "fn", _read_short)
assert await mount.execute_op("read", "/data/f.txt") == b"hi"
@pytest.mark.asyncio
async def test_vfs_unconfigured_read_untouched(monkeypatch):
mount = await _ws_mount()
monkeypatch.setattr(mount._ops[("read", None)], "fn", _read_long)
assert await mount.execute_op("read", "/data/f.txt") == b"hello world"
@pytest.mark.asyncio
async def test_vfs_stat_not_capped_by_byte_limit(monkeypatch):
mount = await _ws_mount()
mount.command_safeguards["stat"] = CommandSafeguard(max_bytes=1)
result = await mount.execute_op("stat", "/data/f.txt")
assert result is not None
assert not isinstance(result, (bytes, bytearray))
@@ -0,0 +1,56 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
import pytest
from mirage import MountMode, Workspace
from mirage.commands.builtin.utils.safeguard import CommandTimeoutError
from mirage.resource.ram import RAMResource
from mirage.types import CommandSafeguard
async def _slow_op(accessor, scope, *args, **kwargs):
await asyncio.sleep(5)
return None
async def _slowish_op(accessor, scope, *args, **kwargs):
await asyncio.sleep(0.2)
return "ok"
async def _ws_mount():
ws = Workspace({"/data": RAMResource()}, mode=MountMode.WRITE)
await ws.execute("echo hi > /data/f.txt")
mount = next(m for m in ws._registry._mounts if m.prefix == "/data/")
return mount
@pytest.mark.asyncio
async def test_vfs_op_honors_per_mount_timeout(monkeypatch):
mount = await _ws_mount()
mount.command_safeguards["stat"] = CommandSafeguard(timeout_seconds=0.05)
monkeypatch.setattr(mount._ops[("stat", None)], "fn", _slow_op)
with pytest.raises(CommandTimeoutError):
await mount.execute_op("stat", "/data/f.txt")
@pytest.mark.asyncio
async def test_vfs_op_unconfigured_is_not_timed(monkeypatch):
mount = await _ws_mount()
monkeypatch.setattr(mount._ops[("stat", None)], "fn", _slowish_op)
result = await mount.execute_op("stat", "/data/f.txt")
assert result == "ok"
@@ -0,0 +1,389 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage.cache.file.ram import RAMFileCacheStore
from mirage.commands.registry import command
from mirage.commands.spec.types import CommandSpec
from mirage.io.types import IOResult
from mirage.resource.base import BaseResource
from mirage.resource.ram import RAMResource
from mirage.resource.ssh import SSHConfig, SSHResource
from mirage.types import MountMode, PathSpec
from mirage.workspace.mount import MountCommandUnsupported, MountRegistry
# ── mount_for ──────────────────────────────────
def test_mount_for_exact(registry):
mount = registry.mount_for("/data/file.txt")
assert mount.prefix == "/data/"
def test_mount_for_nested_path(registry):
mount = registry.mount_for("/data/sub/deep/file.txt")
assert mount.prefix == "/data/"
def test_mount_for_prefix_root(registry):
mount = registry.mount_for("/data")
assert mount.prefix == "/data/"
def test_mount_for_no_match(registry):
with pytest.raises(ValueError, match="no mount"):
registry.mount_for("/unknown/file.txt")
def test_mount_for_dev_default(registry):
mount = registry.mount_for("/dev/null")
assert mount.prefix == "/dev/"
# ── longest prefix match ──────────────────────
def test_nested_prefix_longest_match(nested_registry):
mount = nested_registry.mount_for("/data/sub/deep.txt")
assert mount.prefix == "/data/sub/"
def test_nested_prefix_outer(nested_registry):
mount = nested_registry.mount_for("/data/file.txt")
assert mount.prefix == "/data/"
# ── descendant_mounts ──────────────────────────
def test_descendant_mounts_under_root(multi_registry):
descs = multi_registry.descendant_mounts("/")
prefixes = [m.prefix for m in descs]
# /dev/ is auto-mounted by the registry; ours are /s3/, /disk/, /ram/.
assert "/disk/" in prefixes
assert "/ram/" in prefixes
assert "/s3/" in prefixes
def test_descendant_mounts_under_root_sorted(multi_registry):
descs = multi_registry.descendant_mounts("/")
prefixes = [m.prefix for m in descs]
assert prefixes == sorted(prefixes)
def test_descendant_mounts_at_a_mount_root_is_empty(registry):
# /data/ is a mount; nothing is below it.
assert registry.descendant_mounts("/data") == []
assert registry.descendant_mounts("/data/") == []
def test_descendant_mounts_inside_a_mount_is_empty(registry):
assert registry.descendant_mounts("/data/sub") == []
def test_descendant_mounts_nested(nested_registry):
# Top: /data/, /data/sub/.
descs = nested_registry.descendant_mounts("/")
prefixes = [m.prefix for m in descs]
assert "/data/" in prefixes
assert "/data/sub/" in prefixes
# Below /data/, the only descendant is /data/sub/.
descs2 = nested_registry.descendant_mounts("/data")
assert [m.prefix for m in descs2] == ["/data/sub/"]
# /data/ as a mount root returns its own children only (excludes self).
assert "/data/" not in [m.prefix for m in descs2]
def test_descendant_mounts_excludes_self(nested_registry):
descs = nested_registry.descendant_mounts("/data")
assert all(m.prefix != "/data/" for m in descs)
def test_descendant_mounts_unrelated_path(multi_registry):
# A path in /s3/ has no mount nested below it.
assert multi_registry.descendant_mounts("/s3/data") == []
# ── resolve ────────────────────────────────────
def test_resolve_strips_prefix(registry):
prov, pp, mode = registry.resolve("/data/hello.txt")
assert pp == "/hello.txt"
assert prov.name == "ram"
def test_resolve_root_path(registry):
_, pp, _ = registry.resolve("/data")
assert pp == "/"
def test_resolve_trailing_slash(registry):
_, pp, _ = registry.resolve("/data/sub/")
assert pp.endswith("/")
def test_resolve_mode(registry):
_, _, mode = registry.resolve("/data/file.txt")
assert mode == MountMode.WRITE
def test_resolve_no_match(registry):
with pytest.raises(ValueError):
registry.resolve("/unknown/path")
# ── multi mount (s3 + disk + ram) ──────────────
def test_multi_mount_resolves_s3(multi_registry):
mount = multi_registry.mount_for("/s3/data/report.csv")
assert mount.prefix == "/s3/"
assert mount.resource.name == "s3"
def test_multi_mount_resolves_disk(multi_registry):
mount = multi_registry.mount_for("/disk/readme.txt")
assert mount.prefix == "/disk/"
assert mount.resource.name == "disk"
def test_multi_mount_resolves_ram(multi_registry):
mount = multi_registry.mount_for("/ram/hello.txt")
assert mount.prefix == "/ram/"
assert mount.resource.name == "ram"
def test_multi_mount_modes(multi_registry):
_, _, s3_mode = multi_registry.resolve("/s3/file.txt")
_, _, disk_mode = multi_registry.resolve("/disk/file.txt")
_, _, ram_mode = multi_registry.resolve("/ram/file.txt")
assert s3_mode == MountMode.READ
assert disk_mode == MountMode.WRITE
assert ram_mode == MountMode.WRITE
def test_multi_mount_resource_paths(multi_registry):
_, pp_s3, _ = multi_registry.resolve("/s3/data/report.csv")
_, pp_disk, _ = multi_registry.resolve("/disk/readme.txt")
_, pp_ram, _ = multi_registry.resolve("/ram/hello.txt")
assert pp_s3 == "/data/report.csv"
assert pp_disk == "/readme.txt"
assert pp_ram == "/hello.txt"
# ── mount ──────────────────────────────────────
def test_mount_returns_mount_object():
reg = MountRegistry()
p = RAMResource()
m = reg.mount("/test/", p, MountMode.READ)
assert m.prefix == "/test/"
assert m.resource is p
def test_mount_normalizes_prefix():
reg = MountRegistry()
m = reg.mount("/test/", RAMResource(), MountMode.READ)
assert m.prefix == "/test/"
def test_mount_duplicate_raises(registry):
with pytest.raises(ValueError, match="duplicate"):
registry.mount("/data/", RAMResource())
# ── mounts listing ─────────────────────────────
def test_mounts_returns_all(multi_registry):
prefixes = {m.prefix for m in multi_registry.mounts()}
assert "/s3/" in prefixes
assert "/disk/" in prefixes
assert "/ram/" in prefixes
assert "/dev/" in prefixes
def test_mounts_count(multi_registry):
assert len(multi_registry.mounts()) == 4
# ── group_by_mount ─────────────────────────────
def test_group_by_mount(multi_registry):
groups = multi_registry.group_by_mount(
["/s3/a.txt", "/s3/b.txt", "/disk/c.txt"])
assert len(groups) == 2
s3_group = [g for g in groups if g[0].prefix == "/s3/"][0]
assert len(s3_group[1]) == 2
disk_group = [g for g in groups if g[0].prefix == "/disk/"][0]
assert len(disk_group[1]) == 1
def test_group_by_mount_single(multi_registry):
groups = multi_registry.group_by_mount(["/ram/hello.txt"])
assert len(groups) == 1
# ── get_resource_type ──────────────────────────
def test_get_resource_type_s3(multi_registry):
assert multi_registry.get_resource_type("/s3/file.txt") == "s3"
def test_get_resource_type_disk(multi_registry):
assert multi_registry.get_resource_type("/disk/file.txt") == "disk"
def test_get_resource_type_ram(multi_registry):
assert multi_registry.get_resource_type("/ram/file.txt") == "ram"
def test_get_resource_type_none(multi_registry):
assert multi_registry.get_resource_type(None) is None
def test_get_resource_type_unknown(multi_registry):
assert multi_registry.get_resource_type("/unknown/f") is None
# ── find_resource_by_name ──────────────────────
def test_find_resource_s3(multi_registry):
prov = multi_registry.find_resource_by_name("s3")
assert prov is not None
assert prov.name == "s3"
def test_find_resource_disk(multi_registry):
prov = multi_registry.find_resource_by_name("disk")
assert prov is not None
assert prov.name == "disk"
def test_find_resource_ram(multi_registry):
prov = multi_registry.find_resource_by_name("ram")
assert prov is not None
def test_find_resource_none(multi_registry):
assert multi_registry.find_resource_by_name(None) is None
def test_find_resource_missing(multi_registry):
assert multi_registry.find_resource_by_name("nonexistent") is None
# ── mount_for_command ──────────────────────────
def test_mount_for_command_cat(registry):
mount = registry.mount_for_command("cat")
assert mount is not None
def test_mount_for_command_nonexistent(registry):
assert registry.mount_for_command("nonexistent_cmd") is None
def test_mount_for_command_grep(multi_registry):
mount = multi_registry.mount_for_command("grep")
assert mount is not None
# ── resolve_mount cache redirect ───────────────
def _remote_registry_with_cache():
reg = MountRegistry()
reg.mount("/ssh/", SSHResource(SSHConfig(host="example", root="/srv")),
MountMode.WRITE)
cache = RAMFileCacheStore()
reg.attach_file_cache(cache)
return reg, cache
@pytest.mark.asyncio
async def test_resolve_mount_keeps_cached_read_on_real_mount():
# Warm reads are served in place by with_read_cache, so a cached
# read-only command stays on its real mount (keeping its safeguards and
# custom handlers) instead of being redirected to the cache mount.
reg, cache = _remote_registry_with_cache()
await cache.set("/ssh/a.txt", b"hi")
scope = PathSpec(resource_path="ssh/a.txt",
virtual="/ssh/a.txt",
directory="/ssh",
resolved=True)
mount = await reg.resolve_mount("cat", [scope], "/ssh")
assert mount.prefix == "/ssh/"
@pytest.mark.asyncio
async def test_resolve_mount_keeps_cached_write_on_remote():
reg, cache = _remote_registry_with_cache()
await cache.set("/ssh/a.txt", b"hi")
scope = PathSpec(resource_path="ssh/a.txt",
virtual="/ssh/a.txt",
directory="/ssh",
resolved=True)
mount = await reg.resolve_mount("rm", [scope], "/ssh")
assert mount.prefix == "/ssh/"
class _LimitedResource(BaseResource):
name = "limited"
class _FallbackResource(BaseResource):
name = "fallback"
@command("fallback-only", resource="fallback", spec=CommandSpec())
async def _fallback_only(_store, paths, *texts, **kw):
return b"fallback", IOResult()
def _path_bound_registry_with_default():
reg = MountRegistry()
reg.mount("/limited/", _LimitedResource(), MountMode.WRITE)
fallback = _FallbackResource()
fallback.register(_fallback_only)
reg.mount("/", fallback, MountMode.WRITE)
return reg
@pytest.mark.asyncio
async def test_resolve_mount_rejects_path_bound_unsupported_command():
reg = _path_bound_registry_with_default()
scope = PathSpec(resource_path="limited/file.txt",
virtual="/limited/file.txt",
directory="/limited",
resolved=True)
with pytest.raises(
MountCommandUnsupported,
match="fallback-only: /limited/file.txt: Operation not supported"):
await reg.resolve_mount("fallback-only", [scope], "/limited")
@pytest.mark.asyncio
async def test_resolve_mount_allows_default_without_path_binding():
reg = _path_bound_registry_with_default()
mount = await reg.resolve_mount("fallback-only", [], "/limited")
assert mount is not None
assert mount.prefix == "/"
@@ -0,0 +1,39 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from mirage.resource.ssh import SSHConfig, SSHResource
from mirage.types import MountMode
from mirage.workspace.mount.registry import MountRegistry
def test_ssh_mount_registration():
registry = MountRegistry()
cfg = SSHConfig(host="dev", root="/home/ubuntu")
resource = SSHResource(cfg)
registry.mount("/ssh/", resource, mode=MountMode.WRITE)
mount = registry.mount_for("/ssh/some/file.txt")
assert mount is not None
assert mount.resource.name == "ssh"
def test_ssh_mount_command_resolution():
registry = MountRegistry()
cfg = SSHConfig(host="dev")
resource = SSHResource(cfg)
registry.mount("/remote/", resource, mode=MountMode.WRITE)
mount = registry.mount_for("/remote/test.py")
assert mount is not None
cmd = mount.resolve_command("cat", None)
assert cmd is not None
assert cmd.resource == "ssh"
+13
View File
@@ -0,0 +1,13 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,54 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage import MountMode, RAMResource, Workspace
async def _workspace() -> Workspace:
ws = Workspace({"/": RAMResource()}, mode=MountMode.WRITE)
await ws.execute("mkdir -p /data/sub")
await ws.execute("echo hi > /data/sub/x.txt")
await ws.execute("cd /data")
return ws
@pytest.mark.asyncio
async def test_drain_error_respells_relative_operand():
# cat of a directory errors on the first lazy pull, past the eager
# chokepoint; the drain must still report the operand as typed.
ws = await _workspace()
io = await ws.execute("cat sub")
assert io.exit_code == 1
err = (io.stderr or b"").decode()
assert err.startswith("cat: sub: ")
@pytest.mark.asyncio
async def test_drain_error_keeps_absolute_operand():
ws = await _workspace()
io = await ws.execute("cat /data/sub")
assert io.exit_code == 1
err = (io.stderr or b"").decode()
assert err.startswith("cat: /data/sub: ")
@pytest.mark.asyncio
async def test_eager_error_respells_relative_operand():
ws = await _workspace()
io = await ws.execute("cat sub/missing.txt")
assert io.exit_code == 1
assert (io.stderr or b"").decode() == (
"cat: sub/missing.txt: No such file or directory\n")
@@ -0,0 +1,125 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage.io import IOResult
from mirage.io.types import materialize
from mirage.resource.ram import RAMResource
from mirage.shell.job_table import JobTable
from mirage.shell.parse import parse
from mirage.types import MountMode
from mirage.workspace.mount import MountRegistry
from mirage.workspace.mount.namespace import Namespace
from mirage.workspace.node import run_command_tree as _run_command_tree
from mirage.workspace.session import Session
from mirage.workspace.workspace import Workspace
def run_command_tree(dispatch, registry, *args, **kwargs):
return _run_command_tree(dispatch, registry, Namespace(registry), *args,
**kwargs)
@pytest.fixture
def registry():
"""Minimal registry with a RAM mount at root."""
reg = MountRegistry()
res = RAMResource()
reg.mount("/", res, MountMode.WRITE)
return reg
async def _dispatch_noop(op, path, **kwargs):
return None, IOResult()
async def _noop_execute(command, **kwargs):
return IOResult()
def _session():
return Session(session_id="test", cwd="/")
@pytest.mark.asyncio
async def test_run_command_tree_materializes_stdout(registry):
ast = parse("echo hello")
io, exec_node = await run_command_tree(
_dispatch_noop,
registry,
JobTable(),
_noop_execute,
"agent",
ast,
_session(),
None,
None,
)
assert io.exit_code == 0
assert b"hello" in await materialize(io.stdout)
assert exec_node is not None
@pytest.mark.asyncio
async def test_run_command_tree_propagates_exit_code(registry):
ast = parse("false")
io, _ = await run_command_tree(
_dispatch_noop,
registry,
JobTable(),
_noop_execute,
"agent",
ast,
_session(),
None,
None,
)
assert io.exit_code != 0
async def _cross_node(cmd: str):
# A real two-mount workspace wires dispatch/cache; run_command_tree is the
# seam returning the recorded ExecutionNode (Workspace.execute drops it).
ws = Workspace({
"/a": RAMResource(),
"/b": RAMResource()
},
mode=MountMode.WRITE)
await ws.execute("mkdir -p /a/dir")
await ws.execute("printf 'x\\n' > /a/f.txt")
io, exec_node = await run_command_tree(ws.dispatch, ws._registry,
ws.job_table, _noop_execute,
"agent", parse(cmd),
Session(session_id="t",
cwd="/"), None, None)
return io, exec_node
@pytest.mark.asyncio
async def test_cross_mount_exec_node_records_stderr():
# cp of a directory without -r across mounts: the cross-mount branch builds
# the node via _exec_node, so its stderr/exit_code must match io.
io, exec_node = await _cross_node("cp /a/dir /b/x")
assert io.exit_code == 1
assert b"omitting directory" in await materialize(io.stderr)
assert exec_node.exit_code == 1
assert b"omitting directory" in (exec_node.stderr or b"")
@pytest.mark.asyncio
async def test_cross_mount_exec_node_success_has_no_stderr():
io, exec_node = await _cross_node("cp /a/f.txt /b/f.txt")
assert exec_node.exit_code == 0
assert not (exec_node.stderr or b"")
@@ -0,0 +1,82 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage.provision import Precision, ProvisionResult
from mirage.workspace.provision.control import (handle_for_provision,
handle_if_provision,
handle_while_provision)
COSTS = {
"cond":
ProvisionResult(network_read_low=24,
network_read_high=24,
read_ops=1,
precision=Precision.EXACT),
"then":
ProvisionResult(network_read_low=6,
network_read_high=6,
read_ops=1,
precision=Precision.EXACT),
"else":
ProvisionResult(network_read_low=12,
network_read_high=12,
read_ops=1,
precision=Precision.EXACT),
"free":
ProvisionResult(precision=Precision.EXACT),
}
async def _node(node, session) -> ProvisionResult:
return COSTS[node]
@pytest.mark.asyncio
async def test_if_sums_condition_with_each_branch():
result = await handle_if_provision(_node, [("cond", ["then"])], ["else"],
session=None)
# then-path: 24 + 6 = 30; else-path: 24 + 12 = 36
assert result.network_read_low == 30
assert result.network_read_high == 36
assert result.precision == Precision.RANGE
@pytest.mark.asyncio
async def test_if_without_else_pays_the_conditions():
result = await handle_if_provision(_node, [("cond", ["then"])],
None,
session=None)
# then-path: 24 + 6 = 30; fall-through still stats the condition: 24
assert result.network_read_low == 24
assert result.network_read_high == 30
@pytest.mark.asyncio
async def test_if_elif_ladder_accumulates_conditions():
branches = [("cond", ["free"]), ("cond", ["then"])]
result = await handle_if_provision(_node, branches, None, session=None)
# branch1: 24; branch2: 24 + 24 + 6 = 54; fall-through: 48
assert result.network_read_low == 24
assert result.network_read_high == 54
@pytest.mark.asyncio
async def test_for_scales_and_while_is_unknown():
result = await handle_for_provision(_node, ["then"], 3, session=None)
assert result.network_read_low == 18
assert result.precision == Precision.EXACT
result = await handle_while_provision(_node, ["then"], session=None)
assert result.precision == Precision.UNKNOWN
@@ -0,0 +1,176 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage import MountMode, Workspace
from mirage.resource.ram import RAMResource
from mirage.shell.node_kind import NodeKind
# Drift guard: every statement kind the executor supports must have a
# pinned provision expectation here. The map must cover the full enum
# (asserted below), so adding a NodeKind forces deciding what the
# planner reports for it; a construct can no longer be supported by
# the executor and silently mis-planned.
#
# The seeded file is 24 bytes. Expectations are
# (snippet, network_read, network_write, precision).
PLANS = {
NodeKind.COMMENT: ("# a comment", "0", "0", "exact"),
NodeKind.PROGRAM: ("cat /data/a.txt; cat /data/a.txt", "48", "0", "exact"),
NodeKind.COMMAND: ("cat /data/a.txt", "24", "0", "exact"),
NodeKind.PIPELINE: ("cat /data/a.txt | wc -l", "24", "0", "exact"),
NodeKind.LIST: ("cat /data/a.txt && cat /data/a.txt", "48", "0", "exact"),
NodeKind.REDIRECT:
("cat /data/a.txt > /data/out.txt", "24", "0-24", "range"),
NodeKind.SUBSHELL: ("(cat /data/a.txt)", "24", "0", "exact"),
NodeKind.COMPOUND: ("{ cat /data/a.txt; }", "24", "0", "exact"),
NodeKind.IF: ("if true; then cat /data/a.txt; fi", "0-24", "0", "range"),
NodeKind.FOR:
("for i in 1 2; do cat /data/a.txt; done", "48", "0", "exact"),
NodeKind.SELECT:
("select x in a b; do cat /data/a.txt; done", "24", "0", "unknown"),
NodeKind.WHILE:
("while true; do cat /data/a.txt; done", "24", "0", "unknown"),
NodeKind.UNTIL: ("until false; do cat /data/a.txt; done", "24", "0",
"unknown"),
NodeKind.CASE: ("case x in x) cat /data/a.txt;; esac", "24", "0", "range"),
NodeKind.FUNCTION_DEF: ("f() { cat /data/a.txt; }", "0", "0", "exact"),
NodeKind.DECLARATION: ("export FOO=1", "0", "0", "exact"),
NodeKind.UNSET: ("unset FOO", "0", "0", "exact"),
NodeKind.TEST: ("[[ -n x ]]", "0", "0", "exact"),
NodeKind.NEGATED: ("! grep zzz /data/a.txt", "24", "0", "exact"),
NodeKind.VAR_ASSIGN: ("FOO=1", "0", "0", "exact"),
NodeKind.UNSUPPORTED: ("for ((i=0;i<2;i++)); do true; done", "0", "0",
"unknown"),
}
def test_plans_cover_the_full_enum():
assert set(PLANS) == set(NodeKind)
@pytest.mark.asyncio
@pytest.mark.parametrize("kind", list(NodeKind))
async def test_every_kind_plans(kind):
snippet, net, write, precision = PLANS[kind]
ws = Workspace({"/data": RAMResource()}, mode=MountMode.WRITE)
await ws.execute("tee /data/a.txt > /dev/null", stdin=b"x" * 24)
result = await ws.execute(snippet, provision=True)
assert result.network_read == net, kind
assert result.network_write == write, kind
assert result.precision.value == precision, kind
await ws.close()
@pytest.mark.asyncio
async def test_function_call_and_env_prefix_plan():
ws = Workspace({"/data": RAMResource()}, mode=MountMode.WRITE)
await ws.execute("tee /data/a.txt > /dev/null", stdin=b"x" * 24)
result = await ws.execute("f() { cat /data/a.txt; }; f", provision=True)
assert result.network_read == "24"
assert result.precision.value == "exact"
result = await ws.execute("f() { f; }; f", provision=True)
assert result.precision.value == "unknown"
result = await ws.execute("FOO=1 cat /data/a.txt", provision=True)
assert result.network_read == "24"
assert result.precision.value == "exact"
result = await ws.execute("eval 'cat /data/a.txt'", provision=True)
assert result.precision.value == "unknown"
result = await ws.execute("wc -l < /data/a.txt", provision=True)
assert result.network_read == "24"
result = await ws.execute("cat /data/a.txt > /dev/null", provision=True)
assert result.network_write == "0"
assert result.precision.value == "exact"
await ws.close()
@pytest.mark.asyncio
async def test_provision_follows_symlinks_and_spans_mounts():
ws = Workspace({
"/data": RAMResource(),
"/data2": RAMResource()
},
mode=MountMode.WRITE)
await ws.execute("tee /data/a.txt > /dev/null", stdin=b"x" * 24)
await ws.execute("tee /data2/b.txt > /dev/null", stdin=b"y" * 6)
await ws.execute("ln -s /data/a.txt /data2/lnk.txt")
result = await ws.execute("cat /data2/lnk.txt", provision=True)
assert result.network_read == "24"
assert result.precision.value == "exact"
result = await ws.execute("cat /data/a.txt /data2/b.txt", provision=True)
assert result.network_read == "30"
assert result.read_ops == 2
assert result.precision.value == "exact"
result = await ws.execute("cat /data2/b.txt /data/a.txt", provision=True)
assert result.network_read == "30"
await ws.close()
@pytest.mark.asyncio
async def test_provision_is_dry_and_case_arms_run_fully():
ws = Workspace({"/data": RAMResource()}, mode=MountMode.WRITE)
await ws.execute("tee /data/a.txt > /dev/null", stdin=b"x" * 24)
# a dry run must not execute command substitutions
result = await ws.execute(
"cat $(tee /data/leak.txt > /dev/null; echo /data/a.txt)",
provision=True)
assert result.precision.value == "unknown"
listing = await (await ws.execute("ls /data")).stdout_str()
assert "leak.txt" not in listing
# a case arm runs every statement up to its ;; terminator
out = await (
await
ws.execute("case x in x) echo one; echo two;; esac")).stdout_str()
assert out == "one\ntwo\n"
result = await ws.execute(
"case x in x) cat /data/a.txt; cat /data/a.txt;; esac", provision=True)
assert result.network_read == "48"
# sed reads its operands; -i degrades to a floor
result = await ws.execute("sed s/x/y/ /data/a.txt", provision=True)
assert result.network_read == "24"
assert result.precision.value == "exact"
result = await ws.execute("sed -i s/x/y/ /data/a.txt", provision=True)
assert result.precision.value == "unknown"
await ws.close()
@pytest.mark.asyncio
async def test_stdin_driven_and_expanded_estimates():
ws = Workspace({"/data": RAMResource()}, mode=MountMode.WRITE)
await ws.execute("tee /data/a.txt > /dev/null", stdin=b"x" * 24)
await ws.execute("mkdir /data/tree")
await ws.execute("tee /data/tree/b.txt > /dev/null", stdin=b"y" * 10)
# heredoc-fed stdin is local bytes: exact zero backend I/O
result = await ws.execute("wc -l <<EOF\nabc\nEOF", provision=True)
assert result.network_read == "0"
assert result.precision.value == "exact"
# globs expand during planning
result = await ws.execute("cat /data/tree/*.txt", provision=True)
assert result.network_read == "10"
assert result.precision.value == "exact"
# recursive search walks the tree
result = await ws.execute("grep -r y /data/tree", provision=True)
assert result.network_read == "10"
assert result.precision.value == "exact"
# a suppressed substitution degrades the loop count to a floor
result = await ws.execute("for i in $(echo 1 2); do cat /data/a.txt; done",
provision=True)
assert result.network_read == "24"
assert result.precision.value == "unknown"
# a suppressed substitution hides the redirect target
result = await ws.execute("cat /data/a.txt > $(echo /data/out.txt)",
provision=True)
assert result.precision.value == "unknown"
await ws.close()
@@ -0,0 +1,78 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from mirage.resource.ram import RAMResource
from mirage.types import MountMode
from mirage.workspace import Workspace
from mirage.workspace.route import SHELL_CONSUMERS, Consumer, route
from mirage.workspace.session import Session
def _fixture() -> tuple[Session, Workspace]:
ws = Workspace(resources={"/ram": (RAMResource(), MountMode.WRITE)})
return Session(session_id="t"), ws
def test_builtins_route_session():
session, ws = _fixture()
for name in ("cd", "echo", "export", "history", "test", "xargs"):
assert route(name, session, ws._registry) is Consumer.SESSION
def test_unsupported_builtins_route_session():
session, ws = _fixture()
assert route("exec", session, ws._registry) is Consumer.SESSION
def test_namespace_commands():
session, ws = _fixture()
assert route("ln", session, ws._registry) is Consumer.NAMESPACE
assert route("readlink", session, ws._registry) is Consumer.NAMESPACE
def test_function_routes_function():
session, ws = _fixture()
session.functions["greet"] = []
assert route("greet", session, ws._registry) is Consumer.FUNCTION
def test_builtin_shadows_function():
session, ws = _fixture()
session.functions["echo"] = []
assert route("echo", session, ws._registry) is Consumer.SESSION
def test_function_shadows_mount_command():
session, ws = _fixture()
session.functions["cat"] = []
assert route("cat", session, ws._registry) is Consumer.FUNCTION
def test_mount_command_routes_mount():
session, ws = _fixture()
assert route("cat", session, ws._registry) is Consumer.MOUNT
assert route("grep", session, ws._registry) is Consumer.MOUNT
def test_unregistered_name_routes_unknown():
session, ws = _fixture()
assert route("nosuchcmd", session, ws._registry) is Consumer.UNKNOWN
def test_shell_consumers_resolve_globs():
assert Consumer.SESSION in SHELL_CONSUMERS
assert Consumer.NAMESPACE in SHELL_CONSUMERS
assert Consumer.FUNCTION in SHELL_CONSUMERS
assert Consumer.MOUNT not in SHELL_CONSUMERS
assert Consumer.UNKNOWN not in SHELL_CONSUMERS
@@ -0,0 +1,36 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from mirage.workspace.route import (SHELL_CONSUMERS, Consumer, WordPolicy,
word_policy)
def test_shell_consumers_get_shell_policy():
for consumer in SHELL_CONSUMERS:
assert word_policy(consumer) is WordPolicy.SHELL
def test_mount_gets_mount_policy():
assert word_policy(Consumer.MOUNT) is WordPolicy.MOUNT
def test_unknown_gets_mount_policy():
# Unknown names keep patterns intact: nothing resolves their words,
# the command fails before any backend I/O.
assert word_policy(Consumer.UNKNOWN) is WordPolicy.MOUNT
def test_every_consumer_has_a_policy():
for consumer in Consumer:
assert isinstance(word_policy(consumer), WordPolicy)
@@ -0,0 +1,13 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
@@ -0,0 +1,136 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
import pytest
from mirage.workspace.session import SessionManager
def _run(coro):
return asyncio.run(coro)
def test_manager_default_session():
mgr = SessionManager("default")
s = mgr.get("default")
assert s.session_id == "default"
def test_manager_default_cwd():
mgr = SessionManager("default")
assert mgr.cwd == "/"
mgr.cwd = "/data"
assert mgr.cwd == "/data"
assert mgr.get("default").cwd == "/data"
def test_manager_default_env():
mgr = SessionManager("default")
assert mgr.env == {}
mgr.env = {"A": "1"}
assert mgr.env == {"A": "1"}
assert mgr.get("default").env == {"A": "1"}
def test_manager_create_session():
mgr = SessionManager("default")
s = mgr.create("worker-1")
assert s.session_id == "worker-1"
assert mgr.get("worker-1") is s
def test_manager_create_duplicate_raises():
mgr = SessionManager("default")
mgr.create("s1")
with pytest.raises(ValueError, match="already exists"):
mgr.create("s1")
def test_manager_get_missing_raises():
mgr = SessionManager("default")
with pytest.raises(KeyError):
mgr.get("nonexistent")
def test_manager_list_sessions():
mgr = SessionManager("default")
mgr.create("s1")
mgr.create("s2")
sessions = mgr.list()
ids = {s.session_id for s in sessions}
assert ids == {"default", "s1", "s2"}
def test_manager_close_session():
mgr = SessionManager("default")
mgr.create("temp")
_run(mgr.close("temp"))
with pytest.raises(KeyError):
mgr.get("temp")
def test_manager_close_default_raises():
mgr = SessionManager("default")
with pytest.raises(ValueError, match="Cannot close"):
_run(mgr.close("default"))
def test_manager_close_missing_raises():
mgr = SessionManager("default")
with pytest.raises(KeyError):
_run(mgr.close("nonexistent"))
def test_manager_close_all():
mgr = SessionManager("default")
mgr.create("s1")
mgr.create("s2")
_run(mgr.close_all())
sessions = mgr.list()
assert len(sessions) == 1
assert sessions[0].session_id == "default"
def test_manager_sessions_isolated():
mgr = SessionManager("default")
s1 = mgr.create("s1")
s2 = mgr.create("s2")
s1.env["X"] = "from-s1"
s1.cwd = "/s1"
assert "X" not in s2.env
assert s2.cwd == "/"
def test_manager_lock_for():
mgr = SessionManager("default")
lock = mgr.lock_for("default")
assert lock is not None
mgr.create("s1")
lock2 = mgr.lock_for("s1")
assert lock2 is not lock
def test_manager_create_with_allowed_mounts():
mgr = SessionManager("default")
s = mgr.create("agent", allowed_mounts=frozenset({"/s3", "/slack"}))
assert s.allowed_mounts == frozenset({"/s3", "/slack"})
def test_manager_create_default_unrestricted():
mgr = SessionManager("default")
s = mgr.create("worker")
assert s.allowed_mounts is None
@@ -0,0 +1,157 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from mirage.workspace.session import Session
def test_session_defaults():
s = Session(session_id="test")
assert s.session_id == "test"
assert s.cwd == "/"
assert s.env == {}
assert s.functions == {}
assert s.last_exit_code == 0
assert s._stdin_buffer is None
def test_session_custom_cwd():
s = Session(session_id="s1", cwd="/data")
assert s.cwd == "/data"
def test_session_env():
s = Session(session_id="s1", env={"A": "1", "B": "2"})
assert s.env["A"] == "1"
assert s.env["B"] == "2"
def test_session_env_mutation():
s = Session(session_id="s1")
s.env["X"] = "hello"
assert s.env["X"] == "hello"
del s.env["X"]
assert "X" not in s.env
def test_session_functions():
s = Session(session_id="s1")
s.functions["myfunc"] = "body"
assert "myfunc" in s.functions
def test_session_exit_code():
s = Session(session_id="s1")
s.last_exit_code = 42
assert s.last_exit_code == 42
def test_session_stdin_buffer():
s = Session(session_id="s1")
s._stdin_buffer = b"hello\n"
assert s._stdin_buffer == b"hello\n"
s._stdin_buffer = None
assert s._stdin_buffer is None
def test_session_to_dict():
s = Session(session_id="s1", cwd="/data", env={"K": "V"})
d = s.to_dict()
assert d["session_id"] == "s1"
assert d["cwd"] == "/data"
assert d["env"] == {"K": "V"}
assert "created_at" in d
def test_session_from_dict():
d = {
"session_id": "s2",
"cwd": "/tmp",
"env": {
"A": "1"
},
"created_at": 123.0
}
s = Session.from_dict(d)
assert s.session_id == "s2"
assert s.cwd == "/tmp"
assert s.env["A"] == "1"
assert s.created_at == 123.0
def test_session_roundtrip():
original = Session(session_id="rt", cwd="/x", env={"K": "V"})
restored = Session.from_dict(original.to_dict())
assert restored.session_id == original.session_id
assert restored.cwd == original.cwd
assert restored.env == original.env
def test_session_independent_envs():
s1 = Session(session_id="a")
s2 = Session(session_id="b")
s1.env["X"] = "1"
assert "X" not in s2.env
def test_session_allowed_mounts_default_none():
s = Session(session_id="s")
assert s.allowed_mounts is None
def test_session_allowed_mounts_set():
s = Session(session_id="s", allowed_mounts=frozenset({"/s3", "/slack"}))
assert s.allowed_mounts == frozenset({"/s3", "/slack"})
def test_fork_copies_every_field_including_allowed_mounts():
original = Session(
session_id="orig",
cwd="/disk",
env={"FOO": "bar"},
functions={"f": object()},
last_exit_code=7,
shell_options={"errexit": True},
readonly_vars={"HOME"},
arrays={"ARGV": ["a", "b"]},
allowed_mounts=frozenset({"/s3", "/dev", "/"}),
)
forked = original.fork()
assert forked.session_id == "orig"
assert forked.cwd == "/disk"
assert forked.env == {"FOO": "bar"}
assert forked.allowed_mounts == frozenset({"/s3", "/dev", "/"})
assert forked.shell_options == {"errexit": True}
assert "HOME" in forked.readonly_vars
assert forked.arrays == {"ARGV": ["a", "b"]}
assert forked.last_exit_code == 7
def test_fork_overrides_apply_without_mutating_original():
original = Session(session_id="orig", cwd="/disk", env={"FOO": "bar"})
forked = original.fork(cwd="/ram", env={"BAZ": "qux"})
assert forked.cwd == "/ram"
assert forked.env == {"BAZ": "qux"}
assert original.cwd == "/disk"
assert original.env == {"FOO": "bar"}
def test_fork_deep_copies_mutable_containers():
original = Session(session_id="orig",
env={"FOO": "bar"},
arrays={"A": ["1"]})
forked = original.fork()
forked.env["NEW"] = "leaked?"
forked.arrays["A"].append("2")
assert "NEW" not in original.env
assert original.arrays["A"] == ["1"]
@@ -0,0 +1,46 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from mirage.workspace.session.session import Session
from mirage.workspace.session.shell_dirs import change_dir, home_dir
def test_home_dir_unset_is_none():
session = Session(session_id="s")
assert home_dir(session) is None
def test_home_dir_from_env():
session = Session(session_id="s", env={"HOME": "/data"})
assert home_dir(session) == "/data"
def test_home_dir_empty_env_is_none():
session = Session(session_id="s", env={"HOME": ""})
assert home_dir(session) is None
def test_change_dir_sets_cwd_and_oldpwd():
session = Session(session_id="s", cwd="/data")
change_dir(session, "/data/sub")
assert session.cwd == "/data/sub"
assert session.env["OLDPWD"] == "/data"
def test_change_dir_overwrites_oldpwd():
session = Session(session_id="s", cwd="/a")
change_dir(session, "/b")
change_dir(session, "/c")
assert session.cwd == "/c"
assert session.env["OLDPWD"] == "/b"
@@ -0,0 +1,108 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
from collections.abc import Iterator
import boto3
import pytest
from moto.server import ThreadedMotoServer
from mirage.resource.s3.s3 import S3Config, S3Resource
from mirage.types import MountMode
from mirage.workspace import Workspace
CREDS = dict(aws_access_key_id="testing",
aws_secret_access_key="testing",
region_name="us-east-1")
@pytest.fixture()
def s3_endpoint() -> Iterator[str]:
server = ThreadedMotoServer(ip_address="127.0.0.1", port=0, verbose=False)
server.start()
host, port = server.get_host_and_port()
yield f"http://{host}:{port}"
server.stop()
def _s3_workspace(endpoint: str, bucket: str) -> Workspace:
boto3.client("s3", endpoint_url=endpoint,
**CREDS).create_bucket(Bucket=bucket)
s3 = S3Resource(
S3Config(bucket=bucket,
region="us-east-1",
endpoint_url=endpoint,
aws_access_key_id="testing",
aws_secret_access_key="testing",
path_style=True))
return Workspace({"/data": s3}, mode=MountMode.WRITE)
async def _exec(ws: Workspace, cmd: str) -> tuple[int, str, str]:
result = await ws.execute(cmd)
out = await result.stdout_str()
err = await result.stderr_str()
return result.exit_code, out, err
async def _gzip_roundtrip_interleaved_ls(
ws: Workspace) -> tuple[int, str, str]:
cmd = ("echo two | tee /data/arch/h.txt > /dev/null"
" && gzip /data/arch/h.txt"
" && ls /data/arch"
" && gunzip /data/arch/h.txt.gz"
" && cat /data/arch/h.txt")
return await _exec(ws, cmd)
def test_gzip_roundtrip_with_interleaved_ls(s3_endpoint):
ws = _s3_workspace(s3_endpoint, "bucket-gzip-ls")
code, out, err = asyncio.run(_gzip_roundtrip_interleaved_ls(ws))
assert code == 0, f"exit {code}, stderr: {err!r}"
assert "two" in out
async def _overwrite_then_ls(ws: Workspace) -> tuple[int, str, str]:
setup = await _exec(
ws, "echo one | tee /data/arch/a.txt > /dev/null"
" && ls /data/arch")
assert setup[0] == 0, setup
code, out, err = await _exec(
ws, "echo two | tee /data/arch/b.txt > /dev/null && ls /data/arch")
return code, out, err
def test_ls_sees_file_created_after_listing(s3_endpoint):
ws = _s3_workspace(s3_endpoint, "bucket-ls-create")
code, out, err = asyncio.run(_overwrite_then_ls(ws))
assert code == 0, f"exit {code}, stderr: {err!r}"
assert "b.txt" in out
async def _rm_then_stat(ws: Workspace) -> tuple[int, str, str]:
setup = await _exec(
ws, "echo gone | tee /data/arch/c.txt > /dev/null"
" && ls /data/arch")
assert setup[0] == 0, setup
rm = await _exec(ws, "rm /data/arch/c.txt")
assert rm[0] == 0, rm
return await _exec(ws, "ls /data/arch")
def test_ls_does_not_show_removed_file(s3_endpoint):
ws = _s3_workspace(s3_endpoint, "bucket-rm-stat")
code, out, err = asyncio.run(_rm_then_stat(ws))
assert code == 0, f"exit {code}, stderr: {err!r}"
assert "c.txt" not in out
@@ -0,0 +1,97 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage.commands.registry import command
from mirage.commands.spec import SPECS
from mirage.core.disk.glob import resolve_glob
from mirage.core.disk.read import read_bytes
from mirage.io.types import IOResult
from mirage.resource.disk import DiskResource
from mirage.resource.ram import RAMResource
from mirage.types import ConsistencyPolicy, MountMode, PathSpec
from mirage.workspace import Workspace
@command("stat", resource="disk", spec=SPECS["stat"], filetype=".zzz")
async def stat_zzz_disk(
accessor,
paths: list[PathSpec],
*texts: str,
stdin=None,
index=None,
**_extra: object,
) -> tuple[bytes | None, IOResult]:
paths = await resolve_glob(accessor, paths, index)
raw = await read_bytes(accessor, paths[0])
return b"CUSTOM DISK STAT %d\n" % len(raw), IOResult(
reads={paths[0].mount_path: raw}, cache=[paths[0].mount_path])
@pytest.mark.asyncio
async def test_cache_decoupled_from_root_mount():
"""The file cache is a hidden store reached via ``registry.file_cache``,
not the virtual root mount's resource. When no ``/`` is mounted the root
is an ordinary empty RAM mount at ``/`` (a normal entry in ``_mounts``)
and never holds the cache."""
ws = Workspace({"/data/": RAMResource()}, mode=MountMode.WRITE)
assert ws._registry.file_cache is ws.cache
assert ws._registry.root_mount.resource is not ws.cache
assert ws._registry.root_mount.resource.caches_reads is False
assert ws._registry.root_mount.prefix == "/"
assert ws._registry.root_mount in ws._registry.mounts()
@pytest.mark.asyncio
async def test_warm_read_stays_on_real_mount(tmp_path):
"""Read-through: the second (cached) read still runs the REAL mount's
command. The cache is a hidden store, not a mount, so a warm read serves
the cached bytes while the command stays on its real mount and keeps its
custom handler."""
(tmp_path / "example.zzz").write_bytes(b"payload")
disk = DiskResource(root=str(tmp_path))
disk.caches_reads = True
ws = Workspace({"/": disk}, mode=MountMode.READ)
ws.mount("/").register_fns([stat_zzz_disk])
first = await ws.execute("stat /example.zzz")
second = await ws.execute("stat /example.zzz")
assert "CUSTOM DISK STAT" in (await first.stdout_str())
assert "CUSTOM DISK STAT" in (await second.stdout_str()), (
"warm read lost the real mount's custom handler; read-through should "
"keep the command on the real mount")
@pytest.mark.asyncio
async def test_cross_mount_read_serves_cache(tmp_path):
"""A cross-mount read relays each operand through ``execute_op``, and the
op-layer read-through serves a warm operand from cache. Proven under LAZY
by mutating the file out-of-band: the cross-mount read still returns the
cached v1."""
(tmp_path / "a.txt").write_bytes(b"v1\n")
disk = DiskResource(root=str(tmp_path))
disk.caches_reads = True
ws = Workspace({
"/d/": disk,
"/r/": RAMResource()
},
mode=MountMode.WRITE,
consistency=ConsistencyPolicy.LAZY)
await ws.execute("echo hi > /r/b.txt")
await (await ws.execute("cat /d/a.txt")).stdout_str()
(tmp_path / "a.txt").write_bytes(b"v2\n")
out = await (await ws.execute("cat /d/a.txt /r/b.txt")).stdout_str()
assert "v1" in out and "v2" not in out, (
f"cross-mount read did not serve the warm operand from cache: {out!r}")
@@ -0,0 +1,89 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
from collections.abc import AsyncIterator
from mirage.commands import COMMANDS as _CMDS
from mirage.resource.ram import RAMResource
from mirage.types import MountMode, PathSpec
from mirage.workspace import Workspace
ram_cat = _CMDS["cat"]
def _cat_ops():
return ram_cat.__wrapped__.args[0]
def _spying_stream(real_stream, pulled: list[str]):
def factory(accessor, p: PathSpec, index=None) -> AsyncIterator[bytes]:
return _spy_iter(real_stream(accessor, p, index), p.virtual, pulled)
return factory
async def _spy_iter(source: AsyncIterator[bytes], name: str,
pulled: list[str]) -> AsyncIterator[bytes]:
first = True
async for chunk in source:
if first:
pulled.append(name)
first = False
yield chunk
def _seeded_ws() -> Workspace:
ws = Workspace({"/data": RAMResource()}, mode=MountMode.WRITE)
async def seed():
await ws.execute("tee /data/a.txt > /dev/null", stdin=b"a1\na2\na3\n")
await ws.execute("tee /data/b.txt > /dev/null", stdin=b"b1\nb2\n")
asyncio.run(seed())
return ws
def _spy_cat_reads(ws, command, pulled):
ops = _cat_ops()
real = ops.read_stream
object.__setattr__(ops, "read_stream", _spying_stream(real, pulled))
try:
return asyncio.run(_run_and_collect(ws, command))
finally:
object.__setattr__(ops, "read_stream", real)
async def _run_and_collect(ws, command):
result = await ws.execute(command)
out = await result.stdout_str()
await ws.close()
return out
def test_multi_cat_head_skips_second_file():
ws = _seeded_ws()
pulled: list[str] = []
out = _spy_cat_reads(ws, "cat /data/a.txt /data/b.txt | head -n 1", pulled)
assert out == "a1\n"
assert pulled == ["/data/a.txt"]
def test_multi_cat_full_reads_both_files():
ws = _seeded_ws()
pulled: list[str] = []
out = _spy_cat_reads(ws, "cat /data/a.txt /data/b.txt", pulled)
assert out == "a1\na2\na3\nb1\nb2\n"
assert pulled == ["/data/a.txt", "/data/b.txt"]
@@ -0,0 +1,96 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
from mirage.resource.ram import RAMResource
from mirage.types import CommandSafeguard, MountMode, OnExceed
from mirage.workspace import Workspace
def _build_ws(n_lines: int) -> Workspace:
r = RAMResource()
r._store.dirs.add("/")
body = b"".join(f"line{i}\n".encode() for i in range(n_lines))
r._store.files["/big.txt"] = body
return Workspace({"/": (r, MountMode.WRITE)})
def _override(ws: Workspace, name: str, safeguard: CommandSafeguard) -> None:
mounts = list(ws._registry._mounts)
for m in mounts:
m.command_safeguards[name] = safeguard
async def _run(ws: Workspace, cmd: str):
try:
io = await ws.execute(cmd)
stdout = await io.stdout_str()
stderr = await io.stderr_str()
return io.exit_code, stdout, stderr
finally:
await ws.close()
def test_cat_truncates_at_default_2000():
ws = _build_ws(2500)
code, out, err = asyncio.run(_run(ws, "cat /big.txt"))
assert code == 0
assert out.count("\n") == 2000
assert out.startswith("line0\n")
assert "line1999\n" in out
assert "line2000" not in out
assert "truncated" in err
def test_pipe_intermediate_not_capped():
ws = _build_ws(2500)
code, out, _ = asyncio.run(_run(ws, "cat /big.txt | wc -l"))
assert code == 0
assert out.strip() == "2500"
def test_pipe_terminal_under_limit_no_notice():
ws = _build_ws(2500)
code, out, err = asyncio.run(_run(ws, "cat /big.txt | tail -n 3"))
assert code == 0
assert out == "line2497\nline2498\nline2499\n"
assert "truncated" not in err
def test_mount_override_caps_small():
ws = _build_ws(5)
_override(ws, "cat", CommandSafeguard(max_lines=3))
code, out, err = asyncio.run(_run(ws, "cat /big.txt"))
assert code == 0
assert out == "line0\nline1\nline2\n"
assert "truncated" in err
def test_on_exceed_error_mode():
ws = _build_ws(5)
_override(ws, "cat", CommandSafeguard(max_lines=3,
on_exceed=OnExceed.ERROR))
code, out, err = asyncio.run(_run(ws, "cat /big.txt"))
assert code == 1
assert out == ""
assert "truncated" in err
def test_below_limit_untouched():
ws = _build_ws(5)
code, out, err = asyncio.run(_run(ws, "cat /big.txt"))
assert code == 0
assert out == "line0\nline1\nline2\nline3\nline4\n"
assert "truncated" not in err
@@ -0,0 +1,331 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
import logging
import pytest
from mirage import MountMode, Workspace
from mirage.commands import safeguard as sg
from mirage.resource.ram import RAMResource
from mirage.types import CommandSafeguard, OnExceed
async def _slow_provision(*args, **kwargs):
await asyncio.sleep(5)
@pytest.fixture
def restore_defaults():
snapshot = dict(sg.DEFAULT_COMMAND_SAFEGUARDS)
yield
sg.DEFAULT_COMMAND_SAFEGUARDS.clear()
sg.DEFAULT_COMMAND_SAFEGUARDS.update(snapshot)
def _ws(safeguards: dict | None = None) -> Workspace:
if safeguards:
return Workspace(
{"/data": (RAMResource(), MountMode.WRITE, safeguards)},
mode=MountMode.WRITE)
return Workspace({"/data": RAMResource()}, mode=MountMode.WRITE)
@pytest.mark.asyncio
async def test_quick_builtin_under_default_does_not_fire():
ws = _ws()
r = await ws.execute("echo hi")
assert r.exit_code == 0
assert (await r.stdout_str()) == "hi\n"
@pytest.mark.asyncio
async def test_builtin_default_safeguard_fires(restore_defaults):
sg.DEFAULT_COMMAND_SAFEGUARDS["sleep"] = CommandSafeguard(
timeout_seconds=0.1)
ws = _ws()
r = await ws.execute("sleep 2")
assert r.exit_code == 124
assert "sleep: timed out after 0.1s" in (await r.stderr_str())
@pytest.mark.asyncio
async def test_fallback_safeguard_applies_to_unknown_command(restore_defaults):
sg.DEFAULT_COMMAND_SAFEGUARDS["sleep"] = CommandSafeguard(
timeout_seconds=0.05)
ws = _ws()
r = await ws.execute("sleep 1")
assert r.exit_code == 124
@pytest.mark.asyncio
async def test_pipeline_first_stage_to_trip_wins(restore_defaults):
sg.DEFAULT_COMMAND_SAFEGUARDS["sleep"] = CommandSafeguard(
timeout_seconds=0.1)
ws = _ws()
r = await ws.execute("sleep 2 | echo done")
assert r.exit_code == 124
assert "sleep: timed out" in (await r.stderr_str())
@pytest.mark.asyncio
async def test_timeout_zero_disables(restore_defaults):
sg.DEFAULT_COMMAND_SAFEGUARDS["sleep"] = CommandSafeguard(
timeout_seconds=0)
ws = _ws()
r = await ws.execute("sleep 0.1")
assert r.exit_code == 0
@pytest.mark.asyncio
async def test_mount_override_threaded_via_constructor():
overrides = {"cat": CommandSafeguard(timeout_seconds=42.0, max_lines=99)}
ws = _ws(safeguards=overrides)
mount = next(m for m in ws._registry._mounts if m.prefix == "/data/")
assert mount.command_safeguards["cat"].timeout_seconds == 42.0
assert mount.command_safeguards["cat"].max_lines == 99
@pytest.mark.asyncio
async def test_mount_override_beats_command_default():
overrides = {"cat": CommandSafeguard(timeout_seconds=999.0)}
ws = _ws(safeguards=overrides)
mount = next(m for m in ws._registry._mounts if m.prefix == "/data/")
from mirage.commands.safeguard import resolve_safeguard
resolved = resolve_safeguard("cat", None,
mount.command_safeguards.get("cat"))
assert resolved.timeout_seconds == 999.0
@pytest.mark.asyncio
async def test_timeout_sets_shared_cancel_event(restore_defaults):
sg.DEFAULT_COMMAND_SAFEGUARDS["sleep"] = CommandSafeguard(
timeout_seconds=0.05)
ws = _ws()
cancel = asyncio.Event()
r = await ws.execute("sleep 1", cancel=cancel)
assert r.exit_code == 124
assert cancel.is_set()
@pytest.mark.asyncio
async def test_cross_mount_cat_honors_command_default_safeguard(
restore_defaults):
sg.DEFAULT_COMMAND_SAFEGUARDS["cat"] = CommandSafeguard(max_lines=4)
a = RAMResource()
b = RAMResource()
a._store.dirs.add("/")
b._store.dirs.add("/")
a._store.files["/x.txt"] = b"a\n" * 20
b._store.files["/y.txt"] = b"b\n" * 20
ws = Workspace({"/a/": a, "/b/": b}, mode=MountMode.WRITE)
r = await ws.execute("cat /a/x.txt /b/y.txt")
out = await r.stdout_str()
err = await r.stderr_str()
assert out.count("\n") == 4
assert "output truncated" in err
@pytest.mark.asyncio
async def test_fan_out_find_has_safeguard_set():
a = RAMResource()
a._store.dirs.add("/")
a._store.files["/x.txt"] = b"hi\n"
ws = Workspace({"/a/": a}, mode=MountMode.WRITE)
r = await ws.execute("find /")
assert r.safeguard is not None
assert r.safeguard.timeout_seconds is not None
@pytest.mark.asyncio
async def test_cross_mount_honors_per_mount_timeout_override():
a = RAMResource()
b = RAMResource()
a._store.dirs.add("/")
b._store.dirs.add("/")
a._store.files["/x.txt"] = b"hi\n"
b._store.files["/y.txt"] = b"yo\n"
ws = Workspace(
{
"/a/": (a, MountMode.WRITE, {
"cat": CommandSafeguard(timeout_seconds=3.0)
}),
"/b/":
b,
},
mode=MountMode.WRITE)
r = await ws.execute("cat /a/x.txt /b/y.txt")
assert r.safeguard is not None
assert r.safeguard.timeout_seconds == 3.0
@pytest.mark.asyncio
async def test_fan_out_uses_tightest_timeout_among_mounts():
parent = RAMResource()
child = RAMResource()
parent._store.dirs.add("/")
parent._store.files["/a.txt"] = b"hi\n"
child._store.dirs.add("/")
child._store.files["/b.txt"] = b"yo\n"
ws = Workspace(
{
"/p/":
parent,
"/p/sub/": (child, MountMode.WRITE, {
"find": CommandSafeguard(timeout_seconds=2.0)
}),
},
mode=MountMode.WRITE)
r = await ws.execute("find /p")
assert r.safeguard is not None
assert r.safeguard.timeout_seconds == 2.0
@pytest.mark.asyncio
async def test_fan_out_tightest_when_parent_is_tighter():
parent = RAMResource()
child = RAMResource()
parent._store.dirs.add("/")
parent._store.files["/a.txt"] = b"hi\n"
child._store.dirs.add("/")
child._store.files["/b.txt"] = b"yo\n"
ws = Workspace(
{
"/p/": (parent, MountMode.WRITE, {
"find": CommandSafeguard(timeout_seconds=2.0)
}),
"/p/sub/": (child, MountMode.WRITE, {
"find": CommandSafeguard(timeout_seconds=9.0)
}),
},
mode=MountMode.WRITE)
r = await ws.execute("find /p")
assert r.safeguard is not None
assert r.safeguard.timeout_seconds == 2.0
@pytest.mark.asyncio
async def test_background_job_propagates_timeout(restore_defaults):
sg.DEFAULT_COMMAND_SAFEGUARDS["sleep"] = CommandSafeguard(
timeout_seconds=0.05)
ws = _ws()
r1 = await ws.execute("sleep 2 &")
assert r1.exit_code == 0
r2 = await ws.execute("wait %1")
assert r2.exit_code == 124
err = await r2.stderr_str()
assert "sleep: timed out" in err
@pytest.mark.asyncio
async def test_stderr_redirect_does_not_swallow_timeout_exit(restore_defaults):
sg.DEFAULT_COMMAND_SAFEGUARDS["sleep"] = CommandSafeguard(
timeout_seconds=0.05)
ws = _ws()
r = await ws.execute("sleep 2 2>&1")
assert r.exit_code == 124
r = await ws.execute("sleep 2 2>&1 | cat")
assert r.exit_code == 124
@pytest.mark.asyncio
async def test_job_table_reports_completed_bg_without_wait(restore_defaults):
sg.DEFAULT_COMMAND_SAFEGUARDS["sleep"] = CommandSafeguard(
timeout_seconds=0.05)
ws = _ws()
await ws.execute("sleep 5 &")
await asyncio.sleep(0.2)
jobs = ws.job_table.list_jobs()
assert len(jobs) == 1
assert jobs[0].status.value == "completed"
assert jobs[0].exit_code == 124
assert b"sleep: timed out" in jobs[0].stderr
@pytest.mark.asyncio
async def test_timeout_wrap_preserves_lazy_nonzero_exit_on_no_match():
ws = _ws()
await ws.execute("echo hello > /data/f.txt")
r = await ws.execute("grep zzz /data/f.txt")
assert r.exit_code == 1
@pytest.mark.asyncio
async def test_timeout_wrap_preserves_lazy_zero_exit_on_match():
ws = _ws()
await ws.execute("echo hello > /data/f.txt")
r = await ws.execute("grep hello /data/f.txt")
assert r.exit_code == 0
assert (await r.stdout_str()) == "hello\n"
@pytest.mark.asyncio
async def test_truncation_keeps_lazy_exit_zero_on_match():
ws = _ws({"grep": CommandSafeguard(max_lines=2, timeout_seconds=600)})
await ws.execute("printf 'a\\na\\na\\na\\n' > /data/f.txt")
r = await ws.execute("grep a /data/f.txt")
assert r.exit_code == 0
assert (await r.stdout_str()) == "a\na\n"
@pytest.mark.asyncio
async def test_provision_dry_run_honors_timeout(monkeypatch, restore_defaults):
sg.DEFAULT_COMMAND_SAFEGUARDS["cat"] = CommandSafeguard(
timeout_seconds=0.1)
monkeypatch.setattr("mirage.workspace.workspace.provision_node",
_slow_provision)
ws = _ws()
r = await ws.execute("cat /data/f.txt", provision=True)
assert r.exit_code == 124
@pytest.mark.asyncio
async def test_timeout_preserves_partial_records_and_logs(
caplog, restore_defaults):
sg.DEFAULT_COMMAND_SAFEGUARDS["sleep"] = CommandSafeguard(
timeout_seconds=0.1)
ws = _ws()
await ws.execute("echo hello > /data/f.txt")
before = len(ws._ops.records)
with caplog.at_level(logging.DEBUG, logger="mirage.workspace.workspace"):
r = await ws.execute("cat /data/f.txt; sleep 5")
assert r.exit_code == 124
assert len(ws._ops.records) > before
assert any("timed out" in m for m in caplog.messages)
@pytest.mark.asyncio
async def test_cross_mount_cat_aggregates_tightest_safeguard():
a = RAMResource()
b = RAMResource()
a._store.dirs.add("/")
b._store.dirs.add("/")
a._store.files["/x.txt"] = b"1\n2\n3\n"
b._store.files["/y.txt"] = b"4\n5\n6\n"
ws = Workspace(
{
"/a/": (a, MountMode.WRITE, {
"cat":
CommandSafeguard(max_lines=100, on_exceed=OnExceed.TRUNCATE)
}),
"/b/":
(b, MountMode.WRITE, {
"cat": CommandSafeguard(max_lines=1, on_exceed=OnExceed.ERROR)
}),
},
mode=MountMode.WRITE)
r = await ws.execute("cat /a/x.txt /b/y.txt")
assert r.safeguard.max_lines == 1
assert r.safeguard.on_exceed is OnExceed.ERROR
@@ -0,0 +1,130 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
from mirage.resource.ram import RAMResource
from mirage.types import MountMode
from mirage.workspace import Workspace
def _make_ws():
ram1 = RAMResource()
ram2 = RAMResource()
ram1._store.files["/file.txt"] = b"line1\nline2\nline3\nline4\nline5\n"
ram2._store.files["/file.txt"] = b"aaa\nbbb\nccc\n"
return Workspace(
{
"/a/": (ram1, MountMode.WRITE),
"/b/": (ram2, MountMode.WRITE)
}, )
def _run(ws, cmd):
async def _inner():
io = await ws.execute(cmd)
return await io.stdout_str(), await io.stderr_str(), io.exit_code
return asyncio.run(_inner())
def test_cross_mount_head_invalid_n():
ws = _make_ws()
out, err, code = _run(ws, "head -n abc /a/file.txt /b/file.txt")
assert code == 1
assert "abc" in err
def test_cross_mount_tail_invalid_n():
ws = _make_ws()
out, err, code = _run(ws, "tail -n abc /a/file.txt /b/file.txt")
assert code == 1
assert "abc" in err
def test_cross_mount_head_valid_n():
ws = _make_ws()
out, err, code = _run(ws, "head -n 2 /a/file.txt /b/file.txt")
assert code == 0
assert "line1" in out
assert "line2" in out
assert "aaa" in out
assert "bbb" in out
def test_cross_mount_tail_valid_n():
ws = _make_ws()
out, err, code = _run(ws, "tail -n 1 /a/file.txt /b/file.txt")
assert code == 0
assert "line5" in out
assert "ccc" in out
def test_cross_mount_head_default_n():
ws = _make_ws()
out, err, code = _run(ws, "head /a/file.txt /b/file.txt")
assert code == 0
assert "line1" in out
assert "aaa" in out
def test_cross_mount_head_byte_mode():
ws = _make_ws()
out, err, code = _run(ws, "head -c 3 /a/file.txt /b/file.txt")
assert code == 0
assert "/a/file.txt" in out
assert "lin" in out
assert "line1" not in out
assert "bbb" not in out
def test_cross_mount_grep_invert():
ws = _make_ws()
out, err, code = _run(ws, "grep -v line1 /a/file.txt /b/file.txt")
assert code == 0
assert "/a/file.txt:line2" in out
assert "aaa" in out
assert "line1" not in out
def test_cross_mount_wc_total():
ws = _make_ws()
out, err, code = _run(ws, "wc -l /a/file.txt /b/file.txt")
assert code == 0
assert "total" in out
assert "5" in out
assert "3" in out
assert "8" in out
def test_cross_mount_cat_missing_has_strerror():
ws = _make_ws()
out, err, code = _run(ws, "cat /a/file.txt /b/missing.txt")
assert code == 1
assert err == "cat: /b/missing.txt: No such file or directory\n"
def test_cross_mount_diff_missing_has_strerror():
ws = _make_ws()
out, err, code = _run(ws, "diff /a/missing.txt /b/file.txt")
assert code == 1
assert err == "diff: /a/missing.txt: No such file or directory\n"
def test_cross_mount_cmp_missing_has_strerror():
ws = _make_ws()
out, err, code = _run(ws, "cmp /a/file.txt /b/missing.txt")
assert code == 1
assert err == "cmp: /b/missing.txt: No such file or directory\n"
@@ -0,0 +1,93 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
from mirage.resource.ram import RAMResource
from mirage.types import MountMode
from mirage.workspace import Workspace
def _make_ws():
src = RAMResource()
dst = RAMResource()
src._store.dirs.update({"/dir", "/dir/sub", "/dir/empty"})
src._store.files["/dir/a.txt"] = b"aaa\n"
src._store.files["/dir/sub/b.txt"] = b"bbb\n"
ws = Workspace(
{
"/a": (src, MountMode.WRITE),
"/b": (dst, MountMode.WRITE),
}, )
return ws, src, dst
def _run(ws, cmd):
async def _inner():
io = await ws.execute(cmd)
return await io.stdout_str(), await io.stderr_str(), io.exit_code
return asyncio.run(_inner())
def test_cp_recursive_copies_tree():
ws, _src, dst = _make_ws()
_out, _err, code = _run(ws, "cp -r /a/dir /b/copied")
assert code == 0
assert dst._store.files["/copied/a.txt"] == b"aaa\n"
assert dst._store.files["/copied/sub/b.txt"] == b"bbb\n"
assert "/copied/sub" in dst._store.dirs
assert "/copied/empty" in dst._store.dirs
def test_cp_recursive_into_existing_dir():
ws, _src, dst = _make_ws()
dst._store.dirs.add("/into")
_out, _err, code = _run(ws, "cp -r /a/dir /b/into")
assert code == 0
assert dst._store.files["/into/dir/a.txt"] == b"aaa\n"
assert dst._store.files["/into/dir/sub/b.txt"] == b"bbb\n"
def test_cp_directory_without_recursive_fails():
ws, _src, dst = _make_ws()
_out, err, code = _run(ws, "cp /a/dir /b/copied")
assert code == 1
assert "omitting directory" in err
assert "/copied" not in dst._store.dirs
assert not any(k.startswith("/copied") for k in dst._store.files)
def test_mv_recursive_moves_tree_and_removes_source():
ws, src, dst = _make_ws()
_out, _err, code = _run(ws, "mv /a/dir /b/moved")
assert code == 0
assert dst._store.files["/moved/a.txt"] == b"aaa\n"
assert dst._store.files["/moved/sub/b.txt"] == b"bbb\n"
assert "/dir" not in src._store.dirs
assert "/dir/a.txt" not in src._store.files
assert "/dir/sub/b.txt" not in src._store.files
def test_cp_recursive_no_clobber_skips_existing():
# /b/into exists, so `cp -r /a/dir /b/into` maps to /b/into/dir. Pre-seed
# that mapped tree with an existing a.txt so -n must skip it.
ws, _src, dst = _make_ws()
dst._store.dirs.update({"/into", "/into/dir"})
dst._store.files["/into/dir/a.txt"] = b"keep\n"
_out, _err, code = _run(ws, "cp -rn /a/dir /b/into")
assert code == 0
assert dst._store.files["/into/dir/a.txt"] == b"keep\n"
assert dst._store.files["/into/dir/sub/b.txt"] == b"bbb\n"
@@ -0,0 +1,345 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage import MountMode, Workspace
from mirage.resource.ram import RAMResource
def _make_ws():
resource = RAMResource()
store = resource._store
store.dirs.add("/")
store.dirs.add("/subdir")
store.dirs.add("/subdir/nested")
store.files["/subdir/file.txt"] = b"hello"
store.modified["/subdir/file.txt"] = "2024-01-01"
store.files["/subdir/nested/deep.txt"] = b"deep"
store.modified["/subdir/nested/deep.txt"] = "2024-01-01"
return Workspace({"/ram/": resource}, mode=MountMode.WRITE)
def _make_ws_special_chars():
resource = RAMResource()
store = resource._store
store.dirs.add("/")
store.dirs.add("/Zecheng's Server")
store.files["/Zecheng's Server/image.png"] = b"PNG"
store.modified["/Zecheng's Server/image.png"] = "2024-01-01"
return Workspace({"/ram/": resource}, mode=MountMode.WRITE)
@pytest.mark.asyncio
async def test_pwd_default():
ws = _make_ws()
r = await ws.execute("pwd")
assert (await r.stdout_str()).strip() != ""
@pytest.mark.asyncio
async def test_cd_and_pwd():
ws = _make_ws()
r = await ws.execute("cd /ram && pwd")
assert (await r.stdout_str()).strip() == "/ram"
@pytest.mark.asyncio
async def test_cd_subdir_and_pwd():
ws = _make_ws()
r = await ws.execute("cd /ram/subdir && pwd")
assert (await r.stdout_str()).strip() == "/ram/subdir"
@pytest.mark.asyncio
async def test_cd_dotdot_and_pwd():
ws = _make_ws()
r = await ws.execute("cd /ram/subdir && cd .. && pwd")
assert (await r.stdout_str()).strip() == "/ram"
@pytest.mark.asyncio
async def test_cd_slash_and_pwd():
ws = _make_ws()
r = await ws.execute("cd /ram/subdir && cd / && pwd")
assert (await r.stdout_str()).strip() == "/"
@pytest.mark.asyncio
async def test_cd_tilde_unset_home_errors():
ws = _make_ws()
r = await ws.execute("cd /ram/subdir && cd ~")
assert r.exit_code != 0
@pytest.mark.asyncio
async def test_cd_no_args_unset_home_errors():
ws = _make_ws()
r = await ws.execute("cd /ram/subdir && cd")
assert r.exit_code == 1
assert "HOME not set" in await r.stderr_str()
@pytest.mark.asyncio
async def test_cd_no_args_with_home():
ws = _make_ws()
r = await ws.execute("export HOME=/ram/subdir && cd /ram && cd && pwd")
assert (await r.stdout_str()).strip() == "/ram/subdir"
@pytest.mark.asyncio
async def test_cd_relative_and_pwd():
ws = _make_ws()
r = await ws.execute("cd /ram && cd subdir && pwd")
assert (await r.stdout_str()).strip() == "/ram/subdir"
@pytest.mark.asyncio
async def test_ls_no_args_uses_cwd():
ws = _make_ws()
r = await ws.execute("cd /ram/subdir && ls")
assert "file.txt" in await r.stdout_str()
@pytest.mark.asyncio
async def test_ls_no_args_root():
ws = _make_ws()
r = await ws.execute("cd /ram && ls")
assert "subdir" in await r.stdout_str()
@pytest.mark.asyncio
async def test_cd_relative_nested():
ws = _make_ws()
r = await ws.execute("cd /ram/subdir && cd nested && pwd")
assert (await r.stdout_str()).strip() == "/ram/subdir/nested"
@pytest.mark.asyncio
async def test_cd_dotdot_twice():
ws = _make_ws()
r = await ws.execute('cd /ram/subdir/nested && cd ../.. && pwd')
assert (await r.stdout_str()).strip() == "/ram"
@pytest.mark.asyncio
async def test_ls_backslash_escaped():
ws = _make_ws_special_chars()
r = await ws.execute(r"ls /ram/Zecheng\'s\ Server/")
assert "image.png" in await r.stdout_str()
@pytest.mark.asyncio
async def test_ls_quoted():
ws = _make_ws_special_chars()
r = await ws.execute('ls "/ram/Zecheng\'s Server/"')
assert "image.png" in await r.stdout_str()
@pytest.mark.asyncio
async def test_cd_backslash_escaped_and_ls():
ws = _make_ws_special_chars()
r = await ws.execute(r"cd /ram/Zecheng\'s\ Server && ls")
assert "image.png" in await r.stdout_str()
@pytest.mark.asyncio
async def test_cd_quoted_and_ls():
ws = _make_ws_special_chars()
r = await ws.execute('cd "/ram/Zecheng\'s Server" && ls')
assert "image.png" in await r.stdout_str()
@pytest.mark.asyncio
async def test_cd_backslash_escaped_and_pwd():
ws = _make_ws_special_chars()
r = await ws.execute(r"cd /ram/Zecheng\'s\ Server && pwd")
assert (await r.stdout_str()).strip() == "/ram/Zecheng's Server"
@pytest.mark.asyncio
async def test_pwd_default_root():
ws = _make_ws()
r = await ws.execute("pwd")
assert (await r.stdout_str()).strip() == "/"
@pytest.mark.asyncio
async def test_echo_pwd_tracks_cwd():
ws = _make_ws()
r = await ws.execute("cd /ram/subdir && echo $PWD")
assert (await r.stdout_str()).strip() == "/ram/subdir"
@pytest.mark.asyncio
async def test_echo_home_unset_is_empty():
ws = _make_ws()
r = await ws.execute('echo "[$HOME]"')
assert (await r.stdout_str()).strip() == "[]"
@pytest.mark.asyncio
async def test_cd_updates_oldpwd():
ws = _make_ws()
r = await ws.execute("cd /ram && cd /ram/subdir && echo $OLDPWD")
assert (await r.stdout_str()).strip() == "/ram"
@pytest.mark.asyncio
async def test_cd_dash_returns_and_prints():
ws = _make_ws()
r = await ws.execute("cd /ram && cd /ram/subdir && cd -")
out = await r.stdout_str()
assert out.strip() == "/ram"
@pytest.mark.asyncio
async def test_cd_dash_swaps_cwd():
ws = _make_ws()
r = await ws.execute("cd /ram && cd /ram/subdir && cd - > /dev/null && pwd"
)
assert (await r.stdout_str()).strip() == "/ram"
@pytest.mark.asyncio
async def test_cd_dash_without_oldpwd_errors():
ws = _make_ws()
r = await ws.execute("cd -")
assert r.exit_code == 1
assert "OLDPWD not set" in await r.stderr_str()
@pytest.mark.asyncio
async def test_custom_home_cd_tilde():
ws = _make_ws()
r = await ws.execute("export HOME=/ram/subdir && cd ~ && pwd")
assert (await r.stdout_str()).strip() == "/ram/subdir"
@pytest.mark.asyncio
async def test_custom_home_echo():
ws = _make_ws()
r = await ws.execute("export HOME=/ram/subdir && echo $HOME")
assert (await r.stdout_str()).strip() == "/ram/subdir"
@pytest.mark.asyncio
async def test_tilde_expands_for_commands():
ws = _make_ws()
r = await ws.execute("export HOME=/ram/subdir && cat ~/file.txt")
assert (await r.stdout_str()) == "hello"
@pytest.mark.asyncio
async def test_quoted_tilde_not_expanded():
ws = _make_ws()
r = await ws.execute('export HOME=/ram/subdir && cat "~/file.txt"')
assert r.exit_code != 0
@pytest.mark.asyncio
async def test_subshell_does_not_leak_oldpwd():
ws = _make_ws()
r = await ws.execute("cd /ram && (cd /ram/subdir) && echo $OLDPWD")
assert (await r.stdout_str()).strip() == "/"
@pytest.mark.asyncio
async def test_subshell_does_not_leak_cwd():
ws = _make_ws()
r = await ws.execute("cd /ram && (cd /ram/subdir) && pwd")
assert (await r.stdout_str()).strip() == "/ram"
@pytest.mark.asyncio
async def test_cd_double_slash_collapses():
ws = _make_ws()
r = await ws.execute("cd //ram && pwd")
assert (await r.stdout_str()).strip() == "/ram"
@pytest.mark.asyncio
async def test_cd_triple_slash_collapses():
ws = _make_ws()
r = await ws.execute("cd ///ram/subdir && pwd")
assert (await r.stdout_str()).strip() == "/ram/subdir"
@pytest.mark.asyncio
async def test_cd_physical_flag():
ws = _make_ws()
r = await ws.execute("cd -P /ram/subdir && pwd")
assert (await r.stdout_str()).strip() == "/ram/subdir"
@pytest.mark.asyncio
async def test_cd_logical_flag():
ws = _make_ws()
r = await ws.execute("cd -L /ram && pwd")
assert (await r.stdout_str()).strip() == "/ram"
@pytest.mark.asyncio
async def test_cd_clustered_flags():
ws = _make_ws()
r = await ws.execute("cd -LP /ram && pwd")
assert (await r.stdout_str()).strip() == "/ram"
@pytest.mark.asyncio
async def test_cd_double_dash_terminates_options():
ws = _make_ws()
r = await ws.execute("cd -- /ram && pwd")
assert (await r.stdout_str()).strip() == "/ram"
@pytest.mark.asyncio
async def test_cd_invalid_option_exit2():
ws = _make_ws()
r = await ws.execute("cd -x /ram")
assert r.exit_code == 2
assert "invalid option" in await r.stderr_str()
@pytest.mark.asyncio
async def test_cd_too_many_arguments():
ws = _make_ws()
r = await ws.execute("cd /ram /ram/subdir")
assert r.exit_code == 1
assert "too many arguments" in await r.stderr_str()
@pytest.mark.asyncio
async def test_cd_quoted_tilde_is_literal():
ws = _make_ws()
r = await ws.execute("cd /ram && cd '~'")
assert r.exit_code != 0
@pytest.mark.asyncio
async def test_cd_cdpath_search_and_print():
ws = _make_ws()
r = await ws.execute("export CDPATH=/ram && cd subdir && pwd")
lines = (await r.stdout_str()).splitlines()
assert lines[-1] == "/ram/subdir"
assert "/ram/subdir" in lines[:-1]
@pytest.mark.asyncio
async def test_cd_cdpath_empty_entry_is_cwd_no_print():
ws = _make_ws()
r = await ws.execute("cd /ram && export CDPATH=:/ram && cd subdir && pwd")
lines = (await r.stdout_str()).splitlines()
assert lines[-1] == "/ram/subdir"
assert lines == ["/ram/subdir"]
@@ -0,0 +1,39 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage.resource.ram import RAMResource
from mirage.workspace import Workspace
@pytest.mark.asyncio
async def test_cache_hit_does_not_double_store():
ram = RAMResource()
await ram.write("/big.bin", b"x" * 4096)
ws = Workspace(resources={"/r": ram})
try:
await ws.execute("cat /r/big.bin > /dev/null")
size_after_first = ws._cache.cache_size
keys_first = sorted(ws._cache._entries.keys())
await ws.execute("cat /r/big.bin > /dev/null")
size_after_second = ws._cache.cache_size
keys_second = sorted(ws._cache._entries.keys())
assert size_after_second == size_after_first
assert keys_second == keys_first
assert all(k.startswith("/r/") for k in keys_second)
finally:
await ws.close()
@@ -0,0 +1,66 @@
import pytest
from mirage import MountMode, Workspace
from mirage.core.dify import tree
from mirage.resource.dify import DifyConfig, DifyResource
async def list_documents(config):
return [{
"id": "doc-1",
"name": "Quickstart",
"doc_metadata": [{
"name": "slug",
"value": "guides/quickstart"
}],
"enabled": True,
"indexing_status": "completed",
"archived": False,
"tokens": 5,
"data_source_type": "upload_file",
"data_source_detail_dict": {
"upload_file": {
"size": 321
}
},
"created_at": 1716282000,
}]
def resource() -> DifyResource:
return DifyResource(
DifyConfig(
api_key="secret",
base_url="https://dify.example/v1",
dataset_id="dataset-1",
))
def test_workspace_mount_registers_dify_commands_and_ops():
workspace = Workspace({"/knowledge": resource()}, mode=MountMode.READ)
mount = workspace.mount("/knowledge/")
assert "awk" in mount.commands()
assert "ls" in mount.commands()
assert "cat" in mount.commands()
assert "cut" in mount.commands()
assert "rg" in mount.commands()
assert "sed" in mount.commands()
assert "sort" in mount.commands()
assert "stat" in mount.commands()
assert "tree" in mount.commands()
assert "uniq" in mount.commands()
assert "du" in mount.commands()
assert "read" in mount.registered_ops()
assert "stat" in mount.registered_ops()
@pytest.mark.asyncio
async def test_workspace_executes_dify_ls(monkeypatch):
monkeypatch.setattr(tree, "list_all_documents", list_documents)
workspace = Workspace({"/knowledge": resource()}, mode=MountMode.READ)
result = await workspace.execute("ls /knowledge")
assert result.exit_code == 0
assert await result.stdout_str() == "guides\n"
@@ -0,0 +1,55 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
from mirage.resource.ram import RAMResource
from mirage.types import MountMode
from mirage.workspace import Workspace
def _make_ws() -> tuple[Workspace, RAMResource]:
resource = RAMResource()
resource._store.files["/file.txt"] = b"OLD"
ws = Workspace(
{"/data": (resource, MountMode.WRITE)},
mode=MountMode.WRITE,
)
return ws, resource
def test_redirect_write_overrides_cached_read():
ws, resource = _make_ws()
async def run() -> None:
await ws.execute("cat /data/file.txt")
await ws.execute('echo -n "NEW" > /data/file.txt')
asyncio.run(run())
assert resource._store.files["/file.txt"] == b"NEW", (
"redirect-write should reach the backend even when the path was "
"previously cached by a read")
def test_redirect_append_after_cached_read():
ws, resource = _make_ws()
async def run() -> None:
await ws.execute("cat /data/file.txt")
await ws.execute('echo -n "MORE" >> /data/file.txt')
asyncio.run(run())
assert resource._store.files["/file.txt"] == b"OLDMORE", (
"redirect-append should reach the backend even when the path was "
"previously cached by a read")
@@ -0,0 +1,296 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
import pytest
from mirage import MountMode, Workspace
from mirage.resource.ram import RAMResource
from mirage.types import DEFAULT_SESSION_ID
def _make_ws():
resource = RAMResource()
store = resource._store
store.dirs.add("/")
store.dirs.add("/subdir")
store.dirs.add("/other")
store.files["/subdir/file.txt"] = b"hello"
store.modified["/subdir/file.txt"] = "2024-01-01"
return Workspace({"/ram/": resource}, mode=MountMode.WRITE)
# ── cwd tests ────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_cwd_runs_in_override():
ws = _make_ws()
r = await ws.execute("pwd", cwd="/ram/subdir")
assert (await r.stdout_str()).strip() == "/ram/subdir"
@pytest.mark.asyncio
async def test_cwd_does_not_mutate_session():
ws = _make_ws()
before = ws.get_session(DEFAULT_SESSION_ID).cwd
await ws.execute("pwd", cwd="/ram/subdir")
after = ws.get_session(DEFAULT_SESSION_ID).cwd
assert before == after
@pytest.mark.asyncio
async def test_cwd_cd_does_not_leak():
ws = _make_ws()
before = ws.get_session(DEFAULT_SESSION_ID).cwd
await ws.execute("cd /ram/subdir", cwd="/ram")
after = ws.get_session(DEFAULT_SESSION_ID).cwd
assert before == after
@pytest.mark.asyncio
async def test_cwd_parallel_isolation():
ws = _make_ws()
r1, r2 = await asyncio.gather(
ws.execute("pwd", cwd="/ram/subdir"),
ws.execute("pwd", cwd="/ram/other"),
)
assert (await r1.stdout_str()).strip() == "/ram/subdir"
assert (await r2.stdout_str()).strip() == "/ram/other"
@pytest.mark.asyncio
async def test_setup_persists_overrides_inherit():
ws = _make_ws()
await ws.execute("export DEBUG=1")
assert ws.get_session(DEFAULT_SESSION_ID).env.get("DEBUG") == "1"
before_cwd = ws.get_session(DEFAULT_SESSION_ID).cwd
r = await ws.execute("printenv DEBUG", cwd="/ram/subdir")
assert (await r.stdout_str()).strip() == "1"
assert ws.get_session(DEFAULT_SESSION_ID).cwd == before_cwd
assert ws.get_session(DEFAULT_SESSION_ID).env.get("DEBUG") == "1"
@pytest.mark.asyncio
async def test_function_definitions_do_not_leak():
ws = _make_ws()
await ws.execute("greet() { echo hi; }", cwd="/ram/subdir")
session = ws.get_session(DEFAULT_SESSION_ID)
assert "greet" not in session.functions
# ── env tests ────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_env_exposes_override():
ws = _make_ws()
r = await ws.execute("printenv FOO", env={"FOO": "bar"})
assert r.exit_code == 0
assert (await r.stdout_str()).strip() == "bar"
@pytest.mark.asyncio
async def test_env_does_not_mutate_session():
ws = _make_ws()
before = dict(ws.get_session(DEFAULT_SESSION_ID).env)
await ws.execute("printenv FOO", env={"FOO": "bar"})
after = dict(ws.get_session(DEFAULT_SESSION_ID).env)
assert before == after
@pytest.mark.asyncio
async def test_env_export_does_not_leak():
ws = _make_ws()
await ws.execute("export LEAKED=yes", env={"FOO": "bar"})
assert "LEAKED" not in ws.get_session(DEFAULT_SESSION_ID).env
@pytest.mark.asyncio
async def test_env_layers_onto_session():
ws = _make_ws()
await ws.execute("export BASE=keep")
assert ws.get_session(DEFAULT_SESSION_ID).env.get("BASE") == "keep"
r_base = await ws.execute("printenv BASE", env={"FOO": "bar"})
assert (await r_base.stdout_str()).strip() == "keep"
r_foo = await ws.execute("printenv FOO", env={"FOO": "bar"})
assert (await r_foo.stdout_str()).strip() == "bar"
session_env = ws.get_session(DEFAULT_SESSION_ID).env
assert session_env.get("BASE") == "keep"
assert "FOO" not in session_env
@pytest.mark.asyncio
async def test_env_parallel_isolation():
ws = _make_ws()
r1, r2 = await asyncio.gather(
ws.execute("printenv FOO", env={"FOO": "one"}),
ws.execute("printenv FOO", env={"FOO": "two"}),
)
assert (await r1.stdout_str()).strip() == "one"
assert (await r2.stdout_str()).strip() == "two"
# ── mid-flight cancel ─────────────────────────────────────────────
@pytest.mark.asyncio
async def test_cancel_pre_set_raises_immediately():
ws = _make_ws()
cancel = asyncio.Event()
cancel.set()
with pytest.raises(Exception) as exc_info:
await ws.execute("echo hi", cancel=cancel)
assert "abort" in str(exc_info.value).lower()
@pytest.mark.asyncio
async def test_cancel_aborts_sleep_within_timeout():
ws = _make_ws()
cancel = asyncio.Event()
async def trigger() -> None:
await asyncio.sleep(0.1)
cancel.set()
asyncio.create_task(trigger())
t0 = asyncio.get_event_loop().time()
with pytest.raises(Exception):
await ws.execute("sleep 5", cancel=cancel)
assert asyncio.get_event_loop().time() - t0 < 1.0
@pytest.mark.asyncio
async def test_cancel_inside_for_loop():
ws = _make_ws()
cancel = asyncio.Event()
async def trigger() -> None:
await asyncio.sleep(0.1)
cancel.set()
asyncio.create_task(trigger())
t0 = asyncio.get_event_loop().time()
with pytest.raises(Exception):
await ws.execute(
"for i in 1 2 3 4 5 6 7 8 9 10; do sleep 1; done",
cancel=cancel,
)
assert asyncio.get_event_loop().time() - t0 < 1.5
@pytest.mark.asyncio
async def test_cancel_between_list_stages():
ws = _make_ws()
cancel = asyncio.Event()
async def trigger() -> None:
await asyncio.sleep(0.1)
cancel.set()
asyncio.create_task(trigger())
t0 = asyncio.get_event_loop().time()
with pytest.raises(Exception):
await ws.execute(
"sleep 1 && sleep 1 && sleep 1 && echo done",
cancel=cancel,
)
assert asyncio.get_event_loop().time() - t0 < 2.0
@pytest.mark.asyncio
async def test_cancel_inside_command_substitution():
ws = _make_ws()
cancel = asyncio.Event()
async def trigger() -> None:
await asyncio.sleep(0.1)
cancel.set()
asyncio.create_task(trigger())
t0 = asyncio.get_event_loop().time()
with pytest.raises(Exception):
await ws.execute('echo "$(sleep 5)"', cancel=cancel)
assert asyncio.get_event_loop().time() - t0 < 1.0
@pytest.mark.asyncio
async def test_cancel_workspace_remains_usable():
ws = _make_ws()
cancel = asyncio.Event()
async def trigger() -> None:
await asyncio.sleep(0.05)
cancel.set()
asyncio.create_task(trigger())
with pytest.raises(Exception):
await ws.execute("sleep 5", cancel=cancel)
r = await ws.execute("echo recovered")
assert r.exit_code == 0
assert r.stdout.decode().strip() == "recovered"
# ── agent harness pattern ─────────────────────────────────────────
async def _tool_call(ws, cmd, cwd_v, env_v, timeout):
cancel = asyncio.Event()
asyncio.get_event_loop().call_later(timeout, cancel.set)
return await ws.execute(cmd, cwd=cwd_v, env=env_v, cancel=cancel)
@pytest.mark.asyncio
async def test_agent_pattern_parallel_tool_calls_each_with_own_options():
ws = _make_ws()
a, b = await asyncio.gather(
_tool_call(ws, "pwd; printenv DEBUG", "/ram/subdir", {"DEBUG": "one"},
5.0),
_tool_call(ws, "pwd; printenv DEBUG", "/ram", {"DEBUG": "two"}, 5.0),
)
assert "/ram/subdir" in a.stdout.decode()
assert "one" in a.stdout.decode()
assert "/ram" in b.stdout.decode()
assert "two" in b.stdout.decode()
session = ws.get_session(DEFAULT_SESSION_ID)
assert session.cwd != "/ram/subdir"
assert "DEBUG" not in session.env
@pytest.mark.asyncio
async def test_execute_uses_custom_default_session_id():
resource = RAMResource()
resource._store.dirs.add("/")
ws = Workspace({"/ram/": resource},
mode=MountMode.WRITE,
session_id="mysess")
r = await ws.execute("echo hi > /ram/f.txt")
assert r.exit_code == 0
r2 = await ws.execute("cat /ram/f.txt")
assert (await r2.stdout_str()).strip() == "hi"
@pytest.mark.asyncio
async def test_agent_pattern_one_aborts_while_siblings_complete():
ws = _make_ws()
settled = await asyncio.gather(
_tool_call(ws, "sleep 5", "/ram/subdir", {"DEBUG": "one"}, 0.1),
_tool_call(ws, "echo ok", "/ram", {"DEBUG": "two"}, 5.0),
return_exceptions=True,
)
assert isinstance(settled[0], Exception)
assert "abort" in str(settled[0]).lower()
assert not isinstance(settled[1], Exception)
assert settled[1].stdout.decode().strip() == "ok"
@@ -0,0 +1,84 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from mirage.workspace.types import ExecutionNode
def test_execution_node_leaf():
node = ExecutionNode(command="cat /data/file.txt", stderr=b"", exit_code=0)
assert node.command == "cat /data/file.txt"
assert node.op is None
assert node.children == []
assert node.exit_code == 0
def test_execution_node_pipe():
node = ExecutionNode(
op="|",
stderr=b"",
exit_code=0,
children=[
ExecutionNode(command="grep foo file",
stderr=b"warning",
exit_code=0),
ExecutionNode(command="sort", stderr=b"", exit_code=0),
],
)
assert node.op == "|"
assert node.command is None
assert len(node.children) == 2
assert node.children[0].stderr == b"warning"
def test_execution_node_nested_tree():
tree = ExecutionNode(
op=";",
stderr=b"",
exit_code=0,
children=[
ExecutionNode(
op="|",
stderr=b"",
exit_code=0,
children=[
ExecutionNode(command="grep foo file",
stderr=b"",
exit_code=1),
ExecutionNode(command="sort", stderr=b"", exit_code=0),
],
),
ExecutionNode(command="echo done", stderr=b"", exit_code=0),
],
)
assert tree.children[0].children[0].exit_code == 1
assert tree.children[1].command == "echo done"
def test_execution_node_to_dict():
node = ExecutionNode(
op="|",
stderr=b"warn",
exit_code=0,
children=[
ExecutionNode(command="grep foo", stderr=b"", exit_code=1),
ExecutionNode(command="sort", stderr=b"", exit_code=0),
],
)
d = node.to_dict()
assert d["op"] == "|"
assert d["exit_code"] == 0
assert len(d["children"]) == 2
assert d["children"][0]["command"] == "grep foo"
assert d["children"][0]["stderr"] == ""
assert d["children"][0]["exit_code"] == 1
+544
View File
@@ -0,0 +1,544 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
from unittest.mock import AsyncMock, MagicMock
from mirage.io import IOResult
from mirage.shell import parse
from mirage.shell.call_stack import CallStack
from mirage.shell.helpers import get_parts
from mirage.types import PathSpec
from mirage.workspace.expand import (classify_parts, classify_word,
expand_and_classify, expand_node,
expand_parts)
from mirage.workspace.session import Session
def _session(env=None, cwd="/"):
return Session(session_id="test", cwd=cwd, env=env or {})
def _execute_fn():
return AsyncMock(return_value=IOResult())
def _run(coro):
return asyncio.run(coro)
def _cmd_parts(cmd: str):
"""Parse a command and return its parts nodes."""
root = parse(cmd)
return get_parts(root.named_children[0])
def _first_arg(cmd: str):
"""Parse and return the second child (first argument)."""
return _cmd_parts(cmd)[1]
def _mock_registry(prefixes=None):
"""Create a mock registry that matches given prefixes.
Args:
prefixes: list of mount prefixes, e.g. ["/data/", "/s3/"].
If None, matches everything.
"""
reg = MagicMock()
def _mount_for(path):
if prefixes is None:
root = MagicMock()
root.prefix = "/"
return root
norm = "/" + path.strip("/")
for p in sorted(prefixes, key=len, reverse=True):
norm_p = p.rstrip("/")
if norm == norm_p or norm.startswith(p):
m = MagicMock()
m.prefix = p
return m
raise ValueError(f"no mount matches: {path}")
reg.mount_for = MagicMock(side_effect=_mount_for)
return reg
# ── word ────────────────────────────────────────
def test_expand_word():
node = _cmd_parts("echo hello")[1]
result = _run(expand_node(node, _session(), _execute_fn()))
assert result == "hello"
def test_expand_command_name():
node = _cmd_parts("echo hello")[0]
result = _run(expand_node(node, _session(), _execute_fn()))
assert result == "echo"
# ── simple expansion $VAR ───────────────────────
def test_expand_simple_var():
node = _first_arg("echo $FOO")
session = _session(env={"FOO": "bar"})
result = _run(expand_node(node, session, _execute_fn()))
assert result == "bar"
def test_expand_simple_var_missing():
node = _first_arg("echo $MISSING")
result = _run(expand_node(node, _session(), _execute_fn()))
assert result == ""
def test_expand_special_question():
node = _first_arg("echo $?")
session = _session()
session.last_exit_code = 42
result = _run(expand_node(node, session, _execute_fn()))
assert result == "42"
def test_expand_special_at():
node = _first_arg("echo $@")
cs = CallStack()
cs.push(["a", "b", "c"])
result = _run(expand_node(node, _session(), _execute_fn(), call_stack=cs))
assert result == "a b c"
def test_expand_positional():
node = _first_arg("echo $1")
cs = CallStack()
cs.push(["first", "second"])
result = _run(expand_node(node, _session(), _execute_fn(), call_stack=cs))
assert result == "first"
def test_expand_dollar_zero():
node = _first_arg("echo $0")
result = _run(expand_node(node, _session(), _execute_fn()))
assert result == "mirage"
# ── brace expansion ${VAR} ─────────────────────
def test_expand_braces():
node = _first_arg("echo ${FOO}")
session = _session(env={"FOO": "hello"})
result = _run(expand_node(node, session, _execute_fn()))
assert result == "hello"
def test_expand_braces_default():
node = _first_arg("echo ${FOO:-default_val}")
result = _run(expand_node(node, _session(), _execute_fn()))
assert result == "default_val"
def test_expand_braces_default_not_used():
node = _first_arg("echo ${FOO:-default_val}")
session = _session(env={"FOO": "exists"})
result = _run(expand_node(node, session, _execute_fn()))
assert result == "exists"
# ── command substitution $(cmd) ─────────────────
def test_expand_command_sub():
node = _first_arg("echo $(whoami)")
execute_fn = AsyncMock()
io = IOResult()
io.stdout = b"testuser\n"
execute_fn.return_value = io
result = _run(expand_node(node, _session(), execute_fn))
assert result == "testuser"
execute_fn.assert_called_once()
# ── arithmetic expansion $((expr)) ──────────────
def test_expand_arithmetic():
node = _first_arg("echo $((1 + 2))")
result = _run(expand_node(node, _session(), _execute_fn()))
assert result == "3"
def test_expand_arithmetic_multiply():
node = _first_arg("echo $((3 * 4))")
result = _run(expand_node(node, _session(), _execute_fn()))
assert result == "12"
# ── concatenation $VAR/file.txt ─────────────────
def test_expand_concatenation():
node = _first_arg("echo $DIR/file.txt")
session = _session(env={"DIR": "/data"})
result = _run(expand_node(node, session, _execute_fn()))
assert result == "/data/file.txt"
# ── string "hello $VAR" ────────────────────────
def test_expand_double_quoted():
node = _first_arg('echo "hello $NAME"')
session = _session(env={"NAME": "world"})
result = _run(expand_node(node, session, _execute_fn()))
assert result == "hello world"
def test_expand_double_quoted_no_var():
node = _first_arg('echo "plain text"')
result = _run(expand_node(node, _session(), _execute_fn()))
assert result == "plain text"
# ── raw string 'no expansion' ──────────────────
def test_expand_raw_string():
node = _first_arg("echo 'no $expansion'")
session = _session(env={"expansion": "SHOULD_NOT_SEE"})
result = _run(expand_node(node, session, _execute_fn()))
assert result == "no $expansion"
# ── expand_parts ───────────────────────────────
def test_expand_parts_basic():
parts = _cmd_parts("echo hello world")
session = _session()
result = _run(expand_parts(parts, session, _execute_fn()))
assert result == ["echo", "hello", "world"]
def test_expand_parts_with_var():
parts = _cmd_parts("echo $A $B")
session = _session(env={"A": "foo", "B": "bar"})
result = _run(expand_parts(parts, session, _execute_fn()))
assert result == ["echo", "foo", "bar"]
def test_expand_parts_cmd_sub_splits():
parts = _cmd_parts("echo $(ls)")
execute_fn = AsyncMock()
io = IOResult()
io.stdout = b"file1\nfile2\nfile3\n"
execute_fn.return_value = io
session = _session()
result = _run(expand_parts(parts, session, execute_fn))
assert "file1" in result
assert "file2" in result
assert "file3" in result
def test_expand_parts_empty_var_skipped():
parts = _cmd_parts("echo $EMPTY")
session = _session()
result = _run(expand_parts(parts, session, _execute_fn()))
assert result == ["echo"]
def test_expand_parts_splits_unquoted_var():
parts = _cmd_parts("echo $VAR")
session = _session(env={"VAR": "a b c"})
result = _run(expand_parts(parts, session, _execute_fn()))
assert result == ["echo", "a", "b", "c"]
def test_expand_parts_no_split_quoted_var():
parts = _cmd_parts('echo "$VAR"')
session = _session(env={"VAR": "a b c"})
result = _run(expand_parts(parts, session, _execute_fn()))
assert result == ["echo", "a b c"]
def test_expand_parts_splits_dollar_at():
parts = _cmd_parts("echo $@")
cs = CallStack()
cs.push(["a", "b", "c"])
session = _session()
result = _run(expand_parts(parts, session, _execute_fn(), call_stack=cs))
assert result == ["echo", "a", "b", "c"]
def test_expand_parts_no_split_empty_var():
parts = _cmd_parts("echo $EMPTY")
session = _session(env={"EMPTY": ""})
result = _run(expand_parts(parts, session, _execute_fn()))
assert result == ["echo"]
def test_expand_parts_splits_expansion_braces():
parts = _cmd_parts("echo ${VAR}")
session = _session(env={"VAR": "x y z"})
result = _run(expand_parts(parts, session, _execute_fn()))
assert result == ["echo", "x", "y", "z"]
def test_expand_parts_keeps_empty_raw_string():
"""A quoted empty literal '' is a real (empty) argument, not dropped."""
parts = _cmd_parts("echo a '' b")
result = _run(expand_parts(parts, _session(), _execute_fn()))
assert result == ["echo", "a", "", "b"]
def test_expand_parts_keeps_empty_double_quote():
"""A quoted empty literal "" is a real (empty) argument, not dropped."""
parts = _cmd_parts('echo a "" b')
result = _run(expand_parts(parts, _session(), _execute_fn()))
assert result == ["echo", "a", "", "b"]
def test_expand_parts_keeps_empty_quoted_var():
"""A quoted expansion that yields empty stays a word (bash keeps "$x")."""
parts = _cmd_parts('echo a "$EMPTY" b')
session = _session(env={"EMPTY": ""})
result = _run(expand_parts(parts, session, _execute_fn()))
assert result == ["echo", "a", "", "b"]
def test_expand_parts_drops_empty_quoted_dollar_at():
"""Quoted "$@" with no positional args yields zero words, not one empty."""
parts = _cmd_parts('echo a "$@" b')
result = _run(expand_parts(parts, _session(), _execute_fn()))
assert result == ["echo", "a", "b"]
# ══════════════════════════════════════════════
# classify_word
# ══════════════════════════════════════════════
def test_classify_relative_text():
reg = _mock_registry(["/data/"])
result = classify_word("hello", reg, "/data")
assert result == "hello"
assert isinstance(result, str)
def test_classify_absolute_file():
reg = _mock_registry(["/data/"])
result = classify_word("/data/file.txt", reg, "/")
assert isinstance(result, PathSpec)
assert result.virtual == "/data/file.txt"
assert result.directory == "/data/"
assert result.pattern is None
assert result.resolved is True
def test_classify_absolute_directory():
reg = _mock_registry(["/data/"])
result = classify_word("/data/subdir/", reg, "/")
assert isinstance(result, PathSpec)
assert result.resolved is False
def test_classify_absolute_glob():
reg = _mock_registry(["/data/"])
result = classify_word("/data/*.csv", reg, "/")
assert isinstance(result, PathSpec)
assert result.virtual == "/data/*.csv"
assert result.directory == "/data/"
assert result.pattern == "*.csv"
assert result.resolved is False
def test_classify_absolute_glob_question():
reg = _mock_registry(["/s3/"])
result = classify_word("/s3/file?.txt", reg, "/")
assert isinstance(result, PathSpec)
assert result.pattern == "file?.txt"
def test_classify_absolute_glob_bracket():
reg = _mock_registry(["/s3/"])
result = classify_word("/s3/file[0-9].txt", reg, "/")
assert isinstance(result, PathSpec)
assert result.pattern == "file[0-9].txt"
def test_classify_relative_glob():
reg = _mock_registry(["/data/"])
result = classify_word("*.txt", reg, "/data")
assert isinstance(result, PathSpec)
assert result.virtual == "/data/*.txt"
assert result.directory == "/data/"
assert result.pattern == "*.txt"
def test_classify_relative_dotdot_glob():
reg = _mock_registry(["/data/"])
result = classify_word("../*.txt", reg, "/data/sub")
assert isinstance(result, PathSpec)
assert result.virtual == "/data/*.txt"
assert result.directory == "/data/"
assert result.pattern == "*.txt"
def test_classify_no_mount_returns_text():
reg = _mock_registry(["/data/"])
result = classify_word("/unknown/file.txt", reg, "/")
assert result == "/unknown/file.txt"
assert isinstance(result, str)
def test_classify_no_mount_glob_returns_text():
reg = _mock_registry(["/data/"])
result = classify_word("/unknown/*.txt", reg, "/")
assert result == "/unknown/*.txt"
assert isinstance(result, str)
def test_classify_relative_no_glob_always_text():
reg = _mock_registry(["/data/"])
result = classify_word("error", reg, "/data")
assert result == "error"
assert isinstance(result, str)
def test_classify_relative_glob_no_mount_returns_text():
reg = _mock_registry(["/data/"])
result = classify_word("*.txt", reg, "/nomount")
assert result == "*.txt"
assert isinstance(result, str)
# ══════════════════════════════════════════════
# classify_parts
# ══════════════════════════════════════════════
def test_classify_parts_command_name_stays_str():
reg = _mock_registry(["/data/"])
result = classify_parts(["cat", "/data/file.txt"], reg, "/")
assert result[0] == "cat"
assert isinstance(result[0], str)
assert isinstance(result[1], PathSpec)
assert result[1].virtual == "/data/file.txt"
def test_classify_parts_mixed():
reg = _mock_registry(["/data/"])
result = classify_parts(["grep", "-n", "pattern", "/data/file.txt"], reg,
"/")
assert result[0] == "grep"
assert result[1] == "-n"
assert result[2] == "pattern"
assert isinstance(result[3], PathSpec)
def test_classify_parts_multiple_paths():
reg = _mock_registry(["/data/"])
result = classify_parts(["diff", "/data/a.txt", "/data/b.txt"], reg, "/")
assert isinstance(result[1], PathSpec)
assert isinstance(result[2], PathSpec)
assert result[1].virtual == "/data/a.txt"
assert result[2].virtual == "/data/b.txt"
def test_classify_parts_glob_in_args():
reg = _mock_registry(["/data/"])
result = classify_parts(["cat", "/data/*.csv"], reg, "/")
assert isinstance(result[1], PathSpec)
assert result[1].pattern == "*.csv"
def test_classify_parts_no_paths():
reg = _mock_registry(["/data/"])
result = classify_parts(["echo", "hello", "world"], reg, "/")
assert all(isinstance(r, str) for r in result)
def test_classify_parts_empty():
reg = _mock_registry()
result = classify_parts([], reg, "/")
assert result == []
# ══════════════════════════════════════════════
# expand_and_classify
# ══════════════════════════════════════════════
def test_expand_and_classify_text():
parts = _cmd_parts("echo hello world")[1:]
reg = _mock_registry(["/data/"])
session = _session()
result = _run(expand_and_classify(parts, session, _execute_fn(), reg, "/"))
assert result == ["hello", "world"]
def test_expand_and_classify_glob():
parts = _cmd_parts("echo *.txt")[1:]
reg = _mock_registry(["/data/"])
session = _session(cwd="/data")
result = _run(
expand_and_classify(parts, session, _execute_fn(), reg, "/data"))
assert len(result) == 1
assert isinstance(result[0], PathSpec)
assert result[0].pattern == "*.txt"
def test_expand_and_classify_absolute_path():
parts = _cmd_parts("echo /data/file.txt")[1:]
reg = _mock_registry(["/data/"])
session = _session()
result = _run(expand_and_classify(parts, session, _execute_fn(), reg, "/"))
assert len(result) == 1
assert isinstance(result[0], PathSpec)
assert result[0].virtual == "/data/file.txt"
assert result[0].resolved is True
def test_expand_and_classify_var_to_glob():
parts = _cmd_parts("echo $PATTERN")[1:]
reg = _mock_registry(["/s3/"])
session = _session(env={"PATTERN": "/s3/*.csv"})
result = _run(expand_and_classify(parts, session, _execute_fn(), reg, "/"))
assert len(result) == 1
assert isinstance(result[0], PathSpec)
assert result[0].pattern == "*.csv"
def test_expand_and_classify_var_to_path():
parts = _cmd_parts("echo $FILE")[1:]
reg = _mock_registry(["/data/"])
session = _session(env={"FILE": "/data/report.csv"})
result = _run(expand_and_classify(parts, session, _execute_fn(), reg, "/"))
assert len(result) == 1
assert isinstance(result[0], PathSpec)
assert result[0].virtual == "/data/report.csv"
assert result[0].resolved is True
def test_expand_and_classify_no_mount_stays_text():
parts = _cmd_parts("echo /unknown/file.txt")[1:]
reg = _mock_registry(["/data/"])
session = _session()
result = _run(expand_and_classify(parts, session, _execute_fn(), reg, "/"))
assert result == ["/unknown/file.txt"]
assert isinstance(result[0], str)
@@ -0,0 +1,64 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from mirage.resource.gdocs import GDocsConfig, GDocsResource
from mirage.resource.ram import RAMResource
from mirage.resource.slack import SlackConfig, SlackResource
from mirage.types import MountMode
from mirage.workspace import Workspace
def test_file_prompt_includes_mounted_resources():
ram = RAMResource()
ws = Workspace(
{"/": (ram, MountMode.WRITE)},
mode=MountMode.WRITE,
)
prompt = ws.file_prompt
assert "/" in prompt
assert "In-memory" in prompt
def test_file_prompt_shows_write_commands_for_writable_mounts():
slack = SlackResource(config=SlackConfig(token="xoxb-fake"))
ws = Workspace(
{"/slack": (slack, MountMode.WRITE)},
mode=MountMode.WRITE,
)
prompt = ws.file_prompt
assert "/slack" in prompt
assert "slack-post-message" in prompt
def test_file_prompt_hides_write_commands_for_readonly():
slack = SlackResource(config=SlackConfig(token="xoxb-fake"))
ws = Workspace(
{"/slack": (slack, MountMode.READ)},
mode=MountMode.READ,
)
prompt = ws.file_prompt
assert "/slack" in prompt
assert "slack-post-message" not in prompt
def test_file_prompt_substitutes_prefix_in_write_prompt():
cfg = GDocsConfig(client_id="x", client_secret="y", refresh_token="z")
gdocs = GDocsResource(config=cfg)
ws = Workspace(
{"/home/zecheng/gdocs": (gdocs, MountMode.WRITE)},
mode=MountMode.WRITE,
)
prompt = ws.file_prompt
assert "/home/zecheng/gdocs/owned/<file>.gdoc.json" in prompt
assert "{prefix}" not in prompt
@@ -0,0 +1,201 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
"""Tests for find's action flags (-delete, -print0, -ls).
Per-resource find handlers only emit matched paths. The dispatcher
(`mirage/workspace/executor/find_action_dispatch.py:_apply_find_actions`)
reads the parsed action flags and applies the corresponding side
effect or output reformat.
"""
import asyncio
from mirage.resource.ram import RAMResource
from mirage.types import MountMode
from mirage.workspace import Workspace
def _ws() -> Workspace:
return Workspace({"/": RAMResource()}, mode=MountMode.WRITE)
def _ws_two_mounts() -> Workspace:
return Workspace({
"/a": (RAMResource(), MountMode.WRITE),
"/b": (RAMResource(), MountMode.WRITE),
})
def _run(coro):
return asyncio.run(coro)
async def _setup_html_files(ws: Workspace) -> None:
ws.create_session("s")
await ws.execute("mkdir -p /a/b", session_id="s")
await ws.execute("touch /foo.html /bar.htm /a/b/baz.html", session_id="s")
# ── -delete ────────────────────────────────────────────────────
def test_delete_removes_matched_files() -> None:
async def _go():
ws = _ws()
await _setup_html_files(ws)
r = await ws.execute("find / -name '*.html' -delete", session_id="s")
assert r.exit_code == 0
assert await r.stdout_str() == ""
assert await r.stderr_str() == ""
# html files gone
check = await ws.execute("find / -name '*.html'", session_id="s")
assert await check.stdout_str() == ""
# htm preserved
htm = await ws.execute("find / -name '*.htm'", session_id="s")
assert "/bar.htm" in await htm.stdout_str()
_run(_go())
def test_delete_silent_unless_print() -> None:
async def _go():
ws = _ws()
await _setup_html_files(ws)
r = await ws.execute("find / -name '*.html' -delete", session_id="s")
assert await r.stdout_str() == ""
_run(_go())
def test_delete_with_print_emits_matches() -> None:
async def _go():
ws = _ws()
await _setup_html_files(ws)
r = await ws.execute("find / -name '*.html' -print -delete",
session_id="s")
out = await r.stdout_str()
assert "/foo.html" in out
assert "/a/b/baz.html" in out
_run(_go())
def test_delete_skips_mount_roots() -> None:
# A mount root in the match set must not be unlinked: mounts
# are structural metadata.
async def _go():
ws = _ws_two_mounts()
ws.create_session("s")
await ws.execute("touch /a/x.html /b/y.html", session_id="s")
# Force mount roots into the match set via -type d, then
# -delete must skip them while still listing them in find.
# Without a -name pattern the synthetic /a and /b appear.
await ws.execute("find / -type d -delete", session_id="s")
# Mount roots survive (delete may report errors for other
# dir entries, that's fine).
ls = await ws.execute("ls /", session_id="s")
out = await ls.stdout_str()
assert "a" in out
assert "b" in out
_run(_go())
def test_delete_deepest_first() -> None:
# Children deleted before parents so non-empty-dir errors
# don't fire.
async def _go():
ws = _ws()
ws.create_session("s")
await ws.execute("mkdir -p /tmp/a/b", session_id="s")
await ws.execute("touch /tmp/a/b/file.txt", session_id="s")
r = await ws.execute("find /tmp -name '*.txt' -delete", session_id="s")
assert r.exit_code == 0
_run(_go())
# ── -print0 ────────────────────────────────────────────────────
def test_print0_separates_with_nul() -> None:
async def _go():
ws = _ws()
await _setup_html_files(ws)
r = await ws.execute("find / -name '*.html' -print0", session_id="s")
out = await r.stdout_str()
assert "\x00" in out
assert "\n" not in out.replace("\x00", "")
assert out.endswith("\x00")
_run(_go())
# ── -ls ────────────────────────────────────────────────────────
def test_ls_emits_long_format_per_match() -> None:
async def _go():
ws = _ws()
await _setup_html_files(ws)
r = await ws.execute("find / -name '*.html' -ls", session_id="s")
out = await r.stdout_str()
# ls -ld output per match: starts with permission bits.
lines = [ln for ln in out.split("\n") if ln]
assert len(lines) >= 2
for line in lines:
assert line.startswith(("-", "d", "l"))
_run(_go())
# ── default behavior unchanged ─────────────────────────────────
def test_no_action_flag_unchanged() -> None:
# find without action flags must behave as before.
async def _go():
ws = _ws()
await _setup_html_files(ws)
r = await ws.execute("find / -name '*.html'", session_id="s")
out = await r.stdout_str()
assert "/foo.html" in out
assert "/a/b/baz.html" in out
assert "\x00" not in out
_run(_go())
# ── synthetic mount entries honor -name ───────────────────────
def test_mount_entries_filtered_by_name() -> None:
# Without -type filter, mount roots are synthesized as dir
# entries. -name must still apply to those entries so user
# intent ("find files matching X") isn't overridden by
# spurious mount listings.
async def _go():
ws = _ws_two_mounts()
ws.create_session("s")
# /a and /b are mounts; -name 'a' should match only /a.
r = await ws.execute("find / -name 'a' -type d", session_id="s")
lines = (await r.stdout_str()).strip().split("\n")
assert "/a" in lines
assert "/b" not in lines
_run(_go())
+54
View File
@@ -0,0 +1,54 @@
import asyncio
from mirage.resource.ram import RAMResource
from mirage.types import MountMode
from mirage.workspace import Workspace
def _run(coro):
return asyncio.run(coro)
async def _setup() -> Workspace:
ws = Workspace({"/": RAMResource()}, mode=MountMode.WRITE)
ws.create_session("s")
await ws.execute("mkdir -p /data/sub /data/emptydir", session_id="s")
await ws.execute("touch /data/empty.txt /data/sub/nested.txt",
session_id="s")
await ws.execute("printf x > /data/sub/full.txt", session_id="s")
return ws
def test_empty_matches_empty_files_and_dirs() -> None:
async def _go():
ws = await _setup()
r = await ws.execute("find /data -empty", session_id="s")
out = sorted((await r.stdout_str()).split())
assert out == [
"/data/empty.txt", "/data/emptydir", "/data/sub/nested.txt"
]
_run(_go())
def test_empty_with_type_d() -> None:
async def _go():
ws = await _setup()
r = await ws.execute("find /data -type d -empty", session_id="s")
assert sorted((await r.stdout_str()).split()) == ["/data/emptydir"]
_run(_go())
def test_empty_with_type_f() -> None:
async def _go():
ws = await _setup()
r = await ws.execute("find /data -type f -empty", session_id="s")
assert sorted((await r.stdout_str()).split()) == [
"/data/empty.txt", "/data/sub/nested.txt"
]
_run(_go())
@@ -0,0 +1,56 @@
import asyncio
from mirage.resource.ram import RAMResource
from mirage.types import MountMode
from mirage.workspace import Workspace
def _ws() -> Workspace:
return Workspace({"/": RAMResource()}, mode=MountMode.WRITE)
def _run(coro):
return asyncio.run(coro)
async def _setup(ws: Workspace) -> None:
ws.create_session("s")
await ws.execute("mkdir -p /data/sub", session_id="s")
await ws.execute("touch /data/a.txt /data/sub/nested.txt", session_id="s")
def test_unknown_predicate_exits_1_and_prints_nothing() -> None:
async def _go():
ws = _ws()
await _setup(ws)
r = await ws.execute("find /data -boguspredicate", session_id="s")
assert r.exit_code == 1
assert await r.stdout_str() == ""
assert "-boguspredicate" in await r.stderr_str()
_run(_go())
def test_unsupported_regex_exits_1() -> None:
async def _go():
ws = _ws()
await _setup(ws)
r = await ws.execute("find /data -regex '.*'", session_id="s")
assert r.exit_code == 1
_run(_go())
def test_supported_name_still_exits_0() -> None:
async def _go():
ws = _ws()
await _setup(ws)
r = await ws.execute("find /data -name '*.txt'", session_id="s")
assert r.exit_code == 0
out = await r.stdout_str()
assert "/data/a.txt" in out
_run(_go())
@@ -0,0 +1,132 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
import time
from mirage.resource.disk import DiskResource
from mirage.resource.ram import RAMResource
from mirage.resource.s3 import S3Config, S3Resource
from mirage.types import ConsistencyPolicy, MountMode
from mirage.workspace import Workspace
from tests.integration.s3_mock import MultiBucketSession, patch_s3_session
def test_disk_always_refetches_after_external_mutation(tmp_path):
root = tmp_path / "disk"
root.mkdir()
(root / "file.txt").write_bytes(b"v1")
resource = DiskResource(root=str(root))
ws = Workspace(
{"/data": (resource, MountMode.WRITE)},
mode=MountMode.WRITE,
consistency=ConsistencyPolicy.ALWAYS,
)
async def run() -> tuple[bytes, bytes]:
io1 = await ws.execute("cat /data/file.txt")
first = await io1.materialize_stdout()
time.sleep(1.1)
(root / "file.txt").write_bytes(b"v2")
io2 = await ws.execute("cat /data/file.txt")
second = await io2.materialize_stdout()
return first, second
first, second = asyncio.run(run())
assert first == b"v1"
assert second == b"v2", (
"ALWAYS must refetch from disk after mtime changed; got stale cache")
def test_disk_lazy_keeps_stale_cache_after_external_mutation(tmp_path):
root = tmp_path / "disk"
root.mkdir()
(root / "file.txt").write_bytes(b"v1")
resource = DiskResource(root=str(root))
ws = Workspace(
{"/data": (resource, MountMode.WRITE)},
mode=MountMode.WRITE,
consistency=ConsistencyPolicy.LAZY,
)
async def run() -> tuple[bytes, bytes]:
io1 = await ws.execute("cat /data/file.txt")
first = await io1.materialize_stdout()
time.sleep(1.1)
(root / "file.txt").write_bytes(b"v2")
io2 = await ws.execute("cat /data/file.txt")
second = await io2.materialize_stdout()
return first, second
first, second = asyncio.run(run())
assert first == b"v1"
assert second in (b"v1", b"v2"), (
"LAZY allowed to serve cached bytes; this test just confirms no crash")
def test_s3_always_warm_read_serves_cache_for_non_md5_fingerprint():
"""Multipart-style ETags are not the MD5 of the content. The cold
read must stamp the cache entry with the backend ETag so a warm read
under ALWAYS passes the freshness check and serves from cache
instead of evicting and refetching on every read."""
store = {"data.txt": b"name,age\nalice,30\n"}
session = MultiBucketSession({"test-bucket": store}, etag_suffix="-2")
client = session._client
with patch_s3_session(session):
config = S3Config(
bucket="test-bucket",
region="us-east-1",
aws_access_key_id="fake",
aws_secret_access_key="fake",
)
ws = Workspace(
{"/s3": (S3Resource(config), MountMode.WRITE)},
mode=MountMode.WRITE,
consistency=ConsistencyPolicy.ALWAYS,
)
async def run() -> tuple[bytes, bytes]:
io1 = await ws.execute("cat /s3/data.txt | wc -c")
first = await io1.materialize_stdout()
io2 = await ws.execute("cat /s3/data.txt | wc -c")
second = await io2.materialize_stdout()
return first, second
first, second = asyncio.run(run())
assert first == second == b"18\n"
assert client.calls["head_object"] >= 1, (
"ALWAYS must consult the remote fingerprint on the warm read")
assert client.calls["get_object"] == 1, (
"warm read with an unchanged remote fingerprint must serve from "
"cache; a second get_object means the entry was evicted")
def test_ram_falls_back_to_lazy_when_fingerprint_absent():
resource = RAMResource()
resource._store.files["/file.txt"] = b"v1"
ws = Workspace(
{"/data": (resource, MountMode.WRITE)},
mode=MountMode.WRITE,
consistency=ConsistencyPolicy.ALWAYS,
)
async def run() -> bytes:
io1 = await ws.execute("cat /data/file.txt")
return await io1.materialize_stdout()
data = asyncio.run(run())
assert data == b"v1", (
"RAM read under ALWAYS must succeed (no fingerprint → LAZY fallback)")
+70
View File
@@ -0,0 +1,70 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import subprocess
import tempfile
from mirage.resource.ram import RAMResource
from mirage.types import MountMode
from mirage.workspace.fuse import FuseManager
from mirage.workspace.workspace import Workspace
def _fake_mount(monkeypatch):
monkeypatch.setattr("mirage.workspace.fuse.mount_background",
lambda *_args, **_kwargs: None)
monkeypatch.setattr(subprocess, "run", lambda *_args, **_kwargs: None)
class TestFuseManager:
def test_initial_state(self):
fm = FuseManager()
assert fm.mountpoint is None
def test_close_without_mountpoint_does_nothing(self):
fm = FuseManager()
fm.close()
assert fm.mountpoint is None
def test_close_keeps_caller_owned_mountpoint(self, monkeypatch, tmp_path):
# Regression: explicit mountpoints are caller-owned deployment paths.
# close() should unmount FUSE, not remove the directory given by the
# caller.
_fake_mount(monkeypatch)
ws = Workspace({"/a/": RAMResource()}, mode=MountMode.WRITE)
fm = FuseManager()
fm.setup(ws._ops, prefix="/a/", mountpoint=str(tmp_path))
fm.close()
assert tmp_path.exists()
assert fm.mountpoint is None
def test_close_removes_generated_mountpoint(self, monkeypatch, tmp_path):
# Generated temp mountpoints are Mirage-owned, so close() removes the
# directory it created with an empty-directory rmdir.
generated = tmp_path / "mirage-generated"
generated.mkdir()
_fake_mount(monkeypatch)
monkeypatch.setattr(tempfile, "mkdtemp",
lambda *_args, **_kwargs: str(generated))
ws = Workspace({"/a/": RAMResource()}, mode=MountMode.WRITE)
fm = FuseManager()
fm.setup(ws._ops, prefix="/a/")
fm.close()
assert not generated.exists()
assert fm.mountpoint is None
+155
View File
@@ -0,0 +1,155 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import subprocess
import pytest
from mirage import FuseManager, Mount, MountMode, Workspace
from mirage.resource.ram import RAMResource
class _FakeThread:
def __init__(self):
self.alive = True
def _fake_mount(monkeypatch):
monkeypatch.setattr("mirage.workspace.fuse.mount_background",
lambda ops, mountpoint, root_prefix="": _FakeThread())
monkeypatch.setattr(subprocess, "run", lambda *_args, **_kwargs: None)
def test_add_fuse_mount_registers_and_returns_mountpoint(monkeypatch):
_fake_mount(monkeypatch)
ws = Workspace({"/a/": RAMResource()}, mode=MountMode.WRITE)
mp = ws.add_fuse_mount("/a/", "/tmp/forced-a")
assert mp == "/tmp/forced-a"
assert ws.fuse_mountpoints == {"/a/": "/tmp/forced-a"}
ws.remove_fuse_mount("/a/")
assert ws.fuse_mountpoints == {}
def test_workspace_close_unmounts_managers(monkeypatch):
_fake_mount(monkeypatch)
with Workspace({"/a/": RAMResource()}, mode=MountMode.WRITE) as ws:
ws.add_fuse_mount("/a/", "/tmp/tracked-a")
assert ws.fuse_mountpoints == {"/a/": "/tmp/tracked-a"}
assert ws.fuse_mountpoints == {}
def test_multiple_fuse_mounts_are_independent(monkeypatch):
_fake_mount(monkeypatch)
ws = Workspace({
"/a/": RAMResource(),
"/b/": RAMResource()
},
mode=MountMode.WRITE)
ws.add_fuse_mount("/a/", "/tmp/mp-a")
ws.add_fuse_mount("/b/", "/tmp/mp-b")
assert ws.fuse_mountpoints == {"/a/": "/tmp/mp-a", "/b/": "/tmp/mp-b"}
assert set(ws._fuse_managers) == {"/a/", "/b/"}
ws.remove_fuse_mount("/a/")
assert ws.fuse_mountpoints == {"/b/": "/tmp/mp-b"}
assert set(ws._fuse_managers) == {"/b/"}
def test_collision_rejected_before_mount(monkeypatch):
calls = []
monkeypatch.setattr("mirage.workspace.fuse.mount_background",
lambda ops, mountpoint, root_prefix="":
(calls.append(mountpoint) or _FakeThread()))
monkeypatch.setattr(subprocess, "run", lambda *_args, **_kwargs: None)
ws = Workspace({
"/a/": RAMResource(),
"/b/": RAMResource()
},
mode=MountMode.WRITE)
ws.add_fuse_mount("/a/", "/tmp/dup-mp")
with pytest.raises(ValueError):
ws.add_fuse_mount("/b/", "/tmp/dup-mp")
assert ws.fuse_mountpoints == {"/a/": "/tmp/dup-mp"}
assert calls == ["/tmp/dup-mp"]
def test_double_unmount_is_idempotent(monkeypatch):
_fake_mount(monkeypatch)
ws = Workspace({"/a/": RAMResource()}, mode=MountMode.WRITE)
fm = FuseManager()
fm.setup(ws._ops, "/a/", mountpoint="/tmp/idem-a")
fm.unmount()
fm.unmount() # must not raise
assert fm.mountpoint is None
def test_mount_spec_fuse_true_single(monkeypatch):
_fake_mount(monkeypatch)
ws = Workspace({"/gdocs/": Mount(RAMResource(), fuse=True)},
mode=MountMode.WRITE)
mps = ws.fuse_mountpoints
assert set(mps) == {"/gdocs/"}
assert mps["/gdocs/"]
assert ws.fuse_mountpoint == mps["/gdocs/"]
def test_mount_spec_fuse_pinned_path(monkeypatch):
_fake_mount(monkeypatch)
ws = Workspace({"/whatever/": Mount(RAMResource(), fuse="/tmp/pinned-x")},
mode=MountMode.WRITE)
assert ws.fuse_mountpoints["/whatever/"] == "/tmp/pinned-x"
def test_mount_spec_fuse_each_of_multiple(monkeypatch):
_fake_mount(monkeypatch)
ws = Workspace(
{
"/a/": Mount(RAMResource(), fuse=True),
"/b/": Mount(RAMResource(), fuse=True)
},
mode=MountMode.WRITE)
assert set(ws.fuse_mountpoints) == {"/a/", "/b/"}
with pytest.raises(RuntimeError):
ws.fuse_mountpoint
def test_mount_spec_mode_inherits_and_override(monkeypatch):
_fake_mount(monkeypatch)
ws = Workspace(
{
"/inherit/": Mount(RAMResource()),
"/override/": Mount(RAMResource(), mode=MountMode.READ),
},
mode=MountMode.WRITE)
assert ws.mount("/inherit/").mode == MountMode.WRITE
assert ws.mount("/override/").mode == MountMode.READ
def test_no_fuse_when_bare_or_tuple(monkeypatch):
_fake_mount(monkeypatch)
ws = Workspace(
{
"/bare/": RAMResource(),
"/tup/": (RAMResource(), MountMode.WRITE),
},
mode=MountMode.WRITE)
assert ws.fuse_mountpoints == {}
def test_mount_spec_fuse_unmounts_on_close(monkeypatch):
_fake_mount(monkeypatch)
with Workspace({"/gdocs/": Mount(RAMResource(), fuse=True)},
mode=MountMode.WRITE) as ws:
assert set(ws.fuse_mountpoints) == {"/gdocs/"}
assert ws.fuse_mountpoints == {}
@@ -0,0 +1,49 @@
import pytest
from mirage import MountMode, Workspace
from mirage.resource.ram import RAMResource
def _ws():
return Workspace({
"/a/": RAMResource(),
"/b/": RAMResource()
},
mode=MountMode.WRITE)
def test_no_fuse_mounts_returns_empty_and_none():
with _ws() as ws:
assert ws.fuse_mountpoints == {}
assert ws.fuse_mountpoint is None
def test_register_one_mount_exposes_singular():
with _ws() as ws:
ws._register_fuse("/a/", "/tmp/mp-a")
assert ws.fuse_mountpoints == {"/a/": "/tmp/mp-a"}
assert ws.fuse_mountpoint == "/tmp/mp-a"
def test_register_two_distinct_paths_singular_raises():
with _ws() as ws:
ws._register_fuse("/a/", "/tmp/mp-a")
ws._register_fuse("/b/", "/tmp/mp-b")
assert set(ws.fuse_mountpoints) == {"/a/", "/b/"}
with pytest.raises(RuntimeError):
_ = ws.fuse_mountpoint
def test_register_colliding_path_raises():
with _ws() as ws:
ws._register_fuse("/a/", "/tmp/same")
with pytest.raises(ValueError):
ws._register_fuse("/b/", "/tmp/same")
def test_deregister_removes_entry():
with _ws() as ws:
ws._register_fuse("/a/", "/tmp/mp-a")
ws._deregister_fuse("/a/")
assert ws.fuse_mountpoints == {}
assert ws.fuse_mountpoint is None
@@ -0,0 +1,77 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
from mirage.resource.ram import RAMResource
from mirage.types import MountMode
from mirage.workspace import Workspace
def _build_ws() -> Workspace:
r = RAMResource()
r._store.dirs.add("/")
r._store.dirs.add("/src")
r._store.files["/src/a.js"] = b'legacyFetch("/api");\n'
return Workspace({"/": (r, MountMode.WRITE)})
async def _run(cmd: str):
ws = _build_ws()
try:
io = await ws.execute(cmd)
stdout = await io.stdout_str()
return io.exit_code, stdout
finally:
await ws.close()
def test_grep_r_src_returns_exit_0():
code, out = asyncio.run(_run('grep -rEn "legacyFetch" /src'))
assert code == 0
assert "legacyFetch" in out
def test_grep_r_root_returns_exit_0_when_match_found():
code, out = asyncio.run(_run('grep -rEn "legacyFetch" /'))
assert code == 0
assert "legacyFetch" in out
def test_grep_r_cwd_returns_exit_0_when_match_found():
code, out = asyncio.run(_run('grep -rEn "legacyFetch" .'))
assert code == 0
assert "legacyFetch" in out
def test_grep_r_root_no_match_returns_exit_1():
code, _ = asyncio.run(_run('grep -rEn "doesNotExistAnywhere" /'))
assert code == 1
def test_grep_r_root_in_if_then():
code, out = asyncio.run(
_run('if grep -rEn "legacyFetch" /; then echo FOUND; fi'))
assert "FOUND" in out
def test_grep_r_root_with_and():
_, out = asyncio.run(_run('grep -rEn "legacyFetch" / && echo OK'))
assert "OK" in out
def test_grep_r_root_with_or_does_not_run_right_arm():
_, out = asyncio.run(
_run('grep -rEn "legacyFetch" / || echo SHOULD_NOT_PRINT'))
assert "SHOULD_NOT_PRINT" not in out
@@ -0,0 +1,92 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
from mirage.resource.ram import RAMResource
from mirage.types import MountMode
from mirage.workspace import Workspace
def _run(coro):
return asyncio.run(coro)
def _ws():
ram = RAMResource()
ram._store.files["/hello.txt"] = b"hi\n"
return Workspace(resources={"/ram/": (ram, MountMode.EXEC)}, )
def _exec(ws, cmd):
return _run(ws.execute(cmd))
def test_help_flag_renders_help_through_executor():
ws = _ws()
io = _exec(ws, "cat --help")
assert io.exit_code == 0
out = io.stdout.decode() if isinstance(io.stdout, bytes) else _run(
_materialize(io.stdout))
assert "Usage: cat" in out
assert "--help" in out
def test_man_renders_help_for_known_command():
ws = _ws()
io = _exec(ws, "man cat")
assert io.exit_code == 0
out = io.stdout.decode() if isinstance(io.stdout, bytes) else _run(
_materialize(io.stdout))
assert "cat" in out
def test_man_no_args_lists_commands_grouped_by_resource():
ws = _ws()
io = _exec(ws, "man")
assert io.exit_code == 0
out = io.stdout.decode() if isinstance(io.stdout, bytes) else _run(
_materialize(io.stdout))
assert "RAM" in out
assert "- cat" in out
assert "- ls" in out
assert "# general" in out
def test_man_unknown_command_exits_1():
ws = _ws()
io = _exec(ws, "man definitely-not-a-real-command")
assert io.exit_code == 1
err = io.stderr.decode() if isinstance(io.stderr, bytes) else _run(
_materialize(io.stderr))
assert "no entry for" in err
def test_workspace_file_prompt_mentions_help_and_man():
ws = _ws()
prompt = ws.file_prompt
assert "--help" in prompt
assert "man <cmd>" in prompt
assert "`man`" in prompt
async def _materialize(source):
if source is None:
return ""
if isinstance(source, bytes):
return source.decode()
chunks = []
async for chunk in source:
chunks.append(chunk)
return b"".join(chunks).decode()
+399
View File
@@ -0,0 +1,399 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
import pytest
from mirage.accessor.base import Accessor, NOOPAccessor
from mirage.commands.registry import command
from mirage.commands.spec import SPECS
from mirage.io.types import ByteSource, IOResult
from mirage.resource.ram import RAMResource
from mirage.types import MountMode, PathSpec
from mirage.workspace.snapshot import apply_state_dict, read_tar
from mirage.workspace.workspace import Workspace
@command("history", resource="ram", spec=SPECS["history"])
async def fake_history(
accessor: Accessor = NOOPAccessor(),
paths: list[PathSpec] | None = None,
*texts: str,
**_extra: object,
) -> tuple[ByteSource | None, IOResult]:
return b"FAKE\n", IOResult()
def _ws() -> Workspace:
return Workspace({"/data": (RAMResource(), MountMode.WRITE)})
def _exec(ws: Workspace, cmd: str, **kw) -> IOResult:
return asyncio.run(ws.execute(cmd, **kw))
def _stdout(io: IOResult) -> str:
if io.stdout is None:
return ""
if isinstance(io.stdout, memoryview):
return bytes(io.stdout).decode()
if isinstance(io.stdout, (bytes, bytearray)):
return bytes(io.stdout).decode()
return ""
def test_history_command_per_session():
ws = _ws()
ws.create_session("s2")
_exec(ws, "ls /data")
_exec(ws, "pwd")
_exec(ws, "ls /", session_id="s2")
mine = _stdout(_exec(ws, "history"))
other = _stdout(_exec(ws, "history", session_id="s2"))
assert "ls /data" in mine
assert "pwd" in mine
assert "ls /data" not in other
assert "ls /" in other
def test_history_builtin_wins_over_mount_command():
res = RAMResource()
res.register(fake_history)
ws = Workspace({"/data": (res, MountMode.WRITE)})
_exec(ws, "ls /data")
out = _stdout(_exec(ws, "history", cwd="/data"))
assert "FAKE" not in out
assert "ls /data" in out
def test_cat_bash_history_all_sessions():
ws = _ws()
ws.create_session("s2")
_exec(ws, "ls /data")
_exec(ws, "pwd", session_id="s2")
io = _exec(ws, "cat /.bash_history")
assert io.exit_code == 0
out = _stdout(io)
assert "ls /data" in out
assert "pwd" in out
assert out.count("#") >= 2
def test_grep_and_tail_bash_history():
ws = _ws()
_exec(ws, "ls /data")
_exec(ws, "pwd")
grep_io = _exec(ws, "grep pwd /.bash_history")
assert grep_io.exit_code == 0
grep_out = _stdout(grep_io)
assert "pwd" in grep_out
assert "ls /data" not in grep_out
tail_io = _exec(ws, "tail -n 2 /.bash_history")
assert tail_io.exit_code == 0
assert "pwd" in _stdout(tail_io)
def test_ls_root_hides_dotfile_ls_a_shows():
ws = _ws()
plain = _stdout(_exec(ws, "ls /"))
dotted = _stdout(_exec(ws, "ls -a /"))
assert ".bash_history" not in plain
assert ".bash_history" in dotted
def test_history_c_clears_only_my_session_file_unchanged():
ws = _ws()
ws.create_session("s2")
_exec(ws, "ls /data")
_exec(ws, "pwd", session_id="s2")
clear_io = _exec(ws, "history -c")
assert clear_io.exit_code == 0
mine = _stdout(_exec(ws, "history"))
other = _stdout(_exec(ws, "history", session_id="s2"))
file_out = _stdout(_exec(ws, "cat /.bash_history"))
assert "ls /data" not in mine
assert "pwd" in other
assert "ls /data" in file_out
def test_append_to_bash_history_rejected():
ws = _ws()
io = _exec(ws, "echo hacked >> /.bash_history")
assert io.exit_code != 0
def test_sessions_path_no_longer_resolves():
ws = _ws()
io = _exec(ws, "ls /.sessions")
assert io.exit_code != 0
def test_unmount_history_view_rejected():
ws = _ws()
with pytest.raises(ValueError, match="history view"):
asyncio.run(ws.unmount("/.bash_history"))
def test_snapshot_roundtrip_preserves_tombstones(tmp_path):
ws = _ws()
_exec(ws, "ls /data")
_exec(ws, "history -c")
_exec(ws, "pwd")
snap = tmp_path / "ws.tar"
asyncio.run(ws.snapshot(snap))
dst = asyncio.run(Workspace.load(snap))
mine = _stdout(_exec(dst, "history"))
file_out = _stdout(_exec(dst, "cat /.bash_history"))
assert "ls /data" not in mine
assert "pwd" in mine
assert "ls /data" in file_out
def test_workspace_history_method():
ws = _ws()
_exec(ws, "ls /data")
_exec(ws, "pwd")
events = asyncio.run(ws.history())
assert [e["command"] for e in events] == ["ls /data", "pwd"]
assert all(e["type"] == "command" for e in events)
def test_history_s_appends_without_executing():
ws = _ws()
_exec(ws, "pwd")
io = _exec(ws, "history -s rm -rf /data")
assert io.exit_code == 0
out = _stdout(_exec(ws, "history"))
assert "rm -rf /data" in out
assert _exec(ws, "ls /data/x").exit_code != 0
def test_history_d_deletes_and_renumbers():
ws = _ws()
_exec(ws, "pwd")
_exec(ws, "echo keep")
io = _exec(ws, "history -d 1")
assert io.exit_code == 0
out = _stdout(_exec(ws, "history"))
assert "pwd" not in out.split("history -d 1")[0]
assert out.startswith("1 echo keep")
def test_history_d_attached_offset_deletes():
ws = _ws()
_exec(ws, "pwd")
_exec(ws, "echo keep")
io = _exec(ws, "history -d1")
assert io.exit_code == 0
out = _stdout(_exec(ws, "history"))
assert out.startswith("1 echo keep")
def test_history_d_out_of_range():
ws = _ws()
_exec(ws, "pwd")
io = _exec(ws, "history -d 99")
assert io.exit_code == 1
assert b"99: history position out of range" in io.stderr
def test_history_d_non_numeric():
ws = _ws()
io = _exec(ws, "history -d abc")
assert io.exit_code == 1
assert b"abc: history position out of range" in io.stderr
def test_history_d_trailing_garbage_rejected():
ws = _ws()
_exec(ws, "pwd")
io = _exec(ws, "history -d 1abc")
assert io.exit_code == 1
assert b"1abc: history position out of range" in io.stderr
def test_history_count_trailing_garbage_rejected():
ws = _ws()
_exec(ws, "pwd")
io = _exec(ws, "history 3abc")
assert io.exit_code == 1
assert b"3abc: numeric argument required" in io.stderr
def test_history_d_negative_offset_deletes_last():
ws = _ws()
_exec(ws, "echo first")
_exec(ws, "echo last")
io = _exec(ws, "history -d -1")
assert io.exit_code == 0
out = _stdout(_exec(ws, "history"))
assert "echo first" in out
lines = [ln for ln in out.splitlines() if "echo last" in ln]
assert lines == []
def test_history_d_requires_argument():
ws = _ws()
io = _exec(ws, "history -d")
assert io.exit_code == 2
assert b"-d: option requires an argument" in io.stderr
assert b"history: usage:" in io.stderr
def test_history_invalid_option_usage_exit_2():
ws = _ws()
io = _exec(ws, "history -z")
assert io.exit_code == 2
assert b"-z: invalid option" in io.stderr
assert b"history: usage:" in io.stderr
def test_history_p_prints_without_storing_args():
ws = _ws()
io = _exec(ws, "history -p hello world")
assert io.exit_code == 0
assert _stdout(io) == "hello\nworld\n"
out = _stdout(_exec(ws, "history"))
assert "1 history -p hello world" in out
def test_history_ps_suppresses_print():
ws = _ws()
io = _exec(ws, "history -ps echo hi")
assert io.exit_code == 0
assert _stdout(io) == ""
out = _stdout(_exec(ws, "history"))
assert "echo hi" in out
def test_history_zero_lists_nothing():
ws = _ws()
_exec(ws, "pwd")
_exec(ws, "echo two")
io = _exec(ws, "history 0")
assert io.exit_code == 0
assert _stdout(io) == ""
def test_history_sync_flags_are_noops():
ws = _ws()
_exec(ws, "pwd")
for flag in ("-a", "-r", "-w", "-n"):
io = _exec(ws, f"history {flag}")
assert io.exit_code == 0
assert _stdout(io) == ""
def test_history_cluster_cw():
ws = _ws()
_exec(ws, "pwd")
io = _exec(ws, "history -cw")
assert io.exit_code == 0
out = _stdout(_exec(ws, "history 1"))
assert "pwd" not in out
def test_find_view_via_generic():
ws = _ws()
out = _stdout(_exec(ws, "find /.bash_history"))
assert out == "/.bash_history\n"
out = _stdout(_exec(ws, "find /.bash_history -name '*.bash*'"))
assert out == "/.bash_history\n"
out = _stdout(_exec(ws, "find /.bash_history -type d"))
assert out == ""
def test_substitution_records_only_outer_line():
ws = _ws()
_exec(ws, "echo hi > /data/f.txt")
_exec(ws, "wc -l $(find /data -name '*.txt')")
commands = [e["command"] for e in asyncio.run(ws.history())]
assert commands == [
"echo hi > /data/f.txt",
"wc -l $(find /data -name '*.txt')",
]
def test_substitution_keeps_inner_ops_in_audit():
ws = _ws()
_exec(ws, "echo hi > /data/f.txt")
_exec(ws, "echo $(cat /data/f.txt)")
events = asyncio.run(ws.observer.events())
reads = [e for e in events if e["type"] == "op" and e["op"] == "read"]
assert any(e["path"] == "/data/f.txt" for e in reads)
def test_outer_ops_after_substitution_not_lost():
ws = _ws()
_exec(ws, "echo hi > /data/f.txt")
_exec(ws, "wc -l $(find /data -name '*.txt')")
events = asyncio.run(ws.observer.events())
reads = [(e["op"], e["path"]) for e in events if e["type"] == "op"]
assert ("read", "/data/f.txt") in reads
def test_eval_xargs_source_record_single_entry():
ws = _ws()
_exec(ws, "echo hi > /data/f.txt")
_exec(ws, "eval cat /data/f.txt")
_exec(ws, "echo /data/f.txt | xargs cat")
_exec(ws, "echo 'cat /data/f.txt' > /data/s.sh")
_exec(ws, "source /data/s.sh")
commands = [e["command"] for e in asyncio.run(ws.history())]
assert commands == [
"echo hi > /data/f.txt",
"eval cat /data/f.txt",
"echo /data/f.txt | xargs cat",
"echo 'cat /data/f.txt' > /data/s.sh",
"source /data/s.sh",
]
def test_unrecorded_execute_skips_history_keeps_caller_ops():
ws = _ws()
_exec(ws, "echo hi > /data/f.txt")
_exec(ws, "cat /data/f.txt", record=False)
commands = [e["command"] for e in asyncio.run(ws.history())]
assert commands == ["echo hi > /data/f.txt"]
async def _raise_induced(io):
raise RuntimeError("induced")
def test_in_place_restore_rewinds_history(tmp_path):
src = _ws()
_exec(src, "echo from-snapshot")
snap = tmp_path / "s.tar"
asyncio.run(src.snapshot(snap))
dst = _ws()
_exec(dst, "echo pre-restore")
asyncio.run(apply_state_dict(dst, read_tar(snap)))
cmds = [e["command"] for e in asyncio.run(dst.history())]
assert cmds == ["echo from-snapshot"]
def test_failed_line_ops_still_in_audit(monkeypatch):
ws = _ws()
_exec(ws, "echo hi > /data/f.txt")
monkeypatch.setattr(ws, "apply_io", _raise_induced)
io = _exec(ws, "cat /data/f.txt")
assert io.exit_code == 1
events = asyncio.run(ws.observer.events())
reads = [e for e in events if e["type"] == "op" and e["op"] == "read"]
assert any(e["path"] == "/data/f.txt" for e in reads)
last_cmd = [e for e in events if e["type"] == "command"][-1]
assert last_cmd["command"] == "cat /data/f.txt"
assert last_cmd["exit_code"] == 1
@@ -0,0 +1,46 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from mirage import Workspace
from mirage.cache.index import (RAMIndexCacheStore, RedisIndexCacheStore,
RedisIndexConfig)
from mirage.config import MountBlock, RedisIndexBlock, WorkspaceConfig
from mirage.resource.ram import RAMResource
def test_redis_index_config_default_key_prefix():
assert RedisIndexConfig().key_prefix == "mirage:index:"
def test_workspace_index_param_applies_to_mounts():
r = RAMResource()
Workspace({"/m": r},
index=RedisIndexConfig(url="redis://localhost:6379/0"))
assert isinstance(r.index, RedisIndexCacheStore)
def test_workspace_default_index_is_ram():
r = RAMResource()
Workspace({"/m": r})
assert isinstance(r.index, RAMIndexCacheStore)
def test_config_index_redis_block_builds_redis_config():
cfg = WorkspaceConfig(
mounts={"/m": MountBlock(resource="ram")},
index=RedisIndexBlock(type="redis"),
)
kwargs = cfg.to_workspace_kwargs()
assert isinstance(kwargs["index"], RedisIndexConfig)
assert kwargs["index"].key_prefix == "mirage:index:"
@@ -0,0 +1,165 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
import boto3
from moto import mock_aws
from mirage.cache.index import LookupStatus
from mirage.resource.ram import RAMResource
from mirage.resource.s3.s3 import S3Config, S3Resource
from mirage.types import DEFAULT_SESSION_ID, MountMode
from mirage.workspace import Workspace
def _run(coro):
return asyncio.run(coro)
def _stdout(io):
if io.stdout is None:
return b""
if isinstance(io.stdout, bytes):
return io.stdout
if isinstance(io.stdout, memoryview):
return bytes(io.stdout)
return b""
# ── resource has index ─────────────────────────
def test_ram_resource_has_index():
p = RAMResource()
assert p.index is not None
assert p.index_ttl == 0
def test_s3_resource_has_index():
with mock_aws():
boto3.client("s3",
region_name="us-east-1").create_bucket(Bucket="test-idx")
p = S3Resource(S3Config(bucket="test-idx", region="us-east-1"))
assert p.index is not None
assert p.index_ttl == 600
# ── RAM index integration ─────────────────────
def _ram_ws():
p = RAMResource()
p._store.dirs.add("/sub")
p._store.files["/sub/a.txt"] = b"aaa\n"
p._store.files["/sub/b.txt"] = b"bbb\n"
p._store.files["/sub/c.csv"] = b"col\n"
ws = Workspace(resources={"/data/": (p, MountMode.WRITE)}, )
ws.get_session(DEFAULT_SESSION_ID).cwd = "/data"
return ws, p
def test_ram_glob_populates_index():
"""RAM ttl=0 → index populated but expires immediately."""
ws, prov = _ram_ws()
io = _run(ws.execute("cat /data/sub/*.txt"))
assert io.exit_code == 0
# ttl=0: entries were set but expired by the time we check
# Index now stores virtual paths (with mount prefix)
listing = _run(prov.index.list_dir("/data/sub"))
assert listing.status == LookupStatus.EXPIRED
def test_ram_glob_second_call_uses_index():
ws, prov = _ram_ws()
_run(ws.execute("cat /data/sub/a.txt"))
_run(ws.execute("cat /data/sub/a.txt"))
def test_ram_glob_pattern_works():
ws, prov = _ram_ws()
io = _run(ws.execute("cat /data/sub/*.txt"))
assert io.exit_code == 0
# ── S3 index integration ──────────────────────
def test_s3_resource_index_ttl():
"""S3 resource gets index with TTL=600."""
with mock_aws():
boto3.client("s3",
region_name="us-east-1").create_bucket(Bucket="test-ttl")
prov = S3Resource(S3Config(bucket="test-ttl", region="us-east-1"))
assert prov.index is not None
assert prov.index_ttl == 600
def test_s3_index_can_store_entries():
"""S3 index can store and retrieve entries."""
from mirage.cache.index import IndexEntry
with mock_aws():
boto3.client(
"s3", region_name="us-east-1").create_bucket(Bucket="test-store")
prov = S3Resource(S3Config(bucket="test-store", region="us-east-1"))
_run(
prov.index.set_dir("/data", [
("a.txt", IndexEntry(
id="a", name="a.txt", resource_type="file")),
("b.txt", IndexEntry(
id="b", name="b.txt", resource_type="file")),
]))
listing = _run(prov.index.list_dir("/data"))
assert listing.entries is not None
assert len(listing.entries) == 2
def test_s3_index_separate_from_ram():
"""S3 and RAM have independent indexes."""
ram = RAMResource()
with mock_aws():
boto3.client("s3",
region_name="us-east-1").create_bucket(Bucket="test-sep")
s3 = S3Resource(S3Config(bucket="test-sep", region="us-east-1"))
assert ram.index is not s3.index
# ── index per resource ─────────────────────────
def test_index_per_resource():
p1 = RAMResource()
p2 = RAMResource()
assert p1.index is not p2.index
# ── TTL behavior ───────────────────────────────
def test_ram_index_ttl_zero():
p = RAMResource()
assert p.index_ttl == 0
def test_index_expired_refetches():
p = RAMResource()
p._store.dirs.add("/sub")
p._store.files["/sub/a.txt"] = b"aaa\n"
ws = Workspace(resources={"/data/": (p, MountMode.WRITE)}, )
ws.get_session(DEFAULT_SESSION_ID).cwd = "/data"
_run(ws.execute("cat /data/sub/*.txt"))
listing = _run(p.index.list_dir("/data/sub"))
# RAM ttl=0 → expired immediately after set
expired = listing.status == LookupStatus.EXPIRED
assert expired or listing.entries is not None
@@ -0,0 +1,79 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
from mirage.resource.ram import RAMResource
from mirage.types import MountMode, PathSpec
from mirage.utils.key_prefix import mount_key
from mirage.workspace import Workspace
class _FakeRemote(RAMResource):
caches_reads = True
index_ttl = 600
def _seed_remote() -> _FakeRemote:
remote = _FakeRemote()
remote._store.dirs.add("/data")
remote._store.files["/data/a.txt"] = b"a"
return remote
def test_dispatch_write_invalidates_parent_dir_index():
remote = _seed_remote()
ws = Workspace({"/r": (remote, MountMode.WRITE)}, mode=MountMode.WRITE)
async def run():
before = await ws.readdir("/r/data")
scope = PathSpec(
resource_path=mount_key("/r/data/b.txt", "/r"),
virtual="/r/data/b.txt",
directory="/r/data",
resolved=True,
)
await ws.dispatch("write", scope, data=b"b")
after = await ws.readdir("/r/data")
return before, after
before, after = asyncio.run(run())
assert "/r/data/a.txt" in before
assert "/r/data/b.txt" in after, (
"after dispatch write, readdir should reflect b.txt; "
f"got {after!r}")
def test_dispatch_unlink_invalidates_parent_dir_index():
remote = _seed_remote()
remote._store.files["/data/c.txt"] = b"c"
ws = Workspace({"/r": (remote, MountMode.WRITE)}, mode=MountMode.WRITE)
async def run():
before = await ws.readdir("/r/data")
scope = PathSpec(
resource_path=mount_key("/r/data/c.txt", "/r"),
virtual="/r/data/c.txt",
directory="/r/data",
resolved=True,
)
await ws.dispatch("unlink", scope)
after = await ws.readdir("/r/data")
return before, after
before, after = asyncio.run(run())
assert "/r/data/c.txt" in before
assert "/r/data/c.txt" not in after, (
"after dispatch unlink, readdir should drop c.txt; "
f"got {after!r}")
@@ -0,0 +1,192 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
import io
import zipfile
from collections.abc import Iterator
import boto3
import pytest
from moto.server import ThreadedMotoServer
from mirage.resource.ram import RAMResource
from mirage.resource.s3.s3 import S3Config, S3Resource
from mirage.types import MountMode
from mirage.workspace import Workspace
CREDS = dict(aws_access_key_id="testing",
aws_secret_access_key="testing",
region_name="us-east-1")
@pytest.fixture()
def s3_endpoint() -> Iterator[str]:
server = ThreadedMotoServer(ip_address="127.0.0.1", port=0, verbose=False)
server.start()
host, port = server.get_host_and_port()
yield f"http://{host}:{port}"
server.stop()
def _s3_workspace(endpoint: str, bucket: str) -> Workspace:
boto3.client("s3", endpoint_url=endpoint,
**CREDS).create_bucket(Bucket=bucket)
s3 = S3Resource(
S3Config(bucket=bucket,
region="us-east-1",
endpoint_url=endpoint,
aws_access_key_id="testing",
aws_secret_access_key="testing",
path_style=True))
return Workspace({"/data": s3}, mode=MountMode.WRITE)
def _zip_bytes() -> bytes:
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as zf:
zf.writestr("inner/z.txt", "zip content\n")
return buf.getvalue()
def _capture_io(ws: Workspace) -> list:
captured: list = []
orig = ws._dispatcher.apply_io
async def recording(result, records=None):
captured.append(result)
return await orig(result, records=records)
ws._dispatcher.apply_io = recording
return captured
def _assert_single_prefix(captured: list) -> None:
for result in captured:
keys = (list(result.writes) + list(result.reads) + list(result.cache))
for key in keys:
if key.startswith("/dev/"):
continue
assert key.startswith("/data/"), key
assert not key.startswith("/data/data/"), key
@pytest.mark.parametrize("cmd,stdin", [
("tee /data/t.txt > /dev/null", b"x\ny\n"),
("csplit -f /data/cs_ /data/seed.txt 2", None),
("unzip /data/a.zip -d /data/exout", None),
("cp /data/seed.txt /data/copy.txt", None),
("grep x /data/seed.txt > /data/red.txt", None),
("cat /data/seed.txt >> /data/app.txt", None),
("cat /data/seed.txt | tee /data/piped.txt > /dev/null", None),
("sed s/x/z/ /data/seed.txt > /data/s1.txt && cat /data/s1.txt"
" > /data/s2.txt", None),
])
def test_ram_io_keys_single_prefixed(cmd, stdin):
ws = Workspace({"/data": RAMResource()}, mode=MountMode.WRITE)
async def run():
await ws.execute("tee /data/seed.txt > /dev/null", stdin=b"x\ny\n")
await ws.execute("tee /data/a.zip > /dev/null", stdin=_zip_bytes())
captured = _capture_io(ws)
result = await ws.execute(cmd, stdin=stdin)
assert result.exit_code == 0, await result.stderr_str()
_assert_single_prefix(captured)
await ws.close()
asyncio.run(run())
def test_ram_stderr_redirect_records_mount_relative_key():
ws = Workspace({"/data": RAMResource()}, mode=MountMode.WRITE)
async def run():
captured = _capture_io(ws)
result = await ws.execute("cat /data/missing.txt 2> /data/err.txt")
assert result.exit_code != 0
_assert_single_prefix(captured)
back = await ws.execute("cat /data/err.txt")
assert back.exit_code == 0
assert "missing.txt" in await back.stdout_str()
await ws.close()
asyncio.run(run())
def test_ram_csplit_writes_parts_inside_mount():
ws = Workspace({"/data": RAMResource()}, mode=MountMode.WRITE)
async def run():
await ws.execute("tee /data/seed.txt > /dev/null", stdin=b"x\ny\n")
result = await ws.execute("csplit -f /data/cs_ /data/seed.txt 2")
assert result.exit_code == 0, await result.stderr_str()
part = await ws.execute("cat /data/cs_00")
assert part.exit_code == 0
assert await part.stdout_str() == "x\n"
await ws.close()
asyncio.run(run())
def test_s3_io_keys_single_prefixed(s3_endpoint):
ws = _s3_workspace(s3_endpoint, "key-prefix-test")
async def run():
captured = _capture_io(ws)
await ws.execute("tee /data/t.txt > /dev/null", stdin=b"x\ny\n")
for cmd in (
"touch /data/new.txt",
"mkdir -p /data/newdir",
"csplit -f /data/cs_ /data/t.txt 2",
):
result = await ws.execute(cmd)
assert result.exit_code == 0, await result.stderr_str()
_assert_single_prefix(captured)
await ws.close()
asyncio.run(run())
def test_s3_redirect_write_invalidates_listed_dir(s3_endpoint):
ws = _s3_workspace(s3_endpoint, "key-redirect-test")
async def run():
await ws.execute("tee /data/a.txt > /dev/null", stdin=b"x\ny\n")
await ws.execute("ls -1 /data/")
await ws.execute("grep x /data/a.txt > /data/red.txt")
await ws.execute("cat /data/a.txt | tee /data/piped.txt > /dev/null")
listing = await (await ws.execute("ls -1 /data/")).stdout_str()
assert "red.txt" in listing
assert "piped.txt" in listing
back = await ws.execute("cat /data/red.txt")
assert await back.stdout_str() == "x\n"
await ws.close()
asyncio.run(run())
def test_s3_touch_invalidates_listed_dir(s3_endpoint):
ws = _s3_workspace(s3_endpoint, "key-invalidate-test")
async def run():
await ws.execute("tee /data/a.txt > /dev/null", stdin=b"a\n")
await ws.execute("ls -1 /data/")
await ws.execute("touch /data/late.txt")
result = await ws.execute("rm /data/late.txt")
assert result.exit_code == 0, await result.stderr_str()
gone = await ws.execute("cat /data/late.txt")
assert gone.exit_code != 0
await ws.close()
asyncio.run(run())
@@ -0,0 +1,475 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
"""Mount-root protection tests.
Covers two related behaviors enforced by the command dispatcher
(`mirage/workspace/executor/command.py`):
1. **Write rule** — destructive/conflicting commands targeting a mount
root (`rm /r2`, `mv /r2 /x`, `mkdir /r2`, `touch /r2`, `ln s /r2`)
are refused with Unix-style error messages, instead of silently
modifying the underlying resource.
2. **Read fan-out** — traversal commands (`find`, `tree`, `du`,
`grep -r`, `ls -R`) on a path at or above mount roots run across
each affected mount and concatenate output, so users see
contents from every mount instead of only the parent's resource.
Together these make mount roots behave like first-class directories
that the user can navigate but not accidentally destroy.
"""
import asyncio
from mirage.resource.ram import RAMResource
from mirage.types import MountMode
from mirage.workspace import Workspace
# ── fixtures ──────────────────────────────────────
def _ws_two_mounts() -> Workspace:
return Workspace({
"/r2": (RAMResource(), MountMode.WRITE),
"/ram": (RAMResource(), MountMode.WRITE),
})
def _ws_nested() -> Workspace:
return Workspace({
"/data": (RAMResource(), MountMode.WRITE),
"/data/inner": (RAMResource(), MountMode.WRITE),
})
async def _exec(ws: Workspace, cmd: str):
return await ws.execute(cmd)
def _run(coro):
return asyncio.run(coro)
# ════════════════════════════════════════════════════════════════════
# Write rule
# ════════════════════════════════════════════════════════════════════
# ── rm ─────────────────────────────────────────────
def test_rm_refuses_mount_root():
async def go():
ws = _ws_two_mounts()
r = await _exec(ws, "rm /r2")
assert r.exit_code == 1
assert b"Device or resource busy" in (r.stderr or b"")
assert b"/r2" in (r.stderr or b"")
_run(go())
def test_rmdir_refuses_mount_root():
async def go():
ws = _ws_two_mounts()
r = await _exec(ws, "rmdir /r2")
assert r.exit_code == 1
assert b"Device or resource busy" in (r.stderr or b"")
assert b"/r2" in (r.stderr or b"")
_run(go())
def test_rm_rf_refuses_mount_root():
async def go():
ws = _ws_two_mounts()
r = await _exec(ws, "rm -rf /r2")
assert r.exit_code == 1
assert b"Device or resource busy" in (r.stderr or b"")
_run(go())
def test_rm_rf_refuses_mount_root_with_trailing_slash():
async def go():
ws = _ws_two_mounts()
r = await _exec(ws, "rm -rf /r2/")
assert r.exit_code == 1
assert b"Device or resource busy" in (r.stderr or b"")
_run(go())
def test_rm_inside_mount_still_works():
async def go():
ws = _ws_two_mounts()
await _exec(ws, "touch /r2/file")
r = await _exec(ws, "rm /r2/file")
assert r.exit_code == 0
_run(go())
def test_rm_rf_inside_mount_still_works():
async def go():
ws = _ws_two_mounts()
await _exec(ws, "mkdir /r2/sub")
await _exec(ws, "touch /r2/sub/x")
r = await _exec(ws, "rm -rf /r2/sub")
assert r.exit_code == 0
_run(go())
def test_rm_does_not_remove_mount_after_refusal():
async def go():
ws = _ws_two_mounts()
await _exec(ws, "touch /r2/keep")
r = await _exec(ws, "rm -rf /r2")
assert r.exit_code == 1
# Mount is still mounted and contents preserved.
ls = await _exec(ws, "ls /r2")
assert b"keep" in (ls.stdout or b"")
_run(go())
# ── mv ─────────────────────────────────────────────
def test_mv_refuses_mount_root_as_source():
async def go():
ws = _ws_two_mounts()
r = await _exec(ws, "mv /r2 /elsewhere")
assert r.exit_code == 1
assert b"Device or resource busy" in (r.stderr or b"")
_run(go())
def test_mv_into_mount_root_is_allowed():
# `mv /src /r2` means "move /src INTO /r2" when /r2 is a directory.
# The guard only fires on the SOURCE being a mount root.
async def go():
ws = _ws_two_mounts()
await _exec(ws, "touch /ram/payload")
r = await _exec(ws, "mv /ram/payload /r2/payload")
assert r.exit_code == 0
_run(go())
# ── mkdir ──────────────────────────────────────────
def test_mkdir_refuses_existing_mount_root():
async def go():
ws = _ws_two_mounts()
r = await _exec(ws, "mkdir /r2")
assert r.exit_code == 1
assert b"File exists" in (r.stderr or b"")
_run(go())
def test_mkdir_dash_p_on_mount_root_is_idempotent():
# GNU mkdir -p does not error if the directory already exists.
# Mount roots already exist, so -p must succeed silently.
async def go():
ws = _ws_two_mounts()
r = await _exec(ws, "mkdir -p /r2")
assert r.exit_code == 0
assert (r.stderr or b"") == b""
_run(go())
def test_mkdir_inside_mount_is_allowed():
async def go():
ws = _ws_two_mounts()
r = await _exec(ws, "mkdir /r2/newdir")
assert r.exit_code == 0
_run(go())
# ── touch ─────────────────────────────────────────
def test_touch_refuses_mount_root():
async def go():
ws = _ws_two_mounts()
r = await _exec(ws, "touch /r2")
assert r.exit_code == 1
assert b"Is a directory" in (r.stderr or b"")
_run(go())
def test_touch_inside_mount_is_allowed():
async def go():
ws = _ws_two_mounts()
r = await _exec(ws, "touch /r2/newfile")
assert r.exit_code == 0
_run(go())
# ── ln ────────────────────────────────────────────
def test_ln_refuses_mount_root_as_link_name():
async def go():
ws = _ws_two_mounts()
await _exec(ws, "touch /ram/source")
r = await _exec(ws, "ln /ram/source /r2")
assert r.exit_code == 1
assert b"File exists" in (r.stderr or b"")
_run(go())
def test_ln_s_refuses_mount_root_as_link_name():
async def go():
ws = _ws_two_mounts()
r = await _exec(ws, "ln -s /ram/source /r2")
assert r.exit_code == 1
assert b"File exists" in (r.stderr or b"")
_run(go())
def test_ln_inside_a_single_mount_is_not_blocked_by_guard():
# ln within one mount should not be refused by the mount-root guard.
# (Whether the underlying resource supports ln is a separate concern;
# the guard's job is only to reject mount-root targets.)
async def go():
ws = _ws_two_mounts()
await _exec(ws, "touch /r2/source")
r = await _exec(ws, "ln -s /r2/source /r2/link")
# Either ln succeeds, or the resource doesn't support it. The
# guard's "File exists" message must NOT appear because /r2/link
# is not a mount root.
assert b"File exists" not in (r.stderr or b"")
_run(go())
# ── nested mount as a target ──────────────────────
def test_rm_refuses_nested_mount_root():
async def go():
ws = _ws_nested()
r = await _exec(ws, "rm -rf /data/inner")
assert r.exit_code == 1
assert b"Device or resource busy" in (r.stderr or b"")
_run(go())
def test_rm_inside_nested_mount_still_works():
async def go():
ws = _ws_nested()
await _exec(ws, "touch /data/inner/x")
r = await _exec(ws, "rm /data/inner/x")
assert r.exit_code == 0
_run(go())
def test_rm_inside_outer_mount_still_works():
async def go():
ws = _ws_nested()
await _exec(ws, "touch /data/outer-file")
r = await _exec(ws, "rm /data/outer-file")
assert r.exit_code == 0
_run(go())
# ════════════════════════════════════════════════════════════════════
# Read fan-out
# ════════════════════════════════════════════════════════════════════
def test_find_root_lists_mounts_at_depth_one():
# The original user-reported bug: `find / -maxdepth 1 -mindepth 1
# -type d` should list mount prefixes as directories.
async def go():
ws = _ws_two_mounts()
r = await _exec(ws, "find / -maxdepth 1 -mindepth 1 -type d")
assert r.exit_code == 0
out = (r.stdout or b"").decode()
assert "/r2" in out
assert "/ram" in out
_run(go())
def test_find_root_descends_into_each_mount():
async def go():
ws = _ws_two_mounts()
await _exec(ws, "touch /r2/a")
await _exec(ws, "touch /ram/b")
r = await _exec(ws, "find /")
out = (r.stdout or b"").decode()
assert "/r2/a" in out
assert "/ram/b" in out
_run(go())
def test_find_inside_one_mount_is_unchanged():
# When the path is inside a single mount with no descendants,
# fan-out does NOT trigger and the command behaves normally.
async def go():
ws = _ws_two_mounts()
await _exec(ws, "touch /r2/only-a")
r = await _exec(ws, "find /r2")
out = (r.stdout or b"").decode()
assert "/r2/only-a" in out
# Must not contain entries from /ram
assert "/ram" not in out
_run(go())
def test_find_with_no_descendants_does_not_fan_out():
# Single mount; no descendants under /. Fan-out path must not run.
async def go():
ws = Workspace({"/r2": (RAMResource(), MountMode.WRITE)})
await _exec(ws, "touch /r2/file")
r = await _exec(ws, "find /r2")
assert r.exit_code == 0
out = (r.stdout or b"").decode()
assert "/r2/file" in out
_run(go())
def test_find_root_with_nested_mounts():
# Both /data and /data/inner are mounts. find / must surface
# files from both.
async def go():
ws = _ws_nested()
await _exec(ws, "touch /data/outer-file")
await _exec(ws, "touch /data/inner/inner-file")
r = await _exec(ws, "find /")
out = (r.stdout or b"").decode()
assert "/data/outer-file" in out
assert "/data/inner/inner-file" in out
_run(go())
def test_find_filters_parent_paths_under_descendant_mount():
# If the parent mount has a key whose path overlaps a descendant
# mount's prefix, the descendant mount's content is authoritative.
# The parent's shadowed content must not duplicate into the
# output under the descendant prefix.
async def go():
ws = _ws_nested()
# Put a key in the parent /data resource that lives at the
# SAME path as the /data/inner mount root.
await _exec(ws, "mkdir /data/inner"
) # blocked: /data/inner is a mount root → File exists
# Instead, write under the inner mount and verify no parent leak.
await _exec(ws, "touch /data/inner/from-inner")
r = await _exec(ws, "find /data")
out = (r.stdout or b"").decode()
# /data/inner/from-inner exists from the inner mount.
assert "/data/inner/from-inner" in out
_run(go())
def test_find_maxdepth_skips_too_deep_mount():
# /data/inner is at depth 2 from /. With -maxdepth 1, the inner
# mount must NOT be included.
async def go():
ws = _ws_nested()
await _exec(ws, "touch /data/inner/x")
await _exec(ws, "touch /data/outer-file")
r = await _exec(ws, "find / -maxdepth 1")
out = (r.stdout or b"").decode()
# /data should appear (depth 1)
assert "/data" in out
# /data/inner/x and /data/outer-file are too deep
assert "/data/inner/x" not in out
_run(go())
def test_grep_recursive_root_fans_out():
async def go():
ws = _ws_two_mounts()
await _exec(ws, "sh -c 'echo needle > /r2/a.txt'")
await _exec(ws, "sh -c 'echo other > /ram/b.txt'")
await _exec(ws, "sh -c 'echo needle > /ram/c.txt'")
r = await _exec(ws, "grep -r needle /")
out = (r.stdout or b"").decode()
assert "/r2/a.txt" in out
assert "/ram/c.txt" in out
assert "/ram/b.txt" not in out
_run(go())
def test_du_root_fans_out():
async def go():
ws = _ws_two_mounts()
await _exec(ws, "sh -c 'echo content > /r2/file'")
await _exec(ws, "sh -c 'echo other > /ram/file'")
r = await _exec(ws, "du /")
out = (r.stdout or b"").decode()
# Each mount contributes some line containing its prefix.
assert "/r2" in out
assert "/ram" in out
_run(go())
# ── ls / unchanged ────────────────────────────────
def test_ls_root_still_lists_mounts():
# The pre-existing ls injection path must still work — mounts
# are visible as folder entries in `ls /`.
async def go():
ws = _ws_two_mounts()
r = await _exec(ws, "ls /")
out = (r.stdout or b"").decode()
assert "r2" in out
assert "ram" in out
_run(go())
@@ -0,0 +1,38 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import importlib
import mirage.workspace.types as t
from mirage.workspace import Workspace
def test_workspace_has_no_sync_attribute():
assert not hasattr(Workspace, "sync"), (
"Workspace.sync() was a no-op (DirtyTracker always empty); "
"should be removed in Phase 1 cleanup.")
def test_sync_policy_enum_removed():
assert not hasattr(t, "SyncPolicy"), "SyncPolicy removed in Phase 1."
assert not hasattr(t, "SyncResult"), "SyncResult removed in Phase 1."
assert not hasattr(t, "Inode"), "Inode removed in Phase 1."
def test_dirty_tracker_module_removed():
try:
importlib.import_module("mirage.workspace.tracker")
except ModuleNotFoundError:
return
raise AssertionError("mirage.workspace.tracker should be deleted.")
@@ -0,0 +1,362 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
from mirage.resource.ram import RAMResource
from mirage.types import MountMode
from mirage.workspace import Workspace
def _make_ws():
ram1 = RAMResource()
ram2 = RAMResource()
ram1._store.files["/f.txt"] = b"aaa\n"
return Workspace(
{
"/a/": (ram1, MountMode.WRITE),
"/b/": (ram2, MountMode.WRITE)
}, )
def _make_numbered_ws():
ram = RAMResource()
ram._store.files["/f.txt"] = b"1\n2\n"
ram._store.files["/g.txt"] = b"3\n4\n"
ram._store.files["/h.txt"] = b"hello\n"
return Workspace({"/a/": (ram, MountMode.WRITE)}, )
def _run(ws, cmd):
async def _inner():
io = await ws.execute(cmd)
return await io.stdout_str(), await io.stderr_str(), io.exit_code
return asyncio.run(_inner())
# ── single-mount: good + missing keeps partial output, GNU-style ──
def test_cat_good_then_missing():
out, err, code = _run(_make_ws(), "cat /a/f.txt /a/missing.txt")
assert out == "aaa\n"
assert err == "cat: /a/missing.txt: No such file or directory\n"
assert code == 1
def test_cat_missing_then_good():
out, err, code = _run(_make_ws(), "cat /a/missing.txt /a/f.txt")
assert out == "aaa\n"
assert err == "cat: /a/missing.txt: No such file or directory\n"
assert code == 1
def test_cat_all_missing_reports_each():
out, err, code = _run(_make_ws(), "cat /a/m1.txt /a/m2.txt")
assert out == ""
assert err == ("cat: /a/m1.txt: No such file or directory\n"
"cat: /a/m2.txt: No such file or directory\n")
assert code == 1
def test_wc_good_then_missing_keeps_total():
out, err, code = _run(_make_ws(), "wc -l /a/f.txt /a/missing.txt")
assert out == "1 /a/f.txt\n1 total\n"
assert err == "wc: /a/missing.txt: No such file or directory\n"
assert code == 1
def test_wc_all_missing_zero_total():
out, err, code = _run(_make_ws(), "wc -l /a/m1.txt /a/m2.txt")
assert out == "0 total\n"
assert err == ("wc: /a/m1.txt: No such file or directory\n"
"wc: /a/m2.txt: No such file or directory\n")
assert code == 1
def test_head_good_then_missing_keeps_banner():
out, err, code = _run(_make_ws(), "head -n 1 /a/f.txt /a/missing.txt")
assert out == "==> /a/f.txt <==\naaa\n"
assert err == "head: /a/missing.txt: No such file or directory\n"
assert code == 1
def test_head_missing_first_no_leading_blank():
out, err, code = _run(_make_ws(), "head -n 1 /a/missing.txt /a/f.txt")
assert out == "==> /a/f.txt <==\naaa\n"
assert err == "head: /a/missing.txt: No such file or directory\n"
assert code == 1
def test_tail_good_then_missing_keeps_banner():
out, err, code = _run(_make_ws(), "tail -n 1 /a/f.txt /a/missing.txt")
assert out == "==> /a/f.txt <==\naaa\n"
assert err == "tail: /a/missing.txt: No such file or directory\n"
assert code == 1
def test_single_missing_operand_unchanged():
out, err, code = _run(_make_ws(), "cat /a/missing.txt")
assert out == ""
assert err == "cat: /a/missing.txt: No such file or directory\n"
assert code == 1
# ── cross-mount: same bytes as single-mount ──
def test_cross_cat_good_then_missing():
out, err, code = _run(_make_ws(), "cat /a/f.txt /b/missing.txt")
assert out == "aaa\n"
assert err == "cat: /b/missing.txt: No such file or directory\n"
assert code == 1
def test_cross_wc_good_then_missing_keeps_total():
out, err, code = _run(_make_ws(), "wc -l /a/f.txt /b/missing.txt")
assert out == "1 /a/f.txt\n1 total\n"
assert err == "wc: /b/missing.txt: No such file or directory\n"
assert code == 1
def test_cross_head_good_then_missing_keeps_banner():
out, err, code = _run(_make_ws(), "head -n 1 /a/f.txt /b/missing.txt")
assert out == "==> /a/f.txt <==\naaa\n"
assert err == "head: /b/missing.txt: No such file or directory\n"
assert code == 1
def test_cross_tail_good_then_missing_keeps_banner():
out, err, code = _run(_make_ws(), "tail -n 1 /a/f.txt /b/missing.txt")
assert out == "==> /a/f.txt <==\naaa\n"
assert err == "tail: /b/missing.txt: No such file or directory\n"
assert code == 1
# ── every operand is processed, not just the first (paths[0] bugs) ──
def test_cut_processes_all_operands():
out, err, code = _run(_make_numbered_ws(), "cut -c1 /a/f.txt /a/g.txt")
assert out == "1\n2\n3\n4\n"
assert err == ""
assert code == 0
def test_tac_reverses_each_operand():
out, err, code = _run(_make_numbered_ws(), "tac /a/f.txt /a/g.txt")
assert out == "2\n1\n4\n3\n"
assert err == ""
assert code == 0
def test_nl_numbering_continues_across_operands():
out, err, code = _run(_make_numbered_ws(), "nl /a/f.txt /a/g.txt")
assert out == " 1\t1\n 2\t2\n 3\t3\n 4\t4\n"
assert err == ""
assert code == 0
def test_strings_scans_all_operands():
ws = _make_numbered_ws()
_run(ws, "printf 'worlds\\n' > /a/h2.txt")
out, err, code = _run(ws, "strings /a/h.txt /a/h2.txt")
assert out == "hello\nworlds\n"
assert err == ""
assert code == 0
def test_zcat_concatenates_all_operands():
ws = _make_numbered_ws()
_run(
ws, "printf 'z\\n' > /a/z1.txt && printf 'y\\n' > /a/z2.txt"
" && gzip /a/z1.txt /a/z2.txt")
out, err, code = _run(ws, "zcat /a/z1.txt.gz /a/z2.txt.gz")
assert out == "z\ny\n"
assert err == ""
assert code == 0
# ── the rest of the read family keeps partial output past missing ──
def test_nl_good_then_missing():
out, err, code = _run(_make_numbered_ws(), "nl /a/f.txt /a/missing.txt")
assert out == " 1\t1\n 2\t2\n"
assert err == "nl: /a/missing.txt: No such file or directory\n"
assert code == 1
def test_nl_all_missing_reports_each():
out, err, code = _run(_make_numbered_ws(), "nl /a/m1.txt /a/m2.txt")
assert out == ""
assert err == ("nl: /a/m1.txt: No such file or directory\n"
"nl: /a/m2.txt: No such file or directory\n")
assert code == 1
def test_md5_good_then_missing():
out, err, code = _run(_make_numbered_ws(), "md5 /a/f.txt /a/missing.txt")
assert out == "6ddb4095eb719e2a9f0a3f95677d24e0 /a/f.txt\n"
assert err == "md5: /a/missing.txt: No such file or directory\n"
assert code == 1
def test_sha256sum_good_then_missing():
out, err, code = _run(_make_numbered_ws(),
"sha256sum /a/f.txt /a/missing.txt")
assert out == ("a6e2b7a040683432de03a18fd8a1939a2fdf8258"
"5b364bfc874bdd4095c4cae1 /a/f.txt\n")
assert err == "sha256sum: /a/missing.txt: No such file or directory\n"
assert code == 1
def test_tac_good_then_missing():
out, err, code = _run(_make_numbered_ws(), "tac /a/f.txt /a/missing.txt")
assert out == "2\n1\n"
assert err == "tac: /a/missing.txt: No such file or directory\n"
assert code == 1
def test_rev_good_then_missing():
out, err, code = _run(_make_numbered_ws(), "rev /a/f.txt /a/missing.txt")
assert out == "1\n2\n"
assert err == "rev: /a/missing.txt: No such file or directory\n"
assert code == 1
def test_cut_good_then_missing():
out, err, code = _run(_make_numbered_ws(),
"cut -c1 /a/f.txt /a/missing.txt")
assert out == "1\n2\n"
assert err == "cut: /a/missing.txt: No such file or directory\n"
assert code == 1
def test_expand_good_then_missing():
out, err, code = _run(_make_numbered_ws(),
"expand /a/f.txt /a/missing.txt")
assert out == "1\n2\n"
assert err == "expand: /a/missing.txt: No such file or directory\n"
assert code == 1
def test_unexpand_good_then_missing():
out, err, code = _run(_make_numbered_ws(),
"unexpand /a/f.txt /a/missing.txt")
assert out == "1\n2\n"
assert err == "unexpand: /a/missing.txt: No such file or directory\n"
assert code == 1
def test_fold_good_then_missing():
out, err, code = _run(_make_numbered_ws(), "fold /a/f.txt /a/missing.txt")
assert out == "1\n2\n"
assert err == "fold: /a/missing.txt: No such file or directory\n"
assert code == 1
def test_fmt_good_then_missing():
out, err, code = _run(_make_numbered_ws(), "fmt /a/f.txt /a/missing.txt")
assert out == "1 2\n"
assert err == "fmt: /a/missing.txt: No such file or directory\n"
assert code == 1
def test_strings_good_then_missing():
out, err, code = _run(_make_numbered_ws(),
"strings /a/h.txt /a/missing.txt")
assert out == "hello\n"
assert err == "strings: /a/missing.txt: No such file or directory\n"
assert code == 1
def test_zcat_good_then_missing():
ws = _make_numbered_ws()
_run(ws, "printf 'z\\n' > /a/z1.txt && gzip /a/z1.txt")
out, err, code = _run(ws, "zcat /a/z1.txt.gz /a/missing.gz")
assert out == "z\n"
assert err == "zcat: /a/missing.gz: No such file or directory\n"
assert code == 1
def test_sort_still_aborts_on_missing():
# GNU sort needs all input before emitting anything, so no partial
# output; the repo reports the operand and exits 1 (GNU exits 2).
out, err, code = _run(_make_numbered_ws(), "sort /a/f.txt /a/missing.txt")
assert out == ""
assert err == "sort: /a/missing.txt: No such file or directory\n"
assert code == 1
def _make_cross_numbered_ws():
ram1 = RAMResource()
ram2 = RAMResource()
ram1._store.files["/f.txt"] = b"1\n2\n"
return Workspace(
{
"/a/": (ram1, MountMode.WRITE),
"/b/": (ram2, MountMode.WRITE)
}, )
def test_cross_nl_reports_own_name():
# STREAM commands fetch operand bytes through a native cat sub-run; the
# error line must still carry the command's own name, like single-mount.
out, err, code = _run(_make_cross_numbered_ws(),
"nl /a/f.txt /b/missing.txt")
assert out == " 1\t1\n 2\t2\n"
assert err == "nl: /b/missing.txt: No such file or directory\n"
assert code == 1
def test_cross_md5_good_then_missing():
out, err, code = _run(_make_cross_numbered_ws(),
"md5 /a/f.txt /b/missing.txt")
assert out == "6ddb4095eb719e2a9f0a3f95677d24e0 /a/f.txt\n"
assert err == "md5: /b/missing.txt: No such file or directory\n"
assert code == 1
def test_stat_good_then_missing_keeps_row():
out, err, code = _run(_make_ws(), "stat /a/f.txt /a/missing.txt")
assert "name=f.txt" in out
assert err == "stat: /a/missing.txt: No such file or directory\n"
assert code == 1
def test_sed_good_then_missing_keeps_output():
out, err, code = _run(_make_numbered_ws(),
"sed s/1/X/ /a/f.txt /a/missing.txt")
assert out == "X\n2\n"
assert err == "sed: /a/missing.txt: No such file or directory\n"
assert code == 1
def test_cross_sed_good_then_missing_keeps_output():
out, err, code = _run(_make_cross_numbered_ws(),
"sed s/1/X/ /a/f.txt /b/missing.txt")
assert out == "X\n2\n"
assert err == "sed: /b/missing.txt: No such file or directory\n"
assert code == 1
def test_cross_sort_aborts_like_single_mount():
out, err, code = _run(_make_cross_numbered_ws(),
"sort /a/f.txt /b/missing.txt")
assert out == ""
assert err == "sort: /b/missing.txt: No such file or directory\n"
assert code == 1
@@ -0,0 +1,84 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
import pytest
from mirage import MountMode, Workspace
from mirage.resource.ram import RAMResource
from mirage.workspace.workspace import DEFAULT_SESSION_ID
async def _slow_stdin():
await asyncio.sleep(5)
yield b"late\n"
async def _quick_stdin():
yield b"hi\n"
async def _probed_stdin(flag):
try:
await asyncio.sleep(5)
yield b"late\n"
finally:
flag["closed"] = True
async def _multiline_stdin():
for i in range(1000):
yield f"line{i}\n".encode()
def _ws():
return Workspace({"/data": RAMResource()}, mode=MountMode.WRITE)
@pytest.mark.asyncio
async def test_pipeline_budget_bounds_slow_final_stage():
ws = _ws()
ws._session_mgr.get(DEFAULT_SESSION_ID).pipeline_timeout_seconds = 0.1
r = await ws.execute("cat | cat", stdin=_slow_stdin())
assert r.exit_code == 124
assert "pipeline: timed out after 0.1s" in (await r.stderr_str())
@pytest.mark.asyncio
async def test_pipeline_without_budget_is_unbounded():
ws = _ws()
r = await ws.execute("cat | cat", stdin=_quick_stdin())
assert r.exit_code == 0
assert (await r.stdout_str()) == "hi\n"
@pytest.mark.asyncio
async def test_pipeline_budget_tears_down_upstream_producer():
ws = _ws()
ws._session_mgr.get(DEFAULT_SESSION_ID).pipeline_timeout_seconds = 0.1
flag = {"closed": False}
r = await ws.execute("cat | cat", stdin=_probed_stdin(flag))
assert r.exit_code == 124
assert flag["closed"]
@pytest.mark.asyncio
async def test_early_finish_reports_downstream_code_not_sigpipe():
ws = _ws()
session = ws._session_mgr.get(DEFAULT_SESSION_ID)
session.shell_options["pipefail"] = True
r = await ws.execute("cat | head -n1", stdin=_multiline_stdin())
assert r.exit_code == 0
assert (await r.stdout_str()) == "line0\n"
@@ -0,0 +1,308 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
import pytest
from mirage.resource.disk import DiskResource
from mirage.resource.ram import RAMResource
from mirage.types import DEFAULT_SESSION_ID, MountMode
from mirage.workspace import Workspace
def _run(coro):
return asyncio.run(coro)
def _stdout(io):
if io.stdout is None:
return b""
if isinstance(io.stdout, bytes):
return io.stdout
if isinstance(io.stdout, memoryview):
return bytes(io.stdout)
return b""
# ═══════════════════════════════════════════════
# RAM resource integration
# ═══════════════════════════════════════════════
def _ram_ws():
p = RAMResource()
p._store.files["/hello.txt"] = b"hello world\n"
p._store.files["/data.csv"] = b"name,age\nalice,30\nbob,25\n"
p._store.dirs.add("/sub")
p._store.files["/sub/nested.txt"] = b"nested content\n"
ws = Workspace(resources={"/ram/": (p, MountMode.WRITE)}, )
ws.get_session(DEFAULT_SESSION_ID).cwd = "/ram"
return ws
def test_ram_cat():
ws = _ram_ws()
io = _run(ws.execute("cat /ram/hello.txt"))
assert io.exit_code == 0
assert b"hello world" in _stdout(io)
def test_ram_grep():
ws = _ram_ws()
io = _run(ws.execute("grep alice /ram/data.csv"))
assert io.exit_code == 0
assert b"alice" in _stdout(io)
def test_ram_pipeline():
ws = _ram_ws()
io = _run(ws.execute("cat /ram/data.csv | grep alice | wc -l"))
assert io.exit_code == 0
assert b"1" in _stdout(io)
def test_ram_redirect_write():
ws = _ram_ws()
_run(ws.execute("echo test > /ram/out.txt"))
io = _run(ws.execute("cat /ram/out.txt"))
assert b"test" in _stdout(io)
def test_ram_ls():
ws = _ram_ws()
io = _run(ws.execute("ls /ram/"))
assert io.exit_code == 0
def test_ram_head():
ws = _ram_ws()
io = _run(ws.execute("head -n 1 /ram/data.csv"))
assert io.exit_code == 0
assert b"name" in _stdout(io)
def test_ram_awk():
ws = _ram_ws()
io = _run(ws.execute("awk -F, '{print $1}' /ram/data.csv"))
assert io.exit_code == 0
assert b"alice" in _stdout(io)
def test_ram_sed():
ws = _ram_ws()
io = _run(ws.execute("sed 's/alice/ALICE/' /ram/data.csv"))
assert b"ALICE" in _stdout(io)
def test_ram_sort():
ws = _ram_ws()
io = _run(ws.execute("cat /ram/data.csv | sort"))
assert io.exit_code == 0
# ═══════════════════════════════════════════════
# Disk resource integration
# ═══════════════════════════════════════════════
@pytest.fixture
def disk_ws(tmp_path):
data_dir = tmp_path / "data"
data_dir.mkdir()
(data_dir / "hello.txt").write_bytes(b"hello from disk\n")
(data_dir / "nums.txt").write_bytes(b"3\n1\n2\n")
(data_dir / "report.csv").write_bytes(b"name,age\nalice,30\nbob,25\n")
sub = data_dir / "sub"
sub.mkdir()
(sub / "nested.txt").write_bytes(b"nested\n")
p = DiskResource(root=str(data_dir))
ws = Workspace(resources={"/disk/": (p, MountMode.WRITE)}, )
ws.get_session(DEFAULT_SESSION_ID).cwd = "/disk"
return ws
def test_disk_cat(disk_ws):
io = _run(disk_ws.execute("cat /disk/hello.txt"))
assert io.exit_code == 0
assert b"hello from disk" in _stdout(io)
def test_disk_grep(disk_ws):
io = _run(disk_ws.execute("grep alice /disk/report.csv"))
assert io.exit_code == 0
assert b"alice" in _stdout(io)
def test_disk_pipeline(disk_ws):
io = _run(disk_ws.execute("cat /disk/report.csv | grep alice | wc -l"))
assert io.exit_code == 0
assert b"1" in _stdout(io)
def test_disk_ls(disk_ws):
io = _run(disk_ws.execute("ls /disk/"))
assert io.exit_code == 0
def test_disk_head(disk_ws):
io = _run(disk_ws.execute("head -n 1 /disk/report.csv"))
assert io.exit_code == 0
assert b"name" in _stdout(io)
def test_disk_sort(disk_ws):
io = _run(disk_ws.execute("sort -n /disk/nums.txt"))
assert io.exit_code == 0
lines = _stdout(io).decode().strip().split("\n")
assert lines == ["1", "2", "3"]
def test_disk_redirect_write(disk_ws):
_run(disk_ws.execute("echo written > /disk/out.txt"))
io = _run(disk_ws.execute("cat /disk/out.txt"))
assert b"written" in _stdout(io)
def test_disk_nested_cat(disk_ws):
io = _run(disk_ws.execute("cat /disk/sub/nested.txt"))
assert io.exit_code == 0
assert b"nested" in _stdout(io)
def test_disk_awk(disk_ws):
io = _run(disk_ws.execute("awk -F, '{print $1}' /disk/report.csv"))
assert io.exit_code == 0
assert b"alice" in _stdout(io)
def test_disk_sed(disk_ws):
io = _run(disk_ws.execute("sed 's/bob/BOB/' /disk/report.csv"))
assert b"BOB" in _stdout(io)
# ═══════════════════════════════════════════════
# Cross-resource: RAM + Disk
# ═══════════════════════════════════════════════
@pytest.fixture
def multi_ws(tmp_path):
data_dir = tmp_path / "diskdata"
data_dir.mkdir()
(data_dir / "disk_file.txt").write_bytes(b"from disk\n")
ram = RAMResource()
ram._store.files["/ram_file.txt"] = b"from ram\n"
disk = DiskResource(root=str(data_dir))
ws = Workspace(resources={
"/ram/": (ram, MountMode.WRITE),
"/disk/": (disk, MountMode.WRITE),
}, )
ws.get_session(DEFAULT_SESSION_ID).cwd = "/ram"
return ws
def test_cross_cat_ram(multi_ws):
io = _run(multi_ws.execute("cat /ram/ram_file.txt"))
assert b"from ram" in _stdout(io)
def test_cross_cat_disk(multi_ws):
io = _run(multi_ws.execute("cat /disk/disk_file.txt"))
assert b"from disk" in _stdout(io)
def test_cross_pipeline(multi_ws):
"""Read from RAM, pipe through commands, write to Disk."""
_run(
multi_ws.execute(
"cat /ram/ram_file.txt | tr 'a-z' 'A-Z' > /disk/upper.txt"))
io = _run(multi_ws.execute("cat /disk/upper.txt"))
assert b"FROM RAM" in _stdout(io)
def test_cross_for_loop(multi_ws):
"""for loop across resources."""
_run(
multi_ws.execute("for f in /ram/ram_file.txt /disk/disk_file.txt; do "
"cat $f; done"))
def test_cross_redirect(multi_ws):
"""Read RAM, write to Disk."""
_run(multi_ws.execute("echo hello > /disk/from_ram.txt"))
io = _run(multi_ws.execute("cat /disk/from_ram.txt"))
assert b"hello" in _stdout(io)
def test_cross_cp_ram_to_disk(multi_ws):
"""cp /ram/file /disk/file → cross-mount copy."""
io = _run(multi_ws.execute("cp /ram/ram_file.txt /disk/copied.txt"))
assert io.exit_code == 0
io = _run(multi_ws.execute("cat /disk/copied.txt"))
assert b"from ram" in _stdout(io)
def test_cross_cp_disk_to_ram(multi_ws):
"""cp /disk/file /ram/file → cross-mount copy."""
io = _run(multi_ws.execute("cp /disk/disk_file.txt /ram/copied.txt"))
assert io.exit_code == 0
io = _run(multi_ws.execute("cat /ram/copied.txt"))
assert b"from disk" in _stdout(io)
def test_cross_mv_ram_to_disk(multi_ws):
"""mv /ram/file /disk/file → cross-mount move."""
_run(multi_ws.execute("echo moveme > /ram/move_src.txt"))
io = _run(multi_ws.execute("mv /ram/move_src.txt /disk/move_dst.txt"))
assert io.exit_code == 0
io = _run(multi_ws.execute("cat /disk/move_dst.txt"))
assert b"moveme" in _stdout(io)
io = _run(multi_ws.execute("cat /ram/move_src.txt"))
assert io.exit_code == 1
def test_cross_diff_same(multi_ws):
"""diff across mounts — identical files."""
_run(multi_ws.execute("echo same > /ram/a.txt"))
_run(multi_ws.execute("echo same > /disk/a.txt"))
io = _run(multi_ws.execute("diff /ram/a.txt /disk/a.txt"))
assert io.exit_code == 0
def test_cross_diff_different(multi_ws):
"""diff across mounts — different files."""
io = _run(multi_ws.execute("diff /ram/ram_file.txt /disk/disk_file.txt"))
assert io.exit_code == 1
out = _stdout(io)
assert b"from ram" in out or b"---" in out
def test_cross_cmp_same(multi_ws):
"""cmp across mounts — identical files."""
_run(multi_ws.execute("echo identical > /ram/c.txt"))
_run(multi_ws.execute("echo identical > /disk/c.txt"))
io = _run(multi_ws.execute("cmp /ram/c.txt /disk/c.txt"))
assert io.exit_code == 0
def test_cross_cmp_different(multi_ws):
"""cmp across mounts — different files."""
io = _run(multi_ws.execute("cmp /ram/ram_file.txt /disk/disk_file.txt"))
assert io.exit_code == 1
assert b"differ" in _stdout(io)
+178
View File
@@ -0,0 +1,178 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage import MountMode, Workspace
from mirage.commands.config import command
from mirage.commands.spec import SPECS
from mirage.io.types import IOResult
from mirage.ops.registry import op
from mirage.resource.ram import RAMResource
@pytest.fixture
def ws():
return Workspace({"/data": RAMResource()}, mode=MountMode.WRITE)
@pytest.fixture
def ws_two_mounts():
return Workspace({
"/a": RAMResource(),
"/b": RAMResource(),
},
mode=MountMode.WRITE)
def test_ws_mounts_returns_all(ws):
mounts = ws.mounts()
prefixes = [m.prefix for m in mounts]
assert "/data/" in prefixes
def test_ws_mount_by_prefix(ws):
m = ws.mount("/data/")
assert m.prefix == "/data/"
def test_commands_introspection(ws):
m = ws.mount("/data/")
cmds = m.commands()
assert isinstance(cmds, dict)
assert "cat" in cmds
assert None in cmds["cat"]
def test_commands_has_filetype_variants(ws):
m = ws.mount("/data/")
cmds = m.commands()
assert len(cmds.get("cat", [])) > 1
def test_registered_ops_introspection(ws):
m = ws.mount("/data/")
ops = m.registered_ops()
assert isinstance(ops, dict)
assert "read" in ops
assert "stat" in ops
def test_register_fns_adds_command(ws):
@command("test_custom", resource="ram", spec=SPECS["cat"])
async def custom(accessor, paths, *texts, **kw):
return b"custom", IOResult()
m = ws.mount("/data/")
assert "test_custom" not in m.commands()
m.register_fns([custom])
assert "test_custom" in m.commands()
def test_register_fns_adds_op(ws):
@op("test_custom_op", resource="ram")
async def custom_op(accessor, scope, **kwargs):
return b"hello"
m = ws.mount("/data/")
assert "test_custom_op" not in m.registered_ops()
m.register_fns([custom_op])
assert "test_custom_op" in m.registered_ops()
def test_unregister_removes_command(ws):
m = ws.mount("/data/")
assert "rm" in m.commands()
m.unregister(["rm"])
assert "rm" not in m.commands()
def test_unregister_removes_all_filetypes(ws):
m = ws.mount("/data/")
cmds = m.commands()
assert len(cmds.get("cat", [])) > 1
m.unregister(["cat"])
assert "cat" not in m.commands()
@pytest.mark.asyncio
async def test_unregister_then_register_works(ws):
m = ws.mount("/data/")
m.unregister(["cat"])
assert "cat" not in m.commands()
@command("cat", resource="ram", spec=SPECS["cat"])
async def custom_cat(accessor, paths, *texts, **kw):
return b"custom cat output", IOResult()
m.register_fns([custom_cat])
assert "cat" in m.commands()
await ws.execute('echo hello | tee /data/hello.txt')
result = await ws.execute("cat /data/hello.txt")
assert result.exit_code == 0
assert b"custom cat output" in result.stdout
def test_register_isolated_per_mount(ws_two_mounts):
ma = ws_two_mounts.mount("/a/")
mb = ws_two_mounts.mount("/b/")
ma.unregister(["rm"])
assert "rm" not in ma.commands()
assert "rm" in mb.commands()
def test_register_fns_isolated_per_mount(ws_two_mounts):
@command("only_on_a", resource="ram", spec=SPECS["cat"])
async def only_a(accessor, paths, *texts, **kw):
return b"a", IOResult()
ws_two_mounts.mount("/a/").register_fns([only_a])
assert "only_on_a" in ws_two_mounts.mount("/a/").commands()
assert "only_on_a" not in ws_two_mounts.mount("/b/").commands()
def test_register_fns_wrong_resource_raises(ws):
@command("s3_only", resource="s3", spec=SPECS["cat"])
async def s3_cmd(accessor, paths, *texts, **kw):
return b"s3", IOResult()
m = ws.mount("/data/")
with pytest.raises(ValueError, match=r"'s3'"):
m.register_fns([s3_cmd])
def test_register_fns_wrong_resource_op_raises(ws):
@op("s3_read", resource="s3")
async def s3_op(accessor, scope, **kwargs):
return b"s3"
m = ws.mount("/data/")
with pytest.raises(ValueError, match=r"'s3'"):
m.register_fns([s3_op])
def test_register_fns_multi_resource_filters_to_matching(ws):
@command("multi", resource=["ram", "s3"], spec=SPECS["cat"])
async def multi(accessor, paths, *texts, **kw):
return b"multi", IOResult()
m = ws.mount("/data/")
m.register_fns([multi])
assert "multi" in m.commands()
+106
View File
@@ -0,0 +1,106 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
import time
import pytest
from mirage import MountMode, Workspace, WorkspaceRunner
from mirage.resource.ram import RAMResource
def _make_ws() -> Workspace:
return Workspace(
{"/": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE,
)
@pytest.mark.asyncio
async def test_runner_executes_on_its_own_loop():
ws = _make_ws()
runner = WorkspaceRunner(ws)
try:
outer = asyncio.get_running_loop()
assert runner.loop is not outer
result = await runner.call(runner.ws.execute("echo hello"))
assert result.exit_code == 0
assert (result.stdout or b"").startswith(b"hello")
finally:
await runner.stop()
@pytest.mark.asyncio
async def test_runner_call_does_not_block_caller_loop():
ws = _make_ws()
runner = WorkspaceRunner(ws)
try:
slow = asyncio.create_task(runner.call(runner.ws.execute("sleep 0.5")))
ticks = 0
for _ in range(20):
await asyncio.sleep(0.05)
ticks += 1
if slow.done():
break
assert ticks >= 5
await slow
finally:
await runner.stop()
@pytest.mark.asyncio
async def test_two_runners_are_isolated():
ws_a = _make_ws()
ws_b = _make_ws()
runner_a = WorkspaceRunner(ws_a)
runner_b = WorkspaceRunner(ws_b)
try:
slow = asyncio.create_task(
runner_a.call(runner_a.ws.execute("sleep 1.0")))
await asyncio.sleep(0.05)
start = time.monotonic()
fast_result = await runner_b.call(runner_b.ws.execute("echo quick"))
elapsed = time.monotonic() - start
assert fast_result.exit_code == 0
assert elapsed < 0.5, (
f"workspace B's quick command took {elapsed:.2f}s "
"while workspace A was sleeping; isolation violated")
await slow
finally:
await runner_a.stop()
await runner_b.stop()
@pytest.mark.asyncio
async def test_stop_is_idempotent():
ws = _make_ws()
runner = WorkspaceRunner(ws)
await runner.stop()
await runner.stop()
assert not runner._thread.is_alive()
def test_call_sync_runs_on_workspace_loop():
ws = _make_ws()
runner = WorkspaceRunner(ws)
try:
result = runner.call_sync(runner.ws.execute("echo sync"), timeout=5.0)
assert result.exit_code == 0
assert (result.stdout or b"").startswith(b"sync")
finally:
runner.call_sync(runner.ws.close(), timeout=5.0)
runner.loop.call_soon_threadsafe(runner.loop.stop)
runner._thread.join(timeout=2.0)
runner.loop.close()
@@ -0,0 +1,256 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
import pytest
from mirage.resource.ram import RAMResource
from mirage.types import MountMode
from mirage.workspace import Workspace
from mirage.workspace.session import reset_current_session, set_current_session
def _seed(name: str, body: bytes) -> RAMResource:
r = RAMResource()
r._store.files[f"/{name}"] = body
return r
def test_session_outside_allowlist_is_denied():
a = _seed("x.txt", b"public")
b = _seed("secret.txt", b"SECRET")
ws = Workspace({"/a": a, "/b": b})
ws.create_session("agent", allowed_mounts=frozenset({"/a"}))
async def run():
ok = await ws.execute("cat /a/x.txt", session_id="agent")
denied = await ws.execute("cat /b/secret.txt", session_id="agent")
return ok, denied
ok, denied = asyncio.run(run())
assert ok.exit_code == 0
assert b"public" in (ok.stdout or b"")
assert denied.exit_code != 0
stderr = denied.stderr or b""
assert b"not allowed" in stderr
assert b"/b" in stderr
def test_default_session_unrestricted():
a = _seed("x.txt", b"hi")
ws = Workspace({"/a": a})
async def run():
return await ws.execute("cat /a/x.txt")
io = asyncio.run(run())
assert io.exit_code == 0
assert b"hi" in (io.stdout or b"")
def test_allowed_session_can_write_to_its_mount():
a = _seed("x.txt", b"hi")
ws = Workspace({"/a": (a, MountMode.WRITE)}, mode=MountMode.WRITE)
ws.create_session("agent", allowed_mounts=frozenset({"/a"}))
async def run():
return await ws.execute("echo new > /a/y.txt", session_id="agent")
io = asyncio.run(run())
assert io.exit_code == 0, f"unexpected denial: {io}"
assert a._store.files.get("/y.txt") == b"new\n"
def test_history_view_always_allowed():
a = _seed("x.txt", b"hi")
ws = Workspace({"/a": a})
ws.create_session("agent", allowed_mounts=frozenset({"/a"}))
async def run():
await ws.execute("ls /a", session_id="agent")
return await ws.execute("history", session_id="agent")
io = asyncio.run(run())
assert io.exit_code == 0, (
f"history view should always be reachable, got {io}")
def test_ops_blocks_programmatic_read_outside_allowlist():
a = _seed("x.txt", b"public")
b = _seed("secret.txt", b"SECRET")
ws = Workspace({"/a": a, "/b": b})
sess = ws.create_session("agent", allowed_mounts=frozenset({"/a"}))
async def run():
token = set_current_session(sess)
try:
assert await ws.ops.read("/a/x.txt") == b"public"
with pytest.raises(PermissionError, match="not allowed"):
await ws.ops.read("/b/secret.txt")
finally:
reset_current_session(token)
asyncio.run(run())
def _two_mounts_with_secret() -> Workspace:
a = _seed("x.txt", b"public-A\n")
a._store.files["/y.txt"] = b"public-B\n"
b = _seed("secret.txt", b"SECRET-FROM-B\n")
ws = Workspace({
"/a": (a, MountMode.WRITE),
"/b": (b, MountMode.WRITE)
},
mode=MountMode.WRITE)
ws.create_session("agent", allowed_mounts=frozenset({"/a"}))
return ws
def test_pipe_across_mounts_blocks_forbidden_read():
ws = _two_mounts_with_secret()
async def run():
return await ws.execute("cat /b/secret.txt | wc -l",
session_id="agent")
io = asyncio.run(run())
# Bash convention: a downstream success masks an upstream failure
# (no `pipefail`). Security guarantee: no leak + audit on stderr.
assert b"SECRET" not in (io.stdout or b""), (
f"forbidden read must not reach the pipe, got stdout={io.stdout!r}")
assert b"not allowed" in (io.stderr or b"")
assert b"/b" in (io.stderr or b"")
def test_pipe_within_allowed_mount_succeeds():
ws = _two_mounts_with_secret()
async def run():
return await ws.execute("cat /a/x.txt | wc -c", session_id="agent")
io = asyncio.run(run())
assert io.exit_code == 0, f"in-allowlist pipe must succeed, got {io}"
def test_command_substitution_into_forbidden_mount_is_denied():
ws = _two_mounts_with_secret()
async def run():
return await ws.execute("echo $(cat /b/secret.txt)",
session_id="agent")
io = asyncio.run(run())
assert io.exit_code != 0 or b"SECRET" not in (io.stdout or b""), (
f"command substitution must not leak forbidden read, got {io}")
def test_subshell_inherits_session_capability():
ws = _two_mounts_with_secret()
async def run():
return await ws.execute("(cat /b/secret.txt)", session_id="agent")
io = asyncio.run(run())
assert io.exit_code != 0
assert b"not allowed" in (io.stderr or b"")
def test_and_chain_short_circuits_on_denial():
ws = _two_mounts_with_secret()
async def run():
return await ws.execute("cat /b/secret.txt && cat /a/x.txt",
session_id="agent")
io = asyncio.run(run())
assert io.exit_code != 0
assert b"public-A" not in (io.stdout or b""), (
"denied left side should short-circuit the && chain")
def test_or_chain_falls_through_to_allowed():
ws = _two_mounts_with_secret()
async def run():
return await ws.execute("cat /b/secret.txt || cat /a/x.txt",
session_id="agent")
io = asyncio.run(run())
assert b"public-A" in (io.stdout or b""), (
f"|| should fall through to the allowed branch, got {io}")
def test_redirect_to_forbidden_mount_is_denied():
ws = _two_mounts_with_secret()
async def run():
return await ws.execute("echo leaked > /b/leaked.txt",
session_id="agent")
io = asyncio.run(run())
assert io.exit_code != 0
assert b"not allowed" in (io.stderr or b"")
def test_cross_mount_copy_into_forbidden_mount_is_denied():
ws = _two_mounts_with_secret()
async def run():
return await ws.execute("cp /a/x.txt /b/leaked.txt",
session_id="agent")
io = asyncio.run(run())
assert io.exit_code != 0
assert b"not allowed" in (io.stderr or b"")
def test_concurrent_sessions_isolated():
a = _seed("x.txt", b"A-only\n")
b = _seed("y.txt", b"B-only\n")
ws = Workspace({"/a": a, "/b": b})
ws.create_session("agent_a", allowed_mounts=frozenset({"/a"}))
ws.create_session("agent_b", allowed_mounts=frozenset({"/b"}))
async def run():
results = await asyncio.gather(
ws.execute("cat /a/x.txt", session_id="agent_a"),
ws.execute("cat /b/y.txt", session_id="agent_b"),
ws.execute("cat /b/y.txt", session_id="agent_a"),
ws.execute("cat /a/x.txt", session_id="agent_b"),
)
return results
a_ok, b_ok, a_denied, b_denied = asyncio.run(run())
assert a_ok.exit_code == 0 and b"A-only" in (a_ok.stdout or b"")
assert b_ok.exit_code == 0 and b"B-only" in (b_ok.stdout or b"")
assert a_denied.exit_code != 0
assert b_denied.exit_code != 0
def test_background_job_inherits_capability():
ws = _two_mounts_with_secret()
async def run():
# Background a forbidden read; the job runs in a Task that
# snapshots the contextvar. wait reaps it; jobs reports state.
return await ws.execute(
"cat /b/secret.txt &; wait",
session_id="agent",
)
io = asyncio.run(run())
out = (io.stdout or b"") + (io.stderr or b"")
assert b"SECRET" not in out, (
f"background job must not leak forbidden read, got {io}")
+454
View File
@@ -0,0 +1,454 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
import importlib.metadata
import json
import tarfile
import pytest
from mirage.resource.disk import DiskResource
from mirage.resource.ram import RAMResource
from mirage.resource.s3 import S3Config, S3Resource
from mirage.types import MountMode
from mirage.workspace import Workspace
from mirage.workspace.snapshot import to_state_dict
def _load(*args, **kwargs):
return asyncio.run(Workspace.load(*args, **kwargs))
def _seed(ws, mount: str = "/m") -> None:
async def _do():
await ws.execute(f"echo hello > {mount}/a.txt")
await ws.execute(
f"mkdir -p {mount}/sub && echo world > {mount}/sub/b.txt")
asyncio.run(_do())
def _read(ws, path: str) -> str:
async def _do():
r = await ws.execute(f"cat {path}")
return await r.stdout_str()
return asyncio.run(_do())
# ── RAM round trip ──────────────────────────────────────────────────
def test_save_load_ram_round_trip(tmp_path):
src = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
_seed(src)
snap = tmp_path / "ram.tar"
asyncio.run(src.snapshot(snap))
assert snap.exists() and snap.stat().st_size > 0
dst = _load(snap)
assert _read(dst, "/m/a.txt") == "hello\n"
assert _read(dst, "/m/sub/b.txt") == "world\n"
def test_history_survives_snapshot_round_trip(tmp_path):
src = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
asyncio.run(src.execute("echo one"))
asyncio.run(src.execute("echo two"))
assert len(asyncio.run(src.history())) == 2
snap = tmp_path / "history.tar"
asyncio.run(src.snapshot(snap))
dst = _load(snap)
entries = asyncio.run(dst.history())
assert [e["command"] for e in entries] == ["echo one", "echo two"]
def test_from_state_rebuilds_in_process_without_tar():
src = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
_seed(src)
dst = asyncio.run(Workspace.from_state(asyncio.run(to_state_dict(src))))
assert _read(dst, "/m/a.txt") == "hello\n"
assert _read(dst, "/m/sub/b.txt") == "world\n"
def test_save_load_ram_compressed_gz(tmp_path):
src = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
_seed(src)
snap = tmp_path / "ram.tar.gz"
asyncio.run(src.snapshot(snap, compress="gz"))
dst = _load(snap)
assert _read(dst, "/m/a.txt") == "hello\n"
# ── Disk round trip ────────────────────────────────────────────────
def test_save_load_disk_round_trip(tmp_path):
src_root = tmp_path / "src"
src_root.mkdir()
src = Workspace(
{"/m": (DiskResource(root=str(src_root)), MountMode.WRITE)},
mode=MountMode.WRITE)
_seed(src)
snap = tmp_path / "disk.tar"
asyncio.run(src.snapshot(snap))
dst = _load(snap)
assert _read(dst, "/m/a.txt") == "hello\n"
assert _read(dst, "/m/sub/b.txt") == "world\n"
def test_save_load_disk_with_override_root(tmp_path):
src_root = tmp_path / "src"
src_root.mkdir()
src = Workspace(
{"/m": (DiskResource(root=str(src_root)), MountMode.WRITE)},
mode=MountMode.WRITE)
_seed(src)
snap = tmp_path / "disk.tar"
asyncio.run(src.snapshot(snap))
dst_root = tmp_path / "dst"
dst_root.mkdir()
dst = _load(snap, resources={"/m": DiskResource(root=str(dst_root))})
assert (dst_root / "a.txt").read_bytes() == b"hello\n"
assert (dst_root / "sub" / "b.txt").read_bytes() == b"world\n"
assert _read(dst, "/m/a.txt") == "hello\n"
# ── redacted secret override enforcement ─────────────────────────────
def test_redacted_secret_missing_resource_raises(tmp_path):
cfg = S3Config(bucket="b",
region="us-east-1",
aws_access_key_id="AKIA-LEAK",
aws_secret_access_key="SECRET-LEAK")
src = Workspace({"/s3": (S3Resource(cfg), MountMode.WRITE)},
mode=MountMode.WRITE)
snap = tmp_path / "s3.tar"
asyncio.run(src.snapshot(snap))
with pytest.raises(ValueError, match=r"resources="):
_load(snap)
def test_redacted_secret_lists_all_missing(tmp_path):
src = Workspace(
{
"/ram": (RAMResource(), MountMode.WRITE),
"/s3a": (S3Resource(
S3Config(bucket="a",
region="us-east-1",
aws_access_key_id="x",
aws_secret_access_key="x")), MountMode.WRITE),
"/s3b": (S3Resource(
S3Config(bucket="b",
region="us-east-1",
aws_access_key_id="x",
aws_secret_access_key="x")), MountMode.WRITE),
},
mode=MountMode.WRITE)
snap = tmp_path / "two-s3.tar"
asyncio.run(src.snapshot(snap))
with pytest.raises(ValueError) as ei:
_load(snap)
msg = str(ei.value)
assert "/s3a" in msg
assert "/s3b" in msg
def test_s3_without_inline_secret_loads_without_override(tmp_path):
cfg = S3Config(bucket="b", region="us-east-1", aws_profile="dev")
src = Workspace({"/s3": (S3Resource(cfg), MountMode.WRITE)},
mode=MountMode.WRITE)
snap = tmp_path / "s3-profile.tar"
asyncio.run(src.snapshot(snap))
dst = _load(snap)
mount = dst._registry.mount_for("/s3/")
assert isinstance(mount.resource, S3Resource)
assert mount.resource.config.aws_profile == "dev"
# ── cred redaction in raw bytes ─────────────────────────────────────
def test_no_real_creds_in_tar_bytes(tmp_path):
cfg = S3Config(bucket="b",
region="us-east-1",
aws_access_key_id="AKIA-OBVIOUS-LEAK",
aws_secret_access_key="SECRET-OBVIOUS-LEAK")
src = Workspace({"/s3": (S3Resource(cfg), MountMode.WRITE)},
mode=MountMode.WRITE)
snap = tmp_path / "s3.tar"
asyncio.run(src.snapshot(snap))
raw = snap.read_bytes()
assert b"AKIA-OBVIOUS-LEAK" not in raw
assert b"SECRET-OBVIOUS-LEAK" not in raw
assert b"<REDACTED>" in raw
# ── manifest validity ──────────────────────────────────────────────
def test_manifest_is_valid_json(tmp_path):
src = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
_seed(src)
snap = tmp_path / "snap.tar"
asyncio.run(src.snapshot(snap))
with tarfile.open(snap, "r") as tar:
f = tar.extractfile("manifest.json")
manifest = json.loads(f.read().decode("utf-8"))
assert manifest["version"] == 2
assert "mounts" in manifest
assert "cache" in manifest
def test_disk_files_extractable_from_tar(tmp_path):
src_root = tmp_path / "src"
src_root.mkdir()
src = Workspace(
{"/m": (DiskResource(root=str(src_root)), MountMode.WRITE)},
mode=MountMode.WRITE)
_seed(src)
snap = tmp_path / "disk.tar"
asyncio.run(src.snapshot(snap))
extract = tmp_path / "extract"
extract.mkdir()
with tarfile.open(snap, "r") as tar:
for member in tar.getmembers():
if member.name.startswith("mounts/0/files/"):
tar.extract(member, extract, filter="data")
assert (extract / "mounts/0/files/a.txt").read_bytes() == b"hello\n"
assert (extract / "mounts/0/files/sub/b.txt").read_bytes() == b"world\n"
# ── path-traversal defense ─────────────────────────────────────────
def test_load_rejects_path_traversal_in_blob_ref(tmp_path):
snap = tmp_path / "bad.tar"
manifest = {
"version":
1,
"mirage_version":
"0.1.0",
"default_session_id":
"default",
"default_agent_id":
"default",
"current_agent_id":
"default",
"sessions": [],
"history":
None,
"mounts": [{
"index": 0,
"prefix": "/m",
"mode": "WRITE",
"consistency": "LAZY",
"resource_class": "mirage.resource.ram.RAMResource",
"resource_state": {
"type": "ram",
"files": {
"/x": {
"__file": "../../etc/passwd"
}
},
"dirs": [],
"modified": {},
},
}],
"cache": {
"limit": 0,
"max_drain_bytes": None,
"entries": []
},
"jobs": [],
}
with tarfile.open(snap, "w") as tar:
data = json.dumps(manifest).encode("utf-8")
info = tarfile.TarInfo(name="manifest.json")
info.size = len(data)
import io as _io
tar.addfile(info, _io.BytesIO(data))
with pytest.raises(ValueError, match="Unsafe blob path"):
_load(snap)
# ── copy ───────────────────────────────────────────────────────────
def test_workspace_copy_independence_ram():
src = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
asyncio.run(src.execute("echo hi > /m/a.txt"))
cp = asyncio.run(src.copy())
asyncio.run(cp.execute("echo bye > /m/a.txt"))
assert _read(src, "/m/a.txt") == "hi\n"
assert _read(cp, "/m/a.txt") == "bye\n"
def test_workspace_copy_preserves_max_drain_bytes():
src = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
src.max_drain_bytes = 1234
cp = asyncio.run(src.copy())
assert cp.max_drain_bytes == 1234
# ── state dict shape ───────────────────────────────────────────────
def test_to_state_dict_shape():
ws = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
state = asyncio.run(to_state_dict(ws))
assert state["version"] == 2
assert state["mirage_version"] == importlib.metadata.version("mirage-ai")
assert state["mirage_version"] != "unknown"
assert isinstance(state["mounts"], list)
assert state["cache"]["entries"] == []
assert state["jobs"] == []
def test_snapshot_round_trip_no_sync_policy(tmp_path):
ws = Workspace({"/data": RAMResource()})
target = tmp_path / "snap.tar"
asyncio.run(ws.snapshot(str(target)))
restored = _load(str(target))
assert restored is not None
# ── filenames with spaces / unicode ───────────────────────────────
def test_ram_round_trip_filenames_with_spaces(tmp_path):
src = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
src._registry.mount_for("/m/").resource._store.files["/my file.txt"] = (
b"with spaces")
src._registry.mount_for("/m/").resource._store.files[
"/dir with space/data.txt"] = b"nested with space"
src._registry.mount_for("/m/").resource._store.files["/数据.txt"] = (
"你好".encode())
snap = tmp_path / "spaces.tar"
asyncio.run(src.snapshot(snap))
dst = _load(snap)
files = dst._registry.mount_for("/m/").resource._store.files
assert files["/my file.txt"] == b"with spaces"
assert files["/dir with space/data.txt"] == b"nested with space"
assert files["/数据.txt"].decode() == "你好"
def test_disk_round_trip_filenames_with_spaces(tmp_path):
src_root = tmp_path / "src"
src_root.mkdir()
(src_root / "my file.txt").write_bytes(b"hello space")
(src_root / "dir with space").mkdir()
(src_root / "dir with space" / "data.txt").write_bytes(b"deep space")
(src_root / "数据.txt").write_bytes("你好".encode())
src = Workspace(
{"/m": (DiskResource(root=str(src_root)), MountMode.WRITE)},
mode=MountMode.WRITE)
snap = tmp_path / "disk-spaces.tar"
asyncio.run(src.snapshot(snap))
dst_root = tmp_path / "dst"
dst_root.mkdir()
_load(snap, resources={"/m": DiskResource(root=str(dst_root))})
assert (dst_root / "my file.txt").read_bytes() == b"hello space"
assert ((dst_root / "dir with space" /
"data.txt").read_bytes() == b"deep space")
assert (dst_root / "数据.txt").read_bytes().decode() == "你好"
def test_is_safe_blob_path_allows_spaces_and_unicode():
from mirage.workspace.snapshot import is_safe_blob_path
assert is_safe_blob_path("my file.txt")
assert is_safe_blob_path("dir with space/data.txt")
assert is_safe_blob_path("数据.txt")
assert not is_safe_blob_path("../etc/passwd")
assert not is_safe_blob_path("/abs/path")
assert not is_safe_blob_path("")
assert not is_safe_blob_path("foo/../bar")
assert not is_safe_blob_path("foo\x00bar")
# ── Redis round trip ───────────────────────────────────────────────
@pytest.mark.skipif(not __import__("os").environ.get("REDIS_URL"),
reason="REDIS_URL not set")
def test_redis_round_trip_filenames_with_spaces(tmp_path):
import os
import uuid
import redis as sync_redis
from mirage.resource.redis import RedisResource
redis_url = os.environ["REDIS_URL"]
src_prefix = f"mirage:test:src:{uuid.uuid4().hex}:"
dst_prefix = f"mirage:test:dst:{uuid.uuid4().hex}:"
sc = sync_redis.Redis.from_url(redis_url)
sc.set(f"{src_prefix}file:/my file.txt", b"hello space")
sc.set(f"{src_prefix}file:/dir with space/data.txt", b"deep space")
sc.sadd(f"{src_prefix}dir", "/dir with space")
sc.close()
src = Workspace(
{
"/m": (RedisResource(url=redis_url,
key_prefix=src_prefix), MountMode.WRITE)
},
mode=MountMode.WRITE)
snap = tmp_path / "redis-spaces.tar"
asyncio.run(src.snapshot(snap))
dst_resource = RedisResource(url=redis_url, key_prefix=dst_prefix)
_load(snap, resources={"/m": dst_resource})
sc = sync_redis.Redis.from_url(redis_url)
try:
assert sc.get(f"{dst_prefix}file:/my file.txt") == b"hello space"
assert (sc.get(f"{dst_prefix}file:/dir with space/data.txt") ==
b"deep space")
finally:
for prefix in (src_prefix, dst_prefix):
for key in sc.scan_iter(f"{prefix}*"):
sc.delete(key)
sc.close()
@@ -0,0 +1,211 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
from contextlib import ExitStack
import pytest
from mirage.resource.ram import RAMResource
from mirage.resource.s3 import S3Config, S3Resource
from mirage.types import DriftPolicy, MountMode
from mirage.workspace import Workspace
from mirage.workspace.snapshot import ContentDriftError, install_fingerprints
from tests.integration.s3_mock import patch_s3_multi
def _load(*args, **kwargs):
return asyncio.run(Workspace.load(*args, **kwargs))
def test_install_fingerprints_pins_revision_and_queues_drift():
ws = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
entries = [
{
"path": "/m/pinned.txt",
"mount_prefix": "/m/",
"revision": "v9"
},
{
"path": "/m/checked.txt",
"mount_prefix": "/m/",
"fingerprint": "fp1"
},
]
install_fingerprints(ws, entries, DriftPolicy.STRICT)
mount = ws._registry.mount_for("/m/pinned.txt")
assert mount.revisions["/m/pinned.txt"] == "v9"
assert ws._drift_check_pending is True
queued = [path for (_, path, _) in ws._pending_drift]
assert "/m/checked.txt" in queued
def _config() -> S3Config:
return S3Config(
bucket="test-bucket",
region="us-east-1",
aws_access_key_id="fake",
aws_secret_access_key="fake",
)
def test_strict_load_raises_when_s3_etag_drifts(tmp_path):
"""Snapshot a workspace with one read on /s3, mutate the underlying
object so its ETag changes, load with strict policy: the next read
must raise ContentDriftError rather than silently serve drifted
bytes.
"""
store = {"data.csv": b"version 1 bytes\n"}
with ExitStack() as stack:
stack.enter_context(patch_s3_multi({"test-bucket": store}))
src = Workspace({"/s3": (S3Resource(_config()), MountMode.WRITE)},
mode=MountMode.WRITE)
result = asyncio.run(src.execute("cat /s3/data.csv"))
assert b"version 1" in result.stdout
snap = tmp_path / "snap.tar"
asyncio.run(src.snapshot(snap))
store["data.csv"] = b"VERSION 2 DRIFTED\n"
dst = _load(snap, resources={"/s3": S3Resource(_config())})
with pytest.raises(ContentDriftError) as exc_info:
asyncio.run(dst.execute("cat /s3/data.csv"))
assert exc_info.value.path == "/s3/data.csv"
live = exc_info.value.live_fingerprint
recorded = exc_info.value.snapshot_fingerprint
assert live != recorded
def test_off_load_serves_drifted_bytes_silently(tmp_path):
"""drift_policy=OFF disables the check: the load returns the new
bytes with no error. This is the only opt-out from drift detection.
"""
store = {"data.csv": b"version 1 bytes\n"}
with ExitStack() as stack:
stack.enter_context(patch_s3_multi({"test-bucket": store}))
src = Workspace({"/s3": (S3Resource(_config()), MountMode.WRITE)},
mode=MountMode.WRITE)
asyncio.run(src.execute("cat /s3/data.csv"))
snap = tmp_path / "snap.tar"
asyncio.run(src.snapshot(snap))
store["data.csv"] = b"VERSION 2 DRIFTED\n"
dst = _load(snap,
resources={"/s3": S3Resource(_config())},
drift_policy=DriftPolicy.OFF)
result = asyncio.run(dst.execute("cat /s3/data.csv"))
assert b"VERSION 2 DRIFTED" in result.stdout
def test_strict_load_passes_when_etag_unchanged(tmp_path):
"""The control case: same bytes still in S3, strict load must succeed
and serve them. Verifies the check is precise (no false positives).
"""
store = {"data.csv": b"stable bytes\n"}
with ExitStack() as stack:
stack.enter_context(patch_s3_multi({"test-bucket": store}))
src = Workspace({"/s3": (S3Resource(_config()), MountMode.WRITE)},
mode=MountMode.WRITE)
asyncio.run(src.execute("cat /s3/data.csv"))
snap = tmp_path / "snap.tar"
asyncio.run(src.snapshot(snap))
dst = _load(snap, resources={"/s3": S3Resource(_config())})
result = asyncio.run(dst.execute("cat /s3/data.csv"))
assert b"stable bytes" in result.stdout
def test_unrecorded_path_skips_drift_check(tmp_path):
"""A path the agent did not read at snapshot time has no recorded
fingerprint, so it must NOT be drift-checked at load — just live-
served. Tests that drift-check is opt-in per recorded path.
"""
store = {
"read-me.txt": b"recorded\n",
"added-later.txt": b"not in snapshot\n"
}
with ExitStack() as stack:
stack.enter_context(patch_s3_multi({"test-bucket": store}))
src = Workspace({"/s3": (S3Resource(_config()), MountMode.WRITE)},
mode=MountMode.WRITE)
asyncio.run(src.execute("cat /s3/read-me.txt"))
snap = tmp_path / "snap.tar"
asyncio.run(src.snapshot(snap))
dst = _load(snap, resources={"/s3": S3Resource(_config())})
result = asyncio.run(dst.execute("cat /s3/added-later.txt"))
assert b"not in snapshot" in result.stdout
def test_version_pin_serves_original_bytes_on_versioned_bucket(tmp_path):
"""On a versioned S3 bucket, snapshot captures the object's VersionId
alongside its ETag. After the live bytes drift, load with default
STRICT policy pins reads to the recorded VersionId and serves the
ORIGINAL bytes the agent saw, instead of raising drift or returning
the current head.
"""
store = {"data.csv": b"original\n"}
with ExitStack() as stack:
stack.enter_context(
patch_s3_multi({"test-bucket": store}, versioned={"test-bucket"}))
src = Workspace({"/s3": (S3Resource(_config()), MountMode.WRITE)},
mode=MountMode.WRITE)
asyncio.run(src.execute("cat /s3/data.csv"))
snap = tmp_path / "snap.tar"
asyncio.run(src.snapshot(snap))
store["data.csv"] = b"mutated bytes\n"
dst = _load(snap, resources={"/s3": S3Resource(_config())})
# Cache holds snapshot bytes; clear so we hit S3 and exercise the
# pin path, not the cache path.
_drop_path_from_cache(dst, "/s3/data.csv")
result = asyncio.run(dst.execute("cat /s3/data.csv"))
assert result.stdout == b"original\n"
def _drop_path_from_cache(ws, path: str) -> None:
cache = ws._cache
cache._entries.pop(path, None)
cache._store.files.pop(path, None)
def test_live_only_mount_does_not_block_snapshot(tmp_path, caplog):
"""Workspaces with non-SUPPORTS_SNAPSHOT mounts (RAM here as a stand-
in for Gmail/Slack/Linear) snapshot fine; no fingerprints are
captured for those paths and the load layer logs an honest warning
surfacing the live-only mount list.
"""
src = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
asyncio.run(src.execute("echo body > /m/note.txt"))
asyncio.run(src.execute("cat /m/note.txt"))
snap = tmp_path / "snap.tar"
asyncio.run(src.snapshot(snap))
with caplog.at_level("WARNING"):
_load(snap)
assert any("live-only" in r.message.lower()
or "live-only" in r.getMessage().lower()
for r in caplog.records) or any(
"no drift" in r.message.lower()
or "no drift" in r.getMessage().lower()
for r in caplog.records)
+304
View File
@@ -0,0 +1,304 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage.resource.ram import RAMResource
from mirage.types import MountMode
from mirage.workspace import Workspace
def _ws():
return Workspace({"/data": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
@pytest.mark.asyncio
async def test_ln_readlink_verbatim():
ws = _ws()
await ws.execute("echo hi > /data/a.txt")
r = await ws.execute("ln -s /data/a.txt /data/link.txt")
assert r.exit_code == 0
r = await ws.execute("readlink /data/link.txt")
assert r.stdout.decode() == "/data/a.txt\n"
@pytest.mark.asyncio
async def test_ln_relative_target_kept_verbatim():
ws = _ws()
await ws.execute("echo hi > /data/a.txt")
await ws.execute("ln -s a.txt /data/link.txt")
r = await ws.execute("readlink /data/link.txt")
assert r.stdout.decode() == "a.txt\n"
@pytest.mark.asyncio
async def test_ln_sf_overwrites():
ws = _ws()
await ws.execute("echo a > /data/a.txt")
await ws.execute("echo b > /data/b.txt")
await ws.execute("ln -s /data/a.txt /data/link.txt")
await ws.execute("ln -s -f /data/b.txt /data/link.txt")
r = await ws.execute("readlink /data/link.txt")
assert r.stdout.decode() == "/data/b.txt\n"
@pytest.mark.asyncio
async def test_ln_no_force_refuses_existing_link():
ws = _ws()
await ws.execute("echo a > /data/a.txt")
await ws.execute("ln -s /data/a.txt /data/link.txt")
r = await ws.execute("ln -s /data/a.txt /data/link.txt")
assert r.exit_code == 1
assert b"File exists" in r.stderr
@pytest.mark.asyncio
async def test_cd_through_symlink():
ws = _ws()
await ws.execute("mkdir -p /data/real")
await ws.execute("ln -s /data/real /data/slink")
r = await ws.execute("cd /data/slink && pwd")
assert r.stdout.decode() == "/data/real\n"
@pytest.mark.asyncio
async def test_cd_symlink_loop_is_eloop():
ws = _ws()
await ws.execute("ln -s /data/b /data/a")
await ws.execute("ln -s /data/a /data/b")
r = await ws.execute("cd /data/a")
assert r.exit_code == 1
assert b"Too many levels of symbolic links" in r.stderr
@pytest.mark.asyncio
async def test_symlink_survives_snapshot(tmp_path):
ws = _ws()
await ws.execute("echo hi > /data/a.txt")
await ws.execute("ln -s /data/a.txt /data/link.txt")
target = str(tmp_path / "snap.tar")
await ws.snapshot(target)
ws2 = await Workspace.load(target)
r = await ws2.execute("readlink /data/link.txt")
assert r.stdout.decode() == "/data/a.txt\n"
@pytest.mark.asyncio
async def test_cat_follows_link():
ws = _ws()
await ws.execute("echo hi > /data/a.txt")
await ws.execute("ln -s /data/a.txt /data/link.txt")
r = await ws.execute("cat /data/link.txt")
assert r.exit_code == 0
assert r.stdout.decode() == "hi\n"
@pytest.mark.asyncio
async def test_read_follows_midpath_dir_link():
ws = _ws()
await ws.execute("mkdir -p /data/real && echo hi > /data/real/f.txt")
await ws.execute("ln -s /data/real /data/dirlink")
r = await ws.execute("cat /data/dirlink/f.txt")
assert r.stdout.decode() == "hi\n"
@pytest.mark.asyncio
async def test_read_follows_relative_target():
ws = _ws()
await ws.execute("mkdir -p /data/sub && echo hi > /data/sub/a.txt")
await ws.execute("ln -s a.txt /data/sub/link.txt")
r = await ws.execute("cat /data/sub/link.txt")
assert r.stdout.decode() == "hi\n"
@pytest.mark.asyncio
async def test_write_through_link_updates_target():
ws = _ws()
await ws.execute("echo old > /data/a.txt")
await ws.execute("ln -s /data/a.txt /data/link.txt")
await ws.execute("echo new > /data/link.txt")
r = await ws.execute("cat /data/a.txt")
assert r.stdout.decode() == "new\n"
@pytest.mark.asyncio
async def test_cat_dangling_link_errors_with_typed_name():
ws = _ws()
await ws.execute("ln -s /data/missing /data/dangle")
r = await ws.execute("cat /data/dangle")
assert r.exit_code == 1
assert b"/data/dangle" in r.stderr
assert b"No such file" in r.stderr
@pytest.mark.asyncio
async def test_cat_loop_is_eloop_with_operand():
ws = _ws()
await ws.execute("ln -s /data/b /data/a")
await ws.execute("ln -s /data/a /data/b")
r = await ws.execute("cat /data/a")
assert r.exit_code == 1
assert b"cat: /data/a: Too many levels of symbolic links" in r.stderr
@pytest.mark.asyncio
async def test_ls_lists_links():
ws = _ws()
await ws.execute("echo hi > /data/a.txt")
await ws.execute("ln -s /data/a.txt /data/link.txt")
r = await ws.execute("ls /data")
assert "link.txt" in r.stdout.decode()
r = await ws.execute("ls -F /data")
assert "link.txt@" in r.stdout.decode()
r = await ws.execute("ls -l /data")
assert "link.txt -> /data/a.txt" in r.stdout.decode()
@pytest.mark.asyncio
async def test_ls_through_dir_link():
ws = _ws()
await ws.execute("mkdir -p /data/real && echo hi > /data/real/f.txt")
await ws.execute("ln -s /data/real /data/dirlink")
r = await ws.execute("ls /data/dirlink")
assert r.stdout.decode() == "f.txt\n"
@pytest.mark.asyncio
async def test_rm_removes_link_not_target():
ws = _ws()
await ws.execute("echo hi > /data/a.txt")
await ws.execute("ln -s /data/a.txt /data/link.txt")
r = await ws.execute("rm /data/link.txt")
assert r.exit_code == 0
r = await ws.execute("readlink /data/link.txt")
assert r.exit_code == 1
r = await ws.execute("cat /data/a.txt")
assert r.stdout.decode() == "hi\n"
@pytest.mark.asyncio
async def test_rm_dangling_link():
ws = _ws()
await ws.execute("ln -s /data/missing /data/dangle")
r = await ws.execute("rm /data/dangle")
assert r.exit_code == 0
@pytest.mark.asyncio
async def test_rm_mixed_link_and_file():
ws = _ws()
await ws.execute("echo hi > /data/a.txt && echo x > /data/b.txt")
await ws.execute("ln -s /data/a.txt /data/link.txt")
r = await ws.execute("rm /data/link.txt /data/b.txt")
assert r.exit_code == 0
r = await ws.execute("ls /data")
assert r.stdout.decode() == "a.txt\n"
@pytest.mark.asyncio
async def test_rm_target_leaves_link_dangling():
ws = _ws()
await ws.execute("echo hi > /data/a.txt")
await ws.execute("ln -s /data/a.txt /data/link.txt")
await ws.execute("rm /data/a.txt")
r = await ws.execute("readlink /data/link.txt")
assert r.stdout.decode() == "/data/a.txt\n"
r = await ws.execute("cat /data/link.txt")
assert r.exit_code == 1
@pytest.mark.asyncio
async def test_rm_r_purges_links_under_dir():
ws = _ws()
await ws.execute("mkdir -p /data/sub && echo hi > /data/sub/f.txt")
await ws.execute("ln -s /data/sub/f.txt /data/sub/inner")
r = await ws.execute("rm -r /data/sub")
assert r.exit_code == 0
r = await ws.execute("readlink /data/sub/inner")
assert r.exit_code == 1
@pytest.mark.asyncio
async def test_mv_renames_link_entry():
ws = _ws()
await ws.execute("echo hi > /data/a.txt")
await ws.execute("ln -s /data/a.txt /data/link.txt")
r = await ws.execute("mv /data/link.txt /data/renamed.txt")
assert r.exit_code == 0
r = await ws.execute("readlink /data/renamed.txt")
assert r.stdout.decode() == "/data/a.txt\n"
r = await ws.execute("readlink /data/link.txt")
assert r.exit_code == 1
@pytest.mark.asyncio
async def test_mv_link_into_existing_dir():
ws = _ws()
await ws.execute("mkdir -p /data/dir && echo hi > /data/a.txt")
await ws.execute("ln -s /data/a.txt /data/link.txt")
r = await ws.execute("mv /data/link.txt /data/dir")
assert r.exit_code == 0
r = await ws.execute("readlink /data/dir/link.txt")
assert r.stdout.decode() == "/data/a.txt\n"
@pytest.mark.asyncio
async def test_mv_file_onto_link_replaces_entry():
ws = _ws()
await ws.execute("echo a > /data/a.txt && echo b > /data/b.txt")
await ws.execute("ln -s /data/a.txt /data/link.txt")
r = await ws.execute("mv /data/b.txt /data/link.txt")
assert r.exit_code == 0
r = await ws.execute("readlink /data/link.txt")
assert r.exit_code == 1
r = await ws.execute("cat /data/link.txt")
assert r.stdout.decode() == "b\n"
r = await ws.execute("cat /data/a.txt")
assert r.stdout.decode() == "a\n"
@pytest.mark.asyncio
async def test_cross_mount_link_follow():
ws = Workspace(
{
"/data": (RAMResource(), MountMode.WRITE),
"/other": (RAMResource(), MountMode.WRITE),
},
mode=MountMode.WRITE)
await ws.execute("echo remote > /other/g.txt")
await ws.execute("ln -s /other/g.txt /data/xlink")
r = await ws.execute("cat /data/xlink")
assert r.stdout.decode() == "remote\n"
@pytest.mark.asyncio
async def test_cp_follows_source_link():
ws = _ws()
await ws.execute("echo hi > /data/a.txt")
await ws.execute("ln -s /data/a.txt /data/link.txt")
r = await ws.execute("cp /data/link.txt /data/copy.txt")
assert r.exit_code == 0
r = await ws.execute("cat /data/copy.txt")
assert r.stdout.decode() == "hi\n"
@pytest.mark.asyncio
async def test_grep_follows_link():
ws = _ws()
await ws.execute("printf 'alpha\\nbeta\\n' > /data/a.txt")
await ws.execute("ln -s /data/a.txt /data/link.txt")
r = await ws.execute("grep beta /data/link.txt")
assert r.exit_code == 0
assert "beta" in r.stdout.decode()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,313 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
import copy as _copy
import os
import time
import uuid
import pytest
from mirage.resource.disk import DiskResource
from mirage.resource.ram import RAMResource
from mirage.resource.s3 import S3Config, S3Resource
from mirage.shell.job_table import Job, JobStatus
from mirage.types import MountMode
from mirage.workspace import Workspace
from tests.integration.s3_mock import patch_s3_multi
REDIS_URL = os.environ.get("REDIS_URL", "")
def _load(*args, **kwargs):
return asyncio.run(Workspace.load(*args, **kwargs))
def _read(ws, path):
async def _do():
r = await ws.execute(f"cat {path}")
return await r.stdout_str()
return asyncio.run(_do())
# ── Workspace.save / Workspace.load (instance + classmethod) ─────────
def test_workspace_save_then_load_classmethod(tmp_path):
src = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
asyncio.run(src.execute("echo hi > /m/a.txt"))
snap = tmp_path / "ws.tar"
asyncio.run(src.snapshot(snap))
assert snap.exists() and snap.stat().st_size > 0
dst = _load(snap)
assert _read(dst, "/m/a.txt") == "hi\n"
def test_workspace_save_compressed(tmp_path):
src = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
asyncio.run(src.execute("echo hi > /m/a.txt"))
snap = tmp_path / "ws.tar.gz"
asyncio.run(src.snapshot(snap, compress="gz"))
dst = _load(snap)
assert _read(dst, "/m/a.txt") == "hi\n"
def test_workspace_load_with_disk_override(tmp_path):
src_root = tmp_path / "src"
src_root.mkdir()
(src_root / "a.txt").write_bytes(b"hello\n")
src = Workspace(
{"/m": (DiskResource(root=str(src_root)), MountMode.WRITE)},
mode=MountMode.WRITE)
snap = tmp_path / "ws.tar"
asyncio.run(src.snapshot(snap))
dst_root = tmp_path / "dst"
dst_root.mkdir()
dst = _load(snap, resources={"/m": DiskResource(root=str(dst_root))})
assert _read(dst, "/m/a.txt") == "hello\n"
assert (dst_root / "a.txt").read_bytes() == b"hello\n"
# ── Workspace.copy ────────────────────────────────────────────────
def test_workspace_copy_method_independence_ram():
src = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
asyncio.run(src.execute("echo hi > /m/a.txt"))
cp = asyncio.run(src.copy())
asyncio.run(cp.execute("echo bye > /m/a.txt"))
assert _read(src, "/m/a.txt") == "hi\n"
assert _read(cp, "/m/a.txt") == "bye\n"
# ── copy.deepcopy(ws) → uses __deepcopy__ → uses copy() ──────────
def test_deepcopy_via_stdlib():
src = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
asyncio.run(src.execute("echo hi > /m/a.txt"))
cp = asyncio.run((src).copy())
asyncio.run(cp.execute("echo bye > /m/a.txt"))
assert _read(src, "/m/a.txt") == "hi\n"
assert _read(cp, "/m/a.txt") == "bye\n"
# ── copy.copy(ws) must raise — shallow copy makes no sense ────────
def test_shallow_copy_raises():
src = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
with pytest.raises(NotImplementedError, match="useful shallow copy"):
_copy.copy(src)
def test_shallow_copy_error_mentions_alternatives():
src = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
with pytest.raises(NotImplementedError) as exc_info:
_copy.copy(src)
msg = str(exc_info.value)
assert "ws.copy()" in msg
# ── max_drain_bytes preserved across save/load ───────────────────
def test_save_load_preserves_max_drain_bytes(tmp_path):
src = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
src.max_drain_bytes = 1234
snap = tmp_path / "ws.tar"
asyncio.run(src.snapshot(snap))
dst = _load(snap)
assert dst.max_drain_bytes == 1234
# ── history round trip ────────────────────────────────────────────
def test_history_round_trip(tmp_path):
src = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
asyncio.run(src.execute("echo a > /m/a.txt"))
asyncio.run(src.execute("echo b > /m/b.txt"))
asyncio.run(src.execute("cat /m/a.txt"))
expected_commands = [e["command"] for e in asyncio.run(src.history())]
assert len(expected_commands) == 3
snap = tmp_path / "ws.tar"
asyncio.run(src.snapshot(snap))
dst = _load(snap)
got_commands = [e["command"] for e in asyncio.run(dst.history())]
assert got_commands == expected_commands
# ── finished jobs survive, pending dropped ────────────────────────
def test_finished_jobs_survive(tmp_path):
src = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
finished = Job(id=1,
command="echo done",
task=None,
cwd="/",
status=JobStatus.COMPLETED,
stdout=b"done\n",
stderr=b"",
exit_code=0,
created_at=time.time(),
agent="test",
session_id="default")
src.job_table._jobs[1] = finished
snap = tmp_path / "ws.tar"
asyncio.run(src.snapshot(snap))
dst = _load(snap)
job_ids = {j.id for j in dst.job_table.list_jobs()}
assert 1 in job_ids
# Next job id continues from max(finished)+1 (= 2)
assert dst.job_table._next_id == 2
# ── copy() shares Redis backend (documented divergence) ──────────
@pytest.mark.skipif(not REDIS_URL, reason="REDIS_URL not set")
def test_copy_shares_redis_backend():
import redis as sync_redis
from mirage.resource.redis import RedisResource
prefix = f"mirage:test:copy:{uuid.uuid4().hex}:"
src = Workspace(
{
"/r":
(RedisResource(url=REDIS_URL, key_prefix=prefix), MountMode.WRITE)
},
mode=MountMode.WRITE)
sc = sync_redis.Redis.from_url(REDIS_URL)
sc.set(f"{prefix}file:/seed.txt", b"shared")
sc.close()
cp = asyncio.run(src.copy())
asyncio.run(cp.execute("echo new > /r/added.txt"))
sc = sync_redis.Redis.from_url(REDIS_URL)
try:
# Both src and cp see the new key — they share the same Redis
# instance even though they are independent Workspaces.
assert sc.get(f"{prefix}file:/added.txt") == b"new\n"
finally:
for key in sc.scan_iter(f"{prefix}*"):
sc.delete(key)
sc.close()
# ── copy() independence of cache ─────────────────────────────────
def test_copy_independence_of_cache():
src = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
asyncio.run(src._cache.set("/m/a.txt", b"src-cached"))
cp = asyncio.run(src.copy())
asyncio.run(cp._cache.set("/m/a.txt", b"cp-cached"))
src_cached = asyncio.run(src._cache.get("/m/a.txt"))
cp_cached = asyncio.run(cp._cache.get("/m/a.txt"))
assert src_cached == b"src-cached"
assert cp_cached == b"cp-cached"
# ── S3 round trip via workspace.save (mocked) ──────────────────
def test_workspace_save_load_s3_mounted(tmp_path):
cfg_src = S3Config(bucket="src-bkt",
region="us-east-1",
aws_access_key_id="OLD-AKIA-OBVIOUS",
aws_secret_access_key="OLD-SECRET-OBVIOUS")
cfg_dst = S3Config(bucket="dst-bkt",
region="us-east-1",
aws_access_key_id="NEW-AKIA",
aws_secret_access_key="NEW-SECRET")
buckets: dict = {"src-bkt": {}, "dst-bkt": {}}
with patch_s3_multi(buckets):
src = Workspace({"/s3": (S3Resource(cfg_src), MountMode.WRITE)},
mode=MountMode.WRITE)
snap = tmp_path / "ws.tar"
asyncio.run(src.snapshot(snap))
# Saved tar must not contain old creds
raw = snap.read_bytes()
assert b"OLD-AKIA-OBVIOUS" not in raw
assert b"OLD-SECRET-OBVIOUS" not in raw
assert b"<REDACTED>" in raw
dst = _load(snap, resources={"/s3": S3Resource(cfg_dst)})
# New mount uses fresh creds, fresh bucket
assert dst.mount("/s3").resource.config.bucket == "dst-bkt"
# ── override drops saved index ───────────────────────────────────
def test_override_drops_saved_index(tmp_path):
"""When the caller supplies an override resource, that resource's
own (fresh, empty) index is used — not whatever was on the saved
resource. We verify by checking the loaded mount is the override
object itself.
"""
cfg = S3Config(bucket="b",
region="us-east-1",
aws_access_key_id="x",
aws_secret_access_key="y")
buckets: dict = {"b": {}}
with patch_s3_multi(buckets):
src = Workspace({"/s3": (S3Resource(cfg), MountMode.WRITE)},
mode=MountMode.WRITE)
snap = tmp_path / "ws.tar"
asyncio.run(src.snapshot(snap))
fresh = S3Resource(cfg)
dst = _load(snap, resources={"/s3": fresh})
# The mounted resource IS the user-supplied fresh one,
# carrying its own (empty) index — not anything from the snapshot.
assert dst.mount("/s3").resource is fresh
@@ -0,0 +1,24 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
from mirage.resource.ram import RAMResource
from mirage.workspace import Workspace
def test_workspace_default_no_fuse():
ws = Workspace(resources={"/data": RAMResource()})
assert ws.fuse_mountpoint is None
asyncio.run(ws.close())