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,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,31 @@
|
||||
# ========= 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.notion._client import NotionAPIError, notion_headers
|
||||
from mirage.resource.notion.config import NotionConfig
|
||||
|
||||
|
||||
def test_notion_headers():
|
||||
config = NotionConfig(api_key="ntn_test123")
|
||||
headers = notion_headers(config)
|
||||
assert headers["Authorization"] == "Bearer ntn_test123"
|
||||
assert headers["Notion-Version"] == "2022-06-28"
|
||||
assert headers["Content-Type"] == "application/json"
|
||||
|
||||
|
||||
def test_notion_api_error():
|
||||
err = NotionAPIError("bad request", status=400, code="invalid_json")
|
||||
assert str(err) == "bad request"
|
||||
assert err.status == 400
|
||||
assert err.code == "invalid_json"
|
||||
@@ -0,0 +1,168 @@
|
||||
# ========= 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
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.accessor.notion import NotionAccessor
|
||||
from mirage.cache.index.ram import RAMIndexCacheStore
|
||||
from mirage.core.notion import read as notion_read
|
||||
from mirage.core.notion import readdir as notion_readdir
|
||||
from mirage.core.notion import stat as notion_stat
|
||||
from mirage.core.notion.read import read
|
||||
from mirage.core.notion.readdir import readdir
|
||||
from mirage.core.notion.stat import stat
|
||||
from mirage.resource.notion.config import NotionConfig
|
||||
from mirage.types import FileType, PathSpec
|
||||
|
||||
DATABASE_ID = "db123"
|
||||
ROW_ID = "row456"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def accessor():
|
||||
return NotionAccessor(NotionConfig(api_key="ntn_test"))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_root_includes_pages_and_databases(accessor):
|
||||
entries = await readdir(accessor, "/")
|
||||
assert entries == ["/pages", "/databases"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_databases_lists_database_directories(
|
||||
accessor, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
notion_readdir,
|
||||
"search_databases",
|
||||
AsyncMock(return_value=[{
|
||||
"id": DATABASE_ID,
|
||||
"title": [{
|
||||
"plain_text": "Tasks"
|
||||
}],
|
||||
"last_edited_time": "2026-01-01T00:00:00Z",
|
||||
}]),
|
||||
)
|
||||
entries = await readdir(accessor, "/databases", RAMIndexCacheStore())
|
||||
assert entries == [f"/databases/Tasks__{DATABASE_ID}"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_database_lists_row_pages(accessor, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
notion_readdir,
|
||||
"query_database",
|
||||
AsyncMock(return_value=[{
|
||||
"object": "page",
|
||||
"id": ROW_ID,
|
||||
"properties": {
|
||||
"Name": {
|
||||
"type": "title",
|
||||
"title": [{
|
||||
"plain_text": "Row A"
|
||||
}],
|
||||
}
|
||||
},
|
||||
"last_edited_time": "2026-01-02T00:00:00Z",
|
||||
}]),
|
||||
)
|
||||
entries = await readdir(accessor, f"/databases/Tasks__{DATABASE_ID}")
|
||||
assert entries == [
|
||||
f"/databases/Tasks__{DATABASE_ID}/database.json",
|
||||
f"/databases/Tasks__{DATABASE_ID}/Row_A__{ROW_ID}",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_database_json_returns_database_metadata(
|
||||
accessor, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
notion_read,
|
||||
"get_database",
|
||||
AsyncMock(
|
||||
return_value={
|
||||
"id": DATABASE_ID,
|
||||
"title": [{
|
||||
"plain_text": "Tasks"
|
||||
}],
|
||||
"properties": {
|
||||
"Name": {
|
||||
"type": "title"
|
||||
}
|
||||
},
|
||||
"last_edited_time": "2026-01-03T00:00:00Z",
|
||||
}),
|
||||
)
|
||||
data = await read(accessor,
|
||||
f"/databases/Tasks__{DATABASE_ID}/database.json")
|
||||
import json
|
||||
|
||||
decoded = json.loads(data)
|
||||
assert decoded["database_id"] == DATABASE_ID
|
||||
assert decoded["title"] == "Tasks"
|
||||
assert decoded["properties"] == {"Name": {"type": "title"}}
|
||||
assert "rows" not in decoded
|
||||
assert "row_count" not in decoded
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_database_row_lists_page_json_and_child_pages(
|
||||
accessor,
|
||||
monkeypatch,
|
||||
):
|
||||
monkeypatch.setattr(
|
||||
notion_readdir,
|
||||
"list_block_children",
|
||||
AsyncMock(return_value=[{
|
||||
"id": "child789",
|
||||
"type": "child_page",
|
||||
"child_page": {
|
||||
"title": "Child"
|
||||
},
|
||||
}]),
|
||||
)
|
||||
entries = await readdir(
|
||||
accessor, f"/databases/Tasks__{DATABASE_ID}/Row-A__{ROW_ID}")
|
||||
assert entries == [
|
||||
f"/databases/Tasks__{DATABASE_ID}/Row-A__{ROW_ID}/page.json",
|
||||
f"/databases/Tasks__{DATABASE_ID}/Row-A__{ROW_ID}/Child__child789",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_database_dir_uses_database_metadata(accessor, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
notion_stat,
|
||||
"get_database",
|
||||
AsyncMock(return_value={"last_edited_time": "2026-01-03T00:00:00Z"}),
|
||||
)
|
||||
result = await stat(accessor, f"/databases/Tasks__{DATABASE_ID}")
|
||||
assert result.name == f"Tasks__{DATABASE_ID}"
|
||||
assert result.type == FileType.DIRECTORY
|
||||
assert result.modified == "2026-01-03T00:00:00Z"
|
||||
assert result.extra == {"database_id": DATABASE_ID}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_database_row_dir(accessor):
|
||||
result = await stat(
|
||||
accessor,
|
||||
PathSpec.from_str_path(
|
||||
f"/databases/Tasks__{DATABASE_ID}/Row-A__{ROW_ID}"),
|
||||
)
|
||||
assert result.name == f"Row-A__{ROW_ID}"
|
||||
assert result.type == FileType.DIRECTORY
|
||||
assert result.extra == {"page_id": ROW_ID}
|
||||
@@ -0,0 +1,106 @@
|
||||
# ========= 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.notion import find as find_mod
|
||||
from mirage.types import FileStat, FileType, PathSpec
|
||||
from mirage.utils.key_prefix import mount_key
|
||||
|
||||
_DIRS = {"/db", "/db/sub"}
|
||||
_FILES = {
|
||||
"/db/page1.md": 10,
|
||||
"/db/sub/page2.md": 20,
|
||||
}
|
||||
_CHILDREN = {
|
||||
"/db": ["/db/page1.md", "/db/sub"],
|
||||
"/db/sub": ["/db/sub/page2.md"],
|
||||
}
|
||||
|
||||
|
||||
async def _fake_readdir(accessor, path, index):
|
||||
key = path.virtual if isinstance(path, PathSpec) else path
|
||||
key = "/" + key.strip("/") if key.strip("/") else "/"
|
||||
return _CHILDREN.get(key, [])
|
||||
|
||||
|
||||
async def _fake_stat(accessor, path, index=None):
|
||||
key = path.virtual if isinstance(path, PathSpec) else path
|
||||
key = "/" + key.strip("/") if key.strip("/") else "/"
|
||||
if key in _DIRS:
|
||||
return FileStat(name=key.rsplit("/", 1)[-1] or "/",
|
||||
type=FileType.DIRECTORY)
|
||||
return FileStat(name=key.rsplit("/", 1)[-1],
|
||||
type=FileType.TEXT,
|
||||
size=_FILES.get(key))
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _patch(monkeypatch):
|
||||
monkeypatch.setattr(find_mod, "readdir", _fake_readdir)
|
||||
monkeypatch.setattr(find_mod, "stat", _fake_stat)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_all():
|
||||
out = await find_mod.find(None, PathSpec.from_str_path("/db"))
|
||||
assert out == ["/db", "/db/page1.md", "/db/sub", "/db/sub/page2.md"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_name_matches_mount_root_start_path():
|
||||
spec = PathSpec(resource_path=mount_key("/db", "/db"),
|
||||
virtual="/db",
|
||||
directory="/db")
|
||||
out = await find_mod.find(None, spec, name="db")
|
||||
assert out == ["/"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_type_file():
|
||||
out = await find_mod.find(None, PathSpec.from_str_path("/db"), type="f")
|
||||
assert out == ["/db/page1.md", "/db/sub/page2.md"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_type_dir():
|
||||
out = await find_mod.find(None, PathSpec.from_str_path("/db"), type="d")
|
||||
assert out == ["/db", "/db/sub"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_name_glob():
|
||||
out = await find_mod.find(None, PathSpec.from_str_path("/db"), name="*.md")
|
||||
assert out == ["/db/page1.md", "/db/sub/page2.md"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_maxdepth():
|
||||
out = await find_mod.find(None, PathSpec.from_str_path("/db"), maxdepth=1)
|
||||
assert out == ["/db", "/db/page1.md", "/db/sub"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_mindepth():
|
||||
out = await find_mod.find(None, PathSpec.from_str_path("/db"), mindepth=1)
|
||||
assert out == ["/db/page1.md", "/db/sub", "/db/sub/page2.md"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_min_size():
|
||||
out = await find_mod.find(None,
|
||||
PathSpec.from_str_path("/db"),
|
||||
type="f",
|
||||
min_size=15)
|
||||
assert out == ["/db/sub/page2.md"]
|
||||
@@ -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
|
||||
import pytest_asyncio
|
||||
|
||||
from mirage.accessor.notion import NotionAccessor
|
||||
from mirage.cache.index import IndexEntry, RAMIndexCacheStore
|
||||
from mirage.core.notion.glob import resolve_glob
|
||||
from mirage.types import PathSpec
|
||||
|
||||
|
||||
def page_entry(name: str) -> IndexEntry:
|
||||
return IndexEntry(id=name,
|
||||
name=name,
|
||||
resource_type="notion/page",
|
||||
vfs_name=name)
|
||||
|
||||
|
||||
def file_entry(name: str) -> IndexEntry:
|
||||
return IndexEntry(id=name, name=name, resource_type="file", vfs_name=name)
|
||||
|
||||
|
||||
def glob_spec(virtual: str) -> PathSpec:
|
||||
last_slash = virtual.rfind("/")
|
||||
return PathSpec(
|
||||
virtual=virtual,
|
||||
directory=virtual[:last_slash + 1],
|
||||
resource_path=virtual[len("/notion"):].strip("/"),
|
||||
pattern=virtual[last_slash + 1:],
|
||||
resolved=False,
|
||||
)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def index():
|
||||
store = RAMIndexCacheStore()
|
||||
await store.set_dir("/pages", [
|
||||
("Demo_page__uuid1", page_entry("Demo_page__uuid1")),
|
||||
("Roadmap__uuid2", page_entry("Roadmap__uuid2")),
|
||||
])
|
||||
await store.set_dir("/pages/Demo_page__uuid1", [
|
||||
("page.json", file_entry("page.json")),
|
||||
("page.md", file_entry("page.md")),
|
||||
])
|
||||
return store
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mid_path_glob_expands_from_index(index):
|
||||
spec = glob_spec("/notion/pages/Demo_page__*/page.md")
|
||||
result = await resolve_glob(NotionAccessor(config=None), [spec], index)
|
||||
assert [p.virtual
|
||||
for p in result] == ["/notion/pages/Demo_page__uuid1/page.md"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_last_component_glob(index):
|
||||
spec = glob_spec("/notion/pages/Demo*")
|
||||
result = await resolve_glob(NotionAccessor(config=None), [spec], index)
|
||||
assert [p.virtual for p in result] == ["/notion/pages/Demo_page__uuid1"]
|
||||
@@ -0,0 +1,123 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.core.notion.normalize import normalize_page, to_json_bytes
|
||||
from mirage.core.notion.pathing import page_dirname, split_suffix_id
|
||||
|
||||
|
||||
class TestSplitSuffixId:
|
||||
|
||||
def test_basic(self):
|
||||
label, oid = split_suffix_id("my-page__abc123")
|
||||
assert label == "my-page"
|
||||
assert oid == "abc123"
|
||||
|
||||
def test_with_suffix(self):
|
||||
label, oid = split_suffix_id("my-page__abc123.json", suffix=".json")
|
||||
assert label == "my-page"
|
||||
assert oid == "abc123"
|
||||
|
||||
def test_no_separator_raises(self):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
split_suffix_id("noid")
|
||||
|
||||
def test_wrong_suffix_raises(self):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
split_suffix_id("my-page__abc.json", suffix=".md")
|
||||
|
||||
|
||||
class TestPageDirname:
|
||||
|
||||
def test_with_title(self):
|
||||
page = {
|
||||
"id": "abc-123",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "title",
|
||||
"title": [{
|
||||
"plain_text": "Hello World"
|
||||
}],
|
||||
},
|
||||
},
|
||||
}
|
||||
result = page_dirname(page)
|
||||
assert result.endswith("__abc-123")
|
||||
assert "hello" in result.lower()
|
||||
|
||||
def test_untitled(self):
|
||||
page = {"id": "xyz", "properties": {}}
|
||||
result = page_dirname(page)
|
||||
assert result == "untitled__xyz"
|
||||
|
||||
|
||||
class TestNormalizePage:
|
||||
|
||||
def test_basic(self):
|
||||
page = {
|
||||
"id": "abc-123",
|
||||
"url": "https://notion.so/abc123",
|
||||
"created_time": "2026-01-01T00:00:00.000Z",
|
||||
"last_edited_time": "2026-04-15T00:00:00.000Z",
|
||||
"parent": {
|
||||
"type": "workspace",
|
||||
"workspace": True
|
||||
},
|
||||
"archived": False,
|
||||
"created_by": {
|
||||
"id": "user1"
|
||||
},
|
||||
"last_edited_by": {
|
||||
"id": "user2"
|
||||
},
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "title",
|
||||
"title": [{
|
||||
"plain_text": "Test"
|
||||
}]
|
||||
},
|
||||
},
|
||||
}
|
||||
blocks = [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"paragraph": {
|
||||
"rich_text": [{
|
||||
"plain_text": "Hello",
|
||||
"annotations": {}
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "child_page",
|
||||
"child_page": {
|
||||
"title": "Sub"
|
||||
}
|
||||
},
|
||||
]
|
||||
result = normalize_page(page, blocks)
|
||||
assert result["page_id"] == "abc-123"
|
||||
assert result["title"] == "Test"
|
||||
assert "Hello" in result["markdown"]
|
||||
assert len(result["blocks"]) == 1
|
||||
|
||||
def test_to_json_bytes(self):
|
||||
data = to_json_bytes({"key": "value"})
|
||||
assert isinstance(data, bytes)
|
||||
parsed = json.loads(data)
|
||||
assert parsed["key"] == "value"
|
||||
@@ -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. =========
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.cache.index import RAMIndexCacheStore
|
||||
from mirage.core.notion import readdir as readdir_mod
|
||||
from mirage.types import PathSpec
|
||||
from mirage.utils.key_prefix import mount_key
|
||||
|
||||
_ACCESSOR = SimpleNamespace(config=None)
|
||||
|
||||
TOP_ID = "aaaa1111-2222-3333-4444-555566667777"
|
||||
|
||||
_TOP_PAGE = {
|
||||
"id": TOP_ID,
|
||||
"parent": {
|
||||
"type": "workspace"
|
||||
},
|
||||
"last_edited_time": "2026-01-02T00:00:00.000Z",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "title",
|
||||
"title": [{
|
||||
"type": "text",
|
||||
"plain_text": "Top1"
|
||||
}],
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def _fake_search_pages(config):
|
||||
return [_TOP_PAGE]
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _patch(monkeypatch):
|
||||
monkeypatch.setattr(readdir_mod, "search_pages", _fake_search_pages)
|
||||
|
||||
|
||||
def _spec(original: str, prefix: str = "") -> PathSpec:
|
||||
return PathSpec(resource_path=mount_key(original, prefix),
|
||||
virtual=original,
|
||||
directory=original)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_root_lists_pages_with_prefix():
|
||||
out = await readdir_mod.readdir(_ACCESSOR, _spec("/notion", "/notion"))
|
||||
assert out == ["/notion/pages", "/notion/databases"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pages_listing_cold():
|
||||
out = await readdir_mod.readdir(_ACCESSOR, _spec("/pages"))
|
||||
assert out == [f"/pages/Top1__{TOP_ID}"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pages_listing_keeps_prefix_on_warm_cache_hit():
|
||||
index = RAMIndexCacheStore()
|
||||
spec = _spec("/notion/pages", "/notion")
|
||||
cold = await readdir_mod.readdir(_ACCESSOR, spec, index)
|
||||
warm = await readdir_mod.readdir(_ACCESSOR, spec, index)
|
||||
assert warm == cold
|
||||
assert warm == [f"/notion/pages/Top1__{TOP_ID}"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pages_listing_stores_remote_time():
|
||||
index = RAMIndexCacheStore()
|
||||
spec = _spec("/notion/pages", "/notion")
|
||||
await readdir_mod.readdir(_ACCESSOR, spec, index)
|
||||
lookup = await index.get(f"/pages/Top1__{TOP_ID}")
|
||||
assert lookup.entry is not None
|
||||
assert lookup.entry.remote_time == "2026-01-02T00:00:00.000Z"
|
||||
@@ -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 mirage.core.notion.render import (_block_to_md, _rich_text_to_md,
|
||||
blocks_to_markdown)
|
||||
|
||||
|
||||
class TestRichTextToMd:
|
||||
|
||||
def test_plain(self):
|
||||
rt = [{"plain_text": "hello", "annotations": {}}]
|
||||
assert _rich_text_to_md(rt) == "hello"
|
||||
|
||||
def test_bold(self):
|
||||
rt = [{"plain_text": "bold", "annotations": {"bold": True}}]
|
||||
assert _rich_text_to_md(rt) == "**bold**"
|
||||
|
||||
def test_italic(self):
|
||||
rt = [{"plain_text": "em", "annotations": {"italic": True}}]
|
||||
assert _rich_text_to_md(rt) == "*em*"
|
||||
|
||||
def test_code(self):
|
||||
rt = [{"plain_text": "x", "annotations": {"code": True}}]
|
||||
assert _rich_text_to_md(rt) == "`x`"
|
||||
|
||||
def test_link(self):
|
||||
rt = [{
|
||||
"plain_text": "click",
|
||||
"annotations": {},
|
||||
"href": "https://example.com"
|
||||
}]
|
||||
assert _rich_text_to_md(rt) == "[click](https://example.com)"
|
||||
|
||||
def test_multiple(self):
|
||||
rt = [
|
||||
{
|
||||
"plain_text": "a ",
|
||||
"annotations": {}
|
||||
},
|
||||
{
|
||||
"plain_text": "b",
|
||||
"annotations": {
|
||||
"bold": True
|
||||
}
|
||||
},
|
||||
]
|
||||
assert _rich_text_to_md(rt) == "a **b**"
|
||||
|
||||
|
||||
class TestBlockToMd:
|
||||
|
||||
def test_paragraph(self):
|
||||
block = {
|
||||
"type": "paragraph",
|
||||
"paragraph": {
|
||||
"rich_text": [{
|
||||
"plain_text": "hi",
|
||||
"annotations": {}
|
||||
}]
|
||||
}
|
||||
}
|
||||
assert _block_to_md(block) == "hi"
|
||||
|
||||
def test_heading_1(self):
|
||||
block = {
|
||||
"type": "heading_1",
|
||||
"heading_1": {
|
||||
"rich_text": [{
|
||||
"plain_text": "Title",
|
||||
"annotations": {}
|
||||
}]
|
||||
}
|
||||
}
|
||||
assert _block_to_md(block) == "# Title"
|
||||
|
||||
def test_heading_2(self):
|
||||
block = {
|
||||
"type": "heading_2",
|
||||
"heading_2": {
|
||||
"rich_text": [{
|
||||
"plain_text": "Sub",
|
||||
"annotations": {}
|
||||
}]
|
||||
}
|
||||
}
|
||||
assert _block_to_md(block) == "## Sub"
|
||||
|
||||
def test_bulleted_list_item(self):
|
||||
block = {
|
||||
"type": "bulleted_list_item",
|
||||
"bulleted_list_item": {
|
||||
"rich_text": [{
|
||||
"plain_text": "item",
|
||||
"annotations": {}
|
||||
}]
|
||||
}
|
||||
}
|
||||
assert _block_to_md(block) == "- item"
|
||||
|
||||
def test_to_do_checked(self):
|
||||
block = {
|
||||
"type": "to_do",
|
||||
"to_do": {
|
||||
"rich_text": [{
|
||||
"plain_text": "done",
|
||||
"annotations": {}
|
||||
}],
|
||||
"checked": True
|
||||
}
|
||||
}
|
||||
assert _block_to_md(block) == "- [x] done"
|
||||
|
||||
def test_code_block(self):
|
||||
block = {
|
||||
"type": "code",
|
||||
"code": {
|
||||
"rich_text": [{
|
||||
"plain_text": "print(1)",
|
||||
"annotations": {}
|
||||
}],
|
||||
"language": "python"
|
||||
}
|
||||
}
|
||||
assert _block_to_md(block) == "```python\nprint(1)\n```"
|
||||
|
||||
def test_divider(self):
|
||||
block = {"type": "divider", "divider": {}}
|
||||
assert _block_to_md(block) == "---"
|
||||
|
||||
def test_child_page_empty(self):
|
||||
block = {"type": "child_page", "child_page": {"title": "Sub"}}
|
||||
assert _block_to_md(block) == ""
|
||||
|
||||
|
||||
class TestBlocksToMarkdown:
|
||||
|
||||
def test_multiple_blocks(self):
|
||||
blocks = [
|
||||
{
|
||||
"type": "heading_1",
|
||||
"heading_1": {
|
||||
"rich_text": [{
|
||||
"plain_text": "Title",
|
||||
"annotations": {}
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "paragraph",
|
||||
"paragraph": {
|
||||
"rich_text": [{
|
||||
"plain_text": "body",
|
||||
"annotations": {}
|
||||
}]
|
||||
}
|
||||
},
|
||||
]
|
||||
result = blocks_to_markdown(blocks)
|
||||
assert result == "# Title\n\nbody\n"
|
||||
|
||||
def test_empty(self):
|
||||
assert blocks_to_markdown([]) == ""
|
||||
|
||||
def test_nested_children_indent(self):
|
||||
blocks = [
|
||||
{
|
||||
"type":
|
||||
"bulleted_list_item",
|
||||
"bulleted_list_item": {
|
||||
"rich_text": [{
|
||||
"plain_text": "parent",
|
||||
"annotations": {}
|
||||
}]
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "bulleted_list_item",
|
||||
"bulleted_list_item": {
|
||||
"rich_text": [{
|
||||
"plain_text": "child",
|
||||
"annotations": {}
|
||||
}]
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
result = blocks_to_markdown(blocks)
|
||||
assert result == "- parent\n\n - child\n"
|
||||
@@ -0,0 +1,69 @@
|
||||
# ========= 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.cache.index import RAMIndexCacheStore
|
||||
from mirage.core.notion import readdir as readdir_mod
|
||||
from mirage.core.notion.stat import stat
|
||||
from mirage.types import PathSpec
|
||||
from mirage.utils.key_prefix import mount_key
|
||||
|
||||
_ACCESSOR = SimpleNamespace(config=None)
|
||||
|
||||
TOP_ID = "aaaa1111-2222-3333-4444-555566667777"
|
||||
|
||||
_TOP_PAGE = {
|
||||
"id": TOP_ID,
|
||||
"parent": {
|
||||
"type": "workspace"
|
||||
},
|
||||
"last_edited_time": "2026-01-02T00:00:00.000Z",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "title",
|
||||
"title": [{
|
||||
"type": "text",
|
||||
"plain_text": "Top1"
|
||||
}],
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def _fake_search_pages(config):
|
||||
return [_TOP_PAGE]
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _patch(monkeypatch):
|
||||
monkeypatch.setattr(readdir_mod, "search_pages", _fake_search_pages)
|
||||
|
||||
|
||||
def _spec(original: str, prefix: str = "") -> PathSpec:
|
||||
return PathSpec(resource_path=mount_key(original, prefix),
|
||||
virtual=original,
|
||||
directory=original)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_page_returns_modified_from_index():
|
||||
index = RAMIndexCacheStore()
|
||||
await readdir_mod.readdir(_ACCESSOR, _spec("/notion/pages", "/notion"),
|
||||
index)
|
||||
spec = _spec(f"/notion/pages/Top1__{TOP_ID}", "/notion")
|
||||
s = await stat(_ACCESSOR, spec, index)
|
||||
assert s.modified == "2026-01-02T00:00:00.000Z"
|
||||
Reference in New Issue
Block a user