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,31 @@
|
||||
from mirage.resource.registry import REGISTRY, build_resource
|
||||
from mirage.types import ResourceName
|
||||
|
||||
|
||||
def test_chroma_resource_is_registered():
|
||||
assert ResourceName.CHROMA == "chroma"
|
||||
assert REGISTRY[
|
||||
"chroma"].resource_path == "mirage.resource.chroma:ChromaResource"
|
||||
assert REGISTRY[
|
||||
"chroma"].config_path == "mirage.resource.chroma:ChromaConfig"
|
||||
|
||||
resource = build_resource("chroma", {"collection_name": "docs"})
|
||||
|
||||
assert resource.name == ResourceName.CHROMA
|
||||
assert resource.caches_reads is False
|
||||
assert resource.SUPPORTS_SNAPSHOT is False
|
||||
assert resource.config.collection_name == "docs"
|
||||
assert resource.config.slug_field == "page_slug"
|
||||
assert resource.accessor.config is resource.config
|
||||
|
||||
|
||||
def test_chroma_resource_registers_expected_commands_and_ops():
|
||||
resource = build_resource("chroma", {"collection_name": "docs"})
|
||||
|
||||
commands = {item.name for item in resource.commands()}
|
||||
ops = {item.name for item in resource.ops_list()}
|
||||
|
||||
assert {
|
||||
"cat", "ls", "grep", "find", "head", "tail", "tree", "chroma-query"
|
||||
}.issubset(commands)
|
||||
assert {"read", "readdir", "stat", "grep", "search"}.issubset(ops)
|
||||
@@ -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,99 @@
|
||||
from mirage.accessor import databricks_volume as accessor_module
|
||||
from mirage.accessor.databricks_volume import DatabricksVolumeAccessor
|
||||
from mirage.resource.databricks_volume import DatabricksVolumeConfig
|
||||
|
||||
|
||||
class FakeWorkspaceClient:
|
||||
calls: list[dict] = []
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
self.calls.append(kwargs)
|
||||
self.files = object()
|
||||
|
||||
|
||||
class FakeWorkspaceConfig:
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
|
||||
def test_accessor_passes_timeout_to_workspace_client(monkeypatch):
|
||||
FakeWorkspaceClient.calls = []
|
||||
monkeypatch.setattr(
|
||||
accessor_module,
|
||||
"WorkspaceClient",
|
||||
FakeWorkspaceClient,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
accessor_module,
|
||||
"WorkspaceConfig",
|
||||
FakeWorkspaceConfig,
|
||||
)
|
||||
config = DatabricksVolumeConfig(
|
||||
catalog="main",
|
||||
schema="default",
|
||||
volume="agent_files",
|
||||
host="https://example.cloud.databricks.com",
|
||||
token="secret",
|
||||
timeout=17,
|
||||
)
|
||||
accessor = DatabricksVolumeAccessor(config)
|
||||
assert accessor.files is not None
|
||||
sdk_config = FakeWorkspaceClient.calls[0]["config"]
|
||||
assert sdk_config.host == "https://example.cloud.databricks.com"
|
||||
assert sdk_config.token == "secret"
|
||||
assert sdk_config.http_timeout_seconds == 17
|
||||
|
||||
|
||||
def test_accessor_sets_pat_auth_type_for_explicit_token(monkeypatch):
|
||||
FakeWorkspaceClient.calls = []
|
||||
monkeypatch.setattr(
|
||||
accessor_module,
|
||||
"WorkspaceClient",
|
||||
FakeWorkspaceClient,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
accessor_module,
|
||||
"WorkspaceConfig",
|
||||
FakeWorkspaceConfig,
|
||||
)
|
||||
config = DatabricksVolumeConfig(
|
||||
catalog="main",
|
||||
schema="default",
|
||||
volume="agent_files",
|
||||
host="https://example.cloud.databricks.com",
|
||||
token="request-token",
|
||||
)
|
||||
accessor = DatabricksVolumeAccessor(config)
|
||||
|
||||
assert accessor.files is not None
|
||||
sdk_config = FakeWorkspaceClient.calls[0]["config"]
|
||||
assert sdk_config.auth_type == "pat"
|
||||
assert sdk_config.token == "request-token"
|
||||
|
||||
|
||||
def test_accessor_leaves_auth_type_unset_for_profile(monkeypatch):
|
||||
FakeWorkspaceClient.calls = []
|
||||
monkeypatch.setattr(
|
||||
accessor_module,
|
||||
"WorkspaceClient",
|
||||
FakeWorkspaceClient,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
accessor_module,
|
||||
"WorkspaceConfig",
|
||||
FakeWorkspaceConfig,
|
||||
)
|
||||
config = DatabricksVolumeConfig(
|
||||
catalog="main",
|
||||
schema="default",
|
||||
volume="agent_files",
|
||||
profile="default",
|
||||
)
|
||||
accessor = DatabricksVolumeAccessor(config)
|
||||
|
||||
assert accessor.files is not None
|
||||
sdk_config = FakeWorkspaceClient.calls[0]["config"]
|
||||
assert not hasattr(sdk_config, "auth_type")
|
||||
assert sdk_config.profile == "default"
|
||||
@@ -0,0 +1,725 @@
|
||||
# ========= 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 posixpath
|
||||
from io import BytesIO
|
||||
from types import SimpleNamespace
|
||||
from urllib.parse import unquote
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from mirage import MountMode, Workspace
|
||||
from mirage.cache.index import IndexEntry, LookupStatus
|
||||
from mirage.core.databricks_volume.path import backend_path
|
||||
from mirage.resource.databricks_volume import (DatabricksVolumeConfig,
|
||||
DatabricksVolumeResource)
|
||||
from mirage.types import PathSpec, ResourceName
|
||||
from mirage.utils.key_prefix import mount_key
|
||||
|
||||
|
||||
class NotFoundError(Exception):
|
||||
status_code = 404
|
||||
|
||||
|
||||
class FakeDownload:
|
||||
|
||||
def __init__(self, data: bytes) -> None:
|
||||
self.contents = BytesIO(data)
|
||||
|
||||
|
||||
class FakeFiles:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.downloads: dict[str, bytes] = {}
|
||||
self.metadata: dict[str, object] = {}
|
||||
self.directory_metadata: set[str] = set()
|
||||
self.directories: dict[str, list[object]] = {}
|
||||
self.upload_calls: list[tuple[str, bytes, bool]] = []
|
||||
self.delete_calls: list[str] = []
|
||||
self.create_directory_calls: list[str] = []
|
||||
self.delete_directory_calls: list[str] = []
|
||||
|
||||
def download(self, path: str) -> FakeDownload:
|
||||
if path not in self.downloads:
|
||||
raise NotFoundError(path)
|
||||
return FakeDownload(self.downloads[path])
|
||||
|
||||
def get_metadata(self, path: str) -> object:
|
||||
if path not in self.metadata:
|
||||
raise NotFoundError(path)
|
||||
return self.metadata[path]
|
||||
|
||||
def get_directory_metadata(self, path: str) -> None:
|
||||
if path not in self.directory_metadata:
|
||||
raise NotFoundError(path)
|
||||
|
||||
def list_directory_contents(self, path: str) -> list[object]:
|
||||
if path not in self.directories:
|
||||
raise NotFoundError(path)
|
||||
return self.directories[path]
|
||||
|
||||
def create_directory(self, path: str) -> None:
|
||||
self.create_directory_calls.append(path)
|
||||
cur = ""
|
||||
for part in path.strip("/").split("/"):
|
||||
cur = cur + "/" + part
|
||||
if cur in self.directory_metadata:
|
||||
continue
|
||||
self.directory_metadata.add(cur)
|
||||
self.metadata[cur] = SimpleNamespace(is_directory=True)
|
||||
self.directories.setdefault(cur, [])
|
||||
parent = posixpath.dirname(cur) or "/"
|
||||
self._upsert_directory_entry(
|
||||
parent, SimpleNamespace(path=cur, is_directory=True))
|
||||
|
||||
def delete_directory(self, path: str) -> None:
|
||||
self.delete_directory_calls.append(path)
|
||||
if path not in self.directory_metadata:
|
||||
raise NotFoundError(path)
|
||||
if self.directories.get(path):
|
||||
raise OSError(f"directory not empty: {path}")
|
||||
self.directory_metadata.discard(path)
|
||||
self.metadata.pop(path, None)
|
||||
self.directories.pop(path, None)
|
||||
parent = posixpath.dirname(path.rstrip("/")) or "/"
|
||||
self.directories[parent] = [
|
||||
entry for entry in self.directories.get(parent, [])
|
||||
if getattr(entry, "path", None) != path
|
||||
]
|
||||
|
||||
def upload(self, path: str, contents, overwrite: bool = False) -> None:
|
||||
data = contents.read()
|
||||
self.upload_calls.append((path, data, overwrite))
|
||||
parent = posixpath.dirname(path.rstrip("/")) or "/"
|
||||
if parent not in self.directory_metadata:
|
||||
if parent in self.metadata:
|
||||
raise NotADirectoryError(parent)
|
||||
raise NotFoundError(parent)
|
||||
if path in self.directory_metadata:
|
||||
raise IsADirectoryError(path)
|
||||
self.downloads[path] = data
|
||||
self.metadata[path] = SimpleNamespace(
|
||||
content_length=len(data),
|
||||
content_type=None,
|
||||
last_modified=None,
|
||||
)
|
||||
self._upsert_directory_entry(
|
||||
parent,
|
||||
SimpleNamespace(
|
||||
path=path,
|
||||
is_directory=False,
|
||||
file_size=len(data),
|
||||
),
|
||||
)
|
||||
|
||||
def delete(self, path: str) -> None:
|
||||
self.delete_calls.append(path)
|
||||
if path in self.directory_metadata:
|
||||
raise IsADirectoryError(path)
|
||||
if path not in self.metadata and path not in self.downloads:
|
||||
raise NotFoundError(path)
|
||||
self.metadata.pop(path, None)
|
||||
self.downloads.pop(path, None)
|
||||
parent = posixpath.dirname(path.rstrip("/")) or "/"
|
||||
self.directories[parent] = [
|
||||
entry for entry in self.directories.get(parent, [])
|
||||
if getattr(entry, "path", None) != path
|
||||
]
|
||||
|
||||
def _upsert_directory_entry(self, parent: str, entry: object) -> None:
|
||||
entries = [
|
||||
existing for existing in self.directories.get(parent, [])
|
||||
if getattr(existing, "path", None) != getattr(entry, "path", None)
|
||||
]
|
||||
entries.append(entry)
|
||||
self.directories[parent] = sorted(
|
||||
entries, key=lambda item: getattr(item, "path", ""))
|
||||
|
||||
|
||||
def _apply_range_header(data: bytes, range_header: str) -> bytes:
|
||||
if not range_header.startswith("bytes="):
|
||||
raise ValueError(f"unsupported range header: {range_header}")
|
||||
start_text, end_text = range_header.removeprefix("bytes=").split("-", 1)
|
||||
start = int(start_text) if start_text else 0
|
||||
end = int(end_text) + 1 if end_text else None
|
||||
return data[start:end]
|
||||
|
||||
|
||||
class FakeApiClient:
|
||||
|
||||
def __init__(self, files: FakeFiles) -> None:
|
||||
self.files = files
|
||||
|
||||
def do(
|
||||
self,
|
||||
method: str,
|
||||
path: str | None = None,
|
||||
url: str | None = None,
|
||||
query: dict | None = None,
|
||||
headers: dict | None = None,
|
||||
body: dict | None = None,
|
||||
raw: bool = False,
|
||||
files: object = None,
|
||||
data: object = None,
|
||||
auth: object = None,
|
||||
response_headers: list[str] | None = None,
|
||||
) -> dict:
|
||||
if method != "GET" or path is None:
|
||||
raise ValueError(f"unsupported fake API call: {method} {path}")
|
||||
remote_path = unquote(path.removeprefix("/api/2.0/fs/files"))
|
||||
if remote_path not in self.files.downloads:
|
||||
raise NotFoundError(remote_path)
|
||||
payload = self.files.downloads[remote_path]
|
||||
range_header = (headers or {}).get("Range")
|
||||
if range_header is not None:
|
||||
payload = _apply_range_header(payload, range_header)
|
||||
return {
|
||||
"contents": BytesIO(payload),
|
||||
"content-length": str(len(payload)),
|
||||
"accept-ranges": "bytes",
|
||||
}
|
||||
|
||||
|
||||
class FakeClient:
|
||||
|
||||
def __init__(self, files: FakeFiles) -> None:
|
||||
self.files = files
|
||||
self.api_client = FakeApiClient(files)
|
||||
|
||||
|
||||
def make_resource(files: FakeFiles) -> DatabricksVolumeResource:
|
||||
return DatabricksVolumeResource(
|
||||
DatabricksVolumeConfig(
|
||||
catalog="main",
|
||||
schema="default",
|
||||
volume="agent_files",
|
||||
root_path="/root",
|
||||
token="secret",
|
||||
),
|
||||
client=FakeClient(files),
|
||||
)
|
||||
|
||||
|
||||
def seed_directory(files: FakeFiles, path: str) -> None:
|
||||
files.directory_metadata.add(path)
|
||||
files.metadata[path] = SimpleNamespace(is_directory=True)
|
||||
files.directories.setdefault(path, [])
|
||||
|
||||
|
||||
def seed_file(files: FakeFiles, path: str, data: bytes) -> None:
|
||||
parent = path.rsplit("/", 1)[0]
|
||||
files.downloads[path] = data
|
||||
files.metadata[path] = SimpleNamespace(
|
||||
content_length=len(data),
|
||||
content_type=None,
|
||||
last_modified=None,
|
||||
)
|
||||
files.directories.setdefault(parent, [])
|
||||
files.directories[parent].append(
|
||||
SimpleNamespace(
|
||||
path=path,
|
||||
is_directory=False,
|
||||
file_size=len(data),
|
||||
))
|
||||
|
||||
|
||||
def test_config_validation_and_normalization():
|
||||
config = DatabricksVolumeConfig(
|
||||
catalog="main",
|
||||
schema="default",
|
||||
volume="agent_files",
|
||||
root_path="nested/path",
|
||||
)
|
||||
assert config.root_path == "/nested/path"
|
||||
with pytest.raises(ValidationError):
|
||||
DatabricksVolumeConfig(
|
||||
catalog="main/other",
|
||||
schema="default",
|
||||
volume="agent_files",
|
||||
)
|
||||
|
||||
|
||||
def test_backend_path_uses_volume_root_and_strips_mount_prefix():
|
||||
config = DatabricksVolumeConfig(
|
||||
catalog="main",
|
||||
schema="default",
|
||||
volume="agent_files",
|
||||
root_path="/root",
|
||||
)
|
||||
path = PathSpec(
|
||||
resource_path=mount_key("/volume/reports/latest.md", "/volume"),
|
||||
virtual="/volume/reports/latest.md",
|
||||
directory="/volume/reports",
|
||||
)
|
||||
assert backend_path(
|
||||
config,
|
||||
path) == ("/Volumes/main/default/agent_files/root/reports/latest.md")
|
||||
|
||||
|
||||
def test_resource_state_redacts_token():
|
||||
resource = make_resource(FakeFiles())
|
||||
state = resource.get_state()
|
||||
assert state["type"] == ResourceName.DATABRICKS_VOLUME
|
||||
assert state["needs_override"] is True
|
||||
assert state["config"]["token"] == "<REDACTED>"
|
||||
assert state["config"]["host"] is None
|
||||
assert state["config"]["catalog"] == "main"
|
||||
assert "token" in state["redacted_fields"]
|
||||
|
||||
|
||||
def test_resource_registers_ops():
|
||||
resource = make_resource(FakeFiles())
|
||||
op_names = {op.name for op in resource.ops_list()}
|
||||
assert {"read", "readdir", "stat", "write", "create", "unlink"} <= op_names
|
||||
assert resource.name == "databricks_volume"
|
||||
assert resource.caches_reads is True
|
||||
|
||||
|
||||
def test_resource_registers_commands():
|
||||
resource = make_resource(FakeFiles())
|
||||
command_names = {command.name for command in resource.commands()}
|
||||
assert {
|
||||
"cat",
|
||||
"find",
|
||||
"grep",
|
||||
"head",
|
||||
"ls",
|
||||
"rm",
|
||||
"rg",
|
||||
"stat",
|
||||
"tail",
|
||||
"touch",
|
||||
"tree",
|
||||
} <= command_names
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_stat_readdir_range_stream_and_exists():
|
||||
files = FakeFiles()
|
||||
root = "/Volumes/main/default/agent_files/root"
|
||||
files.downloads[f"{root}/reports/latest.md"] = b"abcdef"
|
||||
files.metadata[f"{root}/reports/latest.md"] = SimpleNamespace(
|
||||
content_length=6,
|
||||
content_type=None,
|
||||
last_modified="Tue, 14 Nov 2023 22:13:20 GMT",
|
||||
)
|
||||
files.metadata[f"{root}/reports"] = SimpleNamespace(is_directory=True)
|
||||
files.directories[f"{root}/reports"] = [
|
||||
SimpleNamespace(
|
||||
path=f"{root}/reports/latest.md",
|
||||
is_directory=False,
|
||||
file_size=6,
|
||||
)
|
||||
]
|
||||
resource = make_resource(files)
|
||||
|
||||
assert await resource.read_bytes(
|
||||
PathSpec.from_str_path(
|
||||
"/volume/reports/latest.md",
|
||||
mount_key("/volume/reports/latest.md", "/volume"))) == b"abcdef"
|
||||
assert await resource.range_read(
|
||||
PathSpec.from_str_path(
|
||||
"/volume/reports/latest.md",
|
||||
mount_key("/volume/reports/latest.md", "/volume")), 1, 4) == b"bcd"
|
||||
chunks = [
|
||||
chunk async for chunk in resource.read_stream(
|
||||
PathSpec.from_str_path(
|
||||
"/volume/reports/latest.md",
|
||||
mount_key("/volume/reports/latest.md", "/volume")),
|
||||
chunk_size=2,
|
||||
)
|
||||
]
|
||||
assert chunks == [b"ab", b"cd", b"ef"]
|
||||
file_stat = await resource.stat(
|
||||
PathSpec.from_str_path(
|
||||
"/volume/reports/latest.md",
|
||||
mount_key("/volume/reports/latest.md", "/volume")))
|
||||
assert file_stat.name == "latest.md"
|
||||
assert file_stat.size == 6
|
||||
assert file_stat.modified == "2023-11-14T22:13:20+00:00"
|
||||
assert await resource.exists(
|
||||
PathSpec.from_str_path(
|
||||
"/volume/reports/latest.md",
|
||||
mount_key("/volume/reports/latest.md", "/volume")))
|
||||
assert not await resource.exists(
|
||||
PathSpec.from_str_path("/volume/missing.md",
|
||||
mount_key("/volume/missing.md", "/volume")))
|
||||
entries = await resource.readdir(
|
||||
PathSpec.from_str_path("/volume/reports",
|
||||
mount_key("/volume/reports", "/volume")),
|
||||
resource.index,
|
||||
)
|
||||
assert entries == ["/volume/reports/latest.md"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_workspace_read_mode_uses_registered_ops():
|
||||
files = FakeFiles()
|
||||
root = "/Volumes/main/default/agent_files/root"
|
||||
files.downloads[f"{root}/latest.md"] = b"hello"
|
||||
files.metadata[f"{root}/latest.md"] = SimpleNamespace(
|
||||
content_length=5,
|
||||
content_type=None,
|
||||
last_modified=None,
|
||||
)
|
||||
ws = Workspace({"/volume": make_resource(files)}, mode=MountMode.READ)
|
||||
|
||||
assert await ws.ops.read("/volume/latest.md") == b"hello"
|
||||
file_stat = await ws.ops.stat("/volume/latest.md")
|
||||
assert file_stat.size == 5
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resource_exposes_file_write_ops():
|
||||
files = FakeFiles()
|
||||
root = "/Volumes/main/default/agent_files/root"
|
||||
seed_directory(files, root)
|
||||
resource = make_resource(files)
|
||||
|
||||
await resource.write(
|
||||
PathSpec.from_str_path("/volume/new.txt",
|
||||
mount_key("/volume/new.txt", "/volume")),
|
||||
b"hello",
|
||||
resource.index,
|
||||
)
|
||||
await resource.create(
|
||||
PathSpec.from_str_path("/volume/empty.txt",
|
||||
mount_key("/volume/empty.txt", "/volume")),
|
||||
resource.index,
|
||||
)
|
||||
await resource.unlink(
|
||||
PathSpec.from_str_path("/volume/new.txt",
|
||||
mount_key("/volume/new.txt", "/volume")),
|
||||
resource.index,
|
||||
)
|
||||
|
||||
assert files.downloads[f"{root}/empty.txt"] == b""
|
||||
assert f"{root}/new.txt" not in files.downloads
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_workspace_write_mode_uses_file_write_ops():
|
||||
files = FakeFiles()
|
||||
root = "/Volumes/main/default/agent_files/root"
|
||||
seed_directory(files, root)
|
||||
ws = Workspace({"/dbx/": make_resource(files)}, mode=MountMode.WRITE)
|
||||
|
||||
await ws.ops.write("/dbx/new.txt", b"hello")
|
||||
await ws.ops.create("/dbx/empty.txt")
|
||||
await ws.ops.unlink("/dbx/new.txt")
|
||||
|
||||
assert f"{root}/new.txt" not in files.downloads
|
||||
assert files.downloads[f"{root}/empty.txt"] == b""
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_workspace_write_mode_invalidates_parent_directory_index():
|
||||
files = FakeFiles()
|
||||
root = "/Volumes/main/default/agent_files/root"
|
||||
seed_directory(files, root)
|
||||
resource = make_resource(files)
|
||||
ws = Workspace({"/dbx/": resource}, mode=MountMode.WRITE)
|
||||
|
||||
await resource.index.set_dir("/dbx", [("old.txt",
|
||||
IndexEntry(
|
||||
id="/dbx/old.txt",
|
||||
name="old.txt",
|
||||
resource_type="file",
|
||||
))])
|
||||
assert (await resource.index.list_dir("/dbx")).entries == ["/dbx/old.txt"]
|
||||
|
||||
await ws.ops.write("/dbx/new.txt", b"hello")
|
||||
assert (await
|
||||
resource.index.list_dir("/dbx")).status == (LookupStatus.NOT_FOUND)
|
||||
|
||||
await resource.index.set_dir("/dbx", [("new.txt",
|
||||
IndexEntry(
|
||||
id="/dbx/new.txt",
|
||||
name="new.txt",
|
||||
resource_type="file",
|
||||
))])
|
||||
await ws.ops.create("/dbx/empty.txt")
|
||||
assert (await
|
||||
resource.index.list_dir("/dbx")).status == (LookupStatus.NOT_FOUND)
|
||||
|
||||
await resource.index.set_dir("/dbx", [("empty.txt",
|
||||
IndexEntry(
|
||||
id="/dbx/empty.txt",
|
||||
name="empty.txt",
|
||||
resource_type="file",
|
||||
))])
|
||||
await ws.ops.unlink("/dbx/empty.txt")
|
||||
assert (await
|
||||
resource.index.list_dir("/dbx")).status == (LookupStatus.NOT_FOUND)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_only_mount_rejects_file_write_ops():
|
||||
files = FakeFiles()
|
||||
root = "/Volumes/main/default/agent_files/root"
|
||||
seed_directory(files, root)
|
||||
ws = Workspace({"/dbx/": make_resource(files)}, mode=MountMode.READ)
|
||||
|
||||
with pytest.raises(PermissionError):
|
||||
await ws.ops.write("/dbx/new.txt", b"hello")
|
||||
with pytest.raises(PermissionError):
|
||||
await ws.ops.create("/dbx/empty.txt")
|
||||
with pytest.raises(PermissionError):
|
||||
await ws.ops.unlink("/dbx/new.txt")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_workspace_execute_databricks_volume_touch_and_rm():
|
||||
files = FakeFiles()
|
||||
root = "/Volumes/main/default/agent_files/root"
|
||||
seed_directory(files, root)
|
||||
ws = Workspace({"/dbx/": make_resource(files)}, mode=MountMode.WRITE)
|
||||
|
||||
touch_io = await ws.execute("touch /dbx/created.txt")
|
||||
rm_io = await ws.execute("rm /dbx/created.txt")
|
||||
|
||||
assert touch_io.exit_code == 0
|
||||
assert touch_io.writes == {"/dbx/created.txt": b""}
|
||||
assert files.delete_calls == [f"{root}/created.txt"]
|
||||
assert f"{root}/created.txt" not in files.downloads
|
||||
assert rm_io.exit_code == 0
|
||||
assert rm_io.writes == {"/dbx/created.txt": b""}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_workspace_execute_databricks_volume_rm_resolves_glob():
|
||||
files = FakeFiles()
|
||||
root = "/Volumes/main/default/agent_files/root"
|
||||
seed_directory(files, root)
|
||||
seed_file(files, f"{root}/one.txt", b"one")
|
||||
seed_file(files, f"{root}/two.txt", b"two")
|
||||
seed_file(files, f"{root}/keep.md", b"keep")
|
||||
ws = Workspace({"/dbx/": make_resource(files)}, mode=MountMode.WRITE)
|
||||
|
||||
io = await ws.execute("rm /dbx/*.txt")
|
||||
|
||||
assert io.exit_code == 0
|
||||
assert files.delete_calls == [f"{root}/one.txt", f"{root}/two.txt"]
|
||||
assert f"{root}/one.txt" not in files.downloads
|
||||
assert f"{root}/two.txt" not in files.downloads
|
||||
assert files.downloads[f"{root}/keep.md"] == b"keep"
|
||||
assert io.writes == {
|
||||
"/dbx/one.txt": b"",
|
||||
"/dbx/two.txt": b"",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_workspace_execute_databricks_volume_touch_resolves_glob():
|
||||
files = FakeFiles()
|
||||
root = "/Volumes/main/default/agent_files/root"
|
||||
seed_directory(files, root)
|
||||
seed_file(files, f"{root}/existing.txt", b"existing")
|
||||
ws = Workspace({"/dbx/": make_resource(files)}, mode=MountMode.WRITE)
|
||||
|
||||
io = await ws.execute("touch /dbx/*.txt")
|
||||
|
||||
assert io.exit_code == 0
|
||||
assert files.upload_calls == []
|
||||
assert f"{root}/*.txt" not in files.downloads
|
||||
assert io.writes == {}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_workspace_execute_databricks_volume_rm_rejects_directory():
|
||||
files = FakeFiles()
|
||||
root = "/Volumes/main/default/agent_files/root"
|
||||
seed_directory(files, root)
|
||||
seed_directory(files, f"{root}/dir")
|
||||
ws = Workspace({"/dbx/": make_resource(files)}, mode=MountMode.WRITE)
|
||||
|
||||
io = await ws.execute("rm /dbx/dir")
|
||||
|
||||
assert io.exit_code == 1
|
||||
assert b"IsADirectoryError" in io.stderr or b"Is a directory" in io.stderr
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_only_mount_rejects_file_write_commands():
|
||||
files = FakeFiles()
|
||||
root = "/Volumes/main/default/agent_files/root"
|
||||
seed_directory(files, root)
|
||||
ws = Workspace({"/dbx/": make_resource(files)}, mode=MountMode.READ)
|
||||
|
||||
touch_io = await ws.execute("touch /dbx/created.txt")
|
||||
rm_io = await ws.execute("rm /dbx/created.txt")
|
||||
|
||||
assert touch_io.exit_code == 1
|
||||
assert b"read-only mount" in touch_io.stderr
|
||||
assert rm_io.exit_code == 1
|
||||
assert b"read-only mount" in rm_io.stderr
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_workspace_execute_uses_databricks_volume_mount_for_ls():
|
||||
files = FakeFiles()
|
||||
root = "/Volumes/main/default/agent_files/root"
|
||||
files.directory_metadata.add(root)
|
||||
files.metadata[f"{root}/debug_output.json"] = SimpleNamespace(
|
||||
content_length=18,
|
||||
content_type=None,
|
||||
last_modified=None,
|
||||
)
|
||||
files.directories[root] = [
|
||||
SimpleNamespace(
|
||||
path=f"{root}/debug_output.json",
|
||||
is_directory=False,
|
||||
file_size=18,
|
||||
)
|
||||
]
|
||||
ws = Workspace({"/dbx/": make_resource(files)}, mode=MountMode.READ)
|
||||
|
||||
io = await ws.execute("ls /dbx")
|
||||
slash_io = await ws.execute("ls /dbx/")
|
||||
|
||||
assert io.exit_code == 0
|
||||
assert b"debug_output.json" in io.stdout
|
||||
assert slash_io.exit_code == 0
|
||||
assert b"debug_output.json" in slash_io.stdout
|
||||
mount = await ws._registry.resolve_mount(
|
||||
"ls",
|
||||
[PathSpec.from_str_path("/dbx", mount_key("/dbx", "/dbx"))],
|
||||
"/",
|
||||
)
|
||||
assert mount is not None
|
||||
assert mount.prefix == "/dbx/"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_workspace_execute_databricks_volume_stat_and_cat():
|
||||
files = FakeFiles()
|
||||
root = "/Volumes/main/default/agent_files/root"
|
||||
files.downloads[f"{root}/debug_output.json"] = b'{"ok": true}\nsecond\n'
|
||||
files.metadata[f"{root}/debug_output.json"] = SimpleNamespace(
|
||||
content_length=20,
|
||||
content_type=None,
|
||||
last_modified=None,
|
||||
)
|
||||
ws = Workspace({"/dbx/": make_resource(files)}, mode=MountMode.READ)
|
||||
|
||||
stat_io = await ws.execute("stat /dbx/debug_output.json")
|
||||
cat_io = await ws.execute("cat /dbx/debug_output.json")
|
||||
head_io = await ws.execute("head -n 1 /dbx/debug_output.json")
|
||||
|
||||
assert stat_io.exit_code == 0
|
||||
assert b"name=debug_output.json" in stat_io.stdout
|
||||
assert cat_io.exit_code == 0
|
||||
assert b'{"ok": true}' in cat_io.stdout
|
||||
assert head_io.exit_code == 0
|
||||
assert head_io.stdout == b'{"ok": true}\n'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_workspace_execute_databricks_volume_find_files():
|
||||
files = FakeFiles()
|
||||
root = "/Volumes/main/default/agent_files/root"
|
||||
files.directory_metadata.update({root, f"{root}/nested"})
|
||||
files.metadata[f"{root}/nested"] = SimpleNamespace(is_directory=True)
|
||||
files.metadata[f"{root}/debug_output.json"] = SimpleNamespace(
|
||||
content_length=2,
|
||||
content_type=None,
|
||||
last_modified=None,
|
||||
)
|
||||
files.metadata[f"{root}/nested/result.txt"] = SimpleNamespace(
|
||||
content_length=2,
|
||||
content_type=None,
|
||||
last_modified=None,
|
||||
)
|
||||
files.directories[root] = [
|
||||
SimpleNamespace(
|
||||
path=f"{root}/debug_output.json",
|
||||
is_directory=False,
|
||||
file_size=2,
|
||||
),
|
||||
SimpleNamespace(path=f"{root}/nested", is_directory=True),
|
||||
]
|
||||
files.directories[f"{root}/nested"] = [
|
||||
SimpleNamespace(
|
||||
path=f"{root}/nested/result.txt",
|
||||
is_directory=False,
|
||||
file_size=2,
|
||||
)
|
||||
]
|
||||
ws = Workspace({"/dbx/": make_resource(files)}, mode=MountMode.READ)
|
||||
|
||||
io = await ws.execute("find /dbx -maxdepth 2 -type f")
|
||||
|
||||
assert io.exit_code == 0
|
||||
assert b"/dbx/debug_output.json" in io.stdout
|
||||
assert b"/dbx/nested/result.txt" in io.stdout
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_workspace_execute_databricks_volume_recursive_grep_and_rg():
|
||||
files = FakeFiles()
|
||||
root = "/Volumes/main/default/agent_files/root"
|
||||
files.directory_metadata.update({
|
||||
root,
|
||||
f"{root}/nested",
|
||||
f"{root}/nested/deeper",
|
||||
})
|
||||
files.metadata[f"{root}/nested"] = SimpleNamespace(is_directory=True)
|
||||
files.metadata[f"{root}/nested/deeper"] = SimpleNamespace(
|
||||
is_directory=True)
|
||||
files.metadata[f"{root}/nested/info.txt"] = SimpleNamespace(
|
||||
content_length=17,
|
||||
content_type=None,
|
||||
last_modified=None,
|
||||
)
|
||||
files.metadata[f"{root}/nested/deeper/notes.md"] = SimpleNamespace(
|
||||
content_length=26,
|
||||
content_type=None,
|
||||
last_modified=None,
|
||||
)
|
||||
files.downloads[f"{root}/nested/info.txt"] = b"alpha debug line\n"
|
||||
files.downloads[f"{root}/nested/deeper/notes.md"] = (
|
||||
b"# Notes\nbeta debug detail\n")
|
||||
files.directories[root] = [
|
||||
SimpleNamespace(path=f"{root}/nested", is_directory=True),
|
||||
]
|
||||
files.directories[f"{root}/nested"] = [
|
||||
SimpleNamespace(
|
||||
path=f"{root}/nested/info.txt",
|
||||
is_directory=False,
|
||||
file_size=17,
|
||||
),
|
||||
SimpleNamespace(path=f"{root}/nested/deeper", is_directory=True),
|
||||
]
|
||||
files.directories[f"{root}/nested/deeper"] = [
|
||||
SimpleNamespace(
|
||||
path=f"{root}/nested/deeper/notes.md",
|
||||
is_directory=False,
|
||||
file_size=26,
|
||||
)
|
||||
]
|
||||
ws = Workspace({"/dbx/": make_resource(files)}, mode=MountMode.READ)
|
||||
|
||||
grep_io = await ws.execute("grep -R -n debug /dbx/nested")
|
||||
rg_io = await ws.execute("rg debug /dbx/nested")
|
||||
|
||||
assert grep_io.exit_code == 0
|
||||
assert b"/dbx/nested/info.txt:1:alpha debug line" in grep_io.stdout
|
||||
assert b"/dbx/nested/deeper/notes.md:2:beta debug detail" in (
|
||||
grep_io.stdout)
|
||||
assert not grep_io.stderr
|
||||
assert rg_io.exit_code == 0
|
||||
assert b"/dbx/nested/info.txt:alpha debug line" in rg_io.stdout
|
||||
assert b"/dbx/nested/deeper/notes.md:beta debug detail" in rg_io.stdout
|
||||
assert not rg_io.stderr
|
||||
@@ -0,0 +1,65 @@
|
||||
from mirage.resource.registry import REGISTRY, build_resource
|
||||
from mirage.types import ResourceName
|
||||
|
||||
|
||||
def test_dify_resource_is_registered_and_redacts_api_key():
|
||||
assert ResourceName.DIFY == "dify"
|
||||
assert REGISTRY[
|
||||
"dify"].resource_path == "mirage.resource.dify:DifyResource"
|
||||
assert REGISTRY["dify"].config_path == "mirage.resource.dify:DifyConfig"
|
||||
|
||||
resource = build_resource(
|
||||
"dify",
|
||||
{
|
||||
"api_key": "dataset-secret",
|
||||
"base_url": "https://api.dify.ai/v1/",
|
||||
"dataset_id": "dataset-1",
|
||||
},
|
||||
)
|
||||
|
||||
assert resource.name == ResourceName.DIFY
|
||||
assert resource.caches_reads is True
|
||||
assert resource.SUPPORTS_SNAPSHOT is False
|
||||
assert resource.config.base_url == "https://api.dify.ai/v1"
|
||||
assert resource.config.slug_metadata_name == "slug"
|
||||
assert resource.accessor.config is resource.config
|
||||
|
||||
state = resource.get_state()
|
||||
assert state["type"] == ResourceName.DIFY
|
||||
assert state["needs_override"] is True
|
||||
assert state["redacted_fields"] == ["api_key"]
|
||||
assert state["config"]["api_key"] == "<REDACTED>"
|
||||
assert state["config"]["dataset_id"] == "dataset-1"
|
||||
assert state["config"]["slug_metadata_name"] == "slug"
|
||||
|
||||
|
||||
def test_dify_resource_accepts_configured_slug_metadata_name():
|
||||
resource = build_resource(
|
||||
"dify",
|
||||
{
|
||||
"api_key": "dataset-secret",
|
||||
"base_url": "https://api.dify.ai/v1",
|
||||
"dataset_id": "dataset-1",
|
||||
"slug_metadata_name": "path",
|
||||
},
|
||||
)
|
||||
|
||||
assert resource.config.slug_metadata_name == "path"
|
||||
|
||||
|
||||
def test_dify_resource_registers_expected_commands_and_ops():
|
||||
resource = build_resource(
|
||||
"dify",
|
||||
{
|
||||
"api_key": "dataset-secret",
|
||||
"base_url": "https://api.dify.ai/v1",
|
||||
"dataset_id": "dataset-1",
|
||||
},
|
||||
)
|
||||
|
||||
commands = {item.name for item in resource.commands()}
|
||||
ops = {item.name for item in resource.ops_list()}
|
||||
|
||||
assert {"cat", "ls", "grep", "find", "head", "tail",
|
||||
"wc"}.issubset(commands)
|
||||
assert {"read", "readdir", "stat", "grep"}.issubset(ops)
|
||||
@@ -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,48 @@
|
||||
# ========= 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.discord.config import DiscordConfig
|
||||
from mirage.resource.discord.discord import DiscordResource
|
||||
from mirage.types import ResourceName
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config():
|
||||
return DiscordConfig(token="test-bot-token")
|
||||
|
||||
|
||||
def test_resource_init(config):
|
||||
resource = DiscordResource(config)
|
||||
assert resource.caches_reads is True
|
||||
|
||||
|
||||
def test_resource_name(config):
|
||||
resource = DiscordResource(config)
|
||||
assert resource.name == ResourceName.DISCORD
|
||||
|
||||
|
||||
def test_resource_accessor(config):
|
||||
resource = DiscordResource(config)
|
||||
assert resource.accessor is not None
|
||||
assert resource.accessor.config is config
|
||||
assert resource.index is not None
|
||||
|
||||
|
||||
def test_resource_commands(config):
|
||||
resource = DiscordResource(config)
|
||||
# 50 native (generic factory read set incl. find and sed + bespoke
|
||||
# grep/rg/head + discord_* writers) + 9 filetype cmds x 7 columnar exts
|
||||
assert len(resource._commands) == 50 + 9 * 7
|
||||
@@ -0,0 +1,85 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage import MountMode, Workspace
|
||||
from mirage.resource.disk.disk import DiskResource
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def local_backend(tmp_path):
|
||||
return DiskResource(str(tmp_path))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ws(tmp_path):
|
||||
resource = DiskResource(str(tmp_path))
|
||||
return Workspace({"/data": resource}, mode=MountMode.WRITE)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_and_cat(ws):
|
||||
await ws.execute('echo "hello" | tee /data/hello.txt')
|
||||
result = await ws.execute("cat /data/hello.txt")
|
||||
assert b"hello" in result.stdout
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mkdir_and_ls(ws):
|
||||
await ws.execute("mkdir /data/mydir")
|
||||
result = await ws.execute("ls /data/")
|
||||
assert b"mydir" in result.stdout
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rm(ws):
|
||||
await ws.execute('echo "x" | tee /data/del.txt')
|
||||
await ws.execute("rm /data/del.txt")
|
||||
result = await ws.execute("stat /data/del.txt")
|
||||
assert result.exit_code != 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_file(ws):
|
||||
await ws.execute('echo "hello" | tee /data/f.txt')
|
||||
result = await ws.execute("stat /data/f.txt")
|
||||
assert result.exit_code == 0
|
||||
assert b"name=f.txt" in result.stdout
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_directory(ws):
|
||||
await ws.execute("mkdir /data/mydir")
|
||||
result = await ws.execute("stat /data/mydir")
|
||||
assert result.exit_code == 0
|
||||
assert b"directory" in result.stdout
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_missing_raises(ws):
|
||||
result = await ws.execute("stat /data/missing.txt")
|
||||
assert result.exit_code != 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_path_traversal_raises(local_backend):
|
||||
from mirage.core.disk.stat import stat as core_stat
|
||||
from mirage.types import PathSpec # noqa: F811
|
||||
with pytest.raises(ValueError):
|
||||
await core_stat(
|
||||
local_backend.accessor,
|
||||
PathSpec(resource_path="../etc/passwd",
|
||||
virtual="/../etc/passwd",
|
||||
directory="/../etc/passwd"))
|
||||
@@ -0,0 +1,152 @@
|
||||
# ========= 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 hashlib
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.types import FileType
|
||||
|
||||
|
||||
def _cat_sync(backend, path):
|
||||
|
||||
async def _collect():
|
||||
return b"".join([c async for c in backend.read_stream(path)])
|
||||
|
||||
return asyncio.run(_collect())
|
||||
|
||||
|
||||
def test_write_and_read(memory_backend):
|
||||
asyncio.run(memory_backend.write("/f.txt", data=b"hello"))
|
||||
assert _cat_sync(memory_backend, "/f.txt") == b"hello"
|
||||
|
||||
|
||||
def test_create_and_write(memory_backend):
|
||||
asyncio.run(memory_backend.write("/a.txt", data=b"data"))
|
||||
assert _cat_sync(memory_backend, "/a.txt") == b"data"
|
||||
|
||||
|
||||
def test_read_partial_with_offset(memory_backend):
|
||||
asyncio.run(memory_backend.write("/f.txt", data=b"abcdef"))
|
||||
assert _cat_sync(memory_backend, "/f.txt")[2:5] == b"cde"
|
||||
|
||||
|
||||
def test_read_missing_raises(memory_backend):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
_cat_sync(memory_backend, "/missing.txt")
|
||||
|
||||
|
||||
def test_mkdir_and_readdir(memory_backend):
|
||||
asyncio.run(memory_backend.mkdir("/mydir"))
|
||||
entries = asyncio.run(
|
||||
memory_backend.readdir("/mydir", memory_backend.index))
|
||||
assert entries == []
|
||||
|
||||
|
||||
def test_mkdir_missing_parent_raises(memory_backend):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
asyncio.run(memory_backend.mkdir("/a/b/c"))
|
||||
|
||||
|
||||
def test_rmdir_empty(memory_backend):
|
||||
asyncio.run(memory_backend.mkdir("/emptydir"))
|
||||
asyncio.run(memory_backend.rmdir("/emptydir"))
|
||||
with pytest.raises(FileNotFoundError):
|
||||
asyncio.run(memory_backend.stat("/emptydir"))
|
||||
|
||||
|
||||
def test_rmdir_nonempty_raises(memory_backend):
|
||||
asyncio.run(memory_backend.mkdir("/dir"))
|
||||
asyncio.run(memory_backend.write("/dir/file.txt", data=b""))
|
||||
with pytest.raises(OSError):
|
||||
asyncio.run(memory_backend.rmdir("/dir"))
|
||||
|
||||
|
||||
def test_unlink(memory_backend):
|
||||
asyncio.run(memory_backend.write("/del.txt", data=b""))
|
||||
asyncio.run(memory_backend.unlink("/del.txt"))
|
||||
with pytest.raises(FileNotFoundError):
|
||||
_cat_sync(memory_backend, "/del.txt")
|
||||
|
||||
|
||||
def test_unlink_missing_raises(memory_backend):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
asyncio.run(memory_backend.unlink("/nope.txt"))
|
||||
|
||||
|
||||
def test_rename_file(memory_backend):
|
||||
asyncio.run(memory_backend.write("/old.txt", data=b"content"))
|
||||
asyncio.run(memory_backend.rename("/old.txt", "/new.txt"))
|
||||
assert _cat_sync(memory_backend, "/new.txt") == b"content"
|
||||
with pytest.raises(FileNotFoundError):
|
||||
_cat_sync(memory_backend, "/old.txt")
|
||||
|
||||
|
||||
def test_stat_file(memory_backend):
|
||||
asyncio.run(memory_backend.write("/f.txt", data=b"hello"))
|
||||
s = asyncio.run(memory_backend.stat("/f.txt"))
|
||||
assert s.name == "f.txt"
|
||||
assert s.size == 5
|
||||
assert s.type == "text"
|
||||
|
||||
|
||||
def test_stat_directory(memory_backend):
|
||||
asyncio.run(memory_backend.mkdir("/mydir"))
|
||||
s = asyncio.run(memory_backend.stat("/mydir"))
|
||||
assert s.type == FileType.DIRECTORY
|
||||
assert s.size is None
|
||||
|
||||
|
||||
def test_stat_missing_raises(memory_backend):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
asyncio.run(memory_backend.stat("/missing.txt"))
|
||||
|
||||
|
||||
def test_exists_true(memory_backend):
|
||||
asyncio.run(memory_backend.write("/f.txt", data=b""))
|
||||
try:
|
||||
asyncio.run(memory_backend.stat("/f.txt"))
|
||||
exists = True
|
||||
except FileNotFoundError:
|
||||
exists = False
|
||||
assert exists
|
||||
|
||||
|
||||
def test_exists_false(memory_backend):
|
||||
try:
|
||||
asyncio.run(memory_backend.stat("/nope.txt"))
|
||||
exists = True
|
||||
except FileNotFoundError:
|
||||
exists = False
|
||||
assert not exists
|
||||
|
||||
|
||||
def test_checksum_deterministic(memory_backend):
|
||||
asyncio.run(memory_backend.write("/f.txt", data=b"data"))
|
||||
raw = asyncio.run(memory_backend.read_bytes("/f.txt"))
|
||||
c1 = hashlib.md5(raw).hexdigest()
|
||||
c2 = hashlib.md5(raw).hexdigest()
|
||||
assert c1 == c2
|
||||
assert len(c1) == 32
|
||||
|
||||
|
||||
def test_readdir_lists_direct_children(memory_backend):
|
||||
asyncio.run(memory_backend.mkdir("/parent"))
|
||||
asyncio.run(memory_backend.mkdir("/parent/child1"))
|
||||
asyncio.run(memory_backend.write("/parent/file.txt", data=b""))
|
||||
entries = asyncio.run(
|
||||
memory_backend.readdir("/parent", memory_backend.index))
|
||||
assert any("child1" in e for e in entries)
|
||||
assert any("file.txt" in e for e in entries)
|
||||
@@ -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,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 pydantic import ValidationError
|
||||
|
||||
from mirage.accessor.email import EmailAccessor
|
||||
from mirage.resource.email.config import EmailConfig
|
||||
from mirage.types import ResourceName
|
||||
|
||||
|
||||
def test_email_resource_name_exists():
|
||||
assert ResourceName.EMAIL == "email"
|
||||
|
||||
|
||||
def test_config_creation():
|
||||
config = EmailConfig(
|
||||
imap_host="imap.fastmail.com",
|
||||
smtp_host="smtp.fastmail.com",
|
||||
username="test@fastmail.com",
|
||||
password="test-password",
|
||||
)
|
||||
assert config.imap_host == "imap.fastmail.com"
|
||||
assert config.imap_port == 993
|
||||
assert config.smtp_port == 587
|
||||
assert config.use_ssl is True
|
||||
|
||||
|
||||
def test_config_requires_credentials():
|
||||
with pytest.raises(ValidationError):
|
||||
EmailConfig()
|
||||
|
||||
|
||||
def test_config_custom_ports():
|
||||
config = EmailConfig(
|
||||
imap_host="imap.example.com",
|
||||
imap_port=143,
|
||||
smtp_host="smtp.example.com",
|
||||
smtp_port=465,
|
||||
username="user",
|
||||
password="pass",
|
||||
use_ssl=False,
|
||||
)
|
||||
assert config.imap_port == 143
|
||||
assert config.smtp_port == 465
|
||||
assert config.use_ssl is False
|
||||
|
||||
|
||||
def test_accessor_creation():
|
||||
config = EmailConfig(
|
||||
imap_host="imap.fastmail.com",
|
||||
smtp_host="smtp.fastmail.com",
|
||||
username="test@fastmail.com",
|
||||
password="test-password",
|
||||
)
|
||||
accessor = EmailAccessor(config)
|
||||
assert accessor.config is config
|
||||
@@ -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.resource.email.config import EmailConfig
|
||||
from mirage.resource.email.email import EmailResource
|
||||
from mirage.types import ResourceName
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config():
|
||||
return EmailConfig(
|
||||
imap_host="imap.test.com",
|
||||
smtp_host="smtp.test.com",
|
||||
username="user@test.com",
|
||||
password="pass",
|
||||
)
|
||||
|
||||
|
||||
def test_resource_init(config):
|
||||
resource = EmailResource(config=config)
|
||||
assert resource.name == ResourceName.EMAIL
|
||||
assert resource.caches_reads is True
|
||||
|
||||
|
||||
def test_resource_accessor(config):
|
||||
resource = EmailResource(config=config)
|
||||
assert resource.accessor is not None
|
||||
assert resource.accessor.config is config
|
||||
|
||||
|
||||
def test_resource_commands_registered(config):
|
||||
resource = EmailResource(config=config)
|
||||
cmds = resource.commands()
|
||||
assert len(cmds) >= 6
|
||||
|
||||
|
||||
def test_resource_ops_registered(config):
|
||||
resource = EmailResource(config=config)
|
||||
ops = resource.ops_list()
|
||||
assert len(ops) == 3
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resource_fingerprint_not_found(config):
|
||||
resource = EmailResource(config=config)
|
||||
fp = await resource.fingerprint("/email/nonexistent")
|
||||
assert fp is None
|
||||
@@ -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,27 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from mirage.resource.gdocs.config import GDocsConfig
|
||||
from mirage.resource.secrets import reveal_secret
|
||||
|
||||
|
||||
def test_config_creation():
|
||||
config = GDocsConfig(
|
||||
client_id="xxx.apps.googleusercontent.com",
|
||||
client_secret="GOCSPx-xxx",
|
||||
refresh_token="1//0abc",
|
||||
)
|
||||
assert config.client_id == "xxx.apps.googleusercontent.com"
|
||||
assert reveal_secret(config.client_secret) == "GOCSPx-xxx"
|
||||
assert reveal_secret(config.refresh_token) == "1//0abc"
|
||||
@@ -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. =========
|
||||
|
||||
from mirage.resource.gdocs.doc_entry import (DocEntry, make_filename,
|
||||
sanitize_title)
|
||||
|
||||
TITLE_MAX = 100
|
||||
|
||||
|
||||
def test_doc_entry_creation():
|
||||
entry = DocEntry(
|
||||
id="abc123",
|
||||
name="My Doc",
|
||||
modified_time="2026-04-01T12:00:00.000Z",
|
||||
created_time="2026-03-01T12:00:00.000Z",
|
||||
owner="user@gmail.com",
|
||||
owned_by_me=True,
|
||||
can_edit=True,
|
||||
filename="My_Doc__abc123.gdoc.json",
|
||||
)
|
||||
assert entry.id == "abc123"
|
||||
assert entry.name == "My Doc"
|
||||
assert entry.owned_by_me is True
|
||||
assert entry.can_edit is True
|
||||
assert entry.filename == "My_Doc__abc123.gdoc.json"
|
||||
|
||||
|
||||
def test_sanitize_title_basic():
|
||||
assert sanitize_title("Hello World") == "Hello_World"
|
||||
|
||||
|
||||
def test_sanitize_title_special_chars():
|
||||
assert sanitize_title("My/Doc: A\\Test") == "My_Doc_A_Test"
|
||||
|
||||
|
||||
def test_sanitize_title_truncation():
|
||||
long_title = "A" * 150
|
||||
result = sanitize_title(long_title)
|
||||
assert len(result) <= TITLE_MAX
|
||||
assert result.endswith("...")
|
||||
|
||||
|
||||
def test_sanitize_title_consecutive_underscores():
|
||||
assert sanitize_title("Hello // World") == "Hello_World"
|
||||
|
||||
|
||||
def test_sanitize_title_empty():
|
||||
assert sanitize_title("") == "Untitled"
|
||||
|
||||
|
||||
def test_make_filename_without_date():
|
||||
assert make_filename("My Doc", "abc123") == "My_Doc__abc123.gdoc.json"
|
||||
|
||||
|
||||
def test_make_filename_with_date():
|
||||
result = make_filename("My Doc", "abc123", "2026-04-01T12:00:00.000Z")
|
||||
assert result == "2026-04-01_My_Doc__abc123.gdoc.json"
|
||||
|
||||
|
||||
def test_make_filename_long_title():
|
||||
long_title = "A" * 150
|
||||
filename = make_filename(long_title, "abc123", "2026-04-01T00:00:00.000Z")
|
||||
assert filename.endswith("__abc123.gdoc.json")
|
||||
assert filename.startswith("2026-04-01_")
|
||||
|
||||
|
||||
def test_make_filename_duplicate_titles_different_dates():
|
||||
f1 = make_filename("My Doc", "abc123", "2026-04-01T00:00:00.000Z")
|
||||
f2 = make_filename("My Doc", "def456", "2026-03-15T00:00:00.000Z")
|
||||
assert f1 != f2
|
||||
assert f1 == "2026-04-01_My_Doc__abc123.gdoc.json"
|
||||
assert f2 == "2026-03-15_My_Doc__def456.gdoc.json"
|
||||
@@ -0,0 +1,42 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from mirage.resource.gdocs.prompt import PROMPT, WRITE_PROMPT
|
||||
|
||||
|
||||
def test_prompt_includes_buckets_and_structure():
|
||||
rendered = PROMPT.format(prefix="/gdocs")
|
||||
assert "owned/" in rendered
|
||||
assert "shared/" in rendered
|
||||
assert "shared with you by others" in rendered
|
||||
assert "still in owned/" in rendered
|
||||
assert "gdoc.json structure" in rendered
|
||||
assert ".body.content[].paragraph.elements[].textRun.content" in rendered
|
||||
|
||||
|
||||
def test_write_prompt_examples_match_actual_signatures():
|
||||
assert "gws-docs-write" in WRITE_PROMPT
|
||||
assert "--document" in WRITE_PROMPT
|
||||
assert "--text" in WRITE_PROMPT
|
||||
assert "gws-docs-documents-create" in WRITE_PROMPT
|
||||
assert "--json" in WRITE_PROMPT
|
||||
assert '{"title":' in WRITE_PROMPT
|
||||
assert "gws-docs-documents-batchUpdate" in WRITE_PROMPT
|
||||
assert "documentId" in WRITE_PROMPT
|
||||
|
||||
|
||||
def test_write_prompt_documents_rm_and_newline_gotcha():
|
||||
assert "rm " in WRITE_PROMPT
|
||||
assert ".gdoc.json" in WRITE_PROMPT
|
||||
assert "$'" in WRITE_PROMPT
|
||||
@@ -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.resource.gdocs.config import GDocsConfig
|
||||
from mirage.resource.gdocs.gdocs import GDocsResource
|
||||
from mirage.types import ResourceName
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config():
|
||||
return GDocsConfig(
|
||||
client_id="test-id",
|
||||
client_secret="test-secret",
|
||||
refresh_token="test-refresh",
|
||||
)
|
||||
|
||||
|
||||
def test_resource_init(config):
|
||||
resource = GDocsResource(config=config)
|
||||
assert resource.name == ResourceName.GDOCS
|
||||
assert resource.caches_reads is True
|
||||
|
||||
|
||||
def test_resource_accessor(config):
|
||||
resource = GDocsResource(config=config)
|
||||
assert resource.accessor is not None
|
||||
assert resource.accessor.config is config
|
||||
assert resource.accessor.token_manager is not None
|
||||
|
||||
|
||||
def test_resource_commands_registered(config):
|
||||
resource = GDocsResource(config=config)
|
||||
cmds = resource.commands()
|
||||
assert len(cmds) > 15
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resource_fingerprint_not_found(config):
|
||||
resource = GDocsResource(config=config)
|
||||
fp = await resource.fingerprint("owned/Nonexistent__xyz.gdoc.json")
|
||||
assert fp is None
|
||||
@@ -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,28 @@
|
||||
# ========= 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.gdrive.prompt import PROMPT
|
||||
|
||||
|
||||
def test_prompt_cross_references_per_service_shapes():
|
||||
rendered = PROMPT.format(prefix="/gdrive")
|
||||
assert ".gdoc.json" in rendered
|
||||
assert ".gsheet.json" in rendered
|
||||
assert ".gslide.json" in rendered
|
||||
assert "/gdocs prompt" in rendered
|
||||
assert "/gsheets prompt" in rendered
|
||||
assert "/gslides prompt" in rendered
|
||||
assert "remote mount" in rendered
|
||||
assert "modifiedTime range" in rendered
|
||||
assert "No owned/ vs shared/" in rendered
|
||||
@@ -0,0 +1,51 @@
|
||||
# ========= 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 patch
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.resource.gdrive.config import GoogleDriveConfig
|
||||
from mirage.resource.gdrive.gdrive import GoogleDriveResource
|
||||
from mirage.types import ResourceName
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config():
|
||||
return GoogleDriveConfig(
|
||||
client_id="test-id",
|
||||
client_secret="test-secret",
|
||||
refresh_token="test-refresh",
|
||||
)
|
||||
|
||||
|
||||
def test_resource_init(config):
|
||||
with patch("mirage.core.google._client.refresh_access_token", ):
|
||||
resource = GoogleDriveResource(config)
|
||||
assert resource.name == ResourceName.GDRIVE
|
||||
assert resource.caches_reads is True
|
||||
|
||||
|
||||
def test_resource_accessor(config):
|
||||
with patch("mirage.core.google._client.refresh_access_token", ):
|
||||
resource = GoogleDriveResource(config)
|
||||
assert resource.accessor is not None
|
||||
assert resource.accessor.config is config
|
||||
assert resource.accessor.token_manager is not None
|
||||
|
||||
|
||||
def test_resource_commands_registered(config):
|
||||
with patch("mirage.core.google._client.refresh_access_token", ):
|
||||
resource = GoogleDriveResource(config)
|
||||
assert len(resource._commands) >= 50
|
||||
@@ -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,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 pydantic import ValidationError
|
||||
|
||||
from mirage.core.github.config import GitHubConfig
|
||||
from mirage.core.github.tree_entry import TreeEntry
|
||||
from mirage.resource.secrets import reveal_secret
|
||||
|
||||
|
||||
def test_github_config_requires_token():
|
||||
with pytest.raises(ValidationError):
|
||||
GitHubConfig()
|
||||
|
||||
|
||||
def test_github_config_with_token():
|
||||
cfg = GitHubConfig(token="ghp_abc123")
|
||||
assert reveal_secret(cfg.token) == "ghp_abc123"
|
||||
|
||||
|
||||
def test_github_config_owner_repo_ref_default():
|
||||
cfg = GitHubConfig(token="ghp_abc123")
|
||||
assert cfg.owner is None
|
||||
assert cfg.repo is None
|
||||
assert cfg.ref == "main"
|
||||
|
||||
|
||||
def test_github_config_accepts_owner_repo_ref():
|
||||
cfg = GitHubConfig(token="ghp_abc123",
|
||||
owner="strukto-ai",
|
||||
repo="mirage",
|
||||
ref="dev")
|
||||
assert cfg.owner == "strukto-ai"
|
||||
assert cfg.repo == "mirage"
|
||||
assert cfg.ref == "dev"
|
||||
|
||||
|
||||
def test_tree_entry_fields():
|
||||
entry = TreeEntry(path="src/main.py", type="blob", sha="abc123", size=1024)
|
||||
assert entry.path == "src/main.py"
|
||||
assert entry.type == "blob"
|
||||
assert entry.sha == "abc123"
|
||||
assert entry.size == 1024
|
||||
|
||||
|
||||
def test_tree_entry_size_none():
|
||||
entry = TreeEntry(path="src", type="tree", sha="def456", size=None)
|
||||
assert entry.size is None
|
||||
@@ -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 unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.core.github.config import GitHubConfig
|
||||
from mirage.core.github.tree_entry import TreeEntry
|
||||
from mirage.resource.github.github import GitHubResource
|
||||
from mirage.types import ResourceName
|
||||
|
||||
CONFIG = GitHubConfig(token="test-token")
|
||||
OWNER = "test-owner"
|
||||
REPO = "test-repo"
|
||||
|
||||
|
||||
def _make_resource(ref: str = "main",
|
||||
default_branch: str = "main",
|
||||
tree: dict | None = None,
|
||||
truncated: bool = False) -> GitHubResource:
|
||||
if tree is None:
|
||||
tree = {}
|
||||
with patch("mirage.resource.github.github.fetch_default_branch_sync",
|
||||
return_value=default_branch), \
|
||||
patch("mirage.resource.github.github.fetch_tree_sync",
|
||||
return_value=(tree, truncated)):
|
||||
return GitHubResource(
|
||||
config=CONFIG,
|
||||
owner=OWNER,
|
||||
repo=REPO,
|
||||
ref=ref,
|
||||
)
|
||||
|
||||
|
||||
def test_name() -> None:
|
||||
resource = _make_resource()
|
||||
assert resource.name == ResourceName.GITHUB
|
||||
|
||||
|
||||
def test_caches_reads() -> None:
|
||||
resource = _make_resource()
|
||||
assert resource.caches_reads is True
|
||||
|
||||
|
||||
def test_bind_args() -> None:
|
||||
resource = _make_resource()
|
||||
assert resource.accessor.config is CONFIG
|
||||
assert resource.accessor.owner == OWNER
|
||||
assert resource.accessor.repo == REPO
|
||||
assert resource.accessor.ref == "main"
|
||||
|
||||
|
||||
def test_owner_repo_ref_fall_back_to_config() -> None:
|
||||
config = GitHubConfig(token="test-token",
|
||||
owner="cfg-owner",
|
||||
repo="cfg-repo",
|
||||
ref="cfg-ref")
|
||||
with patch("mirage.resource.github.github.fetch_default_branch_sync",
|
||||
return_value="main"), \
|
||||
patch("mirage.resource.github.github.fetch_tree_sync",
|
||||
return_value=({}, False)):
|
||||
resource = GitHubResource(config=config)
|
||||
assert resource.accessor.owner == "cfg-owner"
|
||||
assert resource.accessor.repo == "cfg-repo"
|
||||
assert resource.accessor.ref == "cfg-ref"
|
||||
|
||||
|
||||
def test_kwargs_take_precedence_over_config() -> None:
|
||||
config = GitHubConfig(token="test-token",
|
||||
owner="cfg-owner",
|
||||
repo="cfg-repo",
|
||||
ref="cfg-ref")
|
||||
with patch("mirage.resource.github.github.fetch_default_branch_sync",
|
||||
return_value="main"), \
|
||||
patch("mirage.resource.github.github.fetch_tree_sync",
|
||||
return_value=({}, False)):
|
||||
resource = GitHubResource(config=config,
|
||||
owner="kw-owner",
|
||||
repo="kw-repo",
|
||||
ref="kw-ref")
|
||||
assert resource.accessor.owner == "kw-owner"
|
||||
assert resource.accessor.repo == "kw-repo"
|
||||
assert resource.accessor.ref == "kw-ref"
|
||||
|
||||
|
||||
def test_missing_owner_repo_raises() -> None:
|
||||
with pytest.raises(ValueError, match="requires owner and repo"):
|
||||
GitHubResource(config=GitHubConfig(token="test-token"))
|
||||
|
||||
|
||||
def test_is_default_branch_true() -> None:
|
||||
resource = _make_resource(ref="main", default_branch="main")
|
||||
assert resource.is_default_branch is True
|
||||
|
||||
|
||||
def test_is_default_branch_false() -> None:
|
||||
resource = _make_resource(ref="feature-branch", default_branch="main")
|
||||
assert resource.is_default_branch is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fingerprint_returns_sha() -> None:
|
||||
tree = {
|
||||
"src/main.py":
|
||||
TreeEntry(path="src/main.py", type="blob", sha="abc123", size=100),
|
||||
}
|
||||
resource = _make_resource(tree=tree)
|
||||
result = await resource.fingerprint("src/main.py")
|
||||
assert result == "abc123"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fingerprint_strips_slash() -> None:
|
||||
tree = {
|
||||
"src/main.py":
|
||||
TreeEntry(path="src/main.py", type="blob", sha="abc123", size=100),
|
||||
}
|
||||
resource = _make_resource(tree=tree)
|
||||
result = await resource.fingerprint("/src/main.py")
|
||||
assert result == "abc123"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fingerprint_returns_none_when_path_not_in_tree() -> None:
|
||||
resource = _make_resource()
|
||||
result = await resource.fingerprint("nonexistent.py")
|
||||
assert result is None
|
||||
|
||||
|
||||
@patch("mirage.resource.github.github.fetch_tree_sync")
|
||||
@patch("mirage.resource.github.github.fetch_default_branch_sync")
|
||||
def test_init_fetches_default_branch(mock_fetch_branch,
|
||||
mock_fetch_tree) -> None:
|
||||
mock_fetch_branch.return_value = "develop"
|
||||
mock_fetch_tree.return_value = ({}, False)
|
||||
resource = GitHubResource(config=CONFIG,
|
||||
owner=OWNER,
|
||||
repo=REPO,
|
||||
ref="main")
|
||||
assert resource.accessor.default_branch == "develop"
|
||||
mock_fetch_branch.assert_called_once_with(CONFIG, OWNER, REPO)
|
||||
@@ -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,27 @@
|
||||
# ========= 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.gmail.config import GmailConfig
|
||||
from mirage.resource.secrets import reveal_secret
|
||||
|
||||
|
||||
def test_config_creation():
|
||||
config = GmailConfig(
|
||||
client_id="xxx.apps.googleusercontent.com",
|
||||
client_secret="GOCSPx-xxx",
|
||||
refresh_token="1//0abc",
|
||||
)
|
||||
assert config.client_id == "xxx.apps.googleusercontent.com"
|
||||
assert reveal_secret(config.client_secret) == "GOCSPx-xxx"
|
||||
assert reveal_secret(config.refresh_token) == "1//0abc"
|
||||
@@ -0,0 +1,58 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from mirage.resource.gmail.prompt import PROMPT, WRITE_PROMPT
|
||||
|
||||
|
||||
def test_prompt_includes_path_anatomy_and_processed_shape():
|
||||
rendered = PROMPT.format(prefix="/gmail")
|
||||
assert "<label>" in rendered
|
||||
assert "INBOX" in rendered
|
||||
assert "after:/before:" in rendered
|
||||
assert "mirage-processed" in rendered
|
||||
assert ".body_text" in rendered
|
||||
assert "gws-gmail-read" in rendered
|
||||
assert "gws-gmail-triage" in rendered
|
||||
|
||||
|
||||
def test_prompt_documents_file_per_message_layout():
|
||||
rendered = PROMPT.format(prefix="/gmail")
|
||||
assert "<subject>__<message-id>.gmail.json" in rendered
|
||||
assert "<subject>__<message-id>/" in rendered
|
||||
assert "attachments dir" in rendered
|
||||
assert ".attachments[]" in rendered
|
||||
|
||||
|
||||
def test_prompt_mentions_grep_skips_binary_attachments():
|
||||
rendered = PROMPT.format(prefix="/gmail")
|
||||
assert "grep" in rendered
|
||||
assert "binary" in rendered.lower()
|
||||
|
||||
|
||||
def test_write_prompt_examples_match_actual_signatures():
|
||||
assert "gws-gmail-send" in WRITE_PROMPT
|
||||
assert "--to" in WRITE_PROMPT
|
||||
assert "--subject" in WRITE_PROMPT
|
||||
assert "--body" in WRITE_PROMPT
|
||||
assert "gws-gmail-reply" in WRITE_PROMPT
|
||||
assert "gws-gmail-reply-all" in WRITE_PROMPT
|
||||
assert "gws-gmail-forward" in WRITE_PROMPT
|
||||
assert "--message-id" in WRITE_PROMPT
|
||||
|
||||
|
||||
def test_write_prompt_documents_rm_and_newline_gotcha():
|
||||
assert "rm " in WRITE_PROMPT
|
||||
assert ".gmail.json" in WRITE_PROMPT
|
||||
assert "Trash" in WRITE_PROMPT
|
||||
assert "$'" in WRITE_PROMPT
|
||||
@@ -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.resource.gmail.config import GmailConfig
|
||||
from mirage.resource.gmail.gmail import GmailResource
|
||||
from mirage.types import ResourceName
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config():
|
||||
return GmailConfig(
|
||||
client_id="test-id",
|
||||
client_secret="test-secret",
|
||||
refresh_token="test-refresh",
|
||||
)
|
||||
|
||||
|
||||
def test_resource_init(config):
|
||||
resource = GmailResource(config=config)
|
||||
assert resource.name == ResourceName.GMAIL
|
||||
assert resource.caches_reads is True
|
||||
|
||||
|
||||
def test_resource_accessor(config):
|
||||
resource = GmailResource(config=config)
|
||||
assert resource.accessor is not None
|
||||
assert resource.accessor.config is config
|
||||
assert resource.accessor.token_manager is not None
|
||||
|
||||
|
||||
def test_resource_commands_registered(config):
|
||||
resource = GmailResource(config=config)
|
||||
cmds = resource.commands()
|
||||
assert len(cmds) > 15
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resource_fingerprint_not_found(config):
|
||||
resource = GmailResource(config=config)
|
||||
fp = await resource.fingerprint("/gmail/nonexistent")
|
||||
assert fp is None
|
||||
@@ -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,48 @@
|
||||
# ========= 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.gsheets.prompt import PROMPT, WRITE_PROMPT
|
||||
|
||||
|
||||
def test_prompt_includes_buckets_and_structure():
|
||||
rendered = PROMPT.format(prefix="/gsheets")
|
||||
assert "owned/" in rendered
|
||||
assert "shared/" in rendered
|
||||
assert "shared with you by others" in rendered
|
||||
assert "still in owned/" in rendered
|
||||
assert "gsheet.json structure" in rendered
|
||||
assert ".sheets[].properties.title" in rendered
|
||||
assert "gws-sheets-read" in rendered
|
||||
|
||||
|
||||
def test_write_prompt_examples_match_actual_signatures():
|
||||
assert "gws-sheets-write" in WRITE_PROMPT
|
||||
assert "--params" in WRITE_PROMPT
|
||||
assert "--json" in WRITE_PROMPT
|
||||
assert "spreadsheetId" in WRITE_PROMPT
|
||||
assert "valueInputOption" in WRITE_PROMPT
|
||||
assert "gws-sheets-append" in WRITE_PROMPT
|
||||
assert "--spreadsheet" in WRITE_PROMPT
|
||||
assert "--range" in WRITE_PROMPT
|
||||
assert "--values" in WRITE_PROMPT
|
||||
assert "--json-values" in WRITE_PROMPT
|
||||
assert "gws-sheets-spreadsheets-create" in WRITE_PROMPT
|
||||
assert '{"properties": {"title":' in WRITE_PROMPT
|
||||
assert "gws-sheets-spreadsheets-batchUpdate" in WRITE_PROMPT
|
||||
assert "spreadsheetId" in WRITE_PROMPT
|
||||
|
||||
|
||||
def test_write_prompt_documents_rm():
|
||||
assert "rm " in WRITE_PROMPT
|
||||
assert ".gsheet.json" in WRITE_PROMPT
|
||||
@@ -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,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. =========
|
||||
|
||||
from mirage.resource.gslides.prompt import PROMPT, WRITE_PROMPT
|
||||
|
||||
|
||||
def test_prompt_includes_buckets_and_structure():
|
||||
rendered = PROMPT.format(prefix="/gslides")
|
||||
assert "owned/" in rendered
|
||||
assert "shared/" in rendered
|
||||
assert "shared with you by others" in rendered
|
||||
assert "still in owned/" in rendered
|
||||
assert "gslide.json structure" in rendered
|
||||
text_path = (".slides[].pageElements[].shape.text"
|
||||
".textElements[].textRun.content")
|
||||
assert text_path in rendered
|
||||
|
||||
|
||||
def test_write_prompt_examples_match_actual_signatures():
|
||||
assert "gws-slides-presentations-create" in WRITE_PROMPT
|
||||
assert "--json" in WRITE_PROMPT
|
||||
assert '{"title":' in WRITE_PROMPT
|
||||
assert "gws-slides-presentations-batchUpdate" in WRITE_PROMPT
|
||||
assert "presentationId" in WRITE_PROMPT
|
||||
|
||||
|
||||
def test_write_prompt_documents_rm():
|
||||
assert "rm " in WRITE_PROMPT
|
||||
assert ".gslide.json" in WRITE_PROMPT
|
||||
@@ -0,0 +1,88 @@
|
||||
# ========= 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.observe.log_entry import EVENT_COMMAND, LogEntry
|
||||
from mirage.observe.observer import Observer
|
||||
from mirage.resource.history import HistoryViewResource
|
||||
from mirage.types import FileType, MountMode
|
||||
from mirage.workspace.mount.registry import MountRegistry
|
||||
|
||||
|
||||
def _observer_with(commands: list[tuple[str, str]]) -> Observer:
|
||||
obs = Observer()
|
||||
for i, (cmd, session) in enumerate(commands):
|
||||
entry = LogEntry(
|
||||
type=EVENT_COMMAND,
|
||||
agent="a",
|
||||
session=session,
|
||||
timestamp=(i + 1) * 1000,
|
||||
command=cmd,
|
||||
exit_code=0,
|
||||
)
|
||||
asyncio.run(obs._log(entry))
|
||||
return obs
|
||||
|
||||
|
||||
def _mounted(obs: Observer):
|
||||
registry = MountRegistry()
|
||||
registry.mount("/.bash_history", HistoryViewResource(obs), MountMode.READ)
|
||||
return registry.mount_for("/.bash_history")
|
||||
|
||||
|
||||
def test_read_op_renders_gnu_file():
|
||||
mount = _mounted(_observer_with([("ls /data", "s1"), ("pwd", "s2")]))
|
||||
data = asyncio.run(mount.execute_op("read", "/.bash_history"))
|
||||
assert data == b"#1\nls /data\n#2\npwd\n"
|
||||
|
||||
|
||||
def test_read_reflects_new_events_without_invalidation():
|
||||
obs = _observer_with([("ls /a", "s1")])
|
||||
mount = _mounted(obs)
|
||||
first = asyncio.run(mount.execute_op("read", "/.bash_history"))
|
||||
asyncio.run(
|
||||
obs._log(
|
||||
LogEntry(
|
||||
type=EVENT_COMMAND,
|
||||
agent="a",
|
||||
session="s1",
|
||||
timestamp=2000,
|
||||
command="pwd",
|
||||
exit_code=0,
|
||||
)))
|
||||
second = asyncio.run(mount.execute_op("read", "/.bash_history"))
|
||||
assert first != second
|
||||
assert second.endswith(b"#2\npwd\n")
|
||||
|
||||
|
||||
def test_stat_op_reports_file():
|
||||
mount = _mounted(_observer_with([("ls /a", "s1")]))
|
||||
st = asyncio.run(mount.execute_op("stat", "/.bash_history"))
|
||||
assert st.type != FileType.DIRECTORY
|
||||
assert st.size == len(b"#1\nls /a\n")
|
||||
|
||||
|
||||
def test_write_op_not_registered():
|
||||
mount = _mounted(_observer_with([]))
|
||||
with pytest.raises(AttributeError):
|
||||
asyncio.run(mount.execute_op("write", "/.bash_history", b"x"))
|
||||
|
||||
|
||||
def test_other_paths_not_found():
|
||||
mount = _mounted(_observer_with([("ls /a", "s1")]))
|
||||
with pytest.raises(FileNotFoundError):
|
||||
asyncio.run(mount.execute_op("read", "/.bash_history/nope"))
|
||||
@@ -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. =========
|
||||
|
||||
from mirage.resource.lancedb import LanceDBConfig, LanceDBResource
|
||||
from mirage.resource.registry import REGISTRY, build_resource
|
||||
|
||||
|
||||
def _resource(**kw) -> LanceDBResource:
|
||||
base = dict(uri="/tmp/db", group_by=["label"], id_column="id")
|
||||
base.update(kw)
|
||||
return LanceDBResource(LanceDBConfig(**base))
|
||||
|
||||
|
||||
def test_resource_name_local_is_not_remote():
|
||||
res = _resource()
|
||||
assert res.name == "lancedb"
|
||||
assert res.caches_reads is False
|
||||
|
||||
|
||||
def test_resource_remote_uri_caches_reads():
|
||||
res = _resource(uri="s3://bucket/db")
|
||||
assert res.caches_reads is True
|
||||
|
||||
|
||||
def test_resource_registers_ops():
|
||||
res = _resource()
|
||||
assert {"read", "readdir", "stat"} <= {o.name for o in res.ops_list()}
|
||||
|
||||
|
||||
def test_resource_registers_commands():
|
||||
res = _resource()
|
||||
expected = {
|
||||
"cat", "find", "grep", "head", "ls", "rg", "stat", "tail", "tree", "wc"
|
||||
}
|
||||
assert expected <= {c.name for c in res.commands()}
|
||||
|
||||
|
||||
def test_resource_in_registry():
|
||||
assert "lancedb" in REGISTRY
|
||||
res = build_resource("lancedb", {"uri": "/tmp/db"})
|
||||
assert res.name == "lancedb"
|
||||
|
||||
|
||||
def test_resource_get_state_redacts_api_key():
|
||||
res = _resource(api_key="secret-value")
|
||||
state = res.get_state()
|
||||
assert "secret-value" not in str(state)
|
||||
|
||||
|
||||
def test_cloud_config_fields_and_remote():
|
||||
res = _resource(uri="db://my-db",
|
||||
api_key="sk-xxx",
|
||||
region="us-west-2",
|
||||
host_override="https://my-db.region.api.lancedb.com")
|
||||
assert res.caches_reads is True
|
||||
assert res.config.region == "us-west-2"
|
||||
assert res.config.host_override == "https://my-db.region.api.lancedb.com"
|
||||
@@ -0,0 +1,50 @@
|
||||
# ========= 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.linear.prompt import PROMPT, WRITE_PROMPT
|
||||
|
||||
|
||||
def test_prompt_includes_path_anatomy_and_normalized_shapes():
|
||||
rendered = PROMPT.format(prefix="/linear")
|
||||
assert "team.json:" in rendered
|
||||
assert "issue.json:" in rendered
|
||||
assert "comments.jsonl:" in rendered
|
||||
assert "project.json:" in rendered
|
||||
assert "cycle.json:" in rendered
|
||||
assert "user.json:" in rendered
|
||||
assert "mirage-normalized" in rendered
|
||||
assert ".issue_key" in rendered
|
||||
assert ".label_names[]" in rendered
|
||||
assert "linear-search" in rendered
|
||||
assert "0=none 1=urgent" in rendered
|
||||
|
||||
|
||||
def test_write_prompt_examples_match_actual_signatures():
|
||||
assert "linear-issue-create" in WRITE_PROMPT
|
||||
assert "linear-issue-update" in WRITE_PROMPT
|
||||
assert "linear-issue-assign" in WRITE_PROMPT
|
||||
assert "linear-issue-transition" in WRITE_PROMPT
|
||||
assert "linear-issue-set-priority" in WRITE_PROMPT
|
||||
assert "linear-issue-set-project" in WRITE_PROMPT
|
||||
assert "linear-issue-add-label" in WRITE_PROMPT
|
||||
assert "linear-issue-comment-add" in WRITE_PROMPT
|
||||
assert "linear-issue-comment-update" in WRITE_PROMPT
|
||||
assert "--team_id" in WRITE_PROMPT
|
||||
assert "--issue_key" in WRITE_PROMPT
|
||||
assert "--assignee_email" in WRITE_PROMPT
|
||||
assert "--state_name" in WRITE_PROMPT
|
||||
assert "--priority" in WRITE_PROMPT
|
||||
assert "--body_file" in WRITE_PROMPT
|
||||
assert "--description_file" in WRITE_PROMPT
|
||||
assert "UNDERSCORES" in WRITE_PROMPT
|
||||
@@ -0,0 +1,51 @@
|
||||
# ========= 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.ram import RAMIndexCacheStore
|
||||
from mirage.resource.linear.config import LinearConfig
|
||||
from mirage.resource.linear.linear import LinearResource
|
||||
from mirage.types import ResourceName
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config():
|
||||
return LinearConfig(api_key="lin_api_test")
|
||||
|
||||
|
||||
def test_resource_init(config):
|
||||
resource = LinearResource(config)
|
||||
assert resource.caches_reads is True
|
||||
|
||||
|
||||
def test_resource_name(config):
|
||||
resource = LinearResource(config)
|
||||
assert resource.name == ResourceName.LINEAR
|
||||
|
||||
|
||||
def test_resource_accessor(config):
|
||||
resource = LinearResource(config)
|
||||
assert resource.accessor is not None
|
||||
assert resource.accessor.config is config
|
||||
|
||||
|
||||
def test_resource_has_index(config):
|
||||
resource = LinearResource(config)
|
||||
assert isinstance(resource._index, RAMIndexCacheStore)
|
||||
|
||||
|
||||
def test_resource_commands_registered(config):
|
||||
resource = LinearResource(config)
|
||||
assert len(resource._commands) >= 10
|
||||
@@ -0,0 +1,88 @@
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from mirage.resource.nextcloud import NextcloudConfig, NextcloudResource
|
||||
|
||||
|
||||
def test_nextcloudconfig_defaults():
|
||||
c = NextcloudConfig(
|
||||
url="https://cloud.example.com/remote.php/dav/files/user/")
|
||||
assert c.username is None
|
||||
assert c.password is None
|
||||
assert c.verify_ssl is True
|
||||
assert c.timeout == 30
|
||||
|
||||
|
||||
def test_nextcloudconfig_immutable():
|
||||
c = NextcloudConfig(
|
||||
url="https://cloud.example.com/remote.php/dav/files/user/")
|
||||
with pytest.raises(ValidationError):
|
||||
c.url = "https://other.example.com/"
|
||||
|
||||
|
||||
def test_nextcloudconfig_with_credentials():
|
||||
c = NextcloudConfig(
|
||||
url="https://cloud.example.com/remote.php/dav/files/user/",
|
||||
username="alice",
|
||||
password="secret",
|
||||
verify_ssl=False,
|
||||
)
|
||||
assert c.username == "alice"
|
||||
assert c.password == "secret"
|
||||
assert c.verify_ssl is False
|
||||
|
||||
|
||||
def test_nextcloud_write_commands_tagged():
|
||||
from mirage.commands.builtin.nextcloud import COMMANDS
|
||||
write_names = {
|
||||
"cp", "csplit", "gunzip", "gzip", "iconv", "ln", "mkdir", "mktemp",
|
||||
"mv", "patch", "rm", "split", "tar", "tee", "touch", "unzip", "zip"
|
||||
}
|
||||
for fn in COMMANDS:
|
||||
for rc in fn._registered_commands:
|
||||
if rc.name in write_names:
|
||||
assert rc.write is True, f"{rc.name} should be write=True"
|
||||
else:
|
||||
assert rc.write is False, f"{rc.name} should be write=False"
|
||||
|
||||
|
||||
def test_nextcloud_write_ops_tagged():
|
||||
from mirage.ops.nextcloud import OPS
|
||||
write_op_names = {
|
||||
"write", "unlink", "rmdir", "mkdir", "create", "truncate", "rename"
|
||||
}
|
||||
for fn in OPS:
|
||||
for ro in fn._registered_ops:
|
||||
if ro.name in write_op_names:
|
||||
assert ro.write is True, f"op {ro.name} should be write=True"
|
||||
else:
|
||||
assert ro.write is False, f"op {ro.name} should be write=False"
|
||||
|
||||
|
||||
def test_nextcloud_resource_registers_commands():
|
||||
config = NextcloudConfig(
|
||||
url="https://cloud.example.com/remote.php/dav/files/user/")
|
||||
resource = NextcloudResource(config)
|
||||
command_names = {rc.name for rc in resource._commands}
|
||||
assert "ls" in command_names
|
||||
assert "cat" in command_names
|
||||
assert "grep" in command_names
|
||||
assert "find" in command_names
|
||||
assert "mkdir" in command_names
|
||||
assert "rm" in command_names
|
||||
|
||||
|
||||
def test_nextcloud_resource_get_state():
|
||||
config = NextcloudConfig(
|
||||
url="https://cloud.example.com/remote.php/dav/files/user/",
|
||||
username="alice",
|
||||
password="secret",
|
||||
)
|
||||
resource = NextcloudResource(config)
|
||||
state = resource.get_state()
|
||||
assert state["type"] == "nextcloud"
|
||||
assert state["needs_override"] is True
|
||||
assert state["config"]["password"] == "<REDACTED>"
|
||||
assert state["config"]["username"] == "alice"
|
||||
assert state["config"][
|
||||
"url"] == "https://cloud.example.com/remote.php/dav/files/user/"
|
||||
@@ -0,0 +1,65 @@
|
||||
# ========= 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 pydantic import ValidationError
|
||||
|
||||
from mirage.resource.aliyun import AliyunConfig, AliyunResource
|
||||
from mirage.resource.s3 import S3Config
|
||||
from mirage.resource.secrets import reveal_secret
|
||||
from mirage.types import ResourceName
|
||||
|
||||
|
||||
def test_aliyun_regional_endpoint():
|
||||
config = AliyunConfig(bucket="b",
|
||||
region="cn-hangzhou",
|
||||
access_key_id="k",
|
||||
secret_access_key="s")
|
||||
assert config.resolved_endpoint_url() == (
|
||||
"https://s3.oss-cn-hangzhou.aliyuncs.com")
|
||||
|
||||
|
||||
def test_aliyun_requires_region():
|
||||
with pytest.raises(ValidationError):
|
||||
AliyunConfig(bucket="b", access_key_id="k", secret_access_key="s")
|
||||
|
||||
|
||||
def test_aliyun_custom_endpoint_override():
|
||||
config = AliyunConfig(bucket="b",
|
||||
region="cn-beijing",
|
||||
endpoint_url="https://custom.example.com",
|
||||
access_key_id="k",
|
||||
secret_access_key="s")
|
||||
assert config.resolved_endpoint_url() == "https://custom.example.com"
|
||||
|
||||
|
||||
def test_aliyun_to_s3_config():
|
||||
config = AliyunConfig(bucket="b",
|
||||
region="us-west-1",
|
||||
access_key_id="key",
|
||||
secret_access_key="secret")
|
||||
s3 = config.to_s3_config()
|
||||
assert isinstance(s3, S3Config)
|
||||
assert s3.endpoint_url == "https://s3.oss-us-west-1.aliyuncs.com"
|
||||
assert reveal_secret(s3.aws_secret_access_key) == "secret"
|
||||
|
||||
|
||||
def test_aliyun_resource_uses_s3_resource_type():
|
||||
resource = AliyunResource(
|
||||
AliyunConfig(bucket="b",
|
||||
region="cn-hangzhou",
|
||||
access_key_id="k",
|
||||
secret_access_key="s"))
|
||||
assert resource.name == ResourceName.S3
|
||||
assert isinstance(resource.config, S3Config)
|
||||
@@ -0,0 +1,65 @@
|
||||
# ========= 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 pydantic import ValidationError
|
||||
|
||||
from mirage.resource.backblaze import BackblazeConfig, BackblazeResource
|
||||
from mirage.resource.s3 import S3Config
|
||||
from mirage.resource.secrets import reveal_secret
|
||||
from mirage.types import ResourceName
|
||||
|
||||
|
||||
def test_backblaze_regional_endpoint():
|
||||
config = BackblazeConfig(bucket="b",
|
||||
region="us-west-002",
|
||||
access_key_id="k",
|
||||
secret_access_key="s")
|
||||
assert config.resolved_endpoint_url() == (
|
||||
"https://s3.us-west-002.backblazeb2.com")
|
||||
|
||||
|
||||
def test_backblaze_requires_region():
|
||||
with pytest.raises(ValidationError):
|
||||
BackblazeConfig(bucket="b", access_key_id="k", secret_access_key="s")
|
||||
|
||||
|
||||
def test_backblaze_custom_endpoint_override():
|
||||
config = BackblazeConfig(bucket="b",
|
||||
region="us-west-002",
|
||||
endpoint_url="https://custom.example.com",
|
||||
access_key_id="k",
|
||||
secret_access_key="s")
|
||||
assert config.resolved_endpoint_url() == "https://custom.example.com"
|
||||
|
||||
|
||||
def test_backblaze_to_s3_config():
|
||||
config = BackblazeConfig(bucket="b",
|
||||
region="eu-central-003",
|
||||
access_key_id="key",
|
||||
secret_access_key="secret")
|
||||
s3 = config.to_s3_config()
|
||||
assert isinstance(s3, S3Config)
|
||||
assert s3.endpoint_url == "https://s3.eu-central-003.backblazeb2.com"
|
||||
assert reveal_secret(s3.aws_secret_access_key) == "secret"
|
||||
|
||||
|
||||
def test_backblaze_resource_uses_s3_resource_type():
|
||||
resource = BackblazeResource(
|
||||
BackblazeConfig(bucket="b",
|
||||
region="us-west-002",
|
||||
access_key_id="k",
|
||||
secret_access_key="s"))
|
||||
assert resource.name == ResourceName.S3
|
||||
assert isinstance(resource.config, S3Config)
|
||||
@@ -0,0 +1,71 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from mirage.resource.ceph import CephConfig, CephResource
|
||||
from mirage.resource.s3 import S3Config
|
||||
from mirage.resource.secrets import reveal_secret
|
||||
from mirage.types import ResourceName
|
||||
|
||||
|
||||
def test_ceph_config_defaults():
|
||||
config = CephConfig(
|
||||
bucket="my-bucket",
|
||||
endpoint_url="https://ceph.example.com",
|
||||
access_key_id="k",
|
||||
secret_access_key="s",
|
||||
)
|
||||
assert config.region == "us-east-1"
|
||||
assert config.path_style is True
|
||||
|
||||
|
||||
def test_ceph_config_immutable():
|
||||
config = CephConfig(
|
||||
bucket="my-bucket",
|
||||
endpoint_url="https://ceph.example.com",
|
||||
access_key_id="k",
|
||||
secret_access_key="s",
|
||||
)
|
||||
with pytest.raises(ValidationError):
|
||||
config.bucket = "other"
|
||||
|
||||
|
||||
def test_ceph_config_to_s3_config():
|
||||
config = CephConfig(
|
||||
bucket="my-bucket",
|
||||
endpoint_url="https://ceph.example.com",
|
||||
access_key_id="key",
|
||||
secret_access_key="secret",
|
||||
)
|
||||
s3 = config.to_s3_config()
|
||||
assert isinstance(s3, S3Config)
|
||||
assert s3.endpoint_url == "https://ceph.example.com"
|
||||
assert s3.path_style is True
|
||||
assert reveal_secret(s3.aws_access_key_id) == "key"
|
||||
assert reveal_secret(s3.aws_secret_access_key) == "secret"
|
||||
|
||||
|
||||
def test_ceph_resource_uses_s3_resource_type():
|
||||
resource = CephResource(
|
||||
CephConfig(
|
||||
bucket="my-bucket",
|
||||
endpoint_url="https://ceph.example.com",
|
||||
access_key_id="k",
|
||||
secret_access_key="s",
|
||||
))
|
||||
assert resource.name == ResourceName.S3
|
||||
assert isinstance(resource.config, S3Config)
|
||||
assert resource.ceph_config.endpoint_url == "https://ceph.example.com"
|
||||
@@ -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 pydantic import ValidationError
|
||||
|
||||
from mirage.resource.digitalocean import (DigitalOceanConfig,
|
||||
DigitalOceanResource)
|
||||
from mirage.resource.s3 import S3Config
|
||||
from mirage.resource.secrets import reveal_secret
|
||||
from mirage.types import ResourceName
|
||||
|
||||
|
||||
def test_do_regional_endpoint():
|
||||
config = DigitalOceanConfig(bucket="b",
|
||||
region="nyc3",
|
||||
access_key_id="k",
|
||||
secret_access_key="s")
|
||||
assert config.resolved_endpoint_url() == (
|
||||
"https://nyc3.digitaloceanspaces.com")
|
||||
|
||||
|
||||
def test_do_requires_region():
|
||||
with pytest.raises(ValidationError):
|
||||
DigitalOceanConfig(bucket="b",
|
||||
access_key_id="k",
|
||||
secret_access_key="s")
|
||||
|
||||
|
||||
def test_do_custom_endpoint_override():
|
||||
config = DigitalOceanConfig(bucket="b",
|
||||
region="nyc3",
|
||||
endpoint_url="https://custom.example.com",
|
||||
access_key_id="k",
|
||||
secret_access_key="s")
|
||||
assert config.resolved_endpoint_url() == "https://custom.example.com"
|
||||
|
||||
|
||||
def test_do_to_s3_config():
|
||||
config = DigitalOceanConfig(bucket="b",
|
||||
region="fra1",
|
||||
access_key_id="key",
|
||||
secret_access_key="secret")
|
||||
s3 = config.to_s3_config()
|
||||
assert isinstance(s3, S3Config)
|
||||
assert s3.endpoint_url == "https://fra1.digitaloceanspaces.com"
|
||||
assert reveal_secret(s3.aws_access_key_id) == "key"
|
||||
|
||||
|
||||
def test_do_resource_uses_s3_resource_type():
|
||||
resource = DigitalOceanResource(
|
||||
DigitalOceanConfig(bucket="b",
|
||||
region="sfo3",
|
||||
access_key_id="k",
|
||||
secret_access_key="s"))
|
||||
assert resource.name == ResourceName.S3
|
||||
assert isinstance(resource.config, S3Config)
|
||||
@@ -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. =========
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from mirage.resource.gcs import GCSConfig, GCSResource
|
||||
from mirage.resource.secrets import reveal_secret
|
||||
|
||||
|
||||
def test_gcs_config_defaults():
|
||||
c = GCSConfig(
|
||||
bucket="my-bucket",
|
||||
access_key_id="GOOG123",
|
||||
secret_access_key="secret",
|
||||
)
|
||||
assert c.endpoint_url == "https://storage.googleapis.com"
|
||||
assert c.region == "auto"
|
||||
assert c.timeout == 30
|
||||
assert c.proxy is None
|
||||
|
||||
|
||||
def test_gcs_config_immutable():
|
||||
c = GCSConfig(
|
||||
bucket="x",
|
||||
access_key_id="GOOG123",
|
||||
secret_access_key="secret",
|
||||
)
|
||||
with pytest.raises(ValidationError):
|
||||
c.bucket = "y"
|
||||
|
||||
|
||||
def test_gcs_config_custom_endpoint():
|
||||
c = GCSConfig(
|
||||
bucket="my-bucket",
|
||||
access_key_id="GOOG123",
|
||||
secret_access_key="secret",
|
||||
endpoint_url="https://custom.endpoint.com",
|
||||
)
|
||||
assert c.endpoint_url == "https://custom.endpoint.com"
|
||||
|
||||
|
||||
def test_gcs_to_s3_config():
|
||||
c = GCSConfig(
|
||||
bucket="my-bucket",
|
||||
access_key_id="GOOG123",
|
||||
secret_access_key="secret",
|
||||
)
|
||||
s3c = c.to_s3_config()
|
||||
assert s3c.bucket == "my-bucket"
|
||||
assert reveal_secret(s3c.aws_access_key_id) == "GOOG123"
|
||||
assert reveal_secret(s3c.aws_secret_access_key) == "secret"
|
||||
assert s3c.endpoint_url == "https://storage.googleapis.com"
|
||||
assert s3c.region == "auto"
|
||||
|
||||
|
||||
def test_gcs_resource_creates():
|
||||
c = GCSConfig(
|
||||
bucket="my-bucket",
|
||||
access_key_id="GOOG123",
|
||||
secret_access_key="secret",
|
||||
)
|
||||
resource = GCSResource(c)
|
||||
assert resource.gcs_config is c
|
||||
assert resource.accessor.config.bucket == "my-bucket"
|
||||
expected = "https://storage.googleapis.com"
|
||||
assert resource.accessor.config.endpoint_url == expected
|
||||
@@ -0,0 +1,44 @@
|
||||
# ========= 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 pydantic import ValidationError
|
||||
|
||||
from mirage.resource.hf_buckets import HfBucketsConfig, HfBucketsResource
|
||||
from mirage.types import ResourceName
|
||||
|
||||
|
||||
def test_resource_name():
|
||||
r = HfBucketsResource(HfBucketsConfig(bucket="o/b"))
|
||||
assert r.name == ResourceName.HF_BUCKETS
|
||||
assert r.caches_reads is True
|
||||
assert r.SUPPORTS_SNAPSHOT is True
|
||||
|
||||
|
||||
def test_config_immutable():
|
||||
cfg = HfBucketsConfig(bucket="o/b")
|
||||
with pytest.raises(ValidationError):
|
||||
cfg.bucket = "other/other"
|
||||
|
||||
|
||||
def test_resource_registers_ops():
|
||||
r = HfBucketsResource(HfBucketsConfig(bucket="o/b"))
|
||||
op_names = {o.name for o in r.ops_list()}
|
||||
assert {"read", "readdir", "stat", "write", "create", "unlink"} <= op_names
|
||||
|
||||
|
||||
def test_resource_registers_commands():
|
||||
r = HfBucketsResource(HfBucketsConfig(bucket="o/b"))
|
||||
cmd_names = {c.name for c in r.commands()}
|
||||
assert {"cat", "ls", "grep", "stat", "touch", "rm"} <= cmd_names
|
||||
@@ -0,0 +1,44 @@
|
||||
# ========= 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 pydantic import ValidationError
|
||||
|
||||
from mirage.resource.hf_datasets import HfDatasetsConfig, HfDatasetsResource
|
||||
from mirage.types import ResourceName
|
||||
|
||||
|
||||
def test_resource_name():
|
||||
r = HfDatasetsResource(HfDatasetsConfig(repo_id="org/dataset"))
|
||||
assert r.name == ResourceName.HF_DATASETS
|
||||
assert r.caches_reads is True
|
||||
assert r.SUPPORTS_SNAPSHOT is True
|
||||
|
||||
|
||||
def test_config_immutable():
|
||||
cfg = HfDatasetsConfig(repo_id="org/dataset")
|
||||
with pytest.raises(ValidationError):
|
||||
cfg.repo_id = "other/other"
|
||||
|
||||
|
||||
def test_resource_registers_ops():
|
||||
r = HfDatasetsResource(HfDatasetsConfig(repo_id="org/dataset"))
|
||||
op_names = {o.name for o in r.ops_list()}
|
||||
assert {"read", "readdir", "stat", "write", "create", "unlink"} <= op_names
|
||||
|
||||
|
||||
def test_resource_registers_commands():
|
||||
r = HfDatasetsResource(HfDatasetsConfig(repo_id="org/dataset"))
|
||||
cmd_names = {c.name for c in r.commands()}
|
||||
assert {"cat", "ls", "grep", "stat", "touch", "rm"} <= cmd_names
|
||||
@@ -0,0 +1,39 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from mirage.resource.hf_models import HfModelsConfig, HfModelsResource
|
||||
from mirage.types import ResourceName
|
||||
|
||||
|
||||
def test_resource_name():
|
||||
r = HfModelsResource(HfModelsConfig(repo_id="org/model"))
|
||||
assert r.name == ResourceName.HF_MODELS
|
||||
assert r.caches_reads is True
|
||||
|
||||
|
||||
def test_config_immutable():
|
||||
cfg = HfModelsConfig(repo_id="org/model")
|
||||
with pytest.raises(ValidationError):
|
||||
cfg.repo_id = "other/other"
|
||||
|
||||
|
||||
def test_resource_registers_ops_and_commands():
|
||||
r = HfModelsResource(HfModelsConfig(repo_id="org/model"))
|
||||
op_names = {o.name for o in r.ops_list()}
|
||||
cmd_names = {c.name for c in r.commands()}
|
||||
assert {"read", "readdir", "stat"} <= op_names
|
||||
assert {"cat", "ls", "stat"} <= cmd_names
|
||||
@@ -0,0 +1,39 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from mirage.resource.hf_spaces import HfSpacesConfig, HfSpacesResource
|
||||
from mirage.types import ResourceName
|
||||
|
||||
|
||||
def test_resource_name():
|
||||
r = HfSpacesResource(HfSpacesConfig(repo_id="org/space"))
|
||||
assert r.name == ResourceName.HF_SPACES
|
||||
assert r.caches_reads is True
|
||||
|
||||
|
||||
def test_config_immutable():
|
||||
cfg = HfSpacesConfig(repo_id="org/space")
|
||||
with pytest.raises(ValidationError):
|
||||
cfg.repo_id = "other/other"
|
||||
|
||||
|
||||
def test_resource_registers_ops_and_commands():
|
||||
r = HfSpacesResource(HfSpacesConfig(repo_id="org/space"))
|
||||
op_names = {o.name for o in r.ops_list()}
|
||||
cmd_names = {c.name for c in r.commands()}
|
||||
assert {"read", "readdir", "stat"} <= op_names
|
||||
assert {"cat", "ls", "stat"} <= cmd_names
|
||||
@@ -0,0 +1,97 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from mirage.resource.minio import MinIOConfig, MinIOResource
|
||||
from mirage.resource.s3 import S3Config
|
||||
from mirage.resource.secrets import reveal_secret
|
||||
from mirage.types import ResourceName
|
||||
|
||||
|
||||
def test_minio_config_defaults():
|
||||
config = MinIOConfig(
|
||||
bucket="my-bucket",
|
||||
endpoint_url="http://localhost:9000",
|
||||
access_key_id="minioadmin",
|
||||
secret_access_key="minioadmin",
|
||||
)
|
||||
assert config.region == "us-east-1"
|
||||
assert config.path_style is True
|
||||
assert config.timeout == 30
|
||||
|
||||
|
||||
def test_minio_config_immutable():
|
||||
config = MinIOConfig(
|
||||
bucket="my-bucket",
|
||||
endpoint_url="http://localhost:9000",
|
||||
access_key_id="k",
|
||||
secret_access_key="s",
|
||||
)
|
||||
with pytest.raises(ValidationError):
|
||||
config.bucket = "other"
|
||||
|
||||
|
||||
def test_minio_config_to_s3_config():
|
||||
config = MinIOConfig(
|
||||
bucket="my-bucket",
|
||||
endpoint_url="http://localhost:9000",
|
||||
access_key_id="minioadmin",
|
||||
secret_access_key="minioadmin",
|
||||
proxy="http://localhost:8080",
|
||||
)
|
||||
s3 = config.to_s3_config()
|
||||
assert isinstance(s3, S3Config)
|
||||
assert s3.bucket == "my-bucket"
|
||||
assert s3.endpoint_url == "http://localhost:9000"
|
||||
assert s3.path_style is True
|
||||
assert reveal_secret(s3.aws_access_key_id) == "minioadmin"
|
||||
assert reveal_secret(s3.aws_secret_access_key) == "minioadmin"
|
||||
assert s3.proxy == "http://localhost:8080"
|
||||
|
||||
|
||||
def test_minio_config_path_style_override():
|
||||
config = MinIOConfig(
|
||||
bucket="my-bucket",
|
||||
endpoint_url="https://minio.example.com",
|
||||
access_key_id="k",
|
||||
secret_access_key="s",
|
||||
path_style=False,
|
||||
)
|
||||
assert config.to_s3_config().path_style is False
|
||||
|
||||
|
||||
def test_minio_resource_uses_s3_resource_type():
|
||||
resource = MinIOResource(
|
||||
MinIOConfig(
|
||||
bucket="my-bucket",
|
||||
endpoint_url="http://localhost:9000",
|
||||
access_key_id="k",
|
||||
secret_access_key="s",
|
||||
))
|
||||
assert resource.name == ResourceName.S3
|
||||
assert resource.caches_reads is True
|
||||
assert isinstance(resource.config, S3Config)
|
||||
|
||||
|
||||
def test_minio_resource_preserves_original_config():
|
||||
config = MinIOConfig(
|
||||
bucket="my-bucket",
|
||||
endpoint_url="http://localhost:9000",
|
||||
access_key_id="k",
|
||||
secret_access_key="s",
|
||||
)
|
||||
resource = MinIOResource(config)
|
||||
assert resource.minio_config is config
|
||||
@@ -0,0 +1,108 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from mirage.resource.oci import OCIConfig, OCIResource
|
||||
from mirage.resource.s3 import S3Config
|
||||
from mirage.resource.secrets import reveal_secret
|
||||
from mirage.types import ResourceName
|
||||
|
||||
|
||||
def test_oci_config_defaults():
|
||||
config = OCIConfig(
|
||||
bucket="my-bucket",
|
||||
namespace="my-namespace",
|
||||
region="us-ashburn-1",
|
||||
access_key_id="access-key",
|
||||
secret_access_key="secret-key",
|
||||
)
|
||||
assert config.timeout == 30
|
||||
assert config.resolved_endpoint_url() == (
|
||||
"https://my-namespace.compat.objectstorage."
|
||||
"us-ashburn-1.oci.customer-oci.com")
|
||||
|
||||
|
||||
def test_oci_config_immutable():
|
||||
config = OCIConfig(
|
||||
bucket="my-bucket",
|
||||
namespace="my-namespace",
|
||||
region="us-ashburn-1",
|
||||
access_key_id="access-key",
|
||||
secret_access_key="secret-key",
|
||||
)
|
||||
with pytest.raises(ValidationError):
|
||||
config.bucket = "other-bucket"
|
||||
|
||||
|
||||
def test_oci_config_to_s3_config():
|
||||
config = OCIConfig(
|
||||
bucket="my-bucket",
|
||||
namespace="my-namespace",
|
||||
region="us-ashburn-1",
|
||||
access_key_id="access-key",
|
||||
secret_access_key="secret-key",
|
||||
proxy="http://localhost:8080",
|
||||
)
|
||||
s3_config = config.to_s3_config()
|
||||
assert isinstance(s3_config, S3Config)
|
||||
assert s3_config.bucket == "my-bucket"
|
||||
assert s3_config.region == "us-ashburn-1"
|
||||
assert s3_config.endpoint_url == (
|
||||
"https://my-namespace.compat.objectstorage."
|
||||
"us-ashburn-1.oci.customer-oci.com")
|
||||
assert reveal_secret(s3_config.aws_access_key_id) == "access-key"
|
||||
assert reveal_secret(s3_config.aws_secret_access_key) == "secret-key"
|
||||
assert s3_config.path_style is True
|
||||
assert s3_config.proxy == "http://localhost:8080"
|
||||
|
||||
|
||||
def test_oci_config_custom_endpoint():
|
||||
config = OCIConfig(
|
||||
bucket="my-bucket",
|
||||
namespace="my-namespace",
|
||||
region="us-ashburn-1",
|
||||
endpoint_url="https://custom.example.com",
|
||||
access_key_id="access-key",
|
||||
secret_access_key="secret-key",
|
||||
)
|
||||
assert config.resolved_endpoint_url() == "https://custom.example.com"
|
||||
|
||||
|
||||
def test_oci_resource_uses_s3_resource_type():
|
||||
resource = OCIResource(
|
||||
OCIConfig(
|
||||
bucket="my-bucket",
|
||||
namespace="my-namespace",
|
||||
region="us-ashburn-1",
|
||||
access_key_id="access-key",
|
||||
secret_access_key="secret-key",
|
||||
))
|
||||
assert resource.name == ResourceName.S3
|
||||
assert resource.caches_reads is True
|
||||
assert isinstance(resource.config, S3Config)
|
||||
assert resource.config.path_style is True
|
||||
|
||||
|
||||
def test_oci_resource_preserves_original_config():
|
||||
config = OCIConfig(
|
||||
bucket="my-bucket",
|
||||
namespace="my-namespace",
|
||||
region="us-ashburn-1",
|
||||
access_key_id="access-key",
|
||||
secret_access_key="secret-key",
|
||||
)
|
||||
resource = OCIResource(config)
|
||||
assert resource.oci_config is config
|
||||
@@ -0,0 +1,64 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from mirage.resource.qingstor import QingStorConfig, QingStorResource
|
||||
from mirage.resource.s3 import S3Config
|
||||
from mirage.resource.secrets import reveal_secret
|
||||
from mirage.types import ResourceName
|
||||
|
||||
|
||||
def test_qingstor_zone_endpoint():
|
||||
config = QingStorConfig(bucket="b",
|
||||
region="pek3b",
|
||||
access_key_id="k",
|
||||
secret_access_key="s")
|
||||
assert config.resolved_endpoint_url() == "https://s3.pek3b.qingstor.com"
|
||||
|
||||
|
||||
def test_qingstor_requires_region():
|
||||
with pytest.raises(ValidationError):
|
||||
QingStorConfig(bucket="b", access_key_id="k", secret_access_key="s")
|
||||
|
||||
|
||||
def test_qingstor_custom_endpoint_override():
|
||||
config = QingStorConfig(bucket="b",
|
||||
region="sh1a",
|
||||
endpoint_url="https://custom.example.com",
|
||||
access_key_id="k",
|
||||
secret_access_key="s")
|
||||
assert config.resolved_endpoint_url() == "https://custom.example.com"
|
||||
|
||||
|
||||
def test_qingstor_to_s3_config():
|
||||
config = QingStorConfig(bucket="b",
|
||||
region="gd2a",
|
||||
access_key_id="key",
|
||||
secret_access_key="secret")
|
||||
s3 = config.to_s3_config()
|
||||
assert isinstance(s3, S3Config)
|
||||
assert s3.endpoint_url == "https://s3.gd2a.qingstor.com"
|
||||
assert reveal_secret(s3.aws_access_key_id) == "key"
|
||||
|
||||
|
||||
def test_qingstor_resource_uses_s3_resource_type():
|
||||
resource = QingStorResource(
|
||||
QingStorConfig(bucket="b",
|
||||
region="pek3b",
|
||||
access_key_id="k",
|
||||
secret_access_key="s"))
|
||||
assert resource.name == ResourceName.S3
|
||||
assert isinstance(resource.config, S3Config)
|
||||
@@ -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 pydantic import ValidationError
|
||||
|
||||
from mirage.resource.r2 import R2Config, R2Resource
|
||||
from mirage.resource.s3 import S3Config
|
||||
from mirage.resource.secrets import reveal_secret
|
||||
from mirage.types import ResourceName
|
||||
|
||||
|
||||
def test_r2config_defaults():
|
||||
config = R2Config(bucket="my-bucket", account_id="account-123")
|
||||
assert config.region == "auto"
|
||||
assert config.timeout == 30
|
||||
assert config.resolved_endpoint_url() == (
|
||||
"https://account-123.r2.cloudflarestorage.com")
|
||||
|
||||
|
||||
def test_r2config_immutable():
|
||||
config = R2Config(bucket="my-bucket", account_id="account-123")
|
||||
with pytest.raises(ValidationError):
|
||||
config.bucket = "other-bucket"
|
||||
|
||||
|
||||
def test_r2config_to_s3_config():
|
||||
config = R2Config(
|
||||
bucket="my-bucket",
|
||||
account_id="account-123",
|
||||
access_key_id="access-key",
|
||||
secret_access_key="secret-key",
|
||||
proxy="http://localhost:8080",
|
||||
)
|
||||
s3_config = config.to_s3_config()
|
||||
assert isinstance(s3_config, S3Config)
|
||||
assert s3_config.bucket == "my-bucket"
|
||||
assert s3_config.region == "auto"
|
||||
assert s3_config.endpoint_url == (
|
||||
"https://account-123.r2.cloudflarestorage.com")
|
||||
assert reveal_secret(s3_config.aws_access_key_id) == "access-key"
|
||||
assert reveal_secret(s3_config.aws_secret_access_key) == "secret-key"
|
||||
assert s3_config.proxy == "http://localhost:8080"
|
||||
|
||||
|
||||
def test_r2config_custom_endpoint():
|
||||
config = R2Config(
|
||||
bucket="my-bucket",
|
||||
endpoint_url="https://custom.example.com",
|
||||
)
|
||||
assert config.resolved_endpoint_url() == "https://custom.example.com"
|
||||
|
||||
|
||||
def test_r2config_requires_account_id_or_endpoint():
|
||||
config = R2Config(bucket="my-bucket")
|
||||
with pytest.raises(ValueError):
|
||||
config.resolved_endpoint_url()
|
||||
|
||||
|
||||
def test_r2resource_uses_s3_resource_type():
|
||||
resource = R2Resource(
|
||||
R2Config(bucket="my-bucket", account_id="account-123"))
|
||||
assert resource.name == ResourceName.S3
|
||||
assert resource.caches_reads is True
|
||||
assert isinstance(resource.config, S3Config)
|
||||
assert resource.config.endpoint_url == (
|
||||
"https://account-123.r2.cloudflarestorage.com")
|
||||
|
||||
|
||||
def test_r2resource_preserves_original_config():
|
||||
config = R2Config(bucket="my-bucket", account_id="account-123")
|
||||
resource = R2Resource(config)
|
||||
assert resource.r2_config is config
|
||||
@@ -0,0 +1,79 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from mirage.resource.s3 import S3Config
|
||||
|
||||
|
||||
def test_s3config_defaults():
|
||||
c = S3Config(bucket="my-bucket")
|
||||
assert c.region is None
|
||||
assert c.timeout == 30
|
||||
|
||||
|
||||
def test_s3config_immutable():
|
||||
c = S3Config(bucket="x")
|
||||
with pytest.raises(ValidationError):
|
||||
c.bucket = "y"
|
||||
|
||||
|
||||
def test_s3_write_commands_tagged():
|
||||
from mirage.commands.builtin.s3 import COMMANDS
|
||||
write_names = {
|
||||
"rm",
|
||||
"mkdir",
|
||||
"touch",
|
||||
"cp",
|
||||
"mv",
|
||||
"ln",
|
||||
"tee",
|
||||
"mktemp",
|
||||
"split",
|
||||
"csplit",
|
||||
"gzip",
|
||||
"gunzip",
|
||||
"zip",
|
||||
"unzip",
|
||||
"tar",
|
||||
"patch",
|
||||
"iconv",
|
||||
}
|
||||
for fn in COMMANDS:
|
||||
for rc in fn._registered_commands:
|
||||
if rc.name in write_names:
|
||||
assert rc.write is True, (f"{rc.name} should be write=True")
|
||||
else:
|
||||
assert rc.write is False, (f"{rc.name} should be write=False")
|
||||
|
||||
|
||||
def test_s3_write_ops_tagged():
|
||||
from mirage.ops.s3 import OPS
|
||||
write_op_names = {
|
||||
"write",
|
||||
"unlink",
|
||||
"rmdir",
|
||||
"mkdir",
|
||||
"create",
|
||||
"truncate",
|
||||
"rename",
|
||||
}
|
||||
for fn in OPS:
|
||||
for ro in fn._registered_ops:
|
||||
if ro.name in write_op_names:
|
||||
assert ro.write is True, (f"op {ro.name} should be write=True")
|
||||
else:
|
||||
assert ro.write is False, (
|
||||
f"op {ro.name} should be write=False")
|
||||
@@ -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. =========
|
||||
|
||||
import re
|
||||
from io import BytesIO
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from mirage.resource.s3 import S3Config, S3Resource
|
||||
|
||||
|
||||
def _mock_backend(data: bytes) -> S3Resource:
|
||||
config = S3Config(
|
||||
bucket="test-bucket",
|
||||
region="us-east-1",
|
||||
aws_access_key_id="fake",
|
||||
aws_secret_access_key="fake",
|
||||
)
|
||||
backend = S3Resource.__new__(S3Resource)
|
||||
backend.config = config
|
||||
backend._prefix = ""
|
||||
backend._event_handlers = []
|
||||
|
||||
mock_client = MagicMock()
|
||||
body = BytesIO(data)
|
||||
mock_client.get_object.return_value = {"Body": body}
|
||||
backend._client = mock_client
|
||||
return backend
|
||||
|
||||
|
||||
def test_grep_file_uses_streaming():
|
||||
from mirage.commands.builtin.grep_helper import grep_lines
|
||||
data = b"hello world\nfoo bar\nhello again\n"
|
||||
compiled = re.compile("hello")
|
||||
results = grep_lines("test.txt",
|
||||
data.decode(errors="replace").splitlines(),
|
||||
compiled,
|
||||
invert=False,
|
||||
line_numbers=False,
|
||||
count_only=False,
|
||||
files_only=False,
|
||||
only_matching=False,
|
||||
max_count=None)
|
||||
assert len(results) == 2
|
||||
assert "hello world" in results[0]
|
||||
|
||||
|
||||
def test_grep_file_max_count():
|
||||
from mirage.commands.builtin.grep_helper import grep_lines
|
||||
lines = [f"match line {i}".encode() for i in range(100)]
|
||||
data = b"\n".join(lines) + b"\n"
|
||||
compiled = re.compile("match")
|
||||
results = grep_lines("test.txt",
|
||||
data.decode(errors="replace").splitlines(),
|
||||
compiled,
|
||||
invert=False,
|
||||
line_numbers=False,
|
||||
count_only=False,
|
||||
files_only=False,
|
||||
only_matching=False,
|
||||
max_count=3)
|
||||
assert len(results) == 3
|
||||
@@ -0,0 +1,293 @@
|
||||
# ========= 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, timezone
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.accessor.s3 import S3Accessor
|
||||
from mirage.cache.index import IndexEntry
|
||||
from mirage.cache.index.ram import RAMIndexCacheStore
|
||||
from mirage.core.s3.readdir import readdir
|
||||
from mirage.core.s3.stat import stat
|
||||
from mirage.resource.s3 import S3Config
|
||||
from mirage.types import FileType, PathSpec
|
||||
from mirage.utils.key_prefix import mount_key
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def s3_config():
|
||||
return S3Config(bucket="test-bucket", region="us-east-1")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def s3_accessor(s3_config):
|
||||
return S3Accessor(s3_config)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def index():
|
||||
return RAMIndexCacheStore()
|
||||
|
||||
|
||||
def _mock_client_ctx(mock_client):
|
||||
mock_ctx = AsyncMock()
|
||||
mock_ctx.__aenter__.return_value = mock_client
|
||||
return mock_ctx
|
||||
|
||||
|
||||
class TestStatDirectoryFallback:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_file_returns_filestat(self, s3_accessor):
|
||||
with patch("mirage.core.s3.stat.async_session") as mock_session:
|
||||
mock_client = AsyncMock()
|
||||
mock_client.head_object.return_value = {
|
||||
"ContentLength": 1024,
|
||||
"LastModified": datetime(2026, 1, 1, tzinfo=timezone.utc),
|
||||
"ETag": '"abc123"',
|
||||
}
|
||||
mock_session.return_value.client.return_value = _mock_client_ctx(
|
||||
mock_client)
|
||||
|
||||
path = PathSpec(resource_path=mount_key("/s3/data/file.txt",
|
||||
"/s3"),
|
||||
virtual="/s3/data/file.txt",
|
||||
directory="/s3/data/")
|
||||
result = await stat(s3_accessor, path)
|
||||
assert result.name == "file.txt"
|
||||
assert result.size == 1024
|
||||
assert result.type == FileType.TEXT
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_directory_prefix_fallback(self, s3_accessor):
|
||||
with patch("mirage.core.s3.stat.async_session") as mock_session:
|
||||
mock_client = AsyncMock()
|
||||
err = Exception("not found")
|
||||
err.response = {"Error": {"Code": "404"}}
|
||||
mock_client.head_object.side_effect = err
|
||||
mock_client.list_objects_v2.return_value = {
|
||||
"CommonPrefixes": [{
|
||||
"Prefix": "data/subdir/"
|
||||
}],
|
||||
}
|
||||
mock_session.return_value.client.return_value = _mock_client_ctx(
|
||||
mock_client)
|
||||
|
||||
path = PathSpec(resource_path=mount_key("/s3/data", "/s3"),
|
||||
virtual="/s3/data",
|
||||
directory="/s3/")
|
||||
result = await stat(s3_accessor, path)
|
||||
assert result.name == "data"
|
||||
assert result.type == FileType.DIRECTORY
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_directory_prefix_with_contents(self, s3_accessor):
|
||||
with patch("mirage.core.s3.stat.async_session") as mock_session:
|
||||
mock_client = AsyncMock()
|
||||
err = Exception("not found")
|
||||
err.response = {"Error": {"Code": "NoSuchKey"}}
|
||||
mock_client.head_object.side_effect = err
|
||||
mock_client.list_objects_v2.return_value = {
|
||||
"Contents": [{
|
||||
"Key": "data/file.txt"
|
||||
}],
|
||||
}
|
||||
mock_session.return_value.client.return_value = _mock_client_ctx(
|
||||
mock_client)
|
||||
|
||||
path = PathSpec(resource_path=mount_key("/s3/data", "/s3"),
|
||||
virtual="/s3/data",
|
||||
directory="/s3/")
|
||||
result = await stat(s3_accessor, path)
|
||||
assert result.name == "data"
|
||||
assert result.type == FileType.DIRECTORY
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_nonexistent_raises(self, s3_accessor):
|
||||
with patch("mirage.core.s3.stat.async_session") as mock_session:
|
||||
mock_client = AsyncMock()
|
||||
err = Exception("not found")
|
||||
err.response = {"Error": {"Code": "404"}}
|
||||
mock_client.head_object.side_effect = err
|
||||
mock_client.list_objects_v2.return_value = {}
|
||||
mock_session.return_value.client.return_value = _mock_client_ctx(
|
||||
mock_client)
|
||||
|
||||
path = PathSpec(resource_path=mount_key("/s3/nope", "/s3"),
|
||||
virtual="/s3/nope",
|
||||
directory="/s3/")
|
||||
with pytest.raises(FileNotFoundError):
|
||||
await stat(s3_accessor, path)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_root_returns_directory(self, s3_accessor):
|
||||
path = PathSpec(resource_path=mount_key("/s3/", "/s3"),
|
||||
virtual="/s3/",
|
||||
directory="/s3/")
|
||||
result = await stat(s3_accessor, path)
|
||||
assert result.name == "/"
|
||||
assert result.type == FileType.DIRECTORY
|
||||
|
||||
|
||||
class TestStatIndexCache:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_uses_index_for_folder(self, s3_accessor, index):
|
||||
await index.put(
|
||||
"/s3/data",
|
||||
IndexEntry(id="/data", name="data", resource_type="folder"))
|
||||
|
||||
path = PathSpec(resource_path=mount_key("/s3/data", "/s3"),
|
||||
virtual="/s3/data",
|
||||
directory="/s3/")
|
||||
result = await stat(s3_accessor, path, index)
|
||||
assert result.name == "data"
|
||||
assert result.type == FileType.DIRECTORY
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_uses_index_for_file(self, s3_accessor, index):
|
||||
await index.put(
|
||||
"/s3/data/file.txt",
|
||||
IndexEntry(id="/data/file.txt",
|
||||
name="file.txt",
|
||||
resource_type="file",
|
||||
size=2048))
|
||||
|
||||
path = PathSpec(resource_path=mount_key("/s3/data/file.txt", "/s3"),
|
||||
virtual="/s3/data/file.txt",
|
||||
directory="/s3/data/")
|
||||
result = await stat(s3_accessor, path, index)
|
||||
assert result.name == "file.txt"
|
||||
assert result.size == 2048
|
||||
assert result.type == FileType.TEXT
|
||||
|
||||
|
||||
class TestReaddirIndexEntries:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_stores_folder_type(self, s3_accessor, index):
|
||||
with patch("mirage.core.s3.readdir.async_session") as mock_session:
|
||||
mock_client = AsyncMock()
|
||||
page_data = {
|
||||
"CommonPrefixes": [{
|
||||
"Prefix": "subdir/data/"
|
||||
}],
|
||||
"Contents": [{
|
||||
"Key": "subdir/readme.txt",
|
||||
"Size": 100
|
||||
}],
|
||||
}
|
||||
|
||||
async def _paginate(**kwargs):
|
||||
yield page_data
|
||||
|
||||
mock_paginator = MagicMock()
|
||||
mock_paginator.paginate = _paginate
|
||||
mock_client.get_paginator = MagicMock(return_value=mock_paginator)
|
||||
mock_session.return_value.client.return_value = _mock_client_ctx(
|
||||
mock_client)
|
||||
|
||||
path = PathSpec(resource_path=mount_key("/s3/subdir", "/s3"),
|
||||
virtual="/s3/subdir",
|
||||
directory="/s3/")
|
||||
result = await readdir(s3_accessor, path, index)
|
||||
assert "/s3/subdir/data" in result
|
||||
assert "/s3/subdir/readme.txt" in result
|
||||
|
||||
lookup = await index.get("/s3/subdir/data")
|
||||
assert lookup.entry is not None
|
||||
assert lookup.entry.resource_type == "folder"
|
||||
assert lookup.entry.name == "data"
|
||||
|
||||
lookup = await index.get("/s3/subdir/readme.txt")
|
||||
assert lookup.entry is not None
|
||||
assert lookup.entry.resource_type == "file"
|
||||
assert lookup.entry.size == 100
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_cache_hit(self, s3_accessor, index):
|
||||
with patch("mirage.core.s3.readdir.async_session") as mock_session:
|
||||
mock_client = AsyncMock()
|
||||
page_data = {
|
||||
"CommonPrefixes": [{
|
||||
"Prefix": "subdir/nested/"
|
||||
}],
|
||||
"Contents": [],
|
||||
}
|
||||
|
||||
async def _paginate(**kwargs):
|
||||
yield page_data
|
||||
|
||||
mock_paginator = MagicMock()
|
||||
mock_paginator.paginate = _paginate
|
||||
mock_client.get_paginator = MagicMock(return_value=mock_paginator)
|
||||
mock_session.return_value.client.return_value = _mock_client_ctx(
|
||||
mock_client)
|
||||
|
||||
path = PathSpec(resource_path=mount_key("/s3/subdir", "/s3"),
|
||||
virtual="/s3/subdir",
|
||||
directory="/s3/")
|
||||
r1 = await readdir(s3_accessor, path, index)
|
||||
r2 = await readdir(s3_accessor, path, index)
|
||||
assert r1 == r2
|
||||
assert mock_client.get_paginator.call_count == 1
|
||||
|
||||
|
||||
class TestStatAfterReaddir:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_hits_cache_after_readdir(self, s3_accessor, index):
|
||||
with patch("mirage.core.s3.readdir.async_session") as mock_session:
|
||||
mock_client = AsyncMock()
|
||||
page_data = {
|
||||
"CommonPrefixes": [{
|
||||
"Prefix": "subdir/nested/"
|
||||
}],
|
||||
"Contents": [{
|
||||
"Key": "subdir/readme.txt",
|
||||
"Size": 500
|
||||
}],
|
||||
}
|
||||
|
||||
async def _paginate(**kwargs):
|
||||
yield page_data
|
||||
|
||||
mock_paginator = MagicMock()
|
||||
mock_paginator.paginate = _paginate
|
||||
mock_client.get_paginator = MagicMock(return_value=mock_paginator)
|
||||
mock_session.return_value.client.return_value = _mock_client_ctx(
|
||||
mock_client)
|
||||
|
||||
path = PathSpec(resource_path=mount_key("/s3/subdir", "/s3"),
|
||||
virtual="/s3/subdir",
|
||||
directory="/s3/")
|
||||
await readdir(s3_accessor, path, index)
|
||||
|
||||
result = await stat(
|
||||
s3_accessor,
|
||||
PathSpec(resource_path=mount_key("/s3/subdir/nested", "/s3"),
|
||||
virtual="/s3/subdir/nested",
|
||||
directory="/s3/subdir/"), index)
|
||||
assert result.name == "nested"
|
||||
assert result.type == FileType.DIRECTORY
|
||||
|
||||
result = await stat(
|
||||
s3_accessor,
|
||||
PathSpec(resource_path=mount_key("/s3/subdir/readme.txt", "/s3"),
|
||||
virtual="/s3/subdir/readme.txt",
|
||||
directory="/s3/subdir/"), index)
|
||||
assert result.name == "readme.txt"
|
||||
assert result.size == 500
|
||||
@@ -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. =========
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.accessor.s3 import S3Accessor
|
||||
from mirage.resource.s3 import S3Config, S3Resource
|
||||
from mirage.types import PathSpec
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def s3_config():
|
||||
return S3Config(bucket="test-bucket", region="us-east-1")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def s3_accessor(s3_config):
|
||||
return S3Accessor(s3_config)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def s3_backend(s3_config):
|
||||
return S3Resource(s3_config)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_bytes(s3_accessor):
|
||||
from mirage.core.s3.read import read_bytes
|
||||
|
||||
with patch("mirage.core.s3.read.async_session") as mock_session:
|
||||
mock_client = AsyncMock()
|
||||
mock_body = AsyncMock()
|
||||
mock_body.read.return_value = b"hello world\nfoo bar\nbaz"
|
||||
mock_client.get_object.return_value = {"Body": mock_body}
|
||||
mock_ctx = AsyncMock()
|
||||
mock_ctx.__aenter__.return_value = mock_client
|
||||
mock_session.return_value.client.return_value = mock_ctx
|
||||
data = await read_bytes(
|
||||
s3_accessor,
|
||||
PathSpec(resource_path="data/file.txt",
|
||||
virtual="/data/file.txt",
|
||||
directory="/data/file.txt"))
|
||||
assert data == b"hello world\nfoo bar\nbaz"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_range_get(s3_accessor):
|
||||
from mirage.core.s3.read import read_bytes
|
||||
|
||||
with patch("mirage.core.s3.read.async_session") as mock_session:
|
||||
mock_client = AsyncMock()
|
||||
mock_body = AsyncMock()
|
||||
mock_body.read.return_value = b"hello"
|
||||
mock_client.get_object.return_value = {"Body": mock_body}
|
||||
mock_ctx = AsyncMock()
|
||||
mock_ctx.__aenter__.return_value = mock_client
|
||||
mock_session.return_value.client.return_value = mock_ctx
|
||||
data = await read_bytes(s3_accessor,
|
||||
PathSpec(resource_path="data/file.txt",
|
||||
virtual="/data/file.txt",
|
||||
directory="/data/file.txt"),
|
||||
offset=0,
|
||||
size=5)
|
||||
assert data == b"hello"
|
||||
mock_client.get_object.assert_called_once()
|
||||
call_kwargs = mock_client.get_object.call_args[1]
|
||||
assert "Range" in call_kwargs
|
||||
assert call_kwargs["Range"] == "bytes=0-4"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_put_bytes(s3_accessor):
|
||||
from mirage.core.s3.write import write_bytes
|
||||
|
||||
with patch("mirage.core.s3.write.async_session") as mock_session:
|
||||
mock_client = AsyncMock()
|
||||
mock_ctx = AsyncMock()
|
||||
mock_ctx.__aenter__.return_value = mock_client
|
||||
mock_session.return_value.client.return_value = mock_ctx
|
||||
await write_bytes(
|
||||
s3_accessor,
|
||||
PathSpec(resource_path="data/out.txt",
|
||||
virtual="/data/out.txt",
|
||||
directory="/data/out.txt"), b"hello")
|
||||
mock_client.put_object.assert_called_once()
|
||||
call_kwargs = mock_client.put_object.call_args[1]
|
||||
assert call_kwargs["Body"] == b"hello"
|
||||
assert call_kwargs["Key"] == "data/out.txt"
|
||||
@@ -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 asyncio
|
||||
from io import BytesIO
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from mirage.accessor.s3 import S3Accessor
|
||||
from mirage.resource.s3 import S3Config
|
||||
|
||||
|
||||
def _config():
|
||||
return S3Accessor(
|
||||
S3Config(
|
||||
bucket="test-bucket",
|
||||
region="us-east-1",
|
||||
aws_access_key_id="fake",
|
||||
aws_secret_access_key="fake",
|
||||
))
|
||||
|
||||
|
||||
def _mock_session(data: bytes, chunk_size: int = 8192):
|
||||
mock_client = AsyncMock()
|
||||
body_mock = AsyncMock()
|
||||
body_mock.read = AsyncMock(return_value=data)
|
||||
|
||||
async def _iter_chunks(size=chunk_size):
|
||||
buf = BytesIO(data)
|
||||
while True:
|
||||
chunk = buf.read(size)
|
||||
if not chunk:
|
||||
break
|
||||
yield chunk
|
||||
|
||||
body_mock.iter_chunks = _iter_chunks
|
||||
mock_client.get_object = AsyncMock(return_value={"Body": body_mock})
|
||||
|
||||
mock_session = MagicMock()
|
||||
ctx = AsyncMock()
|
||||
ctx.__aenter__ = AsyncMock(return_value=mock_client)
|
||||
ctx.__aexit__ = AsyncMock(return_value=False)
|
||||
mock_session.client = MagicMock(return_value=ctx)
|
||||
return mock_session
|
||||
|
||||
|
||||
def test_read_stream_returns_async_iterator():
|
||||
from mirage.core.s3.stream import read_stream
|
||||
config = _config()
|
||||
session = _mock_session(b"hello world")
|
||||
|
||||
async def _run():
|
||||
chunks = []
|
||||
async for chunk in read_stream(config, "test.txt"):
|
||||
chunks.append(chunk)
|
||||
return chunks
|
||||
|
||||
with patch("mirage.core.s3.stream.async_session", return_value=session):
|
||||
chunks = asyncio.run(_run())
|
||||
assert b"".join(chunks) == b"hello world"
|
||||
|
||||
|
||||
def test_read_stream_yields_chunks():
|
||||
from mirage.core.s3.stream import read_stream
|
||||
config = _config()
|
||||
session = _mock_session(b"a" * 100, chunk_size=30)
|
||||
|
||||
async def _run():
|
||||
chunks = []
|
||||
async for chunk in read_stream(config, "test.txt", chunk_size=30):
|
||||
chunks.append(chunk)
|
||||
return chunks
|
||||
|
||||
with patch("mirage.core.s3.stream.async_session", return_value=session):
|
||||
chunks = asyncio.run(_run())
|
||||
assert b"".join(chunks) == b"a" * 100
|
||||
assert len(chunks) >= 2
|
||||
|
||||
|
||||
def test_read_bytes_returns_bytes():
|
||||
from mirage.core.s3.read import read_bytes
|
||||
config = _config()
|
||||
session = _mock_session(b"file content here")
|
||||
|
||||
with patch("mirage.core.s3.read.async_session", return_value=session):
|
||||
result = asyncio.run(read_bytes(config, "test.txt"))
|
||||
assert isinstance(result, bytes)
|
||||
assert result == b"file content here"
|
||||
@@ -0,0 +1,64 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from mirage.resource.s3 import S3Config
|
||||
from mirage.resource.scaleway import ScalewayConfig, ScalewayResource
|
||||
from mirage.resource.secrets import reveal_secret
|
||||
from mirage.types import ResourceName
|
||||
|
||||
|
||||
def test_scaleway_regional_endpoint():
|
||||
config = ScalewayConfig(bucket="b",
|
||||
region="fr-par",
|
||||
access_key_id="k",
|
||||
secret_access_key="s")
|
||||
assert config.resolved_endpoint_url() == "https://s3.fr-par.scw.cloud"
|
||||
|
||||
|
||||
def test_scaleway_requires_region():
|
||||
with pytest.raises(ValidationError):
|
||||
ScalewayConfig(bucket="b", access_key_id="k", secret_access_key="s")
|
||||
|
||||
|
||||
def test_scaleway_custom_endpoint_override():
|
||||
config = ScalewayConfig(bucket="b",
|
||||
region="nl-ams",
|
||||
endpoint_url="https://custom.example.com",
|
||||
access_key_id="k",
|
||||
secret_access_key="s")
|
||||
assert config.resolved_endpoint_url() == "https://custom.example.com"
|
||||
|
||||
|
||||
def test_scaleway_to_s3_config():
|
||||
config = ScalewayConfig(bucket="b",
|
||||
region="pl-waw",
|
||||
access_key_id="key",
|
||||
secret_access_key="secret")
|
||||
s3 = config.to_s3_config()
|
||||
assert isinstance(s3, S3Config)
|
||||
assert s3.endpoint_url == "https://s3.pl-waw.scw.cloud"
|
||||
assert reveal_secret(s3.aws_access_key_id) == "key"
|
||||
|
||||
|
||||
def test_scaleway_resource_uses_s3_resource_type():
|
||||
resource = ScalewayResource(
|
||||
ScalewayConfig(bucket="b",
|
||||
region="fr-par",
|
||||
access_key_id="k",
|
||||
secret_access_key="s"))
|
||||
assert resource.name == ResourceName.S3
|
||||
assert isinstance(resource.config, S3Config)
|
||||
@@ -0,0 +1,97 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from mirage.resource.s3 import S3Config
|
||||
from mirage.resource.seaweedfs import SeaweedFSConfig, SeaweedFSResource
|
||||
from mirage.resource.secrets import reveal_secret
|
||||
from mirage.types import ResourceName
|
||||
|
||||
|
||||
def test_seaweedfs_config_defaults():
|
||||
config = SeaweedFSConfig(
|
||||
bucket="my-bucket",
|
||||
endpoint_url="http://localhost:8333",
|
||||
access_key_id="weed",
|
||||
secret_access_key="weed",
|
||||
)
|
||||
assert config.region == "us-east-1"
|
||||
assert config.path_style is True
|
||||
assert config.timeout == 30
|
||||
|
||||
|
||||
def test_seaweedfs_config_immutable():
|
||||
config = SeaweedFSConfig(
|
||||
bucket="my-bucket",
|
||||
endpoint_url="http://localhost:8333",
|
||||
access_key_id="k",
|
||||
secret_access_key="s",
|
||||
)
|
||||
with pytest.raises(ValidationError):
|
||||
config.bucket = "other"
|
||||
|
||||
|
||||
def test_seaweedfs_config_to_s3_config():
|
||||
config = SeaweedFSConfig(
|
||||
bucket="my-bucket",
|
||||
endpoint_url="http://localhost:8333",
|
||||
access_key_id="weed",
|
||||
secret_access_key="weed",
|
||||
proxy="http://localhost:8080",
|
||||
)
|
||||
s3 = config.to_s3_config()
|
||||
assert isinstance(s3, S3Config)
|
||||
assert s3.bucket == "my-bucket"
|
||||
assert s3.endpoint_url == "http://localhost:8333"
|
||||
assert s3.path_style is True
|
||||
assert reveal_secret(s3.aws_access_key_id) == "weed"
|
||||
assert reveal_secret(s3.aws_secret_access_key) == "weed"
|
||||
assert s3.proxy == "http://localhost:8080"
|
||||
|
||||
|
||||
def test_seaweedfs_config_path_style_override():
|
||||
config = SeaweedFSConfig(
|
||||
bucket="my-bucket",
|
||||
endpoint_url="https://seaweedfs.example.com",
|
||||
access_key_id="k",
|
||||
secret_access_key="s",
|
||||
path_style=False,
|
||||
)
|
||||
assert config.to_s3_config().path_style is False
|
||||
|
||||
|
||||
def test_seaweedfs_resource_uses_s3_resource_type():
|
||||
resource = SeaweedFSResource(
|
||||
SeaweedFSConfig(
|
||||
bucket="my-bucket",
|
||||
endpoint_url="http://localhost:8333",
|
||||
access_key_id="k",
|
||||
secret_access_key="s",
|
||||
))
|
||||
assert resource.name == ResourceName.S3
|
||||
assert resource.caches_reads is True
|
||||
assert isinstance(resource.config, S3Config)
|
||||
|
||||
|
||||
def test_seaweedfs_resource_preserves_original_config():
|
||||
config = SeaweedFSConfig(
|
||||
bucket="my-bucket",
|
||||
endpoint_url="http://localhost:8333",
|
||||
access_key_id="k",
|
||||
secret_access_key="s",
|
||||
)
|
||||
resource = SeaweedFSResource(config)
|
||||
assert resource.seaweedfs_config is config
|
||||
@@ -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 pydantic import ValidationError
|
||||
|
||||
from mirage.core.s3._client import _client_kwargs
|
||||
from mirage.resource.s3 import S3Config
|
||||
from mirage.resource.secrets import reveal_secret
|
||||
from mirage.resource.supabase import SupabaseConfig, SupabaseResource
|
||||
from mirage.types import ResourceName
|
||||
|
||||
|
||||
def test_supabase_config_defaults():
|
||||
config = SupabaseConfig(
|
||||
bucket="my-bucket",
|
||||
project_ref="project-123",
|
||||
region="us-west-2",
|
||||
access_key_id="access-key",
|
||||
secret_access_key="secret-key",
|
||||
)
|
||||
assert config.timeout == 30
|
||||
assert config.resolved_endpoint_url() == (
|
||||
"https://project-123.storage.supabase.co/storage/v1/s3")
|
||||
|
||||
|
||||
def test_supabase_config_immutable():
|
||||
config = SupabaseConfig(
|
||||
bucket="my-bucket",
|
||||
project_ref="project-123",
|
||||
region="us-west-2",
|
||||
access_key_id="access-key",
|
||||
secret_access_key="secret-key",
|
||||
)
|
||||
with pytest.raises(ValidationError):
|
||||
config.bucket = "other-bucket"
|
||||
|
||||
|
||||
def test_supabase_config_to_s3_config():
|
||||
config = SupabaseConfig(
|
||||
bucket="my-bucket",
|
||||
endpoint_url="https://example.supabase.co/storage/v1/s3",
|
||||
region="us-west-2",
|
||||
access_key_id="access-key",
|
||||
secret_access_key="secret-key",
|
||||
session_token="session-token",
|
||||
proxy="http://localhost:8080",
|
||||
)
|
||||
s3_config = config.to_s3_config()
|
||||
assert isinstance(s3_config, S3Config)
|
||||
assert s3_config.bucket == "my-bucket"
|
||||
assert s3_config.region == "us-west-2"
|
||||
assert (
|
||||
s3_config.endpoint_url == "https://example.supabase.co/storage/v1/s3")
|
||||
assert reveal_secret(s3_config.aws_access_key_id) == "access-key"
|
||||
assert reveal_secret(s3_config.aws_secret_access_key) == "secret-key"
|
||||
assert reveal_secret(s3_config.aws_session_token) == "session-token"
|
||||
assert s3_config.path_style is True
|
||||
assert s3_config.proxy == "http://localhost:8080"
|
||||
|
||||
|
||||
def test_supabase_config_requires_project_ref_or_endpoint():
|
||||
config = SupabaseConfig(
|
||||
bucket="my-bucket",
|
||||
region="us-west-2",
|
||||
access_key_id="access-key",
|
||||
secret_access_key="secret-key",
|
||||
)
|
||||
with pytest.raises(ValueError):
|
||||
config.resolved_endpoint_url()
|
||||
|
||||
|
||||
def test_supabase_resource_uses_s3_resource_type():
|
||||
resource = SupabaseResource(
|
||||
SupabaseConfig(
|
||||
bucket="my-bucket",
|
||||
project_ref="project-123",
|
||||
region="us-west-2",
|
||||
access_key_id="access-key",
|
||||
secret_access_key="secret-key",
|
||||
))
|
||||
assert resource.name == ResourceName.S3
|
||||
assert resource.caches_reads is True
|
||||
assert isinstance(resource.config, S3Config)
|
||||
assert resource.config.path_style is True
|
||||
|
||||
|
||||
def test_supabase_resource_preserves_original_config():
|
||||
config = SupabaseConfig(
|
||||
bucket="my-bucket",
|
||||
project_ref="project-123",
|
||||
region="us-west-2",
|
||||
access_key_id="access-key",
|
||||
secret_access_key="secret-key",
|
||||
)
|
||||
resource = SupabaseResource(config)
|
||||
assert resource.supabase_config is config
|
||||
|
||||
|
||||
def test_s3_client_kwargs_support_path_style_and_session_token():
|
||||
config = S3Config(
|
||||
bucket="my-bucket",
|
||||
region="us-west-2",
|
||||
endpoint_url="https://example.com",
|
||||
aws_access_key_id="access-key",
|
||||
aws_secret_access_key="secret-key",
|
||||
aws_session_token="session-token",
|
||||
path_style=True,
|
||||
)
|
||||
kwargs = _client_kwargs(config)
|
||||
assert kwargs["aws_session_token"] == "session-token"
|
||||
assert kwargs["config"].s3 == {"addressing_style": "path"}
|
||||
@@ -0,0 +1,65 @@
|
||||
# ========= 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 pydantic import ValidationError
|
||||
|
||||
from mirage.resource.s3 import S3Config
|
||||
from mirage.resource.secrets import reveal_secret
|
||||
from mirage.resource.tencent import TencentConfig, TencentResource
|
||||
from mirage.types import ResourceName
|
||||
|
||||
|
||||
def test_tencent_regional_endpoint():
|
||||
config = TencentConfig(bucket="b-1250",
|
||||
region="ap-guangzhou",
|
||||
access_key_id="k",
|
||||
secret_access_key="s")
|
||||
assert config.resolved_endpoint_url() == (
|
||||
"https://cos.ap-guangzhou.myqcloud.com")
|
||||
|
||||
|
||||
def test_tencent_requires_region():
|
||||
with pytest.raises(ValidationError):
|
||||
TencentConfig(bucket="b", access_key_id="k", secret_access_key="s")
|
||||
|
||||
|
||||
def test_tencent_custom_endpoint_override():
|
||||
config = TencentConfig(bucket="b",
|
||||
region="ap-beijing",
|
||||
endpoint_url="https://custom.example.com",
|
||||
access_key_id="k",
|
||||
secret_access_key="s")
|
||||
assert config.resolved_endpoint_url() == "https://custom.example.com"
|
||||
|
||||
|
||||
def test_tencent_to_s3_config():
|
||||
config = TencentConfig(bucket="b-1250",
|
||||
region="ap-singapore",
|
||||
access_key_id="key",
|
||||
secret_access_key="secret")
|
||||
s3 = config.to_s3_config()
|
||||
assert isinstance(s3, S3Config)
|
||||
assert s3.endpoint_url == "https://cos.ap-singapore.myqcloud.com"
|
||||
assert reveal_secret(s3.aws_access_key_id) == "key"
|
||||
|
||||
|
||||
def test_tencent_resource_uses_s3_resource_type():
|
||||
resource = TencentResource(
|
||||
TencentConfig(bucket="b-1250",
|
||||
region="ap-guangzhou",
|
||||
access_key_id="k",
|
||||
secret_access_key="s"))
|
||||
assert resource.name == ResourceName.S3
|
||||
assert isinstance(resource.config, S3Config)
|
||||
@@ -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 pydantic import ValidationError
|
||||
|
||||
from mirage.resource.s3 import S3Config
|
||||
from mirage.resource.secrets import reveal_secret
|
||||
from mirage.resource.wasabi import WasabiConfig, WasabiResource
|
||||
from mirage.types import ResourceName
|
||||
|
||||
|
||||
def test_wasabi_default_region_endpoint():
|
||||
config = WasabiConfig(bucket="b", access_key_id="k", secret_access_key="s")
|
||||
assert config.region == "us-east-1"
|
||||
assert config.resolved_endpoint_url() == "https://s3.wasabisys.com"
|
||||
|
||||
|
||||
def test_wasabi_regional_endpoint():
|
||||
config = WasabiConfig(bucket="b",
|
||||
region="us-west-1",
|
||||
access_key_id="k",
|
||||
secret_access_key="s")
|
||||
assert config.resolved_endpoint_url() == (
|
||||
"https://s3.us-west-1.wasabisys.com")
|
||||
|
||||
|
||||
def test_wasabi_custom_endpoint_override():
|
||||
config = WasabiConfig(bucket="b",
|
||||
endpoint_url="https://custom.example.com",
|
||||
access_key_id="k",
|
||||
secret_access_key="s")
|
||||
assert config.resolved_endpoint_url() == "https://custom.example.com"
|
||||
|
||||
|
||||
def test_wasabi_config_immutable():
|
||||
config = WasabiConfig(bucket="b", access_key_id="k", secret_access_key="s")
|
||||
with pytest.raises(ValidationError):
|
||||
config.bucket = "other"
|
||||
|
||||
|
||||
def test_wasabi_to_s3_config():
|
||||
config = WasabiConfig(bucket="b",
|
||||
region="us-east-2",
|
||||
access_key_id="key",
|
||||
secret_access_key="secret")
|
||||
s3 = config.to_s3_config()
|
||||
assert isinstance(s3, S3Config)
|
||||
assert s3.endpoint_url == "https://s3.us-east-2.wasabisys.com"
|
||||
assert s3.path_style is False
|
||||
assert reveal_secret(s3.aws_access_key_id) == "key"
|
||||
|
||||
|
||||
def test_wasabi_resource_uses_s3_resource_type():
|
||||
resource = WasabiResource(
|
||||
WasabiConfig(bucket="b", access_key_id="k", secret_access_key="s"))
|
||||
assert resource.name == ResourceName.S3
|
||||
assert isinstance(resource.config, S3Config)
|
||||
@@ -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,23 @@
|
||||
# ========= 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.accessor.postgres import PostgresAccessor
|
||||
from mirage.resource.postgres.config import PostgresConfig
|
||||
|
||||
|
||||
def test_accessor_holds_config():
|
||||
cfg = PostgresConfig(dsn="postgres://localhost/db")
|
||||
a = PostgresAccessor(cfg)
|
||||
assert a.config is cfg
|
||||
assert a._pool is None
|
||||
@@ -0,0 +1,31 @@
|
||||
# ========= 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.postgres.config import PostgresConfig
|
||||
from mirage.resource.secrets import reveal_secret
|
||||
|
||||
|
||||
def test_defaults():
|
||||
cfg = PostgresConfig(dsn="postgres://localhost/db")
|
||||
assert reveal_secret(cfg.dsn) == "postgres://localhost/db"
|
||||
assert cfg.schemas is None
|
||||
assert cfg.default_row_limit == 1000
|
||||
assert cfg.max_read_rows == 10_000
|
||||
assert cfg.max_read_bytes == 10 * 1024 * 1024
|
||||
assert cfg.default_search_limit == 100
|
||||
|
||||
|
||||
def test_schema_filter():
|
||||
cfg = PostgresConfig(dsn="postgres://localhost/db", schemas=["public"])
|
||||
assert cfg.schemas == ["public"]
|
||||
@@ -0,0 +1,58 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from mirage.resource.postgres import PostgresConfig, PostgresResource
|
||||
|
||||
|
||||
def test_resource_name():
|
||||
res = PostgresResource(PostgresConfig(dsn="postgres://localhost/db"))
|
||||
assert res.name == "postgres"
|
||||
assert res.caches_reads is False
|
||||
|
||||
|
||||
def test_resource_registers_three_ops():
|
||||
res = PostgresResource(PostgresConfig(dsn="postgres://localhost/db"))
|
||||
op_names = {ro.name for ro in res.ops_list()}
|
||||
assert {"read", "readdir", "stat"} <= op_names
|
||||
|
||||
|
||||
def test_resource_registers_commands():
|
||||
res = PostgresResource(PostgresConfig(dsn="postgres://localhost/db"))
|
||||
cmd_names = {rc.name for rc in res.commands()}
|
||||
expected = {
|
||||
"cat", "find", "head", "jq", "ls", "stat", "tail", "tree", "wc",
|
||||
"grep", "rg"
|
||||
}
|
||||
assert expected <= cmd_names
|
||||
|
||||
|
||||
def test_resource_in_registry():
|
||||
from mirage.resource.registry import REGISTRY, build_resource
|
||||
|
||||
assert "postgres" in REGISTRY
|
||||
res = build_resource("postgres", config={"dsn": "postgres://localhost/db"})
|
||||
assert res.name == "postgres"
|
||||
|
||||
|
||||
def test_resource_get_state_redacts_dsn():
|
||||
res = PostgresResource(PostgresConfig(dsn="postgres://user:pw@host/db"))
|
||||
state = res.get_state()
|
||||
assert state["type"] == "postgres"
|
||||
assert state["config"]["dsn"] == "<REDACTED>"
|
||||
assert "redacted_fields" not in state
|
||||
|
||||
|
||||
def test_resource_load_state_noop():
|
||||
res = PostgresResource(PostgresConfig(dsn="postgres://localhost/db"))
|
||||
res.load_state({"type": "postgres"})
|
||||
@@ -0,0 +1,41 @@
|
||||
from mirage.resource.qdrant import QdrantConfig, QdrantResource
|
||||
from mirage.resource.registry import REGISTRY, build_resource
|
||||
|
||||
|
||||
def _resource(**kw) -> QdrantResource:
|
||||
base = dict(collection="animals", group_by=["label"], id_field="id")
|
||||
base.update(kw)
|
||||
return QdrantResource(QdrantConfig(**base))
|
||||
|
||||
|
||||
def test_resource_name_and_remote():
|
||||
res = _resource()
|
||||
assert res.name == "qdrant"
|
||||
assert res.is_remote is True
|
||||
assert res.SUPPORTS_SNAPSHOT is False
|
||||
|
||||
|
||||
def test_resource_registers_ops():
|
||||
res = _resource()
|
||||
assert {"read", "readdir", "stat"} <= {o.name for o in res.ops_list()}
|
||||
|
||||
|
||||
def test_resource_registers_commands():
|
||||
res = _resource()
|
||||
expected = {
|
||||
"cat", "find", "grep", "head", "ls", "rg", "search", "stat", "tail",
|
||||
"tree", "wc"
|
||||
}
|
||||
assert expected <= {c.name for c in res.commands()}
|
||||
|
||||
|
||||
def test_resource_in_registry():
|
||||
assert "qdrant" in REGISTRY
|
||||
res = build_resource("qdrant", {"collection": "docs"})
|
||||
assert res.name == "qdrant"
|
||||
|
||||
|
||||
def test_resource_get_state_redacts_api_key():
|
||||
res = _resource(api_key="secret-value")
|
||||
state = res.get_state()
|
||||
assert "secret-value" not in str(state)
|
||||
@@ -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,193 @@
|
||||
# ========= 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.resource.redis import RedisResource
|
||||
from mirage.types import MountMode
|
||||
from mirage.workspace import Workspace
|
||||
|
||||
REDIS_URL = os.environ.get("REDIS_URL", "")
|
||||
pytestmark = pytest.mark.skipif(not REDIS_URL, reason="REDIS_URL not set")
|
||||
|
||||
|
||||
@pytest_asyncio.fixture()
|
||||
async def ws():
|
||||
resource = RedisResource(url=REDIS_URL, key_prefix="test:integ:")
|
||||
await resource._store.clear()
|
||||
await resource._store.add_dir("/")
|
||||
w = Workspace(
|
||||
{"/data": (resource, MountMode.WRITE)},
|
||||
mode=MountMode.WRITE,
|
||||
)
|
||||
yield w
|
||||
await resource._store.clear()
|
||||
await resource._store.close()
|
||||
|
||||
|
||||
async def _run(ws, cmd, stdin=None):
|
||||
io = await ws.execute(cmd, stdin=stdin)
|
||||
stdout = io.stdout
|
||||
if stdout is None:
|
||||
return ""
|
||||
if isinstance(stdout, bytes):
|
||||
return stdout.decode(errors="replace")
|
||||
chunks = [chunk async for chunk in stdout]
|
||||
return b"".join(chunks).decode(errors="replace")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_echo_and_cat(ws):
|
||||
await _run(ws, "echo hello > /data/hello.txt")
|
||||
result = await _run(ws, "cat /data/hello.txt")
|
||||
assert result == "hello\n"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ls(ws):
|
||||
await _run(ws, "echo a > /data/a.txt")
|
||||
await _run(ws, "echo b > /data/b.txt")
|
||||
result = await _run(ws, "ls /data/")
|
||||
assert "a.txt" in result
|
||||
assert "b.txt" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat(ws):
|
||||
await _run(ws, "echo hello > /data/f.txt")
|
||||
result = await _run(ws, "stat /data/f.txt")
|
||||
assert "name=f.txt" in result
|
||||
assert "size=" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_head(ws):
|
||||
await _run(ws, "echo 'line1\nline2\nline3' > /data/f.txt")
|
||||
result = await _run(ws, "head -n 2 /data/f.txt")
|
||||
assert "line1" in result
|
||||
assert "line2" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_grep(ws):
|
||||
await _run(
|
||||
ws,
|
||||
"echo 'hello world\nfoo bar\nhello again' "
|
||||
"> /data/f.txt",
|
||||
)
|
||||
result = await _run(ws, "grep hello /data/f.txt")
|
||||
assert "hello world" in result
|
||||
assert "hello again" in result
|
||||
assert "foo bar" not in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wc(ws):
|
||||
await _run(ws, "echo 'hello world' > /data/f.txt")
|
||||
result = await _run(ws, "wc /data/f.txt")
|
||||
assert "1" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mkdir(ws):
|
||||
await _run(ws, "mkdir /data/sub")
|
||||
result = await _run(ws, "ls /data/")
|
||||
assert "sub" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mkdir_p(ws):
|
||||
await _run(ws, "mkdir -p /data/a/b/c")
|
||||
result = await _run(ws, "stat /data/a/b/c")
|
||||
assert "directory" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cp(ws):
|
||||
await _run(ws, "echo data > /data/src.txt")
|
||||
await _run(ws, "cp /data/src.txt /data/dst.txt")
|
||||
result = await _run(ws, "cat /data/dst.txt")
|
||||
assert "data" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mv(ws):
|
||||
await _run(ws, "echo data > /data/old.txt")
|
||||
await _run(ws, "mv /data/old.txt /data/new.txt")
|
||||
result = await _run(ws, "cat /data/new.txt")
|
||||
assert "data" in result
|
||||
ls_result = await _run(ws, "ls /data/")
|
||||
assert "old.txt" not in ls_result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rm(ws):
|
||||
await _run(ws, "echo data > /data/f.txt")
|
||||
await _run(ws, "rm /data/f.txt")
|
||||
result = await _run(ws, "ls /data/")
|
||||
assert "f.txt" not in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tree(ws):
|
||||
await _run(ws, "echo a > /data/a.txt")
|
||||
await _run(ws, "mkdir /data/sub")
|
||||
await _run(ws, "echo b > /data/sub/b.txt")
|
||||
result = await _run(ws, "tree /data/")
|
||||
assert "a.txt" in result
|
||||
assert "sub" in result
|
||||
assert "b.txt" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find(ws):
|
||||
await _run(ws, "echo a > /data/a.txt")
|
||||
await _run(ws, "mkdir /data/sub")
|
||||
await _run(ws, "echo b > /data/sub/b.py")
|
||||
result = await _run(ws, "find /data/ -name '*.py'")
|
||||
assert "b.py" in result
|
||||
assert "a.txt" not in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_du(ws):
|
||||
await _run(ws, "echo hello > /data/f.txt")
|
||||
result = await _run(ws, "du /data/")
|
||||
assert result.strip() != ""
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pipe(ws):
|
||||
await _run(ws, "echo 'a\nb\na\nc\na' > /data/f.txt")
|
||||
result = await _run(ws, "grep a /data/f.txt | wc -l")
|
||||
assert result.strip() == "3"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cd_and_pwd(ws):
|
||||
await _run(ws, "echo hi > /data/f.txt")
|
||||
await _run(ws, "cd /data/")
|
||||
result = await _run(ws, "pwd")
|
||||
assert "/data" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_data_persists_across_commands(ws):
|
||||
await _run(ws, "echo persistent > /data/p.txt")
|
||||
result1 = await _run(ws, "cat /data/p.txt")
|
||||
result2 = await _run(ws, "cat /data/p.txt")
|
||||
assert result1 == result2 == "persistent\n"
|
||||
@@ -0,0 +1,138 @@
|
||||
# ========= 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.resource.redis.store import RedisStore
|
||||
|
||||
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 = RedisStore(url=REDIS_URL, key_prefix="test:store:")
|
||||
await s.clear()
|
||||
await s.add_dir("/")
|
||||
yield s
|
||||
await s.clear()
|
||||
await s.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_set_file(store):
|
||||
await store.set_file("/a.txt", b"hello")
|
||||
assert await store.get_file("/a.txt") == b"hello"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_file_missing(store):
|
||||
assert await store.get_file("/nope") is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_del_file(store):
|
||||
await store.set_file("/a.txt", b"data")
|
||||
await store.del_file("/a.txt")
|
||||
assert await store.get_file("/a.txt") is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_has_file(store):
|
||||
assert await store.has_file("/a.txt") is False
|
||||
await store.set_file("/a.txt", b"x")
|
||||
assert await store.has_file("/a.txt") is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_files(store):
|
||||
await store.set_file("/a.txt", b"a")
|
||||
await store.set_file("/b.txt", b"b")
|
||||
await store.set_file("/sub/c.txt", b"c")
|
||||
files = await store.list_files()
|
||||
assert "/a.txt" in files
|
||||
assert "/b.txt" in files
|
||||
assert "/sub/c.txt" in files
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_files_prefix(store):
|
||||
await store.set_file("/a.txt", b"a")
|
||||
await store.set_file("/sub/b.txt", b"b")
|
||||
files = await store.list_files("/sub/")
|
||||
assert "/sub/b.txt" in files
|
||||
assert "/a.txt" not in files
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_file_len(store):
|
||||
await store.set_file("/a.txt", b"hello")
|
||||
assert await store.file_len("/a.txt") == 5
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_has_dir(store):
|
||||
assert await store.has_dir("/") is True
|
||||
assert await store.has_dir("/sub") is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_remove_dir(store):
|
||||
await store.add_dir("/sub")
|
||||
assert await store.has_dir("/sub") is True
|
||||
await store.remove_dir("/sub")
|
||||
assert await store.has_dir("/sub") is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_dirs(store):
|
||||
await store.add_dir("/a")
|
||||
await store.add_dir("/b")
|
||||
dirs = await store.list_dirs()
|
||||
assert "/" in dirs
|
||||
assert "/a" in dirs
|
||||
assert "/b" in dirs
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_set_modified(store):
|
||||
await store.set_modified("/a.txt", "2026-01-01T00:00:00")
|
||||
result = await store.get_modified("/a.txt")
|
||||
assert result == "2026-01-01T00:00:00"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_modified_missing(store):
|
||||
assert await store.get_modified("/nope") is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_del_modified(store):
|
||||
await store.set_modified("/a.txt", "2026-01-01")
|
||||
await store.del_modified("/a.txt")
|
||||
assert await store.get_modified("/a.txt") is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_clear(store):
|
||||
await store.set_file("/a.txt", b"data")
|
||||
await store.add_dir("/sub")
|
||||
await store.set_modified("/a.txt", "2026-01-01")
|
||||
await store.clear()
|
||||
assert await store.get_file("/a.txt") is None
|
||||
assert await store.has_dir("/sub") is False
|
||||
assert await store.get_modified("/a.txt") is None
|
||||
@@ -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,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. =========
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.resource.slack.config import SlackConfig
|
||||
from mirage.resource.slack.slack import SlackResource
|
||||
from mirage.types import ResourceName
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config():
|
||||
return SlackConfig(token="xoxb-test-token")
|
||||
|
||||
|
||||
def test_resource_init(config):
|
||||
resource = SlackResource(config)
|
||||
assert resource.caches_reads is True
|
||||
|
||||
|
||||
def test_resource_name(config):
|
||||
resource = SlackResource(config)
|
||||
assert resource.name == ResourceName.SLACK
|
||||
|
||||
|
||||
def test_resource_accessor(config):
|
||||
resource = SlackResource(config)
|
||||
assert resource.accessor is not None
|
||||
assert resource.accessor.config is config
|
||||
|
||||
|
||||
def test_resource_commands_registered(config):
|
||||
resource = SlackResource(config)
|
||||
# 52 native (generic factory read set incl. find and sed + bespoke
|
||||
# grep/rg + slack_* writers) + 9 filetype cmds x 7 columnar exts
|
||||
assert len(resource._commands) == 52 + 9 * 7
|
||||
@@ -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,59 @@
|
||||
# ========= 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.ssh import SSHConfig, SSHResource
|
||||
from mirage.types import ResourceName
|
||||
|
||||
|
||||
def test_ssh_config_frozen():
|
||||
cfg = SSHConfig(host="dev", root="/home/ubuntu")
|
||||
assert cfg.host == "dev"
|
||||
assert cfg.root == "/home/ubuntu"
|
||||
with pytest.raises(Exception):
|
||||
cfg.host = "other"
|
||||
|
||||
|
||||
def test_ssh_resource_attributes():
|
||||
cfg = SSHConfig(host="dev")
|
||||
resource = SSHResource(cfg)
|
||||
assert resource.name == ResourceName.SSH
|
||||
assert resource.caches_reads is True
|
||||
assert len(resource.commands()) > 0
|
||||
assert len(resource.ops_list()) > 0
|
||||
|
||||
|
||||
def test_ssh_resource_command_count():
|
||||
cfg = SSHConfig(host="dev")
|
||||
resource = SSHResource(cfg)
|
||||
cmd_names = {c.name for c in resource.commands()}
|
||||
assert "cat" in cmd_names
|
||||
assert "ls" in cmd_names
|
||||
assert "grep" in cmd_names
|
||||
assert "find" in cmd_names
|
||||
assert "cp" in cmd_names
|
||||
assert "rm" in cmd_names
|
||||
|
||||
|
||||
def test_ssh_resource_ops_count():
|
||||
cfg = SSHConfig(host="dev")
|
||||
resource = SSHResource(cfg)
|
||||
op_names = {o.name for o in resource.ops_list()}
|
||||
assert "read" in op_names
|
||||
assert "write" in op_names
|
||||
assert "stat" in op_names
|
||||
assert "readdir" in op_names
|
||||
assert "mkdir" in op_names
|
||||
assert "unlink" in op_names
|
||||
@@ -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.base import BaseResource
|
||||
from mirage.resource.disk.disk import DiskResource
|
||||
from mirage.resource.ram import RAMResource
|
||||
from mirage.resource.s3 import S3Resource
|
||||
from mirage.types import ResourceName
|
||||
|
||||
|
||||
def test_base_resource_name():
|
||||
assert BaseResource.name == "base"
|
||||
|
||||
|
||||
def test_disk_resource_name():
|
||||
assert DiskResource.name == "disk"
|
||||
|
||||
|
||||
def test_memory_resource_name():
|
||||
assert RAMResource.name == "ram"
|
||||
|
||||
|
||||
def test_s3_resource_name():
|
||||
assert S3Resource.name == "s3"
|
||||
|
||||
|
||||
def test_hf_buckets_resource_name():
|
||||
assert ResourceName.HF_BUCKETS.value == "hf_buckets"
|
||||
@@ -0,0 +1,35 @@
|
||||
# ========= 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 (RAMIndexCacheStore, RedisIndexCacheStore,
|
||||
RedisIndexConfig)
|
||||
from mirage.resource.ram import RAMResource
|
||||
|
||||
|
||||
def test_default_index_is_ram():
|
||||
r = RAMResource()
|
||||
assert isinstance(r.index, RAMIndexCacheStore)
|
||||
|
||||
|
||||
def test_set_index_redis():
|
||||
r = RAMResource()
|
||||
r.set_index(RedisIndexConfig(url="redis://localhost:6379/0"))
|
||||
assert isinstance(r.index, RedisIndexCacheStore)
|
||||
|
||||
|
||||
def test_set_index_none_resets_to_ram():
|
||||
r = RAMResource()
|
||||
r.set_index(RedisIndexConfig(url="redis://localhost:6379/0"))
|
||||
r.set_index(None)
|
||||
assert isinstance(r.index, RAMIndexCacheStore)
|
||||
@@ -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. =========
|
||||
|
||||
from mirage.accessor.base import Accessor
|
||||
from mirage.accessor.disk import DiskAccessor
|
||||
from mirage.accessor.ram import RAMAccessor
|
||||
from mirage.resource.base import BaseResource
|
||||
from mirage.resource.disk.disk import DiskResource
|
||||
from mirage.resource.ram import RAMResource
|
||||
|
||||
|
||||
def test_base_command_args():
|
||||
b = BaseResource()
|
||||
assert isinstance(b.accessor, Accessor)
|
||||
|
||||
|
||||
def test_local_command_args():
|
||||
b = DiskResource("/tmp")
|
||||
assert isinstance(b.accessor, DiskAccessor)
|
||||
assert b.accessor.root is not None
|
||||
|
||||
|
||||
def test_memory_command_args():
|
||||
b = RAMResource()
|
||||
assert isinstance(b.accessor, RAMAccessor)
|
||||
assert b.accessor.store is not None
|
||||
@@ -0,0 +1,30 @@
|
||||
# ========= 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.base import BaseResource
|
||||
from mirage.resource.ram import RAMResource
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_base_backend_fingerprint_returns_none():
|
||||
backend = BaseResource()
|
||||
assert await backend.fingerprint("/any") is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_memory_backend_fingerprint_returns_none():
|
||||
backend = RAMResource()
|
||||
assert await backend.fingerprint("/any") is None
|
||||
@@ -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 asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.resource.loader import load_backend_class
|
||||
|
||||
|
||||
def _cat_sync(backend, path):
|
||||
|
||||
async def _collect():
|
||||
return b"".join([c async for c in backend.read_stream(path)])
|
||||
|
||||
return asyncio.run(_collect())
|
||||
|
||||
|
||||
def test_load_from_module():
|
||||
cls = load_backend_class("mirage.resource.ram.ram:RAMResource")
|
||||
from mirage.resource.ram import RAMResource
|
||||
assert cls is RAMResource
|
||||
|
||||
|
||||
def test_load_from_script_file(tmp_path):
|
||||
script = tmp_path / "custom_backend.py"
|
||||
script.write_text("from mirage.resource.ram import RAMResource\n"
|
||||
"class CustomBackend(RAMResource):\n"
|
||||
" pass\n")
|
||||
cls = load_backend_class(f"{script}:CustomBackend")
|
||||
assert cls.__name__ == "CustomBackend"
|
||||
instance = cls()
|
||||
asyncio.run(instance.write("/test.txt", data=b"hello"))
|
||||
assert _cat_sync(instance, "/test.txt") == b"hello"
|
||||
|
||||
|
||||
def test_load_invalid_spec_no_colon():
|
||||
with pytest.raises(ValueError, match="invalid backend spec"):
|
||||
load_backend_class("mirage.resource.ram")
|
||||
|
||||
|
||||
def test_load_missing_file():
|
||||
with pytest.raises(ValueError, match="cannot load script"):
|
||||
load_backend_class("/nonexistent/path.py:Cls")
|
||||
|
||||
|
||||
def test_load_missing_class():
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
load_backend_class("mirage.resource.ram.ram:DoesNotExist")
|
||||
@@ -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.ops.registry import RegisteredOp, op
|
||||
from mirage.resource.base import BaseResource
|
||||
from mirage.resource.ram import RAMResource
|
||||
|
||||
|
||||
def test_resource_register_op():
|
||||
|
||||
@op("read", resource="test", filetype=".custom")
|
||||
async def read_custom(store, path):
|
||||
return b"custom"
|
||||
|
||||
resource = BaseResource()
|
||||
resource.register_op(read_custom)
|
||||
ops = resource.ops_list()
|
||||
assert len(ops) == 1
|
||||
assert isinstance(ops[0], RegisteredOp)
|
||||
assert ops[0].name == "read"
|
||||
assert ops[0].filetype == ".custom"
|
||||
|
||||
|
||||
def test_resource_register_op_empty():
|
||||
resource = BaseResource()
|
||||
assert resource.ops_list() == []
|
||||
|
||||
|
||||
def test_ram_resource_registers_ops():
|
||||
resource = RAMResource()
|
||||
ops = resource.ops_list()
|
||||
assert len(ops) > 0
|
||||
names = {ro.name for ro in ops}
|
||||
assert "read" in names
|
||||
assert "write" in names
|
||||
assert "stat" in names
|
||||
@@ -0,0 +1,55 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import asyncio
|
||||
|
||||
from mirage.resource.ram import RAMResource
|
||||
|
||||
|
||||
def _make_backend():
|
||||
b = RAMResource()
|
||||
b._store.dirs.add("/data")
|
||||
b._store.files["/data/file.txt"] = b"hello world"
|
||||
return b
|
||||
|
||||
|
||||
def test_cat_updates_stats():
|
||||
b = _make_backend()
|
||||
data = asyncio.run(b.read_bytes("/data/file.txt"))
|
||||
assert data == b"hello world"
|
||||
|
||||
|
||||
def test_tee_updates_stats():
|
||||
b = _make_backend()
|
||||
asyncio.run(b.write("/data/out.txt", data=b"test data"))
|
||||
assert b._store.files["/data/out.txt"] == b"test data"
|
||||
|
||||
|
||||
def test_callback_receives_events():
|
||||
b = _make_backend()
|
||||
data = asyncio.run(b.read_bytes("/data/file.txt"))
|
||||
assert data == b"hello world"
|
||||
|
||||
|
||||
def test_write_callback_receives_events():
|
||||
b = _make_backend()
|
||||
asyncio.run(b.write("/data/out.txt", data=b"hello"))
|
||||
assert b._store.files["/data/out.txt"] == b"hello"
|
||||
|
||||
|
||||
def test_stats_accumulate():
|
||||
b = _make_backend()
|
||||
asyncio.run(b.read_bytes("/data/file.txt"))
|
||||
asyncio.run(b.read_bytes("/data/file.txt"))
|
||||
assert b._store.files["/data/file.txt"] == b"hello world"
|
||||
@@ -0,0 +1,31 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from mirage.resource.ram.store import RAMStore
|
||||
|
||||
|
||||
def test_memory_store_holds_state():
|
||||
store = RAMStore()
|
||||
assert store.files == {}
|
||||
assert store.dirs == {"/"}
|
||||
assert store.modified == {}
|
||||
|
||||
|
||||
def test_memory_store_custom_state():
|
||||
files = {"/a": b"hello"}
|
||||
dirs = {"/", "/dir"}
|
||||
modified = {"/a": "2024-01-01"}
|
||||
store = RAMStore(files=files, dirs=dirs, modified=modified)
|
||||
assert store.files["/a"] == b"hello"
|
||||
assert "/dir" in store.dirs
|
||||
@@ -0,0 +1,136 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.resource.hf_buckets import HfBucketsResource
|
||||
from mirage.resource.registry import REGISTRY, build_resource
|
||||
|
||||
EXPECTED_RESOURCES = {
|
||||
"ram",
|
||||
"disk",
|
||||
"redis",
|
||||
"s3",
|
||||
"r2",
|
||||
"oci",
|
||||
"supabase",
|
||||
"gcs",
|
||||
"minio",
|
||||
"ceph",
|
||||
"seaweedfs",
|
||||
"wasabi",
|
||||
"backblaze",
|
||||
"digitalocean",
|
||||
"tencent",
|
||||
"aliyun",
|
||||
"scaleway",
|
||||
"qingstor",
|
||||
"github",
|
||||
"github_ci",
|
||||
"linear",
|
||||
"gdocs",
|
||||
"gsheets",
|
||||
"gslides",
|
||||
"gdrive",
|
||||
"slack",
|
||||
"discord",
|
||||
"gmail",
|
||||
"trello",
|
||||
"mongodb",
|
||||
"postgres",
|
||||
"lancedb",
|
||||
"qdrant",
|
||||
"notion",
|
||||
"langfuse",
|
||||
"ssh",
|
||||
"email",
|
||||
"databricks_volume",
|
||||
"hf_buckets",
|
||||
"hf_datasets",
|
||||
"hf_models",
|
||||
"hf_spaces",
|
||||
"nextcloud",
|
||||
"dify",
|
||||
"chroma",
|
||||
"onedrive",
|
||||
}
|
||||
|
||||
|
||||
def test_registry_covers_all_known_resources():
|
||||
assert set(REGISTRY) == EXPECTED_RESOURCES
|
||||
|
||||
|
||||
def test_build_ram_returns_ram_resource():
|
||||
from mirage.resource.ram import RAMResource
|
||||
p = build_resource("ram")
|
||||
assert isinstance(p, RAMResource)
|
||||
|
||||
|
||||
def test_build_disk_takes_raw_kwargs(tmp_path):
|
||||
from mirage.resource.disk import DiskResource
|
||||
p = build_resource("disk", {"root": str(tmp_path)})
|
||||
assert isinstance(p, DiskResource)
|
||||
|
||||
|
||||
def test_build_s3_uses_config_class():
|
||||
from mirage.resource.s3 import S3Resource
|
||||
p = build_resource(
|
||||
"s3", {
|
||||
"bucket": "b",
|
||||
"region": "us-east-1",
|
||||
"aws_access_key_id": "k",
|
||||
"aws_secret_access_key": "s",
|
||||
})
|
||||
assert isinstance(p, S3Resource)
|
||||
assert p.config.bucket == "b"
|
||||
assert p.config.region == "us-east-1"
|
||||
|
||||
|
||||
def test_build_r2_uses_r2_config():
|
||||
from mirage.resource.r2 import R2Resource
|
||||
p = build_resource(
|
||||
"r2", {
|
||||
"bucket": "b",
|
||||
"account_id": "acct",
|
||||
"access_key_id": "k",
|
||||
"secret_access_key": "s",
|
||||
})
|
||||
assert isinstance(p, R2Resource)
|
||||
|
||||
|
||||
def test_build_redis_takes_raw_kwargs():
|
||||
from mirage.resource.redis import RedisResource
|
||||
p = build_resource("redis", {
|
||||
"url": "redis://localhost:6379/15",
|
||||
"key_prefix": "test:",
|
||||
})
|
||||
assert isinstance(p, RedisResource)
|
||||
|
||||
|
||||
def test_unknown_resource_raises_keyerror():
|
||||
with pytest.raises(KeyError, match="unknown resource 'nonsense'"):
|
||||
build_resource("nonsense")
|
||||
|
||||
|
||||
def test_registry_module_import_is_free_of_resource_deps():
|
||||
import importlib
|
||||
import sys
|
||||
if "mirage.resource.registry" in sys.modules:
|
||||
del sys.modules["mirage.resource.registry"]
|
||||
importlib.import_module("mirage.resource.registry")
|
||||
|
||||
|
||||
def test_build_hf_buckets_resource():
|
||||
r = build_resource("hf_buckets", {"bucket": "o/b"})
|
||||
assert isinstance(r, HfBucketsResource)
|
||||
@@ -0,0 +1,304 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import os
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.resource.disk import DiskResource
|
||||
from mirage.resource.ram import RAMResource
|
||||
from mirage.resource.redis import RedisResource
|
||||
from mirage.resource.s3 import S3Config, S3Resource
|
||||
|
||||
REDIS_URL = os.environ.get("REDIS_URL", "")
|
||||
|
||||
# ── RAM ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_ram_get_state_shape():
|
||||
p = RAMResource()
|
||||
p._store.files["/a.txt"] = b"hello"
|
||||
p._store.dirs.add("/sub")
|
||||
state = p.get_state()
|
||||
assert state["type"] == "ram"
|
||||
assert "redacted_fields" not in state
|
||||
assert state["files"] == {"/a.txt": b"hello"}
|
||||
assert "/sub" in state["dirs"]
|
||||
|
||||
|
||||
def test_ram_round_trip():
|
||||
src = RAMResource()
|
||||
src._store.files["/a.txt"] = b"hello"
|
||||
src._store.files["/sub/b.txt"] = b"world"
|
||||
src._store.dirs.add("/sub")
|
||||
state = src.get_state()
|
||||
|
||||
dst = RAMResource()
|
||||
dst.load_state(state)
|
||||
assert dst._store.files == {"/a.txt": b"hello", "/sub/b.txt": b"world"}
|
||||
assert "/sub" in dst._store.dirs
|
||||
|
||||
|
||||
# ── Disk ───────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_disk_get_state_walks_tree(tmp_path):
|
||||
root = tmp_path / "src"
|
||||
root.mkdir()
|
||||
(root / "a.txt").write_bytes(b"hello")
|
||||
(root / "sub").mkdir()
|
||||
(root / "sub" / "b.txt").write_bytes(b"world")
|
||||
p = DiskResource(root=str(root))
|
||||
state = p.get_state()
|
||||
assert state["type"] == "disk"
|
||||
assert "redacted_fields" not in state
|
||||
assert state["files"] == {"a.txt": b"hello", "sub/b.txt": b"world"}
|
||||
|
||||
|
||||
def test_disk_round_trip(tmp_path):
|
||||
src_root = tmp_path / "src"
|
||||
src_root.mkdir()
|
||||
(src_root / "a.txt").write_bytes(b"hello")
|
||||
(src_root / "sub").mkdir()
|
||||
(src_root / "sub" / "b.txt").write_bytes(b"world")
|
||||
state = DiskResource(root=str(src_root)).get_state()
|
||||
|
||||
dst_root = tmp_path / "dst"
|
||||
dst_root.mkdir()
|
||||
DiskResource(root=str(dst_root)).load_state(state)
|
||||
assert (dst_root / "a.txt").read_bytes() == b"hello"
|
||||
assert (dst_root / "sub" / "b.txt").read_bytes() == b"world"
|
||||
|
||||
|
||||
# ── Redis ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.skipif(not REDIS_URL, reason="REDIS_URL not set")
|
||||
def test_redis_round_trip():
|
||||
import redis as sync_redis
|
||||
src_prefix = f"mirage:test:src:{uuid.uuid4().hex}:"
|
||||
dst_prefix = f"mirage:test:dst:{uuid.uuid4().hex}:"
|
||||
src = RedisResource(url=REDIS_URL, key_prefix=src_prefix)
|
||||
dst = RedisResource(url=REDIS_URL, key_prefix=dst_prefix)
|
||||
|
||||
sc = sync_redis.Redis.from_url(REDIS_URL)
|
||||
sc.set(f"{src_prefix}file:/a.txt", b"hello")
|
||||
sc.set(f"{src_prefix}file:/sub/b.txt", b"world")
|
||||
sc.sadd(f"{src_prefix}dir", "/sub")
|
||||
sc.close()
|
||||
|
||||
state = src.get_state()
|
||||
assert state["type"] == "redis"
|
||||
assert state["config"]["url"] == "<REDACTED>"
|
||||
assert state["config"]["key_prefix"] == src_prefix
|
||||
assert state["files"] == {"/a.txt": b"hello", "/sub/b.txt": b"world"}
|
||||
assert "/sub" in state["dirs"]
|
||||
|
||||
dst.load_state(state)
|
||||
|
||||
sc = sync_redis.Redis.from_url(REDIS_URL)
|
||||
try:
|
||||
assert sc.get(f"{dst_prefix}file:/a.txt") == b"hello"
|
||||
assert sc.get(f"{dst_prefix}file:/sub/b.txt") == b"world"
|
||||
assert sc.sismember(f"{dst_prefix}dir", "/sub")
|
||||
finally:
|
||||
# Cleanup
|
||||
for prefix in (src_prefix, dst_prefix):
|
||||
for key in sc.scan_iter(f"{prefix}*"):
|
||||
sc.delete(key)
|
||||
sc.close()
|
||||
|
||||
|
||||
# ── S3 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_s3_get_state_redacts_creds():
|
||||
cfg = S3Config(
|
||||
bucket="my-bucket",
|
||||
region="us-east-1",
|
||||
aws_access_key_id="AKIA-REAL-KEY-FOR-TEST",
|
||||
aws_secret_access_key="REAL-SECRET-KEY-CHARS",
|
||||
)
|
||||
p = S3Resource(cfg)
|
||||
state = p.get_state()
|
||||
assert state["type"] == "s3"
|
||||
assert state["config"]["bucket"] == "my-bucket"
|
||||
assert state["config"]["aws_access_key_id"] == "<REDACTED>"
|
||||
assert state["config"]["aws_secret_access_key"] == "<REDACTED>"
|
||||
assert "redacted_fields" not in state
|
||||
|
||||
|
||||
def test_s3_no_real_creds_in_state():
|
||||
secret = "TOPSECRET-VALUE-XYZ"
|
||||
cfg = S3Config(
|
||||
bucket="b",
|
||||
region="us-east-1",
|
||||
aws_access_key_id="AKIA-OBVIOUS",
|
||||
aws_secret_access_key=secret,
|
||||
)
|
||||
state = S3Resource(cfg).get_state()
|
||||
blob = repr(state)
|
||||
assert secret not in blob
|
||||
assert "AKIA-OBVIOUS" not in blob
|
||||
assert "<REDACTED>" in blob
|
||||
|
||||
|
||||
def test_s3_get_state_without_inline_creds_has_no_redactions():
|
||||
cfg = S3Config(bucket="b", region="us-east-1", aws_profile="dev")
|
||||
state = S3Resource(cfg).get_state()
|
||||
assert "<REDACTED>" not in repr(state)
|
||||
assert "redacted_fields" not in state
|
||||
assert state["config"]["aws_profile"] == "dev"
|
||||
|
||||
|
||||
def test_s3_load_state_is_noop():
|
||||
cfg = S3Config(bucket="b", region="us-east-1")
|
||||
p = S3Resource(cfg)
|
||||
p.load_state({"some": "state"})
|
||||
|
||||
|
||||
# ── all remote/token resources: cred redaction matrix ──────────────────
|
||||
|
||||
|
||||
def _build(mod_path, cls_name, cfg_cls_name, **cfg_kwargs):
|
||||
import importlib
|
||||
mod = importlib.import_module(mod_path)
|
||||
cfg = getattr(mod, cfg_cls_name)(**cfg_kwargs)
|
||||
return getattr(mod, cls_name)(cfg)
|
||||
|
||||
|
||||
REDACTION_CASES = [
|
||||
("mirage.resource.r2", "R2Resource", "R2Config",
|
||||
dict(bucket="b",
|
||||
account_id="acc",
|
||||
access_key_id="AKIA-R2-LEAK",
|
||||
secret_access_key="R2-SECRET-LEAK"),
|
||||
["AKIA-R2-LEAK", "R2-SECRET-LEAK"]),
|
||||
("mirage.resource.oci", "OCIResource", "OCIConfig",
|
||||
dict(bucket="b",
|
||||
namespace="ns",
|
||||
region="us-ashburn-1",
|
||||
access_key_id="OCI-AKIA-LEAK",
|
||||
secret_access_key="OCI-SECRET-LEAK"),
|
||||
["OCI-AKIA-LEAK", "OCI-SECRET-LEAK"]),
|
||||
("mirage.resource.supabase", "SupabaseResource", "SupabaseConfig",
|
||||
dict(bucket="b",
|
||||
region="us-east-1",
|
||||
project_ref="ref",
|
||||
access_key_id="SUPA-AKIA-LEAK",
|
||||
secret_access_key="SUPA-SECRET-LEAK",
|
||||
session_token="SUPA-TOKEN-LEAK"),
|
||||
["SUPA-AKIA-LEAK", "SUPA-SECRET-LEAK", "SUPA-TOKEN-LEAK"]),
|
||||
("mirage.resource.gcs", "GCSResource", "GCSConfig",
|
||||
dict(bucket="b",
|
||||
access_key_id="GCS-AKIA-LEAK",
|
||||
secret_access_key="GCS-SECRET-LEAK"),
|
||||
["GCS-AKIA-LEAK", "GCS-SECRET-LEAK"]),
|
||||
("mirage.resource.gdrive", "GoogleDriveResource", "GoogleDriveConfig",
|
||||
dict(client_id="id",
|
||||
client_secret="GD-SECRET-LEAK",
|
||||
refresh_token="GD-REFRESH-LEAK"),
|
||||
["GD-SECRET-LEAK", "GD-REFRESH-LEAK"]),
|
||||
("mirage.resource.gmail", "GmailResource", "GmailConfig",
|
||||
dict(client_id="id",
|
||||
client_secret="GM-SECRET-LEAK",
|
||||
refresh_token="GM-REFRESH-LEAK"),
|
||||
["GM-SECRET-LEAK", "GM-REFRESH-LEAK"]),
|
||||
("mirage.resource.gdocs", "GDocsResource", "GDocsConfig",
|
||||
dict(client_id="id",
|
||||
client_secret="GDOC-SECRET-LEAK",
|
||||
refresh_token="GDOC-REFRESH-LEAK"),
|
||||
["GDOC-SECRET-LEAK", "GDOC-REFRESH-LEAK"]),
|
||||
("mirage.resource.gsheets", "GSheetsResource", "GSheetsConfig",
|
||||
dict(client_id="id",
|
||||
client_secret="GSH-SECRET-LEAK",
|
||||
refresh_token="GSH-REFRESH-LEAK"),
|
||||
["GSH-SECRET-LEAK", "GSH-REFRESH-LEAK"]),
|
||||
("mirage.resource.gslides", "GSlidesResource", "GSlidesConfig",
|
||||
dict(client_id="id",
|
||||
client_secret="GSL-SECRET-LEAK",
|
||||
refresh_token="GSL-REFRESH-LEAK"),
|
||||
["GSL-SECRET-LEAK", "GSL-REFRESH-LEAK"]),
|
||||
("mirage.resource.slack", "SlackResource", "SlackConfig",
|
||||
dict(token="SLACK-TOKEN-LEAK", search_token="SLACK-SEARCH-LEAK"),
|
||||
["SLACK-TOKEN-LEAK", "SLACK-SEARCH-LEAK"]),
|
||||
("mirage.resource.discord", "DiscordResource", "DiscordConfig",
|
||||
dict(token="DISCORD-TOKEN-LEAK"), ["DISCORD-TOKEN-LEAK"]),
|
||||
("mirage.resource.notion", "NotionResource", "NotionConfig",
|
||||
dict(api_key="NOTION-KEY-LEAK"), ["NOTION-KEY-LEAK"]),
|
||||
("mirage.resource.linear", "LinearResource", "LinearConfig",
|
||||
dict(api_key="LINEAR-KEY-LEAK"), ["LINEAR-KEY-LEAK"]),
|
||||
("mirage.resource.trello", "TrelloResource", "TrelloConfig",
|
||||
dict(api_key="TRELLO-KEY-LEAK", api_token="TRELLO-TOKEN-LEAK"),
|
||||
["TRELLO-KEY-LEAK", "TRELLO-TOKEN-LEAK"]),
|
||||
("mirage.resource.github_ci", "GitHubCIResource", "GitHubCIConfig",
|
||||
dict(token="GHCI-TOKEN-LEAK", owner="o", repo="r"), ["GHCI-TOKEN-LEAK"]),
|
||||
("mirage.resource.email", "EmailResource", "EmailConfig",
|
||||
dict(imap_host="h",
|
||||
smtp_host="h",
|
||||
username="u",
|
||||
password="EMAIL-PWD-LEAK"), ["EMAIL-PWD-LEAK"]),
|
||||
("mirage.resource.langfuse", "LangfuseResource", "LangfuseConfig",
|
||||
dict(public_key="LF-PUB",
|
||||
secret_key="LF-SECRET-LEAK"), ["LF-SECRET-LEAK"]),
|
||||
("mirage.resource.mongodb", "MongoDBResource", "MongoDBConfig",
|
||||
dict(uri="mongodb://user:pwd@h:27017/db"),
|
||||
["mongodb://user:pwd@h:27017/db"]),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mod,cls,cfg_cls,kwargs,leaks",
|
||||
REDACTION_CASES,
|
||||
ids=[c[1] for c in REDACTION_CASES])
|
||||
def test_resource_get_state_redacts(mod, cls, cfg_cls, kwargs, leaks):
|
||||
p = _build(mod, cls, cfg_cls, **kwargs)
|
||||
state = p.get_state()
|
||||
assert "redacted_fields" not in state
|
||||
blob = repr(state)
|
||||
for leaked in leaks:
|
||||
assert leaked not in blob, (f"{cls}: leaked {leaked!r} in state")
|
||||
assert "<REDACTED>" in blob
|
||||
|
||||
|
||||
def test_github_resource_get_state_redacts(monkeypatch):
|
||||
from mirage.resource.github import GitHubConfig, GitHubResource
|
||||
monkeypatch.setattr(
|
||||
"mirage.resource.github.github.fetch_default_branch_sync",
|
||||
lambda *a, **k: "main")
|
||||
monkeypatch.setattr("mirage.resource.github.github.fetch_tree_sync",
|
||||
lambda *a, **k: ({}, False))
|
||||
cfg = GitHubConfig(token="GH-TOKEN-LEAK")
|
||||
p = GitHubResource(cfg, owner="o", repo="r", ref="main")
|
||||
state = p.get_state()
|
||||
assert state["config"]["token"] == "<REDACTED>"
|
||||
assert "redacted_fields" not in state
|
||||
blob = repr(state)
|
||||
assert "GH-TOKEN-LEAK" not in blob
|
||||
assert "<REDACTED>" in blob
|
||||
assert state["owner"] == "o"
|
||||
assert state["repo"] == "r"
|
||||
assert state["ref"] == "main"
|
||||
|
||||
|
||||
def test_ssh_no_redaction_no_override():
|
||||
from mirage.resource.ssh import SSHConfig, SSHResource
|
||||
cfg = SSHConfig(host="example.com", username="me")
|
||||
p = SSHResource(cfg)
|
||||
state = p.get_state()
|
||||
assert "redacted_fields" not in state
|
||||
assert "<REDACTED>" not in repr(state)
|
||||
# Plain config preserved
|
||||
assert state["config"]["host"] == "example.com"
|
||||
assert state["config"]["username"] == "me"
|
||||
@@ -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,51 @@
|
||||
# ========= 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.ram import RAMIndexCacheStore
|
||||
from mirage.resource.trello.config import TrelloConfig
|
||||
from mirage.resource.trello.trello import TrelloResource
|
||||
from mirage.types import ResourceName
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config():
|
||||
return TrelloConfig(api_key="test_key", api_token="test_token")
|
||||
|
||||
|
||||
def test_resource_init(config):
|
||||
resource = TrelloResource(config)
|
||||
assert resource.caches_reads is True
|
||||
|
||||
|
||||
def test_resource_name(config):
|
||||
resource = TrelloResource(config)
|
||||
assert resource.name == ResourceName.TRELLO
|
||||
|
||||
|
||||
def test_resource_accessor(config):
|
||||
resource = TrelloResource(config)
|
||||
assert resource.accessor is not None
|
||||
assert resource.accessor.config is config
|
||||
|
||||
|
||||
def test_resource_index(config):
|
||||
resource = TrelloResource(config)
|
||||
assert isinstance(resource._index, RAMIndexCacheStore)
|
||||
|
||||
|
||||
def test_resource_commands_registered(config):
|
||||
resource = TrelloResource(config)
|
||||
assert len(resource._commands) >= 10
|
||||
Reference in New Issue
Block a user