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. =========
@@ -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. =========
import base64
from unittest.mock import AsyncMock, patch
import pytest
from mirage.core.gmail.messages import _extract_attachments, get_attachment
def test_extract_attachments_none():
payload = {"mimeType": "text/plain", "body": {"data": "test"}}
assert _extract_attachments(payload) == []
def test_extract_attachments_single():
payload = {
"mimeType":
"multipart/mixed",
"parts": [
{
"mimeType": "text/plain",
"body": {
"data": "text"
}
},
{
"filename": "report.pdf",
"body": {
"attachmentId": "att1",
"size": 1024
},
},
],
}
result = _extract_attachments(payload)
assert len(result) == 1
assert result[0]["filename"] == "report.pdf"
assert result[0]["attachment_id"] == "att1"
assert result[0]["size"] == 1024
def test_extract_attachments_nested():
payload = {
"mimeType":
"multipart/mixed",
"parts": [{
"mimeType":
"multipart/alternative",
"parts": [
{
"filename": "image.png",
"body": {
"attachmentId": "att2",
"size": 2048
},
},
],
}],
}
result = _extract_attachments(payload)
assert len(result) == 1
assert result[0]["filename"] == "image.png"
@pytest.mark.asyncio
async def test_get_attachment():
encoded = base64.urlsafe_b64encode(b"hello world").decode().rstrip("=")
with patch(
"mirage.core.gmail.messages.google_get",
new_callable=AsyncMock,
return_value={"data": encoded},
):
result = await get_attachment(None, "msg1", "att1")
assert result == b"hello world"
@@ -0,0 +1,40 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import pytest
from mirage.core.gmail.date_query import date_dir_to_gmail_query
@pytest.mark.parametrize("name,expected", [
("2026-05-03", "after:2026/05/03 before:2026/05/04"),
("2026-12-31", "after:2026/12/31 before:2027/01/01"),
("2026-01-01", "after:2026/01/01 before:2026/01/02"),
])
def test_date_dir_to_gmail_query_translates(name, expected):
assert date_dir_to_gmail_query(name) == expected
@pytest.mark.parametrize("name", [
"",
"2026-13-01",
"2026-02-30",
"2026-5-3",
"2026",
"not-a-date",
"2026-05",
"2026-05-03-extra",
])
def test_date_dir_to_gmail_query_rejects(name):
assert date_dir_to_gmail_query(name) is None
+72
View File
@@ -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. =========
from unittest.mock import AsyncMock, patch
import pytest
from mirage.core.gmail.labels import list_labels
from mirage.core.google._client import TokenManager
from mirage.core.google.config import GoogleConfig
@pytest.fixture
def token_manager():
config = GoogleConfig(
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_list_labels(token_manager):
api_response = {
"labels": [
{
"id": "INBOX",
"name": "INBOX",
"type": "system"
},
{
"id": "Label_1",
"name": "Work",
"type": "user"
},
]
}
with patch(
"mirage.core.gmail.labels.google_get",
new_callable=AsyncMock,
return_value=api_response,
):
result = await list_labels(token_manager)
assert len(result) == 2
assert result[0]["id"] == "INBOX"
assert result[1]["name"] == "Work"
@pytest.mark.asyncio
async def test_list_labels_empty(token_manager):
with patch(
"mirage.core.gmail.labels.google_get",
new_callable=AsyncMock,
return_value={},
):
result = await list_labels(token_manager)
assert result == []
+234
View File
@@ -0,0 +1,234 @@
# ========= 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.gmail.messages import (_decode_body, _extract_header,
_parse_address, _parse_address_list,
get_message_processed, get_message_raw,
list_messages)
from mirage.core.google._client import TokenManager
from mirage.core.google.config import GoogleConfig
@pytest.fixture
def token_manager():
config = GoogleConfig(
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
def test_extract_header():
headers = [
{
"name": "From",
"value": "alice@example.com"
},
{
"name": "Subject",
"value": "Hello"
},
]
assert _extract_header(headers, "From") == "alice@example.com"
assert _extract_header(headers, "subject") == "Hello"
assert _extract_header(headers, "Missing") == ""
def test_parse_address_with_name():
result = _parse_address('"Alice Smith" <alice@example.com>')
assert result["name"] == "Alice Smith"
assert result["email"] == "alice@example.com"
def test_parse_address_email_only():
result = _parse_address("alice@example.com")
assert result["name"] == ""
assert result["email"] == "alice@example.com"
def test_parse_address_list():
result = _parse_address_list("alice@example.com, Bob <bob@example.com>")
assert len(result) == 2
assert result[0]["email"] == "alice@example.com"
assert result[1]["email"] == "bob@example.com"
def test_parse_address_list_empty():
assert _parse_address_list("") == []
def test_decode_body_plain():
import base64
text = "Hello, world!"
encoded = base64.urlsafe_b64encode(text.encode()).decode().rstrip("=")
payload = {
"mimeType": "text/plain",
"body": {
"data": encoded
},
}
assert _decode_body(payload) == text
def test_decode_body_multipart():
import base64
text = "Nested text"
encoded = base64.urlsafe_b64encode(text.encode()).decode().rstrip("=")
payload = {
"mimeType": "multipart/alternative",
"parts": [
{
"mimeType": "text/plain",
"body": {
"data": encoded
},
},
],
}
assert _decode_body(payload) == text
def test_decode_body_empty():
payload = {"mimeType": "text/html", "body": {"data": ""}}
assert _decode_body(payload) == ""
@pytest.mark.asyncio
async def test_list_messages(token_manager):
api_response = {
"messages": [
{
"id": "msg1",
"threadId": "t1"
},
{
"id": "msg2",
"threadId": "t2"
},
]
}
with patch(
"mirage.core.gmail.messages.google_get",
new_callable=AsyncMock,
return_value=api_response,
):
result = await list_messages(token_manager, label_id="INBOX")
assert len(result) == 2
assert result[0]["id"] == "msg1"
@pytest.mark.asyncio
async def test_get_message_raw(token_manager):
msg = {"id": "msg1", "payload": {"headers": []}}
with patch(
"mirage.core.gmail.messages.google_get",
new_callable=AsyncMock,
return_value=msg,
):
result = await get_message_raw(token_manager, "msg1")
assert result["id"] == "msg1"
@pytest.mark.asyncio
async def test_get_message_processed(token_manager):
import base64
body_text = "Hello!"
encoded = base64.urlsafe_b64encode(body_text.encode()).decode().rstrip("=")
msg = {
"id": "msg1",
"threadId": "t1",
"snippet": "Hello!",
"labelIds": ["INBOX"],
"payload": {
"mimeType":
"text/plain",
"body": {
"data": encoded
},
"headers": [
{
"name": "From",
"value": "alice@example.com"
},
{
"name": "To",
"value": "bob@example.com"
},
{
"name": "Subject",
"value": "Test"
},
{
"name": "Date",
"value": "Mon, 1 Apr 2026 00:00:00 +0000"
},
],
},
}
with patch(
"mirage.core.gmail.messages.google_get",
new_callable=AsyncMock,
return_value=msg,
):
result = await get_message_processed(token_manager, "msg1")
assert result["id"] == "msg1"
assert result["subject"] == "Test"
assert result["body_text"] == body_text
assert result["from"]["email"] == "alice@example.com"
assert result["attachments"] == []
@pytest.mark.asyncio
async def test_get_message_processed_includes_attachment_paths(token_manager):
msg = {
"id": "msg2",
"threadId": "t2",
"labelIds": [],
"snippet": "",
"payload": {
"headers": [{
"name": "Subject",
"value": "With Attach"
}],
"parts": [{
"filename": "invoice.pdf",
"mimeType": "application/pdf",
"body": {
"attachmentId": "att-xyz",
"size": 4096,
},
}],
},
}
with patch(
"mirage.core.gmail.messages.google_get",
new_callable=AsyncMock,
return_value=msg,
):
result = await get_message_processed(token_manager, "msg2")
assert result["attachments"] == [{
"id": "att-xyz",
"filename": "invoice.pdf",
"path": "attachments/att-xyz_invoice.pdf",
"mime_type": "application/pdf",
"size": 4096,
}]
+216
View File
@@ -0,0 +1,216 @@
# ========= 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.gmail import GmailAccessor
from mirage.cache.index import IndexEntry, RAMIndexCacheStore
from mirage.core.gmail.read import read
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
@pytest.fixture
def accessor():
return GmailAccessor(config=None, token_manager=None)
@pytest.fixture
def index():
return RAMIndexCacheStore()
@pytest.mark.asyncio
async def test_read_message(accessor, index):
await index.set_dir("/gmail/INBOX/2026-04-12", [
("Test_Email__msg1.gmail.json",
IndexEntry(
id="msg1",
name="Test Email",
resource_type="gmail/message",
vfs_name="Test_Email__msg1.gmail.json",
)),
])
processed = {"id": "msg1", "subject": "Test Email", "body_text": "Hello!"}
with patch(
"mirage.core.gmail.read.get_message_processed",
new_callable=AsyncMock,
return_value=processed,
):
result = await read(
accessor,
PathSpec(
resource_path=mount_key(
"/gmail/INBOX/2026-04-12"
"/Test_Email__msg1.gmail.json", "/gmail"),
virtual="/gmail/INBOX/2026-04-12"
"/Test_Email__msg1.gmail.json",
directory="/gmail/INBOX/2026-04-12"
"/Test_Email__msg1.gmail.json",
),
index,
)
parsed = json.loads(result)
assert parsed["id"] == "msg1"
assert parsed["subject"] == "Test Email"
@pytest.mark.asyncio
async def test_read_not_found(accessor, index):
with pytest.raises(FileNotFoundError):
await read(
accessor,
PathSpec(resource_path=mount_key(
"/gmail/INBOX/nonexistent.gmail.json", "/gmail"),
virtual="/gmail/INBOX/nonexistent.gmail.json",
directory="/gmail/INBOX/nonexistent.gmail.json"),
index,
)
@pytest.mark.asyncio
async def test_read_is_directory(accessor, index):
await index.set_dir("/gmail", [
("INBOX",
IndexEntry(
id="INBOX",
name="INBOX",
resource_type="gmail/label",
vfs_name="INBOX",
)),
])
with pytest.raises(IsADirectoryError):
await read(
accessor,
PathSpec(resource_path=mount_key("/gmail/INBOX", "/gmail"),
virtual="/gmail/INBOX",
directory="/gmail/INBOX"),
index,
)
@pytest.mark.asyncio
async def test_read_auto_bootstraps_from_empty_index(accessor, index):
raw_msg = {
"id": "msg-1",
"threadId": "t-1",
"internalDate": str(1777248000000),
"sizeEstimate": 1024,
"labelIds": ["INBOX"],
"snippet": "hello",
"payload": {
"headers": [{
"name": "Subject",
"value": "Hello World"
}],
},
}
processed = {"id": "msg-1", "subject": "Hello World", "body_text": "hi"}
with (
patch(
"mirage.core.gmail.readdir.list_labels",
new_callable=AsyncMock,
return_value=[{
"id": "INBOX",
"name": "INBOX",
"type": "system"
}],
),
patch(
"mirage.core.gmail.readdir.list_messages",
new_callable=AsyncMock,
return_value=[{
"id": "msg-1",
"threadId": "t-1"
}],
),
patch(
"mirage.core.gmail.readdir.get_message_raw",
new_callable=AsyncMock,
return_value=raw_msg,
),
patch(
"mirage.core.gmail.read.get_message_processed",
new_callable=AsyncMock,
return_value=processed,
),
):
result = await read(
accessor,
PathSpec(
resource_path=mount_key(
"/gmail/INBOX/2026-04-27"
"/Hello_World__msg-1.gmail.json", "/gmail"),
virtual="/gmail/INBOX/2026-04-27"
"/Hello_World__msg-1.gmail.json",
directory="/gmail/INBOX/2026-04-27"
"/Hello_World__msg-1.gmail.json",
),
index,
)
parsed = json.loads(result)
assert parsed["subject"] == "Hello World"
@pytest.mark.asyncio
async def test_read_attachment(accessor, index):
await index.set_dir("/gmail/INBOX/2026-04-12", [
("Meeting__msg1.gmail.json",
IndexEntry(
id="msg1",
name="Meeting",
resource_type="gmail/message",
vfs_name="Meeting__msg1.gmail.json",
)),
("Meeting__msg1",
IndexEntry(
id="msg1",
name="Meeting__msg1",
resource_type="gmail/attachment_dir",
vfs_name="Meeting__msg1",
)),
])
await index.set_dir(
"/gmail/INBOX/2026-04-12/Meeting__msg1",
[
("report.pdf",
IndexEntry(
id="att1",
name="report.pdf",
resource_type="gmail/attachment",
vfs_name="report.pdf",
size=1024,
)),
],
)
with patch(
"mirage.core.gmail.read.get_attachment",
new_callable=AsyncMock,
return_value=b"pdf-bytes",
):
result = await read(
accessor,
PathSpec(
resource_path=mount_key(
"/gmail/INBOX/2026-04-12/Meeting__msg1/report.pdf",
"/gmail"),
virtual="/gmail/INBOX/2026-04-12/Meeting__msg1/report.pdf",
directory="/gmail/INBOX/2026-04-12/Meeting__msg1/report.pdf",
),
index,
)
assert result == b"pdf-bytes"
+403
View File
@@ -0,0 +1,403 @@
# ========= 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.gmail import GmailAccessor
from mirage.cache.index import IndexEntry, RAMIndexCacheStore
from mirage.core.gmail.readdir import (_date_from_internal, _msg_filename,
_sanitize, readdir)
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
@pytest.fixture
def accessor():
return GmailAccessor(config=None, token_manager=None)
@pytest.fixture
def index():
return RAMIndexCacheStore()
def test_sanitize_normal():
assert _sanitize("Hello World") == "Hello_World"
def test_sanitize_empty():
assert _sanitize("") == "No_Subject"
assert _sanitize(" ") == "No_Subject"
def test_sanitize_special_chars():
result = _sanitize("Re: [Test] Hello!")
assert "!" not in result
assert "[" not in result
def test_sanitize_long():
long = "A" * 100
result = _sanitize(long)
assert len(result) <= 80
def test_msg_filename():
result = _msg_filename("Test Email", "msg123")
assert result == "Test_Email__msg123.gmail.json"
def test_date_from_internal():
assert _date_from_internal("1712966400000") == "2024-04-13"
@pytest.mark.asyncio
async def test_readdir_root(accessor, index):
await index.set_dir("/gmail", [
("INBOX",
IndexEntry(
id="INBOX",
name="INBOX",
resource_type="gmail/label",
vfs_name="INBOX",
)),
("SENT",
IndexEntry(
id="SENT",
name="SENT",
resource_type="gmail/label",
vfs_name="SENT",
)),
])
result = await readdir(
accessor,
PathSpec(resource_path="", virtual="/gmail", directory="/gmail"),
index)
assert "/gmail/INBOX" in result
assert "/gmail/SENT" in result
@pytest.mark.asyncio
async def test_readdir_label(accessor, index):
await index.set_dir("/gmail", [
("INBOX",
IndexEntry(
id="INBOX",
name="INBOX",
resource_type="gmail/label",
vfs_name="INBOX",
)),
])
await index.set_dir("/gmail/INBOX", [
("2026-04-12",
IndexEntry(
id="2026-04-12",
name="2026-04-12",
resource_type="gmail/date",
vfs_name="2026-04-12",
)),
])
result = await readdir(
accessor,
PathSpec(resource_path=mount_key("/gmail/INBOX", "/gmail"),
virtual="/gmail/INBOX",
directory="/gmail/INBOX"), index)
assert "/gmail/INBOX/2026-04-12" in result
@pytest.mark.asyncio
async def test_readdir_not_found(accessor, index):
with pytest.raises(FileNotFoundError):
await readdir(
accessor,
PathSpec(resource_path=mount_key("/gmail/NONEXISTENT", "/gmail"),
virtual="/gmail/NONEXISTENT",
directory="/gmail/NONEXISTENT"), index)
@pytest.mark.asyncio
async def test_readdir_date_dir_uses_after_before_query(accessor, index):
with patch("mirage.core.gmail.readdir.list_labels",
new_callable=AsyncMock,
return_value=[{
"id": "INBOX",
"type": "system"
}]):
await readdir(
accessor,
PathSpec(resource_path=mount_key("/gmail", "/gmail"),
virtual="/gmail",
directory="/gmail"), index)
captured_calls: list[dict] = []
async def fake_list_messages(token_manager,
label_id=None,
query=None,
max_results=50):
captured_calls.append({
"label_id": label_id,
"query": query,
"max_results": max_results,
})
return []
with patch("mirage.core.gmail.readdir.list_messages",
new=fake_list_messages):
result = await readdir(
accessor,
PathSpec(resource_path=mount_key("/gmail/INBOX/2026-05-03",
"/gmail"),
virtual="/gmail/INBOX/2026-05-03",
directory="/gmail/INBOX/2026-05-03"), index)
assert result == []
date_calls = [
c for c in captured_calls
if c["query"] and "after:2026/05/03" in c["query"]
]
assert len(date_calls) == 1
assert date_calls[0]["label_id"] == "INBOX"
assert "before:2026/05/04" in date_calls[0]["query"]
def _msg_stub(mid, subject, internal_date_ms):
return {
"id": mid,
"internalDate": str(internal_date_ms),
"payload": {
"headers": [{
"name": "Subject",
"value": subject
}],
},
}
@pytest.mark.asyncio
async def test_readdir_date_dir_returns_msg_files_not_date_strings(
accessor, index):
target_msgs = [{"id": "x1"}, {"id": "x2"}]
raws = {
"x1": _msg_stub("x1", "Hi 27", 1777291200000),
"x2": _msg_stub("x2", "Bye 27", 1777291200000 + 3600000),
}
async def fake_get_message_raw(_tm, mid):
return raws[mid]
async def fake_list_messages(_tm,
label_id=None,
query=None,
max_results=50):
return target_msgs
with (
patch("mirage.core.gmail.readdir.list_labels",
new_callable=AsyncMock,
return_value=[{
"id": "INBOX",
"type": "system"
}]),
patch("mirage.core.gmail.readdir.list_messages",
new=fake_list_messages),
patch("mirage.core.gmail.readdir.get_message_raw",
new=fake_get_message_raw),
):
result = await readdir(
accessor,
PathSpec(resource_path=mount_key("/gmail/INBOX/2026-04-27",
"/gmail"),
virtual="/gmail/INBOX/2026-04-27",
directory="/gmail/INBOX/2026-04-27"), index)
assert result == [
"/gmail/INBOX/2026-04-27/Hi_27__x1.gmail.json",
"/gmail/INBOX/2026-04-27/Bye_27__x2.gmail.json",
]
for entry in result:
basename = entry.rsplit("/", 1)[-1]
assert basename.endswith(".gmail.json"), (
f"expected .gmail.json file, got: {basename}")
@pytest.mark.asyncio
async def test_readdir_date_dir_warm_cache_matches_cold(accessor, index):
target_msgs = [{"id": "x1"}, {"id": "x2"}]
raws = {
"x1": _msg_stub("x1", "A", 1777291200000),
"x2": _msg_stub("x2", "B", 1777291200000 + 3600000),
}
async def fake_get_message_raw(_tm, mid):
return raws[mid]
fetch_count = {"n": 0}
async def fake_list_messages(_tm,
label_id=None,
query=None,
max_results=50):
fetch_count["n"] += 1
return target_msgs
with (
patch("mirage.core.gmail.readdir.list_labels",
new_callable=AsyncMock,
return_value=[{
"id": "INBOX",
"type": "system"
}]),
patch("mirage.core.gmail.readdir.list_messages",
new=fake_list_messages),
patch("mirage.core.gmail.readdir.get_message_raw",
new=fake_get_message_raw),
):
cold = await readdir(
accessor,
PathSpec(resource_path=mount_key("/gmail/INBOX/2026-04-27",
"/gmail"),
virtual="/gmail/INBOX/2026-04-27",
directory="/gmail/INBOX/2026-04-27"), index)
warm = await readdir(
accessor,
PathSpec(resource_path=mount_key("/gmail/INBOX/2026-04-27",
"/gmail"),
virtual="/gmail/INBOX/2026-04-27",
directory="/gmail/INBOX/2026-04-27"), index)
assert warm == cold
assert fetch_count["n"] == 1, "warm call should not re-fetch"
def _msg_with_attachment(mid, subject, internal_date_ms, att_id, att_filename):
return {
"id": mid,
"internalDate": str(internal_date_ms),
"payload": {
"headers": [{
"name": "Subject",
"value": subject
}],
"parts": [{
"filename": att_filename,
"mimeType": "application/pdf",
"body": {
"attachmentId": att_id,
"size": 1234,
},
}],
},
}
@pytest.mark.asyncio
async def test_readdir_date_dir_lists_msg_file_and_attachment_dir(
accessor, index):
raw = _msg_with_attachment("m1", "Quote", 1777291200000, "att1",
"quote.pdf")
async def fake_get_message_raw(_tm, mid):
return raw
async def fake_list_messages(_tm,
label_id=None,
query=None,
max_results=50):
return [{"id": "m1"}]
with (
patch("mirage.core.gmail.readdir.list_labels",
new_callable=AsyncMock,
return_value=[{
"id": "INBOX",
"type": "system"
}]),
patch("mirage.core.gmail.readdir.list_messages",
new=fake_list_messages),
patch("mirage.core.gmail.readdir.get_message_raw",
new=fake_get_message_raw),
):
date_listing = await readdir(
accessor,
PathSpec(resource_path=mount_key("/gmail/INBOX/2026-04-27",
"/gmail"),
virtual="/gmail/INBOX/2026-04-27",
directory="/gmail/INBOX/2026-04-27"), index)
att_listing = await readdir(
accessor,
PathSpec(
resource_path=mount_key("/gmail/INBOX/2026-04-27/Quote__m1",
"/gmail"),
virtual="/gmail/INBOX/2026-04-27/Quote__m1",
directory="/gmail/INBOX/2026-04-27/Quote__m1",
),
index,
)
assert date_listing == [
"/gmail/INBOX/2026-04-27/Quote__m1.gmail.json",
"/gmail/INBOX/2026-04-27/Quote__m1",
]
assert att_listing == [
"/gmail/INBOX/2026-04-27/Quote__m1/quote.pdf",
]
@pytest.mark.asyncio
async def test_readdir_date_dir_without_attachments_omits_dir(accessor, index):
raw = {
"id": "m1",
"internalDate": str(1777291200000),
"payload": {
"headers": [{
"name": "Subject",
"value": "No Attach"
}],
},
}
async def fake_get_message_raw(_tm, mid):
return raw
async def fake_list_messages(_tm,
label_id=None,
query=None,
max_results=50):
return [{"id": "m1"}]
with (
patch("mirage.core.gmail.readdir.list_labels",
new_callable=AsyncMock,
return_value=[{
"id": "INBOX",
"type": "system"
}]),
patch("mirage.core.gmail.readdir.list_messages",
new=fake_list_messages),
patch("mirage.core.gmail.readdir.get_message_raw",
new=fake_get_message_raw),
):
date_listing = await readdir(
accessor,
PathSpec(resource_path=mount_key("/gmail/INBOX/2026-04-27",
"/gmail"),
virtual="/gmail/INBOX/2026-04-27",
directory="/gmail/INBOX/2026-04-27"), index)
assert date_listing == [
"/gmail/INBOX/2026-04-27/No_Attach__m1.gmail.json",
]
+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. =========
from mirage.core.gmail.scope import detect_scope
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
def _gs(path: str,
prefix: str = "",
pattern: str | None = None,
directory: str | None = None) -> PathSpec:
return PathSpec(
resource_path=mount_key(path, prefix),
virtual=path,
directory=directory
or (path.rsplit("/", 1)[0] + "/" if "/" in path else "/"),
pattern=pattern,
)
def test_root_empty():
scope = detect_scope("/")
assert scope.use_native is True
assert scope.label_name is None
def test_root_with_prefix():
scope = detect_scope(_gs("/gmail/", prefix="/gmail"))
assert scope.use_native is True
def test_label_dir():
scope = detect_scope(_gs("/gmail/INBOX", prefix="/gmail"))
assert scope.use_native is True
assert scope.label_name == "INBOX"
assert scope.date_str is None
def test_label_date_dir():
scope = detect_scope(_gs("/gmail/INBOX/2026-04-10", prefix="/gmail"))
assert scope.use_native is True
assert scope.label_name == "INBOX"
assert scope.date_str == "2026-04-10"
def test_specific_message_file():
scope = detect_scope(
_gs("/gmail/INBOX/2026-04-10/Hello__abc123.gmail.json",
prefix="/gmail"))
assert scope.use_native is False
assert scope.label_name == "INBOX"
def test_attachment_path():
scope = detect_scope(
_gs("/gmail/INBOX/2026-04-10/Hello__abc123/file.pdf", prefix="/gmail"))
assert scope.use_native is False
def test_glob_in_date_dir():
spec = PathSpec(
resource_path=mount_key("/gmail/INBOX/2026-04-10/*.gmail.json",
"/gmail"),
virtual="/gmail/INBOX/2026-04-10/*.gmail.json",
directory="/gmail/INBOX/2026-04-10/",
pattern="*.gmail.json",
resolved=False,
)
scope = detect_scope(spec)
assert scope.use_native is True
assert scope.label_name == "INBOX"
assert scope.date_str == "2026-04-10"
+141
View File
@@ -0,0 +1,141 @@
# ========= 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.gmail.send import forward_message, reply_message, send_message
from mirage.core.google._client import TokenManager
from mirage.core.google.config import GoogleConfig
@pytest.fixture
def token_manager():
config = GoogleConfig(
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_send_message(token_manager):
with patch(
"mirage.core.gmail.send.google_post",
new_callable=AsyncMock,
return_value={
"id": "sent1",
"threadId": "t1"
},
) as mock_post:
result = await send_message(token_manager, "bob@example.com", "Hello",
"Hi Bob!")
assert result["id"] == "sent1"
call_args = mock_post.call_args
payload = call_args[0][2]
assert "raw" in payload
@pytest.mark.asyncio
async def test_reply_message(token_manager):
raw_msg = {
"id": "msg1",
"threadId": "t1",
"payload": {
"headers": [
{
"name": "From",
"value": "alice@example.com"
},
{
"name": "Subject",
"value": "Hello"
},
{
"name": "Message-ID",
"value": "<abc@example.com>"
},
],
},
}
with patch(
"mirage.core.gmail.send.get_message_raw",
new_callable=AsyncMock,
return_value=raw_msg,
), patch(
"mirage.core.gmail.send.google_post",
new_callable=AsyncMock,
return_value={
"id": "reply1",
"threadId": "t1"
},
) as mock_post:
result = await reply_message(token_manager, "msg1", "Thanks!")
assert result["id"] == "reply1"
payload = mock_post.call_args[0][2]
assert payload["threadId"] == "t1"
@pytest.mark.asyncio
async def test_forward_message(token_manager):
body_encoded = base64.urlsafe_b64encode(b"Original body").decode()
raw_msg = {
"id": "msg1",
"threadId": "t1",
"snippet": "Original body",
"labelIds": ["INBOX"],
"payload": {
"mimeType":
"text/plain",
"body": {
"data": body_encoded
},
"headers": [
{
"name": "From",
"value": "alice@example.com"
},
{
"name": "To",
"value": "bob@example.com"
},
{
"name": "Subject",
"value": "Hello"
},
{
"name": "Date",
"value": "Mon, 1 Apr 2026"
},
],
},
}
with patch(
"mirage.core.gmail.messages.google_get",
new_callable=AsyncMock,
return_value=raw_msg,
), patch(
"mirage.core.gmail.send.google_post",
new_callable=AsyncMock,
return_value={"id": "fwd1"},
):
result = await forward_message(token_manager, "msg1",
"charlie@example.com")
assert result["id"] == "fwd1"
+225
View File
@@ -0,0 +1,225 @@
# ========= 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.gmail import GmailAccessor
from mirage.cache.index import IndexEntry, RAMIndexCacheStore
from mirage.core.gmail.stat import stat
from mirage.types import FileType, PathSpec
from mirage.utils.key_prefix import mount_key
@pytest.fixture
def accessor():
return GmailAccessor(config=None, token_manager=None)
@pytest.fixture
def index():
return RAMIndexCacheStore()
async def _populate_index(idx):
await idx.set_dir("/gmail", [
("INBOX",
IndexEntry(
id="INBOX",
name="INBOX",
resource_type="gmail/label",
vfs_name="INBOX",
)),
])
await idx.set_dir("/gmail/INBOX", [
("2026-04-12",
IndexEntry(
id="2026-04-12",
name="2026-04-12",
resource_type="gmail/date",
vfs_name="2026-04-12",
)),
])
await idx.set_dir("/gmail/INBOX/2026-04-12", [
("Test_Email__msg1.gmail.json",
IndexEntry(
id="msg1",
name="Test Email",
resource_type="gmail/message",
vfs_name="Test_Email__msg1.gmail.json",
)),
("Test_Email__msg1",
IndexEntry(
id="msg1",
name="Test_Email__msg1",
resource_type="gmail/attachment_dir",
vfs_name="Test_Email__msg1",
)),
])
await idx.set_dir("/gmail/INBOX/2026-04-12/Test_Email__msg1", [
("image.png",
IndexEntry(
id="att1",
name="image.png",
resource_type="gmail/attachment",
vfs_name="image.png",
size=2048,
)),
])
@pytest.mark.asyncio
async def test_stat_root(accessor, index):
result = await stat(
accessor,
PathSpec(resource_path=mount_key("/", "/gmail"),
virtual="/",
directory="/"), index)
assert result.type == FileType.DIRECTORY
assert result.name == "/"
@pytest.mark.asyncio
async def test_stat_label(accessor, index):
await _populate_index(index)
result = await stat(
accessor,
PathSpec(resource_path=mount_key("/gmail/INBOX", "/gmail"),
virtual="/gmail/INBOX",
directory="/gmail/INBOX"), index)
assert result.type == FileType.DIRECTORY
assert result.name == "INBOX"
assert result.extra["label_id"] == "INBOX"
@pytest.mark.asyncio
async def test_stat_date(accessor, index):
await _populate_index(index)
result = await stat(
accessor,
PathSpec(resource_path=mount_key("/gmail/INBOX/2026-04-12", "/gmail"),
virtual="/gmail/INBOX/2026-04-12",
directory="/gmail/INBOX/2026-04-12"), index)
assert result.type == FileType.DIRECTORY
assert result.name == "2026-04-12"
@pytest.mark.asyncio
async def test_stat_message(accessor, index):
await _populate_index(index)
result = await stat(
accessor,
PathSpec(
resource_path=mount_key(
"/gmail/INBOX/2026-04-12/Test_Email__msg1.gmail.json",
"/gmail"),
virtual="/gmail/INBOX/2026-04-12/Test_Email__msg1.gmail.json",
directory="/gmail/INBOX/2026-04-12/Test_Email__msg1.gmail.json"),
index,
)
assert result.name == "Test_Email__msg1.gmail.json"
assert result.type == FileType.JSON
assert result.extra["message_id"] == "msg1"
@pytest.mark.asyncio
async def test_stat_attachment(accessor, index):
await _populate_index(index)
result = await stat(
accessor,
PathSpec(
resource_path=mount_key(
"/gmail/INBOX/2026-04-12/Test_Email__msg1/image.png",
"/gmail"),
virtual="/gmail/INBOX/2026-04-12/Test_Email__msg1/image.png",
directory="/gmail/INBOX/2026-04-12/Test_Email__msg1/image.png"),
index,
)
assert result.name == "image.png"
assert result.size == 2048
assert result.extra["attachment_id"] == "att1"
@pytest.mark.asyncio
async def test_stat_not_found(accessor, index):
await _populate_index(index)
with patch(
"mirage.core.gmail.readdir.list_labels",
new_callable=AsyncMock,
return_value=[],
):
with patch(
"mirage.core.gmail.readdir.list_messages",
new_callable=AsyncMock,
return_value=([], None),
):
with pytest.raises(FileNotFoundError):
await stat(
accessor,
PathSpec(resource_path=mount_key(
"/gmail/INBOX/nonexistent.gmail.json", "/gmail"),
virtual="/gmail/INBOX/nonexistent.gmail.json",
directory="/gmail/INBOX/nonexistent.gmail.json"),
index,
)
@pytest.mark.asyncio
async def test_stat_unknown_top_level_raises(accessor, index):
with patch(
"mirage.core.gmail.stat.list_labels",
new_callable=AsyncMock,
return_value=[{
"type": "system",
"id": "INBOX"
}],
):
with pytest.raises(FileNotFoundError):
await stat(
accessor,
PathSpec(resource_path=mount_key("/gmail/NoSuchLabel",
"/gmail"),
virtual="/gmail/NoSuchLabel",
directory="/gmail/NoSuchLabel"), index)
@pytest.mark.asyncio
async def test_stat_real_label_via_api(accessor, index):
with patch(
"mirage.core.gmail.stat.list_labels",
new_callable=AsyncMock,
return_value=[{
"type": "system",
"id": "STARRED"
}],
):
result = await stat(
accessor,
PathSpec(resource_path=mount_key("/gmail/STARRED", "/gmail"),
virtual="/gmail/STARRED",
directory="/gmail/STARRED"), index)
assert result.type == FileType.DIRECTORY
assert result.name == "STARRED"
@pytest.mark.asyncio
async def test_stat_index_none_raises(accessor):
with pytest.raises(FileNotFoundError):
await stat(
accessor,
PathSpec(resource_path=mount_key("/gmail/INBOX/x.gmail.json",
"/gmail"),
virtual="/gmail/INBOX/x.gmail.json",
directory="/gmail/INBOX/x.gmail.json"), None)