chore: import upstream snapshot with attribution
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
import base64
|
||||
from difflib import SequenceMatcher
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.resource.qdrant.config import QdrantConfig
|
||||
|
||||
COLLECTION = "animals"
|
||||
|
||||
_ROWS = [
|
||||
{
|
||||
"id": 1,
|
||||
"label": "cat",
|
||||
"kind": "big",
|
||||
"name": "a big orange cat"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"label": "cat",
|
||||
"kind": "small",
|
||||
"name": "a small grey cat"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"label": "dog",
|
||||
"kind": "big",
|
||||
"name": "a big brown dog"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"label": "dog",
|
||||
"kind": "small",
|
||||
"name": "a small white dog"
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def _points() -> list[SimpleNamespace]:
|
||||
points = []
|
||||
for row in _ROWS:
|
||||
payload = {
|
||||
"label": row["label"],
|
||||
"kind": row["kind"],
|
||||
"name": row["name"],
|
||||
"image_bytes":
|
||||
base64.b64encode(f"PNG-{row['id']}".encode()).decode(),
|
||||
}
|
||||
points.append(SimpleNamespace(id=row["id"], payload=payload))
|
||||
return points
|
||||
|
||||
|
||||
def _match(point: SimpleNamespace, scroll_filter) -> bool:
|
||||
if scroll_filter is None:
|
||||
return True
|
||||
for condition in scroll_filter.must:
|
||||
value = (point.payload or {}).get(condition.key)
|
||||
if str(value) != str(condition.match.value):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class FakeQdrantClient:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.points = _points()
|
||||
|
||||
async def get_collections(self):
|
||||
return SimpleNamespace(collections=[SimpleNamespace(name=COLLECTION)])
|
||||
|
||||
async def collection_exists(self, name: str) -> bool:
|
||||
return name == COLLECTION
|
||||
|
||||
async def scroll(self,
|
||||
collection_name,
|
||||
scroll_filter=None,
|
||||
limit=10,
|
||||
offset=None,
|
||||
with_payload=True,
|
||||
with_vectors=False):
|
||||
matched = [p for p in self.points if _match(p, scroll_filter)]
|
||||
start = offset or 0
|
||||
window = matched[start:start + limit]
|
||||
nxt = start + limit if start + limit < len(matched) else None
|
||||
return window, nxt
|
||||
|
||||
async def retrieve(self,
|
||||
collection_name,
|
||||
ids,
|
||||
with_payload=True,
|
||||
with_vectors=False):
|
||||
return [p for p in self.points if p.id in ids]
|
||||
|
||||
async def create_payload_index(self,
|
||||
collection_name,
|
||||
field_name,
|
||||
field_schema=None):
|
||||
pass
|
||||
|
||||
async def query_points(self,
|
||||
collection_name,
|
||||
query=None,
|
||||
limit=10,
|
||||
with_payload=True):
|
||||
text = query.text if query is not None else ""
|
||||
ranked = sorted(
|
||||
self.points,
|
||||
key=lambda p: SequenceMatcher(
|
||||
None, text, str((p.payload or {}).get("name", ""))).ratio(),
|
||||
reverse=True,
|
||||
)
|
||||
scored = []
|
||||
for point in ranked[:limit]:
|
||||
ratio = SequenceMatcher(None, text,
|
||||
str((point.payload
|
||||
or {}).get("name", ""))).ratio()
|
||||
scored.append(
|
||||
SimpleNamespace(id=point.id,
|
||||
payload=point.payload,
|
||||
score=ratio))
|
||||
return SimpleNamespace(points=scored)
|
||||
|
||||
|
||||
class FakeAccessor:
|
||||
|
||||
def __init__(self, config: QdrantConfig, client: FakeQdrantClient) -> None:
|
||||
self.config = config
|
||||
self._client = client
|
||||
self._search_cache: dict = {}
|
||||
self._indexes_ensured: set[str] = set()
|
||||
|
||||
async def client(self):
|
||||
return self._client
|
||||
|
||||
def cached_search(self, key):
|
||||
return self._search_cache.get(key)
|
||||
|
||||
def store_search(self, key, rows):
|
||||
self._search_cache[key] = rows
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def qdrant_config() -> QdrantConfig:
|
||||
return QdrantConfig(
|
||||
group_by=["label", "kind"],
|
||||
id_field="id",
|
||||
text_field="name",
|
||||
blob_field="image_bytes",
|
||||
blob_ext="png",
|
||||
vector_field="vector",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def accessor(qdrant_config) -> FakeAccessor:
|
||||
return FakeAccessor(qdrant_config, FakeQdrantClient())
|
||||
@@ -0,0 +1,72 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.commands.builtin.qdrant import COMMANDS
|
||||
from mirage.types import PathSpec
|
||||
|
||||
|
||||
def _find_command():
|
||||
for fn in COMMANDS:
|
||||
for rc in getattr(fn, "_registered_commands", []):
|
||||
if rc.name == "find" and rc.filetype is None:
|
||||
return fn
|
||||
raise AssertionError("factory find not registered for qdrant")
|
||||
|
||||
|
||||
def _spec(virtual: str) -> PathSpec:
|
||||
return PathSpec(virtual=virtual,
|
||||
directory=virtual,
|
||||
resource_path=virtual.strip("/"))
|
||||
|
||||
|
||||
async def _run(accessor, paths, *texts: str, **flags) -> list[str]:
|
||||
find = _find_command()
|
||||
stdout, _io = await find(accessor, paths, *texts, index=None, **flags)
|
||||
data = stdout if isinstance(stdout, bytes) else b""
|
||||
return data.decode().splitlines()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_plain_find_walks_groups_to_point_files(accessor):
|
||||
lines = await _run(accessor, [_spec("/")])
|
||||
assert "/animals/cat/big/1.json" in lines
|
||||
assert "/animals/cat/big/1.txt" in lines
|
||||
assert "/animals/cat/big/1.png" in lines
|
||||
assert "/animals/dog/small" in lines
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_iname_matches_rendered_text_files(accessor):
|
||||
lines = await _run(accessor, [_spec("/")], iname="*.TXT")
|
||||
assert lines
|
||||
assert all(line.endswith(".txt") for line in lines)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_type_split_uses_field_config_hint(accessor):
|
||||
dirs = await _run(accessor, [_spec("/")], type="d")
|
||||
assert "/animals/cat/big" in dirs
|
||||
assert all(not d.endswith((".json", ".txt", ".png")) for d in dirs)
|
||||
files = await _run(accessor, [_spec("/")], type="f")
|
||||
assert files
|
||||
assert all(f.endswith((".json", ".txt", ".png")) for f in files)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_negated_name_prunes_blobs(accessor):
|
||||
lines = await _run(accessor, [_spec("/")], "!", "-name", "*.png", type="f")
|
||||
assert lines
|
||||
assert all(not line.endswith(".png") for line in lines)
|
||||
@@ -0,0 +1,127 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from qdrant_client.http.exceptions import UnexpectedResponse
|
||||
|
||||
from mirage.core.qdrant import query
|
||||
from mirage.resource.qdrant.config import QdrantConfig
|
||||
|
||||
|
||||
def test_coerce_numeric_only():
|
||||
assert query._coerce("12") == 12
|
||||
assert query._coerce("-3") == -3
|
||||
assert query._coerce("cat") == "cat"
|
||||
|
||||
|
||||
def test_coerce_keeps_lossy_numeric_strings():
|
||||
assert query._coerce("007") == "007"
|
||||
assert query._coerce("05") == "05"
|
||||
assert query._coerce("-0") == "-0"
|
||||
|
||||
|
||||
def test_candidate_ids_by_type():
|
||||
assert query._candidate_ids("7") == [7]
|
||||
uid = "11111111-1111-1111-1111-111111111111"
|
||||
assert query._candidate_ids(uid) == [uid]
|
||||
assert query._candidate_ids("__nf_missing__") == []
|
||||
|
||||
|
||||
class _StrictClient:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.points = [
|
||||
SimpleNamespace(id=1, payload={
|
||||
"code": "100",
|
||||
"name": "a"
|
||||
}),
|
||||
SimpleNamespace(id=2, payload={
|
||||
"code": "200",
|
||||
"name": "b"
|
||||
}),
|
||||
]
|
||||
self.filtered_calls = 0
|
||||
self.index_calls = 0
|
||||
self._indexed = False
|
||||
|
||||
async def scroll(self,
|
||||
collection_name,
|
||||
scroll_filter=None,
|
||||
limit=10,
|
||||
offset=None,
|
||||
with_payload=True,
|
||||
with_vectors=False):
|
||||
if scroll_filter is not None and not self._indexed:
|
||||
self.filtered_calls += 1
|
||||
raise UnexpectedResponse(
|
||||
400, "Bad Request",
|
||||
b'{"status":{"error":"Index required but not found"}}', {})
|
||||
pts = self.points
|
||||
if scroll_filter is not None:
|
||||
conds = {c.key: c.match.value for c in scroll_filter.must}
|
||||
pts = [
|
||||
p for p in pts if all(
|
||||
str(p.payload.get(k)) == str(v) for k, v in conds.items())
|
||||
]
|
||||
start = offset or 0
|
||||
window = pts[start:start + limit]
|
||||
nxt = start + limit if start + limit < len(pts) else None
|
||||
return window, nxt
|
||||
|
||||
async def create_payload_index(self,
|
||||
collection_name,
|
||||
field_name,
|
||||
field_schema=None):
|
||||
self.index_calls += 1
|
||||
self._indexed = True
|
||||
|
||||
|
||||
class _StrictAccessor:
|
||||
|
||||
def __init__(self, client) -> None:
|
||||
self.config = QdrantConfig(collection="c",
|
||||
group_by=["code"],
|
||||
id_field="id",
|
||||
max_rows=1000)
|
||||
self._client = client
|
||||
self._indexes_ensured: set[str] = set()
|
||||
|
||||
async def client(self):
|
||||
return self._client
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_creates_indexes_on_index_required_then_retries():
|
||||
client = _StrictClient()
|
||||
accessor = _StrictAccessor(client)
|
||||
|
||||
rows = await query.rows_matching(accessor, "c", {"code": "100"}, 100)
|
||||
|
||||
assert [r["id"] for r in rows] == [1]
|
||||
assert client.filtered_calls == 1
|
||||
assert client.index_calls == 1
|
||||
assert "c" in accessor._indexes_ensured
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_does_not_recreate_indexes_on_subsequent_calls():
|
||||
client = _StrictClient()
|
||||
accessor = _StrictAccessor(client)
|
||||
|
||||
await query.distinct_values(accessor, "c", "code", {"code": "100"}, 100)
|
||||
await query.distinct_values(accessor, "c", "code", {"code": "100"}, 100)
|
||||
|
||||
assert client.index_calls == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_non_index_error_propagates():
|
||||
client = _StrictClient()
|
||||
|
||||
async def boom(**kwargs):
|
||||
raise UnexpectedResponse(500, "err", b"boom", {})
|
||||
|
||||
client.scroll = boom
|
||||
accessor = _StrictAccessor(client)
|
||||
|
||||
with pytest.raises(UnexpectedResponse):
|
||||
await query.rows_matching(accessor, "c", {"code": "100"}, 100)
|
||||
@@ -0,0 +1,41 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.core.qdrant.read import read
|
||||
from mirage.types import PathSpec
|
||||
|
||||
|
||||
def _ps(path: str) -> PathSpec:
|
||||
return PathSpec(virtual=path,
|
||||
directory=path,
|
||||
resource_path=path.strip("/"))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_json_returns_payload(accessor):
|
||||
data = (await read(accessor, _ps("/animals/cat/big/1.json"))).decode()
|
||||
payload = json.loads(data)
|
||||
assert payload["label"] == "cat"
|
||||
assert payload["name"] == "a big orange cat"
|
||||
assert payload["id"] == 1
|
||||
assert "vector" not in payload
|
||||
assert "image_bytes" not in payload
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_text_returns_source_text(accessor):
|
||||
data = (await read(accessor, _ps("/animals/cat/big/1.txt"))).decode()
|
||||
assert data == "a big orange cat\n"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_blob_returns_raw_bytes(accessor):
|
||||
data = await read(accessor, _ps("/animals/cat/big/1.png"))
|
||||
assert data == b"PNG-1"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_missing_row_raises(accessor):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
await read(accessor, _ps("/animals/cat/big/999.json"))
|
||||
@@ -0,0 +1,51 @@
|
||||
import pytest
|
||||
|
||||
from mirage.core.qdrant.readdir import is_dir_name, readdir
|
||||
from mirage.resource.qdrant.config import QdrantConfig
|
||||
from mirage.types import PathSpec
|
||||
|
||||
|
||||
def _ps(path: str) -> PathSpec:
|
||||
return PathSpec(virtual=path,
|
||||
directory=path,
|
||||
resource_path=path.strip("/"))
|
||||
|
||||
|
||||
def _names(paths: list[str]) -> set[str]:
|
||||
return {p.rsplit("/", 1)[-1] for p in paths}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_root_lists_collection(accessor):
|
||||
out = await readdir(accessor, _ps("/"))
|
||||
assert _names(out) == {"animals"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_collection_lists_groups(accessor):
|
||||
out = await readdir(accessor, _ps("/animals"))
|
||||
assert _names(out) == {"cat", "dog"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_group_lists_next_level(accessor):
|
||||
out = await readdir(accessor, _ps("/animals/cat"))
|
||||
assert _names(out) == {"big", "small"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_leaf_lists_row_files(accessor):
|
||||
out = await readdir(accessor, _ps("/animals/cat/big"))
|
||||
assert _names(out) == {"1.json", "1.txt", "1.png"}
|
||||
|
||||
|
||||
def test_is_dir_name_classifies_row_files():
|
||||
cfg = QdrantConfig(text_field="name", blob_field="img", blob_ext="png")
|
||||
assert is_dir_name("/animals/cat", config=cfg) is True
|
||||
assert is_dir_name("/animals/cat/big/1.json", config=cfg) is False
|
||||
assert is_dir_name("/animals/cat/big/1.txt", config=cfg) is False
|
||||
assert is_dir_name("/animals/cat/big/1.png", config=cfg) is False
|
||||
no_extra = QdrantConfig()
|
||||
assert is_dir_name("/animals/cat/big/1.png", config=no_extra) is True
|
||||
assert is_dir_name("/animals/cat/big/1.txt", config=no_extra) is True
|
||||
assert is_dir_name("/animals/cat/big/1.json", config=no_extra) is False
|
||||
@@ -0,0 +1,38 @@
|
||||
import json
|
||||
|
||||
from mirage.core.qdrant.render import render_json, render_text
|
||||
from mirage.resource.qdrant.config import QdrantConfig
|
||||
|
||||
|
||||
def _cfg() -> QdrantConfig:
|
||||
return QdrantConfig(id_field="id",
|
||||
text_field="name",
|
||||
blob_field="image_bytes",
|
||||
blob_ext="png",
|
||||
vector_field="vector")
|
||||
|
||||
|
||||
def test_render_json_omits_vector_and_blob():
|
||||
row = {
|
||||
"id": 3,
|
||||
"name": "a big brown dog",
|
||||
"label": "dog",
|
||||
"image_bytes": "UE5HLTM=",
|
||||
"vector": [0.1, 0.2],
|
||||
}
|
||||
payload = json.loads(render_json(row, _cfg()).decode())
|
||||
assert payload == {"id": 3, "name": "a big brown dog", "label": "dog"}
|
||||
|
||||
|
||||
def test_render_json_is_compact():
|
||||
out = render_json({"id": 1, "name": "x"}, _cfg()).decode()
|
||||
assert out == '{"id":1,"name":"x"}\n'
|
||||
|
||||
|
||||
def test_render_text_returns_source_text():
|
||||
out = render_text({"id": 3, "name": "a big brown dog"}, _cfg()).decode()
|
||||
assert out == "a big brown dog\n"
|
||||
|
||||
|
||||
def test_render_text_empty_when_field_missing():
|
||||
assert render_text({"id": 3}, _cfg()) == b""
|
||||
@@ -0,0 +1,73 @@
|
||||
from mirage.core.qdrant.scope import ScopeLevel, detect_scope
|
||||
from mirage.resource.qdrant.config import QdrantConfig
|
||||
from mirage.types import PathSpec
|
||||
|
||||
|
||||
def _cfg(**kw) -> QdrantConfig:
|
||||
base = dict(group_by=["label", "kind"],
|
||||
id_field="id",
|
||||
text_field="name",
|
||||
blob_field="image_bytes",
|
||||
blob_ext="png",
|
||||
vector_field="vector")
|
||||
base.update(kw)
|
||||
return QdrantConfig(**base)
|
||||
|
||||
|
||||
def _ps(path: str) -> PathSpec:
|
||||
return PathSpec(virtual=path,
|
||||
directory=path,
|
||||
resource_path=path.strip("/"))
|
||||
|
||||
|
||||
def test_root_multi_collection():
|
||||
s = detect_scope(_ps("/"), _cfg())
|
||||
assert s.level == ScopeLevel.ROOT
|
||||
|
||||
|
||||
def test_collection_group_dir():
|
||||
s = detect_scope(_ps("/animals"), _cfg())
|
||||
assert s.level == ScopeLevel.GROUP_DIR
|
||||
assert s.table == "animals"
|
||||
assert s.filters == {}
|
||||
|
||||
|
||||
def test_nested_group_dir():
|
||||
s = detect_scope(_ps("/animals/cat"), _cfg())
|
||||
assert s.level == ScopeLevel.GROUP_DIR
|
||||
assert s.filters == {"label": "cat"}
|
||||
|
||||
|
||||
def test_leaf_group_dir():
|
||||
s = detect_scope(_ps("/animals/cat/big"), _cfg())
|
||||
assert s.level == ScopeLevel.GROUP_DIR
|
||||
assert s.filters == {"label": "cat", "kind": "big"}
|
||||
|
||||
|
||||
def test_row_json():
|
||||
s = detect_scope(_ps("/animals/cat/big/3.json"), _cfg())
|
||||
assert s.level == ScopeLevel.ROW
|
||||
assert s.row_id == "3"
|
||||
assert s.kind == "json"
|
||||
assert s.filters == {"label": "cat", "kind": "big"}
|
||||
|
||||
|
||||
def test_row_text():
|
||||
s = detect_scope(_ps("/animals/cat/big/3.txt"), _cfg())
|
||||
assert s.level == ScopeLevel.ROW
|
||||
assert s.row_id == "3"
|
||||
assert s.kind == "txt"
|
||||
|
||||
|
||||
def test_row_blob():
|
||||
s = detect_scope(_ps("/animals/cat/big/3.png"), _cfg())
|
||||
assert s.level == ScopeLevel.ROW
|
||||
assert s.row_id == "3"
|
||||
assert s.kind == "blob"
|
||||
|
||||
|
||||
def test_single_collection_pin_elides_collection():
|
||||
s = detect_scope(_ps("/cat/big"), _cfg(collection="animals"))
|
||||
assert s.level == ScopeLevel.GROUP_DIR
|
||||
assert s.table == "animals"
|
||||
assert s.filters == {"label": "cat", "kind": "big"}
|
||||
@@ -0,0 +1,55 @@
|
||||
import pytest
|
||||
|
||||
from mirage.core.qdrant.search import search_rows_output
|
||||
from mirage.types import PathSpec
|
||||
from mirage.utils.key_prefix import mount_key
|
||||
|
||||
|
||||
def _ps(path: str) -> PathSpec:
|
||||
return PathSpec(resource_path=mount_key(path, "/db"),
|
||||
virtual=path,
|
||||
directory=path)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_emits_canonical_path_with_score(accessor):
|
||||
out = (await search_rows_output(accessor,
|
||||
"a small white dog", [_ps("/db/animals")],
|
||||
top_k=2,
|
||||
threshold=0.0,
|
||||
mount_prefix="/db")).decode()
|
||||
first = out.splitlines()[0]
|
||||
assert first.startswith("/db/animals/dog/small/4.txt:")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_body_is_source_text(accessor):
|
||||
out = (await search_rows_output(accessor,
|
||||
"a small white dog", [_ps("/db/animals")],
|
||||
top_k=1,
|
||||
threshold=0.0,
|
||||
mount_prefix="/db")).decode()
|
||||
assert "a small white dog" in out
|
||||
assert "label:" not in out
|
||||
assert "score:" not in out
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_top_k_limits_results(accessor):
|
||||
out = (await search_rows_output(accessor,
|
||||
"a small white dog", [_ps("/db/animals")],
|
||||
top_k=1,
|
||||
threshold=0.0,
|
||||
mount_prefix="/db")).decode()
|
||||
headers = [ln for ln in out.splitlines() if ln.startswith("/db/")]
|
||||
assert len(headers) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_empty_query_raises(accessor):
|
||||
with pytest.raises(ValueError):
|
||||
await search_rows_output(accessor,
|
||||
"", [_ps("/db/animals")],
|
||||
top_k=2,
|
||||
threshold=0.0,
|
||||
mount_prefix="/db")
|
||||
@@ -0,0 +1,44 @@
|
||||
import pytest
|
||||
|
||||
from mirage.core.qdrant.stat import stat
|
||||
from mirage.types import FileType, PathSpec
|
||||
|
||||
|
||||
def _ps(path: str) -> PathSpec:
|
||||
return PathSpec(virtual=path,
|
||||
directory=path,
|
||||
resource_path=path.strip("/"))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_group_dir_is_directory(accessor):
|
||||
s = await stat(accessor, _ps("/animals/cat"))
|
||||
assert s.type == FileType.DIRECTORY
|
||||
assert s.name == "cat"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_json_is_text_with_size(accessor):
|
||||
s = await stat(accessor, _ps("/animals/cat/big/1.json"))
|
||||
assert s.type == FileType.TEXT
|
||||
assert s.size and s.size > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_txt_is_text_with_size(accessor):
|
||||
s = await stat(accessor, _ps("/animals/cat/big/1.txt"))
|
||||
assert s.type == FileType.TEXT
|
||||
assert s.size and s.size > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_blob_is_image(accessor):
|
||||
s = await stat(accessor, _ps("/animals/cat/big/1.png"))
|
||||
assert s.type == FileType.IMAGE_PNG
|
||||
assert s.size == len(b"PNG-1")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_unknown_raises(accessor):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
await stat(accessor, _ps("/animals/cat/big/1.weird/x"))
|
||||
Reference in New Issue
Block a user