bcbd1bdb22
Test (Python) / test-python-gate (push) Blocked by required conditions
Test (TypeScript) / test-typescript-gate (push) Blocked by required conditions
CLI exit codes / cli-gate (push) Blocked by required conditions
Test (Install) / test-install-gate (push) Blocked by required conditions
Integ / integ-gate (push) Blocked by required conditions
CLI exit codes / Python CLI (push) Waiting to run
Integ / changes (push) Has been skipped
Test (Install) / python-minimal (3.11) (push) Waiting to run
Test (Install) / python-minimal (3.12) (push) Waiting to run
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Waiting to run
Integ / integ-ts (push) Waiting to run
Integ / integ (push) Waiting to run
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Waiting to run
Integ / integ-database (push) Waiting to run
Test (Install) / python-extra (email, mirage.resource.email) (push) Waiting to run
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Waiting to run
Integ / integ-ssh (push) Waiting to run
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
CLI exit codes / TypeScript CLI (push) Waiting to run
CLI exit codes / Cross-language snapshot interop (push) Waiting to run
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Waiting to run
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Waiting to run
Test (Python) / changes (push) Has been skipped
Integ / integ-database-ts (push) Waiting to run
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Waiting to run
Integ / integ-data (push) Waiting to run
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Waiting to run
Test (Python) / test (push) Waiting to run
Integ / integ-fuse (push) Waiting to run
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Waiting to run
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Waiting to run
Test (Python) / audit (push) Waiting to run
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Waiting to run
Integ / integ-ssh-ts (push) Waiting to run
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Waiting to run
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Waiting to run
Test (TypeScript) / changes (push) Has been skipped
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Waiting to run
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Waiting to run
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Waiting to run
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Waiting to run
Test (TypeScript) / test (push) Waiting to run
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Waiting to run
Test (TypeScript) / python-fs-shim (push) Waiting to run
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Waiting to run
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Waiting to run
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Waiting to run
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Waiting to run
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Waiting to run
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Waiting to run
Test (Install) / ts-minimal (push) Waiting to run
297 lines
9.5 KiB
Python
297 lines
9.5 KiB
Python
# ========= 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"
|