chore: import upstream snapshot with attribution
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,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,113 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import boto3
|
||||
import pytest
|
||||
from moto import mock_aws
|
||||
|
||||
from mirage.resource.disk import DiskResource
|
||||
from mirage.resource.ram import RAMResource
|
||||
from mirage.resource.s3.s3 import S3Config, S3Resource
|
||||
from mirage.types import MountMode
|
||||
from mirage.workspace.mount import MountRegistry
|
||||
|
||||
|
||||
def _ram_write(p: RAMResource, path: str, data: bytes) -> None:
|
||||
"""Write file to RAMResource store directly (sync, for test setup)."""
|
||||
key = "/" + path.strip("/")
|
||||
parts = key.strip("/").split("/")
|
||||
for i in range(len(parts) - 1):
|
||||
p._store.dirs.add("/" + "/".join(parts[:i + 1]))
|
||||
p._store.files[key] = data
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ram_resource():
|
||||
"""A RAMResource with test data."""
|
||||
p = RAMResource()
|
||||
_ram_write(p, "/hello.txt", b"hello world\n")
|
||||
_ram_write(p, "/nums.txt", b"3\n1\n2\n")
|
||||
_ram_write(p, "/sub/nested.txt", b"nested\n")
|
||||
return p
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def empty_resource():
|
||||
"""An empty RAMResource."""
|
||||
return RAMResource()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def disk_resource(tmp_path):
|
||||
"""A DiskResource backed by a temporary directory."""
|
||||
data_dir = tmp_path / "disk_data"
|
||||
data_dir.mkdir()
|
||||
(data_dir / "readme.txt").write_bytes(b"disk file\n")
|
||||
sub = data_dir / "sub"
|
||||
sub.mkdir()
|
||||
(sub / "deep.txt").write_bytes(b"deep content\n")
|
||||
return DiskResource(root=str(data_dir))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def s3_resource():
|
||||
"""An S3Resource backed by moto mock."""
|
||||
with mock_aws():
|
||||
conn = boto3.client("s3", region_name="us-east-1")
|
||||
conn.create_bucket(Bucket="test-bucket")
|
||||
conn.put_object(Bucket="test-bucket",
|
||||
Key="data/report.csv",
|
||||
Body=b"col1,col2\n1,2\n")
|
||||
conn.put_object(Bucket="test-bucket",
|
||||
Key="data/summary.txt",
|
||||
Body=b"summary\n")
|
||||
|
||||
config = S3Config(
|
||||
bucket="test-bucket",
|
||||
region="us-east-1",
|
||||
endpoint_url=None,
|
||||
)
|
||||
yield S3Resource(config)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def registry(ram_resource):
|
||||
"""MountRegistry with /data/ mounted to RAMResource."""
|
||||
reg = MountRegistry()
|
||||
reg.mount("/data/", ram_resource, MountMode.WRITE)
|
||||
return reg
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def multi_registry(s3_resource, disk_resource, ram_resource):
|
||||
"""MountRegistry with S3, disk, and RAM mounts."""
|
||||
reg = MountRegistry()
|
||||
reg.mount("/s3/", s3_resource, MountMode.READ)
|
||||
reg.mount("/disk/", disk_resource, MountMode.WRITE)
|
||||
reg.mount("/ram/", ram_resource, MountMode.WRITE)
|
||||
return reg
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def nested_registry():
|
||||
"""MountRegistry with nested prefixes."""
|
||||
p1 = RAMResource()
|
||||
p1._store.files["/file.txt"] = b"outer\n"
|
||||
p2 = RAMResource()
|
||||
p2._store.files["/deep.txt"] = b"inner\n"
|
||||
|
||||
reg = MountRegistry()
|
||||
reg.mount("/data/", p1, MountMode.WRITE)
|
||||
reg.mount("/data/sub/", p2, MountMode.WRITE)
|
||||
return reg
|
||||
@@ -0,0 +1,199 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.resource.ram import RAMResource
|
||||
from mirage.types import MountMode, PathSpec
|
||||
from mirage.workspace.mount import MountRegistry
|
||||
from mirage.workspace.mount.mount import MountEntry
|
||||
|
||||
|
||||
def _run(coro):
|
||||
return asyncio.run(coro)
|
||||
|
||||
|
||||
# ── prefix validation ──────────────────────────
|
||||
|
||||
|
||||
def test_mount_accepts_root_prefix():
|
||||
m = MountEntry("/", RAMResource())
|
||||
assert m.prefix == "/"
|
||||
|
||||
|
||||
def test_mount_rejects_no_leading_slash():
|
||||
with pytest.raises(ValueError, match="must start with /"):
|
||||
MountEntry("data/", RAMResource())
|
||||
|
||||
|
||||
def test_mount_rejects_no_trailing_slash():
|
||||
with pytest.raises(ValueError, match="must end with /"):
|
||||
MountEntry("/data", RAMResource())
|
||||
|
||||
|
||||
def test_mount_rejects_double_slash():
|
||||
with pytest.raises(ValueError, match="must not contain //"):
|
||||
MountEntry("/data//sub/", RAMResource())
|
||||
|
||||
|
||||
def test_mount_valid_prefix():
|
||||
m = MountEntry("/data/", RAMResource())
|
||||
assert m.prefix == "/data/"
|
||||
|
||||
|
||||
# ── read-only enforcement ──────────────────────
|
||||
|
||||
|
||||
def test_read_only_blocks_write_ops():
|
||||
reg = MountRegistry()
|
||||
reg.mount("/ro/", RAMResource(), MountMode.READ)
|
||||
mount = reg.mount_for("/ro/file.txt")
|
||||
with pytest.raises(PermissionError, match="read-only"):
|
||||
_run(mount.execute_op("write", "/file.txt", data=b"x"))
|
||||
|
||||
|
||||
def test_write_mode_allows_write_ops():
|
||||
reg = MountRegistry()
|
||||
reg.mount("/rw/", RAMResource(), MountMode.WRITE)
|
||||
mount = reg.mount_for("/rw/file.txt")
|
||||
_run(mount.execute_op("write", "/new.txt", data=b"hello"))
|
||||
|
||||
|
||||
def test_read_only_blocks_write_cmd():
|
||||
reg = MountRegistry()
|
||||
reg.mount("/ro/", RAMResource(), MountMode.READ)
|
||||
mount = reg.mount_for("/ro/file.txt")
|
||||
scope = PathSpec(resource_path="ro/newdir",
|
||||
virtual="/ro/newdir",
|
||||
directory="/ro/",
|
||||
resolved=True)
|
||||
stdout, io = _run(mount.execute_cmd("mkdir", [scope], [], {}))
|
||||
assert io.exit_code != 0
|
||||
assert b"read-only" in io.stderr
|
||||
|
||||
|
||||
def test_write_mode_allows_write_cmd():
|
||||
reg = MountRegistry()
|
||||
reg.mount("/rw/", RAMResource(), MountMode.WRITE)
|
||||
mount = reg.mount_for("/rw/file.txt")
|
||||
scope = PathSpec(resource_path="rw/newdir",
|
||||
virtual="/rw/newdir",
|
||||
directory="/rw/",
|
||||
resolved=True)
|
||||
stdout, io = _run(mount.execute_cmd("mkdir", [scope], [], {}))
|
||||
assert io.exit_code == 0
|
||||
|
||||
|
||||
def test_read_only_allows_read_cmd():
|
||||
reg = MountRegistry()
|
||||
reg.mount("/ro/", RAMResource(), MountMode.READ)
|
||||
mount = reg.mount_for("/ro/")
|
||||
scope = PathSpec(resource_path="ro",
|
||||
virtual="/ro/",
|
||||
directory="/ro/",
|
||||
resolved=False)
|
||||
stdout, io = _run(mount.execute_cmd("ls", [scope], [], {}))
|
||||
assert io.exit_code == 0
|
||||
|
||||
|
||||
# ── execute_cmd ────────────────────────────────
|
||||
|
||||
|
||||
def test_execute_cmd_cat(registry):
|
||||
mount = registry.mount_for("/data/hello.txt")
|
||||
scope = PathSpec(resource_path="data/hello.txt",
|
||||
virtual="/data/hello.txt",
|
||||
directory="/data/",
|
||||
resolved=True)
|
||||
stdout, io = _run(mount.execute_cmd("cat", [scope], [], {}))
|
||||
assert io.exit_code == 0
|
||||
assert stdout is not None
|
||||
|
||||
|
||||
def test_execute_cmd_not_found(registry):
|
||||
mount = registry.mount_for("/data/hello.txt")
|
||||
stdout, io = _run(mount.execute_cmd("nonexistent_cmd", [], [], {}))
|
||||
assert io.exit_code == 127
|
||||
assert b"command not found" in io.stderr
|
||||
|
||||
|
||||
def test_execute_cmd_ls(registry):
|
||||
mount = registry.mount_for("/data/hello.txt")
|
||||
scope = PathSpec(resource_path="data",
|
||||
virtual="/data/",
|
||||
directory="/data/",
|
||||
resolved=False)
|
||||
stdout, io = _run(mount.execute_cmd("ls", [scope], [], {}))
|
||||
assert io.exit_code == 0
|
||||
|
||||
|
||||
def test_execute_cmd_with_flag_kwargs(registry):
|
||||
mount = registry.mount_for("/data/hello.txt")
|
||||
scope = PathSpec(resource_path="data/hello.txt",
|
||||
virtual="/data/hello.txt",
|
||||
directory="/data/",
|
||||
resolved=True)
|
||||
stdout, io = _run(mount.execute_cmd("cat", [scope], [], {"n": True}))
|
||||
assert io.exit_code == 0
|
||||
|
||||
|
||||
def test_execute_cmd_with_texts(registry):
|
||||
mount = registry.mount_for("/data/hello.txt")
|
||||
scope = PathSpec(resource_path="data/hello.txt",
|
||||
virtual="/data/hello.txt",
|
||||
directory="/data/",
|
||||
resolved=True)
|
||||
stdout, io = _run(mount.execute_cmd("grep", [scope], ["hello"], {}))
|
||||
assert io.exit_code == 0
|
||||
|
||||
|
||||
# ── execute_op ─────────────────────────────────
|
||||
|
||||
|
||||
def test_execute_op_stat(registry):
|
||||
mount = registry.mount_for("/data/hello.txt")
|
||||
result = _run(mount.execute_op("stat", "/hello.txt"))
|
||||
assert result is not None
|
||||
assert result.size > 0
|
||||
|
||||
|
||||
def test_execute_op_readdir(registry):
|
||||
mount = registry.mount_for("/data/")
|
||||
result = _run(mount.execute_op("readdir", "/"))
|
||||
assert isinstance(result, list)
|
||||
assert len(result) > 0
|
||||
|
||||
|
||||
def test_execute_op_no_such_op(registry):
|
||||
mount = registry.mount_for("/data/hello.txt")
|
||||
with pytest.raises(AttributeError, match="no op"):
|
||||
_run(mount.execute_op("nonexistent_op", "/file.txt"))
|
||||
|
||||
|
||||
# ── command resolution ─────────────────────────
|
||||
|
||||
|
||||
def test_resolve_command_exists(registry):
|
||||
mount = registry.mount_for("/data/hello.txt")
|
||||
cmd = mount.resolve_command("cat")
|
||||
assert cmd is not None
|
||||
assert cmd.name == "cat"
|
||||
|
||||
|
||||
def test_resolve_command_missing(registry):
|
||||
mount = registry.mount_for("/data/hello.txt")
|
||||
cmd = mount.resolve_command("nonexistent")
|
||||
assert cmd is None
|
||||
@@ -0,0 +1,119 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.workspace.mount.namespace import Namespace
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def namespace(registry):
|
||||
return Namespace(registry)
|
||||
|
||||
|
||||
def test_resolve_delegates_to_registry(namespace, registry):
|
||||
assert namespace.resolve("/data/hello.txt") == registry.resolve(
|
||||
"/data/hello.txt")
|
||||
|
||||
|
||||
def test_resolve_follow_noop_without_links(namespace):
|
||||
assert namespace.resolve(
|
||||
"/data/hello.txt", follow=True) == namespace.resolve("/data/hello.txt",
|
||||
follow=False)
|
||||
|
||||
|
||||
def test_resolve_unknown_path_raises(namespace):
|
||||
with pytest.raises(ValueError, match="no mount"):
|
||||
namespace.resolve("/unknown/x.txt")
|
||||
|
||||
|
||||
def test_mount_for_delegates_to_registry(namespace, registry):
|
||||
assert namespace.mount_for("/data/hello.txt") is registry.mount_for(
|
||||
"/data/hello.txt")
|
||||
|
||||
|
||||
def test_symlink_readlink_roundtrip_verbatim(namespace):
|
||||
namespace.symlink("/data/link", "/data/hello.txt", 1.0)
|
||||
assert namespace.is_link("/data/link")
|
||||
assert namespace.readlink("/data/link") == "/data/hello.txt"
|
||||
|
||||
|
||||
def test_readlink_missing_returns_none(namespace):
|
||||
assert namespace.readlink("/data/nope") is None
|
||||
|
||||
|
||||
def test_symlink_stores_relative_target_verbatim(namespace):
|
||||
namespace.symlink("/data/link", "hello.txt", 1.0)
|
||||
assert namespace.readlink("/data/link") == "hello.txt"
|
||||
|
||||
|
||||
def test_unlink_removes_link(namespace):
|
||||
namespace.symlink("/data/link", "/data/hello.txt", 1.0)
|
||||
assert namespace.unlink("/data/link") is True
|
||||
assert namespace.is_link("/data/link") is False
|
||||
assert namespace.unlink("/data/link") is False
|
||||
|
||||
|
||||
def test_rename_moves_link(namespace):
|
||||
namespace.symlink("/data/a", "/data/hello.txt", 1.0)
|
||||
assert namespace.rename("/data/a", "/data/b") is True
|
||||
assert namespace.is_link("/data/a") is False
|
||||
assert namespace.readlink("/data/b") == "/data/hello.txt"
|
||||
|
||||
|
||||
def test_resolve_follows_link_to_target_mount(namespace):
|
||||
namespace.symlink("/data/link", "/data/hello.txt", 1.0)
|
||||
assert namespace.resolve(
|
||||
"/data/link", follow=True) == namespace.resolve("/data/hello.txt")
|
||||
|
||||
|
||||
def test_resolve_no_follow_keeps_link_path(namespace, registry):
|
||||
namespace.symlink("/data/link", "/data/hello.txt", 1.0)
|
||||
assert namespace.resolve("/data/link",
|
||||
follow=False) == registry.resolve("/data/link")
|
||||
|
||||
|
||||
def test_resolve_cycle_raises(namespace):
|
||||
from mirage.utils.path import CycleError
|
||||
namespace.symlink("/data/a", "/data/b", 1.0)
|
||||
namespace.symlink("/data/b", "/data/a", 1.0)
|
||||
with pytest.raises(CycleError):
|
||||
namespace.resolve("/data/a", follow=True)
|
||||
|
||||
|
||||
def test_follow_resolves_prefix_links(namespace):
|
||||
namespace.symlink("/data/link", "/data/real", 1.0)
|
||||
assert namespace.follow("/data/link/f.txt") == "/data/real/f.txt"
|
||||
assert namespace.follow("/data/other") == "/data/other"
|
||||
|
||||
|
||||
def test_follow_identity_without_links(namespace):
|
||||
assert namespace.follow("/data/x") == "/data/x"
|
||||
|
||||
|
||||
def test_links_under_returns_direct_children_only(namespace):
|
||||
namespace.symlink("/data/a", "/t1", 1.0)
|
||||
namespace.symlink("/data/sub/b", "/t2", 1.0)
|
||||
namespace.symlink("/other/c", "/t3", 1.0)
|
||||
assert namespace.links_under("/data") == {"a": "/t1"}
|
||||
assert namespace.links_under("/data/sub") == {"b": "/t2"}
|
||||
|
||||
|
||||
def test_purge_under_drops_nested_entries(namespace):
|
||||
namespace.symlink("/data/sub/a", "/t1", 1.0)
|
||||
namespace.symlink("/data/sub/deep/b", "/t2", 1.0)
|
||||
namespace.symlink("/data/keep", "/t3", 1.0)
|
||||
assert namespace.purge_under("/data/sub") == 2
|
||||
assert namespace.is_link("/data/keep") is True
|
||||
assert namespace.is_link("/data/sub/a") is False
|
||||
@@ -0,0 +1,89 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage import MountMode, Workspace
|
||||
from mirage.commands.builtin.utils.safeguard import SafeguardExceededError
|
||||
from mirage.resource.ram import RAMResource
|
||||
from mirage.types import CommandSafeguard, OnExceed
|
||||
|
||||
|
||||
async def _read_long(accessor, scope, *args, **kwargs):
|
||||
return b"hello world"
|
||||
|
||||
|
||||
async def _read_lines(accessor, scope, *args, **kwargs):
|
||||
return b"a\nb\nc\nd\n"
|
||||
|
||||
|
||||
async def _read_short(accessor, scope, *args, **kwargs):
|
||||
return b"hi"
|
||||
|
||||
|
||||
async def _ws_mount():
|
||||
ws = Workspace({"/data": RAMResource()}, mode=MountMode.WRITE)
|
||||
await ws.execute("echo hi > /data/f.txt")
|
||||
mount = next(m for m in ws._registry._mounts if m.prefix == "/data/")
|
||||
return mount
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_vfs_read_truncates_to_max_bytes(monkeypatch):
|
||||
mount = await _ws_mount()
|
||||
mount.command_safeguards["read"] = CommandSafeguard(max_bytes=5)
|
||||
monkeypatch.setattr(mount._ops[("read", None)], "fn", _read_long)
|
||||
assert await mount.execute_op("read", "/data/f.txt") == b"hello"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_vfs_read_truncates_to_max_lines(monkeypatch):
|
||||
mount = await _ws_mount()
|
||||
mount.command_safeguards["read"] = CommandSafeguard(max_lines=2)
|
||||
monkeypatch.setattr(mount._ops[("read", None)], "fn", _read_lines)
|
||||
assert await mount.execute_op("read", "/data/f.txt") == b"a\nb\n"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_vfs_read_on_exceed_error_raises(monkeypatch):
|
||||
mount = await _ws_mount()
|
||||
mount.command_safeguards["read"] = CommandSafeguard(
|
||||
max_bytes=5, on_exceed=OnExceed.ERROR)
|
||||
monkeypatch.setattr(mount._ops[("read", None)], "fn", _read_long)
|
||||
with pytest.raises(SafeguardExceededError):
|
||||
await mount.execute_op("read", "/data/f.txt")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_vfs_read_within_limit_untouched(monkeypatch):
|
||||
mount = await _ws_mount()
|
||||
mount.command_safeguards["read"] = CommandSafeguard(max_bytes=100)
|
||||
monkeypatch.setattr(mount._ops[("read", None)], "fn", _read_short)
|
||||
assert await mount.execute_op("read", "/data/f.txt") == b"hi"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_vfs_unconfigured_read_untouched(monkeypatch):
|
||||
mount = await _ws_mount()
|
||||
monkeypatch.setattr(mount._ops[("read", None)], "fn", _read_long)
|
||||
assert await mount.execute_op("read", "/data/f.txt") == b"hello world"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_vfs_stat_not_capped_by_byte_limit(monkeypatch):
|
||||
mount = await _ws_mount()
|
||||
mount.command_safeguards["stat"] = CommandSafeguard(max_bytes=1)
|
||||
result = await mount.execute_op("stat", "/data/f.txt")
|
||||
assert result is not None
|
||||
assert not isinstance(result, (bytes, bytearray))
|
||||
@@ -0,0 +1,56 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage import MountMode, Workspace
|
||||
from mirage.commands.builtin.utils.safeguard import CommandTimeoutError
|
||||
from mirage.resource.ram import RAMResource
|
||||
from mirage.types import CommandSafeguard
|
||||
|
||||
|
||||
async def _slow_op(accessor, scope, *args, **kwargs):
|
||||
await asyncio.sleep(5)
|
||||
return None
|
||||
|
||||
|
||||
async def _slowish_op(accessor, scope, *args, **kwargs):
|
||||
await asyncio.sleep(0.2)
|
||||
return "ok"
|
||||
|
||||
|
||||
async def _ws_mount():
|
||||
ws = Workspace({"/data": RAMResource()}, mode=MountMode.WRITE)
|
||||
await ws.execute("echo hi > /data/f.txt")
|
||||
mount = next(m for m in ws._registry._mounts if m.prefix == "/data/")
|
||||
return mount
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_vfs_op_honors_per_mount_timeout(monkeypatch):
|
||||
mount = await _ws_mount()
|
||||
mount.command_safeguards["stat"] = CommandSafeguard(timeout_seconds=0.05)
|
||||
monkeypatch.setattr(mount._ops[("stat", None)], "fn", _slow_op)
|
||||
with pytest.raises(CommandTimeoutError):
|
||||
await mount.execute_op("stat", "/data/f.txt")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_vfs_op_unconfigured_is_not_timed(monkeypatch):
|
||||
mount = await _ws_mount()
|
||||
monkeypatch.setattr(mount._ops[("stat", None)], "fn", _slowish_op)
|
||||
result = await mount.execute_op("stat", "/data/f.txt")
|
||||
assert result == "ok"
|
||||
@@ -0,0 +1,389 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.cache.file.ram import RAMFileCacheStore
|
||||
from mirage.commands.registry import command
|
||||
from mirage.commands.spec.types import CommandSpec
|
||||
from mirage.io.types import IOResult
|
||||
from mirage.resource.base import BaseResource
|
||||
from mirage.resource.ram import RAMResource
|
||||
from mirage.resource.ssh import SSHConfig, SSHResource
|
||||
from mirage.types import MountMode, PathSpec
|
||||
from mirage.workspace.mount import MountCommandUnsupported, MountRegistry
|
||||
|
||||
# ── mount_for ──────────────────────────────────
|
||||
|
||||
|
||||
def test_mount_for_exact(registry):
|
||||
mount = registry.mount_for("/data/file.txt")
|
||||
assert mount.prefix == "/data/"
|
||||
|
||||
|
||||
def test_mount_for_nested_path(registry):
|
||||
mount = registry.mount_for("/data/sub/deep/file.txt")
|
||||
assert mount.prefix == "/data/"
|
||||
|
||||
|
||||
def test_mount_for_prefix_root(registry):
|
||||
mount = registry.mount_for("/data")
|
||||
assert mount.prefix == "/data/"
|
||||
|
||||
|
||||
def test_mount_for_no_match(registry):
|
||||
with pytest.raises(ValueError, match="no mount"):
|
||||
registry.mount_for("/unknown/file.txt")
|
||||
|
||||
|
||||
def test_mount_for_dev_default(registry):
|
||||
mount = registry.mount_for("/dev/null")
|
||||
assert mount.prefix == "/dev/"
|
||||
|
||||
|
||||
# ── longest prefix match ──────────────────────
|
||||
|
||||
|
||||
def test_nested_prefix_longest_match(nested_registry):
|
||||
mount = nested_registry.mount_for("/data/sub/deep.txt")
|
||||
assert mount.prefix == "/data/sub/"
|
||||
|
||||
|
||||
def test_nested_prefix_outer(nested_registry):
|
||||
mount = nested_registry.mount_for("/data/file.txt")
|
||||
assert mount.prefix == "/data/"
|
||||
|
||||
|
||||
# ── descendant_mounts ──────────────────────────
|
||||
|
||||
|
||||
def test_descendant_mounts_under_root(multi_registry):
|
||||
descs = multi_registry.descendant_mounts("/")
|
||||
prefixes = [m.prefix for m in descs]
|
||||
# /dev/ is auto-mounted by the registry; ours are /s3/, /disk/, /ram/.
|
||||
assert "/disk/" in prefixes
|
||||
assert "/ram/" in prefixes
|
||||
assert "/s3/" in prefixes
|
||||
|
||||
|
||||
def test_descendant_mounts_under_root_sorted(multi_registry):
|
||||
descs = multi_registry.descendant_mounts("/")
|
||||
prefixes = [m.prefix for m in descs]
|
||||
assert prefixes == sorted(prefixes)
|
||||
|
||||
|
||||
def test_descendant_mounts_at_a_mount_root_is_empty(registry):
|
||||
# /data/ is a mount; nothing is below it.
|
||||
assert registry.descendant_mounts("/data") == []
|
||||
assert registry.descendant_mounts("/data/") == []
|
||||
|
||||
|
||||
def test_descendant_mounts_inside_a_mount_is_empty(registry):
|
||||
assert registry.descendant_mounts("/data/sub") == []
|
||||
|
||||
|
||||
def test_descendant_mounts_nested(nested_registry):
|
||||
# Top: /data/, /data/sub/.
|
||||
descs = nested_registry.descendant_mounts("/")
|
||||
prefixes = [m.prefix for m in descs]
|
||||
assert "/data/" in prefixes
|
||||
assert "/data/sub/" in prefixes
|
||||
# Below /data/, the only descendant is /data/sub/.
|
||||
descs2 = nested_registry.descendant_mounts("/data")
|
||||
assert [m.prefix for m in descs2] == ["/data/sub/"]
|
||||
# /data/ as a mount root returns its own children only (excludes self).
|
||||
assert "/data/" not in [m.prefix for m in descs2]
|
||||
|
||||
|
||||
def test_descendant_mounts_excludes_self(nested_registry):
|
||||
descs = nested_registry.descendant_mounts("/data")
|
||||
assert all(m.prefix != "/data/" for m in descs)
|
||||
|
||||
|
||||
def test_descendant_mounts_unrelated_path(multi_registry):
|
||||
# A path in /s3/ has no mount nested below it.
|
||||
assert multi_registry.descendant_mounts("/s3/data") == []
|
||||
|
||||
|
||||
# ── resolve ────────────────────────────────────
|
||||
|
||||
|
||||
def test_resolve_strips_prefix(registry):
|
||||
prov, pp, mode = registry.resolve("/data/hello.txt")
|
||||
assert pp == "/hello.txt"
|
||||
assert prov.name == "ram"
|
||||
|
||||
|
||||
def test_resolve_root_path(registry):
|
||||
_, pp, _ = registry.resolve("/data")
|
||||
assert pp == "/"
|
||||
|
||||
|
||||
def test_resolve_trailing_slash(registry):
|
||||
_, pp, _ = registry.resolve("/data/sub/")
|
||||
assert pp.endswith("/")
|
||||
|
||||
|
||||
def test_resolve_mode(registry):
|
||||
_, _, mode = registry.resolve("/data/file.txt")
|
||||
assert mode == MountMode.WRITE
|
||||
|
||||
|
||||
def test_resolve_no_match(registry):
|
||||
with pytest.raises(ValueError):
|
||||
registry.resolve("/unknown/path")
|
||||
|
||||
|
||||
# ── multi mount (s3 + disk + ram) ──────────────
|
||||
|
||||
|
||||
def test_multi_mount_resolves_s3(multi_registry):
|
||||
mount = multi_registry.mount_for("/s3/data/report.csv")
|
||||
assert mount.prefix == "/s3/"
|
||||
assert mount.resource.name == "s3"
|
||||
|
||||
|
||||
def test_multi_mount_resolves_disk(multi_registry):
|
||||
mount = multi_registry.mount_for("/disk/readme.txt")
|
||||
assert mount.prefix == "/disk/"
|
||||
assert mount.resource.name == "disk"
|
||||
|
||||
|
||||
def test_multi_mount_resolves_ram(multi_registry):
|
||||
mount = multi_registry.mount_for("/ram/hello.txt")
|
||||
assert mount.prefix == "/ram/"
|
||||
assert mount.resource.name == "ram"
|
||||
|
||||
|
||||
def test_multi_mount_modes(multi_registry):
|
||||
_, _, s3_mode = multi_registry.resolve("/s3/file.txt")
|
||||
_, _, disk_mode = multi_registry.resolve("/disk/file.txt")
|
||||
_, _, ram_mode = multi_registry.resolve("/ram/file.txt")
|
||||
assert s3_mode == MountMode.READ
|
||||
assert disk_mode == MountMode.WRITE
|
||||
assert ram_mode == MountMode.WRITE
|
||||
|
||||
|
||||
def test_multi_mount_resource_paths(multi_registry):
|
||||
_, pp_s3, _ = multi_registry.resolve("/s3/data/report.csv")
|
||||
_, pp_disk, _ = multi_registry.resolve("/disk/readme.txt")
|
||||
_, pp_ram, _ = multi_registry.resolve("/ram/hello.txt")
|
||||
assert pp_s3 == "/data/report.csv"
|
||||
assert pp_disk == "/readme.txt"
|
||||
assert pp_ram == "/hello.txt"
|
||||
|
||||
|
||||
# ── mount ──────────────────────────────────────
|
||||
|
||||
|
||||
def test_mount_returns_mount_object():
|
||||
reg = MountRegistry()
|
||||
p = RAMResource()
|
||||
m = reg.mount("/test/", p, MountMode.READ)
|
||||
assert m.prefix == "/test/"
|
||||
assert m.resource is p
|
||||
|
||||
|
||||
def test_mount_normalizes_prefix():
|
||||
reg = MountRegistry()
|
||||
m = reg.mount("/test/", RAMResource(), MountMode.READ)
|
||||
assert m.prefix == "/test/"
|
||||
|
||||
|
||||
def test_mount_duplicate_raises(registry):
|
||||
with pytest.raises(ValueError, match="duplicate"):
|
||||
registry.mount("/data/", RAMResource())
|
||||
|
||||
|
||||
# ── mounts listing ─────────────────────────────
|
||||
|
||||
|
||||
def test_mounts_returns_all(multi_registry):
|
||||
prefixes = {m.prefix for m in multi_registry.mounts()}
|
||||
assert "/s3/" in prefixes
|
||||
assert "/disk/" in prefixes
|
||||
assert "/ram/" in prefixes
|
||||
assert "/dev/" in prefixes
|
||||
|
||||
|
||||
def test_mounts_count(multi_registry):
|
||||
assert len(multi_registry.mounts()) == 4
|
||||
|
||||
|
||||
# ── group_by_mount ─────────────────────────────
|
||||
|
||||
|
||||
def test_group_by_mount(multi_registry):
|
||||
groups = multi_registry.group_by_mount(
|
||||
["/s3/a.txt", "/s3/b.txt", "/disk/c.txt"])
|
||||
assert len(groups) == 2
|
||||
s3_group = [g for g in groups if g[0].prefix == "/s3/"][0]
|
||||
assert len(s3_group[1]) == 2
|
||||
disk_group = [g for g in groups if g[0].prefix == "/disk/"][0]
|
||||
assert len(disk_group[1]) == 1
|
||||
|
||||
|
||||
def test_group_by_mount_single(multi_registry):
|
||||
groups = multi_registry.group_by_mount(["/ram/hello.txt"])
|
||||
assert len(groups) == 1
|
||||
|
||||
|
||||
# ── get_resource_type ──────────────────────────
|
||||
|
||||
|
||||
def test_get_resource_type_s3(multi_registry):
|
||||
assert multi_registry.get_resource_type("/s3/file.txt") == "s3"
|
||||
|
||||
|
||||
def test_get_resource_type_disk(multi_registry):
|
||||
assert multi_registry.get_resource_type("/disk/file.txt") == "disk"
|
||||
|
||||
|
||||
def test_get_resource_type_ram(multi_registry):
|
||||
assert multi_registry.get_resource_type("/ram/file.txt") == "ram"
|
||||
|
||||
|
||||
def test_get_resource_type_none(multi_registry):
|
||||
assert multi_registry.get_resource_type(None) is None
|
||||
|
||||
|
||||
def test_get_resource_type_unknown(multi_registry):
|
||||
assert multi_registry.get_resource_type("/unknown/f") is None
|
||||
|
||||
|
||||
# ── find_resource_by_name ──────────────────────
|
||||
|
||||
|
||||
def test_find_resource_s3(multi_registry):
|
||||
prov = multi_registry.find_resource_by_name("s3")
|
||||
assert prov is not None
|
||||
assert prov.name == "s3"
|
||||
|
||||
|
||||
def test_find_resource_disk(multi_registry):
|
||||
prov = multi_registry.find_resource_by_name("disk")
|
||||
assert prov is not None
|
||||
assert prov.name == "disk"
|
||||
|
||||
|
||||
def test_find_resource_ram(multi_registry):
|
||||
prov = multi_registry.find_resource_by_name("ram")
|
||||
assert prov is not None
|
||||
|
||||
|
||||
def test_find_resource_none(multi_registry):
|
||||
assert multi_registry.find_resource_by_name(None) is None
|
||||
|
||||
|
||||
def test_find_resource_missing(multi_registry):
|
||||
assert multi_registry.find_resource_by_name("nonexistent") is None
|
||||
|
||||
|
||||
# ── mount_for_command ──────────────────────────
|
||||
|
||||
|
||||
def test_mount_for_command_cat(registry):
|
||||
mount = registry.mount_for_command("cat")
|
||||
assert mount is not None
|
||||
|
||||
|
||||
def test_mount_for_command_nonexistent(registry):
|
||||
assert registry.mount_for_command("nonexistent_cmd") is None
|
||||
|
||||
|
||||
def test_mount_for_command_grep(multi_registry):
|
||||
mount = multi_registry.mount_for_command("grep")
|
||||
assert mount is not None
|
||||
|
||||
|
||||
# ── resolve_mount cache redirect ───────────────
|
||||
|
||||
|
||||
def _remote_registry_with_cache():
|
||||
reg = MountRegistry()
|
||||
reg.mount("/ssh/", SSHResource(SSHConfig(host="example", root="/srv")),
|
||||
MountMode.WRITE)
|
||||
cache = RAMFileCacheStore()
|
||||
reg.attach_file_cache(cache)
|
||||
return reg, cache
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_mount_keeps_cached_read_on_real_mount():
|
||||
# Warm reads are served in place by with_read_cache, so a cached
|
||||
# read-only command stays on its real mount (keeping its safeguards and
|
||||
# custom handlers) instead of being redirected to the cache mount.
|
||||
reg, cache = _remote_registry_with_cache()
|
||||
await cache.set("/ssh/a.txt", b"hi")
|
||||
scope = PathSpec(resource_path="ssh/a.txt",
|
||||
virtual="/ssh/a.txt",
|
||||
directory="/ssh",
|
||||
resolved=True)
|
||||
mount = await reg.resolve_mount("cat", [scope], "/ssh")
|
||||
assert mount.prefix == "/ssh/"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_mount_keeps_cached_write_on_remote():
|
||||
reg, cache = _remote_registry_with_cache()
|
||||
await cache.set("/ssh/a.txt", b"hi")
|
||||
scope = PathSpec(resource_path="ssh/a.txt",
|
||||
virtual="/ssh/a.txt",
|
||||
directory="/ssh",
|
||||
resolved=True)
|
||||
mount = await reg.resolve_mount("rm", [scope], "/ssh")
|
||||
assert mount.prefix == "/ssh/"
|
||||
|
||||
|
||||
class _LimitedResource(BaseResource):
|
||||
name = "limited"
|
||||
|
||||
|
||||
class _FallbackResource(BaseResource):
|
||||
name = "fallback"
|
||||
|
||||
|
||||
@command("fallback-only", resource="fallback", spec=CommandSpec())
|
||||
async def _fallback_only(_store, paths, *texts, **kw):
|
||||
return b"fallback", IOResult()
|
||||
|
||||
|
||||
def _path_bound_registry_with_default():
|
||||
reg = MountRegistry()
|
||||
reg.mount("/limited/", _LimitedResource(), MountMode.WRITE)
|
||||
fallback = _FallbackResource()
|
||||
fallback.register(_fallback_only)
|
||||
reg.mount("/", fallback, MountMode.WRITE)
|
||||
return reg
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_mount_rejects_path_bound_unsupported_command():
|
||||
reg = _path_bound_registry_with_default()
|
||||
scope = PathSpec(resource_path="limited/file.txt",
|
||||
virtual="/limited/file.txt",
|
||||
directory="/limited",
|
||||
resolved=True)
|
||||
with pytest.raises(
|
||||
MountCommandUnsupported,
|
||||
match="fallback-only: /limited/file.txt: Operation not supported"):
|
||||
await reg.resolve_mount("fallback-only", [scope], "/limited")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_mount_allows_default_without_path_binding():
|
||||
reg = _path_bound_registry_with_default()
|
||||
mount = await reg.resolve_mount("fallback-only", [], "/limited")
|
||||
assert mount is not None
|
||||
assert mount.prefix == "/"
|
||||
@@ -0,0 +1,39 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from mirage.resource.ssh import SSHConfig, SSHResource
|
||||
from mirage.types import MountMode
|
||||
from mirage.workspace.mount.registry import MountRegistry
|
||||
|
||||
|
||||
def test_ssh_mount_registration():
|
||||
registry = MountRegistry()
|
||||
cfg = SSHConfig(host="dev", root="/home/ubuntu")
|
||||
resource = SSHResource(cfg)
|
||||
registry.mount("/ssh/", resource, mode=MountMode.WRITE)
|
||||
mount = registry.mount_for("/ssh/some/file.txt")
|
||||
assert mount is not None
|
||||
assert mount.resource.name == "ssh"
|
||||
|
||||
|
||||
def test_ssh_mount_command_resolution():
|
||||
registry = MountRegistry()
|
||||
cfg = SSHConfig(host="dev")
|
||||
resource = SSHResource(cfg)
|
||||
registry.mount("/remote/", resource, mode=MountMode.WRITE)
|
||||
mount = registry.mount_for("/remote/test.py")
|
||||
assert mount is not None
|
||||
cmd = mount.resolve_command("cat", None)
|
||||
assert cmd is not None
|
||||
assert cmd.resource == "ssh"
|
||||
Reference in New Issue
Block a user