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,87 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.core.discord.channels import list_channels
|
||||
from mirage.resource.discord.config import DiscordConfig
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config():
|
||||
return DiscordConfig(token="test-bot-token")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_channels(config):
|
||||
mock_data = [
|
||||
{
|
||||
"id": "C001",
|
||||
"name": "general",
|
||||
"type": 0
|
||||
},
|
||||
{
|
||||
"id": "C002",
|
||||
"name": "announcements",
|
||||
"type": 5
|
||||
},
|
||||
{
|
||||
"id": "C003",
|
||||
"name": "forum",
|
||||
"type": 15
|
||||
},
|
||||
{
|
||||
"id": "C004",
|
||||
"name": "voice-chat",
|
||||
"type": 2
|
||||
},
|
||||
]
|
||||
with patch(
|
||||
"mirage.core.discord.channels.discord_get",
|
||||
new_callable=AsyncMock,
|
||||
return_value=mock_data,
|
||||
) as mock_get:
|
||||
result = await list_channels(config, "G001")
|
||||
|
||||
assert len(result) == 3
|
||||
assert result[0]["name"] == "general"
|
||||
assert result[1]["name"] == "announcements"
|
||||
assert result[2]["name"] == "forum"
|
||||
mock_get.assert_called_once_with(config, "/guilds/G001/channels")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_channels_filters_voice(config):
|
||||
mock_data = [
|
||||
{
|
||||
"id": "C001",
|
||||
"name": "voice-chat",
|
||||
"type": 2
|
||||
},
|
||||
{
|
||||
"id": "C002",
|
||||
"name": "stage",
|
||||
"type": 13
|
||||
},
|
||||
]
|
||||
with patch(
|
||||
"mirage.core.discord.channels.discord_get",
|
||||
new_callable=AsyncMock,
|
||||
return_value=mock_data,
|
||||
):
|
||||
result = await list_channels(config, "G001")
|
||||
|
||||
assert len(result) == 0
|
||||
@@ -0,0 +1,108 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.core.discord._client import discord_get, discord_post
|
||||
from mirage.resource.discord.config import DiscordConfig
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config():
|
||||
return DiscordConfig(token="test-bot-token")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_discord_get_success(config):
|
||||
mock_resp = AsyncMock()
|
||||
mock_resp.status = 200
|
||||
mock_resp.json = AsyncMock(return_value=[
|
||||
{
|
||||
"id": "123",
|
||||
"name": "general"
|
||||
},
|
||||
])
|
||||
mock_resp.raise_for_status = MagicMock()
|
||||
mock_session = AsyncMock()
|
||||
mock_session.get = MagicMock(return_value=AsyncMock(
|
||||
__aenter__=AsyncMock(return_value=mock_resp),
|
||||
__aexit__=AsyncMock(return_value=False),
|
||||
))
|
||||
|
||||
with patch("mirage.core.discord._client.aiohttp.ClientSession") as mock_cs:
|
||||
mock_cs.return_value.__aenter__ = AsyncMock(return_value=mock_session)
|
||||
mock_cs.return_value.__aexit__ = AsyncMock(return_value=False)
|
||||
|
||||
result = await discord_get(config, "/guilds/123/channels")
|
||||
|
||||
assert result == [{"id": "123", "name": "general"}]
|
||||
mock_session.get.assert_called_once()
|
||||
call_kwargs = mock_session.get.call_args
|
||||
assert call_kwargs.args[0] == \
|
||||
"https://discord.com/api/v10/guilds/123/channels"
|
||||
assert call_kwargs.kwargs["headers"]["Authorization"] == \
|
||||
"Bot test-bot-token"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_discord_get_rate_limited(config):
|
||||
mock_resp = AsyncMock()
|
||||
mock_resp.status = 429
|
||||
mock_resp.json = AsyncMock(return_value={
|
||||
"retry_after": 5,
|
||||
})
|
||||
mock_session = AsyncMock()
|
||||
mock_session.get = MagicMock(return_value=AsyncMock(
|
||||
__aenter__=AsyncMock(return_value=mock_resp),
|
||||
__aexit__=AsyncMock(return_value=False),
|
||||
))
|
||||
|
||||
with patch("mirage.core.discord._client.aiohttp.ClientSession") as mock_cs:
|
||||
mock_cs.return_value.__aenter__ = AsyncMock(return_value=mock_session)
|
||||
mock_cs.return_value.__aexit__ = AsyncMock(return_value=False)
|
||||
|
||||
with pytest.raises(RuntimeError, match="Rate limited"):
|
||||
await discord_get(config, "/users/@me/guilds")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_discord_post_success(config):
|
||||
mock_resp = AsyncMock()
|
||||
mock_resp.status = 200
|
||||
mock_resp.json = AsyncMock(return_value={
|
||||
"id": "msg1",
|
||||
"content": "hello",
|
||||
})
|
||||
mock_resp.raise_for_status = MagicMock()
|
||||
mock_session = AsyncMock()
|
||||
mock_session.post = MagicMock(return_value=AsyncMock(
|
||||
__aenter__=AsyncMock(return_value=mock_resp),
|
||||
__aexit__=AsyncMock(return_value=False),
|
||||
))
|
||||
|
||||
with patch("mirage.core.discord._client.aiohttp.ClientSession") as mock_cs:
|
||||
mock_cs.return_value.__aenter__ = AsyncMock(return_value=mock_session)
|
||||
mock_cs.return_value.__aexit__ = AsyncMock(return_value=False)
|
||||
|
||||
result = await discord_post(config,
|
||||
"/channels/C001/messages",
|
||||
body={"content": "hello"})
|
||||
|
||||
assert result["id"] == "msg1"
|
||||
mock_session.post.assert_called_once()
|
||||
call_kwargs = mock_session.post.call_args
|
||||
assert call_kwargs.args[0] == \
|
||||
"https://discord.com/api/v10/channels/C001/messages"
|
||||
@@ -0,0 +1,102 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from mirage.core.discord.entry import (channel_dirname, channel_entry,
|
||||
guild_dirname, guild_entry,
|
||||
member_entry, member_filename)
|
||||
|
||||
|
||||
def test_guild_dirname_basic():
|
||||
assert guild_dirname({"id": "G1", "name": "My Server"}) == "My Server__G1"
|
||||
|
||||
|
||||
def test_guild_dirname_apostrophe_is_preserved():
|
||||
assert guild_dirname({
|
||||
"id": "G1",
|
||||
"name": "Zecheng's Server"
|
||||
}) == "Zecheng's Server__G1"
|
||||
|
||||
|
||||
def test_guild_dirname_slash_in_name_is_replaced():
|
||||
assert guild_dirname({"id": "G1", "name": "A/B Test"}) == "A∕B Test__G1"
|
||||
|
||||
|
||||
def test_guild_dirname_falls_back_to_unknown_when_name_missing():
|
||||
assert guild_dirname({"id": "G1"}) == "unknown__G1"
|
||||
|
||||
|
||||
def test_guild_dirname_falls_back_to_unknown_when_name_empty():
|
||||
assert guild_dirname({"id": "G1", "name": ""}) == "unknown__G1"
|
||||
|
||||
|
||||
def test_channel_dirname_basic():
|
||||
assert channel_dirname({"id": "C1", "name": "general"}) == "general__C1"
|
||||
|
||||
|
||||
def test_channel_dirname_with_emoji_is_preserved():
|
||||
assert channel_dirname({"id": "C1", "name": "🔥-deals"}) == "🔥-deals__C1"
|
||||
|
||||
|
||||
def test_channel_dirname_falls_back_to_unknown():
|
||||
assert channel_dirname({"id": "C1"}) == "unknown__C1"
|
||||
|
||||
|
||||
def test_member_filename_basic():
|
||||
member = {"user": {"id": "U1", "username": "alice"}}
|
||||
assert member_filename(member) == "alice__U1.json"
|
||||
|
||||
|
||||
def test_member_filename_preserves_special_chars():
|
||||
member = {"user": {"id": "U1", "username": "bob.smith!"}}
|
||||
assert member_filename(member) == "bob.smith!__U1.json"
|
||||
|
||||
|
||||
def test_member_filename_falls_back_to_unknown():
|
||||
member = {"user": {"id": "U1"}}
|
||||
assert member_filename(member) == "unknown__U1.json"
|
||||
|
||||
|
||||
def test_same_named_members_get_distinct_filenames():
|
||||
a = {"user": {"id": "U1", "username": "alice"}}
|
||||
b = {"user": {"id": "U2", "username": "alice"}}
|
||||
assert member_filename(a) != member_filename(b)
|
||||
assert member_filename(a) == "alice__U1.json"
|
||||
assert member_filename(b) == "alice__U2.json"
|
||||
|
||||
|
||||
def test_same_named_channels_get_distinct_dirnames():
|
||||
a = {"id": "C1", "name": "general"}
|
||||
b = {"id": "C2", "name": "general"}
|
||||
assert channel_dirname(a) != channel_dirname(b)
|
||||
|
||||
|
||||
def test_same_named_guilds_get_distinct_dirnames():
|
||||
a = {"id": "G1", "name": "Test"}
|
||||
b = {"id": "G2", "name": "Test"}
|
||||
assert guild_dirname(a) != guild_dirname(b)
|
||||
|
||||
|
||||
def test_guild_entry_vfs_name_matches_dirname():
|
||||
g = {"id": "G1", "name": "My Server"}
|
||||
assert guild_entry(g).vfs_name == guild_dirname(g)
|
||||
|
||||
|
||||
def test_channel_entry_vfs_name_matches_dirname():
|
||||
c = {"id": "C1", "name": "general", "last_message_id": "M1"}
|
||||
assert channel_entry(c).vfs_name == channel_dirname(c)
|
||||
|
||||
|
||||
def test_member_entry_vfs_name_matches_filename():
|
||||
m = {"user": {"id": "U1", "username": "alice"}}
|
||||
assert member_entry(m).vfs_name == member_filename(m)
|
||||
@@ -0,0 +1,86 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from mirage.core.discord.entry import channel_dirname, guild_dirname
|
||||
from mirage.core.discord.formatters import format_grep_results
|
||||
|
||||
|
||||
def test_format_grep_results_uses_vfs_channel_dirname():
|
||||
msgs = [{
|
||||
"timestamp": "2024-04-10T12:34:56.000000+00:00",
|
||||
"channel_id": "C1",
|
||||
"author": {
|
||||
"username": "alice"
|
||||
},
|
||||
"content": "hello",
|
||||
}]
|
||||
guild = {"id": "G1", "name": "MyGuild"}
|
||||
chan = {"id": "C1", "name": "general"}
|
||||
lines = format_grep_results(
|
||||
msgs,
|
||||
prefix="/discord",
|
||||
guild_dirname=guild_dirname(guild),
|
||||
channel_names={chan["id"]: channel_dirname(chan)})
|
||||
assert lines == [
|
||||
"/discord/MyGuild__G1/channels/general__C1/"
|
||||
"2024-04-10/chat.jsonl:[alice] hello"
|
||||
]
|
||||
|
||||
|
||||
def test_format_grep_results_falls_back_to_channel_id():
|
||||
msgs = [{
|
||||
"timestamp": "2024-04-10T00:00:00+00:00",
|
||||
"channel_id": "C2",
|
||||
"author": {
|
||||
"username": "bob"
|
||||
},
|
||||
"content": "x",
|
||||
}]
|
||||
lines = format_grep_results(msgs, "/discord", "G__G1", channel_names={})
|
||||
assert lines == [
|
||||
"/discord/G__G1/channels/C2/2024-04-10/chat.jsonl:[bob] x"
|
||||
]
|
||||
|
||||
|
||||
def test_format_grep_results_path_matches_vfs_layout():
|
||||
msgs = [{
|
||||
"timestamp": "2024-04-10T00:00:00+00:00",
|
||||
"channel_id": "C1",
|
||||
"author": {
|
||||
"username": "alice"
|
||||
},
|
||||
"content": "hi",
|
||||
}]
|
||||
guild = {"id": "G1", "name": "S"}
|
||||
chan = {"id": "C1", "name": "general"}
|
||||
line = format_grep_results(msgs, "/discord", guild_dirname(guild),
|
||||
{chan["id"]: channel_dirname(chan)})[0]
|
||||
expected_path = (f"/discord/{guild_dirname(guild)}/channels/"
|
||||
f"{channel_dirname(chan)}/2024-04-10/chat.jsonl")
|
||||
assert line.startswith(expected_path + ":")
|
||||
|
||||
|
||||
def test_format_grep_results_replaces_newlines():
|
||||
msgs = [{
|
||||
"timestamp": "2024-01-02",
|
||||
"channel_id": "C",
|
||||
"author": {
|
||||
"username": "u"
|
||||
},
|
||||
"content": "line1\nline2",
|
||||
}]
|
||||
lines = format_grep_results(msgs, "/discord", "G__G1", channel_names={})
|
||||
assert lines == [
|
||||
"/discord/G__G1/channels/C/2024-01-02/chat.jsonl:[u] line1 line2"
|
||||
]
|
||||
@@ -0,0 +1,54 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.core.discord.guilds import list_guilds
|
||||
from mirage.resource.discord.config import DiscordConfig
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config():
|
||||
return DiscordConfig(token="test-bot-token")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_guilds(config):
|
||||
mock_data = [
|
||||
{
|
||||
"id": "G001",
|
||||
"name": "My Server"
|
||||
},
|
||||
{
|
||||
"id": "G002",
|
||||
"name": "Another Server"
|
||||
},
|
||||
]
|
||||
with patch(
|
||||
"mirage.core.discord.paginate.discord_get",
|
||||
new_callable=AsyncMock,
|
||||
return_value=mock_data,
|
||||
) as mock_get:
|
||||
result = await list_guilds(config)
|
||||
|
||||
assert len(result) == 2
|
||||
assert result[0]["name"] == "My Server"
|
||||
assert result[1]["id"] == "G002"
|
||||
mock_get.assert_called_once()
|
||||
args, kwargs = mock_get.call_args
|
||||
assert args[1] == "/users/@me/guilds"
|
||||
assert kwargs["params"]["after"] == "0"
|
||||
assert kwargs["params"]["limit"] == 200
|
||||
@@ -0,0 +1,66 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import json
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.core.discord.history import get_history_jsonl
|
||||
from mirage.resource.discord.config import DiscordConfig
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config():
|
||||
return DiscordConfig(token="test-bot-token")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_history_jsonl(config):
|
||||
messages = [
|
||||
{
|
||||
"id": "200",
|
||||
"content": "second"
|
||||
},
|
||||
{
|
||||
"id": "100",
|
||||
"content": "first"
|
||||
},
|
||||
]
|
||||
with patch(
|
||||
"mirage.core.discord.paginate.discord_get",
|
||||
new_callable=AsyncMock,
|
||||
return_value=messages,
|
||||
):
|
||||
result = await get_history_jsonl(config, "C001", "2024-01-15")
|
||||
|
||||
lines = result.decode().strip().split("\n")
|
||||
assert len(lines) == 2
|
||||
first = json.loads(lines[0])
|
||||
second = json.loads(lines[1])
|
||||
assert int(first["id"]) < int(second["id"])
|
||||
assert first["content"] == "first"
|
||||
assert second["content"] == "second"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_history_empty(config):
|
||||
with patch(
|
||||
"mirage.core.discord.paginate.discord_get",
|
||||
new_callable=AsyncMock,
|
||||
return_value=[],
|
||||
):
|
||||
result = await get_history_jsonl(config, "C001", "2024-01-15")
|
||||
|
||||
assert result == b""
|
||||
@@ -0,0 +1,86 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.core.discord.members import list_members, search_members
|
||||
from mirage.resource.discord.config import DiscordConfig
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config():
|
||||
return DiscordConfig(token="test-bot-token")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_members(config):
|
||||
mock_data = [
|
||||
{
|
||||
"user": {
|
||||
"id": "U001",
|
||||
"username": "alice"
|
||||
}
|
||||
},
|
||||
{
|
||||
"user": {
|
||||
"id": "U002",
|
||||
"username": "bob"
|
||||
}
|
||||
},
|
||||
]
|
||||
with patch(
|
||||
"mirage.core.discord.paginate.discord_get",
|
||||
new_callable=AsyncMock,
|
||||
return_value=mock_data,
|
||||
) as mock_get:
|
||||
result = await list_members(config, "G001")
|
||||
|
||||
assert len(result) == 2
|
||||
assert result[0]["user"]["username"] == "alice"
|
||||
mock_get.assert_called_once()
|
||||
args, kwargs = mock_get.call_args
|
||||
assert args[1] == "/guilds/G001/members"
|
||||
assert kwargs["params"]["after"] == "0"
|
||||
assert kwargs["params"]["limit"] == 1000
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_members(config):
|
||||
mock_data = [
|
||||
{
|
||||
"user": {
|
||||
"id": "U001",
|
||||
"username": "alice"
|
||||
}
|
||||
},
|
||||
]
|
||||
with patch(
|
||||
"mirage.core.discord.members.discord_get",
|
||||
new_callable=AsyncMock,
|
||||
return_value=mock_data,
|
||||
) as mock_get:
|
||||
result = await search_members(config, "G001", "ali")
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0]["user"]["username"] == "alice"
|
||||
mock_get.assert_called_once_with(
|
||||
config,
|
||||
"/guilds/G001/members/search",
|
||||
params={
|
||||
"query": "ali",
|
||||
"limit": 100
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,105 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.core.discord.paginate import after_id_pages, offset_pages
|
||||
from mirage.resource.discord.config import DiscordConfig
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config():
|
||||
return DiscordConfig(token="t")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_after_id_pages_walks_until_partial(config):
|
||||
page1 = [{"id": "1"}, {"id": "2"}]
|
||||
page2 = [{"id": "3"}]
|
||||
with patch("mirage.core.discord.paginate.discord_get",
|
||||
new_callable=AsyncMock,
|
||||
side_effect=[page1, page2]) as mock:
|
||||
collected = []
|
||||
async for page in after_id_pages(
|
||||
config,
|
||||
"/x",
|
||||
base_params={},
|
||||
last_id_fn=lambda m: m["id"],
|
||||
page_size=2,
|
||||
):
|
||||
collected.append(page)
|
||||
assert collected == [page1, page2]
|
||||
assert mock.call_count == 2
|
||||
second_params = mock.call_args_list[1].kwargs["params"]
|
||||
assert second_params["after"] == "2"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_after_id_pages_stops_on_empty(config):
|
||||
with patch("mirage.core.discord.paginate.discord_get",
|
||||
new_callable=AsyncMock,
|
||||
return_value=[]):
|
||||
pages = [
|
||||
p async for p in after_id_pages(
|
||||
config,
|
||||
"/x",
|
||||
base_params={},
|
||||
last_id_fn=lambda m: m["id"],
|
||||
)
|
||||
]
|
||||
assert pages == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_offset_pages_walks_and_terminates_on_total(config):
|
||||
p1 = {"messages": [["m1"], ["m2"]], "total_results": 3}
|
||||
p2 = {"messages": [["m3"]], "total_results": 3}
|
||||
with patch("mirage.core.discord.paginate.discord_get",
|
||||
new_callable=AsyncMock,
|
||||
side_effect=[p1, p2]) as mock:
|
||||
collected = []
|
||||
async for items in offset_pages(
|
||||
config,
|
||||
"/y",
|
||||
base_params={"q": "x"},
|
||||
items_path=("messages", ),
|
||||
page_size=2,
|
||||
):
|
||||
collected.append(items)
|
||||
assert collected == [[["m1"], ["m2"]], [["m3"]]]
|
||||
assert mock.call_count == 2
|
||||
assert mock.call_args_list[0].kwargs["params"]["offset"] == 0
|
||||
assert mock.call_args_list[1].kwargs["params"]["offset"] == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_offset_pages_respects_max_pages(config):
|
||||
p = {"messages": [["m"]], "total_results": 999}
|
||||
with patch("mirage.core.discord.paginate.discord_get",
|
||||
new_callable=AsyncMock,
|
||||
return_value=p) as mock:
|
||||
collected = []
|
||||
async for items in offset_pages(
|
||||
config,
|
||||
"/y",
|
||||
base_params={},
|
||||
items_path=("messages", ),
|
||||
page_size=1,
|
||||
max_pages=2,
|
||||
):
|
||||
collected.append(items)
|
||||
assert len(collected) == 2
|
||||
assert mock.call_count == 2
|
||||
@@ -0,0 +1,78 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.accessor.discord import DiscordAccessor
|
||||
from mirage.cache.index import IndexEntry
|
||||
from mirage.cache.index.ram import RAMIndexCacheStore
|
||||
from mirage.core.discord.read import read
|
||||
from mirage.resource.discord.config import DiscordConfig
|
||||
from mirage.types import PathSpec
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def index():
|
||||
store = RAMIndexCacheStore()
|
||||
asyncio.run(
|
||||
store.put(
|
||||
"/My Server/channels/general",
|
||||
IndexEntry(
|
||||
id="C001",
|
||||
name="general",
|
||||
resource_type="discord/channel",
|
||||
vfs_name="general",
|
||||
),
|
||||
))
|
||||
return store
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def accessor():
|
||||
config = DiscordConfig(token="test-bot-token")
|
||||
return DiscordAccessor(config=config)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_jsonl(accessor, index):
|
||||
fake_data = b'{"id":"100","content":"hello"}\n'
|
||||
with patch(
|
||||
"mirage.core.discord.read.get_history_jsonl",
|
||||
new_callable=AsyncMock,
|
||||
return_value=fake_data,
|
||||
) as mock_hist:
|
||||
path = "/My Server/channels/general/2024-01-15/chat.jsonl"
|
||||
result = await read(
|
||||
accessor,
|
||||
PathSpec(virtual=path,
|
||||
directory=path,
|
||||
resource_path=path.strip("/")),
|
||||
index,
|
||||
)
|
||||
|
||||
assert result == fake_data
|
||||
mock_hist.assert_called_once_with(accessor.config, "C001", "2024-01-15")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_not_found(accessor, index):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
await read(
|
||||
accessor,
|
||||
PathSpec(resource_path="no/such/path",
|
||||
virtual="/no/such/path",
|
||||
directory="/no/such/path"), index)
|
||||
@@ -0,0 +1,190 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.accessor.discord import DiscordAccessor
|
||||
from mirage.cache.index import IndexEntry
|
||||
from mirage.cache.index.ram import RAMIndexCacheStore
|
||||
from mirage.core.discord.readdir import readdir
|
||||
from mirage.resource.discord.config import DiscordConfig
|
||||
from mirage.types import PathSpec
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def index():
|
||||
return RAMIndexCacheStore()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def accessor():
|
||||
return DiscordAccessor(config=DiscordConfig(token="test-bot-token"), )
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_root(accessor, index):
|
||||
guilds = [
|
||||
{
|
||||
"id": "G001",
|
||||
"name": "My Server"
|
||||
},
|
||||
]
|
||||
with patch(
|
||||
"mirage.core.discord.readdir.list_guilds",
|
||||
new_callable=AsyncMock,
|
||||
return_value=guilds,
|
||||
):
|
||||
result = await readdir(
|
||||
accessor, PathSpec(resource_path="", virtual="/", directory="/"),
|
||||
index)
|
||||
|
||||
assert "/My Server__G001" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_root_with_slash_in_name(accessor, index):
|
||||
guilds = [
|
||||
{
|
||||
"id": "G001",
|
||||
"name": "A/B Test Server"
|
||||
},
|
||||
]
|
||||
with patch(
|
||||
"mirage.core.discord.readdir.list_guilds",
|
||||
new_callable=AsyncMock,
|
||||
return_value=guilds,
|
||||
):
|
||||
result = await readdir(
|
||||
accessor, PathSpec(resource_path="", virtual="/", directory="/"),
|
||||
index)
|
||||
|
||||
assert result == ["/A∕B Test Server__G001"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_root_with_apostrophe(accessor, index):
|
||||
guilds = [
|
||||
{
|
||||
"id": "G001",
|
||||
"name": "Zecheng's Server"
|
||||
},
|
||||
]
|
||||
with patch(
|
||||
"mirage.core.discord.readdir.list_guilds",
|
||||
new_callable=AsyncMock,
|
||||
return_value=guilds,
|
||||
):
|
||||
result = await readdir(
|
||||
accessor, PathSpec(resource_path="", virtual="/", directory="/"),
|
||||
index)
|
||||
|
||||
assert "/Zecheng's Server__G001" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_guild(accessor, index):
|
||||
await index.put(
|
||||
"/My Server",
|
||||
IndexEntry(
|
||||
id="G001",
|
||||
name="My Server",
|
||||
resource_type="discord/guild",
|
||||
vfs_name="My Server",
|
||||
),
|
||||
)
|
||||
|
||||
result = await readdir(
|
||||
accessor,
|
||||
PathSpec(resource_path="My Server",
|
||||
virtual="/My Server",
|
||||
directory="/My Server"), index)
|
||||
|
||||
assert result == [
|
||||
"/My Server/channels",
|
||||
"/My Server/members",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_channels(accessor, index):
|
||||
await index.put(
|
||||
"/My Server",
|
||||
IndexEntry(
|
||||
id="G001",
|
||||
name="My Server",
|
||||
resource_type="discord/guild",
|
||||
vfs_name="My Server",
|
||||
),
|
||||
)
|
||||
channels = [
|
||||
{
|
||||
"id": "C001",
|
||||
"name": "general",
|
||||
"type": 0
|
||||
},
|
||||
{
|
||||
"id": "C002",
|
||||
"name": "random",
|
||||
"type": 0
|
||||
},
|
||||
]
|
||||
with patch(
|
||||
"mirage.core.discord.readdir.list_channels",
|
||||
new_callable=AsyncMock,
|
||||
return_value=channels,
|
||||
):
|
||||
result = await readdir(
|
||||
accessor,
|
||||
PathSpec(resource_path="My Server/channels",
|
||||
virtual="/My Server/channels",
|
||||
directory="/My Server/channels"), index)
|
||||
|
||||
assert "/My Server/channels/general__C001" in result
|
||||
assert "/My Server/channels/random__C002" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_channel_dates(accessor, index):
|
||||
await index.put(
|
||||
"/My Server",
|
||||
IndexEntry(
|
||||
id="G001",
|
||||
name="My Server",
|
||||
resource_type="discord/guild",
|
||||
vfs_name="My Server",
|
||||
),
|
||||
)
|
||||
await index.put(
|
||||
"/My Server/channels/general",
|
||||
IndexEntry(
|
||||
id="C001",
|
||||
name="general",
|
||||
resource_type="discord/channel",
|
||||
vfs_name="general",
|
||||
),
|
||||
)
|
||||
|
||||
result = await readdir(
|
||||
accessor,
|
||||
PathSpec(resource_path="My Server/channels/general",
|
||||
virtual="/My Server/channels/general",
|
||||
directory="/My Server/channels/general"), index)
|
||||
|
||||
assert len(result) >= 1
|
||||
# New layout: date directories (no extension)
|
||||
import re
|
||||
date_re = re.compile(r"^/My Server/channels/general/\d{4}-\d{2}-\d{2}$")
|
||||
assert all(date_re.match(r) for r in result)
|
||||
@@ -0,0 +1,153 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import aiohttp
|
||||
import pytest
|
||||
|
||||
from mirage.accessor.discord import DiscordAccessor
|
||||
from mirage.cache.index import IndexEntry
|
||||
from mirage.cache.index.ram import RAMIndexCacheStore
|
||||
from mirage.core.discord.readdir import readdir
|
||||
from mirage.resource.discord.config import DiscordConfig
|
||||
from mirage.types import PathSpec
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def index():
|
||||
return RAMIndexCacheStore()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def accessor():
|
||||
return DiscordAccessor(config=DiscordConfig(token="t"))
|
||||
|
||||
|
||||
async def _seed_channel(idx):
|
||||
await idx.put(
|
||||
"/G",
|
||||
IndexEntry(id="G1",
|
||||
name="G",
|
||||
resource_type="discord/guild",
|
||||
vfs_name="G"),
|
||||
)
|
||||
await idx.put(
|
||||
"/G/channels/ch",
|
||||
IndexEntry(id="C1",
|
||||
name="ch",
|
||||
resource_type="discord/channel",
|
||||
vfs_name="ch"),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_date_dir_contents_lists_chat_and_files(accessor, index):
|
||||
fake_messages = [{
|
||||
"id":
|
||||
"100",
|
||||
"content":
|
||||
"hi",
|
||||
"attachments": [{
|
||||
"id": "A1",
|
||||
"filename": "pic.png",
|
||||
"url": "https://cdn.example/A1/pic.png",
|
||||
"proxy_url": "https://media.example/A1/pic.png",
|
||||
"content_type": "image/png",
|
||||
"size": 1234,
|
||||
}],
|
||||
}]
|
||||
await _seed_channel(index)
|
||||
with patch("mirage.core.discord.readdir.list_messages_for_day",
|
||||
new_callable=AsyncMock,
|
||||
return_value=fake_messages):
|
||||
result = await readdir(
|
||||
accessor,
|
||||
PathSpec(resource_path="G/channels/ch/2024-04-04",
|
||||
virtual="/G/channels/ch/2024-04-04",
|
||||
directory="/G/channels/ch/2024-04-04"),
|
||||
index,
|
||||
)
|
||||
assert "/G/channels/ch/2024-04-04/chat.jsonl" in result
|
||||
assert "/G/channels/ch/2024-04-04/files" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_files_dir_lists_attachments(accessor, index):
|
||||
fake_messages = [{
|
||||
"id":
|
||||
"100",
|
||||
"attachments": [{
|
||||
"id": "A1",
|
||||
"filename": "pic.png",
|
||||
"url": "https://cdn.example/A1/pic.png",
|
||||
"content_type": "image/png",
|
||||
"size": 1234,
|
||||
}],
|
||||
}]
|
||||
await _seed_channel(index)
|
||||
with patch("mirage.core.discord.readdir.list_messages_for_day",
|
||||
new_callable=AsyncMock,
|
||||
return_value=fake_messages):
|
||||
result = await readdir(
|
||||
accessor,
|
||||
PathSpec(resource_path="G/channels/ch/2024-04-04/files",
|
||||
virtual="/G/channels/ch/2024-04-04/files",
|
||||
directory="/G/channels/ch/2024-04-04/files"),
|
||||
index,
|
||||
)
|
||||
assert any(r.endswith("pic__A1.png") for r in result)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_day_swallows_soft_errors(accessor, index):
|
||||
await _seed_channel(index)
|
||||
err = aiohttp.ClientResponseError(
|
||||
request_info=None, # type: ignore[arg-type]
|
||||
history=(),
|
||||
status=403,
|
||||
)
|
||||
with patch("mirage.core.discord.readdir.list_messages_for_day",
|
||||
new_callable=AsyncMock,
|
||||
side_effect=err):
|
||||
result = await readdir(
|
||||
accessor,
|
||||
PathSpec(resource_path="G/channels/ch/2024-04-04",
|
||||
virtual="/G/channels/ch/2024-04-04",
|
||||
directory="/G/channels/ch/2024-04-04"),
|
||||
index,
|
||||
)
|
||||
# empty day on soft error → sealed listing (no chat.jsonl/files entries)
|
||||
assert result == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_day_propagates_hard_errors(accessor, index):
|
||||
await _seed_channel(index)
|
||||
err = aiohttp.ClientResponseError(
|
||||
request_info=None, # type: ignore[arg-type]
|
||||
history=(),
|
||||
status=500,
|
||||
)
|
||||
with patch("mirage.core.discord.readdir.list_messages_for_day",
|
||||
new_callable=AsyncMock,
|
||||
side_effect=err):
|
||||
with pytest.raises(aiohttp.ClientResponseError):
|
||||
await readdir(
|
||||
accessor,
|
||||
PathSpec(resource_path="G/channels/ch/2024-04-04",
|
||||
virtual="/G/channels/ch/2024-04-04",
|
||||
directory="/G/channels/ch/2024-04-04"),
|
||||
index,
|
||||
)
|
||||
@@ -0,0 +1,270 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.cache.index import IndexEntry
|
||||
from mirage.cache.index.ram import RAMIndexCacheStore
|
||||
from mirage.core.discord.scope import coalesce_scopes, detect_scope
|
||||
from mirage.types import PathSpec
|
||||
from mirage.utils.key_prefix import mount_key
|
||||
|
||||
|
||||
def _run(coro):
|
||||
return asyncio.run(coro)
|
||||
|
||||
|
||||
def _gs(path: str,
|
||||
prefix: str = "",
|
||||
pattern: str | None = None,
|
||||
resolved: bool = True) -> PathSpec:
|
||||
return PathSpec(
|
||||
resource_path=mount_key(path, prefix),
|
||||
virtual=path,
|
||||
directory=path.rsplit("/", 1)[0] + "/" if "/" in path else "/",
|
||||
pattern=pattern,
|
||||
resolved=resolved,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def index():
|
||||
idx = RAMIndexCacheStore(ttl=600)
|
||||
_run(
|
||||
idx.put(
|
||||
"/discord/TestGuild",
|
||||
IndexEntry(id="G1",
|
||||
name="TestGuild",
|
||||
resource_type="discord/guild")))
|
||||
_run(
|
||||
idx.put(
|
||||
"/discord/TestGuild/channels/general",
|
||||
IndexEntry(id="C1",
|
||||
name="general",
|
||||
resource_type="discord/channel")))
|
||||
return idx
|
||||
|
||||
|
||||
# ── root ──────────────────────────────────────
|
||||
|
||||
|
||||
def test_root_empty():
|
||||
scope = _run(detect_scope("/"))
|
||||
assert scope.level == "root"
|
||||
|
||||
|
||||
def test_root_prefix():
|
||||
scope = _run(detect_scope(_gs("/discord/", prefix="/discord")))
|
||||
assert scope.level == "root"
|
||||
|
||||
|
||||
# ── guild ─────────────────────────────────────
|
||||
|
||||
|
||||
def test_guild(index):
|
||||
scope = _run(
|
||||
detect_scope(_gs("/discord/TestGuild", prefix="/discord"), index))
|
||||
assert scope.level == "guild"
|
||||
assert scope.guild_id == "G1"
|
||||
|
||||
|
||||
def test_guild_channels(index):
|
||||
scope = _run(
|
||||
detect_scope(_gs("/discord/TestGuild/channels", prefix="/discord"),
|
||||
index))
|
||||
assert scope.level == "guild"
|
||||
assert scope.guild_id == "G1"
|
||||
|
||||
|
||||
def test_guild_members(index):
|
||||
scope = _run(
|
||||
detect_scope(_gs("/discord/TestGuild/members", prefix="/discord"),
|
||||
index))
|
||||
assert scope.level == "guild"
|
||||
assert scope.guild_id == "G1"
|
||||
|
||||
|
||||
# ── channel ───────────────────────────────────
|
||||
|
||||
|
||||
def test_channel(index):
|
||||
scope = _run(
|
||||
detect_scope(
|
||||
_gs("/discord/TestGuild/channels/general", prefix="/discord"),
|
||||
index))
|
||||
assert scope.level == "channel"
|
||||
assert scope.guild_id == "G1"
|
||||
assert scope.channel_id == "C1"
|
||||
|
||||
|
||||
# ── date / messages / files ────────────────────
|
||||
|
||||
|
||||
def test_date_dir(index):
|
||||
scope = _run(
|
||||
detect_scope(
|
||||
_gs("/discord/TestGuild/channels/general/2024-04-10",
|
||||
prefix="/discord"), index))
|
||||
assert scope.level == "date"
|
||||
assert scope.guild_id == "G1"
|
||||
assert scope.channel_id == "C1"
|
||||
assert scope.date_str == "2024-04-10"
|
||||
|
||||
|
||||
def test_messages_file(index):
|
||||
scope = _run(
|
||||
detect_scope(
|
||||
_gs("/discord/TestGuild/channels/general/2024-04-10/chat.jsonl",
|
||||
prefix="/discord"), index))
|
||||
assert scope.level == "messages"
|
||||
assert scope.guild_id == "G1"
|
||||
assert scope.channel_id == "C1"
|
||||
assert scope.date_str == "2024-04-10"
|
||||
|
||||
|
||||
def test_files_dir(index):
|
||||
scope = _run(
|
||||
detect_scope(
|
||||
_gs("/discord/TestGuild/channels/general/2024-04-10/files",
|
||||
prefix="/discord"), index))
|
||||
assert scope.level == "files"
|
||||
assert scope.guild_id == "G1"
|
||||
assert scope.channel_id == "C1"
|
||||
assert scope.date_str == "2024-04-10"
|
||||
|
||||
|
||||
def test_file_blob(index):
|
||||
scope = _run(
|
||||
detect_scope(
|
||||
_gs(
|
||||
"/discord/TestGuild/channels/general/2024-04-10/files/"
|
||||
"img__A1.png",
|
||||
prefix="/discord"), index))
|
||||
assert scope.level == "file_blob"
|
||||
assert scope.channel_id == "C1"
|
||||
assert scope.date_str == "2024-04-10"
|
||||
|
||||
|
||||
# ── PathSpec ─────────────────────────────────
|
||||
|
||||
|
||||
def test_glob_jsonl_in_channel(index):
|
||||
gs = PathSpec(
|
||||
resource_path=mount_key("/discord/TestGuild/channels/general/*.jsonl",
|
||||
"/discord"),
|
||||
virtual="/discord/TestGuild/channels/general/*.jsonl",
|
||||
directory="/discord/TestGuild/channels/general/",
|
||||
pattern="*.jsonl",
|
||||
resolved=False,
|
||||
)
|
||||
scope = _run(detect_scope(gs, index))
|
||||
assert scope.level == "channel"
|
||||
assert scope.guild_id == "G1"
|
||||
assert scope.channel_id == "C1"
|
||||
|
||||
|
||||
def test_glob_specific_date(index):
|
||||
gs = PathSpec(
|
||||
resource_path=mount_key(
|
||||
"/discord/TestGuild/channels/general/2024-04-*.jsonl", "/discord"),
|
||||
virtual="/discord/TestGuild/channels/general/2024-04-*.jsonl",
|
||||
directory="/discord/TestGuild/channels/general/",
|
||||
pattern="2024-04-*.jsonl",
|
||||
resolved=False,
|
||||
)
|
||||
scope = _run(detect_scope(gs, index))
|
||||
assert scope.level == "channel"
|
||||
assert scope.channel_id == "C1"
|
||||
|
||||
|
||||
def test_glob_non_jsonl():
|
||||
gs = PathSpec(
|
||||
resource_path="discord/TestGuild/members/*.json",
|
||||
virtual="/discord/TestGuild/members/*.json",
|
||||
directory="/discord/TestGuild/members/",
|
||||
pattern="*.json",
|
||||
resolved=False,
|
||||
)
|
||||
scope = _run(detect_scope(gs))
|
||||
assert scope.level != "channel"
|
||||
|
||||
|
||||
# ── no index ──────────────────────────────────
|
||||
|
||||
|
||||
def test_guild_no_index():
|
||||
scope = _run(detect_scope("TestGuild"))
|
||||
assert scope.level == "guild"
|
||||
assert scope.guild_id is None
|
||||
|
||||
|
||||
def test_channel_no_index():
|
||||
scope = _run(detect_scope("TestGuild/channels/general"))
|
||||
assert scope.level == "channel"
|
||||
assert scope.guild_id is None
|
||||
assert scope.channel_id is None
|
||||
|
||||
|
||||
def _spec(path: str, prefix: str = "/discord") -> PathSpec:
|
||||
return PathSpec(resource_path=mount_key(path, prefix),
|
||||
virtual=path,
|
||||
directory=path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_index():
|
||||
idx = AsyncMock()
|
||||
|
||||
async def _get(virtual_key):
|
||||
result = AsyncMock()
|
||||
if virtual_key.endswith("/myguild/channels/general"):
|
||||
result.entry = type("E", (), {"id": "ch_456"})
|
||||
elif virtual_key.endswith("/myguild"):
|
||||
result.entry = type("E", (), {"id": "g_123"})
|
||||
else:
|
||||
result.entry = None
|
||||
return result
|
||||
|
||||
idx.get.side_effect = _get
|
||||
return idx
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_coalesce_concrete_jsonl_paths_same_channel(fake_index):
|
||||
paths = [
|
||||
_spec(f"/discord/myguild/channels/general/2026-01-{d:02d}/chat.jsonl")
|
||||
for d in range(1, 8)
|
||||
]
|
||||
scope = await coalesce_scopes(paths, fake_index)
|
||||
assert scope is not None
|
||||
assert scope.level == "channel"
|
||||
assert scope.guild_id == "g_123"
|
||||
assert scope.channel_id == "ch_456"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_coalesce_returns_none_for_mixed_channels(fake_index):
|
||||
paths = [
|
||||
_spec("/discord/myguild/channels/general/2026-01-01/chat.jsonl"),
|
||||
_spec("/discord/myguild/channels/random/2026-01-01/chat.jsonl"),
|
||||
]
|
||||
assert await coalesce_scopes(paths, fake_index) is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_coalesce_empty_list_returns_none(fake_index):
|
||||
assert await coalesce_scopes([], fake_index) is None
|
||||
@@ -0,0 +1,147 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from mirage.core.discord.search import search_guild
|
||||
from mirage.resource.discord.config import DiscordConfig
|
||||
|
||||
|
||||
def _run(coro):
|
||||
return asyncio.run(coro)
|
||||
|
||||
|
||||
def _config():
|
||||
return DiscordConfig(token="test-token")
|
||||
|
||||
|
||||
def _make_search_response(messages, total=None):
|
||||
hits = [[msg] for msg in messages]
|
||||
return {"messages": hits, "total_results": total or len(messages)}
|
||||
|
||||
|
||||
def test_search_returns_messages():
|
||||
msgs = [
|
||||
{
|
||||
"id": "100",
|
||||
"content": "hello world",
|
||||
"author": {
|
||||
"username": "a"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "200",
|
||||
"content": "hello again",
|
||||
"author": {
|
||||
"username": "b"
|
||||
}
|
||||
},
|
||||
]
|
||||
with patch("mirage.core.discord.paginate.discord_get",
|
||||
new_callable=AsyncMock,
|
||||
return_value=_make_search_response(msgs)):
|
||||
results = _run(search_guild(_config(), "G1", "hello"))
|
||||
assert len(results) == 2
|
||||
assert results[0]["id"] == "100"
|
||||
assert results[1]["id"] == "200"
|
||||
|
||||
|
||||
def test_search_with_channel_filter():
|
||||
msgs = [{"id": "300", "content": "test", "author": {"username": "c"}}]
|
||||
with patch("mirage.core.discord.paginate.discord_get",
|
||||
new_callable=AsyncMock,
|
||||
return_value=_make_search_response(msgs)) as mock:
|
||||
results = _run(search_guild(_config(), "G1", "test", channel_id="C1"))
|
||||
assert len(results) == 1
|
||||
call_params = mock.call_args[1].get("params") or mock.call_args[0][2]
|
||||
assert call_params["channel_id"] == "C1"
|
||||
assert call_params["content"] == "test"
|
||||
|
||||
|
||||
def test_search_empty_results():
|
||||
with patch("mirage.core.discord.paginate.discord_get",
|
||||
new_callable=AsyncMock,
|
||||
return_value={
|
||||
"messages": [],
|
||||
"total_results": 0
|
||||
}):
|
||||
results = _run(search_guild(_config(), "G1", "nonexistent"))
|
||||
assert results == []
|
||||
|
||||
|
||||
def test_search_sorted_oldest_first():
|
||||
msgs = [
|
||||
{
|
||||
"id": "500",
|
||||
"content": "newer",
|
||||
"author": {
|
||||
"username": "a"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "100",
|
||||
"content": "older",
|
||||
"author": {
|
||||
"username": "b"
|
||||
}
|
||||
},
|
||||
]
|
||||
with patch("mirage.core.discord.paginate.discord_get",
|
||||
new_callable=AsyncMock,
|
||||
return_value=_make_search_response(msgs)):
|
||||
results = _run(search_guild(_config(), "G1", "hello"))
|
||||
assert results[0]["id"] == "100"
|
||||
assert results[1]["id"] == "500"
|
||||
|
||||
|
||||
def test_search_respects_limit():
|
||||
msgs = [{
|
||||
"id": str(i),
|
||||
"content": f"msg{i}",
|
||||
"author": {
|
||||
"username": "a"
|
||||
}
|
||||
} for i in range(10)]
|
||||
with patch("mirage.core.discord.paginate.discord_get",
|
||||
new_callable=AsyncMock,
|
||||
return_value=_make_search_response(msgs, total=10)):
|
||||
results = _run(search_guild(_config(), "G1", "msg", limit=3))
|
||||
assert len(results) == 3
|
||||
|
||||
|
||||
def test_search_paginates():
|
||||
page1 = [{
|
||||
"id": str(i),
|
||||
"content": f"msg{i}",
|
||||
"author": {
|
||||
"username": "a"
|
||||
}
|
||||
} for i in range(25)]
|
||||
page2 = [{
|
||||
"id": str(i + 25),
|
||||
"content": f"msg{i + 25}",
|
||||
"author": {
|
||||
"username": "a"
|
||||
}
|
||||
} for i in range(5)]
|
||||
responses = [
|
||||
_make_search_response(page1, total=30),
|
||||
_make_search_response(page2, total=30),
|
||||
]
|
||||
with patch("mirage.core.discord.paginate.discord_get",
|
||||
new_callable=AsyncMock,
|
||||
side_effect=responses):
|
||||
results = _run(search_guild(_config(), "G1", "msg", limit=100))
|
||||
assert len(results) == 30
|
||||
@@ -0,0 +1,173 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.accessor.discord import DiscordAccessor
|
||||
from mirage.cache.index import IndexEntry
|
||||
from mirage.cache.index.ram import RAMIndexCacheStore
|
||||
from mirage.core.discord.stat import stat
|
||||
from mirage.types import FileType, PathSpec
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def index():
|
||||
store = RAMIndexCacheStore()
|
||||
asyncio.run(
|
||||
store.put(
|
||||
"/My Server",
|
||||
IndexEntry(
|
||||
id="G001",
|
||||
name="My Server",
|
||||
resource_type="discord/guild",
|
||||
vfs_name="My Server",
|
||||
),
|
||||
))
|
||||
asyncio.run(
|
||||
store.put(
|
||||
"/My Server/channels/general",
|
||||
IndexEntry(
|
||||
id="C001",
|
||||
name="general",
|
||||
resource_type="discord/channel",
|
||||
remote_time="794354201395200000",
|
||||
vfs_name="general",
|
||||
),
|
||||
))
|
||||
asyncio.run(
|
||||
store.put(
|
||||
"/My Server/members/alice.json",
|
||||
IndexEntry(
|
||||
id="U001",
|
||||
name="alice",
|
||||
resource_type="discord/member",
|
||||
vfs_name="alice.json",
|
||||
),
|
||||
))
|
||||
return store
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def accessor():
|
||||
return DiscordAccessor(config=object())
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_root(accessor, index):
|
||||
result = await stat(accessor,
|
||||
PathSpec(resource_path="", virtual="/", directory="/"),
|
||||
index)
|
||||
assert result.type == FileType.DIRECTORY
|
||||
assert result.name == "/"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_guild(accessor, index):
|
||||
result = await stat(
|
||||
accessor,
|
||||
PathSpec(resource_path="My Server",
|
||||
virtual="/My Server",
|
||||
directory="/My Server"), index)
|
||||
assert result.type == FileType.DIRECTORY
|
||||
assert result.extra["guild_id"] == "G001"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_channel(accessor, index):
|
||||
result = await stat(
|
||||
accessor,
|
||||
PathSpec(resource_path="My Server/channels/general",
|
||||
virtual="/My Server/channels/general",
|
||||
directory="/My Server/channels/general"), index)
|
||||
assert result.type == FileType.DIRECTORY
|
||||
assert result.extra["channel_id"] == "C001"
|
||||
assert result.modified == "2021-01-01T00:00:00Z"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_member(accessor, index):
|
||||
result = await stat(
|
||||
accessor,
|
||||
PathSpec(resource_path="My Server/members/alice.json",
|
||||
virtual="/My Server/members/alice.json",
|
||||
directory="/My Server/members/alice.json"), index)
|
||||
assert result.type == FileType.JSON
|
||||
assert result.extra["user_id"] == "U001"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_date_dir(accessor, index):
|
||||
path = "/My Server/channels/general/2024-01-15"
|
||||
result = await stat(
|
||||
accessor,
|
||||
PathSpec(virtual=path, directory=path, resource_path=path.strip("/")),
|
||||
index)
|
||||
assert result.type == FileType.DIRECTORY
|
||||
assert result.name == "2024-01-15"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_chat_jsonl(accessor, index):
|
||||
path = "/My Server/channels/general/2024-01-15/chat.jsonl"
|
||||
result = await stat(
|
||||
accessor,
|
||||
PathSpec(virtual=path, directory=path, resource_path=path.strip("/")),
|
||||
index)
|
||||
assert result.type == FileType.TEXT
|
||||
assert result.name == "chat.jsonl"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_files_dir(accessor, index):
|
||||
path = "/My Server/channels/general/2024-01-15/files"
|
||||
result = await stat(
|
||||
accessor,
|
||||
PathSpec(virtual=path, directory=path, resource_path=path.strip("/")),
|
||||
index)
|
||||
assert result.type == FileType.DIRECTORY
|
||||
assert result.name == "files"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_non_date_chat_jsonl_not_found(accessor, index):
|
||||
path = "/My Server/channels/general/notadate/chat.jsonl"
|
||||
with pytest.raises(FileNotFoundError):
|
||||
await stat(
|
||||
accessor,
|
||||
PathSpec(virtual=path,
|
||||
directory=path,
|
||||
resource_path=path.strip("/")), index)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_non_date_files_dir_not_found(accessor, index):
|
||||
path = "/My Server/channels/general/notadate/files"
|
||||
with pytest.raises(FileNotFoundError):
|
||||
await stat(
|
||||
accessor,
|
||||
PathSpec(virtual=path,
|
||||
directory=path,
|
||||
resource_path=path.strip("/")), index)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_not_found(accessor, index):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
await stat(
|
||||
accessor,
|
||||
PathSpec(resource_path="nonexistent/path",
|
||||
virtual="/nonexistent/path",
|
||||
directory="/nonexistent/path"), index)
|
||||
@@ -0,0 +1,98 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.core.discord.guilds import list_guilds_stream
|
||||
from mirage.core.discord.history import (date_to_snowflake,
|
||||
stream_messages_for_day)
|
||||
from mirage.core.discord.members import list_members_stream
|
||||
from mirage.core.discord.search import search_guild_stream
|
||||
from mirage.resource.discord.config import DiscordConfig
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config():
|
||||
return DiscordConfig(token="t")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_guilds_stream_walks_pages(config):
|
||||
with patch("mirage.core.discord.paginate.discord_get",
|
||||
new_callable=AsyncMock,
|
||||
side_effect=[[{
|
||||
"id": "G1"
|
||||
}, {
|
||||
"id": "G2"
|
||||
}], [{
|
||||
"id": "G3"
|
||||
}]]):
|
||||
collected = []
|
||||
async for page in list_guilds_stream(config, page_size=2):
|
||||
collected.append(page)
|
||||
assert collected == [[{"id": "G1"}, {"id": "G2"}], [{"id": "G3"}]]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_members_stream_walks_user_ids(config):
|
||||
page1 = [{"user": {"id": "U1"}}, {"user": {"id": "U2"}}]
|
||||
page2 = [{"user": {"id": "U3"}}]
|
||||
with patch("mirage.core.discord.paginate.discord_get",
|
||||
new_callable=AsyncMock,
|
||||
side_effect=[page1, page2]) as mock:
|
||||
collected = []
|
||||
async for page in list_members_stream(config, "G1", page_size=2):
|
||||
collected.append(page)
|
||||
assert collected == [page1, page2]
|
||||
assert mock.call_args_list[1].kwargs["params"]["after"] == "U2"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stream_messages_for_day_filters_by_date(config):
|
||||
before_int = int(date_to_snowflake("2024-01-15", end=True))
|
||||
in_range = [{"id": str(before_int - 1000), "content": "ok"}]
|
||||
out_of_range = [{"id": str(before_int + 1000), "content": "next-day"}]
|
||||
with patch("mirage.core.discord.paginate.discord_get",
|
||||
new_callable=AsyncMock,
|
||||
return_value=in_range + out_of_range):
|
||||
pages = [
|
||||
p async for p in stream_messages_for_day(
|
||||
config, "C1", "2024-01-15", page_size=2)
|
||||
]
|
||||
flat = [m for page in pages for m in page]
|
||||
assert all(int(m["id"]) <= before_int for m in flat)
|
||||
assert any(m["content"] == "ok" for m in flat)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_guild_stream_flattens_contexts(config):
|
||||
p1 = {
|
||||
"messages": [[{
|
||||
"id": "1"
|
||||
}], [{
|
||||
"id": "2"
|
||||
}]],
|
||||
"total_results": 30,
|
||||
}
|
||||
p2 = {"messages": [[{"id": "3"}]], "total_results": 30}
|
||||
with patch("mirage.core.discord.paginate.discord_get",
|
||||
new_callable=AsyncMock,
|
||||
side_effect=[p1, p2]):
|
||||
collected = []
|
||||
async for page in search_guild_stream(config, "G1", "x", max_pages=2):
|
||||
collected.append(page)
|
||||
flat = [m["id"] for page in collected for m in page]
|
||||
assert flat == ["1", "2", "3"]
|
||||
Reference in New Issue
Block a user