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
+67
View File
@@ -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 pytest
from mirage.accessor.hf_buckets import HfBucketsAccessor, HfBucketsConfig
from mirage.resource.secrets import reveal_secret
def test_config_defaults():
cfg = HfBucketsConfig(bucket="myorg/mybkt")
assert cfg.bucket == "myorg/mybkt"
assert cfg.namespace == "myorg"
assert cfg.bucket_name == "mybkt"
assert cfg.token is None
assert cfg.endpoint == "https://huggingface.co"
assert cfg.timeout == 30
assert cfg.key_prefix is None
def test_config_immutable():
cfg = HfBucketsConfig(bucket="myorg/mybkt")
with pytest.raises(Exception):
cfg.bucket = "other/other"
def test_config_rejects_bad_bucket_format():
with pytest.raises(ValueError):
HfBucketsConfig(bucket="just-one-segment")
with pytest.raises(ValueError):
HfBucketsConfig(bucket="too/many/slashes")
with pytest.raises(ValueError):
HfBucketsConfig(bucket="/leading")
def test_config_token_secret():
cfg = HfBucketsConfig(bucket="myorg/mybkt", token="hf_abc123")
assert reveal_secret(cfg.token) == "hf_abc123"
assert "hf_abc123" not in repr(cfg)
def test_accessor_holds_config():
cfg = HfBucketsConfig(bucket="myorg/mybkt")
acc = HfBucketsAccessor(cfg)
assert acc.config is cfg
def test_bucket_uri():
cfg = HfBucketsConfig(bucket="myorg/mybkt")
acc = HfBucketsAccessor(cfg)
assert acc.bucket_uri == "hf://buckets/myorg/mybkt"
def test_key_prefix_normalized():
cfg = HfBucketsConfig(bucket="myorg/mybkt", key_prefix="/data/sub/")
assert cfg.key_prefix == "data/sub/"
+54
View File
@@ -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.accessor.hf_datasets import HfDatasetsAccessor, HfDatasetsConfig
from mirage.resource.secrets import reveal_secret
def test_config_defaults():
cfg = HfDatasetsConfig(repo_id="org/dataset")
assert cfg.repo_id == "org/dataset"
assert cfg.namespace == "org"
assert cfg.repo_name == "dataset"
assert cfg.token is None
assert cfg.endpoint == "https://huggingface.co"
assert cfg.key_prefix is None
assert cfg.revision is None
def test_config_rejects_bad_repo_id():
with pytest.raises(ValueError):
HfDatasetsConfig(repo_id="just-one-segment")
with pytest.raises(ValueError):
HfDatasetsConfig(repo_id="too/many/slashes")
def test_config_token_secret():
cfg = HfDatasetsConfig(repo_id="org/dataset", token="hf_abc123")
assert reveal_secret(cfg.token) == "hf_abc123"
assert "hf_abc123" not in repr(cfg)
def test_accessor_repo_type_and_uri():
cfg = HfDatasetsConfig(repo_id="org/dataset")
acc = HfDatasetsAccessor(cfg)
assert acc.REPO_TYPE == "dataset"
assert acc.bucket_uri == "hf://datasets/org/dataset"
def test_key_prefix_normalized():
cfg = HfDatasetsConfig(repo_id="org/dataset", key_prefix="/data/sub/")
assert cfg.key_prefix == "data/sub/"
+45
View File
@@ -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. =========
import pytest
from mirage.accessor.hf_models import HfModelsAccessor, HfModelsConfig
from mirage.resource.secrets import reveal_secret
def test_config_defaults():
cfg = HfModelsConfig(repo_id="org/model")
assert cfg.repo_id == "org/model"
assert cfg.namespace == "org"
assert cfg.repo_name == "model"
assert cfg.token is None
assert cfg.endpoint == "https://huggingface.co"
assert cfg.revision is None
def test_config_rejects_bad_repo_id():
with pytest.raises(ValueError):
HfModelsConfig(repo_id="just-one-segment")
def test_config_token_secret():
cfg = HfModelsConfig(repo_id="org/model", token="hf_abc123")
assert reveal_secret(cfg.token) == "hf_abc123"
def test_accessor_repo_type_and_uri():
cfg = HfModelsConfig(repo_id="org/model")
acc = HfModelsAccessor(cfg)
assert acc.REPO_TYPE == "model"
assert acc.bucket_uri == "hf://models/org/model"
+45
View File
@@ -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. =========
import pytest
from mirage.accessor.hf_spaces import HfSpacesAccessor, HfSpacesConfig
from mirage.resource.secrets import reveal_secret
def test_config_defaults():
cfg = HfSpacesConfig(repo_id="org/space")
assert cfg.repo_id == "org/space"
assert cfg.namespace == "org"
assert cfg.repo_name == "space"
assert cfg.token is None
assert cfg.endpoint == "https://huggingface.co"
assert cfg.revision is None
def test_config_rejects_bad_repo_id():
with pytest.raises(ValueError):
HfSpacesConfig(repo_id="just-one-segment")
def test_config_token_secret():
cfg = HfSpacesConfig(repo_id="org/space", token="hf_abc123")
assert reveal_secret(cfg.token) == "hf_abc123"
def test_accessor_repo_type_and_uri():
cfg = HfSpacesConfig(repo_id="org/space")
acc = HfSpacesAccessor(cfg)
assert acc.REPO_TYPE == "space"
assert acc.bucket_uri == "hf://spaces/org/space"
+63
View File
@@ -0,0 +1,63 @@
# ========= 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 unittest.mock import MagicMock, patch
import pytest
from pymongo.driver_info import DriverInfo
from mirage.accessor.mongodb import _DRIVER_INFO, MongoDBAccessor
from mirage.resource.mongodb.config import MongoDBConfig
@pytest.fixture
def accessor():
return MongoDBAccessor(config=MongoDBConfig(
uri="mongodb://localhost:27017"))
def test_driver_info_is_pymongo_driverinfo_named_mirage():
assert isinstance(_DRIVER_INFO, DriverInfo)
assert _DRIVER_INFO.name == "Mirage"
@pytest.mark.asyncio
async def test_client_constructs_async_mongo_client_with_driver_info(accessor):
sentinel = MagicMock()
with patch("mirage.accessor.mongodb.AsyncMongoClient",
return_value=sentinel) as ctor:
client = accessor.client
assert client is sentinel
ctor.assert_called_once_with("mongodb://localhost:27017",
driver=_DRIVER_INFO)
@pytest.mark.asyncio
async def test_client_is_cached_per_event_loop(accessor):
with patch("mirage.accessor.mongodb.AsyncMongoClient",
side_effect=lambda *a, **k: MagicMock()) as ctor:
first = accessor.client
second = accessor.client
assert first is second
ctor.assert_called_once()
def test_client_built_outside_event_loop_uses_loopless_key(accessor):
with patch("mirage.accessor.mongodb.AsyncMongoClient",
side_effect=lambda *a, **k: MagicMock()) as ctor:
first = accessor.client
second = accessor.client
assert first is second
ctor.assert_called_once()
assert 0 in accessor._clients
+20
View File
@@ -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"]
+54
View File
@@ -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()
+67
View File
@@ -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
+99
View File
@@ -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")
+34
View File
@@ -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
+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. =========
+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. =========
+45
View File
@@ -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. =========
import time
from mirage.cache.file.entry import CacheEntry
def test_cache_entry_creation():
entry = CacheEntry(cached_at=int(time.time()), size=5)
assert entry.fingerprint is None
assert entry.size == 5
def test_cache_entry_mutable():
entry = CacheEntry(cached_at=100, size=1)
entry.cached_at -= 20
assert entry.cached_at == 80
def test_expired_with_ttl():
entry = CacheEntry(cached_at=1000, size=1, ttl=10)
assert entry.expired is True
def test_not_expired_without_ttl():
entry = CacheEntry(cached_at=int(time.time()), size=1)
assert entry.ttl is None
assert entry.expired is False
def test_not_expired_within_ttl():
entry = CacheEntry(cached_at=int(time.time()), size=1, ttl=3600)
assert entry.expired is False
+300
View File
@@ -0,0 +1,300 @@
# ========= 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.cache.file import io as cache_io
from mirage.cache.file.ram import RAMFileCacheStore
from mirage.io import CachableAsyncIterator, IOResult
from mirage.observe.record import OpRecord
def _read_record(path: str, fingerprint: str | None) -> OpRecord:
return OpRecord(op="read",
path=path,
source="s3",
bytes=0,
timestamp=0,
duration_ms=0,
fingerprint=fingerprint)
@pytest.fixture
def cache():
return RAMFileCacheStore()
# ── cache population via apply_io ────────────────────────────────────────
@pytest.mark.asyncio
async def test_apply_io_caches_reads(cache):
"""Command reads a file → apply_io stores it in cache."""
io = IOResult(
reads={"/data/file.txt": b"hello"},
cache=["/data/file.txt"],
)
await cache_io.apply_io(cache, io)
assert await cache.get("/data/file.txt") == b"hello"
@pytest.mark.asyncio
async def test_apply_io_caches_writes(cache):
"""Command writes a file and marks it cacheable → stored in cache."""
io = IOResult(
writes={"/data/out.txt": b"output"},
cache=["/data/out.txt"],
)
await cache_io.apply_io(cache, io)
assert await cache.get("/data/out.txt") == b"output"
@pytest.mark.asyncio
async def test_apply_io_reads_preferred_over_writes(cache):
"""When both reads and writes exist for same path, read data wins."""
io = IOResult(
reads={"/f.txt": b"read-data"},
writes={"/f.txt": b"write-data"},
cache=["/f.txt"],
)
await cache_io.apply_io(cache, io)
assert await cache.get("/f.txt") == b"read-data"
@pytest.mark.asyncio
async def test_apply_io_multiple_paths(cache):
"""Multiple paths in cache list are all stored."""
io = IOResult(
reads={
"/a.txt": b"aaa",
"/b.txt": b"bbb"
},
cache=["/a.txt", "/b.txt"],
)
await cache_io.apply_io(cache, io)
assert await cache.get("/a.txt") == b"aaa"
assert await cache.get("/b.txt") == b"bbb"
# ── backend fingerprint threading ───────────────────────────────────────
@pytest.mark.asyncio
async def test_apply_io_sets_backend_fingerprint_from_records(cache):
"""A read record with a backend fingerprint (ETag, cTag, sha256)
stamps the cache entry, so ALWAYS-mode is_fresh can match it."""
io = IOResult(reads={"/s3/f.txt": b"hello"}, cache=["/s3/f.txt"])
records = [_read_record("/s3/f.txt", "etag-multipart-2")]
await cache_io.apply_io(cache, io, records=records)
assert await cache.get("/s3/f.txt") == b"hello"
assert await cache.is_fresh("/s3/f.txt", "etag-multipart-2")
@pytest.mark.asyncio
async def test_apply_io_exhausted_stream_uses_record_fingerprint(cache):
"""An exhausted stream read carries its record fingerprint too."""
stream = _make_stream(b"hello")
assert await stream.drain() == b"hello"
io = IOResult(reads={"/s3/f.txt": stream}, cache=["/s3/f.txt"])
records = [_read_record("/s3/f.txt", "etag-multipart-2")]
await cache_io.apply_io(cache, io, records=records)
assert await cache.is_fresh("/s3/f.txt", "etag-multipart-2")
@pytest.mark.asyncio
async def test_apply_io_warm_reapply_preserves_fingerprint(cache):
"""A warm read re-applies cache-served bytes with no backend read
record; the entry's backend fingerprint must survive, not be
replaced by the MD5-of-content default."""
cold = IOResult(reads={"/s3/f.txt": b"hello"}, cache=["/s3/f.txt"])
await cache_io.apply_io(cache,
cold,
records=[_read_record("/s3/f.txt", "etag-3")])
warm = IOResult(reads={"/s3/f.txt": b"hello"}, cache=["/s3/f.txt"])
await cache_io.apply_io(cache, warm, records=[])
assert await cache.is_fresh("/s3/f.txt", "etag-3")
@pytest.mark.asyncio
async def test_apply_io_changed_data_without_record_resets_entry(cache):
"""New bytes with no backend fingerprint still replace the entry."""
cold = IOResult(reads={"/s3/f.txt": b"old"}, cache=["/s3/f.txt"])
await cache_io.apply_io(cache,
cold,
records=[_read_record("/s3/f.txt", "etag-3")])
fresh = IOResult(writes={"/s3/f.txt": b"new"}, cache=["/s3/f.txt"])
await cache_io.apply_io(cache, fresh, records=[])
assert await cache.get("/s3/f.txt") == b"new"
assert not await cache.is_fresh("/s3/f.txt", "etag-3")
@pytest.mark.asyncio
async def test_apply_io_drain_uses_fingerprint_recorded_during_drain():
"""Streaming backends set the record fingerprint lazily when the GET
response arrives, i.e. during the background drain. The drained
entry must pick it up."""
cache = RAMFileCacheStore()
records: list[OpRecord] = []
async def _gen():
records.append(_read_record("/s3/f.txt", "etag-multipart-2"))
yield b"hello"
stream = CachableAsyncIterator(_gen())
io = IOResult(reads={"/s3/f.txt": stream}, cache=["/s3/f.txt"])
await cache_io.apply_io(cache, io, records=records)
await asyncio.sleep(0.05)
assert await cache.get("/s3/f.txt") == b"hello"
assert await cache.is_fresh("/s3/f.txt", "etag-multipart-2")
# ── cache invalidation ──────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_apply_io_write_without_cache_invalidates(cache):
"""Write to a path NOT in cache list → invalidate (remove from cache)."""
await cache.set("/f.txt", b"old")
io = IOResult(writes={"/f.txt": b"new"})
await cache_io.apply_io(cache, io)
assert await cache.get("/f.txt") is None
# ── edge cases ───────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_apply_io_no_cache_no_data_skips(cache):
"""Path in cache list but no data in reads/writes → skip, don't cache."""
io = IOResult(cache=["/missing.txt"])
await cache_io.apply_io(cache, io)
assert await cache.get("/missing.txt") is None
@pytest.mark.asyncio
async def test_apply_io_empty_io(cache):
"""Empty IOResult → no-op."""
io = IOResult()
await cache_io.apply_io(cache, io)
# ── background drain ────────────────────────────────────────────────────
def _make_stream(data: bytes) -> CachableAsyncIterator:
async def _gen():
yield data
return CachableAsyncIterator(_gen())
@pytest.mark.asyncio
async def test_no_duplicate_drain():
"""If a drain is already running for a path, a second apply_io
should not start another drain. The first drain finishes and
caches the data; the second stream is ignored."""
cache = RAMFileCacheStore()
stream1 = _make_stream(b"first")
stream2 = _make_stream(b"second")
io1 = IOResult(reads={"/f.txt": stream1}, cache=["/f.txt"])
await cache_io.apply_io(cache, io1)
assert "/f.txt" in cache._drain_tasks
io2 = IOResult(reads={"/f.txt": stream2}, cache=["/f.txt"])
await cache_io.apply_io(cache, io2)
assert len([k for k in cache._drain_tasks if k == "/f.txt"]) == 1
await asyncio.sleep(0.05)
assert await cache.get("/f.txt") == b"first"
@pytest.mark.asyncio
async def test_no_drain_if_already_cached():
"""If the path is already in cache, don't start a drain even if
apply_io receives an unconsumed stream for it. Existing cached
data is preserved."""
cache = RAMFileCacheStore()
await cache.set("/f.txt", b"cached")
stream = _make_stream(b"new")
io = IOResult(reads={"/f.txt": stream}, cache=["/f.txt"])
await cache_io.apply_io(cache, io)
assert "/f.txt" not in cache._drain_tasks
assert await cache.get("/f.txt") == b"cached"
# ── max_drain_bytes (cancellable cache drain) ───────────────────────────
def _make_chunked_stream(chunks: list[bytes]) -> CachableAsyncIterator:
async def _gen():
for c in chunks:
yield c
return CachableAsyncIterator(_gen())
@pytest.mark.asyncio
async def test_drain_unbounded_when_threshold_none():
"""Default behavior: full drain into cache regardless of size."""
cache = RAMFileCacheStore() # max_drain_bytes=None
chunks = [b"a" * 100 for _ in range(10)] # 1000 bytes total
stream = _make_chunked_stream(chunks)
io = IOResult(reads={"/big.txt": stream}, cache=["/big.txt"])
await cache_io.apply_io(cache, io)
await asyncio.sleep(0.05)
cached = await cache.get("/big.txt")
assert cached is not None and len(cached) == 1000
@pytest.mark.asyncio
async def test_drain_completes_below_threshold():
"""Source is smaller than threshold → full drain, cache populated."""
cache = RAMFileCacheStore(max_drain_bytes=10000)
chunks = [b"x" * 100 for _ in range(5)] # 500 bytes total
stream = _make_chunked_stream(chunks)
io = IOResult(reads={"/small.txt": stream}, cache=["/small.txt"])
await cache_io.apply_io(cache, io)
await asyncio.sleep(0.05)
cached = await cache.get("/small.txt")
assert cached is not None and len(cached) == 500
@pytest.mark.asyncio
async def test_drain_cancelled_above_threshold():
"""Source exceeds threshold → drain stops, partial buffer NOT cached."""
cache = RAMFileCacheStore(max_drain_bytes=300)
chunks = [b"z" * 100 for _ in range(20)] # 2000 bytes total
stream = _make_chunked_stream(chunks)
io = IOResult(reads={"/huge.txt": stream}, cache=["/huge.txt"])
await cache_io.apply_io(cache, io)
await asyncio.sleep(0.05)
assert await cache.get("/huge.txt") is None
@pytest.mark.asyncio
async def test_drain_threshold_per_task_not_shared():
"""Each drain task has its own counter, not a shared workspace pool."""
cache = RAMFileCacheStore(max_drain_bytes=300)
s1 = _make_chunked_stream([b"a" * 100, b"a" * 100]) # 200 < 300
s2 = _make_chunked_stream([b"b" * 100, b"b" * 100]) # 200 < 300
io1 = IOResult(reads={"/a.txt": s1}, cache=["/a.txt"])
io2 = IOResult(reads={"/b.txt": s2}, cache=["/b.txt"])
await cache_io.apply_io(cache, io1)
await cache_io.apply_io(cache, io2)
await asyncio.sleep(0.05)
# Both fit individually under the per-task budget → both cached.
assert await cache.get("/a.txt") is not None
assert await cache.get("/b.txt") is not None
+261
View File
@@ -0,0 +1,261 @@
# ========= 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 patch
import pytest
from mirage.cache.file.ram import RAMFileCacheStore
class TestGetSet:
@pytest.mark.asyncio
async def test_set_and_get(self):
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"hello")
result = await cache.get("/a")
assert result == b"hello"
@pytest.mark.asyncio
async def test_get_miss(self):
cache = RAMFileCacheStore(cache_limit="1MB")
assert await cache.get("/missing") is None
@pytest.mark.asyncio
async def test_set_overwrites(self):
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"first")
await cache.set("/a", b"second")
assert await cache.get("/a") == b"second"
@pytest.mark.asyncio
async def test_default_fingerprint(self):
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"data")
entry = cache._entries.get("/a")
assert entry is not None
assert entry.fingerprint is not None
@pytest.mark.asyncio
async def test_explicit_fingerprint(self):
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"data", fingerprint="etag-123")
entry = cache._entries["/a"]
assert entry.fingerprint == "etag-123"
class TestAdd:
@pytest.mark.asyncio
async def test_add_new_key(self):
cache = RAMFileCacheStore(cache_limit="1MB")
result = await cache.add("/a", b"data")
assert result is True
assert await cache.get("/a") == b"data"
@pytest.mark.asyncio
async def test_add_existing_key(self):
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"first")
result = await cache.add("/a", b"second")
assert result is False
assert await cache.get("/a") == b"first"
class TestMulti:
@pytest.mark.asyncio
async def test_multi_get(self):
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"aaa")
await cache.set("/b", b"bbb")
results = await cache.multi_get(["/a", "/missing", "/b"])
assert results[0] == b"aaa"
assert results[1] is None
assert results[2] == b"bbb"
@pytest.mark.asyncio
async def test_multi_set(self):
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.multi_set([("/a", b"aaa"), ("/b", b"bbb")])
assert await cache.get("/a") == b"aaa"
assert await cache.get("/b") == b"bbb"
class TestExistsRemoveClear:
@pytest.mark.asyncio
async def test_exists(self):
cache = RAMFileCacheStore(cache_limit="1MB")
assert await cache.exists("/a") is False
await cache.set("/a", b"data")
assert await cache.exists("/a") is True
@pytest.mark.asyncio
async def test_remove(self):
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"data")
await cache.remove("/a")
assert await cache.get("/a") is None
@pytest.mark.asyncio
async def test_remove_missing(self):
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.remove("/missing")
@pytest.mark.asyncio
async def test_remove_cancels_drain_task(self):
cache = RAMFileCacheStore(cache_limit="1MB")
cancelled = False
async def slow_drain():
nonlocal cancelled
try:
await asyncio.sleep(10)
except asyncio.CancelledError:
cancelled = True
task = asyncio.create_task(slow_drain())
cache._drain_tasks["/a"] = task
await cache.set("/a", b"data")
await asyncio.sleep(0)
await cache.remove("/a")
await asyncio.sleep(0)
assert cancelled
assert "/a" not in cache._drain_tasks
@pytest.mark.asyncio
async def test_clear(self):
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"aaa")
await cache.set("/b", b"bbb")
await cache.clear()
assert await cache.get("/a") is None
assert await cache.get("/b") is None
assert cache.cache_size == 0
@pytest.mark.asyncio
async def test_clear_concurrent_with_set(self):
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"aaa")
async def do_clear():
await cache.clear()
async def do_set():
await cache.set("/b", b"bbb")
await asyncio.gather(do_clear(), do_set())
actual_size = sum(e.size for e in cache._entries.values())
assert cache._cache_size == actual_size
class TestTTL:
@pytest.mark.asyncio
async def test_ttl_not_expired(self):
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"data", ttl=60)
assert await cache.get("/a") == b"data"
@pytest.mark.asyncio
async def test_ttl_expired(self):
cache = RAMFileCacheStore(cache_limit="1MB")
with patch("mirage.cache.file.entry.time.time", return_value=1000.0):
await cache.set("/a", b"data", ttl=10)
with patch("mirage.cache.file.entry.time.time", return_value=1011.0):
result = await cache.get("/a")
assert result is None
@pytest.mark.asyncio
async def test_no_ttl_never_expires(self):
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"data")
entry = cache._entries["/a"]
assert entry.ttl is None
assert entry.expired is False
class TestFingerprint:
@pytest.mark.asyncio
async def test_is_fresh_match(self):
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"data", fingerprint="etag-1")
assert await cache.is_fresh("/a", "etag-1") is True
@pytest.mark.asyncio
async def test_is_fresh_mismatch(self):
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"data", fingerprint="etag-1")
assert await cache.is_fresh("/a", "etag-2") is False
@pytest.mark.asyncio
async def test_is_fresh_missing(self):
cache = RAMFileCacheStore(cache_limit="1MB")
assert await cache.is_fresh("/missing", "etag-1") is False
class TestEviction:
@pytest.mark.asyncio
async def test_lru_evicts_oldest(self):
cache = RAMFileCacheStore(cache_limit=100)
await cache.set("/a", b"x" * 60)
await cache.set("/b", b"y" * 60)
assert await cache.get("/a") is None
assert await cache.get("/b") == b"y" * 60
@pytest.mark.asyncio
async def test_lru_access_refreshes(self):
cache = RAMFileCacheStore(cache_limit=150)
await cache.set("/a", b"x" * 50)
await cache.set("/b", b"y" * 50)
await cache.get("/a")
await cache.set("/c", b"z" * 60)
assert await cache.get("/a") is not None
assert await cache.get("/b") is None
@pytest.mark.asyncio
async def test_cache_size_tracking(self):
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"x" * 100)
assert cache.cache_size == 100
await cache.set("/b", b"y" * 200)
assert cache.cache_size == 300
await cache.remove("/a")
assert cache.cache_size == 200
@pytest.mark.asyncio
async def test_cache_limit(self):
cache = RAMFileCacheStore(cache_limit="1KB")
assert cache.cache_limit == 1024
@pytest.mark.asyncio
async def test_eviction_does_not_corrupt_size(self):
cache = RAMFileCacheStore(cache_limit=100)
await cache.set("/a", b"x" * 60)
async def concurrent_set():
await cache.set("/a", b"z" * 40)
async def trigger_eviction():
await cache.set("/b", b"y" * 60)
await asyncio.gather(concurrent_set(), trigger_eviction())
actual_size = sum(e.size for e in cache._entries.values())
assert cache._cache_size == actual_size
+101
View File
@@ -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 asyncio
import pytest
from mirage.cache.file.ram import RAMFileCacheStore
@pytest.mark.asyncio
async def test_data_stored_in_store_files():
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/f.txt", b"hello")
assert cache._store.files["/f.txt"] == b"hello"
@pytest.mark.asyncio
async def test_entry_stored_in_entries():
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/f.txt", b"hello")
entry = cache._entries["/f.txt"]
assert entry.size == 5
@pytest.mark.asyncio
async def test_remove_cleans_store():
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/f.txt", b"data")
assert "/f.txt" in cache._store.files
await cache.remove("/f.txt")
assert "/f.txt" not in cache._store.files
assert "/f.txt" not in cache._entries
@pytest.mark.asyncio
async def test_clear_empties_store():
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"aaa")
await cache.set("/b", b"bbb")
await cache.clear()
assert len(cache._store.files) == 0
assert len(cache._entries) == 0
@pytest.mark.asyncio
async def test_locks_cleaned_after_remove():
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"data")
await cache.remove("/a")
assert "/a" not in cache._key_locks
@pytest.mark.asyncio
async def test_locks_cleaned_after_clear():
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"aaa")
await cache.set("/b", b"bbb")
await cache.clear()
assert len(cache._key_locks) == 0
@pytest.mark.asyncio
async def test_locks_cleaned_after_eviction():
cache = RAMFileCacheStore(cache_limit=100)
await cache.set("/a", b"x" * 60)
await cache.set("/b", b"y" * 60)
assert "/a" not in cache._key_locks
@pytest.mark.asyncio
async def test_drain_task_cancelled_on_remove():
cache = RAMFileCacheStore(cache_limit="1MB")
cancelled = False
async def slow():
nonlocal cancelled
try:
await asyncio.sleep(10)
except asyncio.CancelledError:
cancelled = True
task = asyncio.create_task(slow())
cache._drain_tasks["/a"] = task
await cache.set("/a", b"data")
await asyncio.sleep(0)
await cache.remove("/a")
await asyncio.sleep(0)
assert cancelled
+176
View File
@@ -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 asyncio
import os
import pytest
import pytest_asyncio
from mirage.cache.file import io as cache_io
from mirage.cache.file.redis import RedisFileCacheStore
from mirage.io import CachableAsyncIterator, IOResult
from mirage.observe.record import OpRecord
REDIS_URL = os.environ.get("REDIS_URL", "")
pytestmark = pytest.mark.skipif(not REDIS_URL, reason="REDIS_URL not set")
@pytest_asyncio.fixture()
async def cache():
c = RedisFileCacheStore(
cache_limit="1MB",
url=REDIS_URL,
key_prefix="test:cache:",
)
await c.clear()
yield c
await c.clear()
await c._store.close()
@pytest.mark.asyncio
async def test_set_and_get(cache):
await cache.set("/file.txt", b"hello")
result = await cache.get("/file.txt")
assert result == b"hello"
@pytest.mark.asyncio
async def test_get_missing(cache):
result = await cache.get("/nope")
assert result is None
@pytest.mark.asyncio
async def test_remove(cache):
await cache.set("/file.txt", b"data")
await cache.remove("/file.txt")
assert await cache.get("/file.txt") is None
@pytest.mark.asyncio
async def test_exists(cache):
assert await cache.exists("/file.txt") is False
await cache.set("/file.txt", b"data")
assert await cache.exists("/file.txt") is True
@pytest.mark.asyncio
async def test_is_fresh(cache):
await cache.set("/file.txt", b"data", fingerprint="abc123")
assert await cache.is_fresh("/file.txt", "abc123") is True
assert await cache.is_fresh("/file.txt", "different") is False
@pytest.mark.asyncio
async def test_is_fresh_missing(cache):
assert await cache.is_fresh("/nope", "abc") is False
@pytest.mark.asyncio
async def test_clear(cache):
await cache.set("/a.txt", b"a")
await cache.set("/b.txt", b"b")
await cache.clear()
assert await cache.get("/a.txt") is None
assert await cache.get("/b.txt") is None
@pytest.mark.asyncio
async def test_add_new(cache):
result = await cache.add("/file.txt", b"data")
assert result is True
assert await cache.get("/file.txt") == b"data"
@pytest.mark.asyncio
async def test_add_existing(cache):
await cache.set("/file.txt", b"first")
result = await cache.add("/file.txt", b"second")
assert result is False
assert await cache.get("/file.txt") == b"first"
@pytest.mark.asyncio
async def test_set_with_fingerprint(cache):
await cache.set("/file.txt", b"data", fingerprint="fp1")
assert await cache.is_fresh("/file.txt", "fp1") is True
@pytest.mark.asyncio
async def test_cache_limit(cache):
assert cache.cache_limit == 1 * 1024 * 1024
@pytest.mark.asyncio
async def test_key_prefix_isolation():
c1 = RedisFileCacheStore(url=REDIS_URL, key_prefix="test:cache:ns1:")
c2 = RedisFileCacheStore(url=REDIS_URL, key_prefix="test:cache:ns2:")
await c1.clear()
await c2.clear()
await c1.set("/shared", b"from-c1")
assert await c2.get("/shared") is None
assert await c1.get("/shared") == b"from-c1"
await c1.clear()
await c2.clear()
await c1._store.close()
await c2._store.close()
@pytest.mark.asyncio
async def test_apply_io_drains_stream_into_cache(cache):
"""An unexhausted stream must background-drain into the Redis cache
like the RAM store does, carrying the record fingerprint."""
async def _gen():
yield b"drained"
stream = CachableAsyncIterator(_gen())
io = IOResult(reads={"/file.txt": stream}, cache=["/file.txt"])
records = [
OpRecord(op="read",
path="/file.txt",
source="s3",
bytes=0,
timestamp=0,
duration_ms=0,
fingerprint="etag-9")
]
await cache_io.apply_io(cache, io, records=records)
tasks = list(cache._drain_tasks.values())
assert tasks, "drain task must be registered"
await asyncio.gather(*tasks)
assert await cache.get("/file.txt") == b"drained"
assert await cache.is_fresh("/file.txt", "etag-9") is True
@pytest.mark.asyncio
async def test_remove_cancels_pending_drain(cache):
started = asyncio.Event()
async def _gen():
started.set()
await asyncio.sleep(1)
yield b"slow"
stream = CachableAsyncIterator(_gen())
io = IOResult(reads={"/slow.txt": stream}, cache=["/slow.txt"])
await cache_io.apply_io(cache, io)
assert "/slow.txt" in cache._drain_tasks
await started.wait()
await cache.remove("/slow.txt")
assert "/slow.txt" not in cache._drain_tasks
await asyncio.sleep(0.05)
assert await cache.get("/slow.txt") is None
+53
View File
@@ -0,0 +1,53 @@
# ========= 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.cache.file.utils import default_fingerprint, parse_limit
def test_parse_limit_bytes():
assert parse_limit(1024) == 1024
def test_parse_limit_kb():
assert parse_limit("1KB") == 1024
def test_parse_limit_mb():
assert parse_limit("512MB") == 536870912
def test_parse_limit_gb():
assert parse_limit("1GB") == 1073741824
def test_parse_limit_lowercase():
assert parse_limit("10mb") == 10485760
def test_parse_limit_plain_string():
assert parse_limit("1024") == 1024
def test_default_fingerprint():
fp = default_fingerprint(b"hello")
assert isinstance(fp, str)
assert len(fp) == 32
def test_default_fingerprint_deterministic():
assert default_fingerprint(b"same") == default_fingerprint(b"same")
def test_default_fingerprint_different_data():
assert default_fingerprint(b"a") != default_fingerprint(b"b")
+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. =========
+79
View File
@@ -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. =========
from mirage.cache.index import (IndexConfig, IndexEntry, ListResult,
LookupResult, LookupStatus, RedisIndexConfig,
ResourceType)
from mirage.types import IndexType
def test_index_entry_defaults():
entry = IndexEntry(id="1", name="f", resource_type="file")
assert entry.remote_time == ""
assert entry.index_time == ""
assert entry.vfs_name == ""
assert entry.size is None
def test_index_entry_with_size():
entry = IndexEntry(id="1", name="f", resource_type="file", size=1024)
assert entry.size == 1024
def test_lookup_result_not_found():
result = LookupResult(status=LookupStatus.NOT_FOUND)
assert result.entry is None
assert result.status == LookupStatus.NOT_FOUND
def test_lookup_result_with_entry():
entry = IndexEntry(id="1", name="f", resource_type="file")
result = LookupResult(entry=entry)
assert result.entry is not None
assert result.status is None
def test_list_result_with_entries():
result = ListResult(entries=["/a", "/b"])
assert result.entries == ["/a", "/b"]
assert result.status is None
def test_list_result_expired():
result = ListResult(status=LookupStatus.EXPIRED)
assert result.entries is None
def test_resource_type_enum():
assert ResourceType.FILE == "file"
assert ResourceType.FOLDER == "folder"
def test_index_config_defaults():
config = IndexConfig()
assert config.ttl == 600
assert config.type == IndexType.RAM
def test_redis_index_config():
config = RedisIndexConfig(ttl=300, key_prefix="s3:")
assert config.type == IndexType.REDIS
assert config.ttl == 300
assert config.key_prefix == "s3:"
assert config.url == "redis://localhost:6379/0"
def test_redis_index_config_is_index_config():
config = RedisIndexConfig()
assert isinstance(config, IndexConfig)
+78
View File
@@ -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 datetime import datetime
import pytest
from mirage.cache.index import IndexEntry, RAMIndexCacheStore
@pytest.fixture
def store():
return RAMIndexCacheStore(ttl=60)
@pytest.mark.asyncio
async def test_entries_stored_in_dict(store):
entry = IndexEntry(id="1", name="f", resource_type="file")
await store.put("/f.txt", entry)
assert "/f.txt" in store._entries
@pytest.mark.asyncio
async def test_children_stored_in_dict(store):
entry = IndexEntry(id="1", name="f", resource_type="file")
await store.set_dir("/dir", [("f.txt", entry)])
assert "/dir" in store._children
assert store._children["/dir"] == ["/dir/f.txt"]
@pytest.mark.asyncio
async def test_expiry_stored_in_dict(store):
entry = IndexEntry(id="1", name="f", resource_type="file")
await store.set_dir("/dir", [("f.txt", entry)])
assert "/dir" in store._expiry
assert isinstance(store._expiry["/dir"], datetime)
@pytest.mark.asyncio
async def test_invalidate_dir_evicts_child_entries(store):
entry = IndexEntry(id="1", name="f", resource_type="file")
await store.set_dir("/dir", [("f.txt", entry)])
assert (await store.get("/dir/f.txt")).entry is not None
await store.invalidate_dir("/dir")
assert (await store.get("/dir/f.txt")).entry is None
assert "/dir" not in store._children
assert "/dir" not in store._expiry
@pytest.mark.asyncio
async def test_clear_empties_all_dicts(store):
entry = IndexEntry(id="1", name="f", resource_type="file")
await store.put("/f.txt", entry)
await store.set_dir("/dir", [("f.txt", entry)])
await store.clear()
assert len(store._entries) == 0
assert len(store._children) == 0
assert len(store._expiry) == 0
@pytest.mark.asyncio
async def test_locks_cleaned_after_clear(store):
entry = IndexEntry(id="1", name="f", resource_type="file")
await store.put("/a", entry)
await store.put("/b", entry)
await store.clear()
assert len(store._key_locks) == 0
+38
View File
@@ -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 pytest
from mirage.cache.index.config import IndexEntry
from mirage.cache.index.ram import RAMIndexCacheStore
@pytest.mark.asyncio
async def test_set_dir_preserves_insertion_order():
store = RAMIndexCacheStore(ttl=60)
entries = [
("2026-05-03_zebra__id3.gdoc.json",
IndexEntry(id="3", name="zebra", resource_type="gdocs/file")),
("2026-05-02_apple__id2.gdoc.json",
IndexEntry(id="2", name="apple", resource_type="gdocs/file")),
("2026-05-01_mango__id1.gdoc.json",
IndexEntry(id="1", name="mango", resource_type="gdocs/file")),
]
await store.set_dir("/gdocs/owned", entries)
result = await store.list_dir("/gdocs/owned")
assert result.entries == [
"/gdocs/owned/2026-05-03_zebra__id3.gdoc.json",
"/gdocs/owned/2026-05-02_apple__id2.gdoc.json",
"/gdocs/owned/2026-05-01_mango__id1.gdoc.json",
]
+215
View File
@@ -0,0 +1,215 @@
# ========= 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 os
import time
from datetime import datetime, timedelta, timezone
import pytest
import pytest_asyncio
from mirage.cache.index import IndexEntry, LookupStatus
from mirage.cache.index.redis import RedisIndexCacheStore
REDIS_URL = os.environ.get("REDIS_URL", "")
pytestmark = pytest.mark.skipif(not REDIS_URL, reason="REDIS_URL not set")
@pytest_asyncio.fixture()
async def store():
s = RedisIndexCacheStore(ttl=60, url=REDIS_URL, key_prefix="test:")
await s.clear()
yield s
await s.clear()
await s.close()
@pytest_asyncio.fixture()
def entry():
return IndexEntry(
id="file1",
name="Test File",
resource_type="text/plain",
remote_time="2026-04-01T00:00:00.000Z",
vfs_name="test_file.txt",
)
@pytest.mark.asyncio
async def test_put_and_get(store, entry):
await store.put("/folder/test_file.txt", entry)
result = await store.get("/folder/test_file.txt")
assert result.entry is not None
assert result.entry.id == "file1"
assert result.entry.name == "Test File"
@pytest.mark.asyncio
async def test_put_sets_index_time(store, entry):
await store.put("/folder/test_file.txt", entry)
result = await store.get("/folder/test_file.txt")
assert result.entry is not None
assert result.entry.index_time != ""
@pytest.mark.asyncio
async def test_put_preserves_existing_index_time(store):
entry = IndexEntry(id="b",
name="b.txt",
resource_type="file",
index_time="2026-01-01T00:00:00")
await store.put("/data/b.txt", entry)
result = await store.get("/data/b.txt")
assert result.entry.index_time == "2026-01-01T00:00:00"
@pytest.mark.asyncio
async def test_get_not_found(store):
result = await store.get("/nonexistent/file.txt")
assert result.status == LookupStatus.NOT_FOUND
assert result.entry is None
@pytest.mark.asyncio
async def test_list_dir_not_found(store):
result = await store.list_dir("/some_dir")
assert result.status == LookupStatus.NOT_FOUND
@pytest.mark.asyncio
async def test_set_dir_and_list(store):
entries = [
("a.txt",
IndexEntry(id="a",
name="A",
resource_type="text/plain",
vfs_name="a.txt")),
("b.txt",
IndexEntry(id="b",
name="B",
resource_type="text/plain",
vfs_name="b.txt")),
]
await store.set_dir("/mydir", entries)
result = await store.list_dir("/mydir")
assert result.status is None
assert sorted(result.entries) == ["/mydir/a.txt", "/mydir/b.txt"]
@pytest.mark.asyncio
async def test_set_dir_root(store):
entries = [
("top.txt",
IndexEntry(id="t",
name="T",
resource_type="text/plain",
vfs_name="top.txt")),
]
await store.set_dir("/", entries)
result = await store.list_dir("/")
assert result.status is None
assert result.entries == ["/top.txt"]
@pytest.mark.asyncio
async def test_set_dir_entries_are_retrievable(store):
entries = [("x.csv", IndexEntry(id="x1",
name="x.csv",
resource_type="file"))]
await store.set_dir("/root", entries)
result = await store.get("/root/x.csv")
assert result.entry is not None
assert result.entry.id == "x1"
@pytest.mark.asyncio
async def test_list_dir_expired(store):
entries = [
("file.txt",
IndexEntry(id="f",
name="F",
resource_type="text/plain",
vfs_name="file.txt")),
]
past = datetime.now(timezone.utc) + timedelta(seconds=1)
await store.set_dir("/dir", entries, expired_at=past)
time.sleep(1.5)
result = await store.list_dir("/dir")
assert result.status in (LookupStatus.NOT_FOUND, LookupStatus.EXPIRED)
@pytest.mark.asyncio
async def test_list_dir_fresh(store):
entries = [
("file.txt",
IndexEntry(id="f",
name="F",
resource_type="text/plain",
vfs_name="file.txt")),
]
future = datetime.now(timezone.utc) + timedelta(seconds=3600)
await store.set_dir("/dir", entries, expired_at=future)
result = await store.list_dir("/dir")
assert result.status is None
assert result.entries == ["/dir/file.txt"]
@pytest.mark.asyncio
async def test_set_dir_overwrites_previous(store):
await store.set_dir("/d", [
("old.txt", IndexEntry(id="o", name="old.txt", resource_type="file"))
])
await store.set_dir("/d", [
("new.txt", IndexEntry(id="n", name="new.txt", resource_type="file"))
])
result = await store.list_dir("/d")
assert result.entries is not None
assert len(result.entries) == 1
assert "new.txt" in result.entries[0]
@pytest.mark.asyncio
async def test_invalidate_dir_evicts_child_entries(store, entry):
await store.set_dir("/folder", [("test.txt", entry)])
assert (await store.get("/folder/test.txt")).entry is not None
await store.invalidate_dir("/folder")
assert (await
store.get("/folder/test.txt")).status == LookupStatus.NOT_FOUND
assert (await store.list_dir("/folder")).status == LookupStatus.NOT_FOUND
@pytest.mark.asyncio
async def test_clear(store, entry):
await store.put("/folder/test.txt", entry)
await store.set_dir("/folder", [("test.txt", entry)])
await store.clear()
result = await store.get("/folder/test.txt")
assert result.status == LookupStatus.NOT_FOUND
result = await store.list_dir("/folder")
assert result.status == LookupStatus.NOT_FOUND
@pytest.mark.asyncio
async def test_key_prefix_isolation():
s1 = RedisIndexCacheStore(ttl=60, url=REDIS_URL, key_prefix="ns1:")
s2 = RedisIndexCacheStore(ttl=60, url=REDIS_URL, key_prefix="ns2:")
await s1.clear()
await s2.clear()
await s1.put("/shared", IndexEntry(id="a", name="a", resource_type="file"))
assert (await s2.get("/shared")).status == LookupStatus.NOT_FOUND
assert (await s1.get("/shared")).entry is not None
await s1.clear()
await s2.clear()
await s1.close()
await s2.close()
+52
View File
@@ -0,0 +1,52 @@
# ========= 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 os
import pytest
import pytest_asyncio
from mirage.cache.index.config import IndexEntry
from mirage.cache.index.redis import RedisIndexCacheStore
REDIS_URL = os.environ.get("REDIS_URL", "")
pytestmark = pytest.mark.skipif(not REDIS_URL, reason="REDIS_URL not set")
@pytest_asyncio.fixture()
async def store():
s = RedisIndexCacheStore(ttl=60, url=REDIS_URL, key_prefix="test:order:")
await s.clear()
yield s
await s.clear()
await s.close()
@pytest.mark.asyncio
async def test_set_dir_preserves_insertion_order(store):
entries = [
("2026-05-03_zebra__id3.gdoc.json",
IndexEntry(id="3", name="zebra", resource_type="gdocs/file")),
("2026-05-02_apple__id2.gdoc.json",
IndexEntry(id="2", name="apple", resource_type="gdocs/file")),
("2026-05-01_mango__id1.gdoc.json",
IndexEntry(id="1", name="mango", resource_type="gdocs/file")),
]
await store.set_dir("/gdocs/owned", entries)
result = await store.list_dir("/gdocs/owned")
assert result.entries == [
"/gdocs/owned/2026-05-03_zebra__id3.gdoc.json",
"/gdocs/owned/2026-05-02_apple__id2.gdoc.json",
"/gdocs/owned/2026-05-01_mango__id1.gdoc.json",
]
+153
View File
@@ -0,0 +1,153 @@
# ========= 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 datetime import datetime, timedelta, timezone
import pytest
from mirage.cache.index import IndexEntry, LookupStatus, RAMIndexCacheStore
@pytest.fixture
def store():
return RAMIndexCacheStore(ttl=60)
@pytest.fixture
def entry():
return IndexEntry(
id="file1",
name="Test File",
resource_type="text/plain",
remote_time="2026-04-01T00:00:00.000Z",
vfs_name="test_file.txt",
)
@pytest.mark.asyncio
async def test_put_and_get(store, entry):
await store.put("/folder/test_file.txt", entry)
result = await store.get("/folder/test_file.txt")
assert result.entry is not None
assert result.entry.id == "file1"
assert result.entry.name == "Test File"
@pytest.mark.asyncio
async def test_put_sets_index_time(store, entry):
await store.put("/folder/test_file.txt", entry)
result = await store.get("/folder/test_file.txt")
assert result.entry is not None
assert result.entry.index_time != ""
@pytest.mark.asyncio
async def test_get_not_found(store):
result = await store.get("/nonexistent/file.txt")
assert result.status == LookupStatus.NOT_FOUND
assert result.entry is None
@pytest.mark.asyncio
async def test_list_dir_not_found(store):
result = await store.list_dir("/some_dir")
assert result.status == LookupStatus.NOT_FOUND
@pytest.mark.asyncio
async def test_set_dir_and_list(store):
entries = [
("a.txt",
IndexEntry(
id="a",
name="A",
resource_type="text/plain",
vfs_name="a.txt",
)),
("b.txt",
IndexEntry(
id="b",
name="B",
resource_type="text/plain",
vfs_name="b.txt",
)),
]
await store.set_dir("/mydir", entries)
result = await store.list_dir("/mydir")
assert result.status is None
assert sorted(result.entries) == ["/mydir/a.txt", "/mydir/b.txt"]
@pytest.mark.asyncio
async def test_set_dir_root(store):
entries = [
("top.txt",
IndexEntry(
id="t",
name="T",
resource_type="text/plain",
vfs_name="top.txt",
)),
]
await store.set_dir("/", entries)
result = await store.list_dir("/")
assert result.status is None
assert result.entries == ["/top.txt"]
@pytest.mark.asyncio
async def test_list_dir_expired(store):
entries = [
("file.txt",
IndexEntry(
id="f",
name="F",
resource_type="text/plain",
vfs_name="file.txt",
)),
]
past = datetime.now(timezone.utc) - timedelta(seconds=1)
await store.set_dir("/dir", entries, expired_at=past)
result = await store.list_dir("/dir")
assert result.status == LookupStatus.EXPIRED
@pytest.mark.asyncio
async def test_list_dir_fresh(store):
entries = [
("file.txt",
IndexEntry(
id="f",
name="F",
resource_type="text/plain",
vfs_name="file.txt",
)),
]
future = datetime.now(timezone.utc) + timedelta(seconds=3600)
await store.set_dir("/dir", entries, expired_at=future)
result = await store.list_dir("/dir")
assert result.status is None
assert result.entries == ["/dir/file.txt"]
@pytest.mark.asyncio
async def test_clear(store, entry):
await store.put("/folder/test.txt", entry)
await store.set_dir("/folder", [("test.txt", entry)])
await store.clear()
result = await store.get("/folder/test.txt")
assert result.status == LookupStatus.NOT_FOUND
result = await store.list_dir("/folder")
assert result.status == LookupStatus.NOT_FOUND
+80
View File
@@ -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 asyncio
from mirage.cache.context import (active_cache_manager,
invalidate_after_unlink,
invalidate_after_write, push_cache_manager)
def _run(coro):
return asyncio.run(coro)
class FakeManager:
def __init__(self) -> None:
self.writes: list[str] = []
self.unlinks: list[str] = []
async def invalidate_after_write(self, path: str) -> None:
self.writes.append(path)
async def invalidate_after_unlink(self, path: str) -> None:
self.unlinks.append(path)
async def _delegates() -> FakeManager:
manager = FakeManager()
prev = push_cache_manager(manager)
await invalidate_after_write("/a.txt")
await invalidate_after_unlink("/b.txt")
push_cache_manager(prev)
return manager
def test_delegates_to_active_manager():
manager = _run(_delegates())
assert manager.writes == ["/a.txt"]
assert manager.unlinks == ["/b.txt"]
async def _noop_without_manager() -> None:
push_cache_manager(None)
await invalidate_after_write("/a.txt")
await invalidate_after_unlink("/b.txt")
def test_noop_without_active_manager():
_run(_noop_without_manager())
async def _push_restores() -> tuple[object, object, object]:
first = FakeManager()
second = FakeManager()
prev0 = push_cache_manager(first)
prev1 = push_cache_manager(second)
active = active_cache_manager()
push_cache_manager(prev1)
restored = active_cache_manager()
push_cache_manager(prev0)
return prev1, active, restored
def test_push_returns_previous_manager():
prev1, active, restored = _run(_push_restores())
assert prev1 is not None
assert active is not restored
assert restored is prev1
+47
View File
@@ -0,0 +1,47 @@
# ========= 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.cache.index.config import IndexEntry
def test_index_entry_extra_defaults_to_empty_dict():
entry = IndexEntry(id="x", name="x", resource_type="t")
assert entry.extra == {}
def test_index_entry_extra_round_trip():
entry = IndexEntry(
id="F1",
name="report",
resource_type="slack/file",
extra={
"url": "https://files.slack.com/x",
"mimetype": "application/pdf"
},
)
assert entry.extra == {
"url": "https://files.slack.com/x",
"mimetype": "application/pdf",
}
def test_index_entry_extra_survives_json_round_trip():
entry = IndexEntry(
id="F1",
name="report",
resource_type="slack/file",
extra={"url": "u"},
)
restored = IndexEntry.model_validate_json(entry.model_dump_json())
assert restored.extra == {"url": "u"}
+92
View File
@@ -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
import pytest
from mirage.cache.lock import KeyLockMixin
class _Store(KeyLockMixin):
pass
@pytest.mark.asyncio
async def test_different_keys_return_different_locks():
store = _Store()
assert store._lock_for("/a") is not store._lock_for("/b")
@pytest.mark.asyncio
async def test_same_key_returns_same_lock():
store = _Store()
assert store._lock_for("/a") is store._lock_for("/a")
@pytest.mark.asyncio
async def test_discard_lock_removes_key():
store = _Store()
store._lock_for("/a")
store._discard_lock("/a")
assert "/a" not in store._key_locks
@pytest.mark.asyncio
async def test_discard_lock_missing_key_no_error():
store = _Store()
store._discard_lock("/nope")
@pytest.mark.asyncio
async def test_clear_locks_removes_all():
store = _Store()
store._lock_for("/a")
store._lock_for("/b")
store._clear_locks()
assert len(store._key_locks) == 0
@pytest.mark.asyncio
async def test_concurrent_different_keys_no_block():
store = _Store()
order = []
async def acquire(key, label):
async with store._lock_for(key):
order.append(f"{label}_start")
await asyncio.sleep(0)
order.append(f"{label}_end")
await asyncio.gather(acquire("/a", "a"), acquire("/b", "b"))
assert "a_start" in order
assert "b_start" in order
@pytest.mark.asyncio
async def test_same_key_serialized():
store = _Store()
order = []
async def acquire(label):
async with store._lock_for("/same"):
order.append(f"{label}_start")
await asyncio.sleep(0.01)
order.append(f"{label}_end")
await asyncio.gather(acquire("first"), acquire("second"))
assert order[0] == "first_start"
assert order[1] == "first_end"
assert order[2] == "second_start"
assert order[3] == "second_end"
+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 asyncio
from mirage.cache.file.ram import RAMFileCacheStore
from mirage.cache.index.config import IndexEntry
from mirage.cache.index.ram import RAMIndexCacheStore
from mirage.cache.manager import CacheManager
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
def _run(coro):
return asyncio.run(coro)
def _stores() -> tuple[RAMFileCacheStore, RAMIndexCacheStore]:
return RAMFileCacheStore(), RAMIndexCacheStore(ttl=600)
async def _seed(cache: RAMFileCacheStore, index: RAMIndexCacheStore) -> None:
await cache.set("/data/arch/h.txt", b"two\n")
await index.set_dir("/data/arch", [
("h.txt", IndexEntry(id="h", name="h.txt", resource_type="file")),
])
async def _write_case() -> tuple[bool, bool]:
cache, index = _stores()
await _seed(cache, index)
manager = CacheManager(cache, index, "/data/", True)
await manager.invalidate_after_write("/arch/h.txt")
cached = await cache.exists("/data/arch/h.txt")
listing = await index.list_dir("/data/arch")
return cached, listing.entries is not None
def test_write_evicts_file_and_parent_listing():
cached, listed = _run(_write_case())
assert cached is False
assert listed is False
async def _unlink_case() -> tuple[bool, bool, object]:
cache, index = _stores()
await _seed(cache, index)
manager = CacheManager(cache, index, "/data/", True)
await manager.invalidate_after_unlink("/arch/h.txt")
cached = await cache.exists("/data/arch/h.txt")
listing = await index.list_dir("/data/arch")
entry = await index.get("/data/arch/h.txt")
return cached, listing.entries is not None, entry.entry
def test_unlink_evicts_file_listing_and_entry():
cached, listed, entry = _run(_unlink_case())
assert cached is False
assert listed is False
assert entry is None
async def _local_case() -> tuple[bool, bool]:
cache, index = _stores()
await _seed(cache, index)
manager = CacheManager(cache, index, "/data/", False)
await manager.invalidate_after_write("/arch/h.txt")
cached = await cache.exists("/data/arch/h.txt")
listing = await index.list_dir("/data/arch")
return cached, listing.entries is not None
def test_local_mount_keeps_file_cache_but_invalidates_index():
cached, listed = _run(_local_case())
assert cached is True
assert listed is False
async def _pathspec_case() -> bool:
cache, index = _stores()
await _seed(cache, index)
manager = CacheManager(cache, index, "/data/", True)
spec = PathSpec(resource_path=mount_key("/data/arch/h.txt", "/data/"),
virtual="/data/arch/h.txt",
directory="/data/arch")
await manager.invalidate_after_write(spec)
return await cache.exists("/data/arch/h.txt")
def test_pathspec_input_maps_to_virtual_key():
assert _run(_pathspec_case()) is False
async def _cached_hit_case() -> bytes | None:
cache, index = _stores()
await cache.set("/data/x.txt", b"cached")
manager = CacheManager(cache, index, "/data/", True)
spec = PathSpec(resource_path=mount_key("/data/x.txt", "/data/"),
virtual="/data/x.txt",
directory="/data/")
return await manager.cached_bytes(spec)
def test_cached_bytes_returns_cached_value():
assert _run(_cached_hit_case()) == b"cached"
async def _cached_miss_case() -> bytes | None:
cache, index = _stores()
manager = CacheManager(cache, index, "/data/", True)
spec = PathSpec(resource_path=mount_key("/data/x.txt", "/data/"),
virtual="/data/x.txt",
directory="/data/")
return await manager.cached_bytes(spec)
def test_cached_bytes_miss_returns_none():
assert _run(_cached_miss_case()) is None
async def _cached_local_case() -> bytes | None:
cache, index = _stores()
await cache.set("/data/x.txt", b"cached")
manager = CacheManager(cache, index, "/data/", False)
spec = PathSpec(resource_path=mount_key("/data/x.txt", "/data/"),
virtual="/data/x.txt",
directory="/data/")
return await manager.cached_bytes(spec)
def test_cached_bytes_local_mount_returns_none():
assert _run(_cached_local_case()) is None
async def _no_index_case() -> bool:
cache, _ = _stores()
await cache.set("/data/a.txt", b"x")
manager = CacheManager(cache, None, "/data/", True)
await manager.invalidate_after_write("/a.txt")
return await cache.exists("/data/a.txt")
def test_missing_index_is_tolerated():
assert _run(_no_index_case()) is False
+169
View File
@@ -0,0 +1,169 @@
# ========= 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.context import push_cache_manager
from mirage.cache.file.ram import RAMFileCacheStore
from mirage.cache.manager import CacheManager
from mirage.cache.read_through import (cache_aware_read_bytes,
cache_aware_read_stream,
cached_prefix_bytes)
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
class _CountingBackend:
def __init__(self, data: bytes) -> None:
self.data = data
self.stream_calls = 0
self.bytes_calls = 0
async def read_stream(self, accessor, path, *args, **kwargs):
self.stream_calls += 1
yield self.data
async def read_bytes(self, accessor, path, *args, **kwargs) -> bytes:
self.bytes_calls += 1
return self.data
def _spec() -> PathSpec:
return PathSpec(resource_path=mount_key("/s3/a.txt", "/s3/"),
virtual="/s3/a.txt",
directory="/s3/")
async def _warm_manager(data: bytes) -> CacheManager:
cache = RAMFileCacheStore()
await cache.set("/s3/a.txt", data)
return CacheManager(cache, None, "/s3/", True)
async def _drain(source) -> bytes:
return b"".join([c async for c in source])
@pytest.mark.asyncio
async def test_read_bytes_warm_serves_cache_without_backend():
backend = _CountingBackend(b"payload")
manager = await _warm_manager(b"payload")
reader = cache_aware_read_bytes(backend.read_bytes)
prev = push_cache_manager(manager)
try:
out = await reader(None, _spec())
finally:
push_cache_manager(prev)
assert out == b"payload"
assert backend.bytes_calls == 0
@pytest.mark.asyncio
async def test_read_bytes_cold_falls_through():
backend = _CountingBackend(b"payload")
manager = CacheManager(RAMFileCacheStore(), None, "/s3/", True)
reader = cache_aware_read_bytes(backend.read_bytes)
prev = push_cache_manager(manager)
try:
out = await reader(None, _spec())
finally:
push_cache_manager(prev)
assert out == b"payload"
assert backend.bytes_calls == 1
@pytest.mark.asyncio
async def test_read_bytes_no_manager_falls_through():
backend = _CountingBackend(b"payload")
reader = cache_aware_read_bytes(backend.read_bytes)
out = await reader(None, _spec())
assert out == b"payload"
assert backend.bytes_calls == 1
@pytest.mark.asyncio
async def test_read_stream_warm_serves_cache_without_backend():
backend = _CountingBackend(b"payload")
manager = await _warm_manager(b"payload")
reader = cache_aware_read_stream(backend.read_stream)
prev = push_cache_manager(manager)
try:
out = await _drain(reader(None, _spec()))
finally:
push_cache_manager(prev)
assert out == b"payload"
assert backend.stream_calls == 0
@pytest.mark.asyncio
async def test_read_stream_cold_falls_through():
backend = _CountingBackend(b"payload")
manager = CacheManager(RAMFileCacheStore(), None, "/s3/", True)
reader = cache_aware_read_stream(backend.read_stream)
prev = push_cache_manager(manager)
try:
out = await _drain(reader(None, _spec()))
finally:
push_cache_manager(prev)
assert out == b"payload"
assert backend.stream_calls == 1
@pytest.mark.asyncio
async def test_read_stream_no_manager_falls_through():
backend = _CountingBackend(b"payload")
reader = cache_aware_read_stream(backend.read_stream)
out = await _drain(reader(None, _spec()))
assert out == b"payload"
assert backend.stream_calls == 1
@pytest.mark.asyncio
async def test_read_stream_captures_manager_before_drain():
# The manager must be captured when the reader is called (inside the
# mount's scope), not when the stream drains (after the scope is gone).
backend = _CountingBackend(b"payload")
manager = await _warm_manager(b"payload")
reader = cache_aware_read_stream(backend.read_stream)
prev = push_cache_manager(manager)
source = reader(None, _spec())
push_cache_manager(prev)
out = await _drain(source)
assert out == b"payload"
assert backend.stream_calls == 0
@pytest.mark.asyncio
async def test_cached_prefix_bytes_slices_when_warm():
manager = await _warm_manager(b"payload")
prev = push_cache_manager(manager)
try:
out = await cached_prefix_bytes(_spec(), 4)
whole = await cached_prefix_bytes(_spec(), None)
finally:
push_cache_manager(prev)
assert out == b"payl"
assert whole == b"payload"
@pytest.mark.asyncio
async def test_cached_prefix_bytes_miss_and_no_manager_return_none():
miss_manager = CacheManager(RAMFileCacheStore(), None, "/s3/", True)
prev = push_cache_manager(miss_manager)
try:
assert await cached_prefix_bytes(_spec(), 4) is None
finally:
push_cache_manager(prev)
assert await cached_prefix_bytes(_spec(), 4) is None
+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. =========
+84
View File
@@ -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 os
import socket
import subprocess
import sys
import time
import httpx
import pytest
def _free_port() -> int:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("127.0.0.1", 0))
return s.getsockname()[1]
def _wait_for_health(url: str, timeout: float = 8.0) -> None:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
try:
r = httpx.get(f"{url}/v1/health", timeout=0.3)
if r.status_code == 200:
return
except httpx.RequestError:
pass
time.sleep(0.1)
raise RuntimeError(f"daemon at {url} did not start within {timeout}s")
@pytest.fixture
def daemon(tmp_path):
"""Spin up a real daemon subprocess for the test, on a free port."""
port = _free_port()
url = f"http://127.0.0.1:{port}"
env = dict(os.environ)
env["MIRAGE_DAEMON_URL"] = url
env["MIRAGE_IDLE_GRACE_SECONDS"] = "60"
env["MIRAGE_HOME"] = str(tmp_path)
env["MIRAGE_VERSION_ROOT"] = str(tmp_path / "repos")
env["MIRAGE_SNAPSHOT_ROOT"] = str(tmp_path)
env.pop("MIRAGE_AUTH_TOKEN", None)
env.pop("MIRAGE_TOKEN", None)
log_file = tmp_path / "daemon.log"
proc = subprocess.Popen(
[
sys.executable,
"-m",
"uvicorn",
"mirage.server.daemon:app",
"--host",
"127.0.0.1",
"--port",
str(port),
"--log-level",
"warning",
],
env=env,
stdout=open(log_file, "wb"),
stderr=subprocess.STDOUT,
)
try:
_wait_for_health(url)
yield {"url": url, "env": env, "log": log_file}
finally:
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait()
+398
View File
@@ -0,0 +1,398 @@
# ========= 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 subprocess
import sys
from pathlib import Path
CONFIG_YAML = """\
mounts:
/:
resource: ram
mode: WRITE
"""
def _write_config(tmp_path: Path) -> Path:
p = tmp_path / "config.yaml"
p.write_text(CONFIG_YAML, encoding="utf-8")
return p
def _run_cli(env: dict,
*args: str,
stdin: bytes | None = None,
expect_exit: int = 0) -> dict | list:
cmd = [sys.executable, "-m", "mirage.cli.main", *args]
proc = subprocess.run(
cmd,
env=env,
input=stdin,
capture_output=True,
timeout=30,
)
if proc.returncode != expect_exit:
raise AssertionError(
f"exit={proc.returncode} (expected {expect_exit})\n"
f"stdout: {proc.stdout.decode()}\nstderr: {proc.stderr.decode()}")
if expect_exit != 0:
return {}
if not proc.stdout.strip():
return {}
return json.loads(proc.stdout)
def test_workspace_lifecycle(daemon, tmp_path):
cfg = _write_config(tmp_path)
created = _run_cli(daemon["env"], "workspace", "create", str(cfg))
wid = created["id"]
assert wid.startswith("ws_")
listed = _run_cli(daemon["env"], "workspace", "list")
assert any(w["id"] == wid for w in listed)
detail = _run_cli(daemon["env"], "workspace", "get", wid)
assert detail["id"] == wid
deleted = _run_cli(daemon["env"], "workspace", "delete", wid)
assert deleted["id"] == wid
def test_workspace_create_with_explicit_id(daemon, tmp_path):
cfg = _write_config(tmp_path)
created = _run_cli(daemon["env"], "workspace", "create", str(cfg), "--id",
"custom-ws")
assert created["id"] == "custom-ws"
_run_cli(daemon["env"], "workspace", "delete", "custom-ws")
def test_session_lifecycle(daemon, tmp_path):
cfg = _write_config(tmp_path)
created = _run_cli(daemon["env"], "workspace", "create", str(cfg), "--id",
"sess-test-ws")
wid = created["id"]
sess = _run_cli(daemon["env"], "session", "create", wid, "--id", "agent_a")
assert sess["session_id"] == "agent_a"
listed = _run_cli(daemon["env"], "session", "list", wid)
assert any(s["session_id"] == "agent_a" for s in listed)
_run_cli(daemon["env"], "session", "delete", wid, "agent_a")
_run_cli(daemon["env"], "workspace", "delete", wid)
def test_execute_returns_json_io_result(daemon, tmp_path):
cfg = _write_config(tmp_path)
created = _run_cli(daemon["env"], "workspace", "create", str(cfg), "--id",
"exec-test")
result = _run_cli(daemon["env"], "execute", "--workspace_id",
created["id"], "--command", "echo hello")
assert result["kind"] == "io"
assert result["exit_code"] == 0
assert result["stdout"].startswith("hello")
_run_cli(daemon["env"], "workspace", "delete", "exec-test")
def test_execute_background_returns_job_id(daemon, tmp_path):
cfg = _write_config(tmp_path)
_run_cli(daemon["env"], "workspace", "create", str(cfg), "--id", "bg-test")
submitted = _run_cli(daemon["env"], "execute", "--workspace_id", "bg-test",
"--command", "echo bg", "--background")
job_id = submitted["job_id"]
assert job_id.startswith("job_")
waited = _run_cli(daemon["env"], "job", "wait", job_id)
assert waited["status"] == "done"
assert waited["result"]["stdout"].startswith("bg")
_run_cli(daemon["env"], "workspace", "delete", "bg-test")
def test_execute_with_stdin_pipe(daemon, tmp_path):
cfg = _write_config(tmp_path)
_run_cli(daemon["env"], "workspace", "create", str(cfg), "--id",
"stdin-test")
result = _run_cli(daemon["env"],
"execute",
"--workspace_id",
"stdin-test",
"--command",
"wc -l",
stdin=b"a\nb\nc\n")
assert result["exit_code"] == 0
assert result["stdout"].strip().startswith("3")
_run_cli(daemon["env"], "workspace", "delete", "stdin-test")
def test_save_then_load_round_trip(daemon, tmp_path):
cfg = _write_config(tmp_path)
_run_cli(daemon["env"], "workspace", "create", str(cfg), "--id",
"save-test")
_run_cli(daemon["env"], "execute", "--workspace_id", "save-test",
"--command", "echo persisted > /report.txt")
tar_path = tmp_path / "snap.tar"
saved = _run_cli(daemon["env"], "workspace", "snapshot", "save-test",
str(tar_path))
assert saved["size"] > 0
assert tar_path.exists()
loaded = _run_cli(daemon["env"], "workspace", "load", str(tar_path),
str(cfg), "--id", "loaded-ws")
assert loaded["id"] == "loaded-ws"
result = _run_cli(daemon["env"], "execute", "--workspace_id", "loaded-ws",
"--command", "cat /report.txt")
assert "persisted" in result["stdout"]
_run_cli(daemon["env"], "workspace", "delete", "save-test")
_run_cli(daemon["env"], "workspace", "delete", "loaded-ws")
def test_workspace_clone_round_trip(daemon, tmp_path):
cfg = _write_config(tmp_path)
_run_cli(daemon["env"], "workspace", "create", str(cfg), "--id",
"clone-src")
_run_cli(daemon["env"], "execute", "--workspace_id", "clone-src",
"--command", "echo source > /report.txt")
cloned = _run_cli(daemon["env"], "workspace", "clone", "clone-src", "--id",
"clone-dst")
assert cloned["id"] == "clone-dst"
result = _run_cli(daemon["env"], "execute", "--workspace_id", "clone-dst",
"--command", "cat /report.txt")
assert "source" in result["stdout"]
_run_cli(daemon["env"], "execute", "--workspace_id", "clone-dst",
"--command", "echo clone > /report.txt")
original = _run_cli(daemon["env"], "execute", "--workspace_id",
"clone-src", "--command", "cat /report.txt")
assert "source" in original["stdout"]
_run_cli(daemon["env"], "workspace", "delete", "clone-src")
_run_cli(daemon["env"], "workspace", "delete", "clone-dst")
def test_workspace_get_verbose_includes_internals(daemon, tmp_path):
cfg = _write_config(tmp_path)
_run_cli(daemon["env"], "workspace", "create", str(cfg), "--id",
"verbose-test")
plain = _run_cli(daemon["env"], "workspace", "get", "verbose-test")
assert plain["internals"] is None
verbose = _run_cli(daemon["env"], "workspace", "get", "verbose-test",
"--verbose")
assert verbose["internals"] is not None
assert "cache_bytes" in verbose["internals"]
_run_cli(daemon["env"], "workspace", "delete", "verbose-test")
def test_unknown_workspace_returns_nonzero(daemon, tmp_path):
_run_cli(daemon["env"],
"workspace",
"get",
"ws_doesnotexist",
expect_exit=2)
def test_env_interpolation_uses_cli_environment(daemon, tmp_path):
"""${VAR}s in the YAML must be resolved from the CLI's env, not the
daemon's. The cross-mount workflow assumes the user sources their
creds before running `mirage workspace create` -- the daemon
process won't have those vars."""
cfg = tmp_path / "interp.yaml"
cfg.write_text(
"mounts:\n"
" /:\n"
" resource: ram\n"
" mode: ${MOUNT_MODE_FROM_ENV}\n",
encoding="utf-8",
)
env = {**daemon["env"], "MOUNT_MODE_FROM_ENV": "WRITE"}
_run_cli(env, "workspace", "create", str(cfg), "--id", "interp-test")
detail = _run_cli(env, "workspace", "get", "interp-test")
assert any(m["mode"] == "write" for m in detail["mounts"])
_run_cli(env, "workspace", "delete", "interp-test")
def test_daemon_status_running(daemon, tmp_path):
out = _run_cli(daemon["env"], "daemon", "status")
assert out["running"] is True
assert out["pid"] is not None
assert out["workspaces"] == 0
def test_daemon_stop_then_status_not_running(daemon, tmp_path):
out = _run_cli(daemon["env"], "daemon", "stop")
assert out["stopped"] is True
import time
time.sleep(0.5)
out = _run_cli(daemon["env"], "daemon", "status", expect_exit=1)
assert out == {} or out.get("running") is False
def test_provision_returns_dry_run_result(daemon, tmp_path):
"""`mirage provision` hits /execute with provision=True and returns
a {"kind": "provision", ...} payload instead of actually running."""
cfg = _write_config(tmp_path)
_run_cli(daemon["env"], "workspace", "create", str(cfg), "--id",
"provision-test")
result = _run_cli(
daemon["env"],
"provision",
"--workspace_id",
"provision-test",
"--command",
"echo would-not-run",
)
assert result["kind"] == "provision"
_run_cli(daemon["env"], "workspace", "delete", "provision-test")
def test_execute_propagates_inner_exit_code(daemon, tmp_path):
cfg = _write_config(tmp_path)
_run_cli(daemon["env"], "workspace", "create", str(cfg), "--id",
"exit-test")
ok = _run_cli(daemon["env"], "execute", "-w", "exit-test", "-c", "echo ok")
assert ok["exit_code"] == 0
_run_cli(daemon["env"],
"execute",
"-w",
"exit-test",
"-c",
"false",
expect_exit=1)
bg = _run_cli(daemon["env"], "execute", "-w", "exit-test", "-c", "false",
"--background")
job_id = bg["job_id"]
_run_cli(daemon["env"], "job", "wait", job_id, expect_exit=1)
_run_cli(daemon["env"], "workspace", "delete", "exit-test")
def test_execute_subshell_cwd_does_not_leak(daemon, tmp_path):
cfg = _write_config(tmp_path)
_run_cli(daemon["env"], "workspace", "create", str(cfg), "--id",
"subshell")
_run_cli(daemon["env"], "execute", "-w", "subshell", "-c", "mkdir /sub")
inside = _run_cli(daemon["env"], "execute", "-w", "subshell", "-c",
"(cd /sub && pwd)")
assert inside["stdout"].strip() == "/sub"
after = _run_cli(daemon["env"], "execute", "-w", "subshell", "-c", "pwd")
assert after["stdout"].strip() == "/"
_run_cli(daemon["env"], "workspace", "delete", "subshell")
def test_execute_env_prefix_does_not_leak(daemon, tmp_path):
cfg = _write_config(tmp_path)
_run_cli(daemon["env"], "workspace", "create", str(cfg), "--id", "envpref")
inside = _run_cli(daemon["env"], "execute", "-w", "envpref", "-c",
"(export FOO=bar; printenv FOO)")
assert inside["stdout"].strip() == "bar"
after = _run_cli(daemon["env"], "execute", "-w", "envpref", "-c",
"printenv FOO || echo absent")
assert after["stdout"].strip() == "absent"
_run_cli(daemon["env"], "workspace", "delete", "envpref")
def test_execute_background_then_cancel(daemon, tmp_path):
cfg = _write_config(tmp_path)
_run_cli(daemon["env"], "workspace", "create", str(cfg), "--id",
"cancel-test")
submitted = _run_cli(daemon["env"], "execute", "-w", "cancel-test", "-c",
"sleep 30", "--background")
job_id = submitted["job_id"]
_run_cli(daemon["env"], "job", "cancel", job_id)
waited = _run_cli(daemon["env"], "job", "wait", job_id, expect_exit=2)
assert waited == {}
_run_cli(daemon["env"], "workspace", "delete", "cancel-test")
def test_missing_env_var_fails_fast_before_daemon_call(daemon, tmp_path):
cfg = tmp_path / "missing.yaml"
cfg.write_text(
"mounts:\n"
" /:\n"
" resource: ram\n"
" mode: ${THIS_VAR_IS_NOT_SET_ANYWHERE}\n",
encoding="utf-8",
)
env = {
k: v
for k, v in daemon["env"].items()
if k != "THIS_VAR_IS_NOT_SET_ANYWHERE"
}
_run_cli(env, "workspace", "create", str(cfg), expect_exit=2)
SAFEGUARD_TRUNCATE_YAML = """\
mounts:
/:
resource: ram
mode: WRITE
command_safeguards:
cat:
max_lines: 2
on_exceed: truncate
"""
SAFEGUARD_ERROR_YAML = """\
mounts:
/:
resource: ram
mode: WRITE
command_safeguards:
cat:
max_lines: 2
on_exceed: error
"""
_SEED_5_LINES = "printf '1\\n2\\n3\\n4\\n5\\n' > /f.txt"
def _write_named(tmp_path: Path, name: str, text: str) -> Path:
p = tmp_path / name
p.write_text(text, encoding="utf-8")
return p
def test_execute_safeguard_truncates_output(daemon, tmp_path):
cfg = _write_named(tmp_path, "sg_trunc.yaml", SAFEGUARD_TRUNCATE_YAML)
_run_cli(daemon["env"], "workspace", "create", str(cfg), "--id",
"sg-trunc")
_run_cli(daemon["env"], "execute", "--workspace_id", "sg-trunc",
"--command", _SEED_5_LINES)
result = _run_cli(daemon["env"], "execute", "--workspace_id", "sg-trunc",
"--command", "cat /f.txt")
assert result["exit_code"] == 0
assert result["stdout"] == "1\n2\n"
_run_cli(daemon["env"], "workspace", "delete", "sg-trunc")
def test_execute_safeguard_error_exits_1(daemon, tmp_path):
cfg = _write_named(tmp_path, "sg_err.yaml", SAFEGUARD_ERROR_YAML)
_run_cli(daemon["env"], "workspace", "create", str(cfg), "--id", "sg-err")
_run_cli(daemon["env"], "execute", "--workspace_id", "sg-err", "--command",
_SEED_5_LINES)
_run_cli(daemon["env"],
"execute",
"--workspace_id",
"sg-err",
"--command",
"cat /f.txt",
expect_exit=1)
_run_cli(daemon["env"], "workspace", "delete", "sg-err")
+105
View File
@@ -0,0 +1,105 @@
# ========= 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.cli.client import DaemonClient
from mirage.cli.settings import DaemonSettings
from mirage.server.daemon_config import DaemonConfigError
from mirage.server.env import ENV_HOME
def test_spawn_daemon_uses_mirage_home_for_log_and_token(
tmp_path, monkeypatch):
monkeypatch.setenv(ENV_HOME, str(tmp_path))
monkeypatch.delenv("MIRAGE_AUTH_MODE", raising=False)
spawned = []
monkeypatch.setattr("mirage.cli.client.subprocess.Popen",
lambda *args, **kwargs: spawned.append(kwargs))
with DaemonClient(DaemonSettings()) as client:
client._spawn_daemon()
assert spawned, "daemon process must be spawned"
assert (tmp_path / "daemon.log").exists()
assert (tmp_path / "auth_token").exists()
assert client.settings.auth_token
def test_spawn_daemon_rejects_bad_config(tmp_path, monkeypatch):
monkeypatch.setenv(ENV_HOME, str(tmp_path))
monkeypatch.delenv("MIRAGE_AUTH_MODE", raising=False)
(tmp_path / "config.toml").write_text('[daemon]\ntypo_key = "x"\n')
spawned = []
monkeypatch.setattr("mirage.cli.client.subprocess.Popen",
lambda *args, **kwargs: spawned.append(kwargs))
with DaemonClient(DaemonSettings()) as client:
with pytest.raises(DaemonConfigError, match="typo_key"):
client._spawn_daemon()
assert not spawned
class _FakePopen:
def __init__(self, sink, cmd, **kwargs):
sink.append(cmd)
def test_spawn_port_config_beats_url(tmp_path, monkeypatch):
monkeypatch.setenv(ENV_HOME, str(tmp_path))
monkeypatch.delenv("MIRAGE_AUTH_MODE", raising=False)
monkeypatch.delenv("MIRAGE_DAEMON_PORT", raising=False)
(tmp_path / "config.toml").write_text("[daemon]\nport = 9100\n")
cmds = []
monkeypatch.setattr("mirage.cli.client.subprocess.Popen",
lambda cmd, **kwargs: _FakePopen(cmds, cmd, **kwargs))
with DaemonClient(DaemonSettings()) as client:
client._spawn_daemon()
assert "9100" in cmds[0]
def test_spawn_port_env_beats_config(tmp_path, monkeypatch):
monkeypatch.setenv(ENV_HOME, str(tmp_path))
monkeypatch.delenv("MIRAGE_AUTH_MODE", raising=False)
monkeypatch.setenv("MIRAGE_DAEMON_PORT", "9200")
(tmp_path / "config.toml").write_text("[daemon]\nport = 9100\n")
cmds = []
monkeypatch.setattr("mirage.cli.client.subprocess.Popen",
lambda cmd, **kwargs: _FakePopen(cmds, cmd, **kwargs))
with DaemonClient(DaemonSettings()) as client:
client._spawn_daemon()
assert "9200" in cmds[0]
def test_spawn_port_falls_back_to_url(tmp_path, monkeypatch):
monkeypatch.setenv(ENV_HOME, str(tmp_path))
monkeypatch.delenv("MIRAGE_AUTH_MODE", raising=False)
monkeypatch.delenv("MIRAGE_DAEMON_PORT", raising=False)
cmds = []
monkeypatch.setattr("mirage.cli.client.subprocess.Popen",
lambda cmd, **kwargs: _FakePopen(cmds, cmd, **kwargs))
with DaemonClient(DaemonSettings(url="http://127.0.0.1:9331")) as client:
client._spawn_daemon()
assert "9331" in cmds[0]
def test_spawn_respects_config_auth_mode(tmp_path, monkeypatch):
monkeypatch.setenv(ENV_HOME, str(tmp_path))
monkeypatch.delenv("MIRAGE_AUTH_MODE", raising=False)
monkeypatch.delenv("MIRAGE_DAEMON_PORT", raising=False)
(tmp_path / "config.toml").write_text('[daemon]\nauth_mode = "token"\n')
spawned = []
monkeypatch.setattr("mirage.cli.client.subprocess.Popen",
lambda *args, **kwargs: spawned.append(kwargs))
with DaemonClient(DaemonSettings()) as client:
client._spawn_daemon()
assert "MIRAGE_AUTH_MODE" not in spawned[0]["env"]
+105
View File
@@ -0,0 +1,105 @@
# ========= 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 typer.testing import CliRunner
from mirage.cli.config import app
runner = CliRunner()
def _isolate_home(monkeypatch, tmp_path):
monkeypatch.setenv("MIRAGE_HOME", str(tmp_path))
monkeypatch.delenv("MIRAGE_PID_FILE", raising=False)
def test_config_set_then_get(monkeypatch, tmp_path):
_isolate_home(monkeypatch, tmp_path)
r = runner.invoke(app, ["set", "version_root", "/data/repos"])
assert r.exit_code == 0
r = runner.invoke(app, ["get", "version_root"])
assert r.exit_code == 0
assert "/data/repos" in r.stdout
def test_config_list(monkeypatch, tmp_path):
_isolate_home(monkeypatch, tmp_path)
runner.invoke(app, ["set", "url", "http://a:1"])
r = runner.invoke(app, ["list"])
assert r.exit_code == 0
assert "url" in r.stdout and "http://a:1" in r.stdout
def test_config_unset(monkeypatch, tmp_path):
_isolate_home(monkeypatch, tmp_path)
runner.invoke(app, ["set", "socket", "/tmp/s.sock"])
runner.invoke(app, ["unset", "socket"])
r = runner.invoke(app, ["get", "socket"])
assert r.exit_code != 0
def test_config_get_unset_exits_nonzero(monkeypatch, tmp_path):
_isolate_home(monkeypatch, tmp_path)
r = runner.invoke(app, ["get", "pid_file"])
assert r.exit_code != 0
def test_config_set_rejects_unknown_key(monkeypatch, tmp_path):
_isolate_home(monkeypatch, tmp_path)
r = runner.invoke(app, ["set", "MIRAGE_HOME", "/x"])
assert r.exit_code == 2
def test_config_set_unknown_key_message_is_clean(monkeypatch, tmp_path):
_isolate_home(monkeypatch, tmp_path)
r = runner.invoke(app, ["set", "nope", "/x"])
assert r.exit_code == 2
assert "unknown config key" in r.output
assert not r.output.strip().startswith("'")
def test_config_list_malformed_toml_fails_cleanly(monkeypatch, tmp_path):
_isolate_home(monkeypatch, tmp_path)
(tmp_path / "config.toml").write_text("[daemon\nnot toml")
r = runner.invoke(app, ["list"])
assert r.exit_code == 2
assert "malformed" in r.output
assert "Traceback" not in r.output
def test_config_list_warns_on_unknown_keys(monkeypatch, tmp_path):
_isolate_home(monkeypatch, tmp_path)
(tmp_path / "config.toml").write_text('[daemon]\ntypo_key = "x"\n')
r = runner.invoke(app, ["list"])
assert r.exit_code == 0
assert "typo_key" in r.output
assert "unknown" in r.output.lower()
def test_config_list_resolved_shows_origin(monkeypatch, tmp_path):
_isolate_home(monkeypatch, tmp_path)
monkeypatch.setenv("MIRAGE_VERSION_ROOT", "/env/repos")
r = runner.invoke(app, ["list", "--resolved"])
assert r.exit_code == 0
assert "/env/repos" in r.output
assert "MIRAGE_VERSION_ROOT" in r.output
def test_config_list_resolved_masks_auth_token(monkeypatch, tmp_path):
_isolate_home(monkeypatch, tmp_path)
monkeypatch.setenv("MIRAGE_TOKEN", "supersecret")
r = runner.invoke(app, ["list", "--resolved"])
assert r.exit_code == 0
assert "supersecret" not in r.output
assert "***" in r.output
+160
View File
@@ -0,0 +1,160 @@
from mirage.cli.output import exit_code_from_response
def test_io_zero():
assert exit_code_from_response({
"kind": "io",
"exit_code": 0,
"stdout": "",
"stderr": ""
}) == 0
def test_io_nonzero():
assert exit_code_from_response({
"kind": "io",
"exit_code": 1,
"stdout": "",
"stderr": ""
}) == 1
assert exit_code_from_response({
"kind": "io",
"exit_code": 42,
"stdout": "",
"stderr": ""
}) == 42
assert exit_code_from_response({
"kind": "io",
"exit_code": 127,
"stdout": "",
"stderr": ""
}) == 127
def test_clamp_high():
assert exit_code_from_response({"kind": "io", "exit_code": 300}) == 255
def test_clamp_negative():
assert exit_code_from_response({"kind": "io", "exit_code": -1}) == 0
def test_truncate_float():
assert exit_code_from_response({"kind": "io", "exit_code": 1.9}) == 1
def test_nan_returns_zero():
assert exit_code_from_response({
"kind": "io",
"exit_code": float("nan")
}) == 0
def test_inf_returns_zero():
assert exit_code_from_response({
"kind": "io",
"exit_code": float("inf")
}) == 0
def test_bool_returns_zero():
assert exit_code_from_response({"kind": "io", "exit_code": True}) == 0
def test_bg_submission():
assert exit_code_from_response({
"job_id": "job_abc",
"workspace_id": "ws",
"submitted_at": 0,
}) == 0
def test_provision_kind():
assert exit_code_from_response({"kind": "provision", "detail": "ok"}) == 0
def test_raw_kind():
assert exit_code_from_response({"kind": "raw", "value": "hi"}) == 0
def test_job_detail_done():
assert exit_code_from_response({
"job_id": "job_x",
"status": "done",
"result": {
"kind": "io",
"exit_code": 7,
"stdout": "",
"stderr": ""
},
"error": None,
}) == 7
def test_job_pending_returns_zero():
assert exit_code_from_response({
"job_id": "job_x",
"status": "pending",
"result": None,
"error": None,
}) == 0
def test_job_running_returns_zero():
assert exit_code_from_response({
"job_id": "job_x",
"status": "running",
"result": None,
"error": None,
}) == 0
def test_job_failed_no_result_returns_two():
assert exit_code_from_response({
"job_id": "job_x",
"status": "failed",
"result": None,
"error": "boom",
}) == 2
def test_job_canceled_no_result_returns_two():
assert exit_code_from_response({
"job_id": "job_x",
"status": "canceled",
"result": None,
"error": None,
}) == 2
def test_job_failed_with_result_prefers_inner():
assert exit_code_from_response({
"job_id": "job_x",
"status": "failed",
"result": {
"kind": "io",
"exit_code": 9,
"stdout": "",
"stderr": ""
},
"error": None,
}) == 9
def test_non_dict_inputs():
assert exit_code_from_response(None) == 0
assert exit_code_from_response("string") == 0
assert exit_code_from_response(42) == 0
assert exit_code_from_response([1, 2, 3]) == 0
def test_io_missing_exit_code():
assert exit_code_from_response({
"kind": "io",
"stdout": "",
"stderr": ""
}) == 0
def test_io_non_numeric_exit_code():
assert exit_code_from_response({"kind": "io", "exit_code": "one"}) == 0
+102
View File
@@ -0,0 +1,102 @@
# ========= 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 re
from contextlib import contextmanager
from typer.testing import CliRunner
from mirage.cli import session as session_cli
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*m")
class _FakeResponse:
status_code = 201
content = b'{"session_id": "agent", "cwd": "/"}'
def json(self) -> dict:
return {"session_id": "agent", "cwd": "/"}
class _FakeClient:
def __init__(self) -> None:
self.last_body: dict | None = None
def __enter__(self):
return self
def __exit__(self, *_):
return False
def ensure_running(self, allow_spawn: bool = False) -> None:
return None
def request(self, method: str, path: str, json: dict | None = None):
self.last_body = json
return _FakeResponse()
@contextmanager
def _patched_client(fake: _FakeClient):
real = session_cli.make_client
session_cli.make_client = lambda: fake
try:
yield fake
finally:
session_cli.make_client = real
def test_session_create_passes_mount_flags():
fake = _FakeClient()
with _patched_client(fake):
result = CliRunner().invoke(
session_cli.app,
[
"create", "demo", "--id", "agent", "-m", "/s3", "--mount",
"/slack"
],
)
assert result.exit_code == 0, result.output
assert fake.last_body == {
"session_id": "agent",
"allowed_mounts": ["/s3", "/slack"],
}
def test_session_create_no_mount_flag_omits_field():
fake = _FakeClient()
with _patched_client(fake):
result = CliRunner().invoke(
session_cli.app,
["create", "demo", "--id", "agent"],
)
assert result.exit_code == 0, result.output
assert fake.last_body == {"session_id": "agent"}
def test_session_create_without_id_or_mount_sends_empty_body():
fake = _FakeClient()
with _patched_client(fake):
result = CliRunner().invoke(session_cli.app, ["create", "demo"])
assert result.exit_code == 0, result.output
assert fake.last_body == {}
def test_session_create_help_lists_mount_flag():
result = CliRunner().invoke(session_cli.app, ["create", "--help"])
assert result.exit_code == 0
plain = _ANSI_RE.sub("", result.output)
assert "--mount" in plain
+203
View File
@@ -0,0 +1,203 @@
# ========= 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.cli.settings import (DEFAULT_DAEMON_URL, config_path, get_config,
list_config, load_daemon_settings,
resolved_config, set_config, unset_config)
from mirage.server.daemon_config import DaemonConfigError
from mirage.server.env import ENV_HOME
def test_load_daemon_settings_falls_back_to_token_file(tmp_path, monkeypatch):
monkeypatch.delenv("MIRAGE_TOKEN", raising=False)
monkeypatch.delenv("MIRAGE_DAEMON_URL", raising=False)
monkeypatch.setenv(ENV_HOME, str(tmp_path))
config_file = tmp_path / "config.toml"
config_file.write_text("")
token_file = tmp_path / "auth_token"
token_file.write_text("file-token\n")
settings = load_daemon_settings(path=config_file)
assert settings.auth_token == "file-token"
def test_load_daemon_settings_env_wins_over_file(tmp_path, monkeypatch):
monkeypatch.setenv("MIRAGE_TOKEN", "from-env")
monkeypatch.delenv("MIRAGE_DAEMON_URL", raising=False)
monkeypatch.setenv(ENV_HOME, str(tmp_path))
config_file = tmp_path / "config.toml"
config_file.write_text("")
token_file = tmp_path / "auth_token"
token_file.write_text("from-file")
settings = load_daemon_settings(path=config_file)
assert settings.auth_token == "from-env"
def test_load_daemon_settings_config_wins_over_file(tmp_path, monkeypatch):
monkeypatch.delenv("MIRAGE_TOKEN", raising=False)
monkeypatch.delenv("MIRAGE_DAEMON_URL", raising=False)
monkeypatch.setenv(ENV_HOME, str(tmp_path))
config_file = tmp_path / "config.toml"
config_file.write_text('[daemon]\nauth_token = "from-config"\n')
token_file = tmp_path / "auth_token"
token_file.write_text("from-file")
settings = load_daemon_settings(path=config_file)
assert settings.auth_token == "from-config"
def test_load_daemon_settings_no_sources_yields_empty_token(
tmp_path, monkeypatch):
monkeypatch.delenv("MIRAGE_TOKEN", raising=False)
monkeypatch.delenv("MIRAGE_DAEMON_URL", raising=False)
monkeypatch.setenv(ENV_HOME, str(tmp_path))
config_file = tmp_path / "config.toml"
config_file.write_text("")
settings = load_daemon_settings(path=config_file)
assert settings.auth_token == ""
def test_config_path_follows_mirage_home(tmp_path, monkeypatch):
monkeypatch.setenv(ENV_HOME, str(tmp_path))
assert config_path() == tmp_path / "config.toml"
def test_load_daemon_settings_reads_config_under_mirage_home(
tmp_path, monkeypatch):
monkeypatch.delenv("MIRAGE_TOKEN", raising=False)
monkeypatch.delenv("MIRAGE_DAEMON_URL", raising=False)
monkeypatch.setenv(ENV_HOME, str(tmp_path))
(tmp_path /
"config.toml").write_text('[daemon]\nurl = "http://127.0.0.1:9999"\n')
settings = load_daemon_settings()
assert settings.url == "http://127.0.0.1:9999"
def test_set_config_creates_file(tmp_path):
p = tmp_path / "config.toml"
set_config("version_root", "/data/repos", path=p)
assert get_config("version_root", path=p) == "/data/repos"
assert '[daemon]' in p.read_text()
def test_set_config_updates_existing_key(tmp_path):
p = tmp_path / "config.toml"
set_config("url", "http://a:1", path=p)
set_config("url", "http://b:2", path=p)
assert get_config("url", path=p) == "http://b:2"
assert p.read_text().count("url =") == 1
def test_set_config_preserves_comments_and_other_keys(tmp_path):
p = tmp_path / "config.toml"
p.write_text('[daemon]\n# keep me\nurl = "http://a:1"\n')
set_config("pid_file", "/tmp/x.pid", path=p)
text = p.read_text()
assert "# keep me" in text
assert 'url = "http://a:1"' in text
assert get_config("pid_file", path=p) == "/tmp/x.pid"
def test_unset_config_removes_key(tmp_path):
p = tmp_path / "config.toml"
set_config("socket", "/tmp/s.sock", path=p)
unset_config("socket", path=p)
assert get_config("socket", path=p) is None
def test_list_config_returns_written_keys(tmp_path):
p = tmp_path / "config.toml"
set_config("url", "http://a:1", path=p)
set_config("snapshot_root", "/snaps", path=p)
assert list_config(path=p) == {
"url": "http://a:1",
"snapshot_root": "/snaps"
}
def test_set_config_rejects_unknown_key(tmp_path):
with pytest.raises(DaemonConfigError, match="unknown config key"):
set_config("MIRAGE_HOME", "/x", path=tmp_path / "config.toml")
def test_numeric_key_written_bare(tmp_path):
p = tmp_path / "config.toml"
set_config("idle_grace_seconds", "45", path=p)
assert "idle_grace_seconds = 45" in p.read_text()
def test_unset_config_accepts_unknown_key(tmp_path):
p = tmp_path / "config.toml"
p.write_text('[daemon]\ntypo_key = "x"\nurl = "http://a:1"\n')
unset_config("typo_key", path=p)
assert "typo_key" not in p.read_text()
assert 'url = "http://a:1"' in p.read_text()
def test_set_config_chmods_0600(tmp_path):
p = tmp_path / "config.toml"
set_config("auth_token", "s3cret", path=p)
assert (p.stat().st_mode & 0o777) == 0o600
def test_unset_config_chmods_0600(tmp_path):
p = tmp_path / "config.toml"
p.write_text('[daemon]\nurl = "http://a:1"\nsocket = "/tmp/s"\n')
unset_config("socket", path=p)
assert (p.stat().st_mode & 0o777) == 0o600
def test_load_daemon_settings_missing_explicit_path_returns_defaults(
tmp_path, monkeypatch):
monkeypatch.delenv("MIRAGE_TOKEN", raising=False)
monkeypatch.delenv("MIRAGE_DAEMON_URL", raising=False)
monkeypatch.setenv(ENV_HOME, str(tmp_path))
settings = load_daemon_settings(path=tmp_path / "nope.toml")
assert settings.url == DEFAULT_DAEMON_URL
def test_resolved_config_reports_origins(monkeypatch, tmp_path):
monkeypatch.setenv(ENV_HOME, str(tmp_path))
monkeypatch.setenv("MIRAGE_VERSION_ROOT", "/env/repos")
monkeypatch.delenv("MIRAGE_SNAPSHOT_ROOT", raising=False)
monkeypatch.delenv("MIRAGE_DAEMON_URL", raising=False)
(tmp_path / "config.toml").write_text(
'[daemon]\nversion_root = "/file/repos"\nurl = "http://f:1"\n')
resolved = resolved_config()
assert resolved["version_root"] == ("/env/repos",
"env MIRAGE_VERSION_ROOT")
assert resolved["url"] == ("http://f:1", "file")
assert resolved["snapshot_root"] == (str(tmp_path / "snapshots"),
"default")
def test_resolved_config_defaults_when_nothing_set(monkeypatch, tmp_path):
monkeypatch.setenv(ENV_HOME, str(tmp_path))
for name in ("MIRAGE_DAEMON_URL", "MIRAGE_TOKEN", "MIRAGE_PID_FILE",
"MIRAGE_VERSION_ROOT", "MIRAGE_SNAPSHOT_ROOT",
"MIRAGE_IDLE_GRACE_SECONDS"):
monkeypatch.delenv(name, raising=False)
resolved = resolved_config()
assert resolved["url"] == (DEFAULT_DAEMON_URL, "default")
assert resolved["pid_file"] == (str(tmp_path / "daemon.pid"), "default")
assert resolved["idle_grace_seconds"] == ("30", "default")
def test_resolved_config_includes_port(monkeypatch, tmp_path):
monkeypatch.setenv(ENV_HOME, str(tmp_path))
monkeypatch.delenv("MIRAGE_DAEMON_PORT", raising=False)
resolved = resolved_config()
assert resolved["port"] == ("8765", "default")
monkeypatch.setenv("MIRAGE_DAEMON_PORT", "9100")
assert resolved_config()["port"] == ("9100", "env MIRAGE_DAEMON_PORT")
+202
View File
@@ -0,0 +1,202 @@
# ========= 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 subprocess
import sys
from pathlib import Path
CONFIG_YAML = """\
mounts:
/:
resource: ram
mode: WRITE
"""
def _write_config(tmp_path: Path) -> Path:
p = tmp_path / "config.yaml"
p.write_text(CONFIG_YAML, encoding="utf-8")
return p
def _run(env: dict, *args: str, expect_exit: int = 0) -> dict | list:
cmd = [sys.executable, "-m", "mirage.cli.main", *args]
proc = subprocess.run(cmd, env=env, capture_output=True, timeout=30)
if proc.returncode != expect_exit:
raise AssertionError(
f"exit={proc.returncode} (expected {expect_exit})\n"
f"stdout: {proc.stdout.decode()}\nstderr: {proc.stderr.decode()}")
if expect_exit != 0 or not proc.stdout.strip():
return {}
return json.loads(proc.stdout)
def test_commit_log_checkout_clone(daemon, tmp_path):
env = daemon["env"]
cfg = _write_config(tmp_path)
wid = _run(env, "workspace", "create", str(cfg))["id"]
_run(env, "execute", "-w", wid, "-c", "echo v1 > /notes.txt")
v1 = _run(env, "workspace", "commit", wid, "-m", "first")["version"]
_run(env, "execute", "-w", wid, "-c", "echo v2 > /notes.txt")
_run(env, "workspace", "commit", wid, "-m", "second")
log = _run(env, "workspace", "log", wid)
assert [e["message"] for e in log] == ["second", "first"]
_run(env, "workspace", "checkout", wid, v1)
reverted = _run(env, "execute", "-w", wid, "-c", "cat /notes.txt")
assert reverted["stdout"] == "v1\n"
clone = _run(env, "workspace", "clone", wid, "--at", v1)
assert clone["id"] != wid
def test_log_empty_before_commit(daemon, tmp_path):
env = daemon["env"]
cfg = _write_config(tmp_path)
wid = _run(env, "workspace", "create", str(cfg))["id"]
assert _run(env, "workspace", "log", wid) == []
def test_diff_versions_and_live(daemon, tmp_path):
env = daemon["env"]
cfg = _write_config(tmp_path)
wid = _run(env, "workspace", "create", str(cfg))["id"]
_run(env, "execute", "-w", wid, "-c", "echo one > /a.txt")
v1 = _run(env, "workspace", "commit", wid, "-m", "first")["version"]
_run(env, "execute", "-w", wid, "-c", "echo two > /a.txt")
_run(env, "execute", "-w", wid, "-c", "echo new > /b.txt")
v2 = _run(env, "workspace", "commit", wid, "-m", "second")["version"]
by_version = _run(env, "workspace", "diff", wid, v1, v2)
assert by_version["modified"] == ["a.txt"]
assert by_version["added"] == ["b.txt"]
_run(env, "execute", "-w", wid, "-c", "echo three > /a.txt")
live = _run(env, "workspace", "diff", wid)
assert live["modified"] == ["a.txt"]
def test_branch_diverges_and_guards_commit(daemon, tmp_path):
env = daemon["env"]
cfg = _write_config(tmp_path)
wid = _run(env, "workspace", "create", str(cfg))["id"]
_run(env, "execute", "-w", wid, "-c", "echo one > /a.txt")
_run(env, "workspace", "commit", wid, "-m", "first")
_run(env, "workspace", "branch", wid, "exp")
_run(env, "execute", "-w", wid, "-c", "echo two > /a.txt")
_run(env, "workspace", "commit", wid, "-b", "exp", "-m", "on exp")
exp_log = _run(env, "workspace", "log", wid, "-b", "exp")
main_log = _run(env, "workspace", "log", wid, "-b", "main")
assert [e["message"] for e in exp_log] == ["on exp", "first"]
assert [e["message"] for e in main_log] == ["first"]
_run(env,
"workspace",
"commit",
wid,
"-b",
"ghost",
"-m",
"x",
expect_exit=2)
def test_diff_includes_deleted(daemon, tmp_path):
env = daemon["env"]
cfg = _write_config(tmp_path)
wid = _run(env, "workspace", "create", str(cfg))["id"]
_run(env, "execute", "-w", wid, "-c", "echo one > /a.txt")
_run(env, "execute", "-w", wid, "-c", "echo two > /b.txt")
v1 = _run(env, "workspace", "commit", wid, "-m", "first")["version"]
_run(env, "execute", "-w", wid, "-c", "rm /b.txt")
v2 = _run(env, "workspace", "commit", wid, "-m", "second")["version"]
changes = _run(env, "workspace", "diff", wid, v1, v2)
assert changes["deleted"] == ["b.txt"]
def test_clone_live_and_explicit_id(daemon, tmp_path):
env = daemon["env"]
cfg = _write_config(tmp_path)
wid = _run(env, "workspace", "create", str(cfg))["id"]
_run(env, "execute", "-w", wid, "-c", "echo hello > /a.txt")
auto = _run(env, "workspace", "clone", wid)
assert auto["id"] != wid
named = _run(env, "workspace", "clone", wid, "--id", "myclone")
assert named["id"] == "myclone"
got = _run(env, "execute", "-w", "myclone", "-c", "cat /a.txt")
assert got["stdout"] == "hello\n"
def test_branch_from_non_main(daemon, tmp_path):
env = daemon["env"]
cfg = _write_config(tmp_path)
wid = _run(env, "workspace", "create", str(cfg))["id"]
_run(env, "execute", "-w", wid, "-c", "echo one > /a.txt")
_run(env, "workspace", "commit", wid, "-m", "first")
_run(env, "workspace", "branch", wid, "exp")
_run(env, "execute", "-w", wid, "-c", "echo two > /a.txt")
_run(env, "workspace", "commit", wid, "-b", "exp", "-m", "on exp")
_run(env, "workspace", "branch", wid, "exp2", "--from", "exp")
log = _run(env, "workspace", "log", wid, "-b", "exp2")
assert [e["message"] for e in log] == ["on exp", "first"]
def test_checkout_by_branch_name(daemon, tmp_path):
env = daemon["env"]
cfg = _write_config(tmp_path)
wid = _run(env, "workspace", "create", str(cfg))["id"]
_run(env, "execute", "-w", wid, "-c", "echo one > /a.txt")
_run(env, "workspace", "commit", wid, "-m", "first")
_run(env, "workspace", "branch", wid, "exp")
_run(env, "execute", "-w", wid, "-c", "echo two > /a.txt")
_run(env, "workspace", "commit", wid, "-b", "exp", "-m", "on exp")
_run(env, "workspace", "checkout", wid, "main")
on_main = _run(env, "execute", "-w", wid, "-c", "cat /a.txt")
assert on_main["stdout"] == "one\n"
_run(env, "workspace", "checkout", wid, "exp")
on_exp = _run(env, "execute", "-w", wid, "-c", "cat /a.txt")
assert on_exp["stdout"] == "two\n"
def test_error_paths_exit_2(daemon, tmp_path):
env = daemon["env"]
cfg = _write_config(tmp_path)
wid = _run(env, "workspace", "create", str(cfg))["id"]
_run(env, "execute", "-w", wid, "-c", "echo one > /a.txt")
_run(env, "workspace", "commit", wid, "-m", "first")
_run(env, "workspace", "branch", wid, "exp")
_run(env, "workspace", "branch", wid, "exp", expect_exit=2)
_run(env, "workspace", "commit", "ghost-ws", "-m", "x", expect_exit=2)
_run(env, "workspace", "checkout", wid, "nope", expect_exit=2)
_run(env, "workspace", "diff", wid, "nope", "nope2", expect_exit=2)
@@ -0,0 +1,25 @@
import pytest
from mirage.commands.builtin.chroma.find import _default_name, _expr_texts
@pytest.mark.parametrize("texts", [
("!", "-name", "x"),
("(", "-name", "a", "-o", "-name", "b", ")"),
("-name", "x"),
("-not", "-name", "x"),
])
def test_expr_texts_preserves_expression(texts):
assert _expr_texts(texts) == texts
def test_expr_texts_strips_bare_leading_name():
assert _expr_texts(("foo", )) == ()
assert _expr_texts(()) == ()
def test_default_name_only_for_bare_word():
assert _default_name(None, ("foo", )) == "foo"
assert _default_name(None, ("!", "-name", "x")) is None
assert _default_name(None, ("(", "-name", "a")) is None
assert _default_name("given", ("foo", )) == "given"
+24
View File
@@ -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 pytest
from mirage.resource.ram import RAMResource
@pytest.fixture
def backend():
b = RAMResource()
b.accessor.store.dirs.add("/tmp")
return b
@@ -0,0 +1,91 @@
# ========= 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.cache.index import RAMIndexCacheStore
from mirage.types import PathSpec
from mirage.utils.stream import collect_bytes
from tests.resource.databricks_volume.test_databricks_volume import (
FakeFiles, make_resource, seed_directory, seed_file)
def seed_text_command_fixture(files: FakeFiles) -> str:
root = "/Volumes/main/default/agent_files/root"
seed_directory(files, root)
seed_file(files, f"{root}/words.txt", b"beta\nalpha\nalpha\n")
seed_file(files, f"{root}/more.txt", b"delta\n")
seed_file(files, f"{root}/table.csv", b"name,score\nann,2\nbob,3\n")
seed_file(files, f"{root}/table_extra.csv", b"name,score\ncam,5\n")
seed_file(files, f"{root}/data.json", b'{"name": "mirage"}\n')
seed_file(files, f"{root}/data2.json", b'{"name": "agent"}\n')
seed_file(files, f"{root}/events.jsonl", b'{"name": "first"}\n')
seed_file(files, f"{root}/events_extra.jsonl", b'{"name": "second"}\n')
seed_file(files, f"{root}/old.txt", b"same\nold\n")
seed_file(files, f"{root}/new.txt", b"same\nnew\n")
return root
async def materialize(source) -> bytes:
if source is None:
return b""
return await collect_bytes(source)
class IndexTrackingReader:
def __init__(self) -> None:
self.seen_indexes: list[RAMIndexCacheStore | None] = []
async def read_bytes(self,
accessor,
path,
index=None,
*args,
**kwargs) -> bytes:
self.seen_indexes.append(index)
original = path.virtual if isinstance(path, PathSpec) else path
if str(original).endswith(".json"):
return b'{"name": "mirage"}\n'
if str(original).endswith(".csv"):
return b"name,score\nann,2\n"
return b"beta\nalpha\nalpha\n"
async def read_stream(self, accessor, path, index=None, *args, **kwargs):
self.seen_indexes.append(index)
yield await self.read_bytes(accessor, path, index, *args, **kwargs)
@pytest.fixture
def databricks_text_files() -> FakeFiles:
files = FakeFiles()
seed_text_command_fixture(files)
return files
@pytest.fixture
def databricks_text_workspace(databricks_text_files: FakeFiles) -> Workspace:
return Workspace({"/dbx/": make_resource(databricks_text_files)},
mode=MountMode.READ)
@pytest.fixture
def expected_index() -> RAMIndexCacheStore:
return RAMIndexCacheStore(ttl=600)
@pytest.fixture
def materialize_output():
return materialize
@@ -0,0 +1,25 @@
# ========= 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
@pytest.mark.asyncio
async def test_workspace_execute_databricks_volume_awk(
databricks_text_workspace):
io = await databricks_text_workspace.execute(
"awk '{print $1}' /dbx/table.csv")
assert io.exit_code == 0
assert io.stdout == b"name,score\nann,2\nbob,3\n"
@@ -0,0 +1,186 @@
import time
from functools import partial
import pytest
from mirage.accessor.databricks_volume import DatabricksVolumeAccessor
from mirage.cache.index import RAMIndexCacheStore
from mirage.commands.builtin.generic.find import parse_find_args, walk_find
from mirage.commands.builtin.generic.ls import ls as generic_ls
from mirage.commands.builtin.generic.tree import tree as generic_tree
from mirage.core.databricks_volume.path import backend_path
from mirage.core.databricks_volume.readdir import readdir
from mirage.core.databricks_volume.stat import stat
from mirage.resource.databricks_volume import DatabricksVolumeConfig
from mirage.types import LsSortBy, PathSpec
from mirage.utils.key_prefix import mount_key
from tests.core.databricks_volume.conftest import (FakeClient, FakeFiles,
directory_entry, file_entry)
MODIFIED_MS = 1_700_000_000_000
def is_dir_name(_child: str) -> bool | None:
# Databricks readdir returns slash-less paths, so classification always
# falls back to stat.
return None
FROZEN_NOW_S = 1_700_000_000
DAY_S = 86_400
AGES_DAYS = (1, 2, 3, 10, 20)
def _rig(
) -> tuple[DatabricksVolumeAccessor, FakeFiles, RAMIndexCacheStore, str]:
config = DatabricksVolumeConfig(
catalog="main",
schema="default",
volume="agent_files",
root_path="/root",
token="secret",
)
files = FakeFiles()
accessor = DatabricksVolumeAccessor(config, FakeClient(files))
index = RAMIndexCacheStore(ttl=600)
return accessor, files, index, backend_path(config, "/")
def _seed_flat(files: FakeFiles, root: str, count: int = 5) -> None:
files.directories[f"{root}/sub"] = [
file_entry(f"{root}/sub/f{i}.txt",
size=i + 1,
modified=MODIFIED_MS + i * 1000) for i in range(count)
]
def _ls_readdir(accessor: DatabricksVolumeAccessor):
return partial(readdir, accessor)
def _ls_stat(accessor: DatabricksVolumeAccessor):
return partial(stat, accessor)
@pytest.mark.asyncio
@pytest.mark.parametrize("sort_by,long", [
(LsSortBy.NAME, False),
(LsSortBy.NAME, True),
(LsSortBy.TIME, False),
(LsSortBy.SIZE, False),
])
async def test_ls_lists_once_without_per_entry_metadata(sort_by, long):
accessor, files, index, root = _rig()
_seed_flat(files, root, count=5)
path = PathSpec.from_str_path("/volume/sub",
mount_key("/volume/sub", "/volume"))
await generic_ls([path],
readdir=_ls_readdir(accessor),
stat=_ls_stat(accessor),
long=long,
sort_by=sort_by,
index=index)
assert files.list_directory_calls == [f"{root}/sub"]
assert files.get_metadata_calls == []
assert files.get_directory_metadata_calls == []
@pytest.mark.asyncio
async def test_ls_recursive_one_list_per_directory():
accessor, files, index, root = _rig()
files.directories[f"{root}/sub"] = [
file_entry(f"{root}/sub/a.txt", size=1, modified=MODIFIED_MS),
directory_entry(f"{root}/sub/inner"),
]
files.directories[f"{root}/sub/inner"] = [
file_entry(f"{root}/sub/inner/b.txt", size=2, modified=MODIFIED_MS),
]
path = PathSpec.from_str_path("/volume/sub",
mount_key("/volume/sub", "/volume"))
await generic_ls([path],
readdir=_ls_readdir(accessor),
stat=_ls_stat(accessor),
recursive=True,
index=index)
assert sorted(
files.list_directory_calls) == [f"{root}/sub", f"{root}/sub/inner"]
assert files.get_metadata_calls == []
assert files.get_directory_metadata_calls == []
@pytest.mark.asyncio
async def test_tree_one_list_per_directory_without_metadata():
accessor, files, index, root = _rig()
files.directories[f"{root}/sub"] = [
file_entry(f"{root}/sub/a.txt", size=1, modified=MODIFIED_MS),
directory_entry(f"{root}/sub/inner"),
]
files.directories[f"{root}/sub/inner"] = [
file_entry(f"{root}/sub/inner/b.txt", size=2, modified=MODIFIED_MS),
]
path = PathSpec.from_str_path("/volume/sub",
mount_key("/volume/sub", "/volume"))
await generic_tree(path,
readdir=_ls_readdir(accessor),
stat=_ls_stat(accessor),
index=index)
assert sorted(
files.list_directory_calls) == [f"{root}/sub", f"{root}/sub/inner"]
assert files.get_metadata_calls == []
assert files.get_directory_metadata_calls == []
async def _run_find(accessor, index, *, type=None, size=None, mtime=None):
args = parse_find_args((), type=type, size=size, mtime=mtime)
path = PathSpec.from_str_path("/volume/sub",
mount_key("/volume/sub", "/volume"))
return await walk_find(path,
readdir=_ls_readdir(accessor),
stat=_ls_stat(accessor),
is_dir_name=is_dir_name,
index=index,
args=args)
@pytest.mark.asyncio
async def test_find_type_does_not_stat_children():
accessor, files, index, root = _rig()
_seed_flat(files, root, count=5)
files.directory_metadata.add(f"{root}/sub")
results = await _run_find(accessor, index, type="f")
assert len(results) == 5
assert files.list_directory_calls == [f"{root}/sub"]
child_metadata = [c for c in files.get_metadata_calls if "/sub/" in c]
assert child_metadata == []
@pytest.mark.asyncio
async def test_find_size_reads_size_from_index():
accessor, files, index, root = _rig()
_seed_flat(files, root, count=5)
files.directory_metadata.add(f"{root}/sub")
results = await _run_find(accessor, index, type="f", size="+3c")
assert sorted(r.rsplit("/", 1)[-1]
for r in results) == ["f3.txt", "f4.txt"]
assert files.list_directory_calls == [f"{root}/sub"]
child_metadata = [c for c in files.get_metadata_calls if "/sub/" in c]
assert child_metadata == []
@pytest.mark.asyncio
async def test_find_mtime_reads_modified_from_index(monkeypatch):
monkeypatch.setattr(time, "time", lambda: float(FROZEN_NOW_S))
accessor, files, index, root = _rig()
files.directories[f"{root}/sub"] = [
file_entry(f"{root}/sub/f{i}.txt",
size=i + 1,
modified=(FROZEN_NOW_S - age * DAY_S) * 1000)
for i, age in enumerate(AGES_DAYS)
]
files.directory_metadata.add(f"{root}/sub")
results = await _run_find(accessor, index, mtime="-5")
assert sorted(r.rsplit("/", 1)[-1]
for r in results) == ["f0.txt", "f1.txt", "f2.txt"]
assert files.list_directory_calls == [f"{root}/sub"]
child_metadata = [c for c in files.get_metadata_calls if "/sub/" in c]
assert child_metadata == []
@@ -0,0 +1,40 @@
# ========= 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
@pytest.mark.asyncio
async def test_cat_single_file(databricks_text_workspace):
io = await databricks_text_workspace.execute("cat /dbx/words.txt")
assert io.exit_code == 0
assert io.stdout == b"beta\nalpha\nalpha\n"
@pytest.mark.asyncio
async def test_cat_multiple_files_concatenated(databricks_text_workspace):
io = await databricks_text_workspace.execute(
"cat /dbx/words.txt /dbx/more.txt")
assert io.exit_code == 0
assert io.stdout == b"beta\nalpha\nalpha\ndelta\n"
@pytest.mark.asyncio
async def test_cat_n_numbers_lines(databricks_text_workspace):
io = await databricks_text_workspace.execute("cat -n /dbx/words.txt")
assert io.exit_code == 0
assert io.stdout == b" 1\tbeta\n 2\talpha\n 3\talpha\n"
@@ -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, Workspace
from tests.resource.databricks_volume.test_databricks_volume import (
FakeFiles, make_resource, seed_directory, seed_file)
ROOT = "/Volumes/main/default/agent_files/root"
@pytest.fixture
def dbx_files() -> FakeFiles:
files = FakeFiles()
seed_directory(files, ROOT)
seed_file(files, f"{ROOT}/src.txt", b"hello")
return files
@pytest.fixture
def write_ws(dbx_files: FakeFiles) -> Workspace:
return Workspace({"/dbx/": make_resource(dbx_files)}, mode=MountMode.WRITE)
@pytest.fixture
def read_ws(dbx_files: FakeFiles) -> Workspace:
return Workspace({"/dbx/": make_resource(dbx_files)}, mode=MountMode.READ)
@pytest.mark.asyncio
async def test_cp_file_preserves_bytes(write_ws, dbx_files):
io = await write_ws.execute("cp /dbx/src.txt /dbx/dst.txt")
assert io.exit_code == 0
assert dbx_files.downloads[f"{ROOT}/dst.txt"] == b"hello"
@pytest.mark.asyncio
async def test_cp_directory_without_recursive_fails(write_ws, dbx_files):
seed_directory(dbx_files, f"{ROOT}/d")
io = await write_ws.execute("cp /dbx/d /dbx/d2")
assert io.exit_code != 0
@pytest.mark.asyncio
async def test_cp_writes_are_mount_relative(write_ws):
io = await write_ws.execute("cp /dbx/src.txt /dbx/dst.txt")
assert io.exit_code == 0
for key in io.writes:
assert key.startswith("/dbx/")
assert not key.startswith("/dbx/dbx/")
@pytest.mark.asyncio
async def test_cp_read_only_mount_rejected(read_ws, dbx_files):
io = await read_ws.execute("cp /dbx/src.txt /dbx/dst.txt")
assert io.exit_code != 0
assert b"read-only" in io.stderr
assert f"{ROOT}/dst.txt" not in dbx_files.downloads
@pytest.mark.asyncio
async def test_cp_onto_same_path_errors_and_preserves_file(
write_ws, dbx_files):
io = await write_ws.execute("cp /dbx/src.txt /dbx/src.txt")
assert io.exit_code != 0
assert b"are the same file" in io.stderr
assert dbx_files.downloads[f"{ROOT}/src.txt"] == b"hello"
@pytest.mark.asyncio
async def test_cp_multiple_sources_require_directory(write_ws, dbx_files):
seed_file(dbx_files, f"{ROOT}/a.txt", b"AAA")
seed_file(dbx_files, f"{ROOT}/b.txt", b"BBB")
seed_file(dbx_files, f"{ROOT}/target.txt", b"target")
io = await write_ws.execute("cp /dbx/a.txt /dbx/b.txt /dbx/target.txt")
assert io.exit_code != 0
assert b"not a directory" in io.stderr
assert dbx_files.downloads[f"{ROOT}/a.txt"] == b"AAA"
assert dbx_files.downloads[f"{ROOT}/b.txt"] == b"BBB"
assert dbx_files.downloads[f"{ROOT}/target.txt"] == b"target"
@pytest.mark.asyncio
async def test_cp_missing_source_reports_cannot_stat(write_ws, dbx_files):
io = await write_ws.execute("cp /dbx/missing /dbx/missing")
assert io.exit_code != 0
assert b"cannot stat" in io.stderr
assert b"are the same file" not in io.stderr
@pytest.mark.asyncio
async def test_cp_recursive_into_itself_errors_and_preserves_tree(
write_ws, dbx_files):
seed_directory(dbx_files, f"{ROOT}/d")
seed_file(dbx_files, f"{ROOT}/d/a.txt", b"aaa")
io = await write_ws.execute("cp -r /dbx/d /dbx/d")
assert io.exit_code != 0
assert b"into itself" in io.stderr
assert dbx_files.downloads[f"{ROOT}/d/a.txt"] == b"aaa"
assert f"{ROOT}/d/d" not in dbx_files.directory_metadata
@@ -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
from mirage import MountMode, Workspace
from mirage.resource.ram import RAMResource
from tests.resource.databricks_volume.test_databricks_volume import (
FakeFiles, make_resource, seed_file)
ROOT = "/Volumes/main/default/agent_files/root"
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 _ws_with_dbx_tree():
files = FakeFiles()
files.create_directory(ROOT)
files.create_directory(f"{ROOT}/tree")
files.create_directory(f"{ROOT}/tree/sub")
seed_file(files, f"{ROOT}/tree/a.txt", b"aaa\n")
seed_file(files, f"{ROOT}/tree/sub/b.txt", b"bbb\n")
dbx = make_resource(files)
ram = RAMResource()
ws = Workspace(
{
"/dbx": (dbx, MountMode.WRITE),
"/m": (ram, MountMode.WRITE),
}, )
return ws, files, ram
def test_cp_recursive_databricks_to_ram():
ws, _files, ram = _ws_with_dbx_tree()
_out, _err, code = _run(ws, "cp -r /dbx/tree /m/copied")
assert code == 0
assert ram._store.files["/copied/a.txt"] == b"aaa\n"
assert ram._store.files["/copied/sub/b.txt"] == b"bbb\n"
def test_cp_recursive_ram_to_databricks_creates_dirs():
ws, files, ram = _ws_with_dbx_tree()
ram._store.dirs.update({"/src", "/src/sub"})
ram._store.files["/src/x.txt"] = b"xxx\n"
ram._store.files["/src/sub/y.txt"] = b"yyy\n"
_out, _err, code = _run(ws, "cp -r /m/src /dbx/put")
assert code == 0
assert files.downloads[f"{ROOT}/put/x.txt"] == b"xxx\n"
assert files.downloads[f"{ROOT}/put/sub/y.txt"] == b"yyy\n"
assert f"{ROOT}/put/sub" in files.directory_metadata
def test_mv_recursive_databricks_to_ram_removes_source():
ws, files, ram = _ws_with_dbx_tree()
_out, _err, code = _run(ws, "mv /dbx/tree /m/moved")
assert code == 0
assert ram._store.files["/moved/a.txt"] == b"aaa\n"
assert ram._store.files["/moved/sub/b.txt"] == b"bbb\n"
assert f"{ROOT}/tree/a.txt" not in files.downloads
assert f"{ROOT}/tree" not in files.directory_metadata
def test_single_file_missing_parent_is_posix_error():
# Option B / POSIX: a single-file copy never creates the destination's
# parent directory; it errors like coreutils instead of mkdir -p.
ws, _files, ram = _ws_with_dbx_tree()
ram._store.files["/lone.txt"] = b"z\n"
_out, _err, code = _run(ws, "cp /m/lone.txt /dbx/nope/deep/lone.txt")
assert code == 1
@@ -0,0 +1,25 @@
# ========= 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
@pytest.mark.asyncio
async def test_workspace_execute_databricks_volume_cut(
databricks_text_workspace):
io = await databricks_text_workspace.execute("cut -d , -f 2 /dbx/table.csv"
)
assert io.exit_code == 0
assert io.stdout == b"score\n2\n3\n"
@@ -0,0 +1,37 @@
# ========= 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
@pytest.mark.asyncio
async def test_workspace_execute_databricks_volume_diff(
databricks_text_workspace):
io = await databricks_text_workspace.execute(
"diff -u /dbx/old.txt /dbx/new.txt")
assert io.exit_code == 1
assert b"-old" in io.stdout
assert b"+new" in io.stdout
@pytest.mark.asyncio
async def test_workspace_execute_databricks_volume_diff_resolves_glob(
databricks_text_workspace):
io = await databricks_text_workspace.execute(
"diff /dbx/old*.txt /dbx/new*.txt")
assert io.exit_code == 1
assert b"old" in io.stdout
assert b"new" in io.stdout
@@ -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, Workspace
from tests.resource.databricks_volume.test_databricks_volume import (
FakeFiles, make_resource, seed_directory, seed_file)
ROOT = "/Volumes/main/default/agent_files/root"
@pytest.fixture
def dbx_files() -> FakeFiles:
files = FakeFiles()
seed_directory(files, ROOT)
seed_file(files, f"{ROOT}/words.txt", b"beta\nalpha\nalpha\n")
seed_file(files, f"{ROOT}/notes.md", b"note\n")
files.create_directory(f"{ROOT}/sub")
seed_file(files, f"{ROOT}/sub/inner.txt", b"gamma\nalpha\n")
return files
@pytest.fixture
def ws(dbx_files: FakeFiles) -> Workspace:
return Workspace({"/dbx/": make_resource(dbx_files)}, mode=MountMode.READ)
@pytest.mark.asyncio
async def test_find_name_glob(ws):
io = await ws.execute("find /dbx/ -name '*.txt'")
assert io.exit_code == 0
out = io.stdout.decode()
assert "words.txt" in out
assert "inner.txt" in out
assert "notes.md" not in out
@pytest.mark.asyncio
async def test_find_type_d(ws):
io = await ws.execute("find /dbx/ -type d")
assert io.exit_code == 0
out = io.stdout.decode()
assert "sub" in out
assert "words.txt" not in out
@pytest.mark.asyncio
async def test_find_maxdepth(ws):
io = await ws.execute("find /dbx/ -maxdepth 1")
assert io.exit_code == 0
out = io.stdout.decode()
assert "words.txt" in out
assert "inner.txt" not in out
@@ -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 import MountMode, Workspace
from tests.resource.databricks_volume.test_databricks_volume import (
FakeFiles, make_resource, seed_directory, seed_file)
ROOT = "/Volumes/main/default/agent_files/root"
@pytest.fixture
def dbx_files() -> FakeFiles:
files = FakeFiles()
seed_directory(files, ROOT)
seed_file(files, f"{ROOT}/words.txt", b"beta\nalpha\nalpha\n")
files.create_directory(f"{ROOT}/sub")
seed_file(files, f"{ROOT}/sub/inner.txt", b"gamma\nalpha\n")
return files
@pytest.fixture
def ws(dbx_files: FakeFiles) -> Workspace:
return Workspace({"/dbx/": make_resource(dbx_files)}, mode=MountMode.READ)
@pytest.mark.asyncio
async def test_grep_single_file(ws):
io = await ws.execute("grep alpha /dbx/words.txt")
assert io.exit_code == 0
assert io.stdout == b"alpha\nalpha\n"
@pytest.mark.asyncio
async def test_grep_line_numbers(ws):
io = await ws.execute("grep -n alpha /dbx/words.txt")
assert io.exit_code == 0
assert io.stdout == b"2:alpha\n3:alpha\n"
@pytest.mark.asyncio
async def test_grep_count_only(ws):
io = await ws.execute("grep -c alpha /dbx/words.txt")
assert io.exit_code == 0
assert io.stdout == b"2\n"
@pytest.mark.asyncio
async def test_grep_recursive(ws):
io = await ws.execute("grep -r alpha /dbx/")
assert io.exit_code == 0
out = io.stdout.decode()
assert "words.txt" in out
assert "inner.txt" in out
@pytest.mark.asyncio
async def test_grep_no_match_exits_nonzero(ws):
io = await ws.execute("grep zeta /dbx/words.txt")
assert io.exit_code != 0
@@ -0,0 +1,34 @@
# ========= 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
@pytest.mark.asyncio
async def test_head_c_positive_prefix(databricks_text_workspace):
io = await databricks_text_workspace.execute("head -c 4 /dbx/words.txt")
assert io.exit_code == 0
assert io.stdout == b"beta"
@pytest.mark.asyncio
async def test_head_c_negative_all_but_last(databricks_text_workspace):
# GNU `head -c -N` emits all but the last N bytes. words.txt is 17 bytes,
# so -c -6 yields the first 11. A negative -c must not take the prefix
# fast path (range read 0..-6 is meaningless).
io = await databricks_text_workspace.execute("head -c -6 /dbx/words.txt")
assert io.exit_code == 0
assert io.stdout == b"beta\nalpha\n"
@@ -0,0 +1,33 @@
# ========= 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
@pytest.mark.asyncio
async def test_workspace_execute_databricks_volume_jq(
databricks_text_workspace):
io = await databricks_text_workspace.execute("jq -r .name /dbx/data.json")
assert io.exit_code == 0
assert io.stdout == b"mirage\n"
@pytest.mark.asyncio
async def test_workspace_execute_databricks_volume_jq_resolves_glob(
databricks_text_workspace):
io = await databricks_text_workspace.execute("jq -r .name /dbx/*.json")
assert io.exit_code == 0
assert io.stdout == b"mirage\nagent\n"
@@ -0,0 +1,81 @@
# ========= 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 tests.resource.databricks_volume.test_databricks_volume import (
FakeFiles, make_resource, seed_directory, seed_file)
ROOT = "/Volumes/main/default/agent_files/root"
@pytest.fixture
def dbx_files() -> FakeFiles:
files = FakeFiles()
seed_directory(files, ROOT)
seed_file(files, f"{ROOT}/words.txt", b"beta\nalpha\nalpha\n")
seed_file(files, f"{ROOT}/.hidden", b"h\n")
files.create_directory(f"{ROOT}/sub")
seed_file(files, f"{ROOT}/sub/inner.txt", b"gamma\nalpha\n")
return files
@pytest.fixture
def ws(dbx_files: FakeFiles) -> Workspace:
return Workspace({"/dbx/": make_resource(dbx_files)}, mode=MountMode.READ)
@pytest.mark.asyncio
async def test_ls_lists_entries(ws):
io = await ws.execute("ls /dbx/")
assert io.exit_code == 0
out = io.stdout.decode()
assert "words.txt" in out
assert "sub" in out
assert ".hidden" not in out
@pytest.mark.asyncio
async def test_ls_a_includes_hidden(ws):
io = await ws.execute("ls -a /dbx/")
assert io.exit_code == 0
assert ".hidden" in io.stdout.decode()
@pytest.mark.asyncio
async def test_ls_long_includes_size(ws):
io = await ws.execute("ls -l /dbx/")
assert io.exit_code == 0
out = io.stdout.decode()
assert "words.txt" in out
assert "17" in out
@pytest.mark.asyncio
async def test_ls_recursive_descends_subdirs(ws):
io = await ws.execute("ls -R /dbx/")
assert io.exit_code == 0
assert "inner.txt" in io.stdout.decode()
@pytest.mark.asyncio
async def test_ls_missing_path_warns(ws):
io = await ws.execute("ls /dbx/missing")
assert io.exit_code != 0
@@ -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 pytest
from mirage import MountMode, Workspace
from tests.resource.databricks_volume.test_databricks_volume import (
FakeFiles, make_resource, seed_directory)
ROOT = "/Volumes/main/default/agent_files/root"
@pytest.fixture
def dbx_files() -> FakeFiles:
files = FakeFiles()
seed_directory(files, ROOT)
return files
@pytest.fixture
def write_ws(dbx_files: FakeFiles) -> Workspace:
return Workspace({"/dbx/": make_resource(dbx_files)}, mode=MountMode.WRITE)
@pytest.fixture
def read_ws(dbx_files: FakeFiles) -> Workspace:
return Workspace({"/dbx/": make_resource(dbx_files)}, mode=MountMode.READ)
@pytest.mark.asyncio
async def test_mkdir_creates_directory(write_ws, dbx_files):
io = await write_ws.execute("mkdir /dbx/newdir")
assert io.exit_code == 0
assert f"{ROOT}/newdir" in dbx_files.directory_metadata
@pytest.mark.asyncio
async def test_mkdir_parent_missing_fails(write_ws):
io = await write_ws.execute("mkdir /dbx/a/b")
assert io.exit_code != 0
@pytest.mark.asyncio
async def test_mkdir_parents_creates_chain(write_ws, dbx_files):
io = await write_ws.execute("mkdir -p /dbx/a/b/c")
assert io.exit_code == 0
assert f"{ROOT}/a/b/c" in dbx_files.directory_metadata
@pytest.mark.asyncio
async def test_mkdir_writes_are_mount_relative(write_ws):
io = await write_ws.execute("mkdir /dbx/newdir")
assert io.exit_code == 0
for key in io.writes:
assert key.startswith("/dbx/")
assert not key.startswith("/dbx/dbx/")
@pytest.mark.asyncio
async def test_mkdir_read_only_mount_rejected(read_ws, dbx_files):
io = await read_ws.execute("mkdir /dbx/newdir")
assert io.exit_code != 0
assert b"read-only" in io.stderr
assert dbx_files.create_directory_calls == []
@pytest.mark.asyncio
async def test_ops_mkdir(write_ws, dbx_files):
await write_ws.ops.mkdir("/dbx/opdir")
assert f"{ROOT}/opdir" in dbx_files.directory_metadata
@pytest.mark.asyncio
async def test_ops_mkdir_read_only_rejected(read_ws):
with pytest.raises(PermissionError):
await read_ws.ops.mkdir("/dbx/opdir")
@@ -0,0 +1,156 @@
# ========= 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 tests.resource.databricks_volume.test_databricks_volume import (
FakeFiles, make_resource, seed_directory, seed_file)
ROOT = "/Volumes/main/default/agent_files/root"
@pytest.fixture
def dbx_files() -> FakeFiles:
files = FakeFiles()
seed_directory(files, ROOT)
seed_file(files, f"{ROOT}/src.txt", b"data")
return files
@pytest.fixture
def write_ws(dbx_files: FakeFiles) -> Workspace:
return Workspace({"/dbx/": make_resource(dbx_files)}, mode=MountMode.WRITE)
@pytest.fixture
def read_ws(dbx_files: FakeFiles) -> Workspace:
return Workspace({"/dbx/": make_resource(dbx_files)}, mode=MountMode.READ)
@pytest.mark.asyncio
async def test_mv_file_moves_bytes(write_ws, dbx_files):
io = await write_ws.execute("mv /dbx/src.txt /dbx/dst.txt")
assert io.exit_code == 0
assert dbx_files.downloads[f"{ROOT}/dst.txt"] == b"data"
assert f"{ROOT}/src.txt" not in dbx_files.downloads
@pytest.mark.asyncio
async def test_mv_writes_both_parents_mount_relative(write_ws):
io = await write_ws.execute("mv /dbx/src.txt /dbx/dst.txt")
assert io.exit_code == 0
assert "/dbx/src.txt" in io.writes
assert "/dbx/dst.txt" in io.writes
for key in io.writes:
assert not key.startswith("/dbx/dbx/")
@pytest.mark.asyncio
async def test_mv_no_clobber_skips_existing(write_ws, dbx_files):
seed_file(dbx_files, f"{ROOT}/dst.txt", b"keep")
io = await write_ws.execute("mv -n /dbx/src.txt /dbx/dst.txt")
assert io.exit_code == 0
assert dbx_files.downloads[f"{ROOT}/dst.txt"] == b"keep"
assert dbx_files.downloads[f"{ROOT}/src.txt"] == b"data"
@pytest.mark.asyncio
async def test_mv_read_only_mount_rejected(read_ws, dbx_files):
io = await read_ws.execute("mv /dbx/src.txt /dbx/dst.txt")
assert io.exit_code != 0
assert b"read-only" in io.stderr
assert dbx_files.downloads[f"{ROOT}/src.txt"] == b"data"
@pytest.mark.asyncio
async def test_ops_rename(write_ws, dbx_files):
await write_ws.ops.rename("/dbx/src.txt", "/dbx/renamed.txt")
assert dbx_files.downloads[f"{ROOT}/renamed.txt"] == b"data"
assert f"{ROOT}/src.txt" not in dbx_files.downloads
@pytest.mark.asyncio
async def test_mv_onto_same_path_errors_and_preserves_file(
write_ws, dbx_files):
io = await write_ws.execute("mv /dbx/src.txt /dbx/src.txt")
assert io.exit_code != 0
assert b"are the same file" in io.stderr
assert dbx_files.downloads[f"{ROOT}/src.txt"] == b"data"
assert f"{ROOT}/src.txt" not in dbx_files.delete_calls
@pytest.mark.asyncio
async def test_mv_into_dir_where_file_already_lives_errors_and_preserves_file(
write_ws, dbx_files):
io = await write_ws.execute("mv /dbx/src.txt /dbx/")
assert io.exit_code != 0
assert b"are the same file" in io.stderr
assert dbx_files.downloads[f"{ROOT}/src.txt"] == b"data"
@pytest.mark.asyncio
async def test_ops_rename_onto_same_path_is_noop(write_ws, dbx_files):
await write_ws.ops.rename("/dbx/src.txt", "/dbx/src.txt")
assert dbx_files.downloads[f"{ROOT}/src.txt"] == b"data"
assert f"{ROOT}/src.txt" not in dbx_files.delete_calls
@pytest.mark.asyncio
async def test_mv_multiple_sources_require_directory(write_ws, dbx_files):
seed_file(dbx_files, f"{ROOT}/a.txt", b"AAA")
seed_file(dbx_files, f"{ROOT}/b.txt", b"BBB")
seed_file(dbx_files, f"{ROOT}/target.txt", b"target")
io = await write_ws.execute("mv /dbx/a.txt /dbx/b.txt /dbx/target.txt")
assert io.exit_code != 0
assert b"not a directory" in io.stderr
assert dbx_files.downloads[f"{ROOT}/a.txt"] == b"AAA"
assert dbx_files.downloads[f"{ROOT}/b.txt"] == b"BBB"
assert dbx_files.downloads[f"{ROOT}/target.txt"] == b"target"
assert f"{ROOT}/a.txt" not in dbx_files.delete_calls
assert f"{ROOT}/b.txt" not in dbx_files.delete_calls
@pytest.mark.asyncio
async def test_mv_missing_source_reports_cannot_stat(write_ws, dbx_files):
io = await write_ws.execute("mv /dbx/missing /dbx/missing")
assert io.exit_code != 0
assert b"cannot stat" in io.stderr
assert b"are the same file" not in io.stderr
@pytest.mark.asyncio
async def test_mv_into_itself_errors_and_preserves_source(write_ws, dbx_files):
seed_directory(dbx_files, f"{ROOT}/d")
seed_file(dbx_files, f"{ROOT}/d/a.txt", b"aaa")
io = await write_ws.execute("mv /dbx/d /dbx/d")
assert io.exit_code != 0
assert b"subdirectory of itself" in io.stderr
assert dbx_files.downloads[f"{ROOT}/d/a.txt"] == b"aaa"
assert f"{ROOT}/d" in dbx_files.directory_metadata
assert f"{ROOT}/d/a.txt" not in dbx_files.delete_calls
@@ -0,0 +1,33 @@
# ========= 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
@pytest.mark.asyncio
async def test_workspace_execute_databricks_volume_nl(
databricks_text_workspace):
io = await databricks_text_workspace.execute("nl /dbx/words.txt")
assert io.exit_code == 0
assert b" 1\tbeta\n" in io.stdout
@pytest.mark.asyncio
async def test_workspace_execute_databricks_volume_nl_resolves_glob(
databricks_text_workspace):
io = await databricks_text_workspace.execute("nl /dbx/*.txt")
assert io.exit_code == 0
assert b" 1\tdelta\n" in io.stdout
@@ -0,0 +1,41 @@
# ========= 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.builtin.databricks_volume import COMMANDS
TEXT_COMMANDS = {
"awk",
"cut",
"diff",
"jq",
"nl",
"sed",
"sort",
"tr",
"uniq",
"wc",
}
def test_databricks_volume_text_commands_registered_read_only():
registered = [
registered for command in COMMANDS
for registered in command._registered_commands
]
names = {command.name for command in registered}
assert TEXT_COMMANDS <= names
for command in registered:
if command.name in TEXT_COMMANDS:
assert command.resource == "databricks_volume"
assert not command.write
@@ -0,0 +1,61 @@
# ========= 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 tests.resource.databricks_volume.test_databricks_volume import (
FakeFiles, make_resource, seed_directory, seed_file)
ROOT = "/Volumes/main/default/agent_files/root"
@pytest.fixture
def dbx_files() -> FakeFiles:
files = FakeFiles()
seed_directory(files, ROOT)
seed_file(files, f"{ROOT}/words.txt", b"beta\nalpha\nalpha\n")
files.create_directory(f"{ROOT}/sub")
seed_file(files, f"{ROOT}/sub/inner.txt", b"gamma\nalpha\n")
return files
@pytest.fixture
def ws(dbx_files: FakeFiles) -> Workspace:
return Workspace({"/dbx/": make_resource(dbx_files)}, mode=MountMode.READ)
@pytest.mark.asyncio
async def test_rg_searches_path_recursively(ws):
io = await ws.execute("rg alpha /dbx/")
assert io.exit_code == 0
out = io.stdout.decode()
assert "words.txt" in out
assert "inner.txt" in out
@pytest.mark.asyncio
async def test_rg_count_only(ws):
io = await ws.execute("rg -c alpha /dbx/words.txt")
assert io.exit_code == 0
assert b"2" in io.stdout
@pytest.mark.asyncio
async def test_rg_no_match_exits_nonzero(ws):
io = await ws.execute("rg zeta /dbx/words.txt")
assert io.exit_code != 0
@@ -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 pytest
from mirage import MountMode, Workspace
from tests.resource.databricks_volume.test_databricks_volume import (
FakeFiles, make_resource, seed_directory, seed_file)
ROOT = "/Volumes/main/default/agent_files/root"
@pytest.fixture
def dbx_files() -> FakeFiles:
files = FakeFiles()
seed_directory(files, ROOT)
files.create_directory(f"{ROOT}/d")
seed_file(files, f"{ROOT}/d/a.txt", b"a")
files.create_directory(f"{ROOT}/d/sub")
seed_file(files, f"{ROOT}/d/sub/b.txt", b"b")
return files
@pytest.fixture
def write_ws(dbx_files: FakeFiles) -> Workspace:
return Workspace({"/dbx/": make_resource(dbx_files)}, mode=MountMode.WRITE)
@pytest.fixture
def read_ws(dbx_files: FakeFiles) -> Workspace:
return Workspace({"/dbx/": make_resource(dbx_files)}, mode=MountMode.READ)
@pytest.mark.asyncio
async def test_rm_recursive_removes_tree(write_ws, dbx_files):
io = await write_ws.execute("rm -r /dbx/d")
assert io.exit_code == 0
assert f"{ROOT}/d" not in dbx_files.directory_metadata
assert f"{ROOT}/d/sub/b.txt" not in dbx_files.downloads
@pytest.mark.asyncio
async def test_rm_recursive_writes_are_mount_relative(write_ws):
io = await write_ws.execute("rm -r /dbx/d")
assert io.exit_code == 0
assert io.writes
for key in io.writes:
assert key.startswith("/dbx/")
assert not key.startswith("/dbx/dbx/")
@pytest.mark.asyncio
async def test_plain_rm_on_directory_fails(write_ws, dbx_files):
io = await write_ws.execute("rm /dbx/d")
assert io.exit_code != 0
assert f"{ROOT}/d" in dbx_files.directory_metadata
@pytest.mark.asyncio
async def test_rm_force_missing_succeeds(write_ws):
io = await write_ws.execute("rm -f /dbx/missing.txt")
assert io.exit_code == 0
@pytest.mark.asyncio
async def test_rm_missing_fails(write_ws):
io = await write_ws.execute("rm /dbx/missing.txt")
assert io.exit_code != 0
@pytest.mark.asyncio
async def test_rm_recursive_read_only_rejected(read_ws, dbx_files):
io = await read_ws.execute("rm -r /dbx/d")
assert io.exit_code != 0
assert b"read-only" in io.stderr
assert f"{ROOT}/d" in dbx_files.directory_metadata
@@ -0,0 +1,60 @@
# ========= 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.builtin.databricks_volume import COMMANDS
def _sed_command():
for cmd in COMMANDS:
for rc in cmd._registered_commands:
if rc.name == "sed":
return cmd
raise LookupError("sed not registered for databricks_volume")
sed_command = _sed_command()
@pytest.mark.asyncio
async def test_workspace_execute_databricks_volume_sed(
databricks_text_workspace):
io = await databricks_text_workspace.execute(
"sed s/alpha/ALPHA/g /dbx/words.txt")
assert io.exit_code == 0
assert io.stdout == b"beta\nALPHA\nALPHA\n"
@pytest.mark.asyncio
async def test_workspace_execute_databricks_volume_sed_resolves_glob(
databricks_text_workspace):
io = await databricks_text_workspace.execute(
"sed s/delta/DELTA/ /dbx/*.txt")
assert io.exit_code == 0
assert b"DELTA\n" in io.stdout
@pytest.mark.asyncio
async def test_databricks_volume_sed_in_place_writes_back(
databricks_text_workspace):
# The volume has a write op, so -i edits in place through the shared
# builder (the old bespoke wrapper refused it unconditionally, #382).
io = await databricks_text_workspace.execute(
"sed -i s/alpha/ALPHA/g /dbx/words.txt")
assert io.exit_code == 0
assert io.writes.get("/dbx/words.txt") == b"beta\nALPHA\nALPHA\n"
@@ -0,0 +1,33 @@
# ========= 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
@pytest.mark.asyncio
async def test_workspace_execute_databricks_volume_sort(
databricks_text_workspace):
io = await databricks_text_workspace.execute("sort /dbx/words.txt")
assert io.exit_code == 0
assert io.stdout == b"alpha\nalpha\nbeta\n"
@pytest.mark.asyncio
async def test_workspace_execute_databricks_volume_sort_resolves_glob(
databricks_text_workspace):
io = await databricks_text_workspace.execute("sort /dbx/*.txt")
assert io.exit_code == 0
assert b"alpha\nalpha\nbeta\n" in io.stdout
@@ -0,0 +1,41 @@
# ========= 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
@pytest.mark.asyncio
async def test_stat_default_format(databricks_text_workspace):
io = await databricks_text_workspace.execute("stat /dbx/words.txt")
assert io.exit_code == 0
out = io.stdout.decode()
assert "name=words.txt" in out
assert "size=17" in out
@pytest.mark.asyncio
async def test_stat_custom_format(databricks_text_workspace):
io = await databricks_text_workspace.execute(
"stat -c '%n %s' /dbx/words.txt")
assert io.exit_code == 0
assert io.stdout.decode().strip() == "/dbx/words.txt 17"
@pytest.mark.asyncio
async def test_stat_missing_file_fails(databricks_text_workspace):
io = await databricks_text_workspace.execute("stat /dbx/missing.txt")
assert io.exit_code != 0
@@ -0,0 +1,25 @@
# ========= 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
@pytest.mark.asyncio
async def test_workspace_execute_databricks_volume_tr(
databricks_text_workspace):
io = await databricks_text_workspace.execute(
"cat /dbx/words.txt | tr a-z A-Z")
assert io.exit_code == 0
assert io.stdout == b"BETA\nALPHA\nALPHA\n"
@@ -0,0 +1,69 @@
# ========= 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 tests.resource.databricks_volume.test_databricks_volume import (
FakeFiles, make_resource, seed_directory, seed_file)
ROOT = "/Volumes/main/default/agent_files/root"
@pytest.fixture
def dbx_files() -> FakeFiles:
files = FakeFiles()
seed_directory(files, ROOT)
seed_file(files, f"{ROOT}/words.txt", b"beta\nalpha\nalpha\n")
files.create_directory(f"{ROOT}/sub")
seed_file(files, f"{ROOT}/sub/inner.txt", b"gamma\nalpha\n")
files.create_directory(f"{ROOT}/sub/deep")
seed_file(files, f"{ROOT}/sub/deep/leaf.txt", b"leaf\n")
return files
@pytest.fixture
def ws(dbx_files: FakeFiles) -> Workspace:
return Workspace({"/dbx/": make_resource(dbx_files)}, mode=MountMode.READ)
@pytest.mark.asyncio
async def test_tree_lists_nested_entries(ws):
io = await ws.execute("tree /dbx/")
assert io.exit_code == 0
out = io.stdout.decode()
assert "words.txt" in out
assert "inner.txt" in out
@pytest.mark.asyncio
async def test_tree_max_depth(ws):
io = await ws.execute("tree -L 1 /dbx/")
assert io.exit_code == 0
out = io.stdout.decode()
assert "sub" in out
assert "inner.txt" in out
assert "leaf.txt" not in out
@pytest.mark.asyncio
async def test_tree_dirs_only(ws):
io = await ws.execute("tree -d /dbx/")
assert io.exit_code == 0
out = io.stdout.decode()
assert "sub" in out
assert "words.txt" not in out
@@ -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 pytest
@pytest.mark.asyncio
async def test_workspace_execute_databricks_volume_uniq(
databricks_text_workspace):
io = await databricks_text_workspace.execute("uniq /dbx/words.txt")
assert io.exit_code == 0
assert io.stdout == b"beta\nalpha\n"
@@ -0,0 +1,33 @@
# ========= 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
@pytest.mark.asyncio
async def test_workspace_execute_databricks_volume_wc(
databricks_text_workspace):
io = await databricks_text_workspace.execute("wc -l /dbx/words.txt")
assert io.exit_code == 0
assert io.stdout == b"3 /dbx/words.txt\n"
@pytest.mark.asyncio
async def test_workspace_execute_databricks_volume_wc_resolves_glob(
databricks_text_workspace):
io = await databricks_text_workspace.execute("wc -l /dbx/*.txt")
assert io.exit_code == 0
assert b"/dbx/more.txt" in io.stdout
@@ -0,0 +1,60 @@
from types import SimpleNamespace
import pytest
from mirage.cache.index import RAMIndexCacheStore
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
def document(document_id: str, name: str, slug: str, size: int = 12) -> dict:
return {
"id": document_id,
"name": name,
"doc_metadata": [{
"name": "slug",
"value": slug
}],
"enabled": True,
"indexing_status": "completed",
"archived": False,
"tokens": 4,
"data_source_type": "upload_file",
"data_source_detail_dict": {
"upload_file": {
"size": size
}
},
"created_at": 1716282000,
}
@pytest.fixture
def dify_accessor() -> SimpleNamespace:
return SimpleNamespace(config=SimpleNamespace(slug_metadata_name="slug"))
@pytest.fixture
def dify_index() -> RAMIndexCacheStore:
return RAMIndexCacheStore()
@pytest.fixture
def knowledge_root() -> PathSpec:
return PathSpec(resource_path=mount_key("/knowledge", "/knowledge"),
virtual="/knowledge",
directory="/knowledge")
@pytest.fixture
def guide_path() -> PathSpec:
return PathSpec.from_str_path(
"/knowledge/guides/quickstart.md",
mount_key("/knowledge/guides/quickstart.md", "/knowledge"))
@pytest.fixture
def guides_path() -> PathSpec:
return PathSpec(resource_path=mount_key("/knowledge/guides", "/knowledge"),
virtual="/knowledge/guides",
directory="/knowledge/guides")
@@ -0,0 +1,76 @@
import pytest
from mirage.commands.builtin.dify.cat import make_cat
from mirage.commands.builtin.generic_bind import CommandIO, with_read_cache
from mirage.core.dify import read, tree
from mirage.core.dify.read import read_bytes as _read_bytes
from mirage.core.dify.read import read_stream as _read_stream
from mirage.core.dify.readdir import readdir as _readdir
from mirage.core.dify.stat import stat as _stat
from mirage.io.types import materialize
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
from .conftest import document
cat = make_cat(
with_read_cache(
CommandIO(
readdir=_readdir,
read_bytes=_read_bytes,
read_stream=_read_stream,
stat=_stat,
is_mounted=lambda a: True,
local=False,
)))
async def list_basic_documents(config):
return [
document("doc-1", "Guide", "guides/quickstart.md"),
document("doc-2", "Readme", "README.md"),
]
async def iter_basic_pages(config, document_id):
if document_id == "doc-1":
yield [{"content": "alpha\nbeta"}, {"content": "gamma"}]
else:
yield [{"content": "readme"}]
@pytest.mark.asyncio
async def test_cat_reads_stream_and_records_cache(monkeypatch, dify_accessor,
dify_index, guide_path):
monkeypatch.setattr(tree, "list_all_documents", list_basic_documents)
monkeypatch.setattr(read, "iter_segment_pages", iter_basic_pages)
stdout, io = await cat(dify_accessor, [guide_path], index=dify_index)
assert await materialize(stdout) == b"alpha\nbeta\ngamma"
assert guide_path.mount_path in io.reads
assert io.cache == [guide_path.mount_path]
async def get_two_doc_segments(config, document_id):
if document_id == "doc-1":
return [{"content": "alpha\nbeta"}, {"content": "gamma"}]
return [{"content": "readme"}]
@pytest.mark.asyncio
async def test_cat_multifile_caches_materialized_bytes_per_file(
monkeypatch, dify_accessor, dify_index, guide_path):
monkeypatch.setattr(tree, "list_all_documents", list_basic_documents)
monkeypatch.setattr(read, "get_document_segments", get_two_doc_segments)
readme_path = PathSpec.from_str_path(
"/knowledge/README.md", mount_key("/knowledge/README.md",
"/knowledge"))
stdout, io = await cat(dify_accessor, [guide_path, readme_path],
index=dify_index)
assert io.reads[guide_path.mount_path] == b"alpha\nbeta\ngamma"
assert io.reads[readme_path.mount_path] == b"readme"
assert all(isinstance(v, bytes) for v in io.reads.values())
assert await materialize(stdout) == b"alpha\nbeta\ngammareadme"
@@ -0,0 +1,98 @@
import pytest
from mirage.commands.builtin.dify.find import find
from mirage.core.dify import tree
from mirage.io.types import materialize
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
from .conftest import document
async def list_basic_documents(config):
return [
document("doc-1", "Guide", "guides/quickstart.md"),
document("doc-2", "Readme", "README.md"),
]
async def list_nested_documents(config):
return [
document("doc-1", "Guide", "guides/quickstart.md"),
document("doc-2", "Guide 2", "guides/deep/note.md"),
document("doc-3", "Readme", "README.md"),
]
@pytest.mark.asyncio
async def test_find_matches_name_pattern(monkeypatch, dify_accessor,
dify_index, knowledge_root):
monkeypatch.setattr(tree, "list_all_documents", list_basic_documents)
stdout, _ = await find(dify_accessor, [knowledge_root],
"quick*.md",
index=dify_index)
assert await materialize(stdout) == b"/knowledge/guides/quickstart.md\n"
@pytest.mark.asyncio
async def test_find_handles_file_missing_and_maxdepth(monkeypatch,
dify_accessor,
dify_index,
knowledge_root,
guide_path):
monkeypatch.setattr(tree, "list_all_documents", list_basic_documents)
file_stdout, file_io = await find(dify_accessor, [guide_path],
index=dify_index)
assert await materialize(file_stdout
) == b"/knowledge/guides/quickstart.md\n"
assert file_io.exit_code == 0
maxdepth_stdout, maxdepth_io = await find(dify_accessor, [knowledge_root],
maxdepth="0",
index=dify_index)
assert await materialize(maxdepth_stdout) == b"/knowledge\n"
assert maxdepth_io.exit_code == 0
missing = PathSpec.from_str_path(
"/knowledge/missing.md",
mount_key("/knowledge/missing.md", "/knowledge"))
missing_stdout, missing_io = await find(dify_accessor, [missing],
index=dify_index)
assert await materialize(missing_stdout) == b""
assert missing_io.stderr is not None
assert b"/knowledge/missing.md" in missing_io.stderr
assert missing_io.exit_code == 1
@pytest.mark.asyncio
async def test_find_uses_cwd_when_path_missing(monkeypatch, dify_accessor,
dify_index, guides_path):
monkeypatch.setattr(tree, "list_all_documents", list_nested_documents)
stdout, io = await find(dify_accessor, [],
"quick*.md",
cwd=guides_path,
index=dify_index)
assert await materialize(stdout) == b"/knowledge/guides/quickstart.md\n"
assert io.exit_code == 0
@pytest.mark.asyncio
async def test_find_resolves_glob_patterns(monkeypatch, dify_accessor,
dify_index):
monkeypatch.setattr(tree, "list_all_documents", list_nested_documents)
path = PathSpec(resource_path=mount_key("/knowledge/guides/*.md",
"/knowledge"),
virtual="/knowledge/guides/*.md",
directory="/knowledge/guides",
pattern="*.md",
resolved=False)
stdout, io = await find(dify_accessor, [path], index=dify_index)
assert await materialize(stdout) == b"/knowledge/guides/quickstart.md\n"
assert io.exit_code == 0
@@ -0,0 +1,108 @@
from types import SimpleNamespace
import pytest
from mirage.cache.index import RAMIndexCacheStore
from mirage.io.types import materialize
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
def document(document_id: str, name: str, slug: str | None = None) -> dict:
metadata = []
if slug is not None:
metadata = [{"name": "slug", "value": slug}]
return {
"id": document_id,
"name": name,
"doc_metadata": metadata,
"enabled": True,
"indexing_status": "completed",
"archived": False,
"tokens": 4,
"data_source_type": "upload_file",
"data_source_detail_dict": {
"upload_file": {
"size": 12
}
},
"created_at": 1716282000,
}
def accessor() -> SimpleNamespace:
return SimpleNamespace(config=SimpleNamespace(slug_metadata_name="slug"))
@pytest.mark.asyncio
async def test_search_command_resolves_globs_and_passes_multiple_documents(
monkeypatch):
from mirage.commands.builtin.dify import search as command_search
from mirage.core.dify import search, tree
calls: list[tuple[list[PathSpec], dict]] = []
async def list_documents(config):
return [
document("doc-1", "API", "guides/api.md"),
document("doc-2", "Auth", "guides/auth.md"),
]
async def search_segments(accessor, query, paths, index, **kwargs):
calls.append((paths, kwargs))
return b"api\nauth"
monkeypatch.setattr(tree, "list_all_documents", list_documents)
monkeypatch.setattr(search, "search_segments", search_segments)
stdout, io = await command_search(
accessor(),
[
PathSpec(resource_path=mount_key("/knowledge/guides/*.md",
"/knowledge"),
virtual="/knowledge/guides/*.md",
directory="/knowledge/guides",
pattern="*.md",
resolved=False)
],
"login",
index=RAMIndexCacheStore(),
)
assert await materialize(stdout) == b"api\nauth"
assert io.reads == {}
assert io.cache == []
assert [path.virtual for path in calls[0][0]] == [
"/knowledge/guides/api.md",
"/knowledge/guides/auth.md",
]
assert calls[0][1]["mount_prefix"] == "/knowledge"
@pytest.mark.asyncio
async def test_search_command_root_searches_whole_dataset(monkeypatch):
from mirage.commands.builtin.dify import search as command_search
from mirage.core.dify import search
calls: list[tuple[list[PathSpec], dict]] = []
async def search_segments(accessor, query, paths, index, **kwargs):
calls.append((paths, kwargs))
return b"dataset"
monkeypatch.setattr(search, "search_segments", search_segments)
root = PathSpec(resource_path=mount_key("/knowledge", "/knowledge"),
virtual="/knowledge",
directory="/knowledge")
stdout, _ = await command_search(accessor(), [root],
"anything",
index=RAMIndexCacheStore())
assert await materialize(stdout) == b"dataset"
assert calls == [([], {
"method": "semantic",
"top_k": 10,
"threshold": 0.0,
"mount_prefix": "/knowledge"
})]
@@ -0,0 +1,53 @@
import pytest
from mirage.commands.builtin.dify import COMMANDS
from mirage.core.dify import read, tree
from mirage.io.types import materialize
from .conftest import document
def _sed_command():
for cmd in COMMANDS:
for rc in cmd._registered_commands:
if rc.name == "sed":
return cmd
raise LookupError("sed not registered for dify")
sed = _sed_command()
async def list_documents(config):
return [document("doc-1", "Guide", "guides/quickstart.md")]
async def get_segments(config, document_id):
return [{"content": "alpha beta"}]
@pytest.mark.asyncio
async def test_sed_transforms_dify_document(monkeypatch, dify_accessor,
dify_index, guide_path):
monkeypatch.setattr(tree, "list_all_documents", list_documents)
monkeypatch.setattr(read, "get_document_segments", get_segments)
stdout, io = await sed(dify_accessor, [guide_path],
"s/alpha/gamma/",
index=dify_index)
assert await materialize(stdout) == b"gamma beta"
assert io.exit_code == 0
@pytest.mark.asyncio
async def test_sed_rejects_in_place(monkeypatch, dify_accessor, dify_index,
guide_path):
monkeypatch.setattr(tree, "list_all_documents", list_documents)
with pytest.raises(PermissionError,
match="-i not supported on this backend"):
await sed(dify_accessor, [guide_path],
"s/alpha/gamma/",
i=True,
index=dify_index)
@@ -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,76 @@
# ========= 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 unittest.mock import AsyncMock, patch
import pytest
from mirage.accessor.discord import DiscordAccessor
from mirage.cache.index.ram import RAMIndexCacheStore
from mirage.commands.builtin.discord import COMMANDS
from mirage.resource.discord.config import DiscordConfig
from mirage.types import PathSpec
GUILDS = [{"id": "G1", "name": "myguild"}]
GUILD_DIR = "/myguild__G1"
def _find_command():
for fn in COMMANDS:
for rc in getattr(fn, "_registered_commands", []):
if rc.name == "find" and rc.filetype is None:
return fn
raise AssertionError("factory find not registered for discord")
def _spec(virtual: str) -> PathSpec:
return PathSpec(virtual=virtual,
directory=virtual,
resource_path=virtual.strip("/"))
async def _run(paths, *texts: str, **flags) -> list[str]:
accessor = DiscordAccessor(DiscordConfig(token="t"))
find = _find_command()
with patch("mirage.core.discord.readdir.list_guilds",
new_callable=AsyncMock,
return_value=GUILDS):
stdout, _io = await find(accessor,
paths,
*texts,
index=RAMIndexCacheStore(),
**flags)
data = stdout if isinstance(stdout, bytes) else b""
return data.decode().splitlines()
@pytest.mark.asyncio
async def test_walk_lists_guild_tree():
lines = await _run([_spec("/")], maxdepth="2")
assert GUILD_DIR in lines
assert f"{GUILD_DIR}/channels" in lines
assert f"{GUILD_DIR}/members" in lines
@pytest.mark.asyncio
async def test_path_pattern_is_honored():
lines = await _run([_spec("/")], maxdepth="2", path="*channels*")
assert lines == [f"{GUILD_DIR}/channels"]
@pytest.mark.asyncio
async def test_size_is_honored_dirs_count_as_zero():
lines = await _run([_spec("/")], maxdepth="2", size="+0c")
assert lines == []
@@ -0,0 +1,252 @@
# ========= 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 unittest.mock import AsyncMock, patch
import pytest
from mirage.commands.builtin.discord.grep import grep
from mirage.commands.builtin.discord.rg import rg
from mirage.types import FileStat, FileType, PathSpec
from mirage.utils.key_prefix import mount_key
def _concrete_paths(n: int = 7):
paths = []
for d in range(1, n + 1):
original = (
f"/discord/myguild/channels/general/2026-01-{d:02d}/chat.jsonl")
paths.append(
PathSpec(
resource_path=mount_key(original, "/discord"),
virtual=original,
directory=original,
))
return paths
def _fake_index(channel_id: str = "ch_456", guild_id: str = "g_123"):
idx = AsyncMock()
async def _get(virtual_key):
result = AsyncMock()
if virtual_key.endswith("/myguild/channels/general"):
result.entry = type("E", (), {"id": channel_id})
elif virtual_key.endswith("/myguild"):
result.entry = type("E", (), {"id": guild_id})
else:
result.entry = None
return result
idx.get.side_effect = _get
return idx
@pytest.mark.asyncio
async def test_discord_grep_with_many_concrete_paths_uses_native_search():
accessor = AsyncMock()
accessor.config = AsyncMock()
fake_msgs = [{
"content": "hello world",
"channel_id": "ch_456",
"author": {
"username": "alice"
},
"timestamp": "2026-01-15T12:34:56.000000+00:00",
"id": "1"
}]
fake_channels = [{"id": "ch_456", "name": "general"}]
with patch(
"mirage.commands.builtin.discord.grep.search_guild",
new=AsyncMock(return_value=fake_msgs),
) as fake_search, patch(
"mirage.commands.builtin.discord.grep.list_channels",
new=AsyncMock(return_value=fake_channels),
):
out, io = await grep(accessor,
_concrete_paths(7),
"hello",
index=_fake_index())
assert fake_search.await_count == 1
assert io.exit_code == 0
assert b"hello" in out
assert out.endswith(b"\n")
assert (b"/discord/myguild/channels/general__ch_456/"
b"2026-01-15/chat.jsonl:") in out
@pytest.mark.asyncio
async def test_discord_grep_falls_back_when_native_raises():
accessor = AsyncMock()
accessor.config = AsyncMock()
paths = [
PathSpec(resource_path=mount_key(
"/discord/myguild/channels/general/*.jsonl", "/discord"),
virtual="/discord/myguild/channels/general/*.jsonl",
directory="/discord/myguild/channels/general/",
pattern="*.jsonl"),
]
with patch(
"mirage.commands.builtin.discord.grep.search_guild",
new=AsyncMock(side_effect=RuntimeError("rate limited")),
), patch(
"mirage.commands.builtin.discord.grep.resolve_glob",
new=AsyncMock(return_value=paths),
) as fake_resolve, patch(
"mirage.commands.builtin.discord.grep.discord_read",
new=AsyncMock(return_value=b""),
), patch(
"mirage.commands.builtin.discord.grep._stat",
new=AsyncMock(return_value=FileStat(name="2026-04-10.jsonl",
type=FileType.TEXT)),
):
out, io = await grep(accessor, paths, "hello", index=_fake_index())
assert fake_resolve.await_count == 1
assert io.exit_code in (0, 1)
@pytest.mark.asyncio
async def test_discord_grep_native_empty_does_not_trigger_fallback():
"""search_guild returning [] is a legit no-match — don't double-scan."""
accessor = AsyncMock()
accessor.config = AsyncMock()
with patch(
"mirage.commands.builtin.discord.grep.search_guild",
new=AsyncMock(return_value=[]),
) as fake_search, patch(
"mirage.commands.builtin.discord.grep.list_channels",
new=AsyncMock(return_value=[]),
), patch(
"mirage.commands.builtin.discord.grep.discord_read",
new=AsyncMock(return_value=b""),
) as fake_read:
out, io = await grep(accessor,
_concrete_paths(7),
"missing",
index=_fake_index())
assert fake_search.await_count == 1
assert fake_read.await_count == 0
assert io.exit_code == 1
assert out == b""
@pytest.mark.asyncio
async def test_discord_grep_multi_pattern_skips_native_search():
"""grep -e a -e b must bypass the native search push-down.
The push-down passes a single newline-joined pattern to the native
search, which treats it as one literal and matches nothing. Multiple
-e patterns must fall through to the generic grep instead.
"""
accessor = AsyncMock()
accessor.config = AsyncMock()
paths = [
PathSpec(resource_path=mount_key(
"/discord/myguild/channels/general/*.jsonl", "/discord"),
virtual="/discord/myguild/channels/general/*.jsonl",
directory="/discord/myguild/channels/general/",
pattern="*.jsonl"),
]
with patch(
"mirage.commands.builtin.discord.grep.search_guild",
new=AsyncMock(return_value=[]),
) as fake_search, patch(
"mirage.commands.builtin.discord.grep.resolve_glob",
new=AsyncMock(return_value=paths),
) as fake_resolve, patch(
"mirage.commands.builtin.discord.grep.discord_read",
new=AsyncMock(return_value=b""),
), patch(
"mirage.commands.builtin.discord.grep._stat",
new=AsyncMock(return_value=FileStat(name="2026-04-10.jsonl",
type=FileType.TEXT)),
):
_, io = await grep(accessor,
paths,
e=["ada", "ben"],
index=_fake_index())
assert fake_search.await_count == 0
assert fake_resolve.await_count == 1
@pytest.mark.asyncio
async def test_discord_rg_with_many_concrete_paths_uses_native_search():
accessor = AsyncMock()
accessor.config = AsyncMock()
fake_msgs = [{
"content": "hello rg",
"channel_id": "ch_456",
"author": {
"username": "bob"
},
"timestamp": "2026-01-15T08:00:00.000000+00:00",
"id": "2"
}]
fake_channels = [{"id": "ch_456", "name": "general"}]
with patch(
"mirage.commands.builtin.discord.rg.search_guild",
new=AsyncMock(return_value=fake_msgs),
) as fake_search, patch(
"mirage.commands.builtin.discord.rg.list_channels",
new=AsyncMock(return_value=fake_channels),
):
out, io = await rg(accessor,
_concrete_paths(7),
"hello",
index=_fake_index())
assert fake_search.await_count == 1
assert io.exit_code == 0
assert b"hello" in out
assert out.endswith(b"\n")
assert (b"/discord/myguild/channels/general__ch_456/"
b"2026-01-15/chat.jsonl:") in out
@pytest.mark.asyncio
async def test_discord_rg_multi_pattern_skips_native_search():
"""rg -e a -e b must bypass the native search push-down.
Like grep, the push-down passes a single newline-joined pattern to the
native search, which matches nothing. Multiple -e patterns must fall
through to the generic rg instead.
"""
accessor = AsyncMock()
accessor.config = AsyncMock()
paths = [
PathSpec(resource_path=mount_key(
"/discord/myguild/channels/general/*.jsonl", "/discord"),
virtual="/discord/myguild/channels/general/*.jsonl",
directory="/discord/myguild/channels/general/",
pattern="*.jsonl"),
]
with patch(
"mirage.commands.builtin.discord.rg.search_guild",
new=AsyncMock(return_value=[]),
) as fake_search, patch(
"mirage.commands.builtin.discord.rg.resolve_glob",
new=AsyncMock(return_value=paths),
) as fake_resolve, patch(
"mirage.commands.builtin.discord.rg.discord_read",
new=AsyncMock(return_value=b""),
), patch(
"mirage.commands.builtin.discord.rg._stat",
new=AsyncMock(return_value=FileStat(name="2026-04-10.jsonl",
type=FileType.TEXT)),
):
_, io = await rg(accessor,
paths,
e=["ada", "ben"],
index=_fake_index())
assert fake_search.await_count == 0
assert fake_resolve.await_count == 1
@@ -0,0 +1,232 @@
# ========= 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, patch
import pytest
from mirage.accessor.discord import DiscordAccessor
from mirage.cache.index import IndexEntry
from mirage.cache.index.ram import RAMIndexCacheStore
from mirage.commands.builtin.discord import COMMANDS
from mirage.commands.builtin.discord.grep import grep
from mirage.commands.builtin.discord.head import head
from mirage.resource.discord.config import DiscordConfig
from mirage.types import PathSpec
GUILD_PATH = "TestGuild"
CHANNEL_PATH = "TestGuild/channels/general"
DATE_DIR_PATH = "TestGuild/channels/general/2024-01-15"
FILE_PATH = "TestGuild/channels/general/2024-01-15/chat.jsonl"
ABS_FILE = "/" + FILE_PATH
ABS_CHANNEL = "/" + CHANNEL_PATH
ABS_DATE_DIR = "/" + DATE_DIR_PATH
def _glob_result(path: str) -> list[PathSpec]:
return [
PathSpec(resource_path=(path).strip("/"),
virtual=path,
directory="/",
resolved=True)
]
FAKE_JSONL = (
b'{"id":"1","content":"hello world","author":{"username":"alice"}}\n'
b'{"id":"2","content":"goodbye moon","author":{"username":"bob"}}\n'
b'{"id":"3","content":"hello again","author":{"username":"alice"}}\n')
def _make_glob(path: str, resolved: bool = True) -> list[PathSpec]:
return [
PathSpec(resource_path=(path).strip("/"),
virtual=path,
directory="/",
resolved=resolved)
]
def _run(coro):
return asyncio.run(coro)
def _find_command():
for fn in COMMANDS:
for rc in getattr(fn, "_registered_commands", []):
if rc.name == "find" and rc.filetype is None:
return fn
raise AssertionError("factory find not registered for discord")
def _make_index() -> RAMIndexCacheStore:
index = RAMIndexCacheStore(ttl=600)
_run(
index.put(
"/" + GUILD_PATH,
IndexEntry(id="G1",
name="TestGuild",
resource_type="discord/guild",
vfs_name="TestGuild")))
_run(
index.put(
"/" + CHANNEL_PATH,
IndexEntry(id="C1",
name="general",
resource_type="discord/channel",
vfs_name="general")))
_run(
index.put(
"/" + DATE_DIR_PATH,
IndexEntry(id="C1:2024-01-15",
name="2024-01-15",
resource_type="discord/history",
vfs_name="2024-01-15")))
_run(
index.set_dir("/" + CHANNEL_PATH, [
("2024-01-15",
IndexEntry(id="C1:2024-01-15",
name="2024-01-15",
resource_type="discord/history",
vfs_name="2024-01-15")),
]))
_run(
index.put(
"/" + FILE_PATH,
IndexEntry(id="C1:2024-01-15:chat",
name="chat.jsonl",
resource_type="discord/chat_jsonl",
vfs_name="chat.jsonl")))
_run(
index.set_dir("/" + DATE_DIR_PATH, [
("chat.jsonl",
IndexEntry(id="C1:2024-01-15:chat",
name="chat.jsonl",
resource_type="discord/chat_jsonl",
vfs_name="chat.jsonl")),
]))
return index
@pytest.fixture
def accessor():
config = DiscordConfig(token="test-token")
return DiscordAccessor(config=config)
@pytest.fixture
def index():
return _make_index()
async def _collect(stream) -> bytes:
if isinstance(stream, bytes):
return stream
chunks = []
async for chunk in stream:
chunks.append(chunk)
return b"".join(chunks)
@pytest.mark.asyncio
async def test_head(accessor):
with (
patch("mirage.commands.builtin.discord.head.resolve_glob",
new_callable=AsyncMock,
return_value=_glob_result(ABS_FILE)),
patch("mirage.commands.builtin.discord.head.discord_read",
new_callable=AsyncMock,
return_value=FAKE_JSONL),
):
stream, io = await head(accessor, _make_glob(ABS_FILE), n="1")
data = await _collect(stream)
assert b"hello world" in data
assert b"goodbye moon" not in data
@pytest.mark.asyncio
async def test_head_default(accessor):
with (
patch("mirage.commands.builtin.discord.head.resolve_glob",
new_callable=AsyncMock,
return_value=_glob_result(ABS_FILE)),
patch("mirage.commands.builtin.discord.head.discord_read",
new_callable=AsyncMock,
return_value=FAKE_JSONL),
):
stream, io = await head(accessor, _make_glob(ABS_FILE))
data = await _collect(stream)
assert b"hello world" in data
assert b"goodbye moon" in data
assert b"hello again" in data
@pytest.mark.asyncio
async def test_grep(accessor):
with (
patch("mirage.commands.builtin.discord.grep.resolve_glob",
new_callable=AsyncMock,
return_value=_glob_result(ABS_FILE)),
patch("mirage.commands.builtin.discord.grep.discord_read",
new_callable=AsyncMock,
return_value=FAKE_JSONL),
):
stream, io = await grep(accessor, _make_glob(ABS_FILE), "hello")
data = await _collect(stream)
assert b"hello world" in data
assert b"hello again" in data
assert b"goodbye" not in data
@pytest.mark.asyncio
async def test_grep_invert(accessor):
with (
patch("mirage.commands.builtin.discord.grep.resolve_glob",
new_callable=AsyncMock,
return_value=_glob_result(ABS_FILE)),
patch("mirage.commands.builtin.discord.grep.discord_read",
new_callable=AsyncMock,
return_value=FAKE_JSONL),
):
stream, io = await grep(accessor,
_make_glob(ABS_FILE),
"hello",
v=True)
data = await _collect(stream)
assert b"goodbye moon" in data
assert b"hello world" not in data
@pytest.mark.asyncio
async def test_find(accessor, index):
find = _find_command()
stream, io = await find(accessor,
_make_glob(ABS_CHANNEL, resolved=False),
index=index)
data = await _collect(stream)
assert b"2024-01-15" in data
@pytest.mark.asyncio
async def test_find_with_name(accessor, index):
find = _find_command()
stream, io = await find(
accessor,
_make_glob(ABS_CHANNEL, resolved=False),
name="chat.jsonl",
index=index,
)
data = await _collect(stream)
assert b"chat.jsonl" in data
@@ -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. =========
from unittest.mock import AsyncMock, patch
import pytest
from mirage.commands.builtin.discord.grep import grep
from mirage.commands.builtin.discord.rg import rg
from mirage.io.types import IOResult
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
def _path(path: str) -> PathSpec:
return PathSpec(resource_path=mount_key(path, "/discord"),
virtual=path,
directory=path)
def _fake_index():
idx = AsyncMock()
async def _get(virtual_key):
result = AsyncMock()
if virtual_key.endswith("/myguild/channels/general"):
result.entry = type("E", (), {"id": "C1", "remote_time": ""})
elif virtual_key.endswith("/myguild"):
result.entry = type("E", (), {"id": "G1"})
else:
result.entry = None
return result
idx.get.side_effect = _get
return idx
@pytest.mark.asyncio
async def test_grep_emits_token_hint_on_forbidden():
accessor = AsyncMock()
accessor.config = AsyncMock()
paths = [_path("/discord/myguild/channels/general/2026-01-01/chat.jsonl")]
with patch(
"mirage.commands.builtin.discord.grep.search_guild",
new=AsyncMock(side_effect=RuntimeError("403 Forbidden")),
), patch(
"mirage.commands.builtin.discord.grep.resolve_glob",
new=AsyncMock(return_value=paths),
), patch(
"mirage.commands.builtin.discord.grep.discord_read",
new=AsyncMock(return_value=b""),
):
_out, io = await grep(accessor,
paths,
"hi",
index=_fake_index(),
args_l=True)
stderr = (io.stderr or b"").decode()
assert "push-down failed" in stderr
assert "READ_MESSAGE_HISTORY" in stderr
@pytest.mark.asyncio
async def test_rg_emits_warning_on_rate_limit():
accessor = AsyncMock()
accessor.config = AsyncMock()
paths = [_path("/discord/myguild/channels/general/2026-01-01/chat.jsonl")]
with patch(
"mirage.commands.builtin.discord.rg.search_guild",
new=AsyncMock(side_effect=RuntimeError("rate limited 429")),
), patch(
"mirage.commands.builtin.discord.rg.resolve_glob",
new=AsyncMock(return_value=paths),
), patch(
"mirage.commands.builtin.discord.rg.generic_rg",
new=AsyncMock(return_value=(b"", IOResult(exit_code=1))),
):
_out, io = await rg(accessor, paths, "hi", index=_fake_index())
stderr = (io.stderr or b"").decode()
assert "push-down failed" in stderr
# 429 doesn't trigger the perm hint; should still warn
assert "READ_MESSAGE_HISTORY" not in stderr
@@ -0,0 +1,69 @@
# ========= 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
from unittest.mock import AsyncMock, patch
import pytest
from mirage.accessor.discord import DiscordAccessor
from mirage.commands.builtin.discord.discord_add_reaction import \
discord_add_reaction
from mirage.commands.builtin.discord.discord_send_message import \
discord_send_message
from mirage.resource.discord.config import DiscordConfig
@pytest.fixture
def accessor():
return DiscordAccessor(config=DiscordConfig(token="test-bot-token"), )
@pytest.mark.asyncio
async def test_send_message(accessor):
with patch(
"mirage.commands.builtin.discord.discord_send_message"
".send_message",
new_callable=AsyncMock,
return_value={
"id": "msg1",
"content": "hello"
},
) as mock_send:
stream, io_result = await discord_send_message(accessor, [],
channel_id="C001",
text="hello")
mock_send.assert_called_once_with(accessor.config, "C001", "hello", None)
out = json.loads(stream)
assert out["id"] == "msg1"
@pytest.mark.asyncio
async def test_add_reaction(accessor):
with patch(
"mirage.commands.builtin.discord.discord_add_reaction"
".add_reaction",
new_callable=AsyncMock,
return_value=None,
) as mock_react:
stream, io_result = await discord_add_reaction(accessor, [],
channel_id="C001",
message_id="msg1",
reaction="thumbsup")
mock_react.assert_called_once_with(accessor.config, "C001", "msg1",
"thumbsup")
out = json.loads(stream)
assert out["ok"] is True
@@ -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 pytest
from mirage import DiskResource, MountMode, Workspace
@pytest.fixture
def workspace(tmp_path):
return Workspace({"/": DiskResource(root=str(tmp_path))},
mode=MountMode.WRITE)
@pytest.mark.asyncio
async def test_cat_basic(workspace):
await workspace.ops.write("/f.txt", b"hello\nworld\n")
io = await workspace.execute("cat /f.txt", session_id="default")
assert io.exit_code == 0
assert io.stdout == b"hello\nworld\n"
@pytest.mark.asyncio
async def test_cat_n_single_digit_alignment(workspace):
await workspace.ops.write("/f.txt", b"a\nb\n")
io = await workspace.execute("cat -n /f.txt", session_id="default")
assert io.exit_code == 0
assert io.stdout == b" 1\ta\n 2\tb\n"
@pytest.mark.asyncio
async def test_cat_n_multidigit_alignment(workspace):
body = b"".join(f"line{i}\n".encode() for i in range(1, 13))
await workspace.ops.write("/big.txt", body)
io = await workspace.execute("cat -n /big.txt", session_id="default")
assert io.exit_code == 0
lines = io.stdout.split(b"\n")
assert lines[0] == b" 1\tline1"
assert lines[8] == b" 9\tline9"
assert lines[9] == b" 10\tline10"
assert lines[11] == b" 12\tline12"
@pytest.mark.asyncio
async def test_cat_preserves_no_trailing_newline(workspace):
await workspace.ops.write("/partial.txt", b"hello")
io = await workspace.execute("cat /partial.txt", session_id="default")
assert io.exit_code == 0
assert io.stdout == b"hello"
@pytest.mark.asyncio
async def test_cat_n_preserves_no_trailing_newline(workspace):
await workspace.ops.write("/partial.txt", b"hello")
io = await workspace.execute("cat -n /partial.txt", session_id="default")
assert io.exit_code == 0
assert io.stdout == b" 1\thello"
@pytest.mark.asyncio
async def test_cat_empty_file(workspace):
await workspace.ops.write("/empty.txt", b"")
io = await workspace.execute("cat /empty.txt", session_id="default")
assert io.exit_code == 0
assert io.stdout == b""
@pytest.mark.asyncio
async def test_cat_only_newlines(workspace):
await workspace.ops.write("/nl.txt", b"\n\n\n")
io = await workspace.execute("cat -n /nl.txt", session_id="default")
assert io.exit_code == 0
assert io.stdout == b" 1\t\n 2\t\n 3\t\n"
@@ -0,0 +1,91 @@
# ========= 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 DiskResource, MountMode, Workspace
@pytest.fixture
def workspace(tmp_path):
return Workspace({"/": DiskResource(root=str(tmp_path))},
mode=MountMode.WRITE)
@pytest.mark.asyncio
async def test_du_single_file(workspace):
await workspace.ops.write("/f.txt", b"hello")
io = await workspace.execute("du /f.txt", session_id="default")
assert io.exit_code == 0
assert io.stdout.decode().strip() == "5\t/f.txt"
@pytest.mark.asyncio
async def test_du_directory_collapses(workspace):
await workspace.ops.mkdir("/dir")
await workspace.ops.write("/dir/a.txt", b"aaa")
await workspace.ops.write("/dir/b.txt", b"bb")
io = await workspace.execute("du /dir", session_id="default")
assert io.exit_code == 0
assert io.stdout.decode().strip() == "5\t/dir"
@pytest.mark.asyncio
async def test_du_a_lists_files(workspace):
await workspace.ops.mkdir("/dir")
await workspace.ops.write("/dir/a.txt", b"aaa")
await workspace.ops.write("/dir/b.txt", b"bb")
io = await workspace.execute("du -a /dir", session_id="default")
assert io.exit_code == 0
out = io.stdout.decode()
assert "a.txt" in out
assert "b.txt" in out
@pytest.mark.asyncio
async def test_du_s_summary(workspace):
await workspace.ops.mkdir("/dir")
await workspace.ops.mkdir("/dir/sub")
await workspace.ops.write("/dir/a.txt", b"hello")
await workspace.ops.write("/dir/sub/b.txt", b"world")
io = await workspace.execute("du -s /dir", session_id="default")
assert io.exit_code == 0
lines = io.stdout.decode().strip().splitlines()
assert len(lines) == 1
@pytest.mark.asyncio
async def test_du_c_total(workspace):
await workspace.ops.write("/a.txt", b"hello")
await workspace.ops.write("/b.txt", b"world")
io = await workspace.execute("du -c /a.txt /b.txt", session_id="default")
assert io.exit_code == 0
lines = io.stdout.decode().strip().splitlines()
assert lines[-1] == "10\ttotal"
@pytest.mark.asyncio
async def test_du_h_human(workspace):
await workspace.ops.write("/big.txt", b"x" * 2048)
io = await workspace.execute("du -h /big.txt", session_id="default")
assert io.exit_code == 0
size_str = io.stdout.decode().strip().split("\t")[0]
assert size_str.endswith("K")
@pytest.mark.asyncio
async def test_du_missing_operand_errors(workspace):
io = await workspace.execute("du", session_id="default")
assert io.exit_code != 0
assert b"missing operand" in (io.stderr or b"")
@@ -0,0 +1,83 @@
# ========= 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 DiskResource, MountMode, Workspace
@pytest.fixture
def workspace(tmp_path):
return Workspace({"/": DiskResource(root=str(tmp_path))},
mode=MountMode.WRITE)
@pytest.mark.asyncio
async def test_find_name_glob(workspace):
await workspace.ops.write("/hello.txt", b"hi")
await workspace.ops.write("/world.py", b"hi")
io = await workspace.execute("find / -name '*.txt'", session_id="default")
assert io.exit_code == 0
out = io.stdout.decode()
assert "hello.txt" in out
assert "world.py" not in out
@pytest.mark.asyncio
async def test_find_type_f(workspace):
await workspace.ops.mkdir("/sub")
await workspace.ops.write("/a.txt", b"a")
await workspace.ops.write("/sub/b.txt", b"b")
io = await workspace.execute("find / -type f", session_id="default")
assert io.exit_code == 0
out = io.stdout.decode()
assert "/a.txt" in out
assert "/sub/b.txt" in out
@pytest.mark.asyncio
async def test_find_type_d(workspace):
await workspace.ops.mkdir("/sub")
await workspace.ops.write("/a.txt", b"a")
io = await workspace.execute("find / -type d", session_id="default")
assert io.exit_code == 0
out = io.stdout.decode()
assert "/sub" in out
assert "/a.txt" not in out
@pytest.mark.asyncio
async def test_find_size_lower_bound(workspace):
await workspace.ops.write("/big.txt", b"x" * 1000)
await workspace.ops.write("/small.txt", b"x")
io = await workspace.execute("find / -size +500c -type f",
session_id="default")
assert io.exit_code == 0
out = io.stdout.decode()
assert "big.txt" in out
assert "small.txt" not in out
@pytest.mark.asyncio
async def test_find_maxdepth(workspace):
await workspace.ops.mkdir("/sub")
await workspace.ops.mkdir("/sub/deep")
await workspace.ops.write("/a.txt", b"a")
await workspace.ops.write("/sub/deep/c.txt", b"c")
io = await workspace.execute("find / -maxdepth 1 -type f",
session_id="default")
assert io.exit_code == 0
out = io.stdout.decode()
assert "/a.txt" in out
assert "/sub/deep/c.txt" not in out
@@ -0,0 +1,75 @@
# ========= 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 DiskResource, MountMode, Workspace
@pytest.fixture
def workspace(tmp_path):
return Workspace({"/": DiskResource(root=str(tmp_path))},
mode=MountMode.WRITE)
@pytest.mark.asyncio
async def test_head_default_n_10(workspace):
body = b"".join(f"line{i}\n".encode() for i in range(1, 15))
await workspace.ops.write("/f.txt", body)
io = await workspace.execute("head /f.txt", session_id="default")
assert io.exit_code == 0
lines = io.stdout.decode().splitlines()
assert len(lines) == 10
assert lines[0] == "line1"
assert lines[9] == "line10"
@pytest.mark.asyncio
async def test_head_n_explicit(workspace):
await workspace.ops.write("/f.txt", b"a\nb\nc\nd\n")
io = await workspace.execute("head -n 2 /f.txt", session_id="default")
assert io.exit_code == 0
assert io.stdout == b"a\nb\n"
@pytest.mark.asyncio
async def test_head_c_bytes(workspace):
await workspace.ops.write("/f.txt", b"hello world")
io = await workspace.execute("head -c 5 /f.txt", session_id="default")
assert io.exit_code == 0
assert io.stdout == b"hello"
@pytest.mark.asyncio
async def test_head_negative_n_excludes_last(workspace):
await workspace.ops.write("/f.txt", b"a\nb\nc\nd\n")
io = await workspace.execute("head -n -1 /f.txt", session_id="default")
assert io.exit_code == 0
assert io.stdout == b"a\nb\nc\n"
@pytest.mark.asyncio
async def test_head_no_trailing_newline(workspace):
await workspace.ops.write("/partial.txt", b"hello")
io = await workspace.execute("head /partial.txt", session_id="default")
assert io.exit_code == 0
assert io.stdout == b"hello"
@pytest.mark.asyncio
async def test_head_empty_file(workspace):
await workspace.ops.write("/empty.txt", b"")
io = await workspace.execute("head /empty.txt", session_id="default")
assert io.exit_code == 0
assert io.stdout == b""
@@ -0,0 +1,90 @@
# ========= 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 DiskResource, MountMode, Workspace
@pytest.fixture
def workspace(tmp_path):
return Workspace({"/": DiskResource(root=str(tmp_path))},
mode=MountMode.WRITE)
@pytest.mark.asyncio
async def test_ls_lists_files(workspace):
await workspace.ops.write("/a.txt", b"a")
await workspace.ops.write("/b.txt", b"b")
io = await workspace.execute("ls /", session_id="default")
assert io.exit_code == 0
names = set(io.stdout.decode().strip().split("\n"))
assert "a.txt" in names
assert "b.txt" in names
@pytest.mark.asyncio
async def test_ls_a_shows_dotfiles(workspace):
await workspace.ops.write("/.hidden", b"h")
await workspace.ops.write("/visible.txt", b"v")
io = await workspace.execute("ls -a /", session_id="default")
assert io.exit_code == 0
names = set(io.stdout.decode().strip().split("\n"))
assert ".hidden" in names
assert "visible.txt" in names
@pytest.mark.asyncio
async def test_ls_l_long_format_includes_size(workspace):
await workspace.ops.write("/f.txt", b"hello")
io = await workspace.execute("ls -l /", session_id="default")
assert io.exit_code == 0
out = io.stdout.decode()
assert "f.txt" in out
assert "5" in out
@pytest.mark.asyncio
async def test_ls_F_classify_marks_dirs(workspace):
await workspace.ops.mkdir("/sub")
await workspace.ops.write("/sub/a.txt", b"a")
io = await workspace.execute("ls -F /", session_id="default")
assert io.exit_code == 0
assert "sub/" in io.stdout.decode()
@pytest.mark.asyncio
async def test_ls_R_recursive(workspace):
await workspace.ops.mkdir("/sub")
await workspace.ops.write("/sub/a.txt", b"a")
io = await workspace.execute("ls -R /", session_id="default")
assert io.exit_code == 0
out = io.stdout.decode()
assert "sub" in out
assert "a.txt" in out
@pytest.mark.asyncio
async def test_ls_d_lists_dir_itself(workspace):
await workspace.ops.mkdir("/sub")
io = await workspace.execute("ls -d /sub", session_id="default")
assert io.exit_code == 0
assert "sub" in io.stdout.decode()
@pytest.mark.asyncio
async def test_ls_missing_path_returns_exit_1(workspace):
io = await workspace.execute("ls /nope", session_id="default")
assert io.exit_code == 1
assert b"nope" in (io.stderr or b"")
@@ -0,0 +1,33 @@
# ========= 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 DiskResource, MountMode, Workspace
@pytest.fixture
def workspace(tmp_path):
return Workspace({"/": DiskResource(root=str(tmp_path))},
mode=MountMode.WRITE)
@pytest.mark.asyncio
async def test_rm_v_terminates_verbose_output(workspace):
await workspace.ops.write("/a.txt", b"a")
io = await workspace.execute("rm -v /a.txt", session_id="default")
assert io.exit_code == 0
assert io.stdout == b"removed '/a.txt'\n"
@@ -0,0 +1,83 @@
# ========= 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 DiskResource, MountMode, Workspace
@pytest.fixture
def workspace(tmp_path):
return Workspace({"/": DiskResource(root=str(tmp_path))},
mode=MountMode.WRITE)
@pytest.mark.asyncio
async def test_tail_default_n_10(workspace):
body = b"\n".join(f"line{i}".encode() for i in range(1, 21)) + b"\n"
await workspace.ops.write("/f.txt", body)
io = await workspace.execute("tail /f.txt", session_id="default")
assert io.exit_code == 0
expected = b"\n".join(f"line{i}".encode() for i in range(11, 21)) + b"\n"
assert io.stdout == expected
@pytest.mark.asyncio
async def test_tail_n_explicit(workspace):
await workspace.ops.write("/f.txt", b"a\nb\nc\nd\ne\n")
io = await workspace.execute("tail -n 3 /f.txt", session_id="default")
assert io.exit_code == 0
assert io.stdout == b"c\nd\ne\n"
@pytest.mark.asyncio
async def test_tail_c_bytes(workspace):
await workspace.ops.write("/f.txt", b"hello world")
io = await workspace.execute("tail -c 5 /f.txt", session_id="default")
assert io.exit_code == 0
assert io.stdout == b"world"
@pytest.mark.asyncio
async def test_tail_plus_n_streams_from_line(workspace):
await workspace.ops.write("/f.txt", b"a\nb\nc\nd\ne\n")
io = await workspace.execute("tail -n +3 /f.txt", session_id="default")
assert io.exit_code == 0
assert io.stdout == b"c\nd\ne\n"
@pytest.mark.asyncio
async def test_tail_no_trailing_newline(workspace):
await workspace.ops.write("/partial.txt", b"hello")
io = await workspace.execute("tail /partial.txt", session_id="default")
assert io.exit_code == 0
assert io.stdout == b"hello"
@pytest.mark.asyncio
async def test_tail_empty_file(workspace):
await workspace.ops.write("/empty.txt", b"")
io = await workspace.execute("tail /empty.txt", session_id="default")
assert io.exit_code == 0
assert io.stdout == b""
@pytest.mark.asyncio
async def test_tail_multi_file_emits_headers(workspace):
await workspace.ops.write("/a.txt", b"x\ny\n")
await workspace.ops.write("/b.txt", b"z\n")
io = await workspace.execute("tail /a.txt /b.txt", session_id="default")
assert io.exit_code == 0
assert b"==> /a.txt <==" in io.stdout
assert b"==> /b.txt <==" in io.stdout
@@ -0,0 +1,61 @@
# ========= 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 DiskResource, MountMode, Workspace
@pytest.fixture
def workspace(tmp_path):
return Workspace({"/": DiskResource(root=str(tmp_path))},
mode=MountMode.WRITE)
@pytest.mark.asyncio
async def test_tree_basic(workspace):
await workspace.ops.mkdir("/d1")
await workspace.ops.write("/d1/a.txt", b"a")
await workspace.ops.write("/d1/b.txt", b"b")
io = await workspace.execute("tree /d1", session_id="default")
assert io.exit_code == 0
out = io.stdout.decode()
assert "a.txt" in out
assert "b.txt" in out
@pytest.mark.asyncio
async def test_tree_L_max_depth(workspace):
await workspace.ops.mkdir("/d1")
await workspace.ops.mkdir("/d1/sub")
await workspace.ops.mkdir("/d1/sub/deep")
await workspace.ops.write("/d1/sub/deep/file.txt", b"d")
io = await workspace.execute("tree -L 1 /d1", session_id="default")
assert io.exit_code == 0
out = io.stdout.decode()
assert "sub" in out
assert "deep" in out
assert "file.txt" not in out
@pytest.mark.asyncio
async def test_tree_d_dirs_only(workspace):
await workspace.ops.mkdir("/d1")
await workspace.ops.mkdir("/d1/sub")
await workspace.ops.write("/d1/file.txt", b"x")
io = await workspace.execute("tree -d /d1", session_id="default")
assert io.exit_code == 0
out = io.stdout.decode()
assert "sub" in out
assert "file.txt" not in out
@@ -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 pytest
from mirage import DiskResource, MountMode, Workspace
@pytest.fixture
def workspace(tmp_path):
return Workspace({"/": DiskResource(root=str(tmp_path))},
mode=MountMode.WRITE)
@pytest.mark.asyncio
async def test_wc_default(workspace):
await workspace.ops.write("/f.txt", b"hello world\nfoo bar\n")
io = await workspace.execute("wc /f.txt", session_id="default")
assert io.exit_code == 0
parts = io.stdout.decode().rstrip("\n").split()
assert parts[0] == "2"
assert parts[1] == "4"
assert parts[2] == "20"
assert parts[3] == "/f.txt"
@pytest.mark.asyncio
async def test_wc_l(workspace):
await workspace.ops.write("/f.txt", b"a\nb\nc\n")
io = await workspace.execute("wc -l /f.txt", session_id="default")
assert io.exit_code == 0
assert io.stdout.decode().split()[0] == "3"
@pytest.mark.asyncio
async def test_wc_w(workspace):
await workspace.ops.write("/f.txt", b"hello world\nfoo\n")
io = await workspace.execute("wc -w /f.txt", session_id="default")
assert io.exit_code == 0
assert io.stdout.decode().split()[0] == "3"
@pytest.mark.asyncio
async def test_wc_c(workspace):
await workspace.ops.write("/f.txt", b"hello\n")
io = await workspace.execute("wc -c /f.txt", session_id="default")
assert io.exit_code == 0
assert io.stdout.decode().split()[0] == "6"
@pytest.mark.asyncio
async def test_wc_m_multibyte(workspace):
await workspace.ops.write("/f.txt", "café".encode())
io = await workspace.execute("wc -m /f.txt", session_id="default")
assert io.exit_code == 0
assert io.stdout.decode().split()[0] == "4"
@pytest.mark.asyncio
async def test_wc_L(workspace):
await workspace.ops.write("/f.txt", b"short\na much longer line\nmed\n")
io = await workspace.execute("wc -L /f.txt", session_id="default")
assert io.exit_code == 0
assert io.stdout.decode().split()[0] == str(len("a much longer line"))
@pytest.mark.asyncio
async def test_wc_empty_file(workspace):
await workspace.ops.write("/f.txt", b"")
io = await workspace.execute("wc /f.txt", session_id="default")
assert io.exit_code == 0
parts = io.stdout.decode().split()
assert parts[:3] == ["0", "0", "0"]
@pytest.mark.asyncio
async def test_wc_multi_file_emits_total(workspace):
await workspace.ops.write("/a.txt", b"hello\n")
await workspace.ops.write("/b.txt", b"world\nfoo\n")
io = await workspace.execute("wc /a.txt /b.txt", session_id="default")
assert io.exit_code == 0
lines = io.stdout.decode().splitlines()
assert any(line.endswith("/a.txt") for line in lines)
assert any(line.endswith("/b.txt") for line in lines)
assert lines[-1].endswith("total")

Some files were not shown because too many files have changed in this diff Show More