chore: import upstream snapshot with attribution
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:30:44 +08:00
commit bcbd1bdb22
5748 changed files with 562488 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
+276
View File
@@ -0,0 +1,276 @@
# ========= 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 contextlib import ExitStack
from datetime import datetime, timezone
from unittest.mock import patch
from mirage.core.ram.mkdir import mkdir as mem_mkdir
from mirage.core.ram.write import write_bytes as mem_write
from mirage.resource.disk import DiskResource
from mirage.resource.ram import RAMResource
from mirage.resource.s3 import S3Config, S3Resource
from mirage.types import MountMode
from mirage.workspace import Workspace
LAST_MODIFIED = datetime(2026, 3, 31, tzinfo=timezone.utc)
_CORE_MODULES = [
"mirage.core.s3.read",
"mirage.core.s3.write",
"mirage.core.s3.stat",
"mirage.core.s3.readdir",
"mirage.core.s3.find",
"mirage.core.s3.du",
"mirage.core.s3.stream",
"mirage.core.s3.copy",
"mirage.core.s3.rename",
"mirage.core.s3.unlink",
"mirage.core.s3.rmdir",
"mirage.core.s3.rm",
"mirage.core.s3.mkdir",
"mirage.core.s3.create",
"mirage.core.s3.truncate",
]
class AsyncMockBody:
def __init__(self, data: bytes) -> None:
self._data = data
async def read(self) -> bytes:
return self._data
async def iter_chunks(self, chunk_size: int = 8192):
for i in range(0, len(self._data), chunk_size):
yield self._data[i:i + chunk_size]
class AsyncMockPaginator:
def __init__(self, objects: dict[str, bytes]) -> None:
self.objects = objects
async def paginate(self,
Bucket: str,
Prefix: str = "",
Delimiter: str | None = None):
del Bucket
if Delimiter == "/":
yield _paginate_directory(self.objects, Prefix)
else:
yield _paginate_flat(self.objects, Prefix)
class AsyncMockS3Client:
def __init__(self, objects: dict[str, bytes]) -> None:
self.objects = objects
async def get_object(self,
Bucket: str,
Key: str,
Range: str | None = None) -> dict:
del Bucket
if Key not in self.objects:
raise _mock_s3_error("NoSuchKey")
data = self.objects[Key]
if Range is not None:
data = _slice_range(data, Range)
return {"Body": AsyncMockBody(data), "ETag": f'"{Key}"'}
async def head_object(self, Bucket: str, Key: str) -> dict:
del Bucket
if Key not in self.objects:
raise _mock_s3_error("NoSuchKey")
return {
"ContentLength": len(self.objects[Key]),
"LastModified": LAST_MODIFIED,
"ETag": f'"{Key}"',
}
def get_paginator(self, name: str) -> AsyncMockPaginator:
assert name == "list_objects_v2"
return AsyncMockPaginator(self.objects)
async def put_object(self, Bucket: str, Key: str, Body: bytes) -> None:
self.objects[Key] = Body
async def delete_object(self, Bucket: str, Key: str) -> None:
self.objects.pop(Key, None)
async def copy_object(self, Bucket: str, CopySource: dict,
Key: str) -> None:
src_key = CopySource["Key"]
if src_key in self.objects:
self.objects[Key] = self.objects[src_key]
async def delete_objects(self, Bucket: str, Delete: dict) -> None:
for obj in Delete.get("Objects", []):
self.objects.pop(obj["Key"], None)
async def __aenter__(self):
return self
async def __aexit__(self, *args):
pass
class MockAsyncSession:
def __init__(self, objects: dict[str, bytes]) -> None:
self._client = AsyncMockS3Client(objects)
def client(self, **kwargs):
return self._client
def _mock_s3_error(code: str) -> Exception:
exc = Exception(code)
exc.response = {"Error": {"Code": code}}
return exc
def _paginate_directory(objects, prefix):
common_prefixes: set[str] = set()
contents: list[dict[str, object]] = []
for key, data in sorted(objects.items()):
if not key.startswith(prefix):
continue
relative = key[len(prefix):]
if not relative:
continue
if "/" in relative:
child = relative.split("/", 1)[0]
common_prefixes.add(prefix + child + "/")
continue
contents.append({"Key": key, "Size": len(data)})
return {
"CommonPrefixes": [{
"Prefix": v
} for v in sorted(common_prefixes)],
"Contents": contents,
}
def _paginate_flat(objects, prefix):
return {
"Contents": [{
"Key": k,
"Size": len(v)
} for k, v in sorted(objects.items()) if k.startswith(prefix)]
}
def _slice_range(data: bytes, range_spec: str) -> bytes:
if not range_spec.startswith("bytes="):
return data
bounds = range_spec.removeprefix("bytes=").split("-", 1)
start = int(bounds[0]) if bounds[0] else 0
end = int(bounds[1]) if bounds[1] else len(data) - 1
return data[start:end + 1]
def patch_async_session(objects):
mock_session = MockAsyncSession(objects)
stack = ExitStack()
for mod in _CORE_MODULES:
stack.enter_context(
patch(f"{mod}.async_session", return_value=mock_session))
return stack
def make_s3_ws(objects: dict[str, bytes]) -> Workspace:
config = S3Config(
bucket="test-bucket",
region="us-east-1",
aws_access_key_id="fake",
aws_secret_access_key="fake",
)
resource = S3Resource(config)
return Workspace(
{"/data": (resource, MountMode.WRITE)},
mode=MountMode.WRITE,
)
def make_memory_ws() -> tuple[Workspace, RAMResource]:
resource = RAMResource()
ws = Workspace(
{"/data": (resource, MountMode.WRITE)},
mode=MountMode.WRITE,
)
return ws, resource
def make_disk_ws(tmp_path) -> tuple[Workspace, object]:
disk_root = tmp_path / "disk_root"
disk_root.mkdir()
resource = DiskResource(root=str(disk_root))
ws = Workspace(
{"/data": (resource, MountMode.WRITE)},
mode=MountMode.WRITE,
)
return ws, disk_root
def memory_create_file(resource: RAMResource, path: str, content: bytes):
accessor = resource.accessor
parts = path.strip("/").split("/")
for i in range(1, len(parts)):
d = "/" + "/".join(parts[:i])
if d not in accessor.store.dirs:
try:
asyncio.run(mem_mkdir(accessor, d))
except (FileExistsError, ValueError):
pass
asyncio.run(mem_write(accessor, path, content))
def run(ws: Workspace, cmd: str) -> str:
async def _run():
io = await ws.execute(cmd)
return await io.stdout_str()
return asyncio.run(_run())
def run_exit(ws: Workspace, cmd: str) -> int:
io = asyncio.run(ws.execute(cmd))
return io.exit_code
def make_resource_ws(request, tmp_path, files: dict[str, bytes]):
if request.param == "s3":
objects: dict[str, bytes] = {}
for path, content in files.items():
objects[path] = content
ws = make_s3_ws(objects)
with patch_async_session(objects):
yield ws
elif request.param == "ram":
ws, resource = make_memory_ws()
for path, content in files.items():
memory_create_file(resource, "/" + path, content)
yield ws
else:
ws, disk_root = make_disk_ws(tmp_path)
for path, content in files.items():
full = disk_root / path
full.parent.mkdir(parents=True, exist_ok=True)
full.write_bytes(content)
yield ws
+255
View File
@@ -0,0 +1,255 @@
# ========= 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 collections.abc import AsyncIterator
from contextlib import ExitStack
from unittest.mock import patch
_FAKE_TOKEN = "fake-gdrive-token"
_FAKE_EXPIRES_IN = 9999999999
_FOLDER_MIME = "application/vnd.google-apps.folder"
_FILE_MIME = "application/octet-stream"
_PATCH_TARGETS = {
"list_files": [
"mirage.core.gdrive.readdir.list_files",
],
"list_shared_drives": [
"mirage.core.gdrive.readdir.list_shared_drives",
],
"list_all_files": [
"mirage.core.gdocs.readdir.list_all_files",
"mirage.core.gsheets.readdir.list_all_files",
"mirage.core.gslides.readdir.list_all_files",
],
"download_file": [
"mirage.core.gdrive.read.download_file",
],
"download_file_stream": [
"mirage.core.gdrive.stream.download_file_stream",
],
}
class FakeGDrive:
def __init__(self) -> None:
self._next_id: int = 1
self._children: dict[str, list[dict]] = {"root": []}
self._bytes: dict[str, bytes] = {}
def add_file(self, path: str, content: bytes) -> str:
parts = [p for p in path.strip("/").split("/") if p]
if not parts:
raise ValueError(f"invalid file path: {path}")
parent_id = self._ensure_dirs(parts[:-1])
name = parts[-1]
existing = self._find_child(parent_id, name)
if existing is not None:
self._bytes[existing["id"]] = content
existing["size"] = str(len(content))
existing["modifiedTime"] = self._next_modified_time()
return existing["id"]
file_id = self._mk_id("f")
entry = {
"id": file_id,
"name": name,
"mimeType": _FILE_MIME,
"size": str(len(content)),
"modifiedTime": "2026-04-16T00:00:00Z",
"parents": [parent_id],
}
self._children[parent_id].append(entry)
self._bytes[file_id] = content
return file_id
def remove_file(self, path: str) -> None:
parts = [p for p in path.strip("/").split("/") if p]
if not parts:
return
parent_id = self._lookup_dirs(parts[:-1])
if parent_id is None:
return
name = parts[-1]
children = self._children.get(parent_id, [])
for i, c in enumerate(list(children)):
if c["name"] == name:
self._bytes.pop(c["id"], None)
children.pop(i)
return
def list_children(self, folder_id: str) -> list[dict]:
return list(self._children.get(folder_id, []))
def all_files(self) -> list[dict]:
result: list[dict] = []
for children in self._children.values():
for c in children:
if c["mimeType"] != _FOLDER_MIME:
result.append(c)
return result
def get_bytes(self, file_id: str) -> bytes:
if file_id not in self._bytes:
raise FileNotFoundError(file_id)
return self._bytes[file_id]
def has_id(self, file_id: str) -> bool:
return file_id in self._bytes
def _mk_id(self, kind: str) -> str:
i = self._next_id
self._next_id += 1
return f"{kind}{i:04d}"
def _next_modified_time(self) -> str:
# Bump the fake modifiedTime on every overwrite so fingerprint
# comparisons can detect content mutation in tests.
i = self._next_id
return f"2026-04-16T00:00:{i:02d}Z"
def _ensure_dirs(self, parts: list[str]) -> str:
parent_id = "root"
for p in parts:
existing = self._find_child(parent_id, p)
if existing is not None and existing["mimeType"] == _FOLDER_MIME:
parent_id = existing["id"]
continue
new_id = self._mk_id("d")
entry = {
"id": new_id,
"name": p,
"mimeType": _FOLDER_MIME,
"modifiedTime": "2026-04-16T00:00:00Z",
"parents": [parent_id],
}
self._children[parent_id].append(entry)
self._children[new_id] = []
parent_id = new_id
return parent_id
def _lookup_dirs(self, parts: list[str]) -> str | None:
parent_id = "root"
for p in parts:
existing = self._find_child(parent_id, p)
if existing is None or existing["mimeType"] != _FOLDER_MIME:
return None
parent_id = existing["id"]
return parent_id
def _find_child(self, parent_id: str, name: str) -> dict | None:
for c in self._children.get(parent_id, []):
if c["name"] == name:
return c
return None
def _resolve_fake(token_manager, registry):
if not registry:
return None
for tm, fake in registry:
if tm is token_manager:
return fake
return registry[0][1]
def _build_fakes(registry):
async def fake_refresh(_config):
return _FAKE_TOKEN, _FAKE_EXPIRES_IN
async def fake_list_files(
token_manager,
folder_id: str = "root",
drive_id: str | None = None,
mime_type: str | None = None,
trashed: bool = False,
page_size: int = 1000,
) -> list[dict]:
del drive_id, mime_type, trashed, page_size
fake = _resolve_fake(token_manager, registry)
if fake is None:
return []
return fake.list_children(folder_id)
async def fake_list_all_files(
token_manager,
mime_type: str | None = None,
trashed: bool = False,
page_size: int = 1000,
) -> list[dict]:
del mime_type, trashed, page_size
fake = _resolve_fake(token_manager, registry)
if fake is None:
return []
return fake.all_files()
async def fake_list_shared_drives(token_manager) -> list[dict]:
return []
async def fake_download_file(token_manager, file_id: str) -> bytes:
fake = _resolve_fake(token_manager, registry)
if fake is None:
raise FileNotFoundError(file_id)
if fake.has_id(file_id):
return fake.get_bytes(file_id)
for _, other in registry:
if other.has_id(file_id):
return other.get_bytes(file_id)
raise FileNotFoundError(file_id)
async def fake_download_file_stream(
token_manager,
file_id: str,
chunk_size: int = 8192,
) -> AsyncIterator[bytes]:
data = await fake_download_file(token_manager, file_id)
for i in range(0, len(data), chunk_size):
yield data[i:i + chunk_size]
return {
"refresh": fake_refresh,
"list_files": fake_list_files,
"list_shared_drives": fake_list_shared_drives,
"list_all_files": fake_list_all_files,
"download_file": fake_download_file,
"download_file_stream": fake_download_file_stream,
}
def patch_gdrive(*pairs) -> ExitStack:
"""Patch gdrive HTTP layer with (token_manager, FakeGDrive) pairs.
Args:
*pairs: tuples of (token_manager, FakeGDrive). The right fake is
selected by token_manager identity, so multiple gdrive resources
can coexist with separate file trees.
"""
if len(pairs) == 1 and isinstance(pairs[0], FakeGDrive):
registry = [(None, pairs[0])]
else:
registry = list(pairs)
fakes = _build_fakes(registry)
stack = ExitStack()
stack.enter_context(
patch("mirage.core.google._client.refresh_access_token",
new=fakes["refresh"]))
for name, targets in _PATCH_TARGETS.items():
for target in targets:
try:
stack.enter_context(patch(target, new=fakes[name]))
except (AttributeError, ModuleNotFoundError):
pass
return stack
+272
View File
@@ -0,0 +1,272 @@
# ========= 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 hashlib
from collections import Counter
from contextlib import ExitStack
from datetime import datetime, timezone
from unittest.mock import patch
LAST_MODIFIED = datetime(2026, 3, 31, tzinfo=timezone.utc)
_CORE_MODULES = [
"mirage.core.s3.read",
"mirage.core.s3.write",
"mirage.core.s3.stat",
"mirage.core.s3.readdir",
"mirage.core.s3.find",
"mirage.core.s3.du",
"mirage.core.s3.stream",
"mirage.core.s3.copy",
"mirage.core.s3.rename",
"mirage.core.s3.unlink",
"mirage.core.s3.rmdir",
"mirage.core.s3.rm",
"mirage.core.s3.mkdir",
"mirage.core.s3.create",
"mirage.core.s3.truncate",
]
class _AsyncMockBody:
def __init__(self, data: bytes) -> None:
self._data = data
async def read(self) -> bytes:
return self._data
async def iter_chunks(self, chunk_size: int = 8192):
for i in range(0, len(self._data), chunk_size):
yield self._data[i:i + chunk_size]
def _mock_s3_error(code: str) -> Exception:
exc = Exception(code)
exc.response = {"Error": {"Code": code}}
return exc
def _paginate_directory(objects, prefix):
common_prefixes: set[str] = set()
contents: list[dict[str, object]] = []
for key, data in sorted(objects.items()):
if not key.startswith(prefix):
continue
relative = key[len(prefix):]
if not relative:
contents.append({
"Key": key,
"Size": len(data),
"LastModified": LAST_MODIFIED
})
continue
if "/" in relative:
child = relative.split("/", 1)[0]
common_prefixes.add(prefix + child + "/")
continue
contents.append({
"Key": key,
"Size": len(data),
"LastModified": LAST_MODIFIED
})
return {
"CommonPrefixes": [{
"Prefix": v
} for v in sorted(common_prefixes)],
"Contents": contents,
}
def _paginate_flat(objects, prefix):
return {
"Contents": [{
"Key": k,
"Size": len(v),
"LastModified": LAST_MODIFIED
} for k, v in sorted(objects.items()) if k.startswith(prefix)]
}
def _slice_range(data: bytes, range_spec: str) -> bytes:
if not range_spec.startswith("bytes="):
return data
bounds = range_spec.removeprefix("bytes=").split("-", 1)
start = int(bounds[0]) if bounds[0] else 0
end = int(bounds[1]) if bounds[1] else len(data) - 1
return data[start:end + 1]
class _MultiBucketPaginator:
def __init__(self, buckets: dict[str, dict[str, bytes]]) -> None:
self.buckets = buckets
async def paginate(self,
Bucket: str,
Prefix: str = "",
Delimiter: str | None = None):
objects = self.buckets.get(Bucket, {})
if Delimiter == "/":
yield _paginate_directory(objects, Prefix)
else:
yield _paginate_flat(objects, Prefix)
class MultiBucketS3Client:
def __init__(self,
buckets: dict[str, dict[str, bytes]],
versioned: set[str] | None = None,
etag_suffix: str = "") -> None:
self.buckets = buckets
self.versioned = versioned or set()
self._versions: dict[tuple[str, str], list[tuple[str, bytes]]] = {}
# Non-empty simulates multipart-upload ETags ("<md5>-2"), which are
# NOT the MD5 of the content.
self.etag_suffix = etag_suffix
self.calls: Counter[str] = Counter()
def _etag(self, data: bytes) -> str:
return hashlib.md5(data).hexdigest() + self.etag_suffix
def _objects(self, bucket: str) -> dict[str, bytes]:
if bucket not in self.buckets:
self.buckets[bucket] = {}
return self.buckets[bucket]
def _track(self, bucket: str, key: str) -> str | None:
if bucket not in self.versioned:
return None
current = self.buckets.get(bucket, {}).get(key)
if current is None:
return None
history = self._versions.setdefault((bucket, key), [])
if not history or history[-1][1] != current:
vid = f"v{len(history) + 1}-{hashlib.md5(current).hexdigest()[:8]}"
history.append((vid, current))
return history[-1][0]
async def get_object(self,
Bucket: str,
Key: str,
Range: str | None = None,
VersionId: str | None = None) -> dict:
self.calls["get_object"] += 1
vid_for_resp = self._track(Bucket, Key)
if VersionId is not None:
history = self._versions.get((Bucket, Key), [])
for vid, data in history:
if vid == VersionId:
vid_for_resp = vid
break
else:
raise _mock_s3_error("NoSuchVersion")
else:
objects = self._objects(Bucket)
if Key not in objects:
raise _mock_s3_error("NoSuchKey")
data = objects[Key]
etag = self._etag(data)
if Range is not None:
data = _slice_range(data, Range)
resp: dict = {"Body": _AsyncMockBody(data), "ETag": f'"{etag}"'}
if vid_for_resp is not None:
resp["VersionId"] = vid_for_resp
return resp
async def head_object(self, Bucket: str, Key: str) -> dict:
self.calls["head_object"] += 1
objects = self._objects(Bucket)
if Key not in objects:
raise _mock_s3_error("NoSuchKey")
data = objects[Key]
etag = self._etag(data)
vid = self._track(Bucket, Key)
resp: dict = {
"ContentLength": len(data),
"LastModified": LAST_MODIFIED,
"ETag": f'"{etag}"',
}
if vid is not None:
resp["VersionId"] = vid
return resp
def get_paginator(self, name: str):
assert name == "list_objects_v2"
return _MultiBucketPaginator(self.buckets)
async def put_object(self, Bucket: str, Key: str, Body: bytes) -> None:
self._objects(Bucket)[Key] = Body
async def delete_object(self, Bucket: str, Key: str) -> None:
self._objects(Bucket).pop(Key, None)
async def copy_object(self, Bucket: str, CopySource: dict,
Key: str) -> None:
src_bucket = CopySource.get("Bucket", Bucket)
src_key = CopySource["Key"]
src_objects = self._objects(src_bucket)
if src_key in src_objects:
self._objects(Bucket)[Key] = src_objects[src_key]
async def delete_objects(self, Bucket: str, Delete: dict) -> None:
objects = self._objects(Bucket)
for obj in Delete.get("Objects", []):
objects.pop(obj["Key"], None)
async def list_objects_v2(self,
Bucket: str,
Prefix: str = "",
Delimiter: str = "",
MaxKeys: int = 1000,
**kwargs) -> dict:
del MaxKeys, kwargs
objects = self._objects(Bucket)
if Delimiter == "/":
return _paginate_directory(objects, Prefix)
return _paginate_flat(objects, Prefix)
async def __aenter__(self):
return self
async def __aexit__(self, *args):
pass
class MultiBucketSession:
def __init__(self,
buckets: dict[str, dict[str, bytes]],
versioned: set[str] | None = None,
etag_suffix: str = "") -> None:
self._client = MultiBucketS3Client(buckets,
versioned=versioned,
etag_suffix=etag_suffix)
def client(self, **kwargs):
return self._client
def patch_s3_session(session: MultiBucketSession) -> ExitStack:
stack = ExitStack()
for mod in _CORE_MODULES:
stack.enter_context(patch(f"{mod}.async_session",
return_value=session))
return stack
def patch_s3_multi(buckets: dict[str, dict[str, bytes]],
versioned: set[str] | None = None) -> ExitStack:
return patch_s3_session(MultiBucketSession(buckets, versioned=versioned))
@@ -0,0 +1,91 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
import pytest
from mirage.resource.ram import RAMResource
from mirage.types import MountMode
from mirage.workspace import Workspace
@pytest.fixture
def ws():
mem = RAMResource()
big_content = b"\n".join([f"line {i}".encode() for i in range(10000)])
asyncio.run(mem.write("/big.txt", data=big_content))
asyncio.run(
mem.write("/small.txt", data=b"apple\nbanana\napricot\ncherry\n"))
return Workspace(
{"/data": (mem, MountMode.WRITE)},
mode=MountMode.WRITE,
)
@pytest.mark.asyncio
async def test_pipe_cat_grep(ws):
io = await ws.execute("cat /data/small.txt | grep ap")
result = await io.stdout_str()
assert "apple" in result
assert "apricot" in result
assert "banana" not in result
@pytest.mark.asyncio
async def test_pipe_cat_head_early_termination(ws):
io = await ws.execute("cat /data/big.txt | head -n 5")
result = await io.stdout_str()
lines = result.strip().split("\n")
assert len(lines) == 5
assert lines[0] == "line 0"
@pytest.mark.asyncio
async def test_pipe_cat_grep_sort(ws):
io = await ws.execute("cat /data/small.txt | grep a | sort")
result = await io.stdout_str()
lines = result.strip().split("\n")
assert lines == sorted(lines)
def test_execute_via_asyncio_run(ws):
async def _run():
io = await ws.execute("cat /data/small.txt")
return await io.stdout_str()
assert "apple" in asyncio.run(_run())
@pytest.mark.asyncio
async def test_pipe_cat_wc(ws):
io = await ws.execute("cat /data/small.txt | wc -l")
assert (await io.stdout_str()).strip() == "4"
@pytest.mark.asyncio
async def test_pipe_cat_tail(ws):
io = await ws.execute("cat /data/big.txt | tail -n 3")
result = await io.stdout_str()
lines = result.strip().split("\n")
assert len(lines) == 3
assert lines[-1] == "line 9999"
@pytest.mark.asyncio
async def test_pipe_cat_sort_uniq(ws):
io = await ws.execute("cat /data/small.txt | sort | uniq")
lines = (await io.stdout_str()).strip().split("\n")
assert len(lines) == len(set(lines))
@@ -0,0 +1,33 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from .conftest import make_s3_ws, patch_async_session
@pytest.mark.asyncio
async def test_s3_rm_after_cat_hits_backend_and_evicts_cache():
objects = {"file.txt": b"hello\n"}
with patch_async_session(objects):
ws = make_s3_ws(objects)
first = await ws.execute("cat /data/file.txt")
assert (await first.stdout_str()) == "hello\n"
removed = await ws.execute("rm /data/file.txt")
assert removed.exit_code == 0
assert "file.txt" not in objects
reread = await ws.execute("cat /data/file.txt")
assert reread.exit_code != 0
@@ -0,0 +1,380 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
import os
import uuid
from contextlib import ExitStack
import pytest
from mirage.core.ram.mkdir import mkdir as mem_mkdir
from mirage.core.ram.write import write_bytes as mem_write
from mirage.core.redis.mkdir import mkdir as redis_mkdir
from mirage.core.redis.write import write_bytes as redis_write
from mirage.resource.disk import DiskResource
from mirage.resource.gdrive import GoogleDriveConfig, GoogleDriveResource
from mirage.resource.ram import RAMResource
from mirage.resource.redis import RedisResource
from mirage.resource.s3 import S3Config, S3Resource
from mirage.types import DEFAULT_SESSION_ID, MountMode
from mirage.workspace import Workspace
from tests.integration.gdrive_mock import FakeGDrive, patch_gdrive
from tests.integration.s3_mock import patch_s3_multi
REDIS_URL = os.environ.get("REDIS_URL", "")
WRITABLE = {"ram", "disk", "redis", "s3"}
_PAIRS = [
("ram", "s3"),
("s3", "ram"),
("disk", "s3"),
("s3", "disk"),
("redis", "s3"),
("s3", "redis"),
("s3", "s3"),
("ram", "gdrive"),
("gdrive", "ram"),
("disk", "gdrive"),
("gdrive", "disk"),
("redis", "gdrive"),
("gdrive", "redis"),
("gdrive", "gdrive"),
]
def _supports_write(ptype: str) -> bool:
return ptype in WRITABLE
def _supports_delete(ptype: str) -> bool:
return ptype in WRITABLE
def _make_s3_resource(bucket: str) -> S3Resource:
config = S3Config(bucket=bucket,
region="us-east-1",
aws_access_key_id="testing",
aws_secret_access_key="testing")
return S3Resource(config)
def _make_redis_resource(prefix: str) -> RedisResource:
return RedisResource(url=REDIS_URL, key_prefix=prefix)
def _make_gdrive_resource() -> GoogleDriveResource:
config = GoogleDriveConfig(
client_id="fake-id",
client_secret="fake-secret",
refresh_token="fake-refresh",
)
return GoogleDriveResource(config)
class _MountState:
def __init__(self, ptype: str, mount_path: str, idx: int) -> None:
self.ptype = ptype
self.mount_path = mount_path
self.idx = idx
self.disk_root = None
self.s3_bucket: str | None = None
self.gdrive: FakeGDrive | None = None
self.redis_prefix: str | None = None
self.resource = None
self.accessor = None
def _build_mount(ptype: str, mount_path: str, tmp_path,
idx: int) -> _MountState:
state = _MountState(ptype, mount_path, idx)
if ptype == "ram":
state.resource = RAMResource()
state.accessor = state.resource.accessor
elif ptype == "disk":
root = tmp_path / f"disk{idx}"
root.mkdir()
state.disk_root = root
state.resource = DiskResource(root=str(root))
elif ptype == "redis":
prefix = f"mirage:test:{uuid.uuid4().hex}:{idx}:"
state.redis_prefix = prefix
state.resource = _make_redis_resource(prefix)
elif ptype == "s3":
state.s3_bucket = f"test-bucket-{idx}"
state.resource = _make_s3_resource(state.s3_bucket)
elif ptype == "gdrive":
state.gdrive = FakeGDrive()
state.resource = _make_gdrive_resource()
else:
raise ValueError(f"unknown resource: {ptype}")
return state
async def _populate_file_async(state: _MountState, name: str,
content: bytes) -> None:
if state.ptype == "ram":
parts = ("/" + name).strip("/").split("/")
for i in range(1, len(parts)):
d = "/" + "/".join(parts[:i])
if d not in state.accessor.store.dirs:
try:
await mem_mkdir(state.accessor, d)
except (FileExistsError, ValueError):
pass
await mem_write(state.accessor, "/" + name, content)
elif state.ptype == "disk":
full = state.disk_root / name
full.parent.mkdir(parents=True, exist_ok=True)
full.write_bytes(content)
elif state.ptype == "redis":
parts = ("/" + name).strip("/").split("/")
for i in range(1, len(parts)):
d = "/" + "/".join(parts[:i])
try:
await redis_mkdir(state.resource.accessor, d)
except (FileExistsError, ValueError):
pass
await redis_write(state.resource.accessor, "/" + name, content)
def _populate_file(state: _MountState, name: str, content: bytes,
buckets: dict) -> None:
if state.ptype in ("ram", "disk", "redis"):
asyncio.run(_populate_file_async(state, name, content))
elif state.ptype == "s3":
buckets.setdefault(state.s3_bucket, {})[name] = content
elif state.ptype == "gdrive":
state.gdrive.add_file(name, content)
async def _ls_for_index(ws: Workspace, state: "_MountState",
name: str) -> None:
mount_path = state.mount_path
parts = name.strip("/").split("/")
for i in range(len(parts)):
sub = "/".join(parts[:i])
path = f"{mount_path}/{sub}".rstrip("/") or mount_path
# Files are seeded straight into the backend, bypassing the mirage
# write path that would normally invalidate the parent listing, so a
# previously warmed (now stale) index entry must be dropped before the
# ls re-lists it.
await state.resource.index.invalidate_dir(path)
await ws.execute(f"ls {path}")
class CrossMountEnv:
def __init__(self, ws: Workspace, m1: _MountState, m2: _MountState,
buckets: dict) -> None:
self.ws = ws
self.m1 = m1
self.m2 = m2
self.buckets = buckets
@property
def src_type(self) -> str:
return self.m1.ptype
@property
def dst_type(self) -> str:
return self.m2.ptype
def create_file(self, mount_idx: int, name: str, content: bytes) -> None:
state = self.m1 if mount_idx == 1 else self.m2
_populate_file(state, name, content, self.buckets)
if state.ptype == "gdrive":
asyncio.run(_ls_for_index(self.ws, state, name))
def run(self, cmd: str) -> str:
async def _inner():
io = await self.ws.execute(cmd)
return await io.stdout_str()
return asyncio.run(_inner())
def exit(self, cmd: str) -> int:
io = asyncio.run(self.ws.execute(cmd))
return io.exit_code
def cleanup_redis(self) -> None:
for state in (self.m1, self.m2):
if state.ptype == "redis":
asyncio.run(state.resource._store.clear())
def _pair_id(pair: tuple[str, str]) -> str:
return f"{pair[0]}->{pair[1]}"
def _pair_marks(pair: tuple[str, str]):
if "redis" in pair and not REDIS_URL:
return [pytest.mark.skip(reason="REDIS_URL not set")]
return []
_PARAMS = [
pytest.param(p, id=_pair_id(p), marks=_pair_marks(p)) for p in _PAIRS
]
@pytest.fixture(params=_PARAMS)
def cross(request, tmp_path):
pair = request.param
p1_type, p2_type = pair
m1 = _build_mount(p1_type, "/m1", tmp_path, 1)
m2 = _build_mount(p2_type, "/m2", tmp_path, 2)
ws = Workspace(
{
"/m1": (m1.resource, MountMode.WRITE),
"/m2": (m2.resource, MountMode.WRITE),
},
mode=MountMode.WRITE,
)
ws.get_session(DEFAULT_SESSION_ID).cwd = "/m1"
buckets: dict[str, dict[str, bytes]] = {}
if m1.ptype == "s3":
buckets[m1.s3_bucket] = {}
if m2.ptype == "s3":
buckets[m2.s3_bucket] = {}
env = CrossMountEnv(ws, m1, m2, buckets)
stack = ExitStack()
if "s3" in pair:
stack.enter_context(patch_s3_multi(buckets))
if "gdrive" in pair:
gd_pairs = []
if m1.ptype == "gdrive":
gd_pairs.append((m1.resource._token_manager, m1.gdrive))
if m2.ptype == "gdrive":
gd_pairs.append((m2.resource._token_manager, m2.gdrive))
stack.enter_context(patch_gdrive(*gd_pairs))
with stack:
try:
yield env
finally:
env.cleanup_redis()
def test_cat_cross(cross):
cross.create_file(1, "a.txt", b"aaa\n")
cross.create_file(2, "b.txt", b"bbb\n")
out = cross.run("cat /m1/a.txt /m2/b.txt")
assert "aaa" in out and "bbb" in out
def test_grep_cross(cross):
cross.create_file(1, "a.txt", b"hello world\n")
cross.create_file(2, "b.txt", b"hello there\n")
out = cross.run("grep hello /m1/a.txt /m2/b.txt")
assert "/m1/a.txt:" in out
assert "/m2/b.txt:" in out
def test_head_cross(cross):
cross.create_file(1, "a.txt", b"a1\na2\na3\n")
cross.create_file(2, "b.txt", b"b1\nb2\nb3\n")
out = cross.run("head -n 1 /m1/a.txt /m2/b.txt")
assert "==> /m1/a.txt <==" in out
assert "==> /m2/b.txt <==" in out
def test_wc_cross(cross):
cross.create_file(1, "a.txt", b"line1\nline2\n")
cross.create_file(2, "b.txt", b"only\n")
out = cross.run("wc -l /m1/a.txt /m2/b.txt")
assert "/m1/a.txt" in out and "/m2/b.txt" in out
def test_diff_identical_cross(cross):
cross.create_file(1, "a.txt", b"same\n")
cross.create_file(2, "b.txt", b"same\n")
out = cross.run("diff /m1/a.txt /m2/b.txt")
assert out == ""
def test_diff_different_cross(cross):
cross.create_file(1, "a.txt", b"hello\n")
cross.create_file(2, "b.txt", b"world\n")
out = cross.run("diff /m1/a.txt /m2/b.txt")
assert "hello" in out or "world" in out
def test_cmp_identical_cross(cross):
cross.create_file(1, "a.txt", b"same\n")
cross.create_file(2, "b.txt", b"same\n")
code = cross.exit("cmp /m1/a.txt /m2/b.txt")
assert code == 0
def test_cp_cross(cross):
cross.create_file(1, "src.txt", b"hello\n")
code = cross.exit("cp /m1/src.txt /m2/dst.txt")
if _supports_write(cross.dst_type):
assert code == 0, f"cp failed for {cross.src_type}->{cross.dst_type}"
assert cross.run("cat /m2/dst.txt") == "hello\n"
else:
assert code != 0, (
f"cp into read-only {cross.dst_type} should have failed")
def test_mv_cross(cross):
cross.create_file(1, "src.txt", b"hello\n")
code = cross.exit("mv /m1/src.txt /m2/moved.txt")
if _supports_write(cross.dst_type) and _supports_delete(cross.src_type):
assert code == 0, f"mv failed for {cross.src_type}->{cross.dst_type}"
assert cross.run("cat /m2/moved.txt") == "hello\n"
assert cross.exit("cat /m1/src.txt") != 0
else:
assert code != 0, (
f"mv with read-only end ({cross.src_type}->{cross.dst_type}) "
"should have failed")
def test_cp_recursive_cross(cross):
cross.create_file(1, "tree/a.txt", b"aaa\n")
cross.create_file(1, "tree/sub/b.txt", b"bbb\n")
code = cross.exit("cp -r /m1/tree /m2/copied")
if _supports_write(cross.dst_type):
assert code == 0, (
f"cp -r failed for {cross.src_type}->{cross.dst_type}")
assert cross.run("cat /m2/copied/a.txt") == "aaa\n"
assert cross.run("cat /m2/copied/sub/b.txt") == "bbb\n"
else:
assert code != 0, (
f"cp -r into read-only {cross.dst_type} should have failed")
def test_mv_recursive_cross(cross):
cross.create_file(1, "tree/a.txt", b"aaa\n")
cross.create_file(1, "tree/sub/b.txt", b"bbb\n")
code = cross.exit("mv /m1/tree /m2/moved")
if _supports_write(cross.dst_type) and _supports_delete(cross.src_type):
assert code == 0, (
f"mv -r failed for {cross.src_type}->{cross.dst_type}")
assert cross.run("cat /m2/moved/a.txt") == "aaa\n"
assert cross.run("cat /m2/moved/sub/b.txt") == "bbb\n"
assert cross.exit("cat /m1/tree/a.txt") != 0
else:
assert code != 0, (
f"mv -r with read-only end ({cross.src_type}->{cross.dst_type}) "
"should have failed")
@@ -0,0 +1,107 @@
# ========= 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 .conftest import make_resource_ws, run
FILES = {
"docs/readme.txt": b"hello world\n",
"docs/notes.txt": b"some notes\n",
"src/main.py": b"print('hello')\n",
"src/utils/helpers.py": b"def helper(): pass\n",
"data.json": b'{"key": "value"}\n',
}
@pytest.fixture(params=["ram", "s3", "disk"])
def resource_ws(request, tmp_path):
yield from make_resource_ws(request, tmp_path, FILES)
def test_find_type_f(resource_ws):
result = run(resource_ws, "find /data -type f | sort")
lines = result.strip().splitlines()
assert "/data/data.json" in lines
assert "/data/docs/readme.txt" in lines
assert "/data/src/main.py" in lines
assert len(lines) == 5
def test_find_maxdepth_0(resource_ws):
result = run(resource_ws, "find /data -maxdepth 0 -type f | sort")
lines = [line for line in result.strip().splitlines() if line]
assert lines == []
def test_find_maxdepth_1(resource_ws):
result = run(resource_ws, "find /data -maxdepth 1 -type f | sort")
lines = result.strip().splitlines()
assert "/data/data.json" in lines
assert "/data/docs/notes.txt" not in lines
assert "/data/src/main.py" not in lines
def test_find_maxdepth_2(resource_ws):
result = run(resource_ws, "find /data -maxdepth 2 -type f | sort")
lines = result.strip().splitlines()
assert "/data/data.json" in lines
assert "/data/docs/notes.txt" in lines
assert "/data/docs/readme.txt" in lines
assert "/data/src/main.py" in lines
assert "/data/src/utils/helpers.py" not in lines
def test_find_name_pattern(resource_ws):
result = run(resource_ws, "find /data -name '*.txt' | sort")
lines = result.strip().splitlines()
assert lines == ["/data/docs/notes.txt", "/data/docs/readme.txt"]
def test_find_pipe_sort_pipe_while_read_echo(resource_ws):
cmd = ("find /data -maxdepth 2 -type f | sort | "
"while read f; do echo \"=== $f ===\"; done")
result = run(resource_ws, cmd)
lines = result.strip().splitlines()
for line in lines:
assert line.startswith("=== ") and line.endswith(" ===")
paths = [line.removeprefix("=== ").removesuffix(" ===") for line in lines]
assert paths == sorted(paths)
assert "/data/data.json" in paths
def test_find_pipe_sort_pipe_while_read_file(resource_ws):
cmd = ("find /data -maxdepth 2 -type f -name '*.json' | sort | "
"while read f; do echo \"=== $f ===\"; file $f; done")
result = run(resource_ws, cmd)
lines = result.strip().splitlines()
assert "=== /data/data.json ===" in lines
assert any("json" in line for line in lines)
def test_find_pipe_while_read_echo_content(resource_ws):
cmd = ("find /data -name '*.txt' -type f | sort | "
"while read f; do echo \"FILE: $f\"; done")
result = run(resource_ws, cmd)
lines = result.strip().splitlines()
file_paths = [line.removeprefix("FILE: ") for line in lines]
assert "/data/docs/notes.txt" in file_paths
assert "/data/docs/readme.txt" in file_paths
def test_find_type_d_memory(resource_ws):
result = run(resource_ws, "find /data -type d | sort")
lines = result.strip().splitlines()
if lines:
assert all("/data" in line for line in lines)
@@ -0,0 +1,82 @@
# ========= 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 contextlib import ExitStack
from mirage.resource.s3 import S3Config, S3Resource
from mirage.types import ConsistencyPolicy, MountMode
from mirage.workspace import Workspace
from tests.integration.s3_mock import patch_s3_multi
def _make_ws(consistency: ConsistencyPolicy) -> Workspace:
config = S3Config(
bucket="test-bucket",
region="us-east-1",
aws_access_key_id="fake",
aws_secret_access_key="fake",
)
resource = S3Resource(config)
return Workspace(
{"/data": (resource, MountMode.WRITE)},
mode=MountMode.WRITE,
consistency=consistency,
)
def test_s3_always_refetches_after_external_mutation():
store = {"file.txt": b"v1"}
stack = ExitStack()
stack.enter_context(patch_s3_multi({"test-bucket": store}))
try:
ws = _make_ws(ConsistencyPolicy.ALWAYS)
async def run() -> tuple[bytes, bytes]:
io1 = await ws.execute("cat /data/file.txt")
first = await io1.materialize_stdout()
store["file.txt"] = b"v2"
io2 = await ws.execute("cat /data/file.txt")
second = await io2.materialize_stdout()
return first, second
first, second = asyncio.run(run())
assert first == b"v1"
assert second == b"v2", (
"S3 ALWAYS must refetch after external write to the mocked store")
finally:
stack.close()
def test_s3_lazy_serves_cache():
store = {"file.txt": b"v1"}
stack = ExitStack()
stack.enter_context(patch_s3_multi({"test-bucket": store}))
try:
ws = _make_ws(ConsistencyPolicy.LAZY)
async def run() -> tuple[bytes, bytes]:
io1 = await ws.execute("cat /data/file.txt")
first = await io1.materialize_stdout()
store["file.txt"] = b"v2"
io2 = await ws.execute("cat /data/file.txt")
second = await io2.materialize_stdout()
return first, second
first, second = asyncio.run(run())
assert first == b"v1"
assert second == b"v1", (
"S3 LAZY must serve cached bytes even after store mutation")
finally:
stack.close()
@@ -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
from contextlib import ExitStack
from mirage.resource.s3 import S3Config, S3Resource
from mirage.types import ConsistencyPolicy, MountMode
from mirage.workspace import Workspace
from tests.integration.s3_mock import patch_s3_multi
def _make_ws(consistency: ConsistencyPolicy) -> Workspace:
config = S3Config(
bucket="shared-bucket",
region="us-east-1",
aws_access_key_id="fake",
aws_secret_access_key="fake",
)
resource = S3Resource(config)
return Workspace(
{"/data": (resource, MountMode.WRITE)},
mode=MountMode.WRITE,
consistency=consistency,
)
def test_two_workspaces_always_sees_other_writers_update():
store = {"file.txt": b"v1"}
stack = ExitStack()
stack.enter_context(patch_s3_multi({"shared-bucket": store}))
try:
ws_a = _make_ws(ConsistencyPolicy.ALWAYS)
ws_b = _make_ws(ConsistencyPolicy.ALWAYS)
async def run() -> tuple[bytes, bytes]:
io_b1 = await ws_b.execute("cat /data/file.txt")
b_first = await io_b1.materialize_stdout()
await ws_a.execute('echo -n "v2" > /data/file.txt')
io_b2 = await ws_b.execute("cat /data/file.txt")
b_second = await io_b2.materialize_stdout()
return b_first, b_second
b_first, b_second = asyncio.run(run())
assert b_first == b"v1"
assert b_second == b"v2", (
"Workspace B under ALWAYS must see Workspace A's write "
"via fingerprint mismatch; got stale cached bytes")
finally:
stack.close()
def test_two_workspaces_lazy_may_serve_stale_after_other_writer():
store = {"file.txt": b"v1"}
stack = ExitStack()
stack.enter_context(patch_s3_multi({"shared-bucket": store}))
try:
ws_a = _make_ws(ConsistencyPolicy.LAZY)
ws_b = _make_ws(ConsistencyPolicy.LAZY)
async def run() -> bytes:
io_b1 = await ws_b.execute("cat /data/file.txt")
await io_b1.materialize_stdout()
await ws_a.execute('echo -n "v2" > /data/file.txt')
io_b2 = await ws_b.execute("cat /data/file.txt")
return await io_b2.materialize_stdout()
b_second = asyncio.run(run())
assert b_second in (b"v1", b"v2"), (
"LAZY is allowed to serve cached bytes; this just documents "
"the trade-off (cache was populated before A's write)")
finally:
stack.close()
@@ -0,0 +1,76 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage.resource.gdrive import GoogleDriveConfig, GoogleDriveResource
from mirage.types import ConsistencyPolicy, MountMode
from mirage.workspace import Workspace
from tests.integration.gdrive_mock import FakeGDrive, patch_gdrive
@pytest.mark.asyncio
async def test_gdrive_always_refetches_after_external_mutation():
fake = FakeGDrive()
fake.add_file("file.txt", b"v1")
config = GoogleDriveConfig(
client_id="fake-id",
client_secret="fake-secret",
refresh_token="fake-refresh",
)
resource = GoogleDriveResource(config)
ws = Workspace(
{"/gd": (resource, MountMode.WRITE)},
mode=MountMode.WRITE,
consistency=ConsistencyPolicy.ALWAYS,
)
with patch_gdrive(fake):
await ws.execute("ls /gd")
io1 = await ws.execute("cat /gd/file.txt")
assert (await io1.materialize_stdout()) == b"v1"
fake.add_file("file.txt", b"v2-external")
await ws.execute("ls /gd")
io2 = await ws.execute("cat /gd/file.txt")
assert (await io2.materialize_stdout()) == b"v2-external", (
"GDrive ALWAYS must refetch after modifiedTime changes")
@pytest.mark.asyncio
async def test_gdrive_lazy_may_serve_stale():
fake = FakeGDrive()
fake.add_file("file.txt", b"v1")
config = GoogleDriveConfig(
client_id="fake-id",
client_secret="fake-secret",
refresh_token="fake-refresh",
)
resource = GoogleDriveResource(config)
ws = Workspace(
{"/gd": (resource, MountMode.WRITE)},
mode=MountMode.WRITE,
consistency=ConsistencyPolicy.LAZY,
)
with patch_gdrive(fake):
await ws.execute("ls /gd")
io1 = await ws.execute("cat /gd/file.txt")
assert (await io1.materialize_stdout()) == b"v1"
fake.add_file("file.txt", b"v2-external")
io2 = await ws.execute("cat /gd/file.txt")
got = await io2.materialize_stdout()
assert got in (b"v1", b"v2-external"), (
"LAZY allowed to serve cache; just confirming no crash")
@@ -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 mirage.resource.gdrive import GoogleDriveConfig, GoogleDriveResource
from mirage.types import MountMode
from mirage.workspace import Workspace
from tests.integration.gdrive_mock import FakeGDrive, patch_gdrive
@pytest.fixture
def gdrive_ws():
fake = FakeGDrive()
fake.add_file("hello.txt", b"hello world\n")
fake.add_file("data/numbers.txt", b"one\ntwo\nthree\n")
config = GoogleDriveConfig(
client_id="fake-id",
client_secret="fake-secret",
refresh_token="fake-refresh",
)
resource = GoogleDriveResource(config)
ws = Workspace({"/gd": resource}, mode=MountMode.READ)
with patch_gdrive(fake):
yield ws, fake
@pytest.mark.asyncio
async def test_gdrive_mock_cat(gdrive_ws):
ws, _ = gdrive_ws
await ws.execute("ls /gd")
r = await ws.execute("cat /gd/hello.txt")
assert (await r.stdout_str()) == "hello world\n", (
f"exit={r.exit_code} stderr={await r.stderr_str()!r}")
@pytest.mark.asyncio
async def test_gdrive_mock_ls(gdrive_ws):
ws, _ = gdrive_ws
r = await ws.execute("ls /gd")
out = await r.stdout_str()
assert "hello.txt" in out
assert "data" in out
@pytest.mark.asyncio
async def test_gdrive_mock_grep(gdrive_ws):
ws, _ = gdrive_ws
await ws.execute("ls /gd")
await ws.execute("ls /gd/data")
r = await ws.execute("grep two /gd/data/numbers.txt")
assert "two" in (await r.stdout_str())
@@ -0,0 +1,156 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage.resource.gcs import GCSConfig, GCSResource
from mirage.resource.ram import RAMResource
from mirage.types import MountMode
from mirage.workspace import Workspace
from .conftest import make_s3_ws, patch_async_session
S3_OBJECTS = {
"data/report.txt": b"line1\nline2\nline3\n",
"data/notes.txt": b"note1\nnote2\n",
"data/config.json": b'{"key": "value"}\n',
"data/metrics.csv": b"a,b\n1,2\n3,4\n",
"archive/2026/deep.txt": b"deep\n",
}
@pytest.fixture
def s3_ws():
return make_s3_ws(S3_OBJECTS)
@pytest.fixture
def gcs_ws():
config = GCSConfig(
bucket="test-bucket",
access_key_id="GOOG_FAKE",
secret_access_key="fake_secret",
)
resource = GCSResource(config)
return Workspace(
{"/gcs": (resource, MountMode.WRITE)},
mode=MountMode.WRITE,
)
@pytest.fixture
def multi_ws():
config = GCSConfig(
bucket="test-bucket",
access_key_id="GOOG_FAKE",
secret_access_key="fake_secret",
)
return Workspace(
{
"/gcs": (GCSResource(config), MountMode.WRITE),
"/tmp": (RAMResource(), MountMode.WRITE),
},
mode=MountMode.WRITE,
)
async def _run(ws, cmd):
io = await ws.execute(cmd)
return await io.stdout_str(), io
@pytest.mark.asyncio
async def test_s3_echo_glob_expands(s3_ws):
with patch_async_session(S3_OBJECTS):
out, io = await _run(s3_ws, "echo /data/data/*.txt")
assert io.exit_code == 0
assert "report.txt" in out
assert "notes.txt" in out
assert "config.json" not in out
@pytest.mark.asyncio
async def test_s3_for_loop_glob(s3_ws):
with patch_async_session(S3_OBJECTS):
out, io = await _run(
s3_ws, "for f in /data/data/*.txt; do echo file:$f; done")
assert io.exit_code == 0
assert "file:/data/data/report.txt" in out
assert "file:/data/data/notes.txt" in out
@pytest.mark.asyncio
async def test_s3_grep_glob(s3_ws):
with patch_async_session(S3_OBJECTS):
out, io = await _run(s3_ws, "grep line /data/data/*.txt")
assert io.exit_code == 0
assert "line1" in out
@pytest.mark.asyncio
async def test_gcs_echo_glob_expands(gcs_ws):
with patch_async_session(S3_OBJECTS):
out, io = await _run(gcs_ws, "echo /gcs/data/*.txt")
assert io.exit_code == 0
assert "report.txt" in out
assert "notes.txt" in out
@pytest.mark.asyncio
async def test_gcs_for_loop_glob(gcs_ws):
with patch_async_session(S3_OBJECTS):
out, io = await _run(
gcs_ws, "for f in /gcs/data/*.txt; do echo file:$f; done")
assert io.exit_code == 0
assert "report.txt" in out
assert "notes.txt" in out
@pytest.mark.asyncio
async def test_gcs_grep_no_match_exit_code(gcs_ws):
with patch_async_session(S3_OBJECTS):
out, io = await _run(gcs_ws, "grep NONEXISTENT /gcs/data/report.txt")
assert io.exit_code == 1
@pytest.mark.asyncio
async def test_gcs_grep_match_exit_code(gcs_ws):
with patch_async_session(S3_OBJECTS):
out, io = await _run(gcs_ws, "grep line1 /gcs/data/report.txt")
assert io.exit_code == 0
assert "line1" in out
@pytest.mark.asyncio
async def test_s3_glob_no_match_keeps_literal(s3_ws):
with patch_async_session(S3_OBJECTS):
out, io = await _run(s3_ws, "echo /data/data/*.xyz")
assert out.strip() == "/data/data/*.xyz"
@pytest.mark.asyncio
async def test_cross_mount_cp_with_gcs(multi_ws):
with patch_async_session(S3_OBJECTS):
await multi_ws.execute("cp /gcs/data/report.txt /tmp/r.txt")
out, io = await _run(multi_ws, "cat /tmp/r.txt")
assert "line1" in out
assert io.exit_code == 0
@pytest.mark.asyncio
async def test_s3_cat_single_from_glob(s3_ws):
with patch_async_session(S3_OBJECTS):
out, io = await _run(s3_ws, "cat /data/data/*.csv")
assert io.exit_code == 0
assert "a,b" in out
@@ -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 asyncio
import pytest
from mirage.resource.ram import RAMResource
from mirage.types import MountMode
from mirage.workspace import Workspace
@pytest.fixture
def ws():
mem = RAMResource()
asyncio.run(
mem.write("/big.txt",
data=b"\n".join(f"line {i}".encode() for i in range(10000))))
asyncio.run(
mem.write("/small.txt", data=b"apple\nbanana\napricot\ncherry\n"))
asyncio.run(mem.write("/dupes.txt", data=b"a\na\nb\nb\nc\n"))
asyncio.run(
mem.write("/csv.txt",
data=b"name,age,city\nalice,30,nyc\nbob,25,sf\n"))
return Workspace(
{"/data": (mem, MountMode.WRITE)},
mode=MountMode.WRITE,
)
@pytest.mark.asyncio
async def test_cat_grep_head_streams(ws):
io = await ws.execute("cat /data/big.txt | grep 'line 1' | head -n 3")
lines = (await io.stdout_str()).strip().split("\n")
assert len(lines) == 3
@pytest.mark.asyncio
async def test_cat_head_early_termination(ws):
io = await ws.execute("cat /data/big.txt | head -n 5")
lines = (await io.stdout_str()).strip().split("\n")
assert len(lines) == 5
assert lines[0] == "line 0"
@pytest.mark.asyncio
async def test_cat_cut_head(ws):
io = await ws.execute("cat /data/csv.txt | cut -d , -f 1 | head -n 2")
lines = (await io.stdout_str()).strip().split("\n")
assert len(lines) == 2
assert lines[0] == "name"
assert lines[1] == "alice"
@pytest.mark.asyncio
async def test_cat_sort_head(ws):
io = await ws.execute("cat /data/small.txt | sort | head -n 2")
lines = (await io.stdout_str()).strip().split("\n")
assert len(lines) == 2
assert lines == sorted(lines)
@pytest.mark.asyncio
async def test_cat_uniq(ws):
io = await ws.execute("cat /data/dupes.txt | uniq")
lines = (await io.stdout_str()).strip().split("\n")
assert lines == ["a", "b", "c"]
@pytest.mark.asyncio
async def test_cat_tr_grep(ws):
io = await ws.execute("cat /data/small.txt | tr a A | grep Ap")
result = await io.stdout_str()
assert "Apple" in result or "Apricot" in result
@pytest.mark.asyncio
async def test_cat_grep_wc_l(ws):
io = await ws.execute("cat /data/small.txt | grep a | wc -l")
assert (await io.stdout_str()).strip() == "3"
@pytest.mark.asyncio
async def test_find_path_output(ws):
io = await ws.execute("find /data -name '*.txt'")
result = await io.stdout_str()
assert "/data/" in result
def test_execute_via_asyncio_run(ws):
async def _run():
io = await ws.execute("cat /data/small.txt | head -n 2")
return (await io.stdout_str()).strip().split("\n")
lines = asyncio.run(_run())
assert len(lines) == 2
@@ -0,0 +1,76 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import json
import os
import pytest
from mirage.resource.notion.config import NotionConfig
from mirage.resource.notion.notion import NotionResource
pytestmark = pytest.mark.skipif(
not os.environ.get("NOTION_API_KEY"),
reason="NOTION_API_KEY not set",
)
@pytest.fixture
def config():
return NotionConfig(api_key=os.environ["NOTION_API_KEY"])
@pytest.fixture
def resource(config):
return NotionResource(config)
@pytest.mark.asyncio
async def test_readdir_root(resource):
from mirage.core.notion.readdir import readdir
entries = await readdir(resource.accessor, "/", resource.index)
assert any("pages" in e for e in entries)
@pytest.mark.asyncio
async def test_readdir_pages(resource):
from mirage.core.notion.readdir import readdir
entries = await readdir(resource.accessor, "/pages", resource.index)
assert len(entries) > 0
@pytest.mark.asyncio
async def test_read_page_json(resource):
from mirage.core.notion.read import read
from mirage.core.notion.readdir import readdir
pages = await readdir(resource.accessor, "/pages", resource.index)
if not pages:
pytest.skip("No pages found")
first_page = pages[0]
data = await read(resource.accessor, f"{first_page}/page.json",
resource.index)
page = json.loads(data)
assert "page_id" in page
assert "title" in page
assert "url" in page
assert "markdown" in page
assert "blocks" in page
assert isinstance(page["blocks"], list)
@pytest.mark.asyncio
async def test_search(resource):
from mirage.core.notion.pages import search_pages
results = await search_pages(resource.config, query="", page_size=5)
assert isinstance(results, list)
+658
View File
@@ -0,0 +1,658 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
import pytest
from mirage.resource.ram import RAMResource
from mirage.types import DEFAULT_SESSION_ID, MountMode
from mirage.workspace import Workspace
@pytest.fixture
def ws():
mem = RAMResource()
asyncio.run(mem.write("/hello.txt", data=b"hello world\n"))
asyncio.run(mem.write("/numbers.txt", data=b"3\n1\n2\n1\n3\n"))
asyncio.run(
mem.write(
"/log.txt",
data=b"INFO start\nERROR fail\nINFO ok\nERROR bad\nINFO done\n"))
asyncio.run(mem.mkdir("/subdir"))
asyncio.run(mem.write("/subdir/a.txt", data=b"aaa\n"))
asyncio.run(mem.write("/subdir/b.txt", data=b"bbb\n"))
asyncio.run(mem.write("/config.json", data=b'{"key": "value"}\n'))
lines = "\n".join(f"row {i}" for i in range(5000)) + "\n"
asyncio.run(mem.write("/big.txt", data=lines.encode()))
ws = Workspace(
{"/data": (mem, MountMode.WRITE)},
mode=MountMode.WRITE,
)
ws.get_session(DEFAULT_SESSION_ID).cwd = "/"
return ws
# --- Pipes ---
@pytest.mark.asyncio
async def test_pipe_grep_sort_uniq(ws):
io = await ws.execute("cat /data/numbers.txt | sort | uniq")
lines = (await io.stdout_str()).strip().split("\n")
assert lines == ["1", "2", "3"]
@pytest.mark.asyncio
async def test_pipe_grep_wc(ws):
io = await ws.execute("grep ERROR /data/log.txt | wc -l")
assert (await io.stdout_str()).strip() == "2"
@pytest.mark.asyncio
async def test_pipe_head_stops_early(ws):
io = await ws.execute("cat /data/big.txt | head -n 3")
lines = (await io.stdout_str()).strip().split("\n")
assert len(lines) == 3
assert lines[0] == "row 0"
@pytest.mark.asyncio
async def test_pipe_tail(ws):
io = await ws.execute("cat /data/log.txt | tail -n 2")
lines = (await io.stdout_str()).strip().split("\n")
assert len(lines) == 2
assert lines[-1] == "INFO done"
@pytest.mark.asyncio
async def test_triple_pipe(ws):
io = await ws.execute("cat /data/log.txt | grep INFO | head -n 2")
lines = (await io.stdout_str()).strip().split("\n")
assert len(lines) == 2
assert all("INFO" in line for line in lines)
# --- Control flow ---
@pytest.mark.asyncio
async def test_and_success(ws):
io = await ws.execute("cat /data/hello.txt && echo done")
assert b"done" in io.stdout
@pytest.mark.asyncio
async def test_and_failure_short_circuits(ws):
io = await ws.execute(
"grep NONEXISTENT /data/hello.txt && echo should_not_appear")
assert b"should_not_appear" not in (io.stdout or b"")
@pytest.mark.asyncio
async def test_or_fallback(ws):
io = await ws.execute("grep NONEXISTENT /data/hello.txt || echo fallback")
assert b"fallback" in io.stdout
@pytest.mark.asyncio
async def test_semicolon_runs_both(ws):
io = await ws.execute("echo first ; echo second")
assert b"second" in io.stdout
# --- Redirects ---
@pytest.mark.asyncio
async def test_redirect_stdout_to_file(ws):
await ws.execute("echo written > /data/out.txt")
io = await ws.execute("cat /data/out.txt")
assert b"written" in io.stdout
@pytest.mark.asyncio
async def test_redirect_append(ws):
await ws.execute("echo line1 > /data/append.txt")
await ws.execute("echo line2 >> /data/append.txt")
io = await ws.execute("cat /data/append.txt")
out = await io.stdout_str()
assert "line1" in out
assert "line2" in out
@pytest.mark.asyncio
async def test_redirect_on_or_chain(ws):
io = await ws.execute("grep hello /data/hello.txt > /data/out.txt || "
"echo fallback > /data/out.txt; "
"cat /data/out.txt")
assert "hello" in (await io.stdout_str())
@pytest.mark.asyncio
async def test_redirect_on_and_chain(ws):
io = await ws.execute("echo first > /data/chain.txt && "
"echo second >> /data/chain.txt; "
"cat /data/chain.txt")
assert "first" in (await io.stdout_str())
assert "second" in (await io.stdout_str())
@pytest.mark.asyncio
async def test_redirect_stdin(ws):
io = await ws.execute("grep world < /data/hello.txt")
assert b"world" in io.stdout
@pytest.mark.asyncio
async def test_heredoc(ws):
io = await ws.execute("cat << EOF\nhello heredoc\nEOF")
assert b"hello heredoc" in io.stdout
# --- Subshell isolation ---
@pytest.mark.asyncio
async def test_subshell_cd_isolated(ws):
await ws.execute("cd /data")
await ws.execute("(cd /data/subdir)")
assert ws.get_session(DEFAULT_SESSION_ID).cwd == "/data"
@pytest.mark.asyncio
async def test_subshell_export_isolated(ws):
await ws.execute("(export LEAK=yes)")
assert "LEAK" not in ws.get_session(DEFAULT_SESSION_ID).env
@pytest.mark.asyncio
async def test_subshell_inherits_parent_env(ws):
await ws.execute("export INHERITED=true")
io = await ws.execute("(printenv INHERITED)")
assert b"true" in io.stdout
@pytest.mark.asyncio
async def test_nested_subshell(ws):
await ws.execute("((export DEEP=yes))")
assert "DEEP" not in ws.get_session(DEFAULT_SESSION_ID).env
# --- Background jobs ---
@pytest.mark.asyncio
async def test_background_basic(ws):
await ws.execute("cat /data/hello.txt &")
io = await ws.execute("wait %1")
assert b"hello" in io.stdout
@pytest.mark.asyncio
async def test_background_isolation_env(ws):
await ws.execute("export BG_VAR=leaked &")
await ws.execute("wait %1")
assert "BG_VAR" not in ws.get_session(DEFAULT_SESSION_ID).env
@pytest.mark.asyncio
async def test_background_isolation_cwd(ws):
await ws.execute("cd /data &")
await ws.execute("wait %1")
assert ws.get_session(DEFAULT_SESSION_ID).cwd == "/"
@pytest.mark.asyncio
async def test_background_sees_parent_env(ws):
await ws.execute("export VISIBLE=yes")
await ws.execute("printenv VISIBLE &")
io = await ws.execute("wait %1")
assert b"yes" in io.stdout
# --- Session: cd + env ---
@pytest.mark.asyncio
async def test_cd_then_relative_cat(ws):
await ws.execute("cd /data/subdir")
io = await ws.execute("cat a.txt")
assert b"aaa" in io.stdout
@pytest.mark.asyncio
async def test_cd_nested_relative(ws):
await ws.execute("cd /data")
await ws.execute("cd subdir")
io = await ws.execute("cat b.txt")
assert b"bbb" in io.stdout
@pytest.mark.asyncio
async def test_export_then_variable_expansion(ws):
await ws.execute("export PATTERN=ERROR")
io = await ws.execute("grep $PATTERN /data/log.txt | wc -l")
assert (await io.stdout_str()).strip() == "2"
@pytest.mark.asyncio
async def test_export_unset_cycle(ws):
await ws.execute("export TMP=val")
assert ws.get_session(DEFAULT_SESSION_ID).env["TMP"] == "val"
await ws.execute("unset TMP")
assert "TMP" not in ws.get_session(DEFAULT_SESSION_ID).env
@pytest.mark.asyncio
async def test_printenv_shows_all(ws):
await ws.execute("export A=1")
await ws.execute("export B=2")
io = await ws.execute("printenv")
out = await io.stdout_str()
assert "A=1" in out
assert "B=2" in out
@pytest.mark.asyncio
async def test_printenv_single_key(ws):
await ws.execute("export SECRET=abc")
io = await ws.execute("printenv SECRET")
assert (await io.stdout_str()).strip() == "abc"
@pytest.mark.asyncio
async def test_printenv_missing_key(ws):
io = await ws.execute("printenv NOSUCH")
assert io.exit_code == 1
# --- Multi-session isolation ---
@pytest.mark.asyncio
async def test_two_sessions_isolated_cwd(ws):
sa = ws.create_session("s-a")
sa.cwd = "/"
sb = ws.create_session("s-b")
sb.cwd = "/"
await ws.execute("cd /data", session_id="s-a")
await ws.execute("cd /data/subdir", session_id="s-b")
assert sa.cwd == "/data"
assert sb.cwd == "/data/subdir"
@pytest.mark.asyncio
async def test_two_sessions_isolated_env(ws):
ws.create_session("s-a")
ws.create_session("s-b")
await ws.execute("export X=from_a", session_id="s-a")
await ws.execute("export X=from_b", session_id="s-b")
assert ws.get_session("s-a").env["X"] == "from_a"
assert ws.get_session("s-b").env["X"] == "from_b"
@pytest.mark.asyncio
async def test_session_env_not_visible_cross_session(ws):
ws.create_session("s-a")
ws.create_session("s-b")
await ws.execute("export PRIVATE=yes", session_id="s-a")
io = await ws.execute("printenv PRIVATE", session_id="s-b")
assert io.exit_code == 1
# --- For loops ---
@pytest.mark.asyncio
async def test_for_loop_basic(ws):
io = await ws.execute(
"for f in /data/subdir/a.txt /data/subdir/b.txt; do cat $f; done")
out = await io.stdout_str()
assert "aaa" in out
assert "bbb" in out
@pytest.mark.asyncio
async def test_for_loop_variable_restored(ws):
await ws.execute("export i=original")
await ws.execute("for i in 1 2 3; do echo $i; done")
assert ws.get_session(DEFAULT_SESSION_ID).env["i"] == "original"
# --- If/else ---
@pytest.mark.asyncio
async def test_if_true_branch(ws):
io = await ws.execute(
"if grep -q world /data/hello.txt; then echo found; else echo nope; fi"
)
assert b"found" in io.stdout
@pytest.mark.asyncio
async def test_if_false_branch(ws):
io = await ws.execute(
"if grep -q NOPE /data/hello.txt; then echo found; else echo nope; fi")
assert b"nope" in io.stdout
# --- Complex combined ---
@pytest.mark.asyncio
async def test_cd_export_grep_pipe(ws):
await ws.execute("cd /data")
await ws.execute("export TERM=ERROR")
io = await ws.execute("grep $TERM log.txt | wc -l")
assert (await io.stdout_str()).strip() == "2"
@pytest.mark.asyncio
async def test_subshell_with_redirect(ws):
await ws.execute("(echo from_subshell) > /data/sub_out.txt")
io = await ws.execute("cat /data/sub_out.txt")
assert b"from_subshell" in io.stdout
@pytest.mark.asyncio
async def test_pipe_into_redirect(ws):
await ws.execute("grep ERROR /data/log.txt | sort > /data/errors.txt")
io = await ws.execute("cat /data/errors.txt")
lines = (await io.stdout_str()).strip().split("\n")
assert len(lines) == 2
assert lines[0] == "ERROR bad"
assert lines[1] == "ERROR fail"
@pytest.mark.asyncio
async def test_background_with_pipe(ws):
await ws.execute("cat /data/numbers.txt | sort | uniq &")
io = await ws.execute("wait %1")
lines = (await io.stdout_str()).strip().split("\n")
assert sorted(lines) == ["1", "2", "3"]
@pytest.mark.asyncio
async def test_history_tracks_session_id(ws):
ws.create_session("s-hist")
ws.get_session("s-hist").cwd = "/"
await ws.execute("echo tracked", session_id="s-hist")
records = [e for e in await ws.history() if e["session"] == "s-hist"]
assert len(records) == 1
assert records[0]["command"] == "echo tracked"
# --- grep exit codes ---
@pytest.mark.asyncio
async def test_grep_no_match_exit_code(ws):
io = await ws.execute("grep NONEXISTENT /data/hello.txt")
assert io.exit_code == 1
@pytest.mark.asyncio
async def test_grep_match_exit_code(ws):
io = await ws.execute("grep hello /data/hello.txt")
assert io.exit_code == 0
@pytest.mark.asyncio
async def test_grep_q_match(ws):
io = await ws.execute("grep -q world /data/hello.txt")
assert io.exit_code == 0
assert not (await io.stdout_str()).strip()
@pytest.mark.asyncio
async def test_grep_q_no_match(ws):
io = await ws.execute("grep -q NONEXISTENT /data/hello.txt")
assert io.exit_code == 1
@pytest.mark.asyncio
async def test_grep_and_short_circuit(ws):
io = await ws.execute(
"grep NONEXISTENT /data/hello.txt && echo should_not_appear")
assert b"should_not_appear" not in (io.stdout or b"")
@pytest.mark.asyncio
async def test_grep_or_fallback(ws):
io = await ws.execute("grep NONEXISTENT /data/hello.txt || echo fallback")
assert b"fallback" in io.stdout
@pytest.mark.asyncio
async def test_grep_if_condition(ws):
io = await ws.execute(
"if grep -q world /data/hello.txt; then echo found; else echo nope; fi"
)
assert b"found" in io.stdout
@pytest.mark.asyncio
async def test_grep_if_no_match(ws):
io = await ws.execute(
"if grep -q NOPE /data/hello.txt; then echo found; else echo nope; fi")
assert b"nope" in io.stdout
@pytest.mark.asyncio
async def test_grep_pipe_no_match_last_stage_wins(ws):
io = await ws.execute("grep NONEXISTENT /data/hello.txt | sort")
assert io.exit_code == 0
@pytest.mark.asyncio
async def test_grep_pipe_match(ws):
io = await ws.execute("grep ERROR /data/log.txt | sort")
assert io.exit_code == 0
lines = (await io.stdout_str()).strip().split("\n")
assert len(lines) == 2
assert lines[0] == "ERROR bad"
assert lines[1] == "ERROR fail"
@pytest.mark.asyncio
async def test_grep_no_match_then_or_chain(ws):
io = await ws.execute(
"grep NOPE /data/hello.txt || grep ERROR /data/log.txt | head -n 1")
assert b"ERROR" in io.stdout
@pytest.mark.asyncio
async def test_grep_count_no_match(ws):
io = await ws.execute("grep -c NONEXISTENT /data/hello.txt")
assert (await io.stdout_str()).strip() == "0"
@pytest.mark.asyncio
async def test_grep_count_match(ws):
io = await ws.execute("grep -c ERROR /data/log.txt")
assert io.exit_code == 0
assert (await io.stdout_str()).strip() == "2"
@pytest.mark.asyncio
async def test_grep_invert_match(ws):
io = await ws.execute("grep -v ERROR /data/log.txt")
assert io.exit_code == 0
lines = (await io.stdout_str()).strip().split("\n")
assert all("ERROR" not in line for line in lines)
@pytest.mark.asyncio
async def test_grep_invert_no_output(ws):
io = await ws.execute("echo hello | grep -v hello")
assert not (await io.stdout_str()).strip()
# --- rg exit codes ---
@pytest.mark.asyncio
async def test_rg_no_match_exit_code(ws):
io = await ws.execute("rg NONEXISTENT /data/hello.txt")
assert io.exit_code == 1
@pytest.mark.asyncio
async def test_rg_match_exit_code(ws):
io = await ws.execute("rg hello /data/hello.txt")
assert io.exit_code == 0
@pytest.mark.asyncio
async def test_rg_no_match_and_chain(ws):
io = await ws.execute("rg NONEXISTENT /data/hello.txt && echo found")
assert b"found" not in (io.stdout or b"")
@pytest.mark.asyncio
async def test_rg_no_match_or_chain(ws):
io = await ws.execute("rg NONEXISTENT /data/hello.txt || echo fallback")
assert b"fallback" in io.stdout
@pytest.mark.asyncio
async def test_rg_pipe_no_match(ws):
io = await ws.execute("rg NONEXISTENT /data/hello.txt | wc -l")
assert io.exit_code == 0
@pytest.mark.asyncio
async def test_rg_match_pipe_head(ws):
io = await ws.execute("rg INFO /data/log.txt | head -n 1")
assert io.exit_code == 0
assert b"INFO" in io.stdout
# --- diff exit codes ---
@pytest.mark.asyncio
async def test_diff_identical_files(ws):
await ws.execute("echo same > /data/diff_a.txt")
await ws.execute("echo same > /data/diff_b.txt")
io = await ws.execute("diff /data/diff_a.txt /data/diff_b.txt")
assert io.exit_code == 0
assert not (await io.stdout_str()).strip()
@pytest.mark.asyncio
async def test_diff_different_files(ws):
await ws.execute("echo aaa > /data/diff_a.txt")
await ws.execute("echo bbb > /data/diff_b.txt")
io = await ws.execute("diff /data/diff_a.txt /data/diff_b.txt")
assert io.exit_code == 1
assert (await io.stdout_str()).strip()
@pytest.mark.asyncio
async def test_diff_and_chain(ws):
await ws.execute("echo same > /data/diff_a.txt")
await ws.execute("echo same > /data/diff_b.txt")
io = await ws.execute(
"diff /data/diff_a.txt /data/diff_b.txt && echo identical")
assert b"identical" in io.stdout
@pytest.mark.asyncio
async def test_diff_or_chain(ws):
await ws.execute("echo aaa > /data/diff_a.txt")
await ws.execute("echo bbb > /data/diff_b.txt")
io = await ws.execute(
"diff /data/diff_a.txt /data/diff_b.txt || echo different")
assert b"different" in io.stdout
@pytest.mark.asyncio
async def test_diff_if_identical(ws):
await ws.execute("echo same > /data/diff_a.txt")
await ws.execute("echo same > /data/diff_b.txt")
cmd = ("if diff /data/diff_a.txt /data/diff_b.txt;"
" then echo same; else echo changed; fi")
io = await ws.execute(cmd)
assert b"same" in io.stdout
@pytest.mark.asyncio
async def test_diff_if_different(ws):
await ws.execute("echo aaa > /data/diff_a.txt")
await ws.execute("echo bbb > /data/diff_b.txt")
cmd = ("if diff /data/diff_a.txt /data/diff_b.txt;"
" then echo same; else echo changed; fi")
io = await ws.execute(cmd)
assert b"changed" in io.stdout
# --- find (verify already correct) ---
@pytest.mark.asyncio
async def test_find_existing(ws):
io = await ws.execute("find /data/subdir")
assert io.exit_code == 0
assert b"a.txt" in io.stdout
@pytest.mark.asyncio
async def test_find_with_name(ws):
io = await ws.execute("find /data/subdir -name a.txt")
assert io.exit_code == 0
assert b"a.txt" in io.stdout
# --- Complex combined scenarios ---
@pytest.mark.asyncio
async def test_grep_no_match_pipe_redirect(ws):
cmd = ("grep NONEXISTENT /data/log.txt > /data/result.txt"
" || echo none > /data/result.txt")
await ws.execute(cmd)
io = await ws.execute("cat /data/result.txt")
assert b"none" in io.stdout
@pytest.mark.asyncio
async def test_grep_match_and_diff(ws):
await ws.execute("grep ERROR /data/log.txt > /data/errors.txt")
await ws.execute("echo 'ERROR fail\nERROR bad' > /data/expected.txt")
io = await ws.execute("diff /data/errors.txt /data/expected.txt")
assert io.exit_code in (0, 1)
@pytest.mark.asyncio
async def test_rg_subshell_isolation(ws):
io = await ws.execute("(rg NONEXISTENT /data/hello.txt) || echo recovered")
assert b"recovered" in io.stdout
@pytest.mark.asyncio
async def test_grep_background_exit_code(ws):
await ws.execute("grep ERROR /data/log.txt &")
io = await ws.execute("wait %1")
assert io.exit_code == 0
assert b"ERROR" in io.stdout
@pytest.mark.asyncio
async def test_grep_no_match_background(ws):
await ws.execute("grep NONEXISTENT /data/log.txt &")
io = await ws.execute("wait %1")
assert io.exit_code == 1
+363
View File
@@ -0,0 +1,363 @@
# ========= 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 contextlib import ExitStack
from datetime import datetime, timezone
from pathlib import Path
from unittest.mock import patch
import pytest
from mirage.commands.builtin.s3 import COMMANDS as _S3_COMMANDS
from mirage.io.cachable_iterator import CachableAsyncIterator
from mirage.resource.ram import RAMResource
from mirage.resource.s3 import S3Config, S3Resource
from mirage.types import MountMode, PathSpec
from mirage.utils.key_prefix import mount_key
from mirage.workspace import Workspace
DATA_DIR = Path(__file__).resolve().parents[3] / "data"
s3_cat = next(fn for fn in _S3_COMMANDS for rc in fn._registered_commands
if rc.name == "cat" and rc.filetype is None)
LAST_MODIFIED = datetime(2026, 3, 26, tzinfo=timezone.utc)
_CORE_MODULES = [
"mirage.core.s3.read",
"mirage.core.s3.write",
"mirage.core.s3.stat",
"mirage.core.s3.readdir",
"mirage.core.s3.find",
"mirage.core.s3.du",
"mirage.core.s3.stream",
"mirage.core.s3.copy",
"mirage.core.s3.rename",
"mirage.core.s3.unlink",
"mirage.core.s3.rmdir",
"mirage.core.s3.rm",
"mirage.core.s3.mkdir",
"mirage.core.s3.create",
"mirage.core.s3.truncate",
]
class MockS3Error(Exception):
def __init__(self, code: str) -> None:
super().__init__(code)
self.response = {"Error": {"Code": code}}
class AsyncMockBody:
def __init__(self, data: bytes) -> None:
self._data = data
async def read(self) -> bytes:
return self._data
async def iter_chunks(self, chunk_size: int = 8192):
for i in range(0, len(self._data), chunk_size):
yield self._data[i:i + chunk_size]
class AsyncMockPaginator:
def __init__(self, objects: dict[str, bytes]) -> None:
self.objects = objects
async def paginate(self,
Bucket: str,
Prefix: str = "",
Delimiter: str | None = None):
del Bucket
if Delimiter == "/":
yield _paginate_directory(self.objects, Prefix)
else:
yield _paginate_flat(self.objects, Prefix)
class AsyncMockS3Client:
def __init__(self, objects: dict[str, bytes]) -> None:
self.objects = objects
async def get_object(self,
Bucket: str,
Key: str,
Range: str | None = None) -> dict:
del Bucket
if Key not in self.objects:
raise MockS3Error("NoSuchKey")
data = self.objects[Key]
if Range is not None:
data = _slice_range(data, Range)
return {"Body": AsyncMockBody(data)}
async def head_object(self, Bucket: str, Key: str) -> dict:
del Bucket
if Key not in self.objects:
raise MockS3Error("NoSuchKey")
return {
"ContentLength": len(self.objects[Key]),
"LastModified": LAST_MODIFIED,
"ETag": f'"{Key}"',
}
def get_paginator(self, name: str) -> AsyncMockPaginator:
assert name == "list_objects_v2"
return AsyncMockPaginator(self.objects)
async def put_object(self, Bucket: str, Key: str, Body: bytes) -> None:
self.objects[Key] = Body
async def delete_object(self, Bucket: str, Key: str) -> None:
self.objects.pop(Key, None)
async def copy_object(self, Bucket: str, CopySource: dict,
Key: str) -> None:
src_key = CopySource["Key"]
if src_key in self.objects:
self.objects[Key] = self.objects[src_key]
async def delete_objects(self, Bucket: str, Delete: dict) -> None:
for obj in Delete.get("Objects", []):
self.objects.pop(obj["Key"], None)
async def __aenter__(self):
return self
async def __aexit__(self, *args):
pass
class MockAsyncSession:
def __init__(self, objects: dict[str, bytes]) -> None:
self._client = AsyncMockS3Client(objects)
def client(self, **kwargs):
return self._client
def _paginate_directory(objects, prefix):
common_prefixes: set[str] = set()
contents: list[dict[str, object]] = []
for key, data in sorted(objects.items()):
if not key.startswith(prefix):
continue
relative = key[len(prefix):]
if not relative:
continue
if "/" in relative:
child = relative.split("/", 1)[0]
common_prefixes.add(prefix + child + "/")
continue
contents.append({"Key": key, "Size": len(data)})
return {
"CommonPrefixes": [{
"Prefix": v
} for v in sorted(common_prefixes)],
"Contents": contents,
}
def _paginate_flat(objects, prefix):
return {
"Contents": [{
"Key": k,
"Size": len(v)
} for k, v in sorted(objects.items()) if k.startswith(prefix)]
}
def _slice_range(data: bytes, range_spec: str) -> bytes:
if not range_spec.startswith("bytes="):
return data
bounds = range_spec.removeprefix("bytes=").split("-", 1)
start = int(bounds[0]) if bounds[0] else 0
end = int(bounds[1]) if bounds[1] else len(data) - 1
return data[start:end + 1]
def _load_example_jsonl(limit: int = 20) -> bytes:
chunks: list[bytes] = []
with (DATA_DIR / "example.jsonl").open("rb") as handle:
for _ in range(limit):
line = handle.readline()
if not line:
break
chunks.append(line)
return b"".join(chunks)
def _s3_objects() -> dict[str, bytes]:
return {
"data/example.json": (DATA_DIR / "example.json").read_bytes(),
"data/example.jsonl": _load_example_jsonl(),
"reports/summary.txt": b"alpha report\nbeta report\n",
"archive/2026/q1/deep.txt": b"deep archive\n",
}
def _s3_backend() -> S3Resource:
config = S3Config(
bucket="test-bucket",
region="us-east-1",
aws_access_key_id="fake",
aws_secret_access_key="fake",
)
return S3Resource(config)
def _patch_async_session(objects):
mock_session = MockAsyncSession(objects)
stack = ExitStack()
for mod in _CORE_MODULES:
stack.enter_context(
patch(f"{mod}.async_session", return_value=mock_session))
return stack
@pytest.fixture
def ws():
objects = _s3_objects()
with _patch_async_session(objects):
yield Workspace(
{
"/s3/": (_s3_backend(), MountMode.READ),
"/tmp/": (RAMResource(), MountMode.WRITE),
},
mode=MountMode.WRITE,
)
@pytest.mark.asyncio
async def test_find_sort_lists_expected_s3_files(ws):
objects = _s3_objects()
with _patch_async_session(objects):
io = await ws.execute("find /s3 -maxdepth 2 -type f | sort")
assert (await io.stdout_str()).strip().splitlines() == [
"/s3/data/example.json",
"/s3/data/example.jsonl",
"/s3/reports/summary.txt",
]
@pytest.mark.asyncio
async def test_file_report_through_redirect_chain(ws):
objects = _s3_objects()
with _patch_async_session(objects):
io = await ws.execute(
"echo '=== /s3/data/example.json ===' > /tmp/file_report.txt && "
"file /s3/data/example.json >> /tmp/file_report.txt && "
"echo >> /tmp/file_report.txt && "
"echo '=== /s3/data/example.jsonl ===' >> /tmp/file_report.txt && "
"file /s3/data/example.jsonl >> /tmp/file_report.txt && "
"echo >> /tmp/file_report.txt && "
"echo '=== /s3/reports/summary.txt ===' "
">> /tmp/file_report.txt && "
"file /s3/reports/summary.txt >> /tmp/file_report.txt && "
"echo >> /tmp/file_report.txt && "
"cat /tmp/file_report.txt")
assert (await io.stdout_str()).strip().splitlines() == [
"=== /s3/data/example.json ===",
"/s3/data/example.json: json",
"",
"=== /s3/data/example.jsonl ===",
"/s3/data/example.jsonl: json",
"",
"=== /s3/reports/summary.txt ===",
"/s3/reports/summary.txt: text",
]
@pytest.mark.asyncio
async def test_wc_report_through_redirect_chain(ws):
objects = _s3_objects()
with _patch_async_session(objects):
io = await ws.execute(
"echo -n '/s3/data/example.json ' > /tmp/size_report.txt && "
"wc -c /s3/data/example.json >> /tmp/size_report.txt && "
"echo -n '/s3/data/example.jsonl ' >> /tmp/size_report.txt && "
"wc -c /s3/data/example.jsonl >> /tmp/size_report.txt && "
"cat /tmp/size_report.txt")
json_size = len(objects["data/example.json"])
jsonl_size = len(objects["data/example.jsonl"])
assert (await io.stdout_str()).strip().splitlines() == [
f"/s3/data/example.json {json_size} /s3/data/example.json",
f"/s3/data/example.jsonl {jsonl_size} /s3/data/example.jsonl",
]
@pytest.mark.asyncio
async def test_grep_then_jq_with_and_or_list(ws):
objects = _s3_objects()
with _patch_async_session(objects):
io = await ws.execute(
"grep -l mirage /s3/data/example.jsonl "
"> /tmp/search_report.txt && "
"echo >> /tmp/search_report.txt && "
"jq .company /s3/data/example.json >> /tmp/search_report.txt || "
"echo missing > /tmp/search_report.txt; "
"cat /tmp/search_report.txt")
assert (await io.stdout_str()).strip().splitlines() == [
"/s3/data/example.jsonl",
"",
'"Strukto"',
]
def _resolved(original: str) -> PathSpec:
return PathSpec(resource_path=mount_key(original, "/s3"),
virtual=original,
directory=original,
resolved=True)
@pytest.mark.asyncio
async def test_cat_multifile_caches_materialized_bytes_per_file():
"""Regression: multi-file cat on a streaming backend must register fully
materialized bytes per path in io.reads (not un-exhausted cachables). A
per-file cachable cannot preserve stdout identity, so the cache-fill
background drain raced the consumer on the same network stream and
poisoned each file's cache slot. Materialized bytes cache deterministically
with no drain and no race."""
objects = _s3_objects()
backend = _s3_backend()
with _patch_async_session(objects):
a = _resolved("/s3/reports/summary.txt")
b = _resolved("/s3/archive/2026/q1/deep.txt")
source, io = await s3_cat(backend.accessor, [a, b], index=None)
assert io.reads[
"/reports/summary.txt"] == b"alpha report\nbeta report\n"
assert io.reads["/archive/2026/q1/deep.txt"] == b"deep archive\n"
assert all(isinstance(v, bytes) for v in io.reads.values())
combined = b"".join([chunk async for chunk in source])
assert combined == b"alpha report\nbeta report\ndeep archive\n"
@pytest.mark.asyncio
async def test_cat_single_file_keeps_streaming_cachable():
"""The single-file path must stay a streaming cachable returned AS stdout
(identity preserved) so large files still stream, not materialize."""
objects = _s3_objects()
backend = _s3_backend()
with _patch_async_session(objects):
a = _resolved("/s3/reports/summary.txt")
source, io = await s3_cat(backend.accessor, [a], index=None)
assert isinstance(source, CachableAsyncIterator)
assert io.reads["/reports/summary.txt"] is source
@@ -0,0 +1,43 @@
# ========= 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 import MountMode, Workspace
from mirage.resource.slack import SlackConfig, SlackResource
@pytest.mark.asyncio
async def test_slack_grep_glob_expanded_to_60_paths_is_one_native_call():
slack = SlackResource(
config=SlackConfig(token="xoxb-test", search_token="xoxp-test"))
ws = Workspace({"/slack": (slack, MountMode.READ)}, mode=MountMode.READ)
fake_payload = (b'{"messages":{"matches":[{"channel":{"name":"general",'
b'"id":"C1"},"ts":"1700000000.0","text":"hello"}]}}')
expanded = " ".join(
f"/slack/channels/general__C1/2026-{m:02d}-{d:02d}/chat.jsonl"
for m in range(1, 5) for d in range(1, 16))
try:
with patch(
"mirage.commands.builtin.slack.grep.search_messages",
new=AsyncMock(return_value=fake_payload),
) as fake_search:
result = await ws.execute(f"grep -i hello {expanded}")
assert fake_search.await_count == 1
assert result.exit_code == 0
assert b"hello" in (result.stdout or b"")
finally:
await ws.close()
@@ -0,0 +1,443 @@
# ========= 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 .conftest import make_resource_ws, run, run_exit
FILES = {
"logs/app.log": (b"2026-01-01 INFO startup\n"
b"2026-01-02 ERROR connection refused\n"
b"2026-01-03 INFO request handled\n"
b"2026-01-04 WARN slow query\n"
b"2026-01-05 ERROR timeout\n"
b"2026-01-06 INFO shutdown\n"),
"logs/access.log": (b"GET /api/users 200\n"
b"POST /api/users 201\n"
b"GET /api/users 200\n"
b"DELETE /api/users/1 404\n"
b"GET /api/health 200\n"),
"data/scores.csv":
b"alice,90\nbob,75\ncharlie,90\nalice,85\nbob,95\n",
"data/words.txt":
b"hello\nworld\nhello\nfoo\nbar\nfoo\nhello\n",
"data/numbers.txt":
b"3\n1\n4\n1\n5\n9\n2\n6\n5\n3\n",
"src/main.py":
b"import os\nimport sys\nprint('hello')\n",
"src/utils.py":
b"def add(a, b):\n return a + b\n",
"config.json":
b'{"name": "mirage", "version": "1.0"}\n',
"empty.txt":
b"",
}
@pytest.fixture(params=["ram", "s3", "disk"])
def ws(request, tmp_path):
yield from make_resource_ws(request, tmp_path, FILES)
# ---------------------------------------------------------------------------
# Nested loops
# ---------------------------------------------------------------------------
def test_nested_for_with_file_ops(ws):
cmd = ("for d in logs data; do "
"for f in $(find /data/$d -type f | sort); do "
"echo \"$d: $f\"; done; done")
result = run(ws, cmd)
assert "logs: /data/logs/app.log" in result
assert "data: /data/data/scores.csv" in result
def test_for_with_if_and_grep(ws):
cmd = ("for f in $(find /data/logs -type f | sort); do "
"if grep -q ERROR $f; then echo \"ERRORS: $f\"; fi; done")
result = run(ws, cmd)
assert "ERRORS: /data/logs/app.log" in result
assert "ERRORS: /data/logs/access.log" not in result
def test_while_read_with_nested_if(ws):
cmd = ("find /data/data -type f | sort | "
"while read f; do "
"if [ \"$(wc -l < $f)\" -gt 3 ]; then "
"echo \"big: $f\"; "
"else echo \"small: $f\"; fi; done")
result = run(ws, cmd)
lines = result.strip().splitlines()
assert len(lines) == 3
# ---------------------------------------------------------------------------
# Multi-stage pipelines
# ---------------------------------------------------------------------------
def test_sort_uniq_count_pipeline(ws):
cmd = "cat /data/data/words.txt | sort | uniq -c | sort -rn | head -n 1"
result = run(ws, cmd).split()
assert result[0] == "3"
assert result[1] == "hello"
def test_grep_pipe_cut_pipe_sort_pipe_uniq(ws):
cmd = ("cat /data/logs/access.log | cut -d ' ' -f 1 | sort | uniq -c "
"| sort -rn")
result = run(ws, cmd).strip()
lines = result.splitlines()
assert len(lines) == 3
first = lines[0].split()
assert first[0] == "3"
assert first[1] == "GET"
def test_five_stage_pipeline(ws):
cmd = ("cat /data/data/numbers.txt | sort -n | uniq | head -n 3 "
"| tr '\\n' ','")
result = run(ws, cmd)
assert result.startswith("1,2,3")
def test_grep_count_errors_across_files(ws):
cmd = "grep -c ERROR /data/logs/app.log"
result = run(ws, cmd).strip()
assert result == "2"
# ---------------------------------------------------------------------------
# Command substitution in pipelines
# ---------------------------------------------------------------------------
def test_command_sub_in_echo(ws):
cmd = "echo \"lines: $(wc -l < /data/data/words.txt)\""
result = run(ws, cmd).strip()
assert result == "lines: 7"
def test_nested_command_sub(ws):
cmd = "echo $(echo $(echo deep))"
result = run(ws, cmd).strip()
assert result == "deep"
def test_command_sub_in_for_values(ws):
cmd = ("for line in $(grep ERROR /data/logs/app.log | cut -d ' ' -f 1); "
"do echo \"date:$line\"; done")
result = run(ws, cmd)
assert "date:2026-01-02" in result
assert "date:2026-01-05" in result
def test_command_sub_in_test(ws):
cmd = ("if [ $(grep -c ERROR /data/logs/app.log) -gt 1 ]; then "
"echo many_errors; else echo few_errors; fi")
result = run(ws, cmd).strip()
assert result == "many_errors"
# ---------------------------------------------------------------------------
# While read with processing
# ---------------------------------------------------------------------------
def test_while_read_with_variable_transform(ws):
cmd = ("echo -e 'alice\\nbob\\ncharlie' | "
"while read name; do echo \"user:$name\"; done")
result = run(ws, cmd)
lines = result.strip().splitlines()
assert lines == ["user:alice", "user:bob", "user:charlie"]
def test_while_read_counter(ws):
cmd = ("export count=0; "
"cat /data/data/words.txt | sort -u | "
"while read w; do "
"export count=$((count+1)); echo \"$count:$w\"; done")
result = run(ws, cmd)
lines = result.strip().splitlines()
assert lines[0] == "1:bar"
def test_while_read_pipe_to_wc(ws):
cmd = ("grep ERROR /data/logs/app.log | "
"while read line; do echo \"ALERT: $line\"; done | wc -l")
result = run(ws, cmd).strip()
assert result == "2"
def test_while_read_with_grep_filter(ws):
cmd = ("cat /data/logs/access.log | "
"while read line; do echo $line; done | grep 200 | wc -l")
result = run(ws, cmd).strip()
assert result == "3"
# ---------------------------------------------------------------------------
# Subshell and grouping
# ---------------------------------------------------------------------------
def test_subshell_pipe_chain(ws):
cmd = "(echo hello; echo world) | sort | tr a-z A-Z"
result = run(ws, cmd)
lines = result.strip().splitlines()
assert lines == ["HELLO", "WORLD"]
def test_brace_group_redirect(ws):
cmd = "{ echo first; echo second; } > /data/out.txt; cat /data/out.txt"
result = run(ws, cmd)
assert result == "first\nsecond\n"
def test_subshell_variable_isolation(ws):
cmd = "export X=outer; (export X=inner; echo $X); echo $X"
result = run(ws, cmd)
lines = result.strip().splitlines()
assert lines == ["inner", "outer"]
def test_subshell_function_def_no_leak(ws):
cmd = "(only_inner() { echo inner; }); only_inner 2>/dev/null || echo gone"
assert run(ws, cmd).strip() == "gone"
def test_subshell_function_redef_isolation(ws):
cmd = "greet() { echo outer; }; (greet() { echo inner; }; greet); greet"
lines = run(ws, cmd).strip().splitlines()
assert lines == ["inner", "outer"]
def test_subshell_function_inherited(ws):
cmd = "greet() { echo hi; }; (greet)"
assert run(ws, cmd).strip() == "hi"
def test_subshell_positional_isolation(ws):
cmd = "set -- a b c; (set -- x); echo $# $1"
assert run(ws, cmd).strip() == "3 a"
def test_subshell_positional_inherited(ws):
cmd = "set -- a b; (echo $1 $2)"
assert run(ws, cmd).strip() == "a b"
# ---------------------------------------------------------------------------
# Functions with pipelines
# ---------------------------------------------------------------------------
def test_function_in_pipeline(ws):
cmd = "upper() { tr a-z A-Z; }; echo hello | upper"
result = run(ws, cmd).strip()
assert result == "HELLO"
def test_function_with_args_in_loop(ws):
cmd = ("classify() { case $1 in "
"*.py) echo \"python: $1\";; "
"*.txt) echo \"text: $1\";; "
"*.json) echo \"json: $1\";; "
"*.csv) echo \"csv: $1\";; "
"*.log) echo \"log: $1\";; "
"*) echo \"other: $1\";; esac; }; "
"find /data -type f | sort | "
"while read f; do classify $f; done")
result = run(ws, cmd)
assert "python: /data/src/main.py" in result
assert "json: /data/config.json" in result
assert "log: /data/logs/app.log" in result
assert "csv: /data/data/scores.csv" in result
# ---------------------------------------------------------------------------
# Conditional chains (&&, ||)
# ---------------------------------------------------------------------------
def test_and_chain_with_grep(ws):
cmd = ("grep -q ERROR /data/logs/app.log && "
"echo 'has errors' || echo 'clean'")
result = run(ws, cmd).strip()
assert result == "has errors"
def test_or_chain_fallback(ws):
cmd = ("grep -q FATAL /data/logs/app.log && "
"echo 'has fatal' || echo 'no fatal'")
result = run(ws, cmd).strip()
assert result == "no fatal"
def test_chained_conditionals(ws):
cmd = ("grep -q ERROR /data/logs/app.log && "
"grep -q INFO /data/logs/app.log && "
"echo 'has both'")
result = run(ws, cmd).strip()
assert result == "has both"
# ---------------------------------------------------------------------------
# Redirects combined with pipes
# ---------------------------------------------------------------------------
def test_pipe_with_output_redirect(ws):
cmd = ("grep ERROR /data/logs/app.log | sort > /data/errors.txt; "
"cat /data/errors.txt")
result = run(ws, cmd)
lines = result.strip().splitlines()
assert len(lines) == 2
assert lines == sorted(lines)
def test_append_redirect_in_loop(ws):
cmd = ("for x in one two three; do "
"echo $x >> /data/result.txt; done; cat /data/result.txt")
result = run(ws, cmd)
assert result == "one\ntwo\nthree\n"
def test_stdin_redirect_in_pipeline(ws):
cmd = "sort < /data/data/numbers.txt | uniq | wc -l"
result = run(ws, cmd).strip()
assert result == "7"
# ---------------------------------------------------------------------------
# Process substitution
# ---------------------------------------------------------------------------
def test_input_process_substitution(ws):
cmd = "cat <(echo hello)"
result = run(ws, cmd)
assert "hello" in result
# ---------------------------------------------------------------------------
# Sed and tr pipelines
# ---------------------------------------------------------------------------
def test_sed_in_pipeline(ws):
cmd = "cat /data/logs/app.log | grep ERROR | sed 's/ERROR/CRITICAL/'"
result = run(ws, cmd)
assert "CRITICAL" in result
assert "ERROR" not in result
def test_tr_multiple_transforms(ws):
cmd = "echo 'Hello World' | tr A-Z a-z | tr ' ' '_'"
result = run(ws, cmd).strip()
assert result == "hello_world"
# ---------------------------------------------------------------------------
# Complex real-world patterns
# ---------------------------------------------------------------------------
def test_log_analysis_pipeline(ws):
cmd = ("cat /data/logs/app.log | grep -v INFO | "
"cut -d ' ' -f 2 | sort | uniq -c | sort -rn")
result = run(ws, cmd).strip()
lines = result.splitlines()
first = lines[0].split()
assert first[0] == "2"
assert first[1] == "ERROR"
def test_find_grep_count_pattern(ws):
cmd = ("find /data/src -name '*.py' -type f | sort | "
"while read f; do "
"echo \"$(grep -c import $f) $f\"; done")
result = run(ws, cmd)
assert "2 /data/src/main.py" in result
def test_csv_processing_pipeline(ws):
cmd = ("cat /data/data/scores.csv | sort -t, -k2 -rn | head -n 1 "
"| cut -d, -f1")
result = run(ws, cmd).strip()
assert result == "bob"
def test_word_frequency_full_pipeline(ws):
cmd = ("cat /data/data/words.txt | sort | uniq -c | sort -rn | "
"while read count word; do "
"echo \"$word appears $count times\"; done | head -n 2")
result = run(ws, cmd)
lines = result.strip().splitlines()
assert "hello appears 3 times" in lines[0]
assert "foo appears 2 times" in lines[1]
def test_while_read_with_command_sub_body(ws):
cmd = ("find /data -name '*.py' -type f | sort | "
"while read f; do "
"echo \"$f: $(wc -l < $f) lines\"; done")
result = run(ws, cmd)
assert "/data/src/main.py: 3 lines" in result
assert "/data/src/utils.py: 2 lines" in result
def test_multiline_script_with_function_and_loop(ws):
cmd = ("count_matches() { grep -c $1 $2; }; "
"find /data/logs -type f | sort | "
"while read f; do "
"echo \"$f: $(count_matches ERROR $f) errors\"; done")
result = run(ws, cmd)
assert "/data/logs/app.log: 2 errors" in result
assert "/data/logs/access.log: 0 errors" in result
# ---------------------------------------------------------------------------
# Edge cases
# ---------------------------------------------------------------------------
def test_empty_file_in_pipeline(ws):
cmd = "cat /data/empty.txt | wc -l"
result = run(ws, cmd).strip()
assert result == "0"
def test_while_read_empty_input(ws):
cmd = ("cat /data/empty.txt | "
"while read line; do echo \"got: $line\"; done; echo done")
result = run(ws, cmd).strip()
assert result == "done"
def test_deeply_nested_command_sub(ws):
cmd = "echo $(echo $(echo $(echo nested)))"
result = run(ws, cmd).strip()
assert result == "nested"
def test_pipe_exit_code_last_command(ws):
code = run_exit(ws, "echo hello | grep world")
assert code != 0
def test_pipe_exit_code_success(ws):
code = run_exit(ws, "echo hello | grep hello")
assert code == 0
@@ -0,0 +1,294 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
import os
import uuid
import boto3
import pytest
from mirage.core.s3.write import write_bytes
from mirage.resource.s3 import S3Config, S3Resource
from mirage.types import DriftPolicy, MountMode
from mirage.workspace import Workspace
from mirage.workspace.snapshot import ContentDriftError
LIVE_BUCKET = os.environ.get("MIRAGE_LIVE_S3_BUCKET") or os.environ.get(
"AWS_S3_BUCKET")
LIVE_REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
LIVE_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID")
LIVE_SECRET = os.environ.get("AWS_SECRET_ACCESS_KEY")
_LIVE_READY = bool(LIVE_BUCKET and LIVE_KEY_ID and LIVE_SECRET)
pytestmark = pytest.mark.skipif(
not _LIVE_READY,
reason=("set MIRAGE_LIVE_S3_BUCKET (or AWS_S3_BUCKET) plus "
"AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY to run live S3 tests"),
)
def _config() -> S3Config:
return S3Config(
bucket=LIVE_BUCKET,
region=LIVE_REGION,
aws_access_key_id=LIVE_KEY_ID,
aws_secret_access_key=LIVE_SECRET,
)
def _boto_client():
return boto3.client(
"s3",
region_name=LIVE_REGION,
aws_access_key_id=LIVE_KEY_ID,
aws_secret_access_key=LIVE_SECRET,
)
def _versioning_enabled() -> bool:
try:
resp = _boto_client().get_bucket_versioning(Bucket=LIVE_BUCKET)
except Exception:
return False
return resp.get("Status") == "Enabled"
def _probe_key() -> str:
return f"mirage-drift-probe-{uuid.uuid4().hex[:8]}.txt"
def _mount(prefix: str = "/s3/") -> dict:
return {prefix: (S3Resource(_config()), MountMode.WRITE)}
def _override(prefix: str = "/s3/") -> dict:
return {prefix: S3Resource(_config())}
def _cleanup_key(key: str) -> None:
client = _boto_client()
try:
if _versioning_enabled():
versions = client.list_object_versions(Bucket=LIVE_BUCKET,
Prefix=key).get(
"Versions", [])
for v in versions:
if v.get("Key") == key:
client.delete_object(Bucket=LIVE_BUCKET,
Key=key,
VersionId=v["VersionId"])
markers = client.list_object_versions(Bucket=LIVE_BUCKET,
Prefix=key).get(
"DeleteMarkers", [])
for m in markers:
if m.get("Key") == key:
client.delete_object(Bucket=LIVE_BUCKET,
Key=key,
VersionId=m["VersionId"])
else:
client.delete_object(Bucket=LIVE_BUCKET, Key=key)
except Exception:
pass
def test_live_strict_raises_on_etag_drift(tmp_path):
"""Snapshot with a recorded read, mutate the live object via boto so
its ETag changes, load with STRICT: the eager drift check must raise
ContentDriftError before any user-visible read.
Requires versioning OFF: on a versioned bucket, stat() captures a
revision and STRICT installs a pin (which intentionally bypasses
drift detection). That path is covered by the version-pin test.
"""
if _versioning_enabled():
pytest.skip(f"bucket {LIVE_BUCKET} is versioned; STRICT will pin "
"instead of drift-checking. See the version-pin test "
"for the versioned path.")
key = _probe_key()
probe = f"/s3/{key}"
client = _boto_client()
client.put_object(Bucket=LIVE_BUCKET, Key=key, Body=b"v1 bytes\n")
try:
src = Workspace(_mount(), mode=MountMode.WRITE)
result = asyncio.run(src.execute(f"cat {probe}"))
assert b"v1 bytes" in result.stdout
snap = tmp_path / "drift.tar"
asyncio.run(src.snapshot(snap))
client.put_object(Bucket=LIVE_BUCKET, Key=key, Body=b"v2 mutated\n")
dst = Workspace.load(snap, resources=_override())
with pytest.raises(ContentDriftError) as exc_info:
asyncio.run(dst.execute(f"cat {probe}"))
assert exc_info.value.path == probe
assert (exc_info.value.snapshot_fingerprint
!= exc_info.value.live_fingerprint)
finally:
_cleanup_key(key)
def test_live_no_drift_passes(tmp_path):
"""Control: snapshot + load with identical bytes on the bucket must
succeed. Guards against false-positive drift on the live ETag path.
"""
key = _probe_key()
probe = f"/s3/{key}"
client = _boto_client()
client.put_object(Bucket=LIVE_BUCKET, Key=key, Body=b"stable\n")
try:
src = Workspace(_mount(), mode=MountMode.WRITE)
asyncio.run(src.execute(f"cat {probe}"))
snap = tmp_path / "stable.tar"
asyncio.run(src.snapshot(snap))
dst = Workspace.load(snap, resources=_override())
result = asyncio.run(dst.execute(f"cat {probe}"))
assert b"stable" in result.stdout
finally:
_cleanup_key(key)
def test_live_pin_records_agent_version_not_snapshot_time_version(tmp_path):
"""Race-fix regression test.
Agent reads V1 at T1. Between T1 and snapshot at T3, an external
actor mutates the object to V2. Old design (live stat at snapshot
time) would capture V2's VersionId and pin replay to V2, serving
bytes the agent never saw. New design records VersionId at READ
time (from the GET response headers), so the snapshot pins to V1
and serves V1 bytes on cache miss.
Skipped on non-versioned buckets (no revision = no pin).
"""
if not _versioning_enabled():
pytest.skip("requires versioning")
key = _probe_key()
probe = f"/s3/{key}"
client = _boto_client()
client.put_object(Bucket=LIVE_BUCKET, Key=key, Body=b"v1\n")
try:
src = Workspace(_mount(), mode=MountMode.WRITE)
result = asyncio.run(src.execute(f"cat {probe}"))
assert b"v1" in result.stdout
# Race: upstream changes BEFORE snapshot fires
client.put_object(Bucket=LIVE_BUCKET, Key=key, Body=b"v2-racy\n")
snap = tmp_path / "racy.tar"
asyncio.run(src.snapshot(snap))
dst = Workspace.load(snap, resources=_override())
dst._cache.evict_paths([probe])
result = asyncio.run(dst.execute(f"cat {probe}"))
assert result.stdout == b"v1\n", (
f"snapshot pinned the wrong VersionId; served {result.stdout!r} "
"instead of the V1 the agent actually saw")
finally:
_cleanup_key(key)
def test_live_version_pin_serves_original_on_versioned_bucket(tmp_path):
"""End-to-end pin: with bucket versioning enabled, snapshot captures
the live VersionId. After the bucket head moves on, STRICT load
serves the original recorded bytes via the pin, not the current
head, and does not raise.
Skipped (xfail) when the bucket is not versioned; the pin path
requires real S3 versioning to exercise.
"""
if not _versioning_enabled():
pytest.skip(
f"bucket {LIVE_BUCKET} does not have versioning enabled; "
"run `aws s3api put-bucket-versioning --bucket "
f"{LIVE_BUCKET} --versioning-configuration Status=Enabled` "
"to exercise the pin path")
key = _probe_key()
probe = f"/s3/{key}"
client = _boto_client()
client.put_object(Bucket=LIVE_BUCKET, Key=key, Body=b"original\n")
try:
src = Workspace(_mount(), mode=MountMode.WRITE)
result = asyncio.run(src.execute(f"cat {probe}"))
assert b"original" in result.stdout
snap = tmp_path / "pin.tar"
asyncio.run(src.snapshot(snap))
client.put_object(Bucket=LIVE_BUCKET, Key=key, Body=b"mutated\n")
dst = Workspace.load(snap, resources=_override())
dst._cache.evict_paths([probe])
result = asyncio.run(dst.execute(f"cat {probe}"))
assert result.stdout == b"original\n", (
"pinned read should serve the recorded version, "
f"got {result.stdout!r}")
finally:
_cleanup_key(key)
def test_live_off_policy_serves_current(tmp_path):
"""drift_policy=OFF must skip both the drift check and any pin
installation, evict the snapshot cache, and serve the live bytes.
Smokes the regression where the pin was installed even when the
caller explicitly asked for live state.
"""
key = _probe_key()
probe = f"/s3/{key}"
client = _boto_client()
client.put_object(Bucket=LIVE_BUCKET, Key=key, Body=b"original\n")
try:
src = Workspace(_mount(), mode=MountMode.WRITE)
asyncio.run(src.execute(f"cat {probe}"))
snap = tmp_path / "off.tar"
asyncio.run(src.snapshot(snap))
client.put_object(Bucket=LIVE_BUCKET, Key=key, Body=b"mutated\n")
dst = Workspace.load(snap,
resources=_override(),
drift_policy=DriftPolicy.OFF)
assert dst.revisions == {}
result = asyncio.run(dst.execute(f"cat {probe}"))
assert b"mutated" in result.stdout
finally:
_cleanup_key(key)
def test_live_stat_populates_revision_when_versioned(tmp_path):
"""Smoke: on a versioned bucket, S3Resource.stat must populate
FileStat.revision so capture_fingerprints records it. Tightens the
contract that downstream pin tests rely on.
"""
if not _versioning_enabled():
pytest.skip("bucket not versioned")
key = _probe_key()
probe = f"/s3/{key}"
client = _boto_client()
client.put_object(Bucket=LIVE_BUCKET, Key=key, Body=b"x\n")
try:
resource = S3Resource(_config())
asyncio.run(write_bytes(resource.accessor, probe, b"x\n"))
stat = asyncio.run(resource._ops["stat"](resource.accessor, probe))
assert stat.fingerprint is not None
assert stat.revision is not None, (
"versioned bucket head should carry a VersionId")
finally:
_cleanup_key(key)