bcbd1bdb22
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
343 lines
11 KiB
Python
343 lines
11 KiB
Python
# ========= 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 datetime as dt
|
|
import json
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import pytest
|
|
from bson import Decimal128, ObjectId
|
|
|
|
from mirage.accessor.mongodb import MongoDBAccessor
|
|
from mirage.cache.index.ram import RAMIndexCacheStore
|
|
from mirage.core.mongodb.stream import read_stream, read_tail, watch_stream
|
|
from mirage.resource.mongodb.config import MongoDBConfig
|
|
from mirage.types import PathSpec
|
|
|
|
DOCS_PATH = "/db1/collections/coll1/documents.jsonl"
|
|
VIEW_DOCS_PATH = "/db1/views/myview/documents.jsonl"
|
|
|
|
|
|
async def _gen(items):
|
|
for item in items:
|
|
yield item
|
|
|
|
|
|
@pytest.fixture
|
|
def index():
|
|
return RAMIndexCacheStore()
|
|
|
|
|
|
@pytest.fixture
|
|
def accessor():
|
|
return MongoDBAccessor(config=MongoDBConfig(
|
|
uri="mongodb://localhost:27017"))
|
|
|
|
|
|
def _patched_iter(docs):
|
|
return patch("mirage.core.mongodb.stream.iter_documents",
|
|
new=lambda *args, **kwargs: _gen(docs))
|
|
|
|
|
|
def _patched_watch(docs):
|
|
return patch("mirage.core.mongodb.stream.iter_inserts",
|
|
new=lambda *args, **kwargs: _gen(docs))
|
|
|
|
|
|
def _path(s: str) -> PathSpec:
|
|
return PathSpec(virtual=s, directory=s, resource_path=s.strip("/"))
|
|
|
|
|
|
async def _collect(gen):
|
|
chunks = []
|
|
async for chunk in gen:
|
|
chunks.append(chunk)
|
|
return b"".join(chunks)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_read_stream_yields_one_jsonl_line_per_doc(accessor, index):
|
|
oid_a, oid_b = ObjectId(), ObjectId()
|
|
docs = [{"_id": oid_a, "title": "A"}, {"_id": oid_b, "title": "B"}]
|
|
with _patched_iter(docs):
|
|
data = await _collect(read_stream(accessor, _path(DOCS_PATH), index))
|
|
lines = [line for line in data.decode().split("\n") if line]
|
|
assert len(lines) == 2
|
|
first = json.loads(lines[0])
|
|
assert first["title"] == "A"
|
|
assert first["_id"] == {"$oid": str(oid_a)}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_read_stream_preserves_bson_types_via_extended_json(
|
|
accessor, index):
|
|
docs = [{
|
|
"_id":
|
|
ObjectId("65f0000000000000000000a1"),
|
|
"date":
|
|
dt.datetime(2026, 5, 15, 12, 30, 45, tzinfo=dt.timezone.utc),
|
|
"decimal":
|
|
Decimal128("123.456"),
|
|
}]
|
|
with _patched_iter(docs):
|
|
data = await _collect(read_stream(accessor, _path(DOCS_PATH), index))
|
|
parsed = json.loads(data.decode().strip())
|
|
assert parsed["_id"] == {"$oid": "65f0000000000000000000a1"}
|
|
assert "$date" in parsed["date"]
|
|
assert parsed["decimal"] == {"$numberDecimal": "123.456"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_read_stream_empty_yields_nothing(accessor, index):
|
|
with _patched_iter([]):
|
|
chunks = []
|
|
async for chunk in read_stream(accessor, _path(DOCS_PATH), index):
|
|
chunks.append(chunk)
|
|
assert chunks == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_read_stream_short_circuits_when_consumer_closes(
|
|
accessor, index):
|
|
consumed: list[int] = []
|
|
|
|
async def _instrumented(*_args, **_kwargs):
|
|
for i in range(1000):
|
|
consumed.append(i)
|
|
yield {"_id": ObjectId(), "i": i}
|
|
|
|
with patch("mirage.core.mongodb.stream.iter_documents", new=_instrumented):
|
|
gen = read_stream(accessor, _path(DOCS_PATH), index)
|
|
await gen.__anext__()
|
|
await gen.aclose()
|
|
assert len(consumed) <= 2
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_read_stream_works_on_view_documents_path(accessor, index):
|
|
oid = ObjectId()
|
|
docs = [{"_id": oid, "v": 1}]
|
|
with _patched_iter(docs):
|
|
data = await _collect(
|
|
read_stream(accessor, _path(VIEW_DOCS_PATH), index))
|
|
parsed = json.loads(data.decode().strip())
|
|
assert parsed["v"] == 1
|
|
assert parsed["_id"] == {"$oid": str(oid)}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_read_stream_directory_path_raises(accessor, index):
|
|
with _patched_iter([]):
|
|
with pytest.raises(FileNotFoundError):
|
|
async for _ in read_stream(accessor, _path("/db1"), index):
|
|
pass
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_read_stream_schema_json_path_raises(accessor, index):
|
|
with _patched_iter([]):
|
|
with pytest.raises(FileNotFoundError):
|
|
async for _ in read_stream(
|
|
accessor, _path("/db1/collections/coll1/schema.json"),
|
|
index):
|
|
pass
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_read_stream_elides_configured_top_level_field(index):
|
|
cfg = MongoDBConfig(
|
|
uri="mongodb://localhost:27017",
|
|
elide_fields={"db1.coll1": ["vector"]},
|
|
)
|
|
acc = MongoDBAccessor(config=cfg)
|
|
oid = ObjectId()
|
|
docs = [{"_id": oid, "title": "hi", "vector": [0.1, 0.2, 0.3, 0.4, 0.5]}]
|
|
with _patched_iter(docs):
|
|
data = await _collect(read_stream(acc, _path(DOCS_PATH), index))
|
|
parsed = json.loads(data.decode().strip())
|
|
assert parsed["title"] == "hi"
|
|
assert "vector" not in parsed
|
|
assert parsed["_id"] == {"$oid": str(oid)}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_read_stream_elides_configured_nested_path(index):
|
|
cfg = MongoDBConfig(
|
|
uri="mongodb://localhost:27017",
|
|
elide_fields={"db1.coll1": ["metadata.embedding"]},
|
|
)
|
|
acc = MongoDBAccessor(config=cfg)
|
|
docs = [{
|
|
"_id": ObjectId(),
|
|
"metadata": {
|
|
"tag": "alpha",
|
|
"embedding": [0.1] * 1024,
|
|
},
|
|
}]
|
|
with _patched_iter(docs):
|
|
data = await _collect(read_stream(acc, _path(DOCS_PATH), index))
|
|
parsed = json.loads(data.decode().strip())
|
|
assert parsed["metadata"]["tag"] == "alpha"
|
|
assert "embedding" not in parsed["metadata"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_read_stream_elision_isolated_to_configured_collection(index):
|
|
cfg = MongoDBConfig(
|
|
uri="mongodb://localhost:27017",
|
|
elide_fields={"db1.other_coll": ["vector"]},
|
|
)
|
|
acc = MongoDBAccessor(config=cfg)
|
|
docs = [{"_id": ObjectId(), "vector": [1.0, 2.0]}]
|
|
with _patched_iter(docs):
|
|
data = await _collect(read_stream(acc, _path(DOCS_PATH), index))
|
|
parsed = json.loads(data.decode().strip())
|
|
assert parsed["vector"] == [1.0, 2.0]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_watch_stream_yields_one_jsonl_line_per_insert(accessor, index):
|
|
oid_a, oid_b = ObjectId(), ObjectId()
|
|
docs = [{"_id": oid_a, "title": "A"}, {"_id": oid_b, "title": "B"}]
|
|
with _patched_watch(docs):
|
|
data = await _collect(watch_stream(accessor, _path(DOCS_PATH), index))
|
|
lines = [line for line in data.decode().split("\n") if line]
|
|
assert len(lines) == 2
|
|
assert json.loads(lines[0])["_id"] == {"$oid": str(oid_a)}
|
|
assert json.loads(lines[1])["title"] == "B"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_watch_stream_preserves_bson_types(accessor, index):
|
|
docs = [{
|
|
"_id":
|
|
ObjectId("65f0000000000000000000a1"),
|
|
"date":
|
|
dt.datetime(2026, 5, 15, 12, 30, 45, tzinfo=dt.timezone.utc),
|
|
"decimal":
|
|
Decimal128("123.456"),
|
|
}]
|
|
with _patched_watch(docs):
|
|
data = await _collect(watch_stream(accessor, _path(DOCS_PATH), index))
|
|
parsed = json.loads(data.decode().strip())
|
|
assert parsed["_id"] == {"$oid": "65f0000000000000000000a1"}
|
|
assert "$date" in parsed["date"]
|
|
assert parsed["decimal"] == {"$numberDecimal": "123.456"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_watch_stream_empty_yields_nothing(accessor, index):
|
|
with _patched_watch([]):
|
|
chunks = []
|
|
async for chunk in watch_stream(accessor, _path(DOCS_PATH), index):
|
|
chunks.append(chunk)
|
|
assert chunks == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_watch_stream_short_circuits_when_consumer_closes(
|
|
accessor, index):
|
|
consumed: list[int] = []
|
|
|
|
async def _instrumented(*_args, **_kwargs):
|
|
for i in range(1000):
|
|
consumed.append(i)
|
|
yield {"_id": ObjectId(), "i": i}
|
|
|
|
with patch("mirage.core.mongodb.stream.iter_inserts", new=_instrumented):
|
|
gen = watch_stream(accessor, _path(DOCS_PATH), index)
|
|
await gen.__anext__()
|
|
await gen.aclose()
|
|
assert len(consumed) <= 2
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_watch_stream_directory_path_raises(accessor, index):
|
|
with _patched_watch([]):
|
|
with pytest.raises(FileNotFoundError):
|
|
async for _ in watch_stream(accessor, _path("/db1"), index):
|
|
pass
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_read_tail_returns_docs_in_ascending_order(accessor):
|
|
docs = [{"_id": 5, "name": "e"}, {"_id": 4, "name": "d"}]
|
|
with patch(
|
|
"mirage.core.mongodb.stream.find_documents",
|
|
new_callable=AsyncMock,
|
|
return_value=list(docs),
|
|
) as fake:
|
|
data = await read_tail(accessor, _path(DOCS_PATH), 2)
|
|
lines = data.decode().splitlines()
|
|
assert '"_id": 4' in lines[0]
|
|
assert '"_id": 5' in lines[1]
|
|
assert data.endswith(b"\n")
|
|
assert fake.await_args.kwargs["limit"] == 2
|
|
assert fake.await_args.kwargs["sort"] == [("_id", -1)]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_read_tail_caps_limit_at_max_doc_limit(accessor):
|
|
with patch(
|
|
"mirage.core.mongodb.stream.find_documents",
|
|
new_callable=AsyncMock,
|
|
return_value=[],
|
|
) as fake:
|
|
data = await read_tail(accessor, _path(DOCS_PATH), 10**9)
|
|
assert data == b""
|
|
assert fake.await_args.kwargs["limit"] == accessor.config.max_doc_limit
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_read_tail_applies_elision(index):
|
|
cfg = MongoDBConfig(
|
|
uri="mongodb://localhost:27017",
|
|
elide_fields={"db1.coll1": ["vector"]},
|
|
)
|
|
acc = MongoDBAccessor(config=cfg)
|
|
docs = [{"_id": 1, "title": "hi", "vector": [0.1, 0.2]}]
|
|
with patch(
|
|
"mirage.core.mongodb.stream.find_documents",
|
|
new_callable=AsyncMock,
|
|
return_value=docs,
|
|
):
|
|
data = await read_tail(acc, _path(DOCS_PATH), 1)
|
|
parsed = json.loads(data.decode().strip())
|
|
assert parsed["title"] == "hi"
|
|
assert "vector" not in parsed
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_read_tail_rejects_non_documents_path(accessor):
|
|
with pytest.raises(FileNotFoundError):
|
|
await read_tail(accessor, _path("/db1/collections/coll1/schema.json"),
|
|
5)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_watch_stream_applies_elision(index):
|
|
cfg = MongoDBConfig(
|
|
uri="mongodb://localhost:27017",
|
|
elide_fields={"db1.coll1": ["vector"]},
|
|
)
|
|
acc = MongoDBAccessor(config=cfg)
|
|
oid = ObjectId()
|
|
docs = [{"_id": oid, "title": "live", "vector": [0.1, 0.2, 0.3]}]
|
|
with _patched_watch(docs):
|
|
data = await _collect(watch_stream(acc, _path(DOCS_PATH), index))
|
|
parsed = json.loads(data.decode().strip())
|
|
assert parsed["title"] == "live"
|
|
assert "vector" not in parsed
|
|
assert parsed["_id"] == {"$oid": str(oid)}
|