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
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:
@@ -0,0 +1,20 @@
|
||||
# ========= 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.util
|
||||
|
||||
collect_ignore_glob: list[str] = []
|
||||
|
||||
if importlib.util.find_spec("camel") is None:
|
||||
collect_ignore_glob = ["test_*.py"]
|
||||
@@ -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 asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.agents.camel._async import AsyncRunner
|
||||
|
||||
|
||||
def test_run_from_sync_with_no_loop():
|
||||
runner = AsyncRunner()
|
||||
|
||||
async def coro():
|
||||
await asyncio.sleep(0)
|
||||
return 42
|
||||
|
||||
assert runner.run(coro()) == 42
|
||||
runner.close()
|
||||
|
||||
|
||||
def test_run_returns_exception():
|
||||
runner = AsyncRunner()
|
||||
|
||||
async def boom():
|
||||
raise ValueError("nope")
|
||||
|
||||
with pytest.raises(ValueError, match="nope"):
|
||||
runner.run(boom())
|
||||
runner.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_from_inside_running_loop():
|
||||
runner = AsyncRunner()
|
||||
|
||||
async def coro():
|
||||
await asyncio.sleep(0)
|
||||
return "from-loop"
|
||||
|
||||
result = await asyncio.to_thread(runner.run, coro())
|
||||
assert result == "from-loop"
|
||||
runner.close()
|
||||
@@ -0,0 +1,67 @@
|
||||
# ========= 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 inspect
|
||||
|
||||
from camel.toolkits import BaseToolkit, FileToolkit, FunctionTool
|
||||
|
||||
|
||||
def test_base_toolkit_has_get_tools():
|
||||
assert hasattr(BaseToolkit, "get_tools")
|
||||
|
||||
|
||||
def test_file_toolkit_private_hooks_exist():
|
||||
expected = [
|
||||
"_resolve_filepath",
|
||||
"_resolve_existing_filepath",
|
||||
"_resolve_search_path",
|
||||
"_sanitize_filename",
|
||||
"_create_backup",
|
||||
"_write_text_file",
|
||||
"_write_simple_text_file",
|
||||
"_write_csv_file",
|
||||
"_write_json_file",
|
||||
"_write_docx_file",
|
||||
"_write_pdf_file",
|
||||
"_normalize_notebook_source",
|
||||
"_build_notebook_cell",
|
||||
]
|
||||
missing = [name for name in expected if not hasattr(FileToolkit, name)]
|
||||
assert not missing, f"FileToolkit missing hooks: {missing}"
|
||||
|
||||
|
||||
def test_file_toolkit_public_methods_signatures():
|
||||
public = [
|
||||
"write_to_file",
|
||||
"read_file",
|
||||
"edit_file",
|
||||
"search_files",
|
||||
"notebook_edit_cell",
|
||||
"glob_files",
|
||||
"grep_files",
|
||||
]
|
||||
missing = [name for name in public if not hasattr(FileToolkit, name)]
|
||||
assert not missing, f"FileToolkit missing public methods: {missing}"
|
||||
for name in public:
|
||||
sig = inspect.signature(getattr(FileToolkit, name))
|
||||
assert "self" in sig.parameters
|
||||
|
||||
|
||||
def test_function_tool_constructible():
|
||||
|
||||
def sample() -> str:
|
||||
return "ok"
|
||||
|
||||
tool = FunctionTool(sample)
|
||||
assert tool is not None
|
||||
@@ -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. =========
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage import MountMode, Workspace
|
||||
from mirage.agents.camel import MirageFileToolkit
|
||||
from mirage.resource.ram import RAMResource
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def workspace():
|
||||
ram = RAMResource()
|
||||
yield Workspace({"/": ram}, mode=MountMode.WRITE)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def toolkit(workspace):
|
||||
tk = MirageFileToolkit(workspace)
|
||||
yield tk
|
||||
tk.close()
|
||||
|
||||
|
||||
def _strip_markdown_code_fence(text: str) -> str:
|
||||
text = text.strip()
|
||||
if text.startswith("```"):
|
||||
lines = text.splitlines()
|
||||
return "\n".join(
|
||||
lines[1:-1] if lines[-1].startswith("```") else lines[1:])
|
||||
return text
|
||||
|
||||
|
||||
def test_write_text_file_then_read(toolkit):
|
||||
msg = toolkit.write_to_file(title="Hello",
|
||||
content="hi there",
|
||||
filename="/notes/hello.md")
|
||||
assert "hello.md" in msg
|
||||
out = toolkit.read_file(file_paths="/notes/hello.md")
|
||||
assert "hi there" in out
|
||||
|
||||
|
||||
def test_write_json_file_then_read(toolkit):
|
||||
msg = toolkit.write_to_file(title="data",
|
||||
content={
|
||||
"a": 1,
|
||||
"b": [2, 3]
|
||||
},
|
||||
filename="/data.json")
|
||||
assert "data.json" in msg
|
||||
out = toolkit.read_file(file_paths="/data.json")
|
||||
parsed = json.loads(_strip_markdown_code_fence(out))
|
||||
assert parsed == {"a": 1, "b": [2, 3]}
|
||||
|
||||
|
||||
def test_edit_file_replaces_content(toolkit):
|
||||
toolkit.write_to_file(title="t", content="old\nkeep\n", filename="/e.txt")
|
||||
msg = toolkit.edit_file(file_path="/e.txt",
|
||||
old_content="old",
|
||||
new_content="new")
|
||||
assert "successfully" in msg.lower() or "edited" in msg.lower()
|
||||
out = toolkit.read_file(file_paths="/e.txt")
|
||||
assert "new" in out and "keep" in out and "old" not in out.split("keep")[0]
|
||||
|
||||
|
||||
def test_search_files_by_name(toolkit):
|
||||
toolkit.write_to_file(title="a", content="x", filename="/dir/a.txt")
|
||||
toolkit.write_to_file(title="b", content="y", filename="/dir/b.md")
|
||||
out = toolkit.search_files(file_name="a.txt", path="/dir")
|
||||
assert "a.txt" in out
|
||||
|
||||
|
||||
def test_glob_files(toolkit):
|
||||
toolkit.write_to_file(title="a", content="x", filename="/g/x.py")
|
||||
toolkit.write_to_file(title="a", content="y", filename="/g/y.py")
|
||||
out = toolkit.glob_files(pattern="*.py", path="/g")
|
||||
assert "x.py" in out and "y.py" in out
|
||||
|
||||
|
||||
def test_grep_files(toolkit):
|
||||
toolkit.write_to_file(title="a",
|
||||
content="needle here\n",
|
||||
filename="/q/a.txt")
|
||||
toolkit.write_to_file(title="b", content="haystack\n", filename="/q/b.txt")
|
||||
out = toolkit.grep_files(pattern="needle", path="/q")
|
||||
assert "needle" in out
|
||||
assert "a.txt" in out
|
||||
@@ -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. =========
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage import MountMode, Workspace
|
||||
from mirage.agents.camel import MirageTerminalToolkit
|
||||
from mirage.resource.ram import RAMResource
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def workspace():
|
||||
ram = RAMResource()
|
||||
ws = Workspace({"/": ram}, mode=MountMode.WRITE)
|
||||
yield ws
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def toolkit(workspace):
|
||||
tk = MirageTerminalToolkit(workspace)
|
||||
yield tk
|
||||
tk.close()
|
||||
|
||||
|
||||
def test_shell_exec_blocking_returns_stdout(toolkit):
|
||||
out = toolkit.shell_exec(id="t1", command="echo hello", block=True)
|
||||
assert "hello" in out
|
||||
|
||||
|
||||
def test_shell_exec_blocking_captures_stderr(toolkit):
|
||||
out = toolkit.shell_exec(id="t1",
|
||||
command="ls /nonexistent-zzz",
|
||||
block=True)
|
||||
assert out, "expected non-empty output for failing command"
|
||||
|
||||
|
||||
def test_shell_write_content_to_file(toolkit, workspace):
|
||||
msg = toolkit.shell_write_content_to_file(content="line1\nline2\n",
|
||||
file_path="/note.txt")
|
||||
assert "note.txt" in msg
|
||||
out = toolkit.shell_exec(id="t1", command="cat /note.txt", block=True)
|
||||
assert "line1" in out and "line2" in out
|
||||
|
||||
|
||||
def test_shell_exec_nonblocking_returns_session_id(toolkit):
|
||||
msg = toolkit.shell_exec(id="bg1", command="sleep 0.5", block=False)
|
||||
assert "bg1" in msg
|
||||
|
||||
|
||||
def test_shell_view_after_completion(toolkit):
|
||||
toolkit.shell_exec(id="bg2", command="echo done", block=False)
|
||||
out = toolkit.shell_view(id="bg2")
|
||||
assert "done" in out
|
||||
|
||||
|
||||
def test_shell_kill_unknown_session(toolkit):
|
||||
msg = toolkit.shell_kill_process(id="never-started")
|
||||
assert "Error" in msg
|
||||
|
||||
|
||||
def test_shell_write_to_process_returns_clear_error(toolkit):
|
||||
toolkit.shell_exec(id="bg3", command="sleep 0.5", block=False)
|
||||
msg = toolkit.shell_write_to_process(id="bg3", command="anything")
|
||||
assert "not interactive" in msg.lower()
|
||||
|
||||
|
||||
def test_get_tools_returns_six(toolkit):
|
||||
tools = toolkit.get_tools()
|
||||
assert len(tools) == 6
|
||||
@@ -0,0 +1,42 @@
|
||||
import pytest
|
||||
|
||||
claude_agent_sdk = pytest.importorskip("claude_agent_sdk")
|
||||
|
||||
from mirage import MountMode, RAMResource, Workspace # noqa: E402
|
||||
from mirage.agents.claude_agent_sdk.options import build_options # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def workspace():
|
||||
return Workspace({"/": RAMResource()}, mode=MountMode.WRITE)
|
||||
|
||||
|
||||
def test_build_options_returns_claude_agent_options(workspace):
|
||||
options = build_options(workspace)
|
||||
assert isinstance(options, claude_agent_sdk.ClaudeAgentOptions)
|
||||
|
||||
|
||||
def test_build_options_has_mirage_server(workspace):
|
||||
options = build_options(workspace)
|
||||
assert "mirage" in options.mcp_servers
|
||||
|
||||
|
||||
def test_build_options_allowed_tools(workspace):
|
||||
options = build_options(workspace)
|
||||
assert "mcp__mirage__*" in options.allowed_tools
|
||||
|
||||
|
||||
def test_build_options_disables_builtin_tools(workspace):
|
||||
options = build_options(workspace)
|
||||
assert options.tools == []
|
||||
|
||||
|
||||
def test_build_options_custom_system_prompt(workspace):
|
||||
options = build_options(workspace, system_prompt="custom prompt")
|
||||
assert options.system_prompt == "custom prompt"
|
||||
|
||||
|
||||
def test_build_options_default_system_prompt(workspace):
|
||||
options = build_options(workspace)
|
||||
assert options.system_prompt is not None
|
||||
assert len(options.system_prompt) > 0
|
||||
@@ -0,0 +1,167 @@
|
||||
import pytest
|
||||
|
||||
pytest.importorskip("claude_agent_sdk")
|
||||
|
||||
from mirage import MountMode, RAMResource, Workspace # noqa: E402
|
||||
from mirage.agents.claude_agent_sdk.server import _MirageTools # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def workspace():
|
||||
return Workspace({"/": RAMResource()}, mode=MountMode.WRITE)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tools(workspace):
|
||||
return _MirageTools(workspace)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_command_echo(tools):
|
||||
result = await tools.execute_command({"command": "echo hello"})
|
||||
assert "hello" in result["content"][0]["text"]
|
||||
assert result.get("is_error") is not True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_command_pipe(tools, workspace):
|
||||
await workspace.ops.write("/pipe.txt", b"aaa\nbbb\naaa\n")
|
||||
result = await tools.execute_command(
|
||||
{"command": "cat /pipe.txt | sort | uniq | wc -l"})
|
||||
assert "2" in result["content"][0]["text"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_file(tools, workspace):
|
||||
await workspace.ops.write("/hello.txt", b"line1\nline2\nline3\n")
|
||||
result = await tools.read({"path": "/hello.txt"})
|
||||
text = result["content"][0]["text"]
|
||||
assert "line1" in text
|
||||
assert "line2" in text
|
||||
assert result.get("is_error") is not True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_file_with_offset_and_limit(tools, workspace):
|
||||
await workspace.ops.write("/multi.txt", b"a\nb\nc\nd\ne\n")
|
||||
result = await tools.read({"path": "/multi.txt", "offset": 1, "limit": 2})
|
||||
text = result["content"][0]["text"]
|
||||
assert "b" in text
|
||||
assert "c" in text
|
||||
assert "a" not in text
|
||||
assert "d" not in text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_file_not_found(tools):
|
||||
result = await tools.read({"path": "/nonexistent.txt"})
|
||||
assert result["is_error"] is True
|
||||
assert "not found" in result["content"][0]["text"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_file(tools, workspace):
|
||||
result = await tools.write({"path": "/new.txt", "content": "hello world"})
|
||||
assert result.get("is_error") is not True
|
||||
data = await workspace.ops.read("/new.txt")
|
||||
assert data == b"hello world"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_file_already_exists(tools, workspace):
|
||||
await workspace.ops.write("/exists.txt", b"first")
|
||||
result = await tools.write({"path": "/exists.txt", "content": "second"})
|
||||
assert result["is_error"] is True
|
||||
assert "already exists" in result["content"][0]["text"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_edit_file(tools, workspace):
|
||||
await workspace.ops.write("/edit.txt", b"foo bar baz")
|
||||
result = await tools.edit({
|
||||
"path": "/edit.txt",
|
||||
"old_string": "bar",
|
||||
"new_string": "qux"
|
||||
})
|
||||
assert result.get("is_error") is not True
|
||||
data = await workspace.ops.read("/edit.txt")
|
||||
assert data == b"foo qux baz"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_edit_file_not_found(tools):
|
||||
result = await tools.edit({
|
||||
"path": "/missing.txt",
|
||||
"old_string": "x",
|
||||
"new_string": "y"
|
||||
})
|
||||
assert result["is_error"] is True
|
||||
assert "not found" in result["content"][0]["text"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_edit_string_not_found(tools, workspace):
|
||||
await workspace.ops.write("/nostr.txt", b"hello world")
|
||||
result = await tools.edit({
|
||||
"path": "/nostr.txt",
|
||||
"old_string": "xyz",
|
||||
"new_string": "abc"
|
||||
})
|
||||
assert result["is_error"] is True
|
||||
assert "not found" in result["content"][0]["text"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_edit_multiple_occurrences_without_replace_all(tools, workspace):
|
||||
await workspace.ops.write("/multi.txt", b"aa bb aa")
|
||||
result = await tools.edit({
|
||||
"path": "/multi.txt",
|
||||
"old_string": "aa",
|
||||
"new_string": "cc"
|
||||
})
|
||||
assert result["is_error"] is True
|
||||
assert "replace_all" in result["content"][0]["text"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_edit_replace_all(tools, workspace):
|
||||
await workspace.ops.write("/all.txt", b"aa bb aa")
|
||||
result = await tools.edit({
|
||||
"path": "/all.txt",
|
||||
"old_string": "aa",
|
||||
"new_string": "cc",
|
||||
"replace_all": True
|
||||
})
|
||||
assert result.get("is_error") is not True
|
||||
data = await workspace.ops.read("/all.txt")
|
||||
assert data == b"cc bb cc"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_creates_parent_dirs(tools, workspace):
|
||||
result = await tools.write({
|
||||
"path": "/nested/deep/file.txt",
|
||||
"content": "hi"
|
||||
})
|
||||
assert result.get("is_error") is not True
|
||||
data = await workspace.ops.read("/nested/deep/file.txt")
|
||||
assert data == b"hi"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ls(tools, workspace):
|
||||
await tools.write({"path": "/dir/a.txt", "content": "a"})
|
||||
await tools.write({"path": "/dir/b.txt", "content": "b"})
|
||||
result = await tools.ls({"path": "/dir"})
|
||||
text = result["content"][0]["text"]
|
||||
assert "a.txt" in text
|
||||
assert "b.txt" in text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_grep(tools, workspace):
|
||||
await workspace.ops.write("/search.txt",
|
||||
b"hello world\ngoodbye world\nhello again\n")
|
||||
result = await tools.grep({"pattern": "hello", "path": "/"})
|
||||
text = result["content"][0]["text"]
|
||||
assert "hello" in text
|
||||
@@ -0,0 +1,123 @@
|
||||
# ========= 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.agents.langchain.backend import LangchainWorkspace
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def workspace():
|
||||
return Workspace({"/": RAMResource()}, mode=MountMode.WRITE)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def backend(workspace):
|
||||
return LangchainWorkspace(workspace)
|
||||
|
||||
|
||||
def test_id(backend):
|
||||
assert backend.id == "mirage"
|
||||
|
||||
|
||||
def test_custom_id(workspace):
|
||||
b = LangchainWorkspace(workspace, sandbox_id="custom")
|
||||
assert b.id == "custom"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aexecute_echo(backend):
|
||||
resp = await backend.aexecute("echo hello")
|
||||
assert resp.exit_code == 0
|
||||
assert "hello" in resp.output
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aexecute_failing_command(backend):
|
||||
resp = await backend.aexecute("cat /nonexistent")
|
||||
assert resp.exit_code != 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_awrite_and_aread(backend):
|
||||
result = await backend.awrite("/test.txt", "hello world")
|
||||
assert result.error is None
|
||||
|
||||
content = await backend.aread("/test.txt")
|
||||
assert "hello world" in content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_awrite_existing_file_errors(backend):
|
||||
await backend.awrite("/exists.txt", "first")
|
||||
result = await backend.awrite("/exists.txt", "second")
|
||||
assert result.error is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aedit(backend):
|
||||
await backend.awrite("/edit.txt", "foo bar baz")
|
||||
result = await backend.aedit("/edit.txt", "bar", "qux")
|
||||
assert result.error is None
|
||||
content = await backend.aread("/edit.txt")
|
||||
assert "qux" in content
|
||||
assert "bar" not in content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_als_info(backend):
|
||||
await backend.awrite("/dir/a.txt", "a")
|
||||
await backend.awrite("/dir/b.txt", "b")
|
||||
entries = await backend.als_info("/dir")
|
||||
paths = [e["path"] for e in entries]
|
||||
assert len(paths) == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agrep_raw(backend):
|
||||
await backend.awrite("/search.txt",
|
||||
"hello world\ngoodbye world\nhello again")
|
||||
result = await backend.agrep_raw("hello", path="/")
|
||||
assert isinstance(result, list)
|
||||
assert len(result) >= 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aglob_info(backend):
|
||||
await backend.awrite("/data/a.txt", "a")
|
||||
await backend.awrite("/data/b.py", "b")
|
||||
entries = await backend.aglob_info("*.txt", path="/data")
|
||||
paths = [e["path"] for e in entries]
|
||||
assert any("a.txt" in p for p in paths)
|
||||
assert not any("b.py" in p for p in paths)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_pipe(backend):
|
||||
await backend.awrite("/pipe.txt", "aaa\nbbb\nccc\naaa\n")
|
||||
resp = await backend.aexecute("cat /pipe.txt | sort | uniq | wc -l")
|
||||
assert resp.exit_code == 0
|
||||
assert "3" in resp.output
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_upload_and_download(backend):
|
||||
files = [("/up1.txt", b"content1"), ("/up2.txt", b"content2")]
|
||||
up_results = await backend.aupload_files(files)
|
||||
assert all(r.error is None for r in up_results)
|
||||
|
||||
down_results = await backend.adownload_files(["/up1.txt", "/up2.txt"])
|
||||
assert down_results[0].content == b"content1"
|
||||
assert down_results[1].content == b"content2"
|
||||
@@ -0,0 +1,72 @@
|
||||
# ========= 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.agents.langchain._convert import (io_to_execute_response,
|
||||
io_to_file_infos,
|
||||
io_to_grep_matches)
|
||||
from mirage.io.types import IOResult
|
||||
|
||||
|
||||
def test_io_to_execute_response_basic():
|
||||
io = IOResult(stdout=b"hello world\n", exit_code=0)
|
||||
resp = io_to_execute_response(io)
|
||||
assert resp.output == "hello world\n"
|
||||
assert resp.exit_code == 0
|
||||
assert resp.truncated is False
|
||||
|
||||
|
||||
def test_io_to_execute_response_none_stdout():
|
||||
io = IOResult(stdout=None, exit_code=1)
|
||||
resp = io_to_execute_response(io)
|
||||
assert resp.output == ""
|
||||
assert resp.exit_code == 1
|
||||
|
||||
|
||||
def test_io_to_execute_response_stderr_appended():
|
||||
io = IOResult(stdout=b"out", stderr=b"err", exit_code=1)
|
||||
resp = io_to_execute_response(io)
|
||||
assert "out" in resp.output
|
||||
assert "err" in resp.output
|
||||
|
||||
|
||||
def test_io_to_grep_matches():
|
||||
io = IOResult(stdout=b"/a.txt:1:hello world\n/b.txt:5:hello there\n",
|
||||
exit_code=0)
|
||||
matches = io_to_grep_matches(io)
|
||||
assert len(matches) == 2
|
||||
assert matches[0]["path"] == "/a.txt"
|
||||
assert matches[0]["line"] == 1
|
||||
assert matches[0]["text"] == "hello world"
|
||||
|
||||
|
||||
def test_io_to_grep_matches_empty():
|
||||
io = IOResult(stdout=b"", exit_code=1)
|
||||
matches = io_to_grep_matches(io)
|
||||
assert matches == []
|
||||
|
||||
|
||||
def test_io_to_file_infos():
|
||||
io = IOResult(stdout=b"/foo/bar.txt\n/foo/baz.py\n/foo/sub/\n",
|
||||
exit_code=0)
|
||||
infos = io_to_file_infos(io)
|
||||
assert len(infos) == 3
|
||||
dirs = [i for i in infos if i.get("is_dir")]
|
||||
assert len(dirs) == 1
|
||||
assert dirs[0]["path"] == "/foo/sub"
|
||||
|
||||
|
||||
def test_io_to_file_infos_empty():
|
||||
io = IOResult(stdout=b"", exit_code=0)
|
||||
infos = io_to_file_infos(io)
|
||||
assert infos == []
|
||||
@@ -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. =========
|
||||
|
||||
from mirage.agents.langchain._messages import extract_text
|
||||
|
||||
|
||||
def test_extract_text_from_string():
|
||||
|
||||
class Msg:
|
||||
content = "hello world"
|
||||
|
||||
assert extract_text([Msg()]) == ["hello world"]
|
||||
|
||||
|
||||
def test_extract_text_from_blocks():
|
||||
|
||||
class Msg:
|
||||
content = [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "hello"
|
||||
},
|
||||
{
|
||||
"type": "tool_use",
|
||||
"name": "ls",
|
||||
"input": {}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "goodbye"
|
||||
},
|
||||
]
|
||||
|
||||
assert extract_text([Msg()]) == ["hello", "goodbye"]
|
||||
|
||||
|
||||
def test_extract_text_skips_empty():
|
||||
|
||||
class Msg:
|
||||
content = [
|
||||
{
|
||||
"type": "text",
|
||||
"text": " "
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "real"
|
||||
},
|
||||
]
|
||||
|
||||
assert extract_text([Msg()]) == ["real"]
|
||||
|
||||
|
||||
def test_extract_text_no_content():
|
||||
|
||||
class Msg:
|
||||
pass
|
||||
|
||||
assert extract_text([Msg()]) == []
|
||||
@@ -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 base64
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.agents.openai_agents.runner import MirageRunner
|
||||
from mirage.types import FileStat, FileType
|
||||
|
||||
|
||||
def _stat(file_type: FileType) -> FileStat:
|
||||
return FileStat(name="x", type=file_type)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ws():
|
||||
workspace = MagicMock()
|
||||
workspace.ops = MagicMock()
|
||||
workspace.ops.stat = AsyncMock()
|
||||
workspace.ops.read = AsyncMock()
|
||||
return workspace
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_blocks_image_inlines_base64(ws):
|
||||
ws.ops.stat.return_value = _stat(FileType.IMAGE_PNG)
|
||||
ws.ops.read.return_value = b"\x89PNG\r\n\x1a\n"
|
||||
runner = MirageRunner(ws)
|
||||
blocks = await runner.build_blocks("hi", ["/img.png"])
|
||||
assert blocks[0] == {"type": "input_text", "text": "hi"}
|
||||
assert blocks[1]["type"] == "input_image"
|
||||
expected_b64 = base64.b64encode(b"\x89PNG\r\n\x1a\n").decode()
|
||||
assert blocks[1]["image_url"] == f"data:image/png;base64,{expected_b64}"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_blocks_pdf_uploads_to_files_api(ws):
|
||||
ws.ops.stat.return_value = _stat(FileType.PDF)
|
||||
ws.ops.read.return_value = b"%PDF-1.4 ..."
|
||||
fake_client = MagicMock()
|
||||
fake_client.files = MagicMock()
|
||||
fake_client.files.create = AsyncMock(return_value=MagicMock(id="file-abc"))
|
||||
runner = MirageRunner(ws, client=fake_client)
|
||||
blocks = await runner.build_blocks("read", ["/doc.pdf"])
|
||||
assert blocks[1] == {"type": "input_file", "file_id": "file-abc"}
|
||||
fake_client.files.create.assert_awaited_once()
|
||||
args, kwargs = fake_client.files.create.call_args
|
||||
assert kwargs["purpose"] == "user_data"
|
||||
fname, fdata = kwargs["file"]
|
||||
assert fname == "doc.pdf"
|
||||
assert fdata == b"%PDF-1.4 ..."
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_blocks_text_decoded_inline(ws):
|
||||
ws.ops.stat.return_value = _stat(FileType.TEXT)
|
||||
ws.ops.read.return_value = b"hello world"
|
||||
runner = MirageRunner(ws)
|
||||
blocks = await runner.build_blocks("look", ["/notes.txt"])
|
||||
assert blocks[1] == {"type": "input_text", "text": "hello world"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_blocks_jpeg(ws):
|
||||
ws.ops.stat.return_value = _stat(FileType.IMAGE_JPEG)
|
||||
ws.ops.read.return_value = b"\xff\xd8\xff..."
|
||||
runner = MirageRunner(ws)
|
||||
blocks = await runner.build_blocks("see", ["/photo.jpg"])
|
||||
assert blocks[1]["type"] == "input_image"
|
||||
assert blocks[1]["image_url"].startswith("data:image/jpeg;base64,")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_blocks_multiple_paths_in_order(ws):
|
||||
types = [FileType.TEXT, FileType.IMAGE_PNG]
|
||||
bytes_seq = [b"first", b"\x89PNG\r\n\x1a\n"]
|
||||
|
||||
async def fake_stat(p):
|
||||
return _stat(types.pop(0))
|
||||
|
||||
async def fake_read(p):
|
||||
return bytes_seq.pop(0)
|
||||
|
||||
ws.ops.stat.side_effect = fake_stat
|
||||
ws.ops.read.side_effect = fake_read
|
||||
runner = MirageRunner(ws)
|
||||
blocks = await runner.build_blocks("two", ["/a.txt", "/b.png"])
|
||||
assert len(blocks) == 3
|
||||
assert blocks[1] == {"type": "input_text", "text": "first"}
|
||||
assert blocks[2]["type"] == "input_image"
|
||||
@@ -0,0 +1,87 @@
|
||||
# ========= 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
|
||||
from pathlib import Path
|
||||
|
||||
from mirage.agents.openai_agents.sandbox import MirageSandboxClient
|
||||
from mirage.resource.ram import RAMResource
|
||||
from mirage.types import MountMode
|
||||
from mirage.workspace import Workspace
|
||||
|
||||
|
||||
def _make_client() -> MirageSandboxClient:
|
||||
ram = RAMResource()
|
||||
ws = Workspace(
|
||||
{"/": (ram, MountMode.WRITE)},
|
||||
mode=MountMode.WRITE,
|
||||
)
|
||||
return MirageSandboxClient(ws)
|
||||
|
||||
|
||||
def test_create_session_with_default_resources():
|
||||
|
||||
async def _run():
|
||||
client = _make_client()
|
||||
session = await client.create()
|
||||
result = await session.exec("echo hello", shell=False)
|
||||
assert result.exit_code == 0
|
||||
assert b"hello" in result.stdout
|
||||
|
||||
asyncio.run(_run())
|
||||
|
||||
|
||||
def test_session_file_read_write():
|
||||
|
||||
async def _run():
|
||||
client = _make_client()
|
||||
session = await client.create()
|
||||
|
||||
content = b"test file content"
|
||||
await session.write(Path("myfile.txt"), io.BytesIO(content))
|
||||
|
||||
stream = await session.read(Path("myfile.txt"))
|
||||
assert stream.read() == content
|
||||
|
||||
asyncio.run(_run())
|
||||
|
||||
|
||||
def test_persist_and_hydrate_workspace():
|
||||
|
||||
async def _run():
|
||||
client = _make_client()
|
||||
session = await client.create()
|
||||
await session.write(Path("data.txt"), io.BytesIO(b"persist me"))
|
||||
|
||||
snapshot = await session.persist_workspace()
|
||||
|
||||
client2 = _make_client()
|
||||
session2 = await client2.create()
|
||||
await session2.hydrate_workspace(snapshot)
|
||||
|
||||
stream = await session2.read(Path("data.txt"))
|
||||
assert stream.read() == b"persist me"
|
||||
|
||||
asyncio.run(_run())
|
||||
|
||||
|
||||
def test_session_running_state():
|
||||
|
||||
async def _run():
|
||||
client = _make_client()
|
||||
session = await client.create()
|
||||
assert await session.running() is True
|
||||
|
||||
asyncio.run(_run())
|
||||
@@ -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. =========
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
pytest.importorskip("openhands")
|
||||
|
||||
from mirage.agents.openhands import MirageWorkspace # noqa: E402
|
||||
from mirage.resource.ram import RAMResource # noqa: E402
|
||||
from mirage.types import MountMode # noqa: E402
|
||||
from mirage.workspace import Workspace # noqa: E402
|
||||
|
||||
|
||||
def _make_backing() -> Workspace:
|
||||
ram = RAMResource()
|
||||
return Workspace({"/": (ram, MountMode.WRITE)}, mode=MountMode.WRITE)
|
||||
|
||||
|
||||
def test_execute_command_basic():
|
||||
with MirageWorkspace(workspace=_make_backing()) as mw:
|
||||
result = mw.execute_command("echo hello")
|
||||
assert result.exit_code == 0
|
||||
assert "hello" in result.stdout
|
||||
assert result.command == "echo hello"
|
||||
assert result.timeout_occurred is False
|
||||
|
||||
|
||||
def test_execute_command_with_cwd():
|
||||
with MirageWorkspace(workspace=_make_backing()) as mw:
|
||||
mw.execute_command("echo data > /file.txt")
|
||||
result = mw.execute_command("cat file.txt", cwd="/")
|
||||
assert result.exit_code == 0, result.stderr
|
||||
assert "data" in result.stdout
|
||||
|
||||
|
||||
def test_execute_command_nonzero_exit():
|
||||
with MirageWorkspace(workspace=_make_backing()) as mw:
|
||||
result = mw.execute_command("cat /no/such/path")
|
||||
assert result.exit_code != 0
|
||||
|
||||
|
||||
def test_file_upload_then_download(tmp_path: Path):
|
||||
src = tmp_path / "src.bin"
|
||||
src.write_bytes(b"abc 123")
|
||||
with MirageWorkspace(workspace=_make_backing()) as mw:
|
||||
up = mw.file_upload(src, "/uploaded.bin")
|
||||
assert up.success is True
|
||||
assert up.file_size == 7
|
||||
|
||||
cat = mw.execute_command("cat /uploaded.bin")
|
||||
assert cat.exit_code == 0
|
||||
assert "abc 123" in cat.stdout
|
||||
|
||||
out = tmp_path / "out.bin"
|
||||
dn = mw.file_download("/uploaded.bin", out)
|
||||
assert dn.success is True
|
||||
assert dn.file_size == 7
|
||||
assert out.read_bytes() == b"abc 123"
|
||||
|
||||
|
||||
def test_file_upload_into_nested_dir(tmp_path: Path):
|
||||
src = tmp_path / "src.txt"
|
||||
src.write_bytes(b"nested")
|
||||
with MirageWorkspace(workspace=_make_backing()) as mw:
|
||||
up = mw.file_upload(src, "/sub/dir/file.txt")
|
||||
assert up.success is True
|
||||
|
||||
cat = mw.execute_command("cat /sub/dir/file.txt")
|
||||
assert cat.exit_code == 0
|
||||
assert "nested" in cat.stdout
|
||||
|
||||
|
||||
def test_file_download_missing_source_returns_error(tmp_path: Path):
|
||||
out = tmp_path / "out.txt"
|
||||
with MirageWorkspace(workspace=_make_backing()) as mw:
|
||||
dn = mw.file_download("/does/not/exist.txt", out)
|
||||
assert dn.success is False
|
||||
assert dn.error
|
||||
assert out.exists() is False
|
||||
|
||||
|
||||
def test_context_manager_closes_backing_workspace():
|
||||
backing = _make_backing()
|
||||
with MirageWorkspace(workspace=backing) as mw:
|
||||
result = mw.execute_command("echo ctx")
|
||||
assert result.exit_code == 0
|
||||
assert backing._closed is True
|
||||
|
||||
|
||||
def test_git_methods_not_supported():
|
||||
with MirageWorkspace(workspace=_make_backing()) as mw:
|
||||
with pytest.raises(NotImplementedError):
|
||||
mw.git_changes("/")
|
||||
with pytest.raises(NotImplementedError):
|
||||
mw.git_diff("/some/path")
|
||||
@@ -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. =========
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from pydantic_ai import ToolReturn
|
||||
|
||||
from mirage import MountMode, RAMResource, Workspace
|
||||
from mirage.agents.pydantic_ai.backend import PydanticAIWorkspace
|
||||
|
||||
DATA_DIR = Path(__file__).resolve().parents[4] / "data"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def workspace():
|
||||
return Workspace({"/": RAMResource()}, mode=MountMode.WRITE)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def backend(workspace):
|
||||
return PydanticAIWorkspace(workspace)
|
||||
|
||||
|
||||
def test_id(backend):
|
||||
assert backend.id == "mirage"
|
||||
|
||||
|
||||
def test_custom_id(workspace):
|
||||
b = PydanticAIWorkspace(workspace, sandbox_id="custom")
|
||||
assert b.id == "custom"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aexecute_echo(backend):
|
||||
resp = await backend.aexecute("echo hello")
|
||||
assert resp.exit_code == 0
|
||||
assert "hello" in resp.output
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aexecute_failing_command(backend):
|
||||
resp = await backend.aexecute("cat /nonexistent")
|
||||
assert resp.exit_code != 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_awrite_and_aread(backend):
|
||||
result = await backend.awrite("/test.txt", "hello world")
|
||||
assert result.error is None
|
||||
|
||||
content = await backend.aread("/test.txt")
|
||||
assert "hello world" in content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_awrite_existing_file_errors(backend):
|
||||
await backend.awrite("/exists.txt", "first")
|
||||
result = await backend.awrite("/exists.txt", "second")
|
||||
assert result.error is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aedit(backend):
|
||||
await backend.awrite("/edit.txt", "foo bar baz")
|
||||
result = await backend.aedit("/edit.txt", "bar", "qux")
|
||||
assert result.error is None
|
||||
content = await backend.aread("/edit.txt")
|
||||
assert "qux" in content
|
||||
assert "bar" not in content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_als_info(backend):
|
||||
await backend.awrite("/dir/a.txt", "a")
|
||||
await backend.awrite("/dir/b.txt", "b")
|
||||
entries = await backend.als_info("/dir")
|
||||
paths = [e["path"] for e in entries]
|
||||
assert len(paths) == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agrep_raw(backend):
|
||||
await backend.awrite("/search.txt",
|
||||
"hello world\ngoodbye world\nhello again")
|
||||
result = await backend.agrep_raw("hello", path="/")
|
||||
assert isinstance(result, list)
|
||||
assert len(result) >= 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aglob_info(backend):
|
||||
await backend.awrite("/data/a.txt", "a")
|
||||
await backend.awrite("/data/b.py", "b")
|
||||
entries = await backend.aglob_info("*.txt", path="/data")
|
||||
paths = [e["path"] for e in entries]
|
||||
assert any("a.txt" in p for p in paths)
|
||||
assert not any("b.py" in p for p in paths)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_pipe(backend):
|
||||
await backend.awrite("/pipe.txt", "aaa\nbbb\nccc\naaa\n")
|
||||
resp = await backend.aexecute("cat /pipe.txt | sort | uniq | wc -l")
|
||||
assert resp.exit_code == 0
|
||||
assert "3" in resp.output
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_bytes(backend):
|
||||
await backend.awrite("/bytes.txt", "binary content")
|
||||
data = await backend._aread_bytes("/bytes.txt")
|
||||
assert data == b"binary content"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aread_pdf_returns_tool_return(backend, workspace):
|
||||
with open(DATA_DIR / "example.pdf", "rb") as f:
|
||||
pdf_bytes = f.read()
|
||||
await workspace.ops.write("/report.pdf", pdf_bytes)
|
||||
|
||||
result = await backend.aread("/report.pdf")
|
||||
assert isinstance(result, ToolReturn)
|
||||
assert "report.pdf" in result.return_value
|
||||
assert len(result.content) > 0
|
||||
@@ -0,0 +1,98 @@
|
||||
# ========= 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 dataclasses import dataclass
|
||||
|
||||
import pytest
|
||||
from pydantic_ai import Agent
|
||||
from pydantic_ai.models.test import TestModel
|
||||
from pydantic_ai_backends import create_console_toolset
|
||||
|
||||
from mirage import MountMode, RAMResource, Workspace
|
||||
from mirage.agents.pydantic_ai.backend import PydanticAIWorkspace
|
||||
|
||||
|
||||
@dataclass
|
||||
class Deps:
|
||||
backend: PydanticAIWorkspace
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def workspace():
|
||||
return Workspace({"/": RAMResource()}, mode=MountMode.WRITE)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def backend(workspace):
|
||||
return PydanticAIWorkspace(workspace)
|
||||
|
||||
|
||||
def test_agent_write_and_read(backend):
|
||||
agent = Agent(
|
||||
TestModel(call_tools=["write_file", "read_file"]),
|
||||
deps_type=Deps,
|
||||
toolsets=[create_console_toolset()],
|
||||
)
|
||||
result = agent.run_sync("write a file", deps=Deps(backend=backend))
|
||||
assert result.output
|
||||
|
||||
|
||||
def test_agent_ls(backend, workspace):
|
||||
import asyncio
|
||||
asyncio.run(backend.awrite("/data/file.txt", "hello"))
|
||||
|
||||
agent = Agent(
|
||||
TestModel(call_tools=["ls"]),
|
||||
deps_type=Deps,
|
||||
toolsets=[create_console_toolset()],
|
||||
)
|
||||
result = agent.run_sync("list files in /data", deps=Deps(backend=backend))
|
||||
assert result.output
|
||||
|
||||
|
||||
def test_agent_edit(backend):
|
||||
import asyncio
|
||||
asyncio.run(backend.awrite("/code.py", "x = 1\ny = 2\n"))
|
||||
|
||||
agent = Agent(
|
||||
TestModel(call_tools=["edit_file"]),
|
||||
deps_type=Deps,
|
||||
toolsets=[create_console_toolset()],
|
||||
)
|
||||
result = agent.run_sync("change x to 42", deps=Deps(backend=backend))
|
||||
assert result.output
|
||||
|
||||
|
||||
def test_agent_grep(backend):
|
||||
import asyncio
|
||||
asyncio.run(backend.awrite("/hello.txt", "hello world\ngoodbye world\n"))
|
||||
|
||||
agent = Agent(
|
||||
TestModel(call_tools=["grep"]),
|
||||
deps_type=Deps,
|
||||
toolsets=[create_console_toolset()],
|
||||
)
|
||||
result = agent.run_sync("search for hello", deps=Deps(backend=backend))
|
||||
assert result.output
|
||||
|
||||
|
||||
def test_no_real_filesystem(backend, workspace):
|
||||
import asyncio
|
||||
asyncio.run(backend.awrite("/test.txt", "in-memory content"))
|
||||
|
||||
content = asyncio.run(workspace.ops.read("/test.txt"))
|
||||
assert content == b"in-memory content"
|
||||
|
||||
import os
|
||||
assert not os.path.exists("/test.txt")
|
||||
@@ -0,0 +1,34 @@
|
||||
from mirage.agents.io_text import decode, io_to_str
|
||||
from mirage.io.types import IOResult
|
||||
|
||||
|
||||
def test_decode_none_returns_empty():
|
||||
assert decode(None) == ""
|
||||
|
||||
|
||||
def test_decode_bytes():
|
||||
assert decode(b"hello") == "hello"
|
||||
|
||||
|
||||
def test_decode_invalid_utf8_replaces():
|
||||
assert decode(b"\xff") == "�"
|
||||
|
||||
|
||||
def test_io_to_str_stdout_only():
|
||||
assert io_to_str(IOResult(stdout=b"out")) == "out"
|
||||
|
||||
|
||||
def test_io_to_str_stderr_only():
|
||||
assert io_to_str(IOResult(stderr=b"err")) == "err"
|
||||
|
||||
|
||||
def test_io_to_str_combines_stdout_and_stderr():
|
||||
assert io_to_str(IOResult(stdout=b"out", stderr=b"err")) == "out\nerr"
|
||||
|
||||
|
||||
async def _stream():
|
||||
yield b"streamed"
|
||||
|
||||
|
||||
def test_io_to_str_ignores_unmaterialized_stream():
|
||||
assert io_to_str(IOResult(stdout=_stream())) == ""
|
||||
@@ -0,0 +1,98 @@
|
||||
# ========= 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 sys
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class _BlockDeepagents:
|
||||
|
||||
def find_spec(self, name, path=None, target=None):
|
||||
if name == "deepagents" or name.startswith("deepagents."):
|
||||
raise ModuleNotFoundError(f"blocked import of {name}")
|
||||
return None
|
||||
|
||||
|
||||
def _evict_agents_and_deepagents(saved):
|
||||
for name in list(sys.modules):
|
||||
if (name == "deepagents" or name.startswith("deepagents.")
|
||||
or name == "mirage.agents"
|
||||
or name.startswith("mirage.agents.")):
|
||||
saved[name] = sys.modules.pop(name)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def deepagents_blocked():
|
||||
"""Simulate an environment where `deepagents` is not installed.
|
||||
|
||||
Yields:
|
||||
None: The fixture only manipulates import state; nothing is returned.
|
||||
"""
|
||||
blocker = _BlockDeepagents()
|
||||
sys.meta_path.insert(0, blocker)
|
||||
saved = {}
|
||||
_evict_agents_and_deepagents(saved)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
sys.meta_path.remove(blocker)
|
||||
for name in list(sys.modules):
|
||||
if (name == "deepagents" or name.startswith("deepagents.")
|
||||
or name == "mirage.agents"
|
||||
or name.startswith("mirage.agents.")):
|
||||
del sys.modules[name]
|
||||
sys.modules.update(saved)
|
||||
|
||||
|
||||
def test_pydantic_ai_prompt_imports_without_deepagents(deepagents_blocked):
|
||||
mod = importlib.import_module("mirage.agents.pydantic_ai.prompt")
|
||||
assert isinstance(mod.MIRAGE_SYSTEM_PROMPT, str)
|
||||
assert callable(mod.build_system_prompt)
|
||||
assert mod.build_system_prompt() == mod.MIRAGE_SYSTEM_PROMPT
|
||||
|
||||
|
||||
def test_openai_agents_prompt_imports_without_deepagents(deepagents_blocked):
|
||||
mod = importlib.import_module("mirage.agents.openai_agents.prompt")
|
||||
assert isinstance(mod.MIRAGE_SYSTEM_PROMPT, str)
|
||||
assert callable(mod.build_system_prompt)
|
||||
|
||||
|
||||
def test_pydantic_ai_package_imports_without_deepagents(deepagents_blocked):
|
||||
mod = importlib.import_module("mirage.agents.pydantic_ai")
|
||||
assert isinstance(mod.MIRAGE_SYSTEM_PROMPT, str)
|
||||
assert mod.PydanticAIWorkspace is not None
|
||||
|
||||
|
||||
def test_openai_agents_package_imports_without_deepagents(deepagents_blocked):
|
||||
mod = importlib.import_module("mirage.agents.openai_agents")
|
||||
assert isinstance(mod.MIRAGE_SYSTEM_PROMPT, str)
|
||||
assert mod.MirageRunner is not None
|
||||
|
||||
|
||||
def test_langchain_full_import_fails_without_deepagents(deepagents_blocked):
|
||||
with pytest.raises(ModuleNotFoundError):
|
||||
importlib.import_module("mirage.agents.langchain")
|
||||
|
||||
|
||||
def test_all_prompts_share_same_content():
|
||||
from mirage.agents.langchain import prompt as lc
|
||||
from mirage.agents.openai_agents import prompt as oa
|
||||
from mirage.agents.prompts import MIRAGE_SYSTEM_PROMPT
|
||||
from mirage.agents.pydantic_ai import prompt as pa
|
||||
|
||||
assert pa.MIRAGE_SYSTEM_PROMPT == MIRAGE_SYSTEM_PROMPT
|
||||
assert oa.MIRAGE_SYSTEM_PROMPT == MIRAGE_SYSTEM_PROMPT
|
||||
assert lc.MIRAGE_SYSTEM_PROMPT == MIRAGE_SYSTEM_PROMPT
|
||||
Reference in New Issue
Block a user