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. =========
+148
View File
@@ -0,0 +1,148 @@
import json
from functools import partial
from types import SimpleNamespace
import pytest
from mirage.cache.index import RAMIndexCacheStore
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
class FakeCollection:
def __init__(self) -> None:
self.documents: dict[str, str] = {}
self.chunks: dict[str, list[dict]] = {}
self.get_calls: list[dict] = []
self.queries: list[dict] = []
self.contains_queries: list[dict] = []
async def get(self, **kwargs):
self.get_calls.append(kwargs)
ids = kwargs.get("ids")
if ids == ["__path_tree__"]:
return {"documents": [self.documents["__path_tree__"]]}
where = kwargs.get("where") or {}
slug = where.get("page_slug")
if isinstance(slug, dict):
slug = slug.get("$eq")
if slug is not None:
chunks = self.chunks.get(slug, [])
offset = kwargs.get("offset") or 0
limit = kwargs.get("limit")
if limit is not None:
chunks = chunks[offset:offset + limit]
elif offset:
chunks = chunks[offset:]
return {
"documents": [item["document"] for item in chunks],
"metadatas": [item["metadata"] for item in chunks],
}
where_document = kwargs.get("where_document") or {}
if "$contains" in where_document or "$regex" in where_document:
self.contains_queries.append(kwargs)
candidates = where.get("page_slug", {}).get("$in", [])
pattern = where_document.get("$contains") or where_document.get(
"$regex")
docs: list[str] = []
metadatas: list[dict] = []
for slug_item in candidates:
for chunk in self.chunks.get(slug_item, []):
if pattern in chunk["document"]:
docs.append(chunk["document"])
metadatas.append(chunk["metadata"])
return {"documents": docs, "metadatas": metadatas}
return {"documents": [], "metadatas": []}
async def query(self, **kwargs):
self.queries.append(kwargs)
return {
"documents": [["quickstart chunk", "api chunk"]],
"metadatas": [[{
"page_slug": "guides/quickstart"
}, {
"page_slug": "api/reference"
}]],
"distances": [[0.1, 0.25]],
}
def path_tree_document() -> str:
return json.dumps({
"guides/quickstart": {
"size": 12,
"created_at": "2026-01-01T00:00:00Z",
"updated_at": "2026-02-01T00:00:00Z",
},
"api/reference": {
"size": None,
"created_at": None,
"updated_at": None,
},
})
@pytest.fixture
def chroma_collection() -> FakeCollection:
collection = FakeCollection()
collection.documents["__path_tree__"] = path_tree_document()
collection.chunks["guides/quickstart"] = [
{
"document": "second",
"metadata": {
"page_slug": "guides/quickstart",
"chunk_index": 2,
},
},
{
"document": "first",
"metadata": {
"page_slug": "guides/quickstart",
"chunk_index": 1,
},
},
]
collection.chunks["api/reference"] = [{
"document": "api",
"metadata": {
"page_slug": "api/reference",
"chunk_index": 0,
},
}]
return collection
async def _get_collection(collection):
return collection
@pytest.fixture
def chroma_accessor(chroma_collection) -> SimpleNamespace:
return SimpleNamespace(
config=SimpleNamespace(slug_field="page_slug",
chunk_index_field="chunk_index"),
collection=chroma_collection,
get_collection=partial(_get_collection, chroma_collection))
@pytest.fixture
def chroma_index() -> RAMIndexCacheStore:
return RAMIndexCacheStore()
@pytest.fixture
def knowledge_root() -> PathSpec:
return PathSpec(resource_path=mount_key("/knowledge", "/knowledge"),
virtual="/knowledge",
directory="/knowledge")
@pytest.fixture
def quickstart_path() -> PathSpec:
return PathSpec.from_str_path(
"/knowledge/guides/quickstart",
mount_key("/knowledge/guides/quickstart", "/knowledge"))
+80
View File
@@ -0,0 +1,80 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage.core.chroma._client import (PAGE_CHUNK_BATCH_SIZE,
fetch_page_chunks, fetch_path_tree,
page_chunks, query_contains)
@pytest.mark.asyncio
async def test_fetch_path_tree(chroma_accessor):
raw = await fetch_path_tree(chroma_accessor)
assert "guides/quickstart" in raw
@pytest.mark.asyncio
async def test_fetch_path_tree_missing_raises(chroma_accessor,
chroma_collection):
del chroma_collection.documents["__path_tree__"]
chroma_collection.get = _empty_get
with pytest.raises(FileNotFoundError):
await fetch_path_tree(chroma_accessor)
async def _empty_get(**kwargs):
return {"documents": []}
@pytest.mark.asyncio
async def test_page_chunks_sorted_by_chunk_index(chroma_accessor):
chunks = await page_chunks(chroma_accessor, "guides/quickstart")
assert [c["document"] for c in chunks] == ["first", "second"]
@pytest.mark.asyncio
async def test_fetch_page_chunks_joins_with_newline(chroma_accessor):
text = await fetch_page_chunks(chroma_accessor, "guides/quickstart")
assert text == "first\nsecond"
@pytest.mark.asyncio
async def test_page_chunks_paginates(chroma_accessor, chroma_collection):
chroma_collection.chunks["big/page"] = [{
"document": f"chunk-{i}",
"metadata": {
"page_slug": "big/page",
"chunk_index": i
},
} for i in range(PAGE_CHUNK_BATCH_SIZE + 5)]
chunks = await page_chunks(chroma_accessor, "big/page")
assert len(chunks) == PAGE_CHUNK_BATCH_SIZE + 5
assert chunks[0]["document"] == "chunk-0"
assert chunks[-1]["document"] == f"chunk-{PAGE_CHUNK_BATCH_SIZE + 4}"
@pytest.mark.asyncio
async def test_query_contains_scopes_to_candidates(chroma_accessor):
matched = await query_contains(chroma_accessor, "first",
["guides/quickstart", "api/reference"])
assert matched == ["guides/quickstart"]
@pytest.mark.asyncio
async def test_query_contains_empty_candidates_short_circuits(
chroma_accessor, chroma_collection):
matched = await query_contains(chroma_accessor, "first", [])
assert matched == []
assert not chroma_collection.contains_queries
+82
View File
@@ -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 pytest
from mirage.core.chroma.find import find
from mirage.types import FindType
@pytest.mark.asyncio
async def test_find_all(chroma_accessor, chroma_index, knowledge_root):
results = await find(chroma_accessor, knowledge_root, index=chroma_index)
assert "/guides/quickstart" in results
assert "/api/reference" in results
assert "/guides" in results
@pytest.mark.asyncio
async def test_find_name_matches_mount_root_start_path(chroma_accessor,
chroma_index,
knowledge_root):
results = await find(chroma_accessor,
knowledge_root,
name="knowledge",
index=chroma_index)
assert results == ["/"]
@pytest.mark.asyncio
async def test_find_by_name(chroma_accessor, chroma_index, knowledge_root):
results = await find(chroma_accessor,
knowledge_root,
name="quick*",
index=chroma_index)
assert results == ["/guides/quickstart"]
@pytest.mark.asyncio
async def test_find_type_file(chroma_accessor, chroma_index, knowledge_root):
results = await find(chroma_accessor,
knowledge_root,
type=FindType.FILE,
index=chroma_index)
assert sorted(results) == ["/api/reference", "/guides/quickstart"]
@pytest.mark.asyncio
async def test_find_type_directory(chroma_accessor, chroma_index,
knowledge_root):
results = await find(chroma_accessor,
knowledge_root,
type=FindType.DIRECTORY,
index=chroma_index)
assert "/guides" in results
assert "/guides/quickstart" not in results
@pytest.mark.asyncio
async def test_find_iname_case_insensitive(chroma_accessor, chroma_index,
knowledge_root):
results = await find(chroma_accessor,
knowledge_root,
iname="QUICK*",
index=chroma_index)
assert results == ["/guides/quickstart"]
@pytest.mark.asyncio
async def test_find_missing_index_raises(chroma_accessor, knowledge_root):
with pytest.raises(ValueError, match="missing index"):
await find(chroma_accessor, knowledge_root, index=None)
+56
View File
@@ -0,0 +1,56 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage.core.chroma.glob import resolve_glob
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
@pytest.mark.asyncio
async def test_plain_path_passes_through(chroma_accessor, chroma_index,
quickstart_path):
result = await resolve_glob(chroma_accessor, [quickstart_path],
chroma_index)
assert result == [quickstart_path]
@pytest.mark.asyncio
async def test_pattern_expands_against_readdir(chroma_accessor, chroma_index):
pattern = PathSpec(resource_path=mount_key("/knowledge/guides/quick*",
"/knowledge"),
virtual="/knowledge/guides/quick*",
directory="/knowledge/guides",
pattern="quick*",
resolved=False)
result = await resolve_glob(chroma_accessor, [pattern], chroma_index)
assert [p.virtual for p in result] == ["/knowledge/guides/quickstart"]
@pytest.mark.asyncio
async def test_pattern_without_match_stays_literal(chroma_accessor,
chroma_index):
# bash with nullglob off: the unmatched glob word stays the literal so
# the command errors on it like GNU.
pattern = PathSpec(resource_path=mount_key("/knowledge/guides/*.zip",
"/knowledge"),
virtual="/knowledge/guides/*.zip",
directory="/knowledge/guides",
pattern="*.zip",
resolved=False)
result = await resolve_glob(chroma_accessor, [pattern], chroma_index)
assert [p.virtual for p in result] == ["/knowledge/guides/*.zip"]
assert result[0].resolved
assert result[0].pattern is None
+80
View File
@@ -0,0 +1,80 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage.core.chroma.path import resolve_path, virtual_key_for
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
@pytest.mark.asyncio
async def test_resolve_file(chroma_accessor, chroma_index, quickstart_path):
resolved = await resolve_path(chroma_accessor, quickstart_path,
chroma_index)
assert not resolved.is_dir
assert resolved.entry is not None
assert resolved.entry.extra["slug"] == "guides/quickstart"
@pytest.mark.asyncio
async def test_resolve_directory(chroma_accessor, chroma_index):
path = PathSpec.from_str_path("/knowledge/guides",
mount_key("/knowledge/guides", "/knowledge"))
resolved = await resolve_path(chroma_accessor, path, chroma_index)
assert resolved.is_dir
@pytest.mark.asyncio
async def test_resolve_mount_root(chroma_accessor, chroma_index,
knowledge_root):
resolved = await resolve_path(chroma_accessor, knowledge_root,
chroma_index)
assert resolved.is_dir
@pytest.mark.asyncio
async def test_resolve_missing_raises(chroma_accessor, chroma_index):
path = PathSpec.from_str_path(
"/knowledge/missing", mount_key("/knowledge/missing", "/knowledge"))
with pytest.raises(FileNotFoundError):
await resolve_path(chroma_accessor, path, chroma_index)
@pytest.mark.asyncio
async def test_resolve_str_path_coerced(chroma_accessor, chroma_index):
resolved = await resolve_path(chroma_accessor, "/guides/quickstart",
chroma_index)
assert not resolved.is_dir
def test_virtual_key_for_prefixed():
path = PathSpec.from_str_path(
"/knowledge/guides/quickstart",
mount_key("/knowledge/guides/quickstart", "/knowledge"))
assert virtual_key_for(path) == "/knowledge/guides/quickstart"
def test_virtual_key_for_root():
path = PathSpec(resource_path=mount_key("/knowledge", "/knowledge"),
virtual="/knowledge",
directory="/knowledge")
assert virtual_key_for(path) == "/knowledge"
def test_virtual_key_for_unprefixed():
path = PathSpec(resource_path="guides/quickstart",
virtual="/guides/quickstart",
directory="/guides")
assert virtual_key_for(path) == "/guides/quickstart"
+48
View File
@@ -0,0 +1,48 @@
import pytest
from mirage.core.chroma import _client, read
@pytest.mark.asyncio
async def test_page_chunks_reads_in_batches(monkeypatch, chroma_accessor,
chroma_collection):
monkeypatch.setattr(_client, "PAGE_CHUNK_BATCH_SIZE", 1)
chunks = await _client.page_chunks(chroma_accessor, "guides/quickstart")
assert [chunk["document"] for chunk in chunks] == ["first", "second"]
page_calls = [
call for call in chroma_collection.get_calls if call.get("where") == {
"page_slug": "guides/quickstart"
}
]
assert [call["limit"] for call in page_calls] == [1, 1, 1]
assert [call["offset"] for call in page_calls] == [0, 1, 2]
@pytest.mark.asyncio
async def test_read_bytes_reassembles_sorted_chunks(chroma_accessor,
chroma_index,
quickstart_path):
data = await read.read_bytes(chroma_accessor, quickstart_path,
chroma_index)
assert data == b"first\nsecond"
@pytest.mark.asyncio
async def test_read_stream_yields_sorted_chunks(chroma_accessor, chroma_index,
quickstart_path):
chunks = [
chunk async for chunk in read.read_stream(
chroma_accessor, quickstart_path, chroma_index)
]
assert chunks == [b"first", b"\n", b"second"]
@pytest.mark.asyncio
async def test_read_bytes_rejects_directories(chroma_accessor, chroma_index,
knowledge_root):
with pytest.raises(IsADirectoryError):
await read.read_bytes(chroma_accessor, knowledge_root, chroma_index)
+41
View File
@@ -0,0 +1,41 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage.core.chroma.readdir import readdir
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
@pytest.mark.asyncio
async def test_readdir_root_lists_top_level(chroma_accessor, chroma_index,
knowledge_root):
entries = await readdir(chroma_accessor, knowledge_root, chroma_index)
assert sorted(entries) == ["/knowledge/api", "/knowledge/guides"]
@pytest.mark.asyncio
async def test_readdir_subdir(chroma_accessor, chroma_index):
path = PathSpec.from_str_path("/knowledge/guides",
mount_key("/knowledge/guides", "/knowledge"))
entries = await readdir(chroma_accessor, path, chroma_index)
assert entries == ["/knowledge/guides/quickstart"]
@pytest.mark.asyncio
async def test_readdir_on_file_raises(chroma_accessor, chroma_index,
quickstart_path):
with pytest.raises(NotADirectoryError):
await readdir(chroma_accessor, quickstart_path, chroma_index)
+51
View File
@@ -0,0 +1,51 @@
import pytest
from mirage.core.chroma import search
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
@pytest.mark.asyncio
async def test_search_segments_scopes_folder_to_candidate_slugs(
chroma_accessor, chroma_index):
result = await search.search_segments(
chroma_accessor,
"setup",
[
PathSpec(resource_path=mount_key("/knowledge/guides",
"/knowledge"),
virtual="/knowledge/guides",
directory="/knowledge/guides")
],
chroma_index,
top_k=3,
)
assert result == b"/knowledge/guides/quickstart:0.90\nquickstart chunk\n"
assert chroma_accessor.collection.queries[0]["where"] == {
"page_slug": {
"$in": ["guides/quickstart"]
}
}
assert chroma_accessor.collection.queries[0]["query_texts"] == ["setup"]
assert chroma_accessor.collection.queries[0]["n_results"] == 3
@pytest.mark.asyncio
async def test_search_segments_empty_paths_searches_collection(
chroma_accessor, chroma_index):
result = await search.search_segments(chroma_accessor,
"setup", [],
chroma_index,
mount_prefix="/knowledge/")
assert result == (b"/knowledge/guides/quickstart:0.90\nquickstart chunk\n"
b"/knowledge/api/reference:0.75\napi chunk\n")
assert "where" not in chroma_accessor.collection.queries[0]
def test_validate_args():
with pytest.raises(ValueError, match="query is required"):
search.validate_args("", 10)
with pytest.raises(ValueError, match="top-k must be positive"):
search.validate_args("docs", 0)
+53
View File
@@ -0,0 +1,53 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage.core.chroma.stat import stat, stat_name
from mirage.types import FileType, PathSpec
from mirage.utils.key_prefix import mount_key
@pytest.mark.asyncio
async def test_stat_file(chroma_accessor, chroma_index, quickstart_path):
result = await stat(chroma_accessor, quickstart_path, chroma_index)
assert result.type == FileType.TEXT
assert result.name == "quickstart"
assert result.size == 12
assert result.modified == "2026-02-01T00:00:00Z"
@pytest.mark.asyncio
async def test_stat_directory(chroma_accessor, chroma_index):
path = PathSpec.from_str_path("/knowledge/guides",
mount_key("/knowledge/guides", "/knowledge"))
result = await stat(chroma_accessor, path, chroma_index)
assert result.type == FileType.DIRECTORY
assert result.name == "guides"
@pytest.mark.asyncio
async def test_stat_missing_raises(chroma_accessor, chroma_index):
path = PathSpec.from_str_path(
"/knowledge/missing", mount_key("/knowledge/missing", "/knowledge"))
with pytest.raises(FileNotFoundError):
await stat(chroma_accessor, path, chroma_index)
def test_stat_name_root():
assert stat_name("/knowledge", "/knowledge/") == "/"
def test_stat_name_nested():
assert stat_name("/knowledge/guides", "/knowledge/") == "guides"
+58
View File
@@ -0,0 +1,58 @@
import base64
import gzip
import json
import pytest
from mirage.core.chroma import tree
@pytest.mark.asyncio
async def test_ensure_tree_builds_prefixed_entries_from_path_tree(
chroma_accessor, chroma_index):
await tree.ensure_tree(chroma_accessor, chroma_index, "/knowledge/")
root = await chroma_index.list_dir("/knowledge")
guides = await chroma_index.list_dir("/knowledge/guides")
api = await chroma_index.list_dir("/knowledge/api")
quickstart = await chroma_index.get("/knowledge/guides/quickstart")
assert root.entries == ["/knowledge/api", "/knowledge/guides"]
assert guides.entries == ["/knowledge/guides/quickstart"]
assert api.entries == ["/knowledge/api/reference"]
assert quickstart.entry.id == "guides/quickstart"
assert quickstart.entry.size == 12
assert quickstart.entry.remote_time == "2026-02-01T00:00:00Z"
assert quickstart.entry.extra == {
"slug": "guides/quickstart",
"size": 12,
"created_at": "2026-01-01T00:00:00Z",
"updated_at": "2026-02-01T00:00:00Z",
}
@pytest.mark.asyncio
async def test_ensure_tree_uses_cached_tree(chroma_accessor, chroma_index):
await tree.ensure_tree(chroma_accessor, chroma_index, "/knowledge/")
chroma_accessor.collection.documents["__path_tree__"] = json.dumps(
{"other": {}})
await tree.ensure_tree(chroma_accessor, chroma_index, "/knowledge/")
assert (await
chroma_index.get("/knowledge/guides/quickstart")).entry is not None
assert (await chroma_index.get("/knowledge/other")).entry is None
def test_parse_path_tree_accepts_gzip_base64():
encoded = base64.b64encode(gzip.compress(b'{"docs/a": {}}')).decode()
assert tree.parse_path_tree(encoded) == {"docs/a": {}}
def test_build_dir_entries_rejects_duplicate_and_collision():
with pytest.raises(ValueError, match="Duplicate Chroma path"):
tree.build_dir_entries({"a": {}, "/a/": {}}, "")
with pytest.raises(ValueError, match="Path collision"):
tree.build_dir_entries({"a": {}, "a/b": {}}, "")
+78
View File
@@ -0,0 +1,78 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage.core.chroma.walk import walk
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
@pytest.mark.asyncio
async def test_walk_recurses_all(chroma_accessor, chroma_index,
knowledge_root):
results = await walk(chroma_accessor, knowledge_root, chroma_index)
assert "/knowledge/guides/quickstart" in results
assert "/knowledge/api/reference" in results
assert "/knowledge/guides" in results
@pytest.mark.asyncio
async def test_walk_include_root(chroma_accessor, chroma_index,
knowledge_root):
results = await walk(chroma_accessor,
knowledge_root,
chroma_index,
include_root=True)
assert results[0] == "/knowledge"
@pytest.mark.asyncio
async def test_walk_maxdepth_limits(chroma_accessor, chroma_index,
knowledge_root):
results = await walk(chroma_accessor,
knowledge_root,
chroma_index,
maxdepth=1)
assert "/knowledge/guides" in results
assert "/knowledge/guides/quickstart" not in results
@pytest.mark.asyncio
async def test_walk_strip_prefix(chroma_accessor, chroma_index,
knowledge_root):
results = await walk(chroma_accessor,
knowledge_root,
chroma_index,
strip_prefix=True)
assert all(not item.startswith("/knowledge") for item in results)
@pytest.mark.asyncio
async def test_walk_missing_raises(chroma_accessor, chroma_index):
path = PathSpec.from_str_path(
"/knowledge/missing", mount_key("/knowledge/missing", "/knowledge"))
with pytest.raises(FileNotFoundError):
await walk(chroma_accessor, path, chroma_index)
@pytest.mark.asyncio
async def test_walk_missing_ignored(chroma_accessor, chroma_index):
path = PathSpec.from_str_path(
"/knowledge/missing", mount_key("/knowledge/missing", "/knowledge"))
results = await walk(chroma_accessor,
path,
chroma_index,
ignore_missing=True)
assert results == []
@@ -0,0 +1,267 @@
import posixpath
from io import BytesIO
from types import SimpleNamespace
from urllib.parse import unquote
import pytest
from mirage.accessor.databricks_volume import DatabricksVolumeAccessor
from mirage.cache.index import RAMIndexCacheStore
from mirage.core.databricks_volume.path import backend_path
from mirage.resource.databricks_volume import DatabricksVolumeConfig
class NotFoundError(Exception):
status_code = 404
class ToThreadRecorder:
def __init__(self) -> None:
self.calls = []
async def __call__(self, fn, *args, **kwargs):
self.calls.append((fn, args, kwargs))
return fn(*args, **kwargs)
class FakeDownload:
def __init__(self, data: bytes) -> None:
self.contents = BytesIO(data)
class FakeFiles:
def __init__(self) -> None:
self.downloads: dict[str, bytes] = {}
self.metadata: dict[str, object] = {}
self.directory_metadata: set[str] = set()
self.directories: dict[str, list[object]] = {}
self.metadata_errors: dict[str, Exception] = {}
self.directory_metadata_errors: dict[str, Exception] = {}
self.download_calls: list[str] = []
self.get_metadata_calls: list[str] = []
self.get_directory_metadata_calls: list[str] = []
self.list_directory_calls: list[str] = []
self.upload_calls: list[tuple[str, bytes, bool]] = []
self.delete_calls: list[str] = []
self.create_directory_calls: list[str] = []
self.delete_directory_calls: list[str] = []
def download(self, path: str) -> FakeDownload:
self.download_calls.append(path)
if path not in self.downloads:
raise NotFoundError(path)
return FakeDownload(self.downloads[path])
def get_metadata(self, path: str) -> object:
self.get_metadata_calls.append(path)
if path in self.metadata_errors:
raise self.metadata_errors[path]
if path not in self.metadata:
raise NotFoundError(path)
return self.metadata[path]
def get_directory_metadata(self, path: str) -> None:
self.get_directory_metadata_calls.append(path)
if path in self.directory_metadata_errors:
raise self.directory_metadata_errors[path]
if path not in self.directory_metadata:
raise NotFoundError(path)
def list_directory_contents(self, path: str) -> list[object]:
self.list_directory_calls.append(path)
if path not in self.directories:
raise NotFoundError(path)
return self.directories[path]
def create_directory(self, path: str) -> None:
self.create_directory_calls.append(path)
cur = ""
for part in path.strip("/").split("/"):
cur = cur + "/" + part
if cur in self.directory_metadata:
continue
self.directory_metadata.add(cur)
self.metadata[cur] = SimpleNamespace(is_directory=True)
self.directories.setdefault(cur, [])
parent = posixpath.dirname(cur) or "/"
self._upsert_directory_entry(
parent, SimpleNamespace(path=cur, is_directory=True))
def delete_directory(self, path: str) -> None:
self.delete_directory_calls.append(path)
if path not in self.directory_metadata:
raise NotFoundError(path)
if self.directories.get(path):
raise OSError(f"directory not empty: {path}")
self.directory_metadata.discard(path)
self.metadata.pop(path, None)
self.directories.pop(path, None)
parent = posixpath.dirname(path.rstrip("/")) or "/"
self.directories[parent] = [
entry for entry in self.directories.get(parent, [])
if getattr(entry, "path", None) != path
]
def upload(self, path: str, contents, overwrite: bool = False) -> None:
data = contents.read()
self.upload_calls.append((path, data, overwrite))
parent = posixpath.dirname(path.rstrip("/")) or "/"
if parent not in self.directory_metadata:
if parent in self.metadata:
raise NotADirectoryError(parent)
raise NotFoundError(parent)
if path in self.directory_metadata:
raise IsADirectoryError(path)
self.downloads[path] = data
self.metadata[path] = file_metadata(len(data))
self._upsert_directory_entry(parent, file_entry(path, len(data)))
def delete(self, path: str) -> None:
self.delete_calls.append(path)
if path in self.directory_metadata:
raise IsADirectoryError(path)
if path not in self.metadata and path not in self.downloads:
raise NotFoundError(path)
self.metadata.pop(path, None)
self.downloads.pop(path, None)
parent = posixpath.dirname(path.rstrip("/")) or "/"
self.directories[parent] = [
entry for entry in self.directories.get(parent, [])
if getattr(entry, "path", None) != path
]
def _upsert_directory_entry(self, parent: str, entry: object) -> None:
entries = [
existing for existing in self.directories.get(parent, [])
if getattr(existing, "path", None) != getattr(entry, "path", None)
]
entries.append(entry)
self.directories[parent] = sorted(
entries, key=lambda item: getattr(item, "path", ""))
def _apply_range_header(data: bytes, range_header: str) -> bytes:
if not range_header.startswith("bytes="):
raise ValueError(f"unsupported range header: {range_header}")
start_text, end_text = range_header.removeprefix("bytes=").split("-", 1)
start = int(start_text) if start_text else 0
end = int(end_text) + 1 if end_text else None
return data[start:end]
class FakeApiClient:
def __init__(self, files: FakeFiles) -> None:
self.files = files
self.do_calls: list[dict[str, object]] = []
def do(
self,
method: str,
path: str | None = None,
url: str | None = None,
query: dict | None = None,
headers: dict | None = None,
body: dict | None = None,
raw: bool = False,
files: object = None,
data: object = None,
auth: object = None,
response_headers: list[str] | None = None,
) -> dict:
call = {
"method": method,
"path": path,
"url": url,
"query": query,
"headers": headers or {},
"body": body,
"raw": raw,
"files": files,
"data": data,
"auth": auth,
"response_headers": response_headers,
}
self.do_calls.append(call)
if method != "GET" or path is None:
raise ValueError(f"unsupported fake API call: {method} {path}")
remote_path = unquote(path.removeprefix("/api/2.0/fs/files"))
if remote_path not in self.files.downloads:
raise NotFoundError(remote_path)
payload = self.files.downloads[remote_path]
range_header = (headers or {}).get("Range")
if range_header is not None:
payload = _apply_range_header(payload, range_header)
return {
"contents": BytesIO(payload),
"content-length": str(len(payload)),
"accept-ranges": "bytes",
}
class FakeClient:
def __init__(self, files: FakeFiles) -> None:
self.files = files
self.api_client = FakeApiClient(files)
@pytest.fixture
def databricks_config() -> DatabricksVolumeConfig:
return DatabricksVolumeConfig(
catalog="main",
schema="default",
volume="agent_files",
root_path="/root",
token="secret",
)
@pytest.fixture
def remote_root(databricks_config: DatabricksVolumeConfig) -> str:
return backend_path(databricks_config, "/")
@pytest.fixture
def files() -> FakeFiles:
return FakeFiles()
@pytest.fixture
def accessor(
databricks_config: DatabricksVolumeConfig,
files: FakeFiles,
) -> DatabricksVolumeAccessor:
return DatabricksVolumeAccessor(databricks_config, FakeClient(files))
@pytest.fixture
def index() -> RAMIndexCacheStore:
return RAMIndexCacheStore(ttl=600)
def file_metadata(size: int = 0, modified: str | None = None) -> object:
return SimpleNamespace(
content_length=size,
content_type=None,
last_modified=modified,
)
def directory_entry(path: str, modified: int | None = None) -> object:
return SimpleNamespace(path=path,
is_directory=True,
file_size=None,
last_modified=modified)
def file_entry(path: str,
size: int = 0,
modified: int | None = None) -> object:
return SimpleNamespace(path=path,
is_directory=False,
file_size=size,
last_modified=modified)
@@ -0,0 +1,148 @@
# ========= 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 types import SimpleNamespace
import pytest
from mirage.core.databricks_volume.copy import copy
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
def _path(path: str) -> PathSpec:
return PathSpec.from_str_path(path, mount_key(path, "/dbx"))
def _seed_directory(files, path: str) -> None:
files.directory_metadata.add(path)
files.directories.setdefault(path, [])
parent = path.rsplit("/", 1)[0]
if parent and parent != path:
files.directories.setdefault(parent, []).append(
SimpleNamespace(path=path, is_directory=True, file_size=None))
def _seed_file(files, path: str, data: bytes) -> None:
parent = path.rsplit("/", 1)[0]
files.downloads[path] = data
files.metadata[path] = SimpleNamespace(is_directory=False,
file_size=len(data))
files.directories.setdefault(parent, []).append(
SimpleNamespace(path=path, is_directory=False, file_size=len(data)))
@pytest.mark.asyncio
async def test_copy_file_preserves_bytes(accessor, files, remote_root, index):
_seed_directory(files, remote_root)
_seed_file(files, f"{remote_root}/src.txt", b"hello")
await copy(accessor, _path("/dbx/src.txt"), _path("/dbx/dst.txt"), index)
assert files.downloads[f"{remote_root}/dst.txt"] == b"hello"
assert files.downloads[f"{remote_root}/src.txt"] == b"hello"
@pytest.mark.asyncio
async def test_copy_missing_source_fails(accessor, files, remote_root, index):
_seed_directory(files, remote_root)
with pytest.raises(FileNotFoundError):
await copy(accessor, _path("/dbx/missing.txt"), _path("/dbx/dst.txt"),
index)
@pytest.mark.asyncio
async def test_copy_missing_parent_fails(accessor, files, remote_root, index):
_seed_directory(files, remote_root)
_seed_file(files, f"{remote_root}/src.txt", b"hi")
with pytest.raises(FileNotFoundError):
await copy(accessor, _path("/dbx/src.txt"),
_path("/dbx/missing/dst.txt"), index)
@pytest.mark.asyncio
async def test_copy_directory_without_recursive_fails(accessor, files,
remote_root, index):
_seed_directory(files, remote_root)
_seed_directory(files, f"{remote_root}/d")
with pytest.raises(IsADirectoryError):
await copy(accessor, _path("/dbx/d"), _path("/dbx/d2"), index)
@pytest.mark.asyncio
async def test_copy_same_path_is_noop(accessor, files, remote_root, index):
_seed_directory(files, remote_root)
_seed_file(files, f"{remote_root}/src.txt", b"hello")
await copy(accessor, _path("/dbx/src.txt"), _path("/dbx/src.txt"), index)
assert files.downloads[f"{remote_root}/src.txt"] == b"hello"
@pytest.mark.asyncio
async def test_copy_same_missing_path_fails(accessor, files, remote_root,
index):
_seed_directory(files, remote_root)
with pytest.raises(FileNotFoundError):
await copy(accessor, _path("/dbx/missing.txt"),
_path("/dbx/missing.txt"), index)
@pytest.mark.asyncio
async def test_copy_same_dir_without_recursive_fails(accessor, files,
remote_root, index):
_seed_directory(files, remote_root)
_seed_directory(files, f"{remote_root}/d")
with pytest.raises(IsADirectoryError):
await copy(accessor, _path("/dbx/d"), _path("/dbx/d"), index)
@pytest.mark.asyncio
async def test_copy_recursive_tree(accessor, files, remote_root, index):
_seed_directory(files, remote_root)
_seed_directory(files, f"{remote_root}/d")
_seed_file(files, f"{remote_root}/d/a.txt", b"aaa")
await copy(accessor,
_path("/dbx/d"),
_path("/dbx/d2"),
index,
recursive=True)
assert f"{remote_root}/d2" in files.directory_metadata
assert files.downloads[f"{remote_root}/d2/a.txt"] == b"aaa"
@pytest.mark.asyncio
async def test_copy_recursive_into_own_subtree_fails(accessor, files,
remote_root, index):
_seed_directory(files, remote_root)
_seed_directory(files, f"{remote_root}/d")
_seed_file(files, f"{remote_root}/d/a.txt", b"aaa")
with pytest.raises(ValueError):
await copy(accessor,
_path("/dbx/d"),
_path("/dbx/d/d"),
index,
recursive=True)
assert files.create_directory_calls == []
assert files.upload_calls == []
assert files.downloads[f"{remote_root}/d/a.txt"] == b"aaa"
@@ -0,0 +1,23 @@
import pytest
from mirage.core.databricks_volume.exists import exists
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
from .conftest import file_metadata
@pytest.mark.asyncio
async def test_exists_true_for_file(accessor, files, remote_root):
files.metadata[f"{remote_root}/reports/latest.md"] = file_metadata(size=6)
path = PathSpec.from_str_path(
"/volume/reports/latest.md",
mount_key("/volume/reports/latest.md", "/volume"))
assert await exists(accessor, path) is True
@pytest.mark.asyncio
async def test_exists_false_for_missing_path(accessor):
path = PathSpec.from_str_path("/volume/missing.md",
mount_key("/volume/missing.md", "/volume"))
assert await exists(accessor, path) is False
@@ -0,0 +1,46 @@
import pytest
from mirage.core.databricks_volume.glob import resolve_glob
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
from .conftest import file_entry
@pytest.mark.asyncio
async def test_resolve_file_path(accessor, index):
scope = PathSpec.from_str_path("/volume/readme.md",
mount_key("/volume/readme.md", "/volume"))
result = await resolve_glob(accessor, [scope], index)
assert result == [scope]
@pytest.mark.asyncio
async def test_resolve_glob_pattern(accessor, files, index, remote_root):
files.directories[f"{remote_root}/src"] = [
file_entry(f"{remote_root}/src/main.py"),
file_entry(f"{remote_root}/src/util.py"),
file_entry(f"{remote_root}/src/data.json"),
]
scope = PathSpec(
resource_path=mount_key("/volume/src/*.py", "/volume"),
virtual="/volume/src/*.py",
directory="/volume/src",
pattern="*.py",
resolved=False,
)
result = await resolve_glob(accessor, [scope], index)
originals = sorted(path.virtual for path in result)
assert originals == ["/volume/src/main.py", "/volume/src/util.py"]
@pytest.mark.asyncio
async def test_resolve_directory_path(accessor, index):
scope = PathSpec(
resource_path=mount_key("/volume/src", "/volume"),
virtual="/volume/src",
directory="/volume/src",
resolved=False,
)
result = await resolve_glob(accessor, [scope], index)
assert result == [scope]
@@ -0,0 +1,29 @@
import pytest
from mirage.commands.builtin.databricks_volume.head import head
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
async def collect_bytes(source) -> bytes:
return b"".join([chunk async for chunk in source])
@pytest.mark.asyncio
async def test_head_bytes_mode_uses_single_small_range_request(
accessor,
files,
remote_root,
):
files.downloads[f"{remote_root}/reports/latest.md"] = b"abcdef"
path = PathSpec.from_str_path(
"/volume/reports/latest.md",
mount_key("/volume/reports/latest.md", "/volume"))
source, _io = await head(accessor, [path], c="3")
assert await collect_bytes(source) == b"abc"
assert files.download_calls == []
assert len(accessor.client.api_client.do_calls) == 1
assert accessor.client.api_client.do_calls[0]["headers"]["Range"] == (
"bytes=0-2")
@@ -0,0 +1,79 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from types import SimpleNamespace
import pytest
from mirage.core.databricks_volume.mkdir import mkdir
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
def _path(path: str) -> PathSpec:
return PathSpec.from_str_path(path, mount_key(path, "/dbx"))
def _seed_directory(files, path: str) -> None:
files.directory_metadata.add(path)
files.directories.setdefault(path, [])
@pytest.mark.asyncio
async def test_mkdir_creates_directory(accessor, files, remote_root, index):
_seed_directory(files, remote_root)
await mkdir(accessor, _path("/dbx/newdir"), index)
assert f"{remote_root}/newdir" in files.create_directory_calls
assert f"{remote_root}/newdir" in files.directory_metadata
@pytest.mark.asyncio
async def test_mkdir_parent_missing_fails(accessor, files, remote_root, index):
_seed_directory(files, remote_root)
with pytest.raises(FileNotFoundError):
await mkdir(accessor, _path("/dbx/a/b"), index)
assert files.create_directory_calls == []
@pytest.mark.asyncio
async def test_mkdir_parents_creates_chain(accessor, files, remote_root,
index):
_seed_directory(files, remote_root)
await mkdir(accessor, _path("/dbx/a/b/c"), index, parents=True)
assert f"{remote_root}/a/b/c" in files.directory_metadata
@pytest.mark.asyncio
async def test_mkdir_existing_target_fails(accessor, files, remote_root,
index):
_seed_directory(files, remote_root)
_seed_directory(files, f"{remote_root}/exists")
with pytest.raises(FileExistsError):
await mkdir(accessor, _path("/dbx/exists"), index)
@pytest.mark.asyncio
async def test_mkdir_parent_is_file_fails(accessor, files, remote_root, index):
_seed_directory(files, remote_root)
files.metadata[f"{remote_root}/file.txt"] = SimpleNamespace(
is_directory=False, file_size=3)
with pytest.raises(NotADirectoryError):
await mkdir(accessor, _path("/dbx/file.txt/sub"), index)
@@ -0,0 +1,60 @@
import pytest
from pydantic import ValidationError
from mirage.core.databricks_volume.path import backend_path, virtual_path
from mirage.resource.databricks_volume import DatabricksVolumeConfig
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
def test_backend_path_uses_volume_root_and_strips_mount_prefix(
databricks_config):
path = PathSpec(
resource_path=mount_key("/volume/reports/latest.md", "/volume"),
virtual="/volume/reports/latest.md",
directory="/volume/reports",
)
assert backend_path(
databricks_config,
path) == ("/Volumes/main/default/agent_files/root/reports/latest.md")
def test_backend_path_allows_normalized_path_inside_root(databricks_config):
path = PathSpec(
resource_path=mount_key("/volume/reports/../latest.md", "/volume"),
virtual="/volume/reports/../latest.md",
directory="/volume/reports",
)
assert backend_path(
databricks_config,
path) == ("/Volumes/main/default/agent_files/root/latest.md")
def test_backend_path_rejects_escape_above_configured_root(databricks_config):
path = PathSpec(
resource_path=mount_key(
"/volume/../../other_schema/other_volume/secret.txt", "/volume"),
virtual="/volume/../../other_schema/other_volume/secret.txt",
directory="/volume",
)
with pytest.raises(ValueError, match="escapes Databricks volume root"):
backend_path(databricks_config, path)
def test_config_rejects_parent_segments_in_root_path():
with pytest.raises(ValidationError):
DatabricksVolumeConfig(
catalog="main",
schema="default",
volume="agent_files",
root_path="/root/../other",
)
def test_virtual_path_rejects_backend_outside_root(databricks_config):
with pytest.raises(ValueError, match="outside Databricks volume root"):
virtual_path(
databricks_config,
"/Volumes/main/default/other_volume/secret.txt",
"/volume",
)
@@ -0,0 +1,115 @@
import asyncio
import pytest
from mirage.core.databricks_volume.read import read_bytes
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
from .conftest import ToThreadRecorder
@pytest.mark.asyncio
async def test_read_file(accessor, files, remote_root):
files.downloads[f"{remote_root}/reports/latest.md"] = b"hello"
path = PathSpec.from_str_path(
"/volume/reports/latest.md",
mount_key("/volume/reports/latest.md", "/volume"))
result = await read_bytes(accessor, path)
assert result == b"hello"
assert files.download_calls == [f"{remote_root}/reports/latest.md"]
@pytest.mark.asyncio
async def test_read_file_not_found(accessor):
path = PathSpec.from_str_path("/volume/missing.md",
mount_key("/volume/missing.md", "/volume"))
with pytest.raises(FileNotFoundError):
await read_bytes(accessor, path)
@pytest.mark.asyncio
async def test_read_slice(accessor, files, remote_root):
files.downloads[f"{remote_root}/reports/latest.md"] = b"abcdef"
path = PathSpec.from_str_path(
"/volume/reports/latest.md",
mount_key("/volume/reports/latest.md", "/volume"))
result = await read_bytes(accessor, path, offset=1, size=3)
assert result == b"bcd"
@pytest.mark.asyncio
async def test_read_file_runs_blocking_download_off_event_loop(
accessor,
files,
remote_root,
monkeypatch,
):
to_thread = ToThreadRecorder()
monkeypatch.setattr(asyncio, "to_thread", to_thread)
files.downloads[f"{remote_root}/reports/latest.md"] = b"hello"
path = PathSpec.from_str_path(
"/volume/reports/latest.md",
mount_key("/volume/reports/latest.md", "/volume"))
result = await read_bytes(accessor, path)
assert result == b"hello"
assert len(to_thread.calls) == 1
@pytest.mark.asyncio
async def test_read_slice_uses_databricks_range_request(
accessor,
files,
remote_root,
):
files.downloads[f"{remote_root}/reports/latest.md"] = b"abcdef"
path = PathSpec.from_str_path(
"/volume/reports/latest.md",
mount_key("/volume/reports/latest.md", "/volume"))
result = await read_bytes(accessor, path, offset=1, size=3)
assert result == b"bcd"
assert files.download_calls == []
assert accessor.client.api_client.do_calls[0]["headers"]["Range"] == (
"bytes=1-3")
assert accessor.client.api_client.do_calls[0]["raw"] is True
@pytest.mark.asyncio
async def test_read_from_offset_uses_open_ended_range(
accessor,
files,
remote_root,
):
files.downloads[f"{remote_root}/reports/latest.md"] = b"abcdef"
path = PathSpec.from_str_path(
"/volume/reports/latest.md",
mount_key("/volume/reports/latest.md", "/volume"))
result = await read_bytes(accessor, path, offset=3)
assert result == b"def"
assert files.download_calls == []
assert accessor.client.api_client.do_calls[0]["headers"]["Range"] == (
"bytes=3-")
@pytest.mark.asyncio
async def test_read_zero_size_returns_empty_without_network(
accessor,
files,
remote_root,
):
files.downloads[f"{remote_root}/reports/latest.md"] = b"abcdef"
path = PathSpec.from_str_path(
"/volume/reports/latest.md",
mount_key("/volume/reports/latest.md", "/volume"))
result = await read_bytes(accessor, path, size=0)
assert result == b""
assert files.download_calls == []
assert accessor.client.api_client.do_calls == []
@@ -0,0 +1,101 @@
import asyncio
import pytest
from mirage.core.databricks_volume.readdir import readdir
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
from .conftest import ToThreadRecorder, directory_entry, file_entry
@pytest.mark.asyncio
async def test_readdir_returns_full_virtual_paths(
accessor,
files,
index,
remote_root,
):
files.directories[f"{remote_root}/reports"] = [
file_entry(f"{remote_root}/reports/latest.md", size=6),
directory_entry(f"{remote_root}/reports/archive"),
]
path = PathSpec.from_str_path("/volume/reports",
mount_key("/volume/reports", "/volume"))
result = await readdir(accessor, path, index)
assert result == [
"/volume/reports/archive",
"/volume/reports/latest.md",
]
@pytest.mark.asyncio
async def test_readdir_uses_cached_listing(accessor, files, index,
remote_root):
files.directories[f"{remote_root}/reports"] = [
file_entry(f"{remote_root}/reports/latest.md", size=6),
]
path = PathSpec.from_str_path("/volume/reports",
mount_key("/volume/reports", "/volume"))
assert await readdir(accessor, path,
index) == ["/volume/reports/latest.md"]
files.directories[f"{remote_root}/reports"] = []
assert await readdir(accessor, path,
index) == ["/volume/reports/latest.md"]
assert files.list_directory_calls == [f"{remote_root}/reports"]
@pytest.mark.asyncio
async def test_readdir_populates_index_with_size_and_modified(
accessor,
files,
index,
remote_root,
):
files.directories[f"{remote_root}/reports"] = [
file_entry(f"{remote_root}/reports/latest.md",
size=6,
modified=1_700_000_000_000),
directory_entry(f"{remote_root}/reports/archive"),
]
path = PathSpec.from_str_path("/volume/reports",
mount_key("/volume/reports", "/volume"))
await readdir(accessor, path, index)
file_lookup = await index.get("/volume/reports/latest.md")
assert file_lookup.entry is not None
assert file_lookup.entry.resource_type == "file"
assert file_lookup.entry.size == 6
assert file_lookup.entry.remote_time == "2023-11-14T22:13:20+00:00"
dir_lookup = await index.get("/volume/reports/archive")
assert dir_lookup.entry is not None
assert dir_lookup.entry.resource_type == "folder"
@pytest.mark.asyncio
async def test_readdir_missing_directory_raises(accessor, index):
path = PathSpec.from_str_path("/volume/missing",
mount_key("/volume/missing", "/volume"))
with pytest.raises(FileNotFoundError):
await readdir(accessor, path, index)
@pytest.mark.asyncio
async def test_readdir_runs_blocking_list_off_event_loop(
accessor,
files,
index,
remote_root,
monkeypatch,
):
to_thread = ToThreadRecorder()
monkeypatch.setattr(asyncio, "to_thread", to_thread)
files.directories[f"{remote_root}/reports"] = [
file_entry(f"{remote_root}/reports/latest.md", size=6),
]
path = PathSpec.from_str_path("/volume/reports",
mount_key("/volume/reports", "/volume"))
result = await readdir(accessor, path, index)
assert result == ["/volume/reports/latest.md"]
assert len(to_thread.calls) == 1
@@ -0,0 +1,116 @@
# ========= 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 types import SimpleNamespace
import pytest
from mirage.core.databricks_volume.rename import rename
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
def _path(path: str) -> PathSpec:
return PathSpec.from_str_path(path, mount_key(path, "/dbx"))
def _seed_directory(files, path: str) -> None:
files.directory_metadata.add(path)
files.directories.setdefault(path, [])
parent = path.rsplit("/", 1)[0]
if parent and parent != path:
files.directories.setdefault(parent, []).append(
SimpleNamespace(path=path, is_directory=True, file_size=None))
def _seed_file(files, path: str, data: bytes) -> None:
parent = path.rsplit("/", 1)[0]
files.downloads[path] = data
files.metadata[path] = SimpleNamespace(is_directory=False,
file_size=len(data))
files.directories.setdefault(parent, []).append(
SimpleNamespace(path=path, is_directory=False, file_size=len(data)))
@pytest.mark.asyncio
async def test_rename_file_moves_bytes(accessor, files, remote_root, index):
_seed_directory(files, remote_root)
_seed_file(files, f"{remote_root}/src.txt", b"data")
await rename(accessor, _path("/dbx/src.txt"), _path("/dbx/dst.txt"), index)
assert files.downloads[f"{remote_root}/dst.txt"] == b"data"
assert f"{remote_root}/src.txt" not in files.downloads
@pytest.mark.asyncio
async def test_rename_missing_source_fails(accessor, files, remote_root,
index):
_seed_directory(files, remote_root)
with pytest.raises(FileNotFoundError):
await rename(accessor, _path("/dbx/missing.txt"),
_path("/dbx/dst.txt"), index)
@pytest.mark.asyncio
async def test_rename_same_path_is_noop(accessor, files, remote_root, index):
_seed_directory(files, remote_root)
_seed_file(files, f"{remote_root}/src.txt", b"data")
await rename(accessor, _path("/dbx/src.txt"), _path("/dbx/src.txt"), index)
assert files.downloads[f"{remote_root}/src.txt"] == b"data"
assert f"{remote_root}/src.txt" not in files.delete_calls
@pytest.mark.asyncio
async def test_rename_same_missing_path_fails(accessor, files, remote_root,
index):
_seed_directory(files, remote_root)
with pytest.raises(FileNotFoundError):
await rename(accessor, _path("/dbx/missing.txt"),
_path("/dbx/missing.txt"), index)
@pytest.mark.asyncio
async def test_rename_directory_moves_tree(accessor, files, remote_root,
index):
_seed_directory(files, remote_root)
_seed_directory(files, f"{remote_root}/d")
_seed_file(files, f"{remote_root}/d/a.txt", b"aaa")
await rename(accessor, _path("/dbx/d"), _path("/dbx/d2"), index)
assert files.downloads[f"{remote_root}/d2/a.txt"] == b"aaa"
assert f"{remote_root}/d" not in files.directory_metadata
@pytest.mark.asyncio
async def test_rename_into_own_subtree_fails(accessor, files, remote_root,
index):
_seed_directory(files, remote_root)
_seed_directory(files, f"{remote_root}/d")
_seed_file(files, f"{remote_root}/d/a.txt", b"aaa")
with pytest.raises(ValueError):
await rename(accessor, _path("/dbx/d"), _path("/dbx/d/d"), index)
assert files.downloads[f"{remote_root}/d/a.txt"] == b"aaa"
assert f"{remote_root}/d" in files.directory_metadata
assert files.create_directory_calls == []
assert files.upload_calls == []
assert files.delete_calls == []
assert files.delete_directory_calls == []
@@ -0,0 +1,81 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from types import SimpleNamespace
import pytest
from mirage.core.databricks_volume.rm import rm_recursive
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
def _path(path: str) -> PathSpec:
return PathSpec.from_str_path(path, mount_key(path, "/dbx"))
def _seed_directory(files, path: str) -> None:
files.directory_metadata.add(path)
files.directories.setdefault(path, [])
parent = path.rsplit("/", 1)[0]
if parent and parent != path:
files.directories.setdefault(parent, []).append(
SimpleNamespace(path=path, is_directory=True, file_size=None))
def _seed_file(files, path: str, data: bytes = b"x") -> None:
parent = path.rsplit("/", 1)[0]
files.downloads[path] = data
files.metadata[path] = SimpleNamespace(is_directory=False,
file_size=len(data))
files.directories.setdefault(parent, []).append(
SimpleNamespace(path=path, is_directory=False, file_size=len(data)))
@pytest.mark.asyncio
async def test_rm_recursive_file(accessor, files, remote_root, index):
_seed_directory(files, remote_root)
_seed_file(files, f"{remote_root}/a.txt")
removed = await rm_recursive(accessor, _path("/dbx/a.txt"), index)
assert removed == ["/a.txt"]
assert f"{remote_root}/a.txt" not in files.downloads
@pytest.mark.asyncio
async def test_rm_recursive_nested_tree(accessor, files, remote_root, index):
_seed_directory(files, remote_root)
_seed_directory(files, f"{remote_root}/d")
_seed_file(files, f"{remote_root}/d/a.txt")
_seed_directory(files, f"{remote_root}/d/sub")
_seed_file(files, f"{remote_root}/d/sub/b.txt")
removed = await rm_recursive(accessor, _path("/dbx/d"), index)
assert f"{remote_root}/d/a.txt" in files.delete_calls
assert f"{remote_root}/d/sub/b.txt" in files.delete_calls
assert f"{remote_root}/d/sub" in files.delete_directory_calls
assert f"{remote_root}/d" in files.delete_directory_calls
assert "/d" in removed
assert f"{remote_root}/d" not in files.directory_metadata
@pytest.mark.asyncio
async def test_rm_recursive_missing_raises(accessor, files, remote_root,
index):
_seed_directory(files, remote_root)
with pytest.raises(FileNotFoundError):
await rm_recursive(accessor, _path("/dbx/missing"), index)
@@ -0,0 +1,79 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from types import SimpleNamespace
import pytest
from mirage.core.databricks_volume.rmdir import rmdir
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
def _path(path: str) -> PathSpec:
return PathSpec.from_str_path(path, mount_key(path, "/dbx"))
def _seed_directory(files, path: str) -> None:
files.directory_metadata.add(path)
files.directories.setdefault(path, [])
def _seed_file(files, path: str, data: bytes = b"x") -> None:
parent = path.rsplit("/", 1)[0]
files.downloads[path] = data
files.metadata[path] = SimpleNamespace(is_directory=False,
file_size=len(data))
files.directories.setdefault(parent, [])
files.directories[parent].append(
SimpleNamespace(path=path, is_directory=False, file_size=len(data)))
@pytest.mark.asyncio
async def test_rmdir_empty_succeeds(accessor, files, remote_root, index):
_seed_directory(files, remote_root)
_seed_directory(files, f"{remote_root}/empty")
await rmdir(accessor, _path("/dbx/empty"), index)
assert files.delete_directory_calls == [f"{remote_root}/empty"]
assert f"{remote_root}/empty" not in files.directory_metadata
@pytest.mark.asyncio
async def test_rmdir_non_empty_fails(accessor, files, remote_root, index):
_seed_directory(files, remote_root)
_seed_directory(files, f"{remote_root}/full")
_seed_file(files, f"{remote_root}/full/a.txt")
with pytest.raises(OSError):
await rmdir(accessor, _path("/dbx/full"), index)
assert files.delete_directory_calls == []
@pytest.mark.asyncio
async def test_rmdir_file_target_fails(accessor, files, remote_root, index):
_seed_directory(files, remote_root)
_seed_file(files, f"{remote_root}/a.txt")
with pytest.raises(NotADirectoryError):
await rmdir(accessor, _path("/dbx/a.txt"), index)
@pytest.mark.asyncio
async def test_rmdir_missing_fails(accessor, files, remote_root, index):
_seed_directory(files, remote_root)
with pytest.raises(FileNotFoundError):
await rmdir(accessor, _path("/dbx/missing"), index)
@@ -0,0 +1,311 @@
import asyncio
from datetime import datetime, timezone
import pytest
from mirage.cache.index import RAMIndexCacheStore
from mirage.core.databricks_volume.readdir import readdir
from mirage.core.databricks_volume.stat import (_name_from_backend_path,
modified_to_iso, stat)
from mirage.types import FileType, PathSpec
from mirage.utils.key_prefix import mount_key
from .conftest import (ToThreadRecorder, directory_entry, file_entry,
file_metadata)
def test_modified_none_and_empty_string_return_none():
assert modified_to_iso(None) is None
assert modified_to_iso("") is None
def test_modified_parses_http_date_to_iso_utc():
assert modified_to_iso(
"Tue, 14 Nov 2023 22:13:20 GMT") == "2023-11-14T22:13:20+00:00"
def test_modified_returns_unparseable_string_verbatim():
assert modified_to_iso("not a date") == "not a date"
def test_modified_coerces_naive_datetime_to_utc():
naive = datetime(2023, 11, 14, 22, 13, 20)
assert modified_to_iso(naive) == "2023-11-14T22:13:20+00:00"
def test_modified_converts_aware_datetime_to_utc():
aware = datetime(2023, 11, 14, 22, 13, 20, tzinfo=timezone.utc)
assert modified_to_iso(aware) == "2023-11-14T22:13:20+00:00"
def test_modified_treats_large_int_as_epoch_milliseconds():
assert modified_to_iso(1_700_000_000_000) == "2023-11-14T22:13:20+00:00"
def test_name_from_backend_path_file():
assert _name_from_backend_path(
"/Volumes/main/default/agent_files/root/latest.md") == "latest.md"
def test_name_from_backend_path_directory_with_trailing_slash():
assert _name_from_backend_path(
"/Volumes/main/default/agent_files/root/reports/") == "reports"
@pytest.mark.asyncio
async def test_stat_file(accessor, files, remote_root):
files.metadata[f"{remote_root}/reports/latest.md"] = file_metadata(
size=6,
modified="Tue, 14 Nov 2023 22:13:20 GMT",
)
path = PathSpec.from_str_path(
"/volume/reports/latest.md",
mount_key("/volume/reports/latest.md", "/volume"))
result = await stat(accessor, path)
assert result.name == "latest.md"
assert result.size == 6
assert result.modified == "2023-11-14T22:13:20+00:00"
assert result.type != FileType.DIRECTORY
@pytest.mark.asyncio
async def test_stat_file_from_index_skips_sdk(accessor, files, index,
remote_root):
files.directories[f"{remote_root}/reports"] = [
file_entry(f"{remote_root}/reports/latest.md",
size=6,
modified=1_700_000_000_000),
]
await readdir(
accessor,
PathSpec.from_str_path("/volume/reports",
mount_key("/volume/reports", "/volume")), index)
path = PathSpec.from_str_path(
"/volume/reports/latest.md",
mount_key("/volume/reports/latest.md", "/volume"))
result = await stat(accessor, path, index)
assert result.name == "latest.md"
assert result.size == 6
assert result.modified == "2023-11-14T22:13:20+00:00"
assert result.type != FileType.DIRECTORY
assert files.get_metadata_calls == []
assert files.get_directory_metadata_calls == []
@pytest.mark.asyncio
async def test_stat_directory_from_index_skips_sdk(accessor, files, index,
remote_root):
files.directories[f"{remote_root}/reports"] = [
directory_entry(f"{remote_root}/reports/archive"),
]
await readdir(
accessor,
PathSpec.from_str_path("/volume/reports",
mount_key("/volume/reports", "/volume")), index)
path = PathSpec.from_str_path(
"/volume/reports/archive",
mount_key("/volume/reports/archive", "/volume"))
result = await stat(accessor, path, index)
assert result.name == "archive"
assert result.type == FileType.DIRECTORY
assert files.get_metadata_calls == []
assert files.get_directory_metadata_calls == []
@pytest.mark.asyncio
async def test_stat_index_negative_cache_raises_without_sdk(
accessor,
files,
index,
remote_root,
):
files.directories[f"{remote_root}/reports"] = [
file_entry(f"{remote_root}/reports/latest.md", size=6),
]
await readdir(
accessor,
PathSpec.from_str_path("/volume/reports",
mount_key("/volume/reports", "/volume")), index)
path = PathSpec.from_str_path(
"/volume/reports/missing.md",
mount_key("/volume/reports/missing.md", "/volume"))
with pytest.raises(FileNotFoundError):
await stat(accessor, path, index)
assert files.get_metadata_calls == []
assert files.get_directory_metadata_calls == []
@pytest.mark.asyncio
async def test_stat_index_fast_path_matches_sdk(accessor, files, index,
remote_root):
files.directories[f"{remote_root}/reports"] = [
file_entry(f"{remote_root}/reports/latest.md",
size=6,
modified=1_700_000_000_000),
]
files.metadata[f"{remote_root}/reports/latest.md"] = file_metadata(
size=6,
modified="Tue, 14 Nov 2023 22:13:20 GMT",
)
await readdir(
accessor,
PathSpec.from_str_path("/volume/reports",
mount_key("/volume/reports", "/volume")), index)
path = PathSpec.from_str_path(
"/volume/reports/latest.md",
mount_key("/volume/reports/latest.md", "/volume"))
fast = await stat(accessor, path, index)
slow = await stat(accessor, path, RAMIndexCacheStore(ttl=600))
assert fast == slow
assert files.get_metadata_calls == [f"{remote_root}/reports/latest.md"]
@pytest.mark.asyncio
async def test_stat_directory_index_fast_path_matches_sdk(
accessor,
files,
index,
remote_root,
):
files.directories[f"{remote_root}/reports"] = [
directory_entry(f"{remote_root}/reports/archive"),
]
files.directory_metadata.add(f"{remote_root}/reports/archive")
await readdir(
accessor,
PathSpec.from_str_path("/volume/reports",
mount_key("/volume/reports", "/volume")), index)
path = PathSpec.from_str_path(
"/volume/reports/archive",
mount_key("/volume/reports/archive", "/volume"))
fast = await stat(accessor, path, index)
slow = await stat(accessor, path, RAMIndexCacheStore(ttl=600))
assert fast == slow
assert files.get_directory_metadata_calls == [
f"{remote_root}/reports/archive"
]
@pytest.mark.asyncio
async def test_stat_root_does_not_call_sdk(accessor, files):
path = PathSpec.from_str_path("/volume", mount_key("/volume", "/volume"))
result = await stat(accessor, path)
assert result.name == "/"
assert result.type == FileType.DIRECTORY
assert files.get_metadata_calls == []
assert files.get_directory_metadata_calls == []
@pytest.mark.asyncio
async def test_stat_directory_uses_directory_metadata_fallback(
accessor,
files,
remote_root,
):
files.directory_metadata.add(f"{remote_root}/reports")
path = PathSpec.from_str_path("/volume/reports",
mount_key("/volume/reports", "/volume"))
result = await stat(accessor, path)
assert result.name == "reports"
assert result.size is None
assert result.type == FileType.DIRECTORY
assert files.get_metadata_calls == [f"{remote_root}/reports"]
assert files.get_directory_metadata_calls == [f"{remote_root}/reports"]
@pytest.mark.asyncio
async def test_stat_missing_path_raises(accessor):
path = PathSpec.from_str_path("/volume/missing",
mount_key("/volume/missing", "/volume"))
with pytest.raises(FileNotFoundError):
await stat(accessor, path)
@pytest.mark.asyncio
async def test_stat_missing_path_checks_directory_metadata(
accessor,
files,
remote_root,
):
path = PathSpec.from_str_path("/volume/missing",
mount_key("/volume/missing", "/volume"))
with pytest.raises(FileNotFoundError):
await stat(accessor, path)
assert files.get_metadata_calls == [f"{remote_root}/missing"]
assert files.get_directory_metadata_calls == [f"{remote_root}/missing"]
@pytest.mark.asyncio
async def test_stat_rejects_path_escape(accessor):
path = PathSpec.from_str_path("/volume/../outside",
mount_key("/volume/../outside", "/volume"))
with pytest.raises(ValueError, match="escapes Databricks volume root"):
await stat(accessor, path)
@pytest.mark.asyncio
async def test_stat_metadata_error_propagates(accessor, files, remote_root):
remote_path = f"{remote_root}/reports"
files.metadata_errors[remote_path] = RuntimeError("metadata failed")
path = PathSpec.from_str_path("/volume/reports",
mount_key("/volume/reports", "/volume"))
with pytest.raises(RuntimeError, match="metadata failed"):
await stat(accessor, path)
assert files.get_metadata_calls == [remote_path]
assert files.get_directory_metadata_calls == []
@pytest.mark.asyncio
async def test_stat_directory_metadata_error_propagates(
accessor,
files,
remote_root,
):
remote_path = f"{remote_root}/reports"
files.directory_metadata_errors[remote_path] = RuntimeError(
"directory metadata failed")
path = PathSpec.from_str_path("/volume/reports",
mount_key("/volume/reports", "/volume"))
with pytest.raises(RuntimeError, match="directory metadata failed"):
await stat(accessor, path)
assert files.get_metadata_calls == [remote_path]
assert files.get_directory_metadata_calls == [remote_path]
@pytest.mark.asyncio
async def test_stat_runs_blocking_metadata_off_event_loop(
accessor,
files,
remote_root,
monkeypatch,
):
to_thread = ToThreadRecorder()
monkeypatch.setattr(asyncio, "to_thread", to_thread)
files.metadata[f"{remote_root}/reports/latest.md"] = file_metadata(size=6)
path = PathSpec.from_str_path(
"/volume/reports/latest.md",
mount_key("/volume/reports/latest.md", "/volume"))
result = await stat(accessor, path)
assert result.name == "latest.md"
assert len(to_thread.calls) == 1
@pytest.mark.asyncio
async def test_stat_directory_fallback_runs_off_event_loop(
accessor,
files,
remote_root,
monkeypatch,
):
to_thread = ToThreadRecorder()
monkeypatch.setattr(asyncio, "to_thread", to_thread)
files.directory_metadata.add(f"{remote_root}/reports")
path = PathSpec.from_str_path("/volume/reports",
mount_key("/volume/reports", "/volume"))
result = await stat(accessor, path)
assert result.type == FileType.DIRECTORY
assert len(to_thread.calls) == 2
@@ -0,0 +1,114 @@
import pytest
from mirage.core.databricks_volume.stream import range_read, read_stream
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
class TrackingContents:
def __init__(self, data: bytes) -> None:
self.data = data
self.offset = 0
self.read_sizes: list[int] = []
self.closed = False
def read(self, size: int = -1) -> bytes:
self.read_sizes.append(size)
if size < 0:
size = len(self.data) - self.offset
chunk = self.data[self.offset:self.offset + size]
self.offset += len(chunk)
return chunk
def close(self) -> None:
self.closed = True
class TrackingDownload:
def __init__(self, contents: TrackingContents) -> None:
self.contents = contents
class TrackingFiles:
def __init__(self, contents: TrackingContents) -> None:
self.contents = contents
self.download_calls: list[str] = []
def download(self, path: str) -> TrackingDownload:
self.download_calls.append(path)
return TrackingDownload(self.contents)
@pytest.mark.asyncio
async def test_read_stream_chunks_file(accessor, files, remote_root):
files.downloads[f"{remote_root}/reports/latest.md"] = b"abcdef"
path = PathSpec.from_str_path(
"/volume/reports/latest.md",
mount_key("/volume/reports/latest.md", "/volume"))
chunks = [
chunk async for chunk in read_stream(accessor, path, chunk_size=2)
]
assert chunks == [b"ab", b"cd", b"ef"]
# Streaming should use one download body, not one Range GET per chunk.
assert files.download_calls == [f"{remote_root}/reports/latest.md"]
assert accessor.client.api_client.do_calls == []
@pytest.mark.asyncio
async def test_range_read_uses_end_exclusive(accessor, files, remote_root):
files.downloads[f"{remote_root}/reports/latest.md"] = b"abcdef"
path = PathSpec.from_str_path(
"/volume/reports/latest.md",
mount_key("/volume/reports/latest.md", "/volume"))
result = await range_read(accessor, path, 1, 4)
assert result == b"bcd"
@pytest.mark.asyncio
async def test_range_read_uses_single_databricks_range_request(
accessor,
files,
remote_root,
):
files.downloads[f"{remote_root}/reports/latest.md"] = b"abcdef"
path = PathSpec.from_str_path(
"/volume/reports/latest.md",
mount_key("/volume/reports/latest.md", "/volume"))
result = await range_read(accessor, path, 1, 4)
assert result == b"bcd"
assert files.download_calls == []
assert len(accessor.client.api_client.do_calls) == 1
assert accessor.client.api_client.do_calls[0]["headers"]["Range"] == (
"bytes=1-3")
@pytest.mark.asyncio
async def test_read_stream_reads_single_download_body_in_chunks(
accessor,
remote_root,
):
contents = TrackingContents(b"abcdef")
tracking_files = TrackingFiles(contents)
accessor.client.files = tracking_files
path = PathSpec.from_str_path(
"/volume/reports/latest.md",
mount_key("/volume/reports/latest.md", "/volume"))
stream = read_stream(accessor, path, chunk_size=2)
first = await anext(stream)
second = await anext(stream)
assert first == b"ab"
assert second == b"cd"
assert tracking_files.download_calls == [
f"{remote_root}/reports/latest.md"
]
assert contents.read_sizes == [2, 2]
assert not contents.closed
await stream.aclose()
assert contents.closed
@@ -0,0 +1,158 @@
# ========= 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.core.databricks_volume.create import create
from mirage.core.databricks_volume.unlink import unlink
from mirage.core.databricks_volume.write import write_bytes
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
def _path(path: str) -> PathSpec:
return PathSpec.from_str_path(path, mount_key(path, "/dbx"))
def _seed_directory(files, path: str) -> None:
files.directory_metadata.add(path)
files.metadata[path] = type("Metadata", (), {"is_directory": True})()
files.directories.setdefault(path, [])
def _seed_file(files, path: str, data: bytes) -> None:
parent = path.rsplit("/", 1)[0]
files.downloads[path] = data
files.metadata[path] = type(
"Metadata",
(),
{
"content_length": len(data),
},
)()
files.directories.setdefault(parent, [])
files.directories[parent].append(
type(
"Entry",
(),
{
"path": path,
"is_directory": False,
"file_size": len(data),
},
)())
@pytest.mark.asyncio
async def test_write_new_file(accessor, files, remote_root, index):
_seed_directory(files, remote_root)
await write_bytes(accessor, _path("/dbx/new.txt"), b"hello", index)
assert files.downloads[f"{remote_root}/new.txt"] == b"hello"
assert files.upload_calls == [(f"{remote_root}/new.txt", b"hello", True)]
@pytest.mark.asyncio
async def test_write_overwrites_existing_file(accessor, files, remote_root,
index):
_seed_directory(files, remote_root)
_seed_file(files, f"{remote_root}/new.txt", b"old")
await write_bytes(accessor, _path("/dbx/new.txt"), b"new", index)
assert files.downloads[f"{remote_root}/new.txt"] == b"new"
assert files.metadata[f"{remote_root}/new.txt"].content_length == 3
@pytest.mark.asyncio
async def test_write_fails_when_parent_is_missing(accessor, files, remote_root,
index):
_seed_directory(files, remote_root)
with pytest.raises(FileNotFoundError):
await write_bytes(accessor, _path("/dbx/missing/new.txt"), b"x", index)
@pytest.mark.asyncio
async def test_write_fails_when_parent_is_file(accessor, files, remote_root,
index):
_seed_directory(files, remote_root)
_seed_file(files, f"{remote_root}/parent.txt", b"file")
with pytest.raises(NotADirectoryError):
await write_bytes(accessor, _path("/dbx/parent.txt/new.txt"), b"x",
index)
@pytest.mark.asyncio
async def test_create_empty_file(accessor, files, remote_root, index):
_seed_directory(files, remote_root)
await create(accessor, _path("/dbx/empty.txt"), index)
assert files.downloads[f"{remote_root}/empty.txt"] == b""
@pytest.mark.asyncio
async def test_create_fails_when_parent_is_missing(accessor, files,
remote_root, index):
_seed_directory(files, remote_root)
with pytest.raises(FileNotFoundError):
await create(accessor, _path("/dbx/missing/empty.txt"), index)
@pytest.mark.asyncio
async def test_unlink_file(accessor, files, remote_root, index):
_seed_directory(files, remote_root)
_seed_file(files, f"{remote_root}/delete.txt", b"bye")
await unlink(accessor, _path("/dbx/delete.txt"), index)
assert f"{remote_root}/delete.txt" not in files.downloads
assert files.delete_calls == [f"{remote_root}/delete.txt"]
@pytest.mark.asyncio
async def test_unlink_missing_file_raises_file_not_found(
accessor, files, remote_root, index):
_seed_directory(files, remote_root)
with pytest.raises(FileNotFoundError):
await unlink(accessor, _path("/dbx/missing.txt"), index)
@pytest.mark.asyncio
async def test_unlink_directory_raises_is_a_directory(accessor, files,
remote_root, index):
_seed_directory(files, remote_root)
_seed_directory(files, f"{remote_root}/dir")
with pytest.raises(IsADirectoryError):
await unlink(accessor, _path("/dbx/dir"), index)
@pytest.mark.asyncio
async def test_file_mutation_paths_cannot_escape_root(accessor, files,
remote_root, index):
_seed_directory(files, remote_root)
escaping = _path("/dbx/../escape.txt")
with pytest.raises(ValueError):
await write_bytes(accessor, escaping, b"x", index)
with pytest.raises(ValueError):
await create(accessor, escaping, index)
with pytest.raises(ValueError):
await unlink(accessor, escaping, index)
+97
View File
@@ -0,0 +1,97 @@
from types import SimpleNamespace
import pytest
from mirage.cache.index import RAMIndexCacheStore
from mirage.cache.index.config import IndexEntry
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
def document(
document_id: str,
name: str,
*,
slug: object | None = None,
enabled: bool = True,
indexing_status: str = "completed",
archived: bool = False,
size: int | None = 123,
) -> dict:
doc_metadata = []
if slug is not None:
doc_metadata = [{"name": "slug", "value": slug}]
data_source_detail_dict = {}
if size is not None:
data_source_detail_dict = {"upload_file": {"size": size}}
return {
"id": document_id,
"name": name,
"doc_metadata": doc_metadata,
"enabled": enabled,
"indexing_status": indexing_status,
"archived": archived,
"tokens": 9,
"data_source_type": "upload_file",
"data_source_detail_dict": data_source_detail_dict,
"created_at": 1716282000,
}
def file_entry(document_id: str, name: str, size: int = 123) -> IndexEntry:
return IndexEntry(id=document_id,
name=name,
resource_type="file",
size=size)
def folder_entry(name: str) -> IndexEntry:
return IndexEntry(id=name, name=name, resource_type="folder")
async def no_documents(config):
return []
async def list_basic_documents(config):
return [
document("doc-1", "Quickstart", slug="guides/quickstart", size=333),
document("doc-2", "README.md", size=None),
]
async def list_nested_documents(config):
return [
document("doc-1", "Quickstart", slug="guides/quickstart", size=333),
document("doc-2", "Notes", slug="guides/deep/note", size=20),
document("doc-3", "README.md", size=10),
]
async def noop_ensure_tree(accessor, index, prefix=""):
return None
@pytest.fixture
def dify_accessor() -> SimpleNamespace:
return SimpleNamespace(config=SimpleNamespace(dataset_id="dataset-1",
slug_metadata_name="slug"))
@pytest.fixture
def dify_index() -> RAMIndexCacheStore:
return RAMIndexCacheStore()
@pytest.fixture
def knowledge_root() -> PathSpec:
return PathSpec(resource_path=mount_key("/knowledge", "/knowledge"),
virtual="/knowledge",
directory="/knowledge")
@pytest.fixture
def guide_path() -> PathSpec:
return PathSpec.from_str_path(
"/knowledge/guides/quickstart",
mount_key("/knowledge/guides/quickstart", "/knowledge"))
+147
View File
@@ -0,0 +1,147 @@
import httpx
import pytest
from mirage.core.dify import _client
from mirage.resource.dify.config import DifyConfig
def config() -> DifyConfig:
return DifyConfig(
api_key="secret",
base_url="https://dify.example/v1/",
dataset_id="dataset-1",
)
@pytest.mark.asyncio
async def test_list_all_documents_paginates_and_filters(httpx_mock):
httpx_mock.add_response(
json={
"data": [
{
"id": "doc-1",
"enabled": True,
"indexing_status": "completed",
"archived": False,
"data_source_detail_dict": {
"upload_file": {
"size": 10
}
},
},
{
"id": "doc-2",
"enabled": False,
"indexing_status": "completed",
"archived": False,
},
],
"has_more":
True,
})
httpx_mock.add_response(
json={
"data": [
{
"id": "doc-3",
"enabled": True,
"indexing_status": "completed",
"archived": False,
},
{
"id": "doc-4",
"enabled": True,
"indexing_status": "indexing",
"archived": False,
},
],
"has_more":
False,
})
documents = await _client.list_all_documents(config())
assert [item["id"] for item in documents] == ["doc-1", "doc-3"]
assert documents[0]["data_source_detail_dict"]["upload_file"]["size"] == 10
requests = httpx_mock.get_requests()
assert len(requests) == 2
assert requests[0].headers["authorization"] == "Bearer secret"
assert requests[0].url.params["page"] == "1"
assert requests[0].url.params["limit"] == "100"
assert requests[1].url.params["page"] == "2"
@pytest.mark.asyncio
async def test_dify_get_retries_rate_limit(monkeypatch, httpx_mock):
sleeps: list[int] = []
async def sleep(delay: int) -> None:
sleeps.append(delay)
monkeypatch.setattr(_client.asyncio, "sleep", sleep)
httpx_mock.add_response(status_code=429, json={"message": "rate limit"})
httpx_mock.add_response(json={"ok": True})
payload = await _client.dify_get(config(), "/datasets/dataset-1/documents")
assert payload == {"ok": True}
assert sleeps == [1]
@pytest.mark.asyncio
async def test_dify_get_raises_http_status_errors(httpx_mock):
httpx_mock.add_response(status_code=401, json={"message": "unauthorized"})
with pytest.raises(httpx.HTTPStatusError) as exc_info:
await _client.dify_get(config(), "/datasets/dataset-1/documents")
assert exc_info.value.response.status_code == 401
@pytest.mark.asyncio
async def test_dify_post_sends_json_and_retries_server_error(
monkeypatch, httpx_mock):
sleeps: list[int] = []
async def sleep(delay: int) -> None:
sleeps.append(delay)
monkeypatch.setattr(_client.asyncio, "sleep", sleep)
httpx_mock.add_response(status_code=500, json={"message": "temporary"})
httpx_mock.add_response(json={"ok": True})
payload = await _client.dify_post(config(), "/datasets/dataset-1/retrieve",
{"query": "hello"})
assert payload == {"ok": True}
assert sleeps == [1]
requests = httpx_mock.get_requests()
assert len(requests) == 2
assert requests[0].headers["authorization"] == "Bearer secret"
assert requests[0].read() == b'{"query":"hello"}'
@pytest.mark.asyncio
async def test_get_document_segments_paginates_with_server_filters(httpx_mock):
httpx_mock.add_response(json={
"data": [{
"content": "first"
}],
"has_more": True,
})
httpx_mock.add_response(json={
"data": [{
"content": "second"
}],
"has_more": False,
})
segments = await _client.get_document_segments(config(), "doc-1")
assert [item["content"] for item in segments] == ["first", "second"]
requests = httpx_mock.get_requests()
assert requests[0].url.params["status"] == "completed"
assert requests[0].url.params["enabled"] == "true"
assert requests[0].url.params["page"] == "1"
assert requests[1].url.params["page"] == "2"
+72
View File
@@ -0,0 +1,72 @@
import pytest
from mirage.core.dify import find, stat, tree
from mirage.types import FindType
from .conftest import list_nested_documents
async def get_detail(config, document_id):
sizes = {
"doc-1": 333,
"doc-2": 20,
"doc-3": 10,
}
return {
"id": document_id,
"updated_at": 1716285600,
"data_source_info": {
"upload_file": {
"size": sizes[document_id]
}
},
}
@pytest.mark.asyncio
async def test_find_name_matches_mount_root_start_path(monkeypatch,
dify_accessor,
dify_index,
knowledge_root):
monkeypatch.setattr(tree, "list_all_documents", list_nested_documents)
monkeypatch.setattr(stat, "get_document_detail", get_detail)
results = await find.find(dify_accessor,
knowledge_root,
name="knowledge",
index=dify_index)
assert results == ["/"]
@pytest.mark.asyncio
async def test_find_filters_name_type_size_and_depth(monkeypatch,
dify_accessor, dify_index,
knowledge_root):
monkeypatch.setattr(tree, "list_all_documents", list_nested_documents)
monkeypatch.setattr(stat, "get_document_detail", get_detail)
named = await find.find(dify_accessor,
knowledge_root,
name="quick*",
index=dify_index)
assert named == ["/guides/quickstart"]
directories = await find.find(dify_accessor,
knowledge_root,
type=FindType.DIRECTORY,
index=dify_index)
assert directories == ["/", "/guides", "/guides/deep"]
large_files = await find.find(dify_accessor,
knowledge_root,
type=FindType.FILE,
min_size=100,
index=dify_index)
assert large_files == ["/guides/quickstart"]
deep = await find.find(dify_accessor,
knowledge_root,
mindepth=2,
index=dify_index)
assert "/" not in deep
assert "/guides" not in deep
assert "/guides/deep" in deep
+40
View File
@@ -0,0 +1,40 @@
import pytest
from mirage.core.dify import glob, tree
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
from .conftest import list_nested_documents
@pytest.mark.asyncio
async def test_resolve_glob_keeps_unresolved_non_pattern_path(
monkeypatch, dify_accessor, dify_index):
monkeypatch.setattr(tree, "list_all_documents", list_nested_documents)
path = PathSpec(resource_path=mount_key("/knowledge/guides", "/knowledge"),
virtual="/knowledge/guides",
directory="/knowledge/guides",
resolved=False)
matches = await glob.resolve_glob(dify_accessor, [path], dify_index)
assert matches == [path]
@pytest.mark.asyncio
async def test_resolve_glob_matches_directory_pattern(monkeypatch,
dify_accessor,
dify_index):
monkeypatch.setattr(tree, "list_all_documents", list_nested_documents)
path = PathSpec(resource_path=mount_key("/knowledge/guides/quick*",
"/knowledge"),
virtual="/knowledge/guides/quick*",
directory="/knowledge/guides",
pattern="quick*",
resolved=False)
matches = await glob.resolve_glob(dify_accessor, [path], dify_index)
assert [item.virtual
for item in matches] == ["/knowledge/guides/quickstart"]
assert matches[0].directory == "/knowledge/guides/"
+25
View File
@@ -0,0 +1,25 @@
import pytest
from mirage.core.dify import grep, read, tree
from .conftest import list_basic_documents
async def iter_pages(config, document_id):
yield [{"content": "Alpha"}]
yield [{"content": "beta"}]
@pytest.mark.asyncio
async def test_grep_bytes_matches_streamed_lines_and_records_reads(
monkeypatch, dify_accessor, dify_index, guide_path):
monkeypatch.setattr(tree, "list_all_documents", list_basic_documents)
monkeypatch.setattr(read, "iter_segment_pages", iter_pages)
output, reads = await grep.grep_bytes(dify_accessor, [guide_path],
"alpha",
dify_index,
ignore_case=True)
assert output == b"/knowledge/guides/quickstart:1:Alpha"
assert reads == {guide_path.virtual: b"Alpha\nbeta"}
+74
View File
@@ -0,0 +1,74 @@
import pytest
from mirage.cache.index.config import IndexEntry
from mirage.core.dify import path
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
from .conftest import folder_entry, noop_ensure_tree
@pytest.mark.asyncio
async def test_resolve_path_finds_files_and_directories(
monkeypatch, dify_accessor, dify_index):
monkeypatch.setattr(path, "ensure_tree", noop_ensure_tree)
await dify_index.set_dir(
"/knowledge",
[
("guides", folder_entry("guides")),
("README.md",
IndexEntry(id="doc-1", name="README.md", resource_type="file")),
],
)
await dify_index.set_dir("/knowledge/guides", [])
file_result = await path.resolve_path(
dify_accessor,
PathSpec.from_str_path("/knowledge/README.md",
mount_key("/knowledge/README.md",
"/knowledge")),
dify_index,
)
dir_result = await path.resolve_path(
dify_accessor,
PathSpec.from_str_path("/knowledge/guides",
mount_key("/knowledge/guides", "/knowledge")),
dify_index,
)
assert file_result.virtual_key == "/knowledge/README.md"
assert file_result.is_dir is False
assert file_result.entry is not None
assert dir_result.virtual_key == "/knowledge/guides"
assert dir_result.is_dir is True
assert dir_result.entry is not None
assert dir_result.entry.resource_type == "folder"
@pytest.mark.asyncio
async def test_resolve_path_raises_missing(monkeypatch, dify_accessor,
dify_index):
monkeypatch.setattr(path, "ensure_tree", noop_ensure_tree)
await dify_index.set_dir("/knowledge", [])
with pytest.raises(FileNotFoundError):
await path.resolve_path(
dify_accessor,
PathSpec.from_str_path(
"/knowledge/missing",
mount_key("/knowledge/missing", "/knowledge")),
dify_index,
)
def test_virtual_key_for_honors_prefix_and_patterns():
assert path.virtual_key_for(
PathSpec.from_str_path(
"/knowledge/guides/quickstart",
"guides/quickstart")) == "/knowledge/guides/quickstart"
assert path.virtual_key_for(
PathSpec(resource_path=mount_key("/knowledge/guides/*.md",
"/knowledge"),
virtual="/knowledge/guides/*.md",
directory="/knowledge/guides",
pattern="*.md")) == "/knowledge/guides"
+48
View File
@@ -0,0 +1,48 @@
import pytest
from mirage.core.dify import read, tree
from .conftest import list_basic_documents
async def get_segments(config, document_id):
return [{"content": "first"}, {"content": "second"}]
async def iter_pages(config, document_id):
yield [{"content": "first"}]
yield [{"content": "second"}]
@pytest.mark.asyncio
async def test_read_bytes_uses_document_segments(monkeypatch, dify_accessor,
dify_index, guide_path):
monkeypatch.setattr(tree, "list_all_documents", list_basic_documents)
monkeypatch.setattr(read, "get_document_segments", get_segments)
data = await read.read_bytes(dify_accessor, guide_path, dify_index)
assert data == b"first\nsecond"
@pytest.mark.asyncio
async def test_read_stream_separates_pages_with_single_newline(
monkeypatch, dify_accessor, dify_index, guide_path):
monkeypatch.setattr(tree, "list_all_documents", list_basic_documents)
monkeypatch.setattr(read, "iter_segment_pages", iter_pages)
chunks = [
chunk async for chunk in read.read_stream(dify_accessor, guide_path,
dify_index)
]
assert chunks == [b"first", b"\n", b"second"]
@pytest.mark.asyncio
async def test_read_bytes_rejects_directories(monkeypatch, dify_accessor,
dify_index, knowledge_root):
monkeypatch.setattr(tree, "list_all_documents", list_basic_documents)
with pytest.raises(IsADirectoryError):
await read.read_bytes(dify_accessor, knowledge_root, dify_index)
+24
View File
@@ -0,0 +1,24 @@
import pytest
from mirage.core.dify import readdir, tree
from .conftest import list_basic_documents
@pytest.mark.asyncio
async def test_readdir_returns_directory_children(monkeypatch, dify_accessor,
dify_index, knowledge_root):
monkeypatch.setattr(tree, "list_all_documents", list_basic_documents)
children = await readdir.readdir(dify_accessor, knowledge_root, dify_index)
assert children == ["/knowledge/README.md", "/knowledge/guides"]
@pytest.mark.asyncio
async def test_readdir_rejects_files(monkeypatch, dify_accessor, dify_index,
guide_path):
monkeypatch.setattr(tree, "list_all_documents", list_basic_documents)
with pytest.raises(NotADirectoryError):
await readdir.readdir(dify_accessor, guide_path, dify_index)
+428
View File
@@ -0,0 +1,428 @@
from types import SimpleNamespace
import pytest
from mirage.cache.index import RAMIndexCacheStore
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
def document(document_id: str, name: str, slug: str | None = None) -> dict:
metadata = []
if slug is not None:
metadata = [{"name": "slug", "value": slug}]
return {
"id": document_id,
"name": name,
"doc_metadata": metadata,
"enabled": True,
"indexing_status": "completed",
"archived": False,
"tokens": 4,
"data_source_type": "upload_file",
"data_source_detail_dict": {
"upload_file": {
"size": 12
}
},
"created_at": 1716282000,
}
def accessor() -> SimpleNamespace:
return SimpleNamespace(config=SimpleNamespace(dataset_id="dataset-1",
slug_metadata_name="slug"))
def custom_slug_accessor() -> SimpleNamespace:
return SimpleNamespace(config=SimpleNamespace(dataset_id="dataset-1",
slug_metadata_name="path"))
custom_slug_search_bodies: list[dict] = []
async def list_path_metadata_documents(config):
return [{
**document("doc-1", "API"), "doc_metadata": [{
"name": "path",
"value": "guides/api"
}]
}]
async def record_empty_custom_slug_search(config, endpoint, body):
custom_slug_search_bodies.append(body)
return {"records": []}
@pytest.mark.asyncio
async def test_search_segments_scopes_folder_to_all_documents(monkeypatch):
from mirage.core.dify import search, tree
bodies: list[dict] = []
async def list_documents(config):
return [
document("doc-1", "API", "guides/api"),
document("doc-2", "Auth", "guides/auth"),
document("doc-3", "README.md"),
]
async def dify_post(config, endpoint, body):
bodies.append(body)
return {
"records": [{
"segment": {
"content": "api segment",
"document": {
"name": "API",
"doc_metadata": [{
"name": "slug",
"value": "guides/api"
}],
},
},
"score": 0.92,
}, {
"segment": {
"content": "auth segment",
"document": {
"name": "Auth",
"doc_metadata": [{
"name": "slug",
"value": "guides/auth"
}],
},
},
"score": 0.81,
}]
}
monkeypatch.setattr(tree, "list_all_documents", list_documents)
monkeypatch.setattr(search, "dify_post", dify_post)
result = await search.search_segments(
accessor(),
"login docs",
[
PathSpec(resource_path=mount_key("/knowledge/guides",
"/knowledge"),
virtual="/knowledge/guides",
directory="/knowledge/guides")
],
RAMIndexCacheStore(),
method="hybrid",
top_k=2,
threshold=0.4,
)
assert result == (b"/knowledge/guides/api:0.92\napi segment\n"
b"/knowledge/guides/auth:0.81\nauth segment\n")
retrieval = bodies[0]["retrieval_model"]
assert retrieval["search_method"] == "hybrid_search"
assert retrieval["top_k"] == 2
assert retrieval["score_threshold_enabled"] is True
assert retrieval["score_threshold"] == 0.4
assert retrieval["metadata_filtering_conditions"] == {
"logical_operator":
"or",
"conditions": [{
"name": "slug",
"comparison_operator": "in",
"value": ["guides/api", "guides/auth"],
}],
}
@pytest.mark.asyncio
async def test_search_segments_unions_slug_and_name_based_paths(monkeypatch):
from mirage.core.dify import search, tree
bodies: list[dict] = []
async def list_documents(config):
return [
document("doc-1", "API", "guides/api"),
document("doc-2", "README.md"),
]
async def dify_post(config, endpoint, body):
bodies.append(body)
return {"records": []}
monkeypatch.setattr(tree, "list_all_documents", list_documents)
monkeypatch.setattr(search, "dify_post", dify_post)
result = await search.search_segments(
accessor(),
"setup",
[
PathSpec(resource_path=mount_key("/knowledge/guides/api",
"/knowledge"),
virtual="/knowledge/guides/api",
directory="/knowledge/guides/api"),
PathSpec(resource_path=mount_key("/knowledge/README.md",
"/knowledge"),
virtual="/knowledge/README.md",
directory="/knowledge/README.md"),
],
RAMIndexCacheStore(),
)
assert result == b""
assert bodies[0]["retrieval_model"]["metadata_filtering_conditions"] == {
"logical_operator":
"or",
"conditions": [{
"name": "slug",
"comparison_operator": "in",
"value": ["guides/api"],
}, {
"name": "document_name",
"comparison_operator": "in",
"value": ["README.md"],
}],
}
@pytest.mark.asyncio
async def test_search_segments_uses_configured_slug_metadata_name(monkeypatch):
from mirage.core.dify import search, tree
custom_slug_search_bodies.clear()
monkeypatch.setattr(tree, "list_all_documents",
list_path_metadata_documents)
monkeypatch.setattr(search, "dify_post", record_empty_custom_slug_search)
await search.search_segments(
custom_slug_accessor(),
"setup",
[
PathSpec(resource_path=mount_key("/knowledge/guides/api",
"/knowledge"),
virtual="/knowledge/guides/api",
directory="/knowledge/guides/api")
],
RAMIndexCacheStore(),
)
assert custom_slug_search_bodies[0]["retrieval_model"][
"metadata_filtering_conditions"] == {
"logical_operator":
"or",
"conditions": [{
"name": "path",
"comparison_operator": "in",
"value": ["guides/api"],
}],
}
@pytest.mark.asyncio
async def test_search_segments_empty_paths_searches_whole_dataset(monkeypatch):
from mirage.core.dify import search
bodies: list[dict] = []
async def dify_post(config, endpoint, body):
bodies.append(body)
return {
"records": [{
"segment": {
"content": "dataset match",
"document": {
"name": "README.md",
"doc_metadata": None,
},
},
"score": 0.77,
}]
}
monkeypatch.setattr(search, "dify_post", dify_post)
result = await search.search_segments(accessor(),
"anything", [],
RAMIndexCacheStore(),
mount_prefix="/knowledge/")
assert result == b"/knowledge/README.md:0.77\ndataset match\n"
assert "metadata_filtering_conditions" not in bodies[0]["retrieval_model"]
def test_records_to_bytes_formats_absolute_paths_and_scores():
from mirage.core.dify.search import records_to_bytes
result = records_to_bytes(
[{
"segment": {
"content": "api segment",
"document": {
"name": "API",
"doc_metadata": [{
"name": "slug",
"value": "guides/api"
}],
},
},
"score": 0.92,
}, {
"segment": {
"content": "auth segment",
"document": {
"name": "README.md",
"doc_metadata": None,
},
},
}],
"slug",
"/knowledge/",
)
assert result == (b"/knowledge/guides/api:0.92\napi segment\n"
b"/knowledge/README.md\nauth segment\n")
def test_records_to_bytes_keeps_multiple_chunks_for_same_document():
from mirage.core.dify.search import records_to_bytes
result = records_to_bytes(
[{
"segment": {
"content": "first chunk",
"document": {
"name":
"refunds.md",
"doc_metadata": [{
"name": "slug",
"value": "policies/refunds"
}],
},
},
"score": 0.82,
}, {
"segment": {
"content": "second chunk",
"document": {
"name":
"refunds.md",
"doc_metadata": [{
"name": "slug",
"value": "policies/refunds"
}],
},
},
"score": 0.79,
}],
"slug",
"/knowledge/",
)
assert result == (b"/knowledge/policies/refunds:0.82\nfirst chunk\n"
b"/knowledge/policies/refunds:0.79\nsecond chunk\n")
def test_records_to_bytes_skips_records_with_invalid_slug():
from mirage.core.dify.search import records_to_bytes
result = records_to_bytes(
[{
"segment": {
"content": "bad chunk",
"document": {
"name": "ok.md",
"doc_metadata": [{
"name": "slug",
"value": ""
}],
},
},
"score": 0.82,
}, {
"segment": {
"content": "good chunk",
"document": {
"name":
"refunds.md",
"doc_metadata": [{
"name": "slug",
"value": "policies/refunds"
}],
},
},
"score": 0.79,
}],
"slug",
"/knowledge/",
)
assert result == b"/knowledge/policies/refunds:0.79\ngood chunk\n"
def test_records_to_bytes_omits_score_for_non_numeric_values():
from mirage.core.dify.search import records_to_bytes
result = records_to_bytes(
[{
"segment": {
"content": "chunk",
"document": {
"name":
"refunds.md",
"doc_metadata": [{
"name": "slug",
"value": "policies/refunds"
}],
},
},
"score": True,
}],
"slug",
"/knowledge/",
)
assert result == b"/knowledge/policies/refunds\nchunk\n"
@pytest.mark.asyncio
async def test_search_segments_caps_top_k_at_dify_limit(monkeypatch):
from mirage.core.dify import search
bodies: list[dict] = []
async def dify_post(config, endpoint, body):
bodies.append(body)
return {"records": []}
monkeypatch.setattr(search, "dify_post", dify_post)
await search.search_segments(accessor(),
"anything", [],
RAMIndexCacheStore(),
top_k=150)
assert bodies[0]["retrieval_model"]["top_k"] == 100
@pytest.mark.asyncio
async def test_search_segments_validates_arguments():
from mirage.core.dify.search import search_segments
with pytest.raises(ValueError, match="search: query is required"):
await search_segments(accessor(), "", [], RAMIndexCacheStore())
with pytest.raises(ValueError, match="search: top-k must be positive"):
await search_segments(accessor(),
"x", [],
RAMIndexCacheStore(),
top_k=0)
with pytest.raises(ValueError, match="search: threshold must be in"):
await search_segments(accessor(),
"x", [],
RAMIndexCacheStore(),
threshold=1.2)
with pytest.raises(ValueError, match="search: method must be one of"):
await search_segments(accessor(),
"x", [],
RAMIndexCacheStore(),
method="bad")
+95
View File
@@ -0,0 +1,95 @@
from types import SimpleNamespace
import pytest
from mirage.cache.index import RAMIndexCacheStore
from mirage.types import FileType, PathSpec
from mirage.utils.key_prefix import mount_key
def document(
document_id: str,
name: str,
*,
slug: str,
size: int = 123,
) -> dict:
return {
"id": document_id,
"name": name,
"doc_metadata": [{
"name": "slug",
"value": slug
}],
"enabled": True,
"indexing_status": "completed",
"archived": False,
"tokens": 9,
"data_source_type": "upload_file",
"data_source_detail_dict": {
"upload_file": {
"size": size
}
},
"created_at": 1716282000,
}
def accessor() -> SimpleNamespace:
return SimpleNamespace(config=SimpleNamespace(slug_metadata_name="slug"))
@pytest.mark.asyncio
async def test_stat_light_uses_index_entry_without_detail_call(monkeypatch):
from mirage.core.dify import stat, tree
async def list_documents(config):
return [document("doc-1", "Quickstart", slug="guides/quickstart")]
async def get_detail(config, document_id):
raise AssertionError("stat_light should not fetch document detail")
monkeypatch.setattr(tree, "list_all_documents", list_documents)
monkeypatch.setattr(stat, "get_document_detail", get_detail)
index = RAMIndexCacheStore()
path = PathSpec(
resource_path=mount_key("/knowledge/guides/quickstart", "/knowledge"),
virtual="/knowledge/guides/quickstart",
directory="/knowledge/guides/quickstart",
)
item = await stat.stat_light(accessor(), path, index)
assert item.name == "quickstart"
assert item.type == FileType.TEXT
assert item.size == 123
assert item.modified == "2024-05-21T09:00:00+00:00"
assert item.extra["slug"] == "guides/quickstart"
@pytest.mark.asyncio
async def test_stat_light_returns_directory_without_detail_call(monkeypatch):
from mirage.core.dify import stat, tree
async def list_documents(config):
return [document("doc-1", "Quickstart", slug="guides/quickstart")]
async def get_detail(config, document_id):
raise AssertionError("stat_light should not fetch document detail")
monkeypatch.setattr(tree, "list_all_documents", list_documents)
monkeypatch.setattr(stat, "get_document_detail", get_detail)
index = RAMIndexCacheStore()
path = PathSpec(
resource_path=mount_key("/knowledge/guides", "/knowledge"),
virtual="/knowledge/guides",
directory="/knowledge/guides",
)
item = await stat.stat_light(accessor(), path, index)
assert item.name == "guides"
assert item.type == FileType.DIRECTORY
assert item.extra == {"children_count": 0}
+125
View File
@@ -0,0 +1,125 @@
import pytest
from mirage.core.dify import tree
from mirage.core.dify._client import is_visible_document
from .conftest import document
tree_calls = {"documents": 0}
async def list_documents_with_hidden_and_fallbacks(config):
# Mirrors the list_all_documents contract: hidden documents (archived,
# disabled, still indexing) never leave the client.
documents = [
document("doc-1", "Quickstart", slug="guides/quickstart", size=333),
document("doc-2", "API", slug="api", size=444, archived=True),
document("doc-3", "Draft", slug="draft", indexing_status="indexing"),
document("doc-4", "Disabled", slug="disabled", enabled=False),
document("doc-5", "Archived", slug="archived", archived=True),
document("doc-6", "README.md", size=None),
]
return [doc for doc in documents if is_visible_document(doc)]
async def counted_documents(config):
tree_calls["documents"] += 1
return await list_documents_with_hidden_and_fallbacks(config)
async def duplicate_documents(config):
return [
document("doc-1", "one", slug="same"),
document("doc-2", "two", slug="same"),
]
async def collision_documents(config):
return [
document("doc-1", "foo", slug="foo"),
document("doc-2", "bar", slug="foo/bar"),
]
@pytest.mark.asyncio
async def test_ensure_tree_builds_prefixed_entries_and_uses_api_size(
monkeypatch, dify_accessor, dify_index):
tree_calls["documents"] = 0
monkeypatch.setattr(tree, "list_all_documents", counted_documents)
await tree.ensure_tree(dify_accessor, dify_index, "/knowledge/")
root = await dify_index.list_dir("/knowledge")
guides = await dify_index.list_dir("/knowledge/guides")
quickstart = await dify_index.get("/knowledge/guides/quickstart")
readme = await dify_index.get("/knowledge/README.md")
assert root.entries == ["/knowledge/README.md", "/knowledge/guides"]
assert guides.entries == ["/knowledge/guides/quickstart"]
assert quickstart.entry.id == "doc-1"
assert quickstart.entry.size == 333
assert quickstart.entry.extra["slug"] == "guides/quickstart"
assert quickstart.entry.extra["raw_slug"] == "guides/quickstart"
assert quickstart.entry.extra["has_slug"] is True
assert readme.entry.id == "doc-6"
assert readme.entry.size is None
assert readme.entry.extra["raw_slug"] == "README.md"
assert readme.entry.extra["has_slug"] is False
await tree.ensure_tree(dify_accessor, dify_index, "/knowledge/")
assert tree_calls["documents"] == 1
@pytest.mark.asyncio
async def test_ensure_tree_rejects_duplicate_and_path_collision(
monkeypatch, dify_accessor, dify_index):
monkeypatch.setattr(tree, "list_all_documents", duplicate_documents)
with pytest.raises(ValueError, match="Duplicate slug 'same'"):
await tree.ensure_tree(dify_accessor, dify_index, "")
await dify_index.clear()
monkeypatch.setattr(tree, "list_all_documents", collision_documents)
with pytest.raises(ValueError, match="Path collision"):
await tree.ensure_tree(dify_accessor, dify_index, "")
def test_tree_slug_and_timestamp_helpers():
assert tree.extract_slug({
"doc_metadata": {
"slug": "a/b"
},
"name": "fallback"
}) == ("a/b", True)
assert tree.extract_slug(
{
"doc_metadata": [{
"name": "path",
"value": "docs/start"
}],
"name": "fallback"
}, "path") == ("docs/start", True)
assert tree.extract_slug(
{
"doc_metadata": {
"path": "docs/map"
},
"name": "fallback"
}, "path") == ("docs/map", True)
assert tree.normalize_slug("/a//b/") == "/a/b"
assert tree.extract_document_size(
{"data_source_info": {
"upload_file": {
"size": 7
}
}}) == 7
assert tree.timestamp_to_iso(None) == ""
assert tree.virtual_path("/a", "/knowledge/") == "/knowledge/a"
assert tree.parent("/a/b") == "/a"
assert tree.gnu_basename("/a/b") == "b"
def test_normalize_slug_rejects_invalid_segments():
with pytest.raises(ValueError, match="Invalid empty"):
tree.normalize_slug("/")
with pytest.raises(ValueError, match="Invalid Dify document slug segment"):
tree.normalize_slug("../x")
+44
View File
@@ -0,0 +1,44 @@
import pytest
from mirage.core.dify import tree, walk
from .conftest import list_nested_documents, no_documents
@pytest.mark.asyncio
async def test_walk_returns_recursive_paths_with_depth_controls(
monkeypatch, dify_accessor, dify_index, knowledge_root):
monkeypatch.setattr(tree, "list_all_documents", list_nested_documents)
full = await walk.walk(dify_accessor,
knowledge_root,
dify_index,
include_root=True,
strip_prefix=True)
shallow = await walk.walk(dify_accessor,
knowledge_root,
dify_index,
include_root=True,
maxdepth=1,
strip_prefix=True)
assert full == [
"/",
"/README.md",
"/guides",
"/guides/deep",
"/guides/deep/note",
"/guides/quickstart",
]
assert shallow == ["/", "/README.md", "/guides"]
@pytest.mark.asyncio
async def test_walk_can_ignore_missing_paths(dify_accessor, dify_index,
guide_path, monkeypatch):
monkeypatch.setattr(tree, "list_all_documents", no_documents)
assert await walk.walk(dify_accessor,
guide_path,
dify_index,
ignore_missing=True) == []
+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. =========
@@ -0,0 +1,87 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from unittest.mock import AsyncMock, patch
import pytest
from mirage.core.discord.channels import list_channels
from mirage.resource.discord.config import DiscordConfig
@pytest.fixture
def config():
return DiscordConfig(token="test-bot-token")
@pytest.mark.asyncio
async def test_list_channels(config):
mock_data = [
{
"id": "C001",
"name": "general",
"type": 0
},
{
"id": "C002",
"name": "announcements",
"type": 5
},
{
"id": "C003",
"name": "forum",
"type": 15
},
{
"id": "C004",
"name": "voice-chat",
"type": 2
},
]
with patch(
"mirage.core.discord.channels.discord_get",
new_callable=AsyncMock,
return_value=mock_data,
) as mock_get:
result = await list_channels(config, "G001")
assert len(result) == 3
assert result[0]["name"] == "general"
assert result[1]["name"] == "announcements"
assert result[2]["name"] == "forum"
mock_get.assert_called_once_with(config, "/guilds/G001/channels")
@pytest.mark.asyncio
async def test_list_channels_filters_voice(config):
mock_data = [
{
"id": "C001",
"name": "voice-chat",
"type": 2
},
{
"id": "C002",
"name": "stage",
"type": 13
},
]
with patch(
"mirage.core.discord.channels.discord_get",
new_callable=AsyncMock,
return_value=mock_data,
):
result = await list_channels(config, "G001")
assert len(result) == 0
+108
View File
@@ -0,0 +1,108 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from mirage.core.discord._client import discord_get, discord_post
from mirage.resource.discord.config import DiscordConfig
@pytest.fixture
def config():
return DiscordConfig(token="test-bot-token")
@pytest.mark.asyncio
async def test_discord_get_success(config):
mock_resp = AsyncMock()
mock_resp.status = 200
mock_resp.json = AsyncMock(return_value=[
{
"id": "123",
"name": "general"
},
])
mock_resp.raise_for_status = MagicMock()
mock_session = AsyncMock()
mock_session.get = MagicMock(return_value=AsyncMock(
__aenter__=AsyncMock(return_value=mock_resp),
__aexit__=AsyncMock(return_value=False),
))
with patch("mirage.core.discord._client.aiohttp.ClientSession") as mock_cs:
mock_cs.return_value.__aenter__ = AsyncMock(return_value=mock_session)
mock_cs.return_value.__aexit__ = AsyncMock(return_value=False)
result = await discord_get(config, "/guilds/123/channels")
assert result == [{"id": "123", "name": "general"}]
mock_session.get.assert_called_once()
call_kwargs = mock_session.get.call_args
assert call_kwargs.args[0] == \
"https://discord.com/api/v10/guilds/123/channels"
assert call_kwargs.kwargs["headers"]["Authorization"] == \
"Bot test-bot-token"
@pytest.mark.asyncio
async def test_discord_get_rate_limited(config):
mock_resp = AsyncMock()
mock_resp.status = 429
mock_resp.json = AsyncMock(return_value={
"retry_after": 5,
})
mock_session = AsyncMock()
mock_session.get = MagicMock(return_value=AsyncMock(
__aenter__=AsyncMock(return_value=mock_resp),
__aexit__=AsyncMock(return_value=False),
))
with patch("mirage.core.discord._client.aiohttp.ClientSession") as mock_cs:
mock_cs.return_value.__aenter__ = AsyncMock(return_value=mock_session)
mock_cs.return_value.__aexit__ = AsyncMock(return_value=False)
with pytest.raises(RuntimeError, match="Rate limited"):
await discord_get(config, "/users/@me/guilds")
@pytest.mark.asyncio
async def test_discord_post_success(config):
mock_resp = AsyncMock()
mock_resp.status = 200
mock_resp.json = AsyncMock(return_value={
"id": "msg1",
"content": "hello",
})
mock_resp.raise_for_status = MagicMock()
mock_session = AsyncMock()
mock_session.post = MagicMock(return_value=AsyncMock(
__aenter__=AsyncMock(return_value=mock_resp),
__aexit__=AsyncMock(return_value=False),
))
with patch("mirage.core.discord._client.aiohttp.ClientSession") as mock_cs:
mock_cs.return_value.__aenter__ = AsyncMock(return_value=mock_session)
mock_cs.return_value.__aexit__ = AsyncMock(return_value=False)
result = await discord_post(config,
"/channels/C001/messages",
body={"content": "hello"})
assert result["id"] == "msg1"
mock_session.post.assert_called_once()
call_kwargs = mock_session.post.call_args
assert call_kwargs.args[0] == \
"https://discord.com/api/v10/channels/C001/messages"
+102
View File
@@ -0,0 +1,102 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from mirage.core.discord.entry import (channel_dirname, channel_entry,
guild_dirname, guild_entry,
member_entry, member_filename)
def test_guild_dirname_basic():
assert guild_dirname({"id": "G1", "name": "My Server"}) == "My Server__G1"
def test_guild_dirname_apostrophe_is_preserved():
assert guild_dirname({
"id": "G1",
"name": "Zecheng's Server"
}) == "Zecheng's Server__G1"
def test_guild_dirname_slash_in_name_is_replaced():
assert guild_dirname({"id": "G1", "name": "A/B Test"}) == "AB Test__G1"
def test_guild_dirname_falls_back_to_unknown_when_name_missing():
assert guild_dirname({"id": "G1"}) == "unknown__G1"
def test_guild_dirname_falls_back_to_unknown_when_name_empty():
assert guild_dirname({"id": "G1", "name": ""}) == "unknown__G1"
def test_channel_dirname_basic():
assert channel_dirname({"id": "C1", "name": "general"}) == "general__C1"
def test_channel_dirname_with_emoji_is_preserved():
assert channel_dirname({"id": "C1", "name": "🔥-deals"}) == "🔥-deals__C1"
def test_channel_dirname_falls_back_to_unknown():
assert channel_dirname({"id": "C1"}) == "unknown__C1"
def test_member_filename_basic():
member = {"user": {"id": "U1", "username": "alice"}}
assert member_filename(member) == "alice__U1.json"
def test_member_filename_preserves_special_chars():
member = {"user": {"id": "U1", "username": "bob.smith!"}}
assert member_filename(member) == "bob.smith!__U1.json"
def test_member_filename_falls_back_to_unknown():
member = {"user": {"id": "U1"}}
assert member_filename(member) == "unknown__U1.json"
def test_same_named_members_get_distinct_filenames():
a = {"user": {"id": "U1", "username": "alice"}}
b = {"user": {"id": "U2", "username": "alice"}}
assert member_filename(a) != member_filename(b)
assert member_filename(a) == "alice__U1.json"
assert member_filename(b) == "alice__U2.json"
def test_same_named_channels_get_distinct_dirnames():
a = {"id": "C1", "name": "general"}
b = {"id": "C2", "name": "general"}
assert channel_dirname(a) != channel_dirname(b)
def test_same_named_guilds_get_distinct_dirnames():
a = {"id": "G1", "name": "Test"}
b = {"id": "G2", "name": "Test"}
assert guild_dirname(a) != guild_dirname(b)
def test_guild_entry_vfs_name_matches_dirname():
g = {"id": "G1", "name": "My Server"}
assert guild_entry(g).vfs_name == guild_dirname(g)
def test_channel_entry_vfs_name_matches_dirname():
c = {"id": "C1", "name": "general", "last_message_id": "M1"}
assert channel_entry(c).vfs_name == channel_dirname(c)
def test_member_entry_vfs_name_matches_filename():
m = {"user": {"id": "U1", "username": "alice"}}
assert member_entry(m).vfs_name == member_filename(m)
@@ -0,0 +1,86 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from mirage.core.discord.entry import channel_dirname, guild_dirname
from mirage.core.discord.formatters import format_grep_results
def test_format_grep_results_uses_vfs_channel_dirname():
msgs = [{
"timestamp": "2024-04-10T12:34:56.000000+00:00",
"channel_id": "C1",
"author": {
"username": "alice"
},
"content": "hello",
}]
guild = {"id": "G1", "name": "MyGuild"}
chan = {"id": "C1", "name": "general"}
lines = format_grep_results(
msgs,
prefix="/discord",
guild_dirname=guild_dirname(guild),
channel_names={chan["id"]: channel_dirname(chan)})
assert lines == [
"/discord/MyGuild__G1/channels/general__C1/"
"2024-04-10/chat.jsonl:[alice] hello"
]
def test_format_grep_results_falls_back_to_channel_id():
msgs = [{
"timestamp": "2024-04-10T00:00:00+00:00",
"channel_id": "C2",
"author": {
"username": "bob"
},
"content": "x",
}]
lines = format_grep_results(msgs, "/discord", "G__G1", channel_names={})
assert lines == [
"/discord/G__G1/channels/C2/2024-04-10/chat.jsonl:[bob] x"
]
def test_format_grep_results_path_matches_vfs_layout():
msgs = [{
"timestamp": "2024-04-10T00:00:00+00:00",
"channel_id": "C1",
"author": {
"username": "alice"
},
"content": "hi",
}]
guild = {"id": "G1", "name": "S"}
chan = {"id": "C1", "name": "general"}
line = format_grep_results(msgs, "/discord", guild_dirname(guild),
{chan["id"]: channel_dirname(chan)})[0]
expected_path = (f"/discord/{guild_dirname(guild)}/channels/"
f"{channel_dirname(chan)}/2024-04-10/chat.jsonl")
assert line.startswith(expected_path + ":")
def test_format_grep_results_replaces_newlines():
msgs = [{
"timestamp": "2024-01-02",
"channel_id": "C",
"author": {
"username": "u"
},
"content": "line1\nline2",
}]
lines = format_grep_results(msgs, "/discord", "G__G1", channel_names={})
assert lines == [
"/discord/G__G1/channels/C/2024-01-02/chat.jsonl:[u] line1 line2"
]
+54
View File
@@ -0,0 +1,54 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from unittest.mock import AsyncMock, patch
import pytest
from mirage.core.discord.guilds import list_guilds
from mirage.resource.discord.config import DiscordConfig
@pytest.fixture
def config():
return DiscordConfig(token="test-bot-token")
@pytest.mark.asyncio
async def test_list_guilds(config):
mock_data = [
{
"id": "G001",
"name": "My Server"
},
{
"id": "G002",
"name": "Another Server"
},
]
with patch(
"mirage.core.discord.paginate.discord_get",
new_callable=AsyncMock,
return_value=mock_data,
) as mock_get:
result = await list_guilds(config)
assert len(result) == 2
assert result[0]["name"] == "My Server"
assert result[1]["id"] == "G002"
mock_get.assert_called_once()
args, kwargs = mock_get.call_args
assert args[1] == "/users/@me/guilds"
assert kwargs["params"]["after"] == "0"
assert kwargs["params"]["limit"] == 200
+66
View File
@@ -0,0 +1,66 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import json
from unittest.mock import AsyncMock, patch
import pytest
from mirage.core.discord.history import get_history_jsonl
from mirage.resource.discord.config import DiscordConfig
@pytest.fixture
def config():
return DiscordConfig(token="test-bot-token")
@pytest.mark.asyncio
async def test_get_history_jsonl(config):
messages = [
{
"id": "200",
"content": "second"
},
{
"id": "100",
"content": "first"
},
]
with patch(
"mirage.core.discord.paginate.discord_get",
new_callable=AsyncMock,
return_value=messages,
):
result = await get_history_jsonl(config, "C001", "2024-01-15")
lines = result.decode().strip().split("\n")
assert len(lines) == 2
first = json.loads(lines[0])
second = json.loads(lines[1])
assert int(first["id"]) < int(second["id"])
assert first["content"] == "first"
assert second["content"] == "second"
@pytest.mark.asyncio
async def test_get_history_empty(config):
with patch(
"mirage.core.discord.paginate.discord_get",
new_callable=AsyncMock,
return_value=[],
):
result = await get_history_jsonl(config, "C001", "2024-01-15")
assert result == b""
+86
View File
@@ -0,0 +1,86 @@
# ========= 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.core.discord.members import list_members, search_members
from mirage.resource.discord.config import DiscordConfig
@pytest.fixture
def config():
return DiscordConfig(token="test-bot-token")
@pytest.mark.asyncio
async def test_list_members(config):
mock_data = [
{
"user": {
"id": "U001",
"username": "alice"
}
},
{
"user": {
"id": "U002",
"username": "bob"
}
},
]
with patch(
"mirage.core.discord.paginate.discord_get",
new_callable=AsyncMock,
return_value=mock_data,
) as mock_get:
result = await list_members(config, "G001")
assert len(result) == 2
assert result[0]["user"]["username"] == "alice"
mock_get.assert_called_once()
args, kwargs = mock_get.call_args
assert args[1] == "/guilds/G001/members"
assert kwargs["params"]["after"] == "0"
assert kwargs["params"]["limit"] == 1000
@pytest.mark.asyncio
async def test_search_members(config):
mock_data = [
{
"user": {
"id": "U001",
"username": "alice"
}
},
]
with patch(
"mirage.core.discord.members.discord_get",
new_callable=AsyncMock,
return_value=mock_data,
) as mock_get:
result = await search_members(config, "G001", "ali")
assert len(result) == 1
assert result[0]["user"]["username"] == "alice"
mock_get.assert_called_once_with(
config,
"/guilds/G001/members/search",
params={
"query": "ali",
"limit": 100
},
)
+105
View File
@@ -0,0 +1,105 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from unittest.mock import AsyncMock, patch
import pytest
from mirage.core.discord.paginate import after_id_pages, offset_pages
from mirage.resource.discord.config import DiscordConfig
@pytest.fixture
def config():
return DiscordConfig(token="t")
@pytest.mark.asyncio
async def test_after_id_pages_walks_until_partial(config):
page1 = [{"id": "1"}, {"id": "2"}]
page2 = [{"id": "3"}]
with patch("mirage.core.discord.paginate.discord_get",
new_callable=AsyncMock,
side_effect=[page1, page2]) as mock:
collected = []
async for page in after_id_pages(
config,
"/x",
base_params={},
last_id_fn=lambda m: m["id"],
page_size=2,
):
collected.append(page)
assert collected == [page1, page2]
assert mock.call_count == 2
second_params = mock.call_args_list[1].kwargs["params"]
assert second_params["after"] == "2"
@pytest.mark.asyncio
async def test_after_id_pages_stops_on_empty(config):
with patch("mirage.core.discord.paginate.discord_get",
new_callable=AsyncMock,
return_value=[]):
pages = [
p async for p in after_id_pages(
config,
"/x",
base_params={},
last_id_fn=lambda m: m["id"],
)
]
assert pages == []
@pytest.mark.asyncio
async def test_offset_pages_walks_and_terminates_on_total(config):
p1 = {"messages": [["m1"], ["m2"]], "total_results": 3}
p2 = {"messages": [["m3"]], "total_results": 3}
with patch("mirage.core.discord.paginate.discord_get",
new_callable=AsyncMock,
side_effect=[p1, p2]) as mock:
collected = []
async for items in offset_pages(
config,
"/y",
base_params={"q": "x"},
items_path=("messages", ),
page_size=2,
):
collected.append(items)
assert collected == [[["m1"], ["m2"]], [["m3"]]]
assert mock.call_count == 2
assert mock.call_args_list[0].kwargs["params"]["offset"] == 0
assert mock.call_args_list[1].kwargs["params"]["offset"] == 2
@pytest.mark.asyncio
async def test_offset_pages_respects_max_pages(config):
p = {"messages": [["m"]], "total_results": 999}
with patch("mirage.core.discord.paginate.discord_get",
new_callable=AsyncMock,
return_value=p) as mock:
collected = []
async for items in offset_pages(
config,
"/y",
base_params={},
items_path=("messages", ),
page_size=1,
max_pages=2,
):
collected.append(items)
assert len(collected) == 2
assert mock.call_count == 2
+78
View File
@@ -0,0 +1,78 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
from unittest.mock import AsyncMock, patch
import pytest
from mirage.accessor.discord import DiscordAccessor
from mirage.cache.index import IndexEntry
from mirage.cache.index.ram import RAMIndexCacheStore
from mirage.core.discord.read import read
from mirage.resource.discord.config import DiscordConfig
from mirage.types import PathSpec
@pytest.fixture
def index():
store = RAMIndexCacheStore()
asyncio.run(
store.put(
"/My Server/channels/general",
IndexEntry(
id="C001",
name="general",
resource_type="discord/channel",
vfs_name="general",
),
))
return store
@pytest.fixture
def accessor():
config = DiscordConfig(token="test-bot-token")
return DiscordAccessor(config=config)
@pytest.mark.asyncio
async def test_read_jsonl(accessor, index):
fake_data = b'{"id":"100","content":"hello"}\n'
with patch(
"mirage.core.discord.read.get_history_jsonl",
new_callable=AsyncMock,
return_value=fake_data,
) as mock_hist:
path = "/My Server/channels/general/2024-01-15/chat.jsonl"
result = await read(
accessor,
PathSpec(virtual=path,
directory=path,
resource_path=path.strip("/")),
index,
)
assert result == fake_data
mock_hist.assert_called_once_with(accessor.config, "C001", "2024-01-15")
@pytest.mark.asyncio
async def test_read_not_found(accessor, index):
with pytest.raises(FileNotFoundError):
await read(
accessor,
PathSpec(resource_path="no/such/path",
virtual="/no/such/path",
directory="/no/such/path"), index)
+190
View File
@@ -0,0 +1,190 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from unittest.mock import AsyncMock, patch
import pytest
from mirage.accessor.discord import DiscordAccessor
from mirage.cache.index import IndexEntry
from mirage.cache.index.ram import RAMIndexCacheStore
from mirage.core.discord.readdir import readdir
from mirage.resource.discord.config import DiscordConfig
from mirage.types import PathSpec
@pytest.fixture
def index():
return RAMIndexCacheStore()
@pytest.fixture
def accessor():
return DiscordAccessor(config=DiscordConfig(token="test-bot-token"), )
@pytest.mark.asyncio
async def test_readdir_root(accessor, index):
guilds = [
{
"id": "G001",
"name": "My Server"
},
]
with patch(
"mirage.core.discord.readdir.list_guilds",
new_callable=AsyncMock,
return_value=guilds,
):
result = await readdir(
accessor, PathSpec(resource_path="", virtual="/", directory="/"),
index)
assert "/My Server__G001" in result
@pytest.mark.asyncio
async def test_readdir_root_with_slash_in_name(accessor, index):
guilds = [
{
"id": "G001",
"name": "A/B Test Server"
},
]
with patch(
"mirage.core.discord.readdir.list_guilds",
new_callable=AsyncMock,
return_value=guilds,
):
result = await readdir(
accessor, PathSpec(resource_path="", virtual="/", directory="/"),
index)
assert result == ["/AB Test Server__G001"]
@pytest.mark.asyncio
async def test_readdir_root_with_apostrophe(accessor, index):
guilds = [
{
"id": "G001",
"name": "Zecheng's Server"
},
]
with patch(
"mirage.core.discord.readdir.list_guilds",
new_callable=AsyncMock,
return_value=guilds,
):
result = await readdir(
accessor, PathSpec(resource_path="", virtual="/", directory="/"),
index)
assert "/Zecheng's Server__G001" in result
@pytest.mark.asyncio
async def test_readdir_guild(accessor, index):
await index.put(
"/My Server",
IndexEntry(
id="G001",
name="My Server",
resource_type="discord/guild",
vfs_name="My Server",
),
)
result = await readdir(
accessor,
PathSpec(resource_path="My Server",
virtual="/My Server",
directory="/My Server"), index)
assert result == [
"/My Server/channels",
"/My Server/members",
]
@pytest.mark.asyncio
async def test_readdir_channels(accessor, index):
await index.put(
"/My Server",
IndexEntry(
id="G001",
name="My Server",
resource_type="discord/guild",
vfs_name="My Server",
),
)
channels = [
{
"id": "C001",
"name": "general",
"type": 0
},
{
"id": "C002",
"name": "random",
"type": 0
},
]
with patch(
"mirage.core.discord.readdir.list_channels",
new_callable=AsyncMock,
return_value=channels,
):
result = await readdir(
accessor,
PathSpec(resource_path="My Server/channels",
virtual="/My Server/channels",
directory="/My Server/channels"), index)
assert "/My Server/channels/general__C001" in result
assert "/My Server/channels/random__C002" in result
@pytest.mark.asyncio
async def test_readdir_channel_dates(accessor, index):
await index.put(
"/My Server",
IndexEntry(
id="G001",
name="My Server",
resource_type="discord/guild",
vfs_name="My Server",
),
)
await index.put(
"/My Server/channels/general",
IndexEntry(
id="C001",
name="general",
resource_type="discord/channel",
vfs_name="general",
),
)
result = await readdir(
accessor,
PathSpec(resource_path="My Server/channels/general",
virtual="/My Server/channels/general",
directory="/My Server/channels/general"), index)
assert len(result) >= 1
# New layout: date directories (no extension)
import re
date_re = re.compile(r"^/My Server/channels/general/\d{4}-\d{2}-\d{2}$")
assert all(date_re.match(r) for r in result)
@@ -0,0 +1,153 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from unittest.mock import AsyncMock, patch
import aiohttp
import pytest
from mirage.accessor.discord import DiscordAccessor
from mirage.cache.index import IndexEntry
from mirage.cache.index.ram import RAMIndexCacheStore
from mirage.core.discord.readdir import readdir
from mirage.resource.discord.config import DiscordConfig
from mirage.types import PathSpec
@pytest.fixture
def index():
return RAMIndexCacheStore()
@pytest.fixture
def accessor():
return DiscordAccessor(config=DiscordConfig(token="t"))
async def _seed_channel(idx):
await idx.put(
"/G",
IndexEntry(id="G1",
name="G",
resource_type="discord/guild",
vfs_name="G"),
)
await idx.put(
"/G/channels/ch",
IndexEntry(id="C1",
name="ch",
resource_type="discord/channel",
vfs_name="ch"),
)
@pytest.mark.asyncio
async def test_date_dir_contents_lists_chat_and_files(accessor, index):
fake_messages = [{
"id":
"100",
"content":
"hi",
"attachments": [{
"id": "A1",
"filename": "pic.png",
"url": "https://cdn.example/A1/pic.png",
"proxy_url": "https://media.example/A1/pic.png",
"content_type": "image/png",
"size": 1234,
}],
}]
await _seed_channel(index)
with patch("mirage.core.discord.readdir.list_messages_for_day",
new_callable=AsyncMock,
return_value=fake_messages):
result = await readdir(
accessor,
PathSpec(resource_path="G/channels/ch/2024-04-04",
virtual="/G/channels/ch/2024-04-04",
directory="/G/channels/ch/2024-04-04"),
index,
)
assert "/G/channels/ch/2024-04-04/chat.jsonl" in result
assert "/G/channels/ch/2024-04-04/files" in result
@pytest.mark.asyncio
async def test_files_dir_lists_attachments(accessor, index):
fake_messages = [{
"id":
"100",
"attachments": [{
"id": "A1",
"filename": "pic.png",
"url": "https://cdn.example/A1/pic.png",
"content_type": "image/png",
"size": 1234,
}],
}]
await _seed_channel(index)
with patch("mirage.core.discord.readdir.list_messages_for_day",
new_callable=AsyncMock,
return_value=fake_messages):
result = await readdir(
accessor,
PathSpec(resource_path="G/channels/ch/2024-04-04/files",
virtual="/G/channels/ch/2024-04-04/files",
directory="/G/channels/ch/2024-04-04/files"),
index,
)
assert any(r.endswith("pic__A1.png") for r in result)
@pytest.mark.asyncio
async def test_fetch_day_swallows_soft_errors(accessor, index):
await _seed_channel(index)
err = aiohttp.ClientResponseError(
request_info=None, # type: ignore[arg-type]
history=(),
status=403,
)
with patch("mirage.core.discord.readdir.list_messages_for_day",
new_callable=AsyncMock,
side_effect=err):
result = await readdir(
accessor,
PathSpec(resource_path="G/channels/ch/2024-04-04",
virtual="/G/channels/ch/2024-04-04",
directory="/G/channels/ch/2024-04-04"),
index,
)
# empty day on soft error → sealed listing (no chat.jsonl/files entries)
assert result == []
@pytest.mark.asyncio
async def test_fetch_day_propagates_hard_errors(accessor, index):
await _seed_channel(index)
err = aiohttp.ClientResponseError(
request_info=None, # type: ignore[arg-type]
history=(),
status=500,
)
with patch("mirage.core.discord.readdir.list_messages_for_day",
new_callable=AsyncMock,
side_effect=err):
with pytest.raises(aiohttp.ClientResponseError):
await readdir(
accessor,
PathSpec(resource_path="G/channels/ch/2024-04-04",
virtual="/G/channels/ch/2024-04-04",
directory="/G/channels/ch/2024-04-04"),
index,
)
+270
View File
@@ -0,0 +1,270 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
from unittest.mock import AsyncMock
import pytest
from mirage.cache.index import IndexEntry
from mirage.cache.index.ram import RAMIndexCacheStore
from mirage.core.discord.scope import coalesce_scopes, detect_scope
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
def _run(coro):
return asyncio.run(coro)
def _gs(path: str,
prefix: str = "",
pattern: str | None = None,
resolved: bool = True) -> PathSpec:
return PathSpec(
resource_path=mount_key(path, prefix),
virtual=path,
directory=path.rsplit("/", 1)[0] + "/" if "/" in path else "/",
pattern=pattern,
resolved=resolved,
)
@pytest.fixture
def index():
idx = RAMIndexCacheStore(ttl=600)
_run(
idx.put(
"/discord/TestGuild",
IndexEntry(id="G1",
name="TestGuild",
resource_type="discord/guild")))
_run(
idx.put(
"/discord/TestGuild/channels/general",
IndexEntry(id="C1",
name="general",
resource_type="discord/channel")))
return idx
# ── root ──────────────────────────────────────
def test_root_empty():
scope = _run(detect_scope("/"))
assert scope.level == "root"
def test_root_prefix():
scope = _run(detect_scope(_gs("/discord/", prefix="/discord")))
assert scope.level == "root"
# ── guild ─────────────────────────────────────
def test_guild(index):
scope = _run(
detect_scope(_gs("/discord/TestGuild", prefix="/discord"), index))
assert scope.level == "guild"
assert scope.guild_id == "G1"
def test_guild_channels(index):
scope = _run(
detect_scope(_gs("/discord/TestGuild/channels", prefix="/discord"),
index))
assert scope.level == "guild"
assert scope.guild_id == "G1"
def test_guild_members(index):
scope = _run(
detect_scope(_gs("/discord/TestGuild/members", prefix="/discord"),
index))
assert scope.level == "guild"
assert scope.guild_id == "G1"
# ── channel ───────────────────────────────────
def test_channel(index):
scope = _run(
detect_scope(
_gs("/discord/TestGuild/channels/general", prefix="/discord"),
index))
assert scope.level == "channel"
assert scope.guild_id == "G1"
assert scope.channel_id == "C1"
# ── date / messages / files ────────────────────
def test_date_dir(index):
scope = _run(
detect_scope(
_gs("/discord/TestGuild/channels/general/2024-04-10",
prefix="/discord"), index))
assert scope.level == "date"
assert scope.guild_id == "G1"
assert scope.channel_id == "C1"
assert scope.date_str == "2024-04-10"
def test_messages_file(index):
scope = _run(
detect_scope(
_gs("/discord/TestGuild/channels/general/2024-04-10/chat.jsonl",
prefix="/discord"), index))
assert scope.level == "messages"
assert scope.guild_id == "G1"
assert scope.channel_id == "C1"
assert scope.date_str == "2024-04-10"
def test_files_dir(index):
scope = _run(
detect_scope(
_gs("/discord/TestGuild/channels/general/2024-04-10/files",
prefix="/discord"), index))
assert scope.level == "files"
assert scope.guild_id == "G1"
assert scope.channel_id == "C1"
assert scope.date_str == "2024-04-10"
def test_file_blob(index):
scope = _run(
detect_scope(
_gs(
"/discord/TestGuild/channels/general/2024-04-10/files/"
"img__A1.png",
prefix="/discord"), index))
assert scope.level == "file_blob"
assert scope.channel_id == "C1"
assert scope.date_str == "2024-04-10"
# ── PathSpec ─────────────────────────────────
def test_glob_jsonl_in_channel(index):
gs = PathSpec(
resource_path=mount_key("/discord/TestGuild/channels/general/*.jsonl",
"/discord"),
virtual="/discord/TestGuild/channels/general/*.jsonl",
directory="/discord/TestGuild/channels/general/",
pattern="*.jsonl",
resolved=False,
)
scope = _run(detect_scope(gs, index))
assert scope.level == "channel"
assert scope.guild_id == "G1"
assert scope.channel_id == "C1"
def test_glob_specific_date(index):
gs = PathSpec(
resource_path=mount_key(
"/discord/TestGuild/channels/general/2024-04-*.jsonl", "/discord"),
virtual="/discord/TestGuild/channels/general/2024-04-*.jsonl",
directory="/discord/TestGuild/channels/general/",
pattern="2024-04-*.jsonl",
resolved=False,
)
scope = _run(detect_scope(gs, index))
assert scope.level == "channel"
assert scope.channel_id == "C1"
def test_glob_non_jsonl():
gs = PathSpec(
resource_path="discord/TestGuild/members/*.json",
virtual="/discord/TestGuild/members/*.json",
directory="/discord/TestGuild/members/",
pattern="*.json",
resolved=False,
)
scope = _run(detect_scope(gs))
assert scope.level != "channel"
# ── no index ──────────────────────────────────
def test_guild_no_index():
scope = _run(detect_scope("TestGuild"))
assert scope.level == "guild"
assert scope.guild_id is None
def test_channel_no_index():
scope = _run(detect_scope("TestGuild/channels/general"))
assert scope.level == "channel"
assert scope.guild_id is None
assert scope.channel_id is None
def _spec(path: str, prefix: str = "/discord") -> PathSpec:
return PathSpec(resource_path=mount_key(path, prefix),
virtual=path,
directory=path)
@pytest.fixture
def fake_index():
idx = AsyncMock()
async def _get(virtual_key):
result = AsyncMock()
if virtual_key.endswith("/myguild/channels/general"):
result.entry = type("E", (), {"id": "ch_456"})
elif virtual_key.endswith("/myguild"):
result.entry = type("E", (), {"id": "g_123"})
else:
result.entry = None
return result
idx.get.side_effect = _get
return idx
@pytest.mark.asyncio
async def test_coalesce_concrete_jsonl_paths_same_channel(fake_index):
paths = [
_spec(f"/discord/myguild/channels/general/2026-01-{d:02d}/chat.jsonl")
for d in range(1, 8)
]
scope = await coalesce_scopes(paths, fake_index)
assert scope is not None
assert scope.level == "channel"
assert scope.guild_id == "g_123"
assert scope.channel_id == "ch_456"
@pytest.mark.asyncio
async def test_coalesce_returns_none_for_mixed_channels(fake_index):
paths = [
_spec("/discord/myguild/channels/general/2026-01-01/chat.jsonl"),
_spec("/discord/myguild/channels/random/2026-01-01/chat.jsonl"),
]
assert await coalesce_scopes(paths, fake_index) is None
@pytest.mark.asyncio
async def test_coalesce_empty_list_returns_none(fake_index):
assert await coalesce_scopes([], fake_index) is None
+147
View File
@@ -0,0 +1,147 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
from unittest.mock import AsyncMock, patch
from mirage.core.discord.search import search_guild
from mirage.resource.discord.config import DiscordConfig
def _run(coro):
return asyncio.run(coro)
def _config():
return DiscordConfig(token="test-token")
def _make_search_response(messages, total=None):
hits = [[msg] for msg in messages]
return {"messages": hits, "total_results": total or len(messages)}
def test_search_returns_messages():
msgs = [
{
"id": "100",
"content": "hello world",
"author": {
"username": "a"
}
},
{
"id": "200",
"content": "hello again",
"author": {
"username": "b"
}
},
]
with patch("mirage.core.discord.paginate.discord_get",
new_callable=AsyncMock,
return_value=_make_search_response(msgs)):
results = _run(search_guild(_config(), "G1", "hello"))
assert len(results) == 2
assert results[0]["id"] == "100"
assert results[1]["id"] == "200"
def test_search_with_channel_filter():
msgs = [{"id": "300", "content": "test", "author": {"username": "c"}}]
with patch("mirage.core.discord.paginate.discord_get",
new_callable=AsyncMock,
return_value=_make_search_response(msgs)) as mock:
results = _run(search_guild(_config(), "G1", "test", channel_id="C1"))
assert len(results) == 1
call_params = mock.call_args[1].get("params") or mock.call_args[0][2]
assert call_params["channel_id"] == "C1"
assert call_params["content"] == "test"
def test_search_empty_results():
with patch("mirage.core.discord.paginate.discord_get",
new_callable=AsyncMock,
return_value={
"messages": [],
"total_results": 0
}):
results = _run(search_guild(_config(), "G1", "nonexistent"))
assert results == []
def test_search_sorted_oldest_first():
msgs = [
{
"id": "500",
"content": "newer",
"author": {
"username": "a"
}
},
{
"id": "100",
"content": "older",
"author": {
"username": "b"
}
},
]
with patch("mirage.core.discord.paginate.discord_get",
new_callable=AsyncMock,
return_value=_make_search_response(msgs)):
results = _run(search_guild(_config(), "G1", "hello"))
assert results[0]["id"] == "100"
assert results[1]["id"] == "500"
def test_search_respects_limit():
msgs = [{
"id": str(i),
"content": f"msg{i}",
"author": {
"username": "a"
}
} for i in range(10)]
with patch("mirage.core.discord.paginate.discord_get",
new_callable=AsyncMock,
return_value=_make_search_response(msgs, total=10)):
results = _run(search_guild(_config(), "G1", "msg", limit=3))
assert len(results) == 3
def test_search_paginates():
page1 = [{
"id": str(i),
"content": f"msg{i}",
"author": {
"username": "a"
}
} for i in range(25)]
page2 = [{
"id": str(i + 25),
"content": f"msg{i + 25}",
"author": {
"username": "a"
}
} for i in range(5)]
responses = [
_make_search_response(page1, total=30),
_make_search_response(page2, total=30),
]
with patch("mirage.core.discord.paginate.discord_get",
new_callable=AsyncMock,
side_effect=responses):
results = _run(search_guild(_config(), "G1", "msg", limit=100))
assert len(results) == 30
+173
View File
@@ -0,0 +1,173 @@
# ========= 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.accessor.discord import DiscordAccessor
from mirage.cache.index import IndexEntry
from mirage.cache.index.ram import RAMIndexCacheStore
from mirage.core.discord.stat import stat
from mirage.types import FileType, PathSpec
@pytest.fixture
def index():
store = RAMIndexCacheStore()
asyncio.run(
store.put(
"/My Server",
IndexEntry(
id="G001",
name="My Server",
resource_type="discord/guild",
vfs_name="My Server",
),
))
asyncio.run(
store.put(
"/My Server/channels/general",
IndexEntry(
id="C001",
name="general",
resource_type="discord/channel",
remote_time="794354201395200000",
vfs_name="general",
),
))
asyncio.run(
store.put(
"/My Server/members/alice.json",
IndexEntry(
id="U001",
name="alice",
resource_type="discord/member",
vfs_name="alice.json",
),
))
return store
@pytest.fixture
def accessor():
return DiscordAccessor(config=object())
@pytest.mark.asyncio
async def test_stat_root(accessor, index):
result = await stat(accessor,
PathSpec(resource_path="", virtual="/", directory="/"),
index)
assert result.type == FileType.DIRECTORY
assert result.name == "/"
@pytest.mark.asyncio
async def test_stat_guild(accessor, index):
result = await stat(
accessor,
PathSpec(resource_path="My Server",
virtual="/My Server",
directory="/My Server"), index)
assert result.type == FileType.DIRECTORY
assert result.extra["guild_id"] == "G001"
@pytest.mark.asyncio
async def test_stat_channel(accessor, index):
result = await stat(
accessor,
PathSpec(resource_path="My Server/channels/general",
virtual="/My Server/channels/general",
directory="/My Server/channels/general"), index)
assert result.type == FileType.DIRECTORY
assert result.extra["channel_id"] == "C001"
assert result.modified == "2021-01-01T00:00:00Z"
@pytest.mark.asyncio
async def test_stat_member(accessor, index):
result = await stat(
accessor,
PathSpec(resource_path="My Server/members/alice.json",
virtual="/My Server/members/alice.json",
directory="/My Server/members/alice.json"), index)
assert result.type == FileType.JSON
assert result.extra["user_id"] == "U001"
@pytest.mark.asyncio
async def test_stat_date_dir(accessor, index):
path = "/My Server/channels/general/2024-01-15"
result = await stat(
accessor,
PathSpec(virtual=path, directory=path, resource_path=path.strip("/")),
index)
assert result.type == FileType.DIRECTORY
assert result.name == "2024-01-15"
@pytest.mark.asyncio
async def test_stat_chat_jsonl(accessor, index):
path = "/My Server/channels/general/2024-01-15/chat.jsonl"
result = await stat(
accessor,
PathSpec(virtual=path, directory=path, resource_path=path.strip("/")),
index)
assert result.type == FileType.TEXT
assert result.name == "chat.jsonl"
@pytest.mark.asyncio
async def test_stat_files_dir(accessor, index):
path = "/My Server/channels/general/2024-01-15/files"
result = await stat(
accessor,
PathSpec(virtual=path, directory=path, resource_path=path.strip("/")),
index)
assert result.type == FileType.DIRECTORY
assert result.name == "files"
@pytest.mark.asyncio
async def test_stat_non_date_chat_jsonl_not_found(accessor, index):
path = "/My Server/channels/general/notadate/chat.jsonl"
with pytest.raises(FileNotFoundError):
await stat(
accessor,
PathSpec(virtual=path,
directory=path,
resource_path=path.strip("/")), index)
@pytest.mark.asyncio
async def test_stat_non_date_files_dir_not_found(accessor, index):
path = "/My Server/channels/general/notadate/files"
with pytest.raises(FileNotFoundError):
await stat(
accessor,
PathSpec(virtual=path,
directory=path,
resource_path=path.strip("/")), index)
@pytest.mark.asyncio
async def test_stat_not_found(accessor, index):
with pytest.raises(FileNotFoundError):
await stat(
accessor,
PathSpec(resource_path="nonexistent/path",
virtual="/nonexistent/path",
directory="/nonexistent/path"), index)
+98
View File
@@ -0,0 +1,98 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from unittest.mock import AsyncMock, patch
import pytest
from mirage.core.discord.guilds import list_guilds_stream
from mirage.core.discord.history import (date_to_snowflake,
stream_messages_for_day)
from mirage.core.discord.members import list_members_stream
from mirage.core.discord.search import search_guild_stream
from mirage.resource.discord.config import DiscordConfig
@pytest.fixture
def config():
return DiscordConfig(token="t")
@pytest.mark.asyncio
async def test_list_guilds_stream_walks_pages(config):
with patch("mirage.core.discord.paginate.discord_get",
new_callable=AsyncMock,
side_effect=[[{
"id": "G1"
}, {
"id": "G2"
}], [{
"id": "G3"
}]]):
collected = []
async for page in list_guilds_stream(config, page_size=2):
collected.append(page)
assert collected == [[{"id": "G1"}, {"id": "G2"}], [{"id": "G3"}]]
@pytest.mark.asyncio
async def test_list_members_stream_walks_user_ids(config):
page1 = [{"user": {"id": "U1"}}, {"user": {"id": "U2"}}]
page2 = [{"user": {"id": "U3"}}]
with patch("mirage.core.discord.paginate.discord_get",
new_callable=AsyncMock,
side_effect=[page1, page2]) as mock:
collected = []
async for page in list_members_stream(config, "G1", page_size=2):
collected.append(page)
assert collected == [page1, page2]
assert mock.call_args_list[1].kwargs["params"]["after"] == "U2"
@pytest.mark.asyncio
async def test_stream_messages_for_day_filters_by_date(config):
before_int = int(date_to_snowflake("2024-01-15", end=True))
in_range = [{"id": str(before_int - 1000), "content": "ok"}]
out_of_range = [{"id": str(before_int + 1000), "content": "next-day"}]
with patch("mirage.core.discord.paginate.discord_get",
new_callable=AsyncMock,
return_value=in_range + out_of_range):
pages = [
p async for p in stream_messages_for_day(
config, "C1", "2024-01-15", page_size=2)
]
flat = [m for page in pages for m in page]
assert all(int(m["id"]) <= before_int for m in flat)
assert any(m["content"] == "ok" for m in flat)
@pytest.mark.asyncio
async def test_search_guild_stream_flattens_contexts(config):
p1 = {
"messages": [[{
"id": "1"
}], [{
"id": "2"
}]],
"total_results": 30,
}
p2 = {"messages": [[{"id": "3"}]], "total_results": 30}
with patch("mirage.core.discord.paginate.discord_get",
new_callable=AsyncMock,
side_effect=[p1, p2]):
collected = []
async for page in search_guild_stream(config, "G1", "x", max_pages=2):
collected.append(page)
flat = [m["id"] for page in collected for m in page]
assert flat == ["1", "2", "3"]
+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. =========
+53
View File
@@ -0,0 +1,53 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage.accessor.disk import DiskAccessor
from mirage.core.disk.du import du, du_all
from mirage.types import PathSpec
@pytest.mark.asyncio
async def test_du_single_file(tmp_path):
(tmp_path / "a.txt").write_bytes(b"hello")
accessor = DiskAccessor(tmp_path)
result = await du(
accessor,
PathSpec(resource_path="a.txt", virtual="/a.txt", directory="/a.txt"))
assert result == 5
@pytest.mark.asyncio
async def test_du_directory(tmp_path):
(tmp_path / "a.txt").write_bytes(b"aaa")
(tmp_path / "b.txt").write_bytes(b"bb")
accessor = DiskAccessor(tmp_path)
result = await du(accessor,
PathSpec(resource_path="", virtual="/", directory="/"))
assert result == 5
@pytest.mark.asyncio
async def test_du_all_returns_pairs(tmp_path):
(tmp_path / "a.txt").write_bytes(b"aaa")
(tmp_path / "sub").mkdir()
(tmp_path / "sub" / "b.txt").write_bytes(b"bb")
accessor = DiskAccessor(tmp_path)
entries, total = await du_all(
accessor, PathSpec(resource_path="", virtual="/", directory="/"))
paths = [p for p, _ in entries]
assert "/a.txt" in paths
assert "/sub/b.txt" in paths
assert total == 5
+84
View File
@@ -0,0 +1,84 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage.accessor.disk import DiskAccessor
from mirage.core.disk.find import find
from mirage.types import PathSpec
@pytest.mark.asyncio
async def test_find_all_files(tmp_path):
(tmp_path / "a.txt").write_text("a")
(tmp_path / "sub").mkdir()
(tmp_path / "sub" / "b.txt").write_text("b")
accessor = DiskAccessor(tmp_path)
result = await find(accessor,
PathSpec(resource_path="", virtual="/", directory="/"))
assert "/a.txt" in result
assert "/sub/b.txt" in result
assert "/sub" in result
@pytest.mark.asyncio
async def test_find_with_name_pattern(tmp_path):
(tmp_path / "a.txt").write_text("a")
(tmp_path / "b.py").write_text("b")
accessor = DiskAccessor(tmp_path)
result = await find(accessor,
PathSpec(resource_path="", virtual="/", directory="/"),
name="*.txt")
assert result == ["/a.txt"]
@pytest.mark.asyncio
async def test_find_with_type_filter_file(tmp_path):
(tmp_path / "a.txt").write_text("a")
(tmp_path / "sub").mkdir()
accessor = DiskAccessor(tmp_path)
result = await find(accessor,
PathSpec(resource_path="", virtual="/", directory="/"),
type="f")
assert "/a.txt" in result
assert "/sub" not in result
@pytest.mark.asyncio
async def test_find_with_type_filter_directory(tmp_path):
(tmp_path / "a.txt").write_text("a")
(tmp_path / "sub").mkdir()
accessor = DiskAccessor(tmp_path)
result = await find(accessor,
PathSpec(resource_path="", virtual="/", directory="/"),
type="d")
assert "/sub" in result
assert "/a.txt" not in result
@pytest.mark.asyncio
async def test_find_with_maxdepth(tmp_path):
(tmp_path / "a.txt").write_text("a")
(tmp_path / "sub").mkdir()
(tmp_path / "sub" / "b.txt").write_text("b")
(tmp_path / "sub" / "deep").mkdir()
(tmp_path / "sub" / "deep" / "c.txt").write_text("c")
accessor = DiskAccessor(tmp_path)
result = await find(accessor,
PathSpec(resource_path="", virtual="/", directory="/"),
maxdepth=1)
assert "/a.txt" in result
assert "/sub" in result
assert "/sub/b.txt" not in result
assert "/sub/deep/c.txt" not in result
+65
View File
@@ -0,0 +1,65 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage.accessor.disk import DiskAccessor
from mirage.cache.index import RAMIndexCacheStore
from mirage.core.disk.glob import resolve_glob
from mirage.types import PathSpec
@pytest.mark.asyncio
async def test_resolve_file_path(tmp_path):
(tmp_path / "a.txt").write_text("a")
accessor = DiskAccessor(tmp_path)
index = RAMIndexCacheStore(ttl=0)
scope = PathSpec(resource_path="a.txt",
virtual="/a.txt",
directory="/",
resolved=True)
result = await resolve_glob(accessor, [scope], index)
assert len(result) == 1
assert result[0].virtual == "/a.txt"
assert result[0].resolved is True
@pytest.mark.asyncio
async def test_resolve_glob_pattern(tmp_path):
(tmp_path / "a.txt").write_text("a")
(tmp_path / "b.txt").write_text("b")
(tmp_path / "c.py").write_text("c")
accessor = DiskAccessor(tmp_path)
index = RAMIndexCacheStore(ttl=0)
scope = PathSpec(resource_path="*.txt",
virtual="/*.txt",
directory="/",
pattern="*.txt",
resolved=False)
result = await resolve_glob(accessor, [scope], index)
originals = sorted(r.virtual for r in result)
assert originals == ["/a.txt", "/b.txt"]
@pytest.mark.asyncio
async def test_resolve_directory_path(tmp_path):
accessor = DiskAccessor(tmp_path)
index = RAMIndexCacheStore(ttl=0)
scope = PathSpec(resource_path="",
virtual="/",
directory="/",
resolved=False)
result = await resolve_glob(accessor, [scope], index)
assert len(result) == 1
assert result[0].virtual == "/"
+58
View File
@@ -0,0 +1,58 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage.accessor.disk import DiskAccessor
from mirage.cache.index import RAMIndexCacheStore
from mirage.core.disk.read import read_bytes
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
@pytest.mark.asyncio
async def test_read_file(tmp_path):
(tmp_path / "hello.txt").write_bytes(b"hello world")
accessor = DiskAccessor(tmp_path)
index = RAMIndexCacheStore(ttl=0)
result = await read_bytes(
accessor,
PathSpec(resource_path=mount_key("/hello.txt", "/disk"),
virtual="/hello.txt",
directory="/hello.txt"), index)
assert result == b"hello world"
@pytest.mark.asyncio
async def test_read_file_not_found(tmp_path):
accessor = DiskAccessor(tmp_path)
index = RAMIndexCacheStore(ttl=0)
with pytest.raises(FileNotFoundError):
await read_bytes(
accessor,
PathSpec(resource_path="missing.txt",
virtual="/missing.txt",
directory="/missing.txt"), index)
@pytest.mark.asyncio
async def test_read_with_glob_scope_and_prefix(tmp_path):
(tmp_path / "data.bin").write_bytes(b"\x00\x01\x02")
accessor = DiskAccessor(tmp_path)
index = RAMIndexCacheStore(ttl=0)
scope = PathSpec(resource_path=mount_key("/disk/data.bin", "/disk"),
virtual="/disk/data.bin",
directory="/disk/")
result = await read_bytes(accessor, scope, index)
assert result == b"\x00\x01\x02"
+140
View File
@@ -0,0 +1,140 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage.accessor.disk import DiskAccessor
from mirage.cache.index import RAMIndexCacheStore
from mirage.core.disk.readdir import readdir
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
@pytest.mark.asyncio
async def test_empty_directory(tmp_path):
accessor = DiskAccessor(tmp_path)
index = RAMIndexCacheStore(ttl=0)
result = await readdir(
accessor,
PathSpec(resource_path=mount_key("/", "/disk"),
virtual="/",
directory="/"), index)
assert result == []
@pytest.mark.asyncio
async def test_directory_with_files(tmp_path):
(tmp_path / "a.txt").write_text("a")
(tmp_path / "b.txt").write_text("b")
accessor = DiskAccessor(tmp_path)
index = RAMIndexCacheStore(ttl=0)
result = await readdir(
accessor, PathSpec(resource_path="", virtual="/", directory="/"),
index)
assert result == ["/a.txt", "/b.txt"]
@pytest.mark.asyncio
async def test_directory_with_subdirectories(tmp_path):
(tmp_path / "sub").mkdir()
(tmp_path / "file.txt").write_text("x")
accessor = DiskAccessor(tmp_path)
index = RAMIndexCacheStore(ttl=0)
result = await readdir(
accessor, PathSpec(resource_path="", virtual="/", directory="/"),
index)
assert result == ["/file.txt", "/sub"]
@pytest.mark.asyncio
async def test_cache_hit(tmp_path):
(tmp_path / "a.txt").write_text("a")
accessor = DiskAccessor(tmp_path)
index = RAMIndexCacheStore(ttl=600)
first = await readdir(
accessor, PathSpec(resource_path="", virtual="/", directory="/"),
index)
(tmp_path / "b.txt").write_text("b")
second = await readdir(
accessor, PathSpec(resource_path="", virtual="/", directory="/"),
index)
assert first == second
@pytest.mark.asyncio
async def test_with_prefix(tmp_path):
(tmp_path / "a.txt").write_text("a")
accessor = DiskAccessor(tmp_path)
index = RAMIndexCacheStore(ttl=0)
result = await readdir(
accessor,
PathSpec(resource_path=mount_key("/disk/", "/disk"),
virtual="/disk/",
directory="/disk/"), index)
assert result == ["/disk/a.txt"]
@pytest.mark.asyncio
async def test_with_glob_scope(tmp_path):
(tmp_path / "a.txt").write_text("a")
accessor = DiskAccessor(tmp_path)
index = RAMIndexCacheStore(ttl=0)
scope = PathSpec(resource_path=mount_key("/disk/", "/disk"),
virtual="/disk/",
directory="/disk/")
result = await readdir(accessor, scope, index)
assert result == ["/disk/a.txt"]
@pytest.mark.asyncio
async def test_not_a_directory(tmp_path):
(tmp_path / "file.txt").write_text("x")
accessor = DiskAccessor(tmp_path)
index = RAMIndexCacheStore(ttl=0)
with pytest.raises(NotADirectoryError):
await readdir(
accessor,
PathSpec(resource_path="file.txt",
virtual="/file.txt",
directory="/file.txt"), index)
@pytest.mark.asyncio
async def test_cache_hit_entries_stay_clean(tmp_path):
(tmp_path / "a.txt").write_text("a")
accessor = DiskAccessor(tmp_path)
index = RAMIndexCacheStore(ttl=600)
spec = PathSpec(resource_path=mount_key("/data/", "/data"),
virtual="/data/",
directory="/data/")
cold = await readdir(accessor, spec, index)
warm = await readdir(accessor, spec, index)
assert cold == ["/data/a.txt"]
assert warm == cold
@pytest.mark.asyncio
async def test_cache_key_ignores_trailing_slash(tmp_path):
(tmp_path / "a.txt").write_text("a")
accessor = DiskAccessor(tmp_path)
index = RAMIndexCacheStore(ttl=600)
slashed = PathSpec(resource_path=mount_key("/data/", "/data"),
virtual="/data/",
directory="/data/")
bare = PathSpec(resource_path=mount_key("/data", "/data"),
virtual="/data",
directory="/data")
first = await readdir(accessor, slashed, index)
second = await readdir(accessor, bare, index)
assert first == second == ["/data/a.txt"]
+90
View File
@@ -0,0 +1,90 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage.accessor.disk import DiskAccessor
from mirage.cache.index import RAMIndexCacheStore
from mirage.core.disk.stat import stat
from mirage.types import FileType, PathSpec
from mirage.utils.key_prefix import mount_key
@pytest.mark.asyncio
async def test_stat_file(tmp_path):
(tmp_path / "hello.txt").write_text("hello")
accessor = DiskAccessor(tmp_path)
index = RAMIndexCacheStore(ttl=0)
result = await stat(
accessor,
PathSpec(resource_path=mount_key("/hello.txt", "/disk"),
virtual="/hello.txt",
directory="/hello.txt"), index)
assert result.name == "hello.txt"
assert result.size == 5
assert result.modified is not None
assert result.modified.endswith("Z")
assert "+00:00" not in result.modified
assert result.type != FileType.DIRECTORY
@pytest.mark.asyncio
async def test_stat_directory(tmp_path):
(tmp_path / "sub").mkdir()
accessor = DiskAccessor(tmp_path)
index = RAMIndexCacheStore(ttl=0)
result = await stat(
accessor,
PathSpec(resource_path="sub", virtual="/sub", directory="/sub"), index)
assert result.type == FileType.DIRECTORY
assert result.size is None
@pytest.mark.asyncio
async def test_stat_file_not_found(tmp_path):
accessor = DiskAccessor(tmp_path)
index = RAMIndexCacheStore(ttl=0)
with pytest.raises(FileNotFoundError):
await stat(
accessor,
PathSpec(resource_path="missing.txt",
virtual="/missing.txt",
directory="/missing.txt"), index)
@pytest.mark.asyncio
async def test_stat_with_glob_scope(tmp_path):
(tmp_path / "a.txt").write_text("data")
accessor = DiskAccessor(tmp_path)
index = RAMIndexCacheStore(ttl=0)
scope = PathSpec(resource_path=mount_key("/disk/a.txt", "/disk"),
virtual="/disk/a.txt",
directory="/disk/")
result = await stat(accessor, scope, index)
assert result.name == "a.txt"
assert result.size == 4
@pytest.mark.asyncio
async def test_stat_with_prefix(tmp_path):
(tmp_path / "a.txt").write_text("data")
accessor = DiskAccessor(tmp_path)
index = RAMIndexCacheStore(ttl=0)
result = await stat(
accessor,
PathSpec(resource_path=mount_key("/disk/a.txt", "/disk"),
virtual="/disk/a.txt",
directory="/disk/a.txt"), index)
assert result.name == "a.txt"
assert result.size == 4
@@ -0,0 +1,29 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
from mirage.accessor.disk import DiskAccessor
from mirage.core.disk.stat import stat
from mirage.types import PathSpec
def test_disk_stat_returns_fingerprint_from_mtime(tmp_path):
p = tmp_path / "f.txt"
p.write_bytes(b"hi")
accessor = DiskAccessor(root=tmp_path)
scope = PathSpec(resource_path="f.txt", virtual="/f.txt", directory="/")
result = asyncio.run(stat(accessor, scope))
assert result.fingerprint is not None
assert result.fingerprint == result.modified
+50
View File
@@ -0,0 +1,50 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage.accessor.disk import DiskAccessor
from mirage.cache.index import RAMIndexCacheStore
from mirage.core.disk.stream import read_stream
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
@pytest.mark.asyncio
async def test_stream_file(tmp_path):
(tmp_path / "data.txt").write_bytes(b"stream content")
accessor = DiskAccessor(tmp_path)
index = RAMIndexCacheStore(ttl=0)
chunks = []
async for chunk in read_stream(
accessor,
PathSpec(resource_path="data.txt",
virtual="/data.txt",
directory="/data.txt"), index):
chunks.append(chunk)
assert b"".join(chunks) == b"stream content"
@pytest.mark.asyncio
async def test_stream_with_glob_scope(tmp_path):
(tmp_path / "data.txt").write_bytes(b"abc")
accessor = DiskAccessor(tmp_path)
index = RAMIndexCacheStore(ttl=0)
scope = PathSpec(resource_path=mount_key("/disk/data.txt", "/disk"),
virtual="/disk/data.txt",
directory="/disk/")
chunks = []
async for chunk in read_stream(accessor, scope, index):
chunks.append(chunk)
assert b"".join(chunks) == b"abc"
+53
View File
@@ -0,0 +1,53 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage.accessor.disk import DiskAccessor
from mirage.core.disk.write import write_bytes
from mirage.types import PathSpec
@pytest.mark.asyncio
async def test_write_new_file(tmp_path):
accessor = DiskAccessor(tmp_path)
await write_bytes(
accessor,
PathSpec(resource_path="new.txt",
virtual="/new.txt",
directory="/new.txt"), b"content")
assert (tmp_path / "new.txt").read_bytes() == b"content"
@pytest.mark.asyncio
async def test_overwrite_existing_file(tmp_path):
(tmp_path / "exist.txt").write_bytes(b"old")
accessor = DiskAccessor(tmp_path)
await write_bytes(
accessor,
PathSpec(resource_path="exist.txt",
virtual="/exist.txt",
directory="/exist.txt"), b"new")
assert (tmp_path / "exist.txt").read_bytes() == b"new"
@pytest.mark.asyncio
async def test_parent_directory_auto_creation(tmp_path):
accessor = DiskAccessor(tmp_path)
await write_bytes(
accessor,
PathSpec(resource_path="a/b/c/file.txt",
virtual="/a/b/c/file.txt",
directory="/a/b/c/file.txt"), b"deep")
assert (tmp_path / "a" / "b" / "c" / "file.txt").read_bytes() == b"deep"
+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. =========
+102
View File
@@ -0,0 +1,102 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from unittest.mock import AsyncMock, MagicMock
import pytest
from mirage.accessor.email import EmailAccessor
from mirage.core.email._client import list_folders, list_message_uids
from mirage.resource.email.config import EmailConfig
@pytest.fixture
def config():
return EmailConfig(
imap_host="imap.test.com",
smtp_host="smtp.test.com",
username="user@test.com",
password="pass",
)
@pytest.fixture
def accessor(config):
return EmailAccessor(config)
@pytest.mark.asyncio
async def test_list_folders(accessor):
mock_imap = AsyncMock()
mock_response = MagicMock()
mock_response.lines = [
b'(\\HasNoChildren) "/" "INBOX"',
b'(\\HasNoChildren) "/" "Sent"',
b'(\\HasNoChildren) "/" "Drafts"',
]
mock_imap.list.return_value = mock_response
accessor._imap = mock_imap
accessor._imap.protocol = True
folders = await list_folders(accessor)
assert "INBOX" in folders
assert "Sent" in folders
assert "Drafts" in folders
@pytest.mark.asyncio
async def test_list_message_uids(accessor):
mock_imap = AsyncMock()
mock_select_response = MagicMock()
mock_select_response.result = "OK"
mock_imap.select.return_value = mock_select_response
mock_search_response = MagicMock()
mock_search_response.result = "OK"
mock_search_response.lines = [b"1 2 3 4 5"]
mock_imap.search.return_value = mock_search_response
mock_fetch_response = MagicMock()
mock_fetch_response.lines = [
b"1 FETCH (UID 101)",
b"2 FETCH (UID 102)",
b"3 FETCH (UID 103)",
b"4 FETCH (UID 104)",
b"5 FETCH (UID 105)",
b"FETCH completed",
]
mock_imap.fetch.return_value = mock_fetch_response
accessor._imap = mock_imap
accessor._imap.protocol = True
uids = await list_message_uids(accessor, "INBOX")
assert uids == ["101", "102", "103", "104", "105"]
@pytest.mark.asyncio
async def test_list_message_uids_empty(accessor):
mock_imap = AsyncMock()
mock_select_response = MagicMock()
mock_select_response.result = "OK"
mock_imap.select.return_value = mock_select_response
mock_search_response = MagicMock()
mock_search_response.result = "OK"
mock_search_response.lines = [b""]
mock_imap.search.return_value = mock_search_response
accessor._imap = mock_imap
accessor._imap.protocol = True
uids = await list_message_uids(accessor, "INBOX")
assert uids == []
+99
View File
@@ -0,0 +1,99 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from mirage.core.email._parse import parse_rfc822
def test_parse_simple_text_email():
raw = (b"From: Alice <alice@example.com>\r\n"
b"To: Bob <bob@example.com>\r\n"
b"Subject: Hello\r\n"
b"Date: Mon, 14 Apr 2026 10:30:00 +0000\r\n"
b"Message-ID: <abc123@example.com>\r\n"
b"\r\n"
b"Hello, world!")
result = parse_rfc822(raw)
assert result["from"] == {"name": "Alice", "email": "alice@example.com"}
assert result["to"] == [{"name": "Bob", "email": "bob@example.com"}]
assert result["subject"] == "Hello"
assert result["body_text"] == "Hello, world!"
assert result["message_id"] == "<abc123@example.com>"
assert result["has_attachments"] is False
def test_parse_multipart_email():
msg = MIMEMultipart()
msg["From"] = "Alice <alice@example.com>"
msg["To"] = "Bob <bob@example.com>"
msg["Subject"] = "With HTML"
msg["Message-ID"] = "<def456@example.com>"
msg.attach(MIMEText("Plain text body", "plain"))
msg.attach(MIMEText("<p>HTML body</p>", "html"))
raw = msg.as_bytes()
result = parse_rfc822(raw)
assert result["body_text"] == "Plain text body"
assert result["body_html"] == "<p>HTML body</p>"
assert result["has_attachments"] is False
def test_parse_headers_only():
raw = (b"From: Alice <alice@example.com>\r\n"
b"Subject: Test\r\n"
b"\r\n"
b"Body text here")
result = parse_rfc822(raw, headers_only=True)
assert result["subject"] == "Test"
assert result["body_text"] == ""
def test_parse_reply_headers():
raw = (b"From: Bob <bob@example.com>\r\n"
b"To: Alice <alice@example.com>\r\n"
b"Subject: Re: Hello\r\n"
b"In-Reply-To: <abc123@example.com>\r\n"
b"References: <abc123@example.com> <def456@example.com>\r\n"
b"\r\n"
b"Thanks!")
result = parse_rfc822(raw)
assert result["in_reply_to"] == "<abc123@example.com>"
assert result["references"] == [
"<abc123@example.com>", "<def456@example.com>"
]
def test_parse_address_without_name():
raw = (b"From: alice@example.com\r\n"
b"To: bob@example.com\r\n"
b"Subject: Test\r\n"
b"\r\n"
b"Body")
result = parse_rfc822(raw)
assert result["from"]["email"] == "alice@example.com"
assert result["from"]["name"] == ""
def test_parse_cc():
raw = (b"From: alice@example.com\r\n"
b"To: bob@example.com\r\n"
b"Cc: Charlie <charlie@example.com>, dave@example.com\r\n"
b"Subject: Test\r\n"
b"\r\n"
b"Body")
result = parse_rfc822(raw)
assert len(result["cc"]) == 2
assert result["cc"][0]["email"] == "charlie@example.com"
+92
View File
@@ -0,0 +1,92 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from unittest.mock import AsyncMock, patch
import pytest
from mirage.core.email.send import forward_message, reply_message, send_message
from mirage.resource.email.config import EmailConfig
@pytest.fixture
def config():
return EmailConfig(
imap_host="imap.test.com",
smtp_host="smtp.test.com",
smtp_port=587,
username="user@test.com",
password="pass",
)
@pytest.mark.asyncio
async def test_send_message(config):
with patch("mirage.core.email.send.aiosmtplib.send",
new_callable=AsyncMock) as mock_send:
result = await send_message(config, "bob@example.com", "Hello",
"Hi there")
assert result["status"] == "sent"
assert result["to"] == "bob@example.com"
assert result["subject"] == "Hello"
mock_send.assert_called_once()
@pytest.mark.asyncio
async def test_reply_message(config):
original = {
"from": {
"name": "Alice",
"email": "alice@example.com"
},
"to": [{
"name": "",
"email": "user@test.com"
}],
"cc": [],
"subject": "Hello",
"message_id": "<abc123@example.com>",
"references": [],
"body_text": "Original text",
"date": "Mon, 14 Apr 2026 10:30:00 +0000",
}
with patch("mirage.core.email.send.aiosmtplib.send",
new_callable=AsyncMock) as mock_send:
result = await reply_message(config, original, "Thanks!")
assert result["status"] == "sent"
assert result["to"] == "alice@example.com"
assert result["subject"] == "Re: Hello"
mock_send.assert_called_once()
sent_msg = mock_send.call_args[0][0]
assert sent_msg["In-Reply-To"] == "<abc123@example.com>"
@pytest.mark.asyncio
async def test_forward_message(config):
original = {
"from": {
"name": "Alice",
"email": "alice@example.com"
},
"subject": "Hello",
"body_text": "Original text",
"date": "Mon, 14 Apr 2026 10:30:00 +0000",
}
with patch("mirage.core.email.send.aiosmtplib.send",
new_callable=AsyncMock) as mock_send:
result = await forward_message(config, original, "charlie@example.com")
assert result["status"] == "sent"
assert result["to"] == "charlie@example.com"
assert result["subject"] == "Fwd: Hello"
mock_send.assert_called_once()
+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. =========
+171
View File
@@ -0,0 +1,171 @@
# ========= 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 io
import pandas as pd
import pyarrow as pa
import pyarrow.feather as feather
import pytest
from mirage.core.filetype.feather import cat, cut, grep, head, stat, tail, wc
def _make_feather(num_rows: int = 100) -> bytes:
df = pd.DataFrame({
"name": [f"user{i}" for i in range(num_rows)],
"score":
list(range(num_rows)),
"grade": ["A" if i % 2 == 0 else "B" for i in range(num_rows)],
})
table = pa.Table.from_pandas(df)
buf = io.BytesIO()
feather.write_feather(table, buf)
return buf.getvalue()
FEATHER_BYTES = _make_feather()
SMALL_FEATHER = _make_feather(num_rows=5)
class TestCat:
def test_cat_returns_schema_and_preview(self):
result = cat(FEATHER_BYTES)
text = result.decode()
assert "name:" in text
assert "score: int64" in text
assert "user0" in text
def test_cat_limits_preview_rows(self):
result = cat(FEATHER_BYTES, max_rows=5)
text = result.decode()
assert "user4" in text
assert "user5" not in text
def test_cat_small_file(self):
result = cat(SMALL_FEATHER)
text = result.decode()
assert "user0" in text
assert "user4" in text
class TestHead:
def test_head_default_10_rows(self):
result = head(FEATHER_BYTES)
text = result.decode()
assert "user0" in text
assert "user9" in text
def test_head_custom_n(self):
result = head(FEATHER_BYTES, n=3)
text = result.decode()
assert "user2" in text
assert "user3" not in text
def test_head_includes_schema(self):
result = head(FEATHER_BYTES, n=3)
text = result.decode()
assert "name:" in text
class TestTail:
def test_tail_default_10_rows(self):
result = tail(FEATHER_BYTES)
text = result.decode()
assert "user99" in text
assert "user90" in text
def test_tail_custom_n(self):
result = tail(FEATHER_BYTES, n=3)
text = result.decode()
assert "user99" in text
assert "user97" in text
assert "user96" not in text
def test_tail_includes_schema(self):
result = tail(FEATHER_BYTES, n=3)
text = result.decode()
assert "name:" in text
class TestWc:
def test_wc_returns_row_count(self):
result = wc(FEATHER_BYTES)
assert result == 100
def test_wc_small(self):
result = wc(SMALL_FEATHER)
assert result == 5
class TestStat:
def test_stat_includes_schema(self):
result = stat(FEATHER_BYTES)
text = result.decode()
assert "name:" in text
assert "score: int64" in text
def test_stat_includes_row_count(self):
result = stat(FEATHER_BYTES)
text = result.decode()
assert "100" in text
def test_stat_header(self):
result = stat(FEATHER_BYTES)
text = result.decode()
assert "Feather file" in text
class TestGrep:
def test_grep_finds_matching_rows(self):
result = grep(FEATHER_BYTES, "user1")
text = result.decode()
assert "user1," in text or "user1" in text
def test_grep_case_insensitive(self):
result = grep(FEATHER_BYTES, "USER1", ignore_case=True)
text = result.decode()
assert "user1" in text
def test_grep_no_match(self):
result = grep(FEATHER_BYTES, "nonexistent")
text = result.decode()
lines = [line for line in text.strip().splitlines() if line.strip()]
assert len(lines) <= 1
class TestCut:
def test_cut_single_column(self):
result = cut(FEATHER_BYTES, columns=["name"])
text = result.decode()
assert "name" in text
assert "score" not in text
def test_cut_multiple_columns(self):
result = cut(FEATHER_BYTES, columns=["name", "grade"])
text = result.decode()
assert "name" in text
assert "grade" in text
assert "score" not in text
def test_cut_invalid_column(self):
with pytest.raises(ValueError):
cut(FEATHER_BYTES, columns=["nonexistent"])
+105
View File
@@ -0,0 +1,105 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import tempfile
import pandas as pd
import pytest
from mirage.core.filetype.hdf5 import cat, cut, grep, head, stat, tail, wc
def _make_hdf5(num_rows: int = 100) -> bytes:
df = pd.DataFrame({
"name": [f"user{i}" for i in range(num_rows)],
"score":
list(range(num_rows)),
"grade": ["A" if i % 2 == 0 else "B" for i in range(num_rows)],
})
with tempfile.NamedTemporaryFile(suffix=".h5", delete=False) as f:
df.to_hdf(f.name, key="data", mode="w")
tmp = f.name
with open(tmp, "rb") as fh:
return fh.read()
HDF5_BYTES = _make_hdf5()
SMALL_HDF5 = _make_hdf5(num_rows=5)
class TestCat:
def test_cat_returns_preview(self):
result = cat(HDF5_BYTES)
text = result.decode()
assert "user0" in text
def test_cat_limits_preview_rows(self):
result = cat(HDF5_BYTES, max_rows=5)
text = result.decode()
assert "user4" in text
assert "user5" not in text
class TestHead:
def test_head_custom_n(self):
result = head(HDF5_BYTES, n=3)
text = result.decode()
assert "user2" in text
assert "user3" not in text
class TestTail:
def test_tail_custom_n(self):
result = tail(HDF5_BYTES, n=3)
text = result.decode()
assert "user99" in text
assert "user96" not in text
class TestWc:
def test_wc_returns_row_count(self):
assert wc(HDF5_BYTES) == 100
class TestStat:
def test_stat_includes_row_count(self):
result = stat(HDF5_BYTES)
text = result.decode()
assert "100" in text
class TestGrep:
def test_grep_finds_matching_rows(self):
result = grep(HDF5_BYTES, "user1")
text = result.decode()
assert "user1" in text
class TestCut:
def test_cut_single_column(self):
result = cut(HDF5_BYTES, columns=["name"])
text = result.decode()
assert "name" in text
assert "score" not in text
def test_cut_invalid_column(self):
with pytest.raises(ValueError):
cut(HDF5_BYTES, columns=["nonexistent"])
+171
View File
@@ -0,0 +1,171 @@
# ========= 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 io
import pandas as pd
import pyarrow as pa
import pyarrow.orc as orc
import pytest
from mirage.core.filetype.orc import cat, cut, grep, head, stat, tail, wc
def _make_orc(num_rows: int = 100) -> bytes:
df = pd.DataFrame({
"name": [f"user{i}" for i in range(num_rows)],
"score":
list(range(num_rows)),
"grade": ["A" if i % 2 == 0 else "B" for i in range(num_rows)],
})
table = pa.Table.from_pandas(df)
buf = io.BytesIO()
orc.write_table(table, buf)
return buf.getvalue()
ORC_BYTES = _make_orc()
SMALL_ORC = _make_orc(num_rows=5)
class TestCat:
def test_cat_returns_schema_and_preview(self):
result = cat(ORC_BYTES)
text = result.decode()
assert "name:" in text
assert "score: int64" in text
assert "user0" in text
def test_cat_limits_preview_rows(self):
result = cat(ORC_BYTES, max_rows=5)
text = result.decode()
assert "user4" in text
assert "user5" not in text
def test_cat_small_file(self):
result = cat(SMALL_ORC)
text = result.decode()
assert "user0" in text
assert "user4" in text
class TestHead:
def test_head_default_10_rows(self):
result = head(ORC_BYTES)
text = result.decode()
assert "user0" in text
assert "user9" in text
def test_head_custom_n(self):
result = head(ORC_BYTES, n=3)
text = result.decode()
assert "user2" in text
assert "user3" not in text
def test_head_includes_schema(self):
result = head(ORC_BYTES, n=3)
text = result.decode()
assert "name:" in text
class TestTail:
def test_tail_default_10_rows(self):
result = tail(ORC_BYTES)
text = result.decode()
assert "user99" in text
assert "user90" in text
def test_tail_custom_n(self):
result = tail(ORC_BYTES, n=3)
text = result.decode()
assert "user99" in text
assert "user97" in text
assert "user96" not in text
def test_tail_includes_schema(self):
result = tail(ORC_BYTES, n=3)
text = result.decode()
assert "name:" in text
class TestWc:
def test_wc_returns_row_count(self):
result = wc(ORC_BYTES)
assert result == 100
def test_wc_small(self):
result = wc(SMALL_ORC)
assert result == 5
class TestStat:
def test_stat_includes_schema(self):
result = stat(ORC_BYTES)
text = result.decode()
assert "name:" in text
assert "score: int64" in text
def test_stat_includes_row_count(self):
result = stat(ORC_BYTES)
text = result.decode()
assert "100" in text
def test_stat_includes_stripes(self):
result = stat(ORC_BYTES)
text = result.decode()
assert "stripes:" in text
class TestGrep:
def test_grep_finds_matching_rows(self):
result = grep(ORC_BYTES, "user1")
text = result.decode()
assert "user1," in text or "user1" in text
def test_grep_case_insensitive(self):
result = grep(ORC_BYTES, "USER1", ignore_case=True)
text = result.decode()
assert "user1" in text
def test_grep_no_match(self):
result = grep(ORC_BYTES, "nonexistent")
text = result.decode()
lines = [line for line in text.strip().splitlines() if line.strip()]
assert len(lines) <= 1
class TestCut:
def test_cut_single_column(self):
result = cut(ORC_BYTES, columns=["name"])
text = result.decode()
assert "name" in text
assert "score" not in text
def test_cut_multiple_columns(self):
result = cut(ORC_BYTES, columns=["name", "grade"])
text = result.decode()
assert "name" in text
assert "grade" in text
assert "score" not in text
def test_cut_invalid_column(self):
with pytest.raises(ValueError):
cut(ORC_BYTES, columns=["nonexistent"])
+171
View File
@@ -0,0 +1,171 @@
# ========= 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 io
import pandas as pd
import pyarrow as pa
import pyarrow.parquet as pq
import pytest
from mirage.core.filetype.parquet import cat, cut, grep, head, stat, tail, wc
def _make_parquet(num_rows: int = 100, row_group_size: int = 50) -> bytes:
df = pd.DataFrame({
"name": [f"user{i}" for i in range(num_rows)],
"score":
list(range(num_rows)),
"grade": ["A" if i % 2 == 0 else "B" for i in range(num_rows)],
})
table = pa.Table.from_pandas(df)
buf = io.BytesIO()
pq.write_table(table, buf, row_group_size=row_group_size)
return buf.getvalue()
PARQUET_BYTES = _make_parquet()
SMALL_PARQUET = _make_parquet(num_rows=5, row_group_size=5)
class TestCat:
def test_cat_returns_schema_and_preview(self):
result = cat(PARQUET_BYTES)
text = result.decode()
assert "name:" in text
assert "score: int64" in text
assert "user0" in text
def test_cat_limits_preview_rows(self):
result = cat(PARQUET_BYTES, max_rows=5)
text = result.decode()
assert "user4" in text
assert "user5" not in text
def test_cat_small_file(self):
result = cat(SMALL_PARQUET)
text = result.decode()
assert "user0" in text
assert "user4" in text
class TestHead:
def test_head_default_10_rows(self):
result = head(PARQUET_BYTES)
text = result.decode()
assert "user0" in text
assert "user9" in text
def test_head_custom_n(self):
result = head(PARQUET_BYTES, n=3)
text = result.decode()
assert "user2" in text
assert "user3" not in text
def test_head_includes_schema(self):
result = head(PARQUET_BYTES, n=3)
text = result.decode()
assert "name:" in text
class TestTail:
def test_tail_default_10_rows(self):
result = tail(PARQUET_BYTES)
text = result.decode()
assert "user99" in text
assert "user90" in text
def test_tail_custom_n(self):
result = tail(PARQUET_BYTES, n=3)
text = result.decode()
assert "user99" in text
assert "user97" in text
assert "user96" not in text
def test_tail_includes_schema(self):
result = tail(PARQUET_BYTES, n=3)
text = result.decode()
assert "name:" in text
class TestWc:
def test_wc_returns_row_count(self):
result = wc(PARQUET_BYTES)
assert result == 100
def test_wc_small(self):
result = wc(SMALL_PARQUET)
assert result == 5
class TestStat:
def test_stat_includes_schema(self):
result = stat(PARQUET_BYTES)
text = result.decode()
assert "name:" in text
assert "score: int64" in text
def test_stat_includes_row_count(self):
result = stat(PARQUET_BYTES)
text = result.decode()
assert "100" in text
def test_stat_includes_row_groups(self):
result = stat(PARQUET_BYTES)
text = result.decode()
assert "row_groups: 2" in text
class TestGrep:
def test_grep_finds_matching_rows(self):
result = grep(PARQUET_BYTES, "user1")
text = result.decode()
assert "user1," in text or "user1" in text
def test_grep_case_insensitive(self):
result = grep(PARQUET_BYTES, "USER1", ignore_case=True)
text = result.decode()
assert "user1" in text
def test_grep_no_match(self):
result = grep(PARQUET_BYTES, "nonexistent")
text = result.decode()
lines = [line for line in text.strip().splitlines() if line.strip()]
assert len(lines) <= 1
class TestCut:
def test_cut_single_column(self):
result = cut(PARQUET_BYTES, columns=["name"])
text = result.decode()
assert "name" in text
assert "score" not in text
def test_cut_multiple_columns(self):
result = cut(PARQUET_BYTES, columns=["name", "grade"])
text = result.decode()
assert "name" in text
assert "grade" in text
assert "score" not in text
def test_cut_invalid_column(self):
with pytest.raises(ValueError):
cut(PARQUET_BYTES, columns=["nonexistent"])
+61
View File
@@ -0,0 +1,61 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from pathlib import Path
import pytest
from mirage.core.filetype.pdf import cat, pages_to_images
_EXAMPLE_PDF = Path(__file__).resolve().parents[4] / "data" / "example.pdf"
@pytest.fixture
def pdf_bytes():
with open(_EXAMPLE_PDF, "rb") as f:
return f.read()
def test_pages_to_images_returns_pngs(pdf_bytes):
images = pages_to_images(pdf_bytes, max_pages=3)
assert len(images) == 3
for page_num, png_bytes in images:
assert isinstance(page_num, int)
assert png_bytes[:8] == b"\x89PNG\r\n\x1a\n"
def test_pages_to_images_page_numbers(pdf_bytes):
images = pages_to_images(pdf_bytes, max_pages=2)
assert images[0][0] == 1
assert images[1][0] == 2
def test_pages_to_images_max_pages(pdf_bytes):
images = pages_to_images(pdf_bytes, max_pages=2)
assert len(images) == 2
def test_cat_returns_text(pdf_bytes):
result = cat(pdf_bytes, max_pages=2)
assert isinstance(result, bytes)
text = result.decode()
assert "# PDF: 15 pages" in text
assert "## Page 1" in text
assert "## Page 2" in text
def test_cat_includes_text_content(pdf_bytes):
result = cat(pdf_bytes, max_pages=1)
text = result.decode()
assert "SEAR" in text or "Schema" in text
+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. =========
+90
View File
@@ -0,0 +1,90 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import time
from unittest.mock import AsyncMock, patch
import pytest
from mirage.core.google._client import TokenManager, google_headers
from mirage.core.google.config import GoogleConfig
@pytest.fixture
def config():
return GoogleConfig(
client_id="test-id",
client_secret="test-secret",
refresh_token="test-refresh",
)
@pytest.mark.asyncio
async def test_token_manager_refreshes_on_first_call(config):
mgr = TokenManager(config)
with patch(
"mirage.core.google._client.refresh_access_token",
new_callable=AsyncMock,
return_value=("new-token", 3600),
) as mock_refresh:
token = await mgr.get_token()
assert token == "new-token"
mock_refresh.assert_called_once_with(config)
@pytest.mark.asyncio
async def test_token_manager_caches_token(config):
mgr = TokenManager(config)
with patch(
"mirage.core.google._client.refresh_access_token",
new_callable=AsyncMock,
return_value=("cached-token", 3600),
) as mock_refresh:
t1 = await mgr.get_token()
t2 = await mgr.get_token()
assert t1 == t2 == "cached-token"
assert mock_refresh.call_count == 1
@pytest.mark.asyncio
async def test_token_manager_refreshes_when_expired(config):
mgr = TokenManager(config)
with patch(
"mirage.core.google._client.refresh_access_token",
new_callable=AsyncMock,
return_value=("token-1", 3600),
):
await mgr.get_token()
mgr._expires_at = time.time() - 1
with patch(
"mirage.core.google._client.refresh_access_token",
new_callable=AsyncMock,
return_value=("token-2", 3600),
):
token = await mgr.get_token()
assert token == "token-2"
@pytest.mark.asyncio
async def test_google_headers(config):
mgr = TokenManager(config)
with patch(
"mirage.core.google._client.refresh_access_token",
new_callable=AsyncMock,
return_value=("my-token", 3600),
):
headers = await google_headers(mgr)
assert headers["Authorization"] == "Bearer my-token"
+171
View File
@@ -0,0 +1,171 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import json
from unittest.mock import AsyncMock, patch
import pytest
from mirage.accessor.gdocs import GDocsAccessor
from mirage.cache.index import IndexEntry
from mirage.cache.index.ram import RAMIndexCacheStore
from mirage.core.gdocs._client import TokenManager
from mirage.core.gdocs.read import read, read_doc
from mirage.resource.gdocs.config import GDocsConfig
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
@pytest.fixture
def token_manager():
config = GDocsConfig(
client_id="test-id",
client_secret="test-secret",
refresh_token="test-refresh",
)
mgr = TokenManager(config)
mgr._access_token = "fake-token"
mgr._expires_at = 9999999999
return mgr
@pytest.fixture
def accessor(token_manager):
return GDocsAccessor(config=None, token_manager=token_manager)
@pytest.fixture
def index():
return RAMIndexCacheStore()
@pytest.mark.asyncio
async def test_read_doc(token_manager):
doc_json = {
"documentId": "abc123",
"title": "Test Doc",
"body": {
"content": []
},
}
with patch(
"mirage.core.gdocs.read.google_get",
new_callable=AsyncMock,
return_value=doc_json,
):
result = await read_doc(token_manager, "abc123")
parsed = json.loads(result)
assert parsed["documentId"] == "abc123"
assert parsed["title"] == "Test Doc"
@pytest.mark.asyncio
async def test_read_via_index(accessor, index):
await index.set_dir("/gdocs/owned", [
("2026-04-01_My_Doc__doc1.gdoc.json",
IndexEntry(id="abc123",
name="Test Doc",
resource_type="gdocs/file",
vfs_name="2026-04-01_My_Doc__doc1.gdoc.json")),
])
doc_json = {
"documentId": "abc123",
"title": "Test Doc",
"body": {
"content": []
},
}
with patch(
"mirage.core.gdocs.read.google_get",
new_callable=AsyncMock,
return_value=doc_json,
):
result = await read(
accessor,
PathSpec(
resource_path=mount_key(
"/gdocs/owned/2026-04-01_My_Doc__doc1.gdoc.json",
"/gdocs"),
virtual="/gdocs/owned/2026-04-01_My_Doc__doc1.gdoc.json",
directory="/gdocs/owned/2026-04-01_My_Doc__doc1.gdoc.json"),
index)
parsed = json.loads(result)
assert parsed["documentId"] == "abc123"
@pytest.mark.asyncio
async def test_read_no_index(accessor):
with pytest.raises(FileNotFoundError):
await read(
accessor,
PathSpec(resource_path=mount_key(
"/gdocs/owned/nonexistent.gdoc.json", "/gdocs"),
virtual="/gdocs/owned/nonexistent.gdoc.json",
directory="/gdocs/owned/nonexistent.gdoc.json"), None)
@pytest.mark.asyncio
async def test_read_auto_bootstraps_from_empty_index(accessor, index):
files = [{
"id": "doc1",
"name": "Notes",
"modifiedTime": "2026-04-01T00:00:00.000Z",
"owners": [{
"me": True
}],
}]
with (
patch(
"mirage.core.gdocs.readdir.list_all_files",
new_callable=AsyncMock,
return_value=files,
),
patch(
"mirage.core.gdocs.read.read_doc",
new_callable=AsyncMock,
return_value=b'{"documentId":"doc1"}',
),
):
path = PathSpec(
resource_path=mount_key(
"/gdocs/owned/2026-04-01_Notes__doc1.gdoc.json", "/gdocs"),
virtual="/gdocs/owned/2026-04-01_Notes__doc1.gdoc.json",
directory="/gdocs/owned/2026-04-01_Notes__doc1.gdoc.json",
)
result = await read(accessor, path, index)
assert b"doc1" in result
@pytest.mark.asyncio
async def test_read_missing_file_raises_after_recursion(accessor, index):
with (
patch(
"mirage.core.gdocs.readdir.list_all_files",
new_callable=AsyncMock,
return_value=[],
),
patch(
"mirage.core.gdocs.read.read_doc",
new_callable=AsyncMock,
side_effect=AssertionError("should not call read_doc"),
),
):
path = PathSpec(
resource_path=mount_key("/gdocs/owned/Missing__xyz.gdoc.json",
"/gdocs"),
virtual="/gdocs/owned/Missing__xyz.gdoc.json",
directory="/gdocs/owned/Missing__xyz.gdoc.json",
)
with pytest.raises(FileNotFoundError):
await read(accessor, path, index)
+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 unittest.mock import AsyncMock, patch
import pytest
from mirage.accessor.gdocs import GDocsAccessor
from mirage.cache.index.ram import RAMIndexCacheStore
from mirage.core.gdocs.readdir import readdir
from mirage.core.gdocs.stat import stat
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
@pytest.fixture
def accessor():
return GDocsAccessor(config=None, token_manager=None)
@pytest.fixture
def index():
return RAMIndexCacheStore()
@pytest.mark.asyncio
async def test_readdir_root(accessor, index):
result = await readdir(
accessor,
PathSpec(resource_path=mount_key("/gdocs", "/gdocs"),
virtual="/gdocs",
directory="/gdocs"), index)
assert result == ["/gdocs/owned", "/gdocs/shared"]
@pytest.mark.asyncio
async def test_readdir_owned(accessor, index):
files = [
{
"id": "doc1",
"name": "My Doc",
"modifiedTime": "2026-04-01T00:00:00.000Z",
"owners": [{
"me": True
}],
},
]
with patch(
"mirage.core.gdocs.readdir.list_all_files",
new_callable=AsyncMock,
return_value=files,
):
result = await readdir(
accessor,
PathSpec(resource_path=mount_key("/gdocs/owned", "/gdocs"),
virtual="/gdocs/owned",
directory="/gdocs/owned"), index)
assert len(result) == 1
assert "doc1" in result[0]
@pytest.mark.asyncio
async def test_readdir_shared(accessor, index):
files = [
{
"id": "doc2",
"name": "Shared Doc",
"modifiedTime": "2026-03-15T00:00:00.000Z",
"owners": [{
"me": False
}],
},
]
with patch(
"mirage.core.gdocs.readdir.list_all_files",
new_callable=AsyncMock,
return_value=files,
):
result = await readdir(
accessor,
PathSpec(resource_path=mount_key("/gdocs/shared", "/gdocs"),
virtual="/gdocs/shared",
directory="/gdocs/shared"), index)
assert len(result) == 1
assert "doc2" in result[0]
@pytest.mark.asyncio
async def test_readdir_file_path_raises(accessor, index):
with pytest.raises(FileNotFoundError):
await readdir(
accessor,
PathSpec(resource_path=mount_key("/gdocs/owned/file.gdoc.json",
"/gdocs"),
virtual="/gdocs/owned/file.gdoc.json",
directory="/gdocs/owned/file.gdoc.json"), index)
@pytest.mark.asyncio
async def test_readdir_invalid_path_raises(accessor, index):
with pytest.raises(FileNotFoundError):
await readdir(
accessor,
PathSpec(resource_path=mount_key("/gdocs/bogus", "/gdocs"),
virtual="/gdocs/bogus",
directory="/gdocs/bogus"), index)
@pytest.mark.asyncio
async def test_readdir_owned_pushes_modified_range(accessor, index):
captured = {}
async def fake_list(token_manager, mime_type=None, **kwargs):
captured.update(kwargs)
captured["mime_type"] = mime_type
return []
with patch("mirage.core.gdocs.readdir.list_all_files", new=fake_list):
await readdir(
accessor,
PathSpec(resource_path=mount_key("/gdocs/owned/2026-05-*",
"/gdocs"),
virtual="/gdocs/owned/2026-05-*",
directory="/gdocs/owned",
pattern="2026-05-*"), index)
assert captured["modified_after"] == "2026-05-01T00:00:00Z"
assert captured["modified_before"] == "2026-06-01T00:00:00Z"
@pytest.mark.asyncio
async def test_readdir_owned_filtered_does_not_cache(accessor, index):
files = [{
"id": "may",
"name": "MayDoc",
"modifiedTime": "2026-05-15T00:00:00.000Z",
"owners": [{
"me": True
}]
}]
full_files = files + [{
"id": "jan",
"name": "JanDoc",
"modifiedTime": "2026-01-15T00:00:00.000Z",
"owners": [{
"me": True
}]
}]
call_count = {"n": 0}
async def fake_list(token_manager,
mime_type=None,
modified_after=None,
modified_before=None,
**kwargs):
call_count["n"] += 1
if modified_after:
return files
return full_files
with patch("mirage.core.gdocs.readdir.list_all_files", new=fake_list):
await readdir(
accessor,
PathSpec(resource_path=mount_key("/gdocs/owned/2026-05-*",
"/gdocs"),
virtual="/gdocs/owned/2026-05-*",
directory="/gdocs/owned",
pattern="2026-05-*"), index)
result = await readdir(
accessor,
PathSpec(resource_path=mount_key("/gdocs/owned", "/gdocs"),
virtual="/gdocs/owned",
directory="/gdocs/owned"), index)
assert call_count["n"] == 2
assert len(result) == 2
@pytest.mark.asyncio
async def test_readdir_owned_filtered_bypasses_warm_cache(accessor, index):
full_files = [
{
"id": "may",
"name": "MayDoc",
"modifiedTime": "2026-05-15T00:00:00.000Z",
"owners": [{
"me": True
}]
},
{
"id": "jan",
"name": "JanDoc",
"modifiedTime": "2026-01-15T00:00:00.000Z",
"owners": [{
"me": True
}]
},
]
may_only = [full_files[0]]
call_count = {"n": 0}
async def fake_list(token_manager,
mime_type=None,
modified_after=None,
modified_before=None,
**kwargs):
call_count["n"] += 1
if modified_after:
return may_only
return full_files
with patch("mirage.core.gdocs.readdir.list_all_files", new=fake_list):
await readdir(
accessor,
PathSpec(resource_path=mount_key("/gdocs/owned", "/gdocs"),
virtual="/gdocs/owned",
directory="/gdocs/owned"), index)
await readdir(
accessor,
PathSpec(resource_path=mount_key("/gdocs/owned/2026-05-*",
"/gdocs"),
virtual="/gdocs/owned/2026-05-*",
directory="/gdocs/owned",
pattern="2026-05-*"), index)
assert call_count["n"] == 2
@pytest.mark.asyncio
async def test_readdir_owned_no_pattern_omits_range(accessor, index):
captured = {}
async def fake_list(token_manager, mime_type=None, **kwargs):
captured.update(kwargs)
return []
with patch("mirage.core.gdocs.readdir.list_all_files", new=fake_list):
await readdir(
accessor,
PathSpec(resource_path=mount_key("/gdocs/owned", "/gdocs"),
virtual="/gdocs/owned",
directory="/gdocs/owned"), index)
assert captured.get("modified_after") is None
assert captured.get("modified_before") is None
@pytest.mark.asyncio
async def test_readdir_owned_non_date_pattern_omits_range(accessor, index):
captured = {}
async def fake_list(token_manager, mime_type=None, **kwargs):
captured.update(kwargs)
return []
with patch("mirage.core.gdocs.readdir.list_all_files", new=fake_list):
await readdir(
accessor,
PathSpec(resource_path=mount_key("/gdocs/owned/*foo*", "/gdocs"),
virtual="/gdocs/owned/*foo*",
directory="/gdocs/owned",
pattern="*foo*"), index)
assert captured.get("modified_after") is None
assert captured.get("modified_before") is None
@pytest.mark.asyncio
async def test_readdir_filtered_then_stat_succeeds(accessor, index):
files = [{
"id": "may1",
"name": "MayDoc",
"modifiedTime": "2026-05-15T00:00:00.000Z",
"owners": [{
"me": False
}]
}]
with patch(
"mirage.core.gdocs.readdir.list_all_files",
new_callable=AsyncMock,
return_value=files,
):
listed = await readdir(
accessor,
PathSpec(resource_path=mount_key("/gdocs/shared/2026-05-*",
"/gdocs"),
virtual="/gdocs/shared/2026-05-*",
directory="/gdocs/shared",
pattern="2026-05-*"), index)
assert len(listed) == 1
matched = listed[0]
result = await stat(
accessor,
PathSpec(resource_path=mount_key(matched, "/gdocs"),
virtual=matched,
directory=matched),
index,
)
assert result.extra["doc_id"] == "may1"
@pytest.mark.asyncio
async def test_readdir_owned_newest_first_across_cache(accessor, index):
"""Relies on API mock newest-first, mirroring orderBy modifiedTime desc."""
files = [
{
"id": "new",
"name": "Latest",
"modifiedTime": "2026-05-03T00:00:00.000Z",
"owners": [{
"me": True
}]
},
{
"id": "mid",
"name": "Middle",
"modifiedTime": "2026-04-01T00:00:00.000Z",
"owners": [{
"me": True
}]
},
{
"id": "old",
"name": "Oldest",
"modifiedTime": "2026-01-01T00:00:00.000Z",
"owners": [{
"me": True
}]
},
]
with patch(
"mirage.core.gdocs.readdir.list_all_files",
new_callable=AsyncMock,
return_value=files,
) as mock_list:
first = await readdir(
accessor,
PathSpec(resource_path=mount_key("/gdocs/owned", "/gdocs"),
virtual="/gdocs/owned",
directory="/gdocs/owned"), index)
second = await readdir(
accessor,
PathSpec(resource_path=mount_key("/gdocs/owned", "/gdocs"),
virtual="/gdocs/owned",
directory="/gdocs/owned"), index)
assert mock_list.call_count == 1
assert first == second
assert "new" in first[0]
assert "mid" in first[1]
assert "old" in first[-1]
+156
View File
@@ -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. =========
from unittest.mock import AsyncMock, patch
import pytest
from mirage.accessor.gdocs import GDocsAccessor
from mirage.cache.index import IndexEntry
from mirage.cache.index.ram import RAMIndexCacheStore
from mirage.core.gdocs.stat import stat
from mirage.types import FileType, PathSpec
from mirage.utils.key_prefix import mount_key
@pytest.fixture
def accessor():
return GDocsAccessor(config=None, token_manager=None)
@pytest.fixture
def index():
return RAMIndexCacheStore()
@pytest.mark.asyncio
async def test_stat_root(accessor, index):
result = await stat(
accessor,
PathSpec(resource_path=mount_key("/gdocs", "/gdocs"),
virtual="/gdocs",
directory="/gdocs"), index)
assert result.type == FileType.DIRECTORY
assert result.name == "/"
@pytest.mark.asyncio
async def test_stat_owned_dir(accessor, index):
result = await stat(
accessor,
PathSpec(resource_path=mount_key("/gdocs/owned", "/gdocs"),
virtual="/gdocs/owned",
directory="/gdocs/owned"), index)
assert result.type == FileType.DIRECTORY
assert result.name == "owned"
@pytest.mark.asyncio
async def test_stat_shared_dir(accessor, index):
result = await stat(
accessor,
PathSpec(resource_path=mount_key("/gdocs/shared", "/gdocs"),
virtual="/gdocs/shared",
directory="/gdocs/shared"), index)
assert result.type == FileType.DIRECTORY
assert result.name == "shared"
@pytest.mark.asyncio
async def test_stat_doc(accessor, index):
await index.set_dir("/gdocs/owned", [
("2026-04-01_My_Doc__doc1.gdoc.json",
IndexEntry(id="doc1",
name="My Doc",
resource_type="gdocs/file",
remote_time="2026-04-01T00:00:00.000Z",
vfs_name="2026-04-01_My_Doc__doc1.gdoc.json",
size=1000)),
])
result = await stat(
accessor,
PathSpec(resource_path=mount_key(
"/gdocs/owned/2026-04-01_My_Doc__doc1.gdoc.json", "/gdocs"),
virtual="/gdocs/owned/2026-04-01_My_Doc__doc1.gdoc.json",
directory="/gdocs/owned/2026-04-01_My_Doc__doc1.gdoc.json"),
index,
)
assert result.name == "2026-04-01_My_Doc__doc1.gdoc.json"
assert result.type == FileType.JSON
assert result.modified == "2026-04-01T00:00:00.000Z"
assert result.extra["doc_id"] == "doc1"
assert result.extra["doc_name"] == "My Doc"
@pytest.mark.asyncio
async def test_stat_not_found(accessor, index):
files = [{
"id": "doc1",
"name": "My Doc",
"modifiedTime": "2026-04-01T00:00:00.000Z",
"owners": [{
"me": True
}],
}]
with patch(
"mirage.core.gdocs.readdir.list_all_files",
new_callable=AsyncMock,
return_value=files,
):
with pytest.raises(FileNotFoundError):
await stat(
accessor,
PathSpec(resource_path=mount_key(
"/gdocs/owned/nonexistent.gdoc.json", "/gdocs"),
virtual="/gdocs/owned/nonexistent.gdoc.json",
directory="/gdocs/owned/nonexistent.gdoc.json"),
index)
@pytest.mark.asyncio
async def test_stat_cache_miss_falls_back_via_readdir(accessor, index):
files = [{
"id": "doc1",
"name": "My Doc",
"modifiedTime": "2026-04-01T00:00:00.000Z",
"size": "1234",
"owners": [{
"me": True
}],
}]
target = "/gdocs/owned/2026-04-01_My_Doc__doc1.gdoc.json"
with patch(
"mirage.core.gdocs.readdir.list_all_files",
new_callable=AsyncMock,
return_value=files,
) as mock_list:
result = await stat(
accessor,
PathSpec(resource_path=mount_key(target, "/gdocs"),
virtual=target,
directory=target), index)
assert result.type == FileType.JSON
assert result.extra["doc_id"] == "doc1"
assert mock_list.call_count == 1
@pytest.mark.asyncio
async def test_stat_cache_miss_with_index_none_raises(accessor):
with pytest.raises(FileNotFoundError):
await stat(
accessor,
PathSpec(resource_path=mount_key("/gdocs/owned/doc.gdoc.json",
"/gdocs"),
virtual="/gdocs/owned/doc.gdoc.json",
directory="/gdocs/owned/doc.gdoc.json"), None)
+82
View File
@@ -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. =========
from unittest.mock import AsyncMock, patch
import pytest
from mirage.accessor.gdocs import GDocsAccessor
from mirage.cache.index import IndexEntry
from mirage.cache.index.ram import RAMIndexCacheStore
from mirage.core.gdocs.unlink import unlink
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
@pytest.fixture
def accessor():
return GDocsAccessor(config=None, token_manager=None)
@pytest.fixture
def index():
return RAMIndexCacheStore()
@pytest.mark.asyncio
async def test_unlink_calls_delete_with_doc_id(accessor, index):
await index.set_dir("/gdocs/owned", [
("foo.gdoc.json",
IndexEntry(id="doc1",
name="Foo",
resource_type="gdocs/file",
vfs_name="foo.gdoc.json")),
])
with patch("mirage.core.google.tree_ops.delete_file",
new_callable=AsyncMock) as mock_delete:
await unlink(
accessor,
PathSpec(resource_path=mount_key("/gdocs/owned/foo.gdoc.json",
"/gdocs"),
virtual="/gdocs/owned/foo.gdoc.json",
directory="/gdocs/owned/foo.gdoc.json"), index)
mock_delete.assert_awaited_once()
assert mock_delete.await_args.args[1] == "doc1"
listing = await index.list_dir("/gdocs/owned")
assert listing.entries is None or listing.entries == []
@pytest.mark.asyncio
async def test_unlink_virtual_dir_raises(accessor, index):
with pytest.raises(IsADirectoryError):
await unlink(
accessor,
PathSpec(resource_path=mount_key("/gdocs/owned", "/gdocs"),
virtual="/gdocs/owned",
directory="/gdocs/owned"), index)
@pytest.mark.asyncio
async def test_unlink_missing_raises(accessor, index):
files = []
with patch("mirage.core.gdocs.readdir.list_all_files",
new_callable=AsyncMock,
return_value=files):
with pytest.raises(FileNotFoundError):
await unlink(
accessor,
PathSpec(resource_path=mount_key("/gdocs/owned/nope.gdoc.json",
"/gdocs"),
virtual="/gdocs/owned/nope.gdoc.json",
directory="/gdocs/owned/nope.gdoc.json"), index)
+70
View File
@@ -0,0 +1,70 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import json
from unittest.mock import AsyncMock, patch
import pytest
from mirage.core.gdocs._client import TokenManager
from mirage.core.gdocs.update import batch_update
from mirage.resource.gdocs.config import GDocsConfig
@pytest.fixture
def token_manager():
config = GDocsConfig(
client_id="test-id",
client_secret="test-secret",
refresh_token="test-refresh",
)
mgr = TokenManager(config)
mgr._access_token = "fake-token"
mgr._expires_at = 9999999999
return mgr
@pytest.mark.asyncio
async def test_batch_update(token_manager):
payload = json.dumps({
"requests": [{
"insertText": {
"location": {
"index": 1
},
"text": "Hello"
}
}]
})
api_response = {"documentId": "abc123", "replies": [{}]}
with patch(
"mirage.core.gdocs.update.google_post",
new_callable=AsyncMock,
return_value=api_response,
) as mock_post:
result = await batch_update(token_manager, "abc123", payload)
assert result["documentId"] == "abc123"
mock_post.assert_called_once()
@pytest.mark.asyncio
async def test_batch_update_invalid_json(token_manager):
with pytest.raises(ValueError, match="requests"):
await batch_update(token_manager, "abc123", "not json")
@pytest.mark.asyncio
async def test_batch_update_missing_requests_key(token_manager):
with pytest.raises(ValueError, match="requests"):
await batch_update(token_manager, "abc123", '{"foo": "bar"}')
+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. =========
+200
View File
@@ -0,0 +1,200 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from unittest.mock import AsyncMock, patch
import pytest
from mirage.accessor.gdrive import GDriveAccessor
from mirage.cache.index.config import IndexEntry
from mirage.cache.index.ram import RAMIndexCacheStore
from mirage.core.gdrive.read import read, read_bytes
from mirage.core.google._client import TokenManager
from mirage.core.google.config import GoogleConfig
from mirage.types import PathSpec
@pytest.fixture
def config():
return GoogleConfig(
client_id="test-id",
client_secret="test-secret",
refresh_token="test-refresh",
)
@pytest.fixture
def token_manager(config):
mgr = TokenManager(config)
mgr._access_token = "fake-token"
mgr._expires_at = 9999999999
return mgr
@pytest.fixture
def accessor(config, token_manager):
return GDriveAccessor(config=config, token_manager=token_manager)
@pytest.fixture
def index():
store = RAMIndexCacheStore()
return store
@pytest.mark.asyncio
async def test_read_bytes(token_manager):
content = b"pdf content here"
with patch(
"mirage.core.gdrive.read.download_file",
new_callable=AsyncMock,
return_value=content,
) as mock_download:
result = await read_bytes(token_manager, "file123")
assert result == content
mock_download.assert_called_once_with(token_manager, "file123")
@pytest.mark.asyncio
async def test_read_file(accessor, index):
await index.put(
"/Team Drive/report.pdf",
IndexEntry(
id="file123",
name="report",
resource_type="gdrive/file",
remote_time="2026-04-01T00:00:00.000Z",
vfs_name="report.pdf",
extra={"drive_id": "drive1"},
))
content = b"pdf content here"
with patch(
"mirage.core.gdrive.read.download_file",
new_callable=AsyncMock,
return_value=content,
):
result = await read(
accessor,
PathSpec(resource_path="Team Drive/report.pdf",
virtual="/Team Drive/report.pdf",
directory="/Team Drive/report.pdf"), index)
assert result == content
@pytest.mark.asyncio
async def test_read_shared_drive_raises_is_a_directory(accessor, index):
await index.put(
"/Team Drive",
IndexEntry(
id="drive1",
name="Team Drive",
resource_type="gdrive/shared_drive",
vfs_name="Team Drive",
extra={"drive_id": "drive1"},
))
with patch(
"mirage.core.gdrive.read.download_file",
new_callable=AsyncMock,
) as mock_download:
with pytest.raises(IsADirectoryError):
await read(
accessor,
PathSpec(resource_path="Team Drive",
virtual="/Team Drive",
directory="/Team Drive"),
index,
)
mock_download.assert_not_awaited()
@pytest.mark.asyncio
async def test_read_not_found(accessor, index):
with pytest.raises(FileNotFoundError):
await read(
accessor,
PathSpec(resource_path="missing/file.txt",
virtual="/missing/file.txt",
directory="/missing/file.txt"), index)
@pytest.mark.asyncio
async def test_read_auto_bootstraps_from_empty_index(accessor, index):
async def fake_list_files(_tm, folder_id, drive_id=None):
if folder_id == "root":
return [{
"id": "f1",
"name": "report.pdf",
"mimeType": "application/pdf",
"modifiedTime": "2026-04-01T00:00:00.000Z",
"owners": [],
"capabilities": {},
}]
raise AssertionError(f"unexpected folder_id={folder_id}")
with (
patch(
"mirage.core.gdrive.readdir.list_files",
new=fake_list_files,
),
patch(
"mirage.core.gdrive.read.download_file",
new_callable=AsyncMock,
return_value=b"pdf-bytes",
),
):
result = await read(
accessor,
PathSpec(resource_path="report.pdf",
virtual="/report.pdf",
directory="/report.pdf"),
index,
)
assert result == b"pdf-bytes"
@pytest.mark.asyncio
async def test_read_missing_file_raises_after_recursion(accessor, index):
async def fake_list_files(_tm, folder_id, drive_id=None):
if folder_id == "root":
return [{
"id": "f1",
"name": "other.txt",
"mimeType": "text/plain",
"modifiedTime": "2026-04-01T00:00:00.000Z",
"owners": [],
"capabilities": {},
}]
raise AssertionError(f"unexpected folder_id={folder_id}")
with (
patch(
"mirage.core.gdrive.readdir.list_files",
new=fake_list_files,
),
patch(
"mirage.core.gdrive.read.download_file",
new_callable=AsyncMock,
side_effect=AssertionError("should not call download_file"),
),
):
with pytest.raises(FileNotFoundError):
await read(
accessor,
PathSpec(resource_path="missing.txt",
virtual="/missing.txt",
directory="/missing.txt"),
index,
)
+332
View File
@@ -0,0 +1,332 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from unittest.mock import AsyncMock, patch
import pytest
from mirage.accessor.gdrive import GDriveAccessor
from mirage.cache.index.config import IndexEntry
from mirage.cache.index.ram import RAMIndexCacheStore
from mirage.core.gdrive.readdir import readdir
from mirage.core.google._client import TokenManager
from mirage.core.google.config import GoogleConfig
from mirage.types import PathSpec
@pytest.fixture
def config():
return GoogleConfig(
client_id="test-id",
client_secret="test-secret",
refresh_token="test-refresh",
)
@pytest.fixture
def token_manager(config):
mgr = TokenManager(config)
mgr._access_token = "fake-token"
mgr._expires_at = 9999999999
return mgr
@pytest.fixture
def accessor(config, token_manager):
return GDriveAccessor(config=config, token_manager=token_manager)
@pytest.fixture
def index():
return RAMIndexCacheStore()
@pytest.mark.asyncio
async def test_readdir_root(accessor, index):
files = [
{
"id": "f1",
"name": "readme.txt",
"mimeType": "text/plain",
"modifiedTime": "2026-04-01T00:00:00.000Z",
"owners": [{
"me": True,
"emailAddress": "me@gmail.com"
}],
"capabilities": {
"canEdit": True
},
},
]
with patch(
"mirage.core.gdrive.readdir.list_files",
new_callable=AsyncMock,
return_value=files,
):
result = await readdir(
accessor, PathSpec(resource_path="", virtual="/", directory="/"),
index)
assert "/readme.txt" in result
@pytest.mark.asyncio
async def test_readdir_cached(accessor, index):
entry = IndexEntry(
id="f1",
name="cached.txt",
resource_type="gdrive/file",
remote_time="2026-04-01T00:00:00.000Z",
vfs_name="cached.txt",
)
await index.set_dir("/", [("cached.txt", entry)])
result = await readdir(
accessor, PathSpec(resource_path="", virtual="/", directory="/"),
index)
assert any("cached.txt" in r for r in result)
@pytest.mark.asyncio
async def test_readdir_subfolder(accessor, index):
await index.put(
"/docs",
IndexEntry(
id="folder1",
name="docs",
resource_type="gdrive/folder",
remote_time="2026-04-01T00:00:00.000Z",
vfs_name="docs",
))
files = [
{
"id": "f2",
"name": "notes.txt",
"mimeType": "text/plain",
"modifiedTime": "2026-04-01T00:00:00.000Z",
"owners": [],
"capabilities": {
"canEdit": False
},
},
]
with patch(
"mirage.core.gdrive.readdir.list_files",
new_callable=AsyncMock,
return_value=files,
) as mock_list:
result = await readdir(
accessor,
PathSpec(resource_path="docs", virtual="/docs", directory="/docs"),
index)
assert "/docs/notes.txt" in result
mock_list.assert_called_once_with(accessor.token_manager,
folder_id="folder1",
drive_id=None)
@pytest.mark.asyncio
async def test_readdir_repopulates_evicted_subfolder(accessor, index):
root_files = [{
"id": "folder1",
"name": "docs",
"mimeType": "application/vnd.google-apps.folder",
"modifiedTime": "2026-04-01T00:00:00.000Z",
"owners": [],
"capabilities": {},
}]
docs_files = [{
"id": "f2",
"name": "notes.txt",
"mimeType": "text/plain",
"modifiedTime": "2026-04-01T00:00:00.000Z",
"owners": [],
"capabilities": {},
}]
async def fake_list_files(_tm, folder_id, drive_id=None):
if folder_id == "root":
return root_files
if folder_id == "folder1":
return docs_files
raise AssertionError(f"unexpected folder_id={folder_id}")
with patch(
"mirage.core.gdrive.readdir.list_files",
new=fake_list_files,
):
result = await readdir(
accessor,
PathSpec(resource_path="docs", virtual="/docs", directory="/docs"),
index)
assert "/docs/notes.txt" in result
@pytest.mark.asyncio
async def test_readdir_missing_subfolder_raises_after_recursion(
accessor, index):
root_files = [{
"id": "f1",
"name": "other.txt",
"mimeType": "text/plain",
"modifiedTime": "2026-04-01T00:00:00.000Z",
"owners": [],
"capabilities": {},
}]
async def fake_list_files(_tm, folder_id, drive_id=None):
if folder_id == "root":
return root_files
raise AssertionError(f"should not list folder_id={folder_id}")
with patch(
"mirage.core.gdrive.readdir.list_files",
new=fake_list_files,
):
with pytest.raises(FileNotFoundError):
await readdir(
accessor,
PathSpec(resource_path="docs",
virtual="/docs",
directory="/docs"), index)
@pytest.mark.asyncio
async def test_readdir_root_includes_shared_drives(accessor, index):
files = [{
"id": "f1",
"name": "readme.txt",
"mimeType": "text/plain",
"modifiedTime": "2026-04-01T00:00:00.000Z",
"owners": [],
"capabilities": {},
}]
drives = [{"id": "drive1", "name": "Team Drive"}]
with patch("mirage.core.gdrive.readdir.list_files",
new_callable=AsyncMock, return_value=files), \
patch("mirage.core.gdrive.readdir.list_shared_drives",
new_callable=AsyncMock, return_value=drives):
result = await readdir(
accessor, PathSpec(resource_path="", virtual="/", directory="/"),
index)
assert "/readme.txt" in result
# Shared Drives appear as top-level directories.
assert "/Team Drive/" in result
# The drive id is carried on the cached entry for nested listings.
entry = (await index.get("/Team Drive")).entry
assert entry is not None
assert entry.extra.get("drive_id") == "drive1"
@pytest.mark.asyncio
async def test_readdir_root_uniquifies_duplicate_shared_drive_names(
accessor, index):
drives = [
{
"id": "drive1",
"name": "Team"
},
{
"id": "drive2",
"name": "Team"
},
{
"id": "drive3",
"name": "Team"
},
]
with patch("mirage.core.gdrive.readdir.list_files",
new_callable=AsyncMock, return_value=[]), \
patch("mirage.core.gdrive.readdir.list_shared_drives",
new_callable=AsyncMock, return_value=drives):
result = await readdir(
accessor, PathSpec(resource_path="", virtual="/", directory="/"),
index)
assert result == [
"/Team/",
"/Team [Shared Drive]/",
"/Team [Shared Drive 2]/",
]
assert (await index.get("/Team")).entry.id == "drive1"
assert (await index.get("/Team [Shared Drive]")).entry.id == "drive2"
assert (await index.get("/Team [Shared Drive 2]")).entry.id == "drive3"
@pytest.mark.asyncio
async def test_readdir_root_shared_drives_best_effort(accessor, index):
"""If Shared Drive enumeration fails, My Drive listing still succeeds."""
files = [{
"id": "f1",
"name": "readme.txt",
"mimeType": "text/plain",
"modifiedTime": "2026-04-01T00:00:00.000Z",
"owners": [],
"capabilities": {},
}]
with patch("mirage.core.gdrive.readdir.list_files",
new_callable=AsyncMock, return_value=files), \
patch("mirage.core.gdrive.readdir.list_shared_drives",
new_callable=AsyncMock, side_effect=RuntimeError("no scope")):
result = await readdir(
accessor, PathSpec(resource_path="", virtual="/", directory="/"),
index)
assert "/readme.txt" in result
@pytest.mark.asyncio
async def test_readdir_workspace_files_get_extensions(accessor, index):
files = [
{
"id": "d1",
"name": "My Document",
"mimeType": "application/vnd.google-apps.document",
"modifiedTime": "2026-04-01T00:00:00.000Z",
"owners": [{
"me": True,
"emailAddress": "me@gmail.com"
}],
"capabilities": {
"canEdit": True
},
},
{
"id": "s1",
"name": "My Sheet",
"mimeType": "application/vnd.google-apps.spreadsheet",
"modifiedTime": "2026-04-01T00:00:00.000Z",
"owners": [],
"capabilities": {
"canEdit": False
},
},
{
"id": "p1",
"name": "My Slides",
"mimeType": "application/vnd.google-apps.presentation",
"modifiedTime": "2026-04-01T00:00:00.000Z",
"owners": [],
"capabilities": {},
},
]
with patch(
"mirage.core.gdrive.readdir.list_files",
new_callable=AsyncMock,
return_value=files,
):
result = await readdir(
accessor, PathSpec(resource_path="", virtual="/", directory="/"),
index)
assert "/My Document.gdoc.json" in result
assert "/My Sheet.gsheet.json" in result
assert "/My Slides.gslide.json" in result
+183
View File
@@ -0,0 +1,183 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from unittest.mock import AsyncMock, patch
import pytest
from mirage.accessor.gdrive import GDriveAccessor
from mirage.cache.index.config import IndexEntry
from mirage.cache.index.ram import RAMIndexCacheStore
from mirage.core.gdrive.stat import stat
from mirage.core.google._client import TokenManager
from mirage.core.google.config import GoogleConfig
from mirage.types import FileType, PathSpec
@pytest.fixture
def config():
return GoogleConfig(
client_id="test-id",
client_secret="test-secret",
refresh_token="test-refresh",
)
@pytest.fixture
def token_manager(config):
mgr = TokenManager(config)
mgr._access_token = "fake-token"
mgr._expires_at = 9999999999
return mgr
@pytest.fixture
def accessor(config, token_manager):
return GDriveAccessor(config=config, token_manager=token_manager)
@pytest.fixture
def index():
store = RAMIndexCacheStore()
return store
async def _populate_index(index):
await index.put(
"/report.pdf",
IndexEntry(
id="f1",
name="report",
resource_type="gdrive/file",
remote_time="2026-04-01T00:00:00.000Z",
vfs_name="report.pdf",
size=1024,
))
await index.put(
"/docs",
IndexEntry(
id="folder1",
name="docs",
resource_type="gdrive/folder",
remote_time="2026-04-01T00:00:00.000Z",
vfs_name="docs",
))
return index
@pytest.mark.asyncio
async def test_stat_root(accessor, index):
idx = await _populate_index(index)
result = await stat(accessor,
PathSpec(resource_path="", virtual="/", directory="/"),
idx)
assert result.type == FileType.DIRECTORY
assert result.name == "/"
@pytest.mark.asyncio
async def test_stat_file(accessor, index):
idx = await _populate_index(index)
result = await stat(
accessor,
PathSpec(resource_path="report.pdf",
virtual="/report.pdf",
directory="/report.pdf"), idx)
assert result.name == "report.pdf"
assert result.size == 1024
assert result.extra["file_id"] == "f1"
assert result.extra["resource_type"] == "gdrive/file"
@pytest.mark.asyncio
async def test_stat_folder(accessor, index):
idx = await _populate_index(index)
result = await stat(
accessor,
PathSpec(resource_path="docs", virtual="/docs", directory="/docs"),
idx)
assert result.type == FileType.DIRECTORY
assert result.extra["file_id"] == "folder1"
@pytest.mark.asyncio
async def test_stat_shared_drive_is_directory(accessor, index):
await index.put(
"/Team Drive",
IndexEntry(
id="drive1",
name="Team Drive",
resource_type="gdrive/shared_drive",
vfs_name="Team Drive",
extra={"drive_id": "drive1"},
))
result = await stat(
accessor,
PathSpec(resource_path="Team Drive",
virtual="/Team Drive",
directory="/Team Drive"),
index,
)
assert result.type == FileType.DIRECTORY
assert result.extra["file_id"] == "drive1"
@pytest.mark.asyncio
async def test_stat_not_found(accessor, index):
idx = await _populate_index(index)
with patch(
"mirage.core.gdrive.readdir.list_files",
new_callable=AsyncMock,
return_value=[],
):
with pytest.raises(FileNotFoundError):
await stat(
accessor,
PathSpec(resource_path="nonexistent.txt",
virtual="/nonexistent.txt",
directory="/nonexistent.txt"), idx)
@pytest.mark.asyncio
async def test_stat_cache_miss_falls_back_via_readdir(accessor, index):
files = [{
"id": "f99",
"name": "fresh.pdf",
"mimeType": "application/pdf",
"modifiedTime": "2026-04-15T00:00:00.000Z",
"size": "2048",
}]
with patch(
"mirage.core.gdrive.readdir.list_files",
new_callable=AsyncMock,
return_value=files,
) as mock_list:
result = await stat(
accessor,
PathSpec(resource_path="fresh.pdf",
virtual="/fresh.pdf",
directory="/fresh.pdf"), index)
assert result.name == "fresh.pdf"
assert result.extra["file_id"] == "f99"
assert mock_list.call_count == 1
@pytest.mark.asyncio
async def test_stat_index_none_raises(accessor):
with pytest.raises(FileNotFoundError):
await stat(
accessor,
PathSpec(resource_path="x.pdf",
virtual="/x.pdf",
directory="/x.pdf"), None)
+188
View File
@@ -0,0 +1,188 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from unittest.mock import patch
import pytest
from mirage.accessor.gdrive import GDriveAccessor
from mirage.cache.index.config import IndexEntry
from mirage.cache.index.ram import RAMIndexCacheStore
from mirage.core.gdrive.stream import stream
from mirage.core.google._client import TokenManager
from mirage.core.google.config import GoogleConfig
from mirage.types import PathSpec
@pytest.fixture
def config():
return GoogleConfig(
client_id="test-id",
client_secret="test-secret",
refresh_token="test-refresh",
)
@pytest.fixture
def token_manager(config):
mgr = TokenManager(config)
mgr._access_token = "fake-token"
mgr._expires_at = 9999999999
return mgr
@pytest.fixture
def accessor(config, token_manager):
return GDriveAccessor(config=config, token_manager=token_manager)
@pytest.fixture
def index():
return RAMIndexCacheStore()
async def _fake_download_stream(*args, **kwargs):
yield b"chunk1"
yield b"chunk2"
async def _collect(source):
data = b""
async for chunk in source:
data += chunk
return data
@pytest.mark.asyncio
async def test_stream_file(accessor, index):
await index.put(
"/Team Drive/report.pdf",
IndexEntry(
id="file123",
name="report",
resource_type="gdrive/file",
remote_time="2026-04-01T00:00:00.000Z",
vfs_name="report.pdf",
extra={"drive_id": "drive1"},
))
with patch(
"mirage.core.gdrive.stream.download_file_stream",
side_effect=_fake_download_stream,
):
data = await _collect(
stream(
accessor,
PathSpec(resource_path="Team Drive/report.pdf",
virtual="/Team Drive/report.pdf",
directory="/Team Drive/report.pdf"), index))
assert data == b"chunk1chunk2"
@pytest.mark.asyncio
async def test_stream_not_found(accessor, index):
async def fake_list_files(_tm, folder_id, drive_id=None):
return []
with patch("mirage.core.gdrive.readdir.list_files", new=fake_list_files):
with pytest.raises(FileNotFoundError):
await _collect(
stream(
accessor,
PathSpec(resource_path="missing/file.txt",
virtual="/missing/file.txt",
directory="/missing/file.txt"), index))
@pytest.mark.asyncio
async def test_stream_auto_bootstraps_from_empty_index(accessor, index):
async def fake_list_files(_tm, folder_id, drive_id=None):
if folder_id == "root":
return [{
"id": "f1",
"name": "report.pdf",
"mimeType": "application/pdf",
"modifiedTime": "2026-04-01T00:00:00.000Z",
"owners": [],
"capabilities": {},
}]
raise AssertionError(f"unexpected folder_id={folder_id}")
with (
patch(
"mirage.core.gdrive.readdir.list_files",
new=fake_list_files,
),
patch(
"mirage.core.gdrive.stream.download_file_stream",
side_effect=_fake_download_stream,
),
):
data = await _collect(
stream(
accessor,
PathSpec(resource_path="report.pdf",
virtual="/report.pdf",
directory="/report.pdf"),
index,
))
assert data == b"chunk1chunk2"
@pytest.mark.asyncio
async def test_stream_folder_raises(accessor, index):
await index.put(
"/data",
IndexEntry(
id="folder1",
name="data",
resource_type="gdrive/folder",
remote_time="2026-04-01T00:00:00.000Z",
vfs_name="data",
))
with pytest.raises(IsADirectoryError):
await _collect(
stream(
accessor,
PathSpec(resource_path="data",
virtual="/data",
directory="/data"), index))
@pytest.mark.asyncio
async def test_stream_shared_drive_raises(accessor, index):
await index.put(
"/Team Drive",
IndexEntry(
id="drive1",
name="Team Drive",
resource_type="gdrive/shared_drive",
vfs_name="Team Drive",
extra={"drive_id": "drive1"},
))
with patch(
"mirage.core.gdrive.stream.download_file_stream",
side_effect=_fake_download_stream,
) as mock_download:
with pytest.raises(IsADirectoryError):
await _collect(
stream(
accessor,
PathSpec(resource_path="Team Drive",
virtual="/Team Drive",
directory="/Team Drive"),
index,
))
mock_download.assert_not_called()
+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. =========
+35
View File
@@ -0,0 +1,35 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from mirage.core.github._client import github_headers, github_url
def test_github_headers_contains_auth():
headers = github_headers("ghp_test123")
assert headers["Authorization"] == "Bearer ghp_test123"
assert headers["Accept"] == "application/vnd.github+json"
assert "X-GitHub-Api-Version" in headers
def test_github_url_simple():
url = github_url("/repos/{owner}/{repo}/git/trees/{sha}",
owner="acme",
repo="proj",
sha="abc123")
assert url == "https://api.github.com/repos/acme/proj/git/trees/abc123"
def test_github_url_no_params():
url = github_url("/rate_limit")
assert url == "https://api.github.com/rate_limit"
+126
View File
@@ -0,0 +1,126 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage.cache.index import IndexEntry
from mirage.cache.index.ram import RAMIndexCacheStore
from mirage.core.github.find import find
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
def _index() -> RAMIndexCacheStore:
index = RAMIndexCacheStore()
index._entries.update({
"/src":
IndexEntry(id="a", name="src", resource_type="folder", size=None),
"/src/main.py":
IndexEntry(id="b", name="main.py", resource_type="file", size=120),
"/src/utils":
IndexEntry(id="c", name="utils", resource_type="folder", size=None),
"/src/utils/helpers.py":
IndexEntry(id="d", name="helpers.py", resource_type="file", size=80),
"/README.md":
IndexEntry(id="e", name="README.md", resource_type="file", size=50),
})
return index
def _spec(path: str, prefix: str = "") -> PathSpec:
return PathSpec(resource_path=mount_key(path, prefix),
virtual=path,
directory=path)
@pytest.mark.asyncio
async def test_find_all_from_root():
results = await find(None, _spec("/"), index=_index())
assert results == [
"/", "/README.md", "/src", "/src/main.py", "/src/utils",
"/src/utils/helpers.py"
]
@pytest.mark.asyncio
async def test_find_name_pattern():
results = await find(None, _spec("/"), name="*.py", index=_index())
assert results == ["/src/main.py", "/src/utils/helpers.py"]
@pytest.mark.asyncio
async def test_find_type_directory():
results = await find(None, _spec("/src"), type="d", index=_index())
assert results == ["/src", "/src/utils"]
@pytest.mark.asyncio
async def test_find_type_file_under_subdir():
results = await find(None, _spec("/src"), type="f", index=_index())
assert results == ["/src/main.py", "/src/utils/helpers.py"]
@pytest.mark.asyncio
async def test_find_maxdepth():
results = await find(None, _spec("/src"), maxdepth=1, index=_index())
assert results == ["/src", "/src/main.py", "/src/utils"]
@pytest.mark.asyncio
async def test_find_mindepth():
results = await find(None, _spec("/src"), mindepth=2, index=_index())
assert results == ["/src/utils/helpers.py"]
@pytest.mark.asyncio
async def test_find_strips_mount_prefix():
results = await find(None,
_spec("/github/src", prefix="/github"),
type="f",
index=_index())
assert results == ["/src/main.py", "/src/utils/helpers.py"]
@pytest.mark.asyncio
async def test_find_size_filters():
# Directories contribute size 0 to -size, so the root is excluded
# under a positive minimum (#318).
results = await find(None, _spec("/"), min_size=100, index=_index())
assert results == ["/src/main.py"]
@pytest.mark.asyncio
async def test_find_file_start_path():
results = await find(None, _spec("/src/main.py"), index=_index())
assert results == ["/src/main.py"]
@pytest.mark.asyncio
async def test_find_size_filters_file_start():
too_big = await find(None,
_spec("/src/main.py"),
max_size=50,
index=_index())
assert too_big == []
big_enough = await find(None,
_spec("/src/main.py"),
min_size=100,
index=_index())
assert big_enough == ["/src/main.py"]
@pytest.mark.asyncio
async def test_find_no_index_raises():
with pytest.raises(ValueError):
await find(None, _spec("/"), index=None)
+45
View File
@@ -0,0 +1,45 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import base64
from unittest.mock import AsyncMock, patch
import pytest
from mirage.core.github.config import GitHubConfig
from mirage.core.github.read import read_bytes
@pytest.fixture
def config():
return GitHubConfig(token="ghp_test")
@pytest.mark.asyncio
@patch("mirage.core.github.read.github_get", new_callable=AsyncMock)
async def test_read_bytes_utf8(mock_get, config):
content = b"hello world"
mock_get.return_value = {"content": base64.b64encode(content).decode()}
result = await read_bytes(config, "acme", "proj", "sha123")
assert result == content
assert result.decode("utf-8") == "hello world"
@pytest.mark.asyncio
@patch("mirage.core.github.read.github_get", new_callable=AsyncMock)
async def test_read_bytes_binary(mock_get, config):
content = bytes(range(256))
mock_get.return_value = {"content": base64.b64encode(content).decode()}
result = await read_bytes(config, "acme", "proj", "sha456")
assert result == content
+115
View File
@@ -0,0 +1,115 @@
# ========= 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 import defaultdict
from datetime import datetime, timedelta, timezone
from unittest.mock import MagicMock
import pytest
from mirage.cache.index import IndexEntry
from mirage.cache.index.ram import RAMIndexCacheStore
from mirage.core.github.readdir import readdir
from mirage.core.github.tree_entry import TreeEntry
from mirage.types import PathSpec
def _index_from_tree(tree: dict[str, TreeEntry]) -> RAMIndexCacheStore:
index = RAMIndexCacheStore()
dirs: dict[str, list[tuple[str, IndexEntry]]] = defaultdict(list)
for path, entry in tree.items():
parts = path.rsplit("/", 1)
if len(parts) == 2:
parent, name = "/" + parts[0], parts[1]
else:
parent, name = "/", parts[0]
resource_type = "folder" if entry.type == "tree" else "file"
idx_entry = IndexEntry(
id=entry.sha,
name=name,
resource_type=resource_type,
size=entry.size,
)
dirs[parent].append((name, idx_entry))
for parent, entries in dirs.items():
index._entries.update({
("/" + parent.strip("/") + "/" + name).replace("//", "/"):
e
for name, e in entries
})
child_keys = sorted(
("/" + parent.strip("/") + "/" + name).replace("//", "/")
for name, _ in entries)
index._children[parent] = child_keys
index._expiry[parent] = datetime.now(
timezone.utc) + timedelta(days=365)
return index
@pytest.fixture
def tree():
return {
"src":
TreeEntry(path="src", type="tree", sha="aaa", size=None),
"src/main.py":
TreeEntry(path="src/main.py", type="blob", sha="bbb", size=120),
"src/utils":
TreeEntry(path="src/utils", type="tree", sha="ccc", size=None),
"src/utils/helpers.py":
TreeEntry(path="src/utils/helpers.py", type="blob", sha="ddd",
size=80),
"README.md":
TreeEntry(path="README.md", type="blob", sha="eee", size=50),
}
@pytest.mark.asyncio
async def test_readdir_root(tree):
index = _index_from_tree(tree)
result = await readdir(
None, PathSpec(resource_path="", virtual="/", directory="/"), index)
assert result == ["/README.md", "/src"]
@pytest.mark.asyncio
async def test_readdir_subdirectory(tree):
index = _index_from_tree(tree)
result = await readdir(
None, PathSpec(resource_path="src", virtual="/src", directory="/src"),
index)
assert result == ["/src/main.py", "/src/utils"]
@pytest.mark.asyncio
async def test_readdir_nested(tree):
index = _index_from_tree(tree)
result = await readdir(
None,
PathSpec(resource_path="src/utils",
virtual="/src/utils",
directory="/src/utils"), index)
assert result == ["/src/utils/helpers.py"]
@pytest.mark.asyncio
async def test_readdir_missing_directory(tree):
index = _index_from_tree(tree)
accessor = MagicMock()
accessor.truncated = False
with pytest.raises(FileNotFoundError):
await readdir(
accessor,
PathSpec(resource_path="nonexistent",
virtual="/nonexistent",
directory="/nonexistent"), index)
+45
View File
@@ -0,0 +1,45 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from unittest.mock import AsyncMock, patch
import pytest
from mirage.core.github.config import GitHubConfig
from mirage.core.github.repo import fetch_default_branch
@pytest.fixture
def config():
return GitHubConfig(token="ghp_test")
@pytest.mark.asyncio
@patch("mirage.core.github.repo.github_get", new_callable=AsyncMock)
async def test_fetch_default_branch_main(mock_get, config):
mock_get.return_value = {"default_branch": "main"}
result = await fetch_default_branch(config, "acme", "proj")
assert result == "main"
mock_get.assert_awaited_once_with(config.token,
"/repos/{owner}/{repo}",
owner="acme",
repo="proj")
@pytest.mark.asyncio
@patch("mirage.core.github.repo.github_get", new_callable=AsyncMock)
async def test_fetch_default_branch_master(mock_get, config):
mock_get.return_value = {"default_branch": "master"}
result = await fetch_default_branch(config, "acme", "proj")
assert result == "master"
+77
View File
@@ -0,0 +1,77 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from types import SimpleNamespace
import pytest
from mirage.core.github.scope import (count_scope_files, is_repo_root,
scope_relative_key)
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
@pytest.fixture
def entries():
def _f():
return SimpleNamespace(resource_type="file")
def _d():
return SimpleNamespace(resource_type="folder")
return {
"/README.md": _f(),
"/src": _d(),
"/src/main.py": _f(),
"/src/utils.py": _f(),
"/src/models": _d(),
"/src/models/user.py": _f(),
}
def test_scope_relative_key_strips_mount_prefix():
path = PathSpec(resource_path=mount_key("/gh/src", "/gh"),
virtual="/gh/src",
directory="/gh/src")
assert scope_relative_key(path) == "/src"
def test_scope_relative_key_root_becomes_slash():
path = PathSpec(resource_path=mount_key("/gh", "/gh"),
virtual="/gh",
directory="/gh")
assert scope_relative_key(path) == "/"
def test_is_repo_root():
assert is_repo_root("/")
assert is_repo_root("")
assert not is_repo_root("/src")
def test_count_scope_files_root_counts_all(entries):
assert count_scope_files(entries, "/") == 4
def test_count_scope_files_subdir(entries):
assert count_scope_files(entries, "/src") == 3
def test_count_scope_files_single_file(entries):
assert count_scope_files(entries, "/src/main.py") == 1
def test_count_scope_files_missing(entries):
assert count_scope_files(entries, "/nope") == 0
+117
View File
@@ -0,0 +1,117 @@
# ========= 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.core.github.config import GitHubConfig
from mirage.core.github.search import SearchResult, narrow_paths, search_code
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
@pytest.fixture
def config():
return GitHubConfig(token="ghp_test")
@pytest.mark.asyncio
@patch("mirage.core.github.search.github_get", new_callable=AsyncMock)
async def test_search_code_basic(mock_get, config):
mock_get.return_value = {
"items": [
{
"path": "src/main.py",
"sha": "aaa"
},
{
"path": "src/utils.py",
"sha": "bbb"
},
]
}
result = await search_code(config, "acme", "proj", "import os")
assert len(result) == 2
assert result[0] == SearchResult(path="src/main.py", sha="aaa")
mock_get.assert_awaited_once_with(config.token,
"/search/code",
params={"q": "import os repo:acme/proj"})
@pytest.mark.asyncio
@patch("mirage.core.github.search.github_get", new_callable=AsyncMock)
async def test_search_code_empty_results(mock_get, config):
mock_get.return_value = {"items": []}
result = await search_code(config, "acme", "proj", "nonexistent")
assert result == []
@pytest.mark.asyncio
@patch("mirage.core.github.search.github_get", new_callable=AsyncMock)
async def test_search_code_with_path_filter(mock_get, config):
mock_get.return_value = {"items": [{"path": "src/main.py", "sha": "aaa"}]}
await search_code(config, "acme", "proj", "import os", path_filter="src/")
mock_get.assert_awaited_once_with(
config.token,
"/search/code",
params={"q": "import os repo:acme/proj path:src/"})
@pytest.mark.asyncio
@patch("mirage.core.github.search.search_code", new_callable=AsyncMock)
async def test_narrow_paths_strips_leading_slash_in_filter(
mock_search, config):
mock_search.return_value = [SearchResult(path="src/main.py", sha="aaa")]
paths = [PathSpec(resource_path="src", virtual="/src", directory="/src")]
await narrow_paths(config, "acme", "proj", "import", paths)
_, kwargs = mock_search.await_args
assert kwargs["path_filter"] == "src"
@pytest.mark.asyncio
@patch("mirage.core.github.search.search_code", new_callable=AsyncMock)
async def test_narrow_paths_root_uses_no_filter(mock_search, config):
mock_search.return_value = [SearchResult(path="src/main.py", sha="aaa")]
paths = [PathSpec(resource_path="", virtual="/", directory="/")]
await narrow_paths(config, "acme", "proj", "import", paths)
_, kwargs = mock_search.await_args
assert kwargs["path_filter"] is None
@pytest.mark.asyncio
@patch("mirage.core.github.search.search_code", new_callable=AsyncMock)
async def test_narrow_paths_normalizes_results_with_leading_slash(
mock_search, config):
mock_search.return_value = [
SearchResult(path="src/main.py", sha="aaa"),
SearchResult(path="src/utils.py", sha="bbb"),
]
paths = [
PathSpec(resource_path=mount_key("/gh", "/gh"),
virtual="/gh",
directory="/gh")
]
out = await narrow_paths(config, "acme", "proj", "import", paths)
assert [p.virtual for p in out] == ["/gh/src/main.py", "/gh/src/utils.py"]
assert [p.resource_path for p in out] == ["src/main.py", "src/utils.py"]
@pytest.mark.asyncio
@patch("mirage.core.github.search.search_code", new_callable=AsyncMock)
async def test_narrow_paths_logs_and_continues_on_error(mock_search, config):
mock_search.side_effect = RuntimeError("boom")
paths = [PathSpec(resource_path="src", virtual="/src", directory="/src")]
out = await narrow_paths(config, "acme", "proj", "import", paths)
assert out == []
+125
View File
@@ -0,0 +1,125 @@
# ========= 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 import defaultdict
from datetime import datetime, timedelta, timezone
import pytest
from mirage.cache.index import IndexEntry
from mirage.cache.index.ram import RAMIndexCacheStore
from mirage.core.github.stat import stat
from mirage.core.github.tree_entry import TreeEntry
from mirage.types import FileType, PathSpec
def _index_from_tree(tree: dict[str, TreeEntry]) -> RAMIndexCacheStore:
index = RAMIndexCacheStore()
dirs: dict[str, list[tuple[str, IndexEntry]]] = defaultdict(list)
for path, entry in tree.items():
parts = path.rsplit("/", 1)
if len(parts) == 2:
parent, name = "/" + parts[0], parts[1]
else:
parent, name = "/", parts[0]
resource_type = "folder" if entry.type == "tree" else "file"
idx_entry = IndexEntry(
id=entry.sha,
name=name,
resource_type=resource_type,
size=entry.size,
)
dirs[parent].append((name, idx_entry))
for parent, entries in dirs.items():
index._entries.update({
("/" + parent.strip("/") + "/" + name).replace("//", "/"):
e
for name, e in entries
})
child_keys = sorted(
("/" + parent.strip("/") + "/" + name).replace("//", "/")
for name, _ in entries)
index._children[parent] = child_keys
index._expiry[parent] = datetime.now(
timezone.utc) + timedelta(days=365)
return index
@pytest.fixture
def tree():
return {
"src":
TreeEntry(path="src", type="tree", sha="aaa", size=None),
"src/main.py":
TreeEntry(path="src/main.py", type="blob", sha="bbb", size=120),
"README.md":
TreeEntry(path="README.md", type="blob", sha="ccc", size=50),
}
@pytest.mark.asyncio
async def test_stat_file(tree):
index = _index_from_tree(tree)
result = await stat(
None,
PathSpec(resource_path="src/main.py",
virtual="/src/main.py",
directory="/src/main.py"), index)
assert result.name == "main.py"
assert result.size == 120
assert result.type == FileType.TEXT
assert result.extra == {"sha": "bbb"}
@pytest.mark.asyncio
async def test_stat_directory(tree):
index = _index_from_tree(tree)
result = await stat(
None, PathSpec(resource_path="src", virtual="/src", directory="/src"),
index)
assert result.name == "src"
assert result.type == FileType.DIRECTORY
@pytest.mark.asyncio
async def test_stat_root(tree):
index = _index_from_tree(tree)
result = await stat(None,
PathSpec(resource_path="", virtual="/", directory="/"),
index)
assert result.name == "/"
assert result.type == FileType.DIRECTORY
@pytest.mark.asyncio
async def test_stat_not_found(tree):
index = _index_from_tree(tree)
with pytest.raises(FileNotFoundError):
await stat(
None,
PathSpec(resource_path="nonexistent.py",
virtual="/nonexistent.py",
directory="/nonexistent.py"), index)
@pytest.mark.asyncio
async def test_stat_strip_slashes(tree):
index = _index_from_tree(tree)
result = await stat(
None,
PathSpec(resource_path="README.md",
virtual="/README.md",
directory="/README.md"), index)
assert result.name == "README.md"
assert result.size == 50
@@ -0,0 +1,66 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from datetime import datetime, timedelta, timezone
import pytest
from mirage.cache.index import IndexEntry
from mirage.cache.index.ram import RAMIndexCacheStore
from mirage.core.github.stat import stat
from mirage.types import PathSpec
@pytest.mark.asyncio
async def test_github_stat_returns_fingerprint_from_blob_sha():
index = RAMIndexCacheStore()
entry = IndexEntry(
id="abc123",
name="main.py",
resource_type="file",
size=42,
)
index._entries["/src/main.py"] = entry
index._children["/src"] = ["/src/main.py"]
index._expiry["/src"] = datetime.now(timezone.utc) + timedelta(days=365)
result = await stat(
None,
PathSpec(resource_path="src/main.py",
virtual="/src/main.py",
directory="/src/main.py"),
index,
)
assert result.fingerprint == "abc123"
assert result.extra == {"sha": "abc123"}
@pytest.mark.asyncio
async def test_github_stat_directory_has_no_fingerprint():
index = RAMIndexCacheStore()
entry = IndexEntry(
id="dir_sha",
name="src",
resource_type="folder",
)
index._entries["/src"] = entry
result = await stat(
None,
PathSpec(resource_path="src", virtual="/src", directory="/src"),
index,
)
assert result.fingerprint is None
+85
View File
@@ -0,0 +1,85 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import logging
from unittest.mock import AsyncMock, patch
import pytest
from mirage.core.github.config import GitHubConfig
from mirage.core.github.tree import fetch_tree
from mirage.core.github.tree_entry import TreeEntry
@pytest.fixture
def config():
return GitHubConfig(token="ghp_test")
@pytest.mark.asyncio
@patch("mirage.core.github.tree.github_get", new_callable=AsyncMock)
async def test_fetch_tree_parses_entries(mock_get, config):
mock_get.return_value = {
"truncated":
False,
"tree": [
{
"path": "src",
"type": "tree",
"sha": "aaa",
"size": None
},
{
"path": "src/main.py",
"type": "blob",
"sha": "bbb",
"size": 120
},
],
}
tree, truncated = await fetch_tree(config, "acme", "proj", "main")
assert "src" in tree
assert "src/main.py" in tree
assert tree["src"] == TreeEntry(path="src",
type="tree",
sha="aaa",
size=None)
assert tree["src/main.py"] == TreeEntry(path="src/main.py",
type="blob",
sha="bbb",
size=120)
@pytest.mark.asyncio
@patch("mirage.core.github.tree.github_get", new_callable=AsyncMock)
async def test_fetch_tree_truncation_warning(mock_get, config, caplog):
mock_get.return_value = {"truncated": True, "tree": []}
with caplog.at_level(logging.WARNING):
await fetch_tree(config, "acme", "proj", "main")
assert "truncated" in caplog.text
@pytest.mark.asyncio
@patch("mirage.core.github.tree.github_get", new_callable=AsyncMock)
async def test_fetch_tree_passes_params(mock_get, config):
mock_get.return_value = {"tree": []}
await fetch_tree(config, "acme", "proj", "v1")
mock_get.assert_awaited_once_with(
config.token,
"/repos/{owner}/{repo}/git/trees/{ref}",
owner="acme",
repo="proj",
ref="v1",
params={"recursive": "1"},
)
+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. =========

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