chore: import upstream snapshot with attribution
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:30:44 +08:00
commit bcbd1bdb22
5748 changed files with 562488 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
+120
View File
@@ -0,0 +1,120 @@
# ========= 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.slack.channels import list_channels, list_dms
from mirage.resource.slack.config import SlackConfig
@pytest.fixture
def config():
return SlackConfig(token="xoxb-test-token")
@pytest.mark.asyncio
async def test_list_channels(config):
mock_data = {
"ok":
True,
"channels": [
{
"id": "C001",
"name": "general"
},
{
"id": "C002",
"name": "random"
},
],
"response_metadata": {
"next_cursor": ""
},
}
with patch(
"mirage.core.slack.paginate.slack_get",
new_callable=AsyncMock,
return_value=mock_data,
) as mock_get:
result = await list_channels(config)
assert len(result) == 2
assert result[0]["name"] == "general"
assert result[1]["id"] == "C002"
mock_get.assert_called_once()
call_kwargs = mock_get.call_args
assert call_kwargs.kwargs["params"]["types"] == \
"public_channel,private_channel"
@pytest.mark.asyncio
async def test_list_channels_pagination(config):
page1 = {
"ok": True,
"channels": [{
"id": "C001",
"name": "general"
}],
"response_metadata": {
"next_cursor": "cursor_abc"
},
}
page2 = {
"ok": True,
"channels": [{
"id": "C002",
"name": "random"
}],
"response_metadata": {
"next_cursor": ""
},
}
with patch(
"mirage.core.slack.paginate.slack_get",
new_callable=AsyncMock,
side_effect=[page1, page2],
) as mock_get:
result = await list_channels(config)
assert len(result) == 2
assert mock_get.call_count == 2
second_call = mock_get.call_args_list[1]
assert second_call.kwargs["params"]["cursor"] == "cursor_abc"
@pytest.mark.asyncio
async def test_list_dms(config):
mock_data = {
"ok": True,
"channels": [{
"id": "D001",
"user": "U001"
}],
"response_metadata": {
"next_cursor": ""
},
}
with patch(
"mirage.core.slack.paginate.slack_get",
new_callable=AsyncMock,
return_value=mock_data,
) as mock_get:
result = await list_dms(config)
assert len(result) == 1
assert result[0]["id"] == "D001"
call_kwargs = mock_get.call_args
assert call_kwargs.kwargs["params"]["types"] == "im,mpim"
@@ -0,0 +1,61 @@
# ========= 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 patch
import pytest
from mirage.core.slack.channels import list_channels_stream
from mirage.resource.slack.config import SlackConfig
@pytest.mark.asyncio
async def test_list_channels_stream_yields_pages():
cfg = SlackConfig(token="xoxb-t")
pages = [
{
"channels": [{
"id": "C1"
}, {
"id": "C2"
}],
"response_metadata": {
"next_cursor": "cur1"
}
},
{
"channels": [{
"id": "C3"
}],
"response_metadata": {
"next_cursor": ""
}
},
]
calls = []
async def fake_get(_cfg, method, params=None, token=None):
assert method == "conversations.list"
calls.append(dict(params or {}))
return pages[len(calls) - 1]
with patch("mirage.core.slack.paginate.slack_get", new=fake_get):
seen = []
async for page in list_channels_stream(cfg):
seen.append(page)
assert [ch["id"] for ch in seen[0]] == ["C1", "C2"]
assert [ch["id"] for ch in seen[1]] == ["C3"]
assert calls[0]["types"] == "public_channel,private_channel"
assert calls[0]["exclude_archived"] == "true"
assert calls[0]["limit"] == 200
+191
View File
@@ -0,0 +1,191 @@
# ========= 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.slack._client import slack_get, slack_post
from mirage.resource.slack.config import SlackConfig
@pytest.fixture
def config():
return SlackConfig(token="xoxb-test-token")
@pytest.mark.asyncio
async def test_slack_get_success(config):
mock_resp = AsyncMock()
mock_resp.json = AsyncMock(return_value={
"ok": True,
"channels": [],
})
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.slack._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 slack_get(config,
"conversations.list",
params={"limit": 10})
assert result["ok"] is True
mock_session.get.assert_called_once()
call_kwargs = mock_session.get.call_args
assert "https://slack.com/api/conversations.list" in call_kwargs.args \
or call_kwargs.args[0] == "https://slack.com/api/conversations.list"
assert call_kwargs.kwargs["headers"]["Authorization"] == \
"Bearer xoxb-test-token"
@pytest.mark.asyncio
async def test_slack_get_uses_search_token_for_search_methods():
config = SlackConfig(token="xoxb-bot", search_token="xoxp-user")
mock_resp = AsyncMock()
mock_resp.json = AsyncMock(return_value={
"ok": True,
"messages": {
"matches": [],
},
})
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.slack._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 slack_get(config,
"search.messages",
params={"query": "hello"})
assert result["ok"] is True
call_kwargs = mock_session.get.call_args
assert call_kwargs.kwargs["headers"]["Authorization"] == \
"Bearer xoxp-user"
@pytest.mark.asyncio
async def test_slack_get_error(config):
mock_resp = AsyncMock()
mock_resp.json = AsyncMock(return_value={
"ok": False,
"error": "channel_not_found",
})
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.slack._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=r"\(conversations\.info\): channel_not_found"):
await slack_get(config, "conversations.info")
@pytest.mark.asyncio
async def test_slack_get_missing_scope_surfaces_scopes(config):
mock_resp = AsyncMock()
mock_resp.json = AsyncMock(
return_value={
"ok": False,
"error": "missing_scope",
"needed": "im:read,mpim:read",
"provided": "channels:read,users:read",
})
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.slack._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=(r"\(conversations\.list\): missing_scope "
r"\(needed: im:read,mpim:read; "
r"provided: channels:read,users:read\)"),
):
await slack_get(config, "conversations.list")
@pytest.mark.asyncio
async def test_slack_get_missing_scope_no_needed_falls_back(config):
mock_resp = AsyncMock()
mock_resp.json = AsyncMock(return_value={
"ok": False,
"error": "missing_scope",
})
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.slack._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) as ei:
await slack_get(config, "conversations.list")
assert str(ei.value).endswith(
"Slack API error (conversations.list): missing_scope")
@pytest.mark.asyncio
async def test_slack_post_success(config):
mock_resp = AsyncMock()
mock_resp.json = AsyncMock(return_value={
"ok": True,
"ts": "1234567890.123456",
})
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.slack._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 slack_post(config,
"chat.postMessage",
body={
"channel": "C123",
"text": "hello",
})
assert result["ok"] is True
assert result["ts"] == "1234567890.123456"
mock_session.post.assert_called_once()
call_kwargs = mock_session.post.call_args
assert call_kwargs.args[0] == "https://slack.com/api/chat.postMessage"
+186
View File
@@ -0,0 +1,186 @@
# ========= 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.slack import SlackAccessor
from mirage.cache.index import IndexEntry, RAMIndexCacheStore
from mirage.core.slack.readdir import readdir
from mirage.resource.slack.config import SlackConfig
from mirage.types import PathSpec
@pytest.fixture
def config():
return SlackConfig(token="xoxb-test")
@pytest.fixture
def accessor(config):
return SlackAccessor(config=config)
@pytest.fixture
def index():
return RAMIndexCacheStore()
@pytest.fixture
def messages_with_files():
return [
{
"type":
"message",
"user":
"U1",
"ts":
"1712707200.0",
"text":
"here's the report",
"files": [{
"id":
"F1ABC",
"name":
"report.pdf",
"title":
"report.pdf",
"filetype":
"pdf",
"mimetype":
"application/pdf",
"size":
4096,
"url_private_download": ("https://files.slack.com/files-pri"
"/T1-F1ABC/download/report.pdf"),
"timestamp":
1712707200,
}],
},
{
"type": "message",
"user": "U2",
"ts": "1712707260.0",
"text": "no file here",
},
]
@pytest.mark.asyncio
async def test_files_dir_listing_from_messages(accessor, index,
messages_with_files):
await index.set_dir("/channels", [
("general__C001",
IndexEntry(id="C001",
name="general",
resource_type="slack/channel",
vfs_name="general__C001",
remote_time="1700000000")),
])
await index.set_dir("/channels/general__C001", [
("2026-04-10",
IndexEntry(id="C001:2026-04-10",
name="2026-04-10",
resource_type="slack/date_dir",
vfs_name="2026-04-10")),
])
with patch("mirage.core.slack.readdir.fetch_messages_for_day",
new_callable=AsyncMock,
return_value=messages_with_files):
result = await readdir(
accessor,
PathSpec(resource_path="channels/general__C001/2026-04-10/files",
virtual="/channels/general__C001/2026-04-10/files",
directory="/channels/general__C001/2026-04-10/files"),
index=index,
)
assert result == [
"/channels/general__C001/2026-04-10/files/report__F1ABC.pdf"
]
@pytest.mark.asyncio
async def test_files_dir_empty_on_no_attachments(accessor, index):
await index.set_dir("/channels", [
("general__C001",
IndexEntry(id="C001",
name="general",
resource_type="slack/channel",
vfs_name="general__C001",
remote_time="1700000000")),
])
await index.set_dir("/channels/general__C001", [
("2026-04-10",
IndexEntry(id="C001:2026-04-10",
name="2026-04-10",
resource_type="slack/date_dir",
vfs_name="2026-04-10")),
])
no_file_msgs = [{
"type": "message",
"user": "U1",
"ts": "1712707200.0",
"text": "hi"
}]
with patch("mirage.core.slack.readdir.fetch_messages_for_day",
new_callable=AsyncMock,
return_value=no_file_msgs):
result = await readdir(
accessor,
PathSpec(resource_path="channels/general__C001/2026-04-10/files",
virtual="/channels/general__C001/2026-04-10/files",
directory="/channels/general__C001/2026-04-10/files"),
index=index,
)
assert result == []
@pytest.mark.asyncio
async def test_file_blob_index_entry_stores_url(accessor, index,
messages_with_files):
await index.set_dir("/channels", [
("general__C001",
IndexEntry(id="C001",
name="general",
resource_type="slack/channel",
vfs_name="general__C001",
remote_time="1700000000")),
])
await index.set_dir("/channels/general__C001", [
("2026-04-10",
IndexEntry(id="C001:2026-04-10",
name="2026-04-10",
resource_type="slack/date_dir",
vfs_name="2026-04-10")),
])
with patch("mirage.core.slack.readdir.fetch_messages_for_day",
new_callable=AsyncMock,
return_value=messages_with_files):
await readdir(
accessor,
PathSpec(resource_path="channels/general__C001/2026-04-10/files",
virtual="/channels/general__C001/2026-04-10/files",
directory="/channels/general__C001/2026-04-10/files"),
index=index,
)
blob = await index.get(
"/channels/general__C001/2026-04-10/files/report__F1ABC.pdf")
assert blob.entry is not None
assert blob.entry.id == "F1ABC"
assert blob.entry.size == 4096
assert blob.entry.extra["mimetype"] == "application/pdf"
assert "url_private_download" in blob.entry.extra
assert blob.entry.extra["filetype"] == "pdf"
assert blob.entry.extra["ts"] == "1712707200.0"
+71
View File
@@ -0,0 +1,71 @@
# ========= 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.accessor.slack import SlackAccessor
from mirage.cache.index import IndexEntry, RAMIndexCacheStore
from mirage.core.slack.glob import resolve_glob
from mirage.resource.slack.config import SlackConfig
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
@pytest.fixture
def accessor():
return SlackAccessor(config=SlackConfig(token="xoxb"))
@pytest.fixture
def index():
return RAMIndexCacheStore()
@pytest.mark.asyncio
async def test_resolve_glob_files_pdf(accessor, index):
await index.set_dir("/channels/general__C001/2026-04-10/files", [
("a__F1.pdf",
IndexEntry(id="F1",
name="a",
resource_type="slack/file",
vfs_name="a__F1.pdf",
extra={
"mimetype": "application/pdf",
"url_private_download": "u",
"channel_id": "C001",
"date": "2026-04-10"
})),
("b__F2.txt",
IndexEntry(id="F2",
name="b",
resource_type="slack/file",
vfs_name="b__F2.txt",
extra={
"mimetype": "text/plain",
"url_private_download": "u",
"channel_id": "C001",
"date": "2026-04-10"
})),
])
spec = PathSpec(
resource_path=mount_key(
"/channels/general__C001/2026-04-10/files/*.pdf", ""),
virtual="/channels/general__C001/2026-04-10/files/*.pdf",
directory="/channels/general__C001/2026-04-10/files/",
pattern="*.pdf",
resolved=False,
)
matched = await resolve_glob(accessor, [spec], index=index)
assert len(matched) == 1
assert matched[0].virtual.endswith("a__F1.pdf")
+77
View File
@@ -0,0 +1,77 @@
# ========= 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.slack.history import get_history_jsonl
from mirage.resource.slack.config import SlackConfig
@pytest.fixture
def config():
return SlackConfig(token="xoxb-test-token")
@pytest.mark.asyncio
async def test_get_history_jsonl(config):
mock_data = {
"ok":
True,
"messages": [
{
"text": "second",
"ts": "1700000002.000000"
},
{
"text": "first",
"ts": "1700000001.000000"
},
],
"has_more":
False,
}
with patch(
"mirage.core.slack.paginate.slack_get",
new_callable=AsyncMock,
return_value=mock_data,
):
result = await get_history_jsonl(config, "C001", "2023-11-14")
lines = result.decode().strip().split("\n")
assert len(lines) == 2
first = json.loads(lines[0])
second = json.loads(lines[1])
assert first["text"] == "first"
assert second["text"] == "second"
assert float(first["ts"]) < float(second["ts"])
@pytest.mark.asyncio
async def test_get_history_empty(config):
mock_data = {
"ok": True,
"messages": [],
"has_more": False,
}
with patch(
"mirage.core.slack.paginate.slack_get",
new_callable=AsyncMock,
return_value=mock_data,
):
result = await get_history_jsonl(config, "C001", "2023-11-14")
assert result == b""
@@ -0,0 +1,101 @@
# ========= 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 patch
import pytest
from mirage.core.slack.history import (fetch_messages_for_day,
stream_messages_for_day)
from mirage.resource.slack.config import SlackConfig
@pytest.mark.asyncio
async def test_stream_messages_for_day_applies_day_bounds_and_yields_pages():
cfg = SlackConfig(token="xoxb-t")
pages = [
{
"messages": [{
"ts": "1.0",
"text": "a"
}],
"response_metadata": {
"next_cursor": "cur1"
},
},
{
"messages": [{
"ts": "2.0",
"text": "b"
}],
"response_metadata": {
"next_cursor": ""
},
},
]
calls = []
async def fake_get(_cfg, method, params=None, token=None):
assert method == "conversations.history"
calls.append(dict(params or {}))
return pages[len(calls) - 1]
with patch("mirage.core.slack.paginate.slack_get", new=fake_get):
seen = []
async for page in stream_messages_for_day(cfg, "C1", "2026-05-10"):
seen.append(page)
assert [m["text"] for m in seen[0]] == ["a"]
assert [m["text"] for m in seen[1]] == ["b"]
assert calls[0]["channel"] == "C1"
assert calls[0]["inclusive"] == "true"
assert "oldest" in calls[0]
assert "latest" in calls[0]
assert calls[1]["cursor"] == "cur1"
@pytest.mark.asyncio
async def test_fetch_messages_for_day_collects_and_sorts_across_pages():
cfg = SlackConfig(token="xoxb-t")
pages = [
{
"messages": [{
"ts": "3.0"
}, {
"ts": "1.0"
}],
"response_metadata": {
"next_cursor": "cur1"
},
},
{
"messages": [{
"ts": "2.0"
}],
"response_metadata": {
"next_cursor": ""
},
},
]
calls = {"n": 0}
async def fake_get(_cfg, _method, params=None, token=None):
page = pages[calls["n"]]
calls["n"] += 1
return page
with patch("mirage.core.slack.paginate.slack_get", new=fake_get):
result = await fetch_messages_for_day(cfg, "C1", "2026-05-10")
assert [m["ts"] for m in result] == ["1.0", "2.0", "3.0"]
+98
View File
@@ -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 patch
import pytest
from mirage.core.slack.paginate import cursor_pages
from mirage.resource.slack.config import SlackConfig
@pytest.mark.asyncio
async def test_cursor_pages_walks_until_empty_cursor():
cfg = SlackConfig(token="xoxb-t")
pages = [
{
"items": [1, 2],
"response_metadata": {
"next_cursor": "cur1"
}
},
{
"items": [3],
"response_metadata": {
"next_cursor": ""
}
},
]
calls = []
async def fake_get(_cfg, _method, params=None, token=None):
calls.append(dict(params or {}))
return pages[len(calls) - 1]
with patch("mirage.core.slack.paginate.slack_get", new=fake_get):
result = []
async for page in cursor_pages(cfg,
"conversations.list",
base_params={
"types": "x",
"limit": 100
},
items_key="items"):
result.append(page)
assert result == [[1, 2], [3]]
assert calls[0] == {"types": "x", "limit": 100}
assert calls[1] == {"types": "x", "limit": 100, "cursor": "cur1"}
@pytest.mark.asyncio
async def test_cursor_pages_propagates_cancellation():
cfg = SlackConfig(token="xoxb-t")
pages = [
{
"items": [1],
"response_metadata": {
"next_cursor": "cur1"
}
},
{
"items": [2],
"response_metadata": {
"next_cursor": "cur2"
}
},
{
"items": [3],
"response_metadata": {
"next_cursor": ""
}
},
]
calls = []
async def fake_get(_cfg, _method, params=None, token=None):
calls.append(dict(params or {}))
return pages[len(calls) - 1]
with patch("mirage.core.slack.paginate.slack_get", new=fake_get):
gen = cursor_pages(cfg,
"conversations.list",
base_params={"limit": 1},
items_key="items")
first = await gen.__anext__()
await gen.aclose()
assert first == [1]
assert len(calls) == 1
+212
View File
@@ -0,0 +1,212 @@
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import json
from unittest.mock import AsyncMock, patch
import pytest
from mirage.accessor.slack import SlackAccessor
from mirage.cache.index import IndexEntry, RAMIndexCacheStore
from mirage.core.slack.read import read
from mirage.resource.slack.config import SlackConfig
from mirage.types import PathSpec
@pytest.fixture
def config():
return SlackConfig(token="xoxb-test-token")
@pytest.fixture
def accessor(config):
return SlackAccessor(config=config)
@pytest.fixture
def index():
return RAMIndexCacheStore()
async def _populate_index(index: RAMIndexCacheStore) -> RAMIndexCacheStore:
await index.set_dir("/channels", [
(
"general__C001",
IndexEntry(
id="C001",
name="general",
resource_type="slack/channel",
vfs_name="general__C001",
),
),
])
await index.set_dir("/users", [
(
"alice.json",
IndexEntry(
id="U001",
name="alice",
resource_type="slack/user",
vfs_name="alice.json",
),
),
])
return index
@pytest.mark.asyncio
async def test_read_jsonl(accessor, index):
await _populate_index(index)
history_bytes = b'{"text":"hello","ts":"1700000001"}\n'
with patch(
"mirage.core.slack.read.get_history_jsonl",
new_callable=AsyncMock,
return_value=history_bytes,
) as mock_hist:
result = await read(
accessor,
PathSpec(
resource_path=("/channels/general__C001/2023-11-14/chat.jsonl"
).strip("/"),
virtual="/channels/general__C001/2023-11-14/chat.jsonl",
directory="/channels/general__C001/2023-11-14/chat.jsonl"),
index=index)
assert result == history_bytes
mock_hist.assert_called_once_with(accessor.config, "C001", "2023-11-14")
@pytest.mark.asyncio
async def test_read_file_blob(accessor, index):
await index.set_dir("/channels", [
(
"general__C001",
IndexEntry(
id="C001",
name="general",
resource_type="slack/channel",
vfs_name="general__C001",
),
),
])
await index.set_dir(
"/channels/general__C001/2026-04-10/files",
[
(
"report__F1.pdf",
IndexEntry(
id="F1",
name="report.pdf",
resource_type="slack/file",
vfs_name="report__F1.pdf",
size=4096,
extra={
"mimetype": "application/pdf",
"url_private_download":
"https://files.slack.com/x/report.pdf",
"channel_id": "C001",
"date": "2026-04-10",
},
),
),
],
)
with patch("mirage.core.slack.files.download_file",
new_callable=AsyncMock,
return_value=b"%PDF-1.4 fake bytes"):
data = await read(
accessor,
PathSpec(resource_path=("channels/general__C001/2026-04-10"
"/files/report__F1.pdf"),
virtual=("/channels/general__C001/2026-04-10"
"/files/report__F1.pdf"),
directory=("/channels/general__C001/2026-04-10"
"/files/report__F1.pdf")),
index=index,
)
assert data == b"%PDF-1.4 fake bytes"
@pytest.mark.asyncio
async def test_read_user_json(accessor, index):
await _populate_index(index)
user_data = {
"id": "U001",
"name": "alice",
"real_name": "Alice Smith",
}
with patch(
"mirage.core.slack.read.get_user_profile",
new_callable=AsyncMock,
return_value=user_data,
):
result = await read(accessor,
PathSpec(resource_path="users/alice.json",
virtual="/users/alice.json",
directory="/users/alice.json"),
index=index)
parsed = json.loads(result)
assert parsed["id"] == "U001"
assert parsed["name"] == "alice"
@pytest.mark.asyncio
async def test_read_not_found(accessor, index):
with pytest.raises(FileNotFoundError):
await read(accessor,
PathSpec(resource_path="nonexistent/path",
virtual="/nonexistent/path",
directory="/nonexistent/path"),
index=index)
@pytest.mark.asyncio
async def test_download_file_uses_bot_token():
from mirage.core.slack.files import download_file
seen: list[dict] = []
class _Resp:
async def __aenter__(self):
return self
async def __aexit__(self, *a):
return None
def raise_for_status(self):
return None
async def read(self):
return b"OK"
class _Sess:
async def __aenter__(self):
return self
async def __aexit__(self, *a):
return None
def get(self, url, headers=None):
seen.append(headers)
return _Resp()
with patch("mirage.core.slack.files.aiohttp.ClientSession", _Sess):
await download_file(
SlackConfig(token="xoxb-bot", search_token="xoxp-user"),
"http://x")
await download_file(SlackConfig(token="xoxb-bot"), "http://x")
assert seen[0] == {"Authorization": "Bearer xoxb-bot"}
assert seen[1] == {"Authorization": "Bearer xoxb-bot"}
+274
View File
@@ -0,0 +1,274 @@
# ========= 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 datetime import datetime, timezone
from unittest.mock import AsyncMock, patch
import pytest
from mirage.accessor.slack import SlackAccessor
from mirage.cache.index import IndexEntry, RAMIndexCacheStore
from mirage.core.slack.readdir import _date_range, readdir
from mirage.resource.slack.config import SlackConfig
from mirage.types import PathSpec
@pytest.fixture
def config():
return SlackConfig(token="xoxb-test-token")
@pytest.fixture
def accessor(config):
return SlackAccessor(config=config)
@pytest.fixture
def index():
return RAMIndexCacheStore()
@pytest.mark.asyncio
async def test_readdir_root(accessor, index):
result = await readdir(accessor,
PathSpec(resource_path="",
virtual="/",
directory="/"),
index=index)
assert result == ["/channels", "/dms", "/users"]
@pytest.mark.asyncio
async def test_readdir_channels(accessor, index):
channels = [
{
"id": "C001",
"name": "general"
},
{
"id": "C002",
"name": "random"
},
]
with patch(
"mirage.core.slack.readdir.list_channels",
new_callable=AsyncMock,
return_value=channels,
):
result = await readdir(accessor,
PathSpec(resource_path="channels",
virtual="/channels",
directory="/channels"),
index=index)
assert "/channels/general__C001" in result
assert "/channels/random__C002" in result
@pytest.mark.asyncio
async def test_readdir_users(accessor, index):
users = [
{
"id": "U001",
"name": "alice"
},
{
"id": "U002",
"name": "bob"
},
]
with patch(
"mirage.core.slack.readdir.list_users",
new_callable=AsyncMock,
return_value=users,
):
result = await readdir(accessor,
PathSpec(resource_path="users",
virtual="/users",
directory="/users"),
index=index)
assert "/users/alice__U001.json" in result
assert "/users/bob__U002.json" in result
@pytest.mark.asyncio
async def test_readdir_channel_dates(accessor, index):
await index.set_dir("/channels", [
(
"general__C001",
IndexEntry(
id="C001",
name="general",
resource_type="slack/channel",
vfs_name="general__C001",
),
),
])
now = datetime.now(timezone.utc)
with patch("mirage.core.slack.readdir._latest_message_ts",
new_callable=AsyncMock,
return_value=now.timestamp()):
result = await readdir(accessor,
PathSpec(resource_path="channels/general__C001",
virtual="/channels/general__C001",
directory="/channels/general__C001"),
index=index)
assert len(result) >= 1
assert all(not r.endswith(".jsonl") for r in result)
assert all(r.startswith("/channels/general__C001/") for r in result)
assert result[0].endswith(now.strftime('%Y-%m-%d'))
def test_date_range_recent():
now = datetime.now(timezone.utc)
latest = now.timestamp()
created = int(now.timestamp()) - 86400 * 5
dates = _date_range(latest, created)
assert len(dates) == 6
assert dates[0] == now.strftime("%Y-%m-%d")
def test_date_range_capped_at_90():
now = datetime.now(timezone.utc)
dates = _date_range(now.timestamp(), 1000000000)
assert len(dates) == 90
@pytest.mark.asyncio
async def test_readdir_channels_stores_created(accessor, index):
channels = [
{
"id": "C001",
"name": "general",
"created": 1700000000
},
]
with patch(
"mirage.core.slack.readdir.list_channels",
new_callable=AsyncMock,
return_value=channels,
):
await readdir(accessor,
PathSpec(resource_path="channels",
virtual="/channels",
directory="/channels"),
index=index)
lookup = await index.get("/channels/general__C001")
assert lookup.entry is not None
assert lookup.entry.remote_time == "1700000000"
@pytest.mark.asyncio
async def test_readdir_channel_dates_with_created(accessor, index):
await index.set_dir("/channels", [
(
"general__C001",
IndexEntry(
id="C001",
name="general",
resource_type="slack/channel",
remote_time="1700000000",
vfs_name="general__C001",
),
),
])
now = datetime.now(timezone.utc)
with patch("mirage.core.slack.readdir._latest_message_ts",
new_callable=AsyncMock,
return_value=now.timestamp()):
result = await readdir(accessor,
PathSpec(resource_path="channels/general__C001",
virtual="/channels/general__C001",
directory="/channels/general__C001"),
index=index)
assert len(result) == 90
assert all(not r.endswith(".jsonl") for r in result)
assert result[0].endswith(now.strftime('%Y-%m-%d'))
@pytest.mark.asyncio
async def test_readdir_channel_dates_cached_in_entries(accessor, index):
await index.set_dir("/channels", [
(
"general__C001",
IndexEntry(
id="C001",
name="general",
resource_type="slack/channel",
remote_time="1700000000",
vfs_name="general__C001",
),
),
])
now = datetime.now(timezone.utc)
with patch("mirage.core.slack.readdir._latest_message_ts",
new_callable=AsyncMock,
return_value=now.timestamp()):
await readdir(accessor,
PathSpec(resource_path="channels/general__C001",
virtual="/channels/general__C001",
directory="/channels/general__C001"),
index=index)
listing = await index.list_dir("/channels/general__C001")
assert listing.entries is not None
assert len(listing.entries) == 90
assert all(not e.endswith(".jsonl") for e in listing.entries)
@pytest.mark.asyncio
async def test_readdir_date_dir_returns_chat_and_files(accessor, index):
await index.set_dir("/channels", [
(
"general__C001",
IndexEntry(
id="C001",
name="general",
resource_type="slack/channel",
vfs_name="general__C001",
remote_time="1700000000",
),
),
])
await index.set_dir("/channels/general__C001", [
(
"2026-04-10",
IndexEntry(
id="C001:2026-04-10",
name="2026-04-10",
resource_type="slack/date_dir",
vfs_name="2026-04-10",
),
),
])
with patch("mirage.core.slack.readdir.fetch_messages_for_day",
new_callable=AsyncMock,
return_value=[]):
result = await readdir(
accessor,
PathSpec(resource_path="channels/general__C001/2026-04-10",
virtual="/channels/general__C001/2026-04-10",
directory="/channels/general__C001/2026-04-10"),
index=index,
)
assert sorted(result) == [
"/channels/general__C001/2026-04-10/chat.jsonl",
"/channels/general__C001/2026-04-10/files",
]
@@ -0,0 +1,129 @@
# ========= 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.slack import SlackAccessor
from mirage.cache.index import RAMIndexCacheStore
from mirage.core.slack.readdir import _fetch_day, _latest_message_ts, readdir
from mirage.resource.slack.config import SlackConfig
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
@pytest.fixture
def config():
return SlackConfig(token="xoxb-test")
@pytest.fixture
def index():
return RAMIndexCacheStore(ttl=600)
@pytest.mark.asyncio
async def test_latest_message_ts_returns_none_on_not_in_channel(config):
err = RuntimeError(
"Slack API error (conversations.history): not_in_channel")
with patch("mirage.core.slack.readdir.slack_get",
new=AsyncMock(side_effect=err)):
result = await _latest_message_ts(config, "C_INACCESSIBLE")
assert result is None
@pytest.mark.asyncio
async def test_latest_message_ts_returns_none_on_missing_scope(config):
err = RuntimeError(
"Slack API error (conversations.history): missing_scope "
"(needed: channels:history; provided: channels:read)")
with patch("mirage.core.slack.readdir.slack_get",
new=AsyncMock(side_effect=err)):
result = await _latest_message_ts(config, "C_NO_SCOPE")
assert result is None
@pytest.mark.asyncio
async def test_latest_message_ts_reraises_unrelated_errors(config):
err = RuntimeError("Slack API error (conversations.history): rate_limited")
with patch("mirage.core.slack.readdir.slack_get",
new=AsyncMock(side_effect=err)):
with pytest.raises(RuntimeError, match="rate_limited"):
await _latest_message_ts(config, "C1")
@pytest.mark.asyncio
async def test_fetch_day_seals_empty_dir_on_not_in_channel(config, index):
err = RuntimeError(
"Slack API error (conversations.history): not_in_channel")
accessor = SlackAccessor(config=config)
async def fake_history(_cfg, channel_id, date_str):
raise err
with patch("mirage.core.slack.readdir.fetch_messages_for_day",
new=fake_history):
await _fetch_day(accessor, "C_INACCESSIBLE", "2026-05-10",
"/slack/channels/foo__C_INACCESSIBLE/2026-05-10",
index)
listing = await index.list_dir(
"/slack/channels/foo__C_INACCESSIBLE/2026-05-10")
assert listing.entries == []
@pytest.mark.asyncio
async def test_readdir_channel_inaccessible_yields_no_dates(config, index):
"""Full integration: ls /slack/channels/inaccessible/ → no dates."""
accessor = SlackAccessor(config=config)
channels_page = {
"channels": [{
"id": "C_INACCESSIBLE",
"name": "private",
"created": 1
}],
"response_metadata": {
"next_cursor": ""
},
}
err = RuntimeError(
"Slack API error (conversations.history): not_in_channel")
async def fake_get(_cfg, method, params=None, token=None):
if method == "conversations.list":
return channels_page
if method == "conversations.history":
raise err
raise AssertionError(f"unexpected {method}")
with patch("mirage.core.slack.paginate.slack_get", new=fake_get), \
patch("mirage.core.slack.readdir.slack_get", new=fake_get):
await readdir(
accessor,
PathSpec(resource_path=mount_key("/slack/channels", "/slack"),
virtual="/slack/channels",
directory="/slack/channels/"),
index,
)
dates = await readdir(
accessor,
PathSpec(
resource_path=mount_key(
"/slack/channels/private__C_INACCESSIBLE", "/slack"),
virtual="/slack/channels/private__C_INACCESSIBLE",
directory="/slack/channels/private__C_INACCESSIBLE/",
),
index,
)
assert dates == []
+223
View File
@@ -0,0 +1,223 @@
# ========= 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.slack.scope import coalesce_scopes, detect_scope
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_key
def _gs(path: str,
prefix: str = "",
pattern: str | None = None,
directory: str | None = None) -> PathSpec:
return PathSpec(
resource_path=mount_key(path, prefix),
virtual=path,
directory=directory
or (path.rsplit("/", 1)[0] + "/" if "/" in path else "/"),
pattern=pattern,
)
def test_root_empty():
scope = detect_scope("/")
assert scope.use_native is True
assert scope.channel_name is None
def test_root_with_prefix():
scope = detect_scope(_gs("/slack/", prefix="/slack"))
assert scope.use_native is True
def test_channels_root():
scope = detect_scope(_gs("/slack/channels", prefix="/slack"))
assert scope.use_native is True
assert scope.container == "channels"
assert scope.channel_name is None
def test_channel_dir():
scope = detect_scope(_gs("/slack/channels/general__C1", prefix="/slack"))
assert scope.use_native is True
assert scope.container == "channels"
assert scope.channel_name == "general"
assert scope.channel_id == "C1"
def test_channel_dm_dir():
scope = detect_scope(_gs("/slack/dms/alice__D1", prefix="/slack"))
assert scope.use_native is True
assert scope.container == "dms"
assert scope.channel_name == "alice"
assert scope.channel_id == "D1"
def test_channel_glob_jsonl():
spec = PathSpec(
resource_path=mount_key("/slack/channels/general__C1/*/chat.jsonl",
"/slack"),
virtual="/slack/channels/general__C1/*/chat.jsonl",
directory="/slack/channels/general__C1/",
pattern="*/chat.jsonl",
resolved=False,
)
scope = detect_scope(spec)
assert scope.use_native is True
assert scope.channel_name == "general"
assert scope.channel_id == "C1"
def test_specific_chat_jsonl():
scope = detect_scope(
_gs("/slack/channels/general__C1/2026-04-10/chat.jsonl",
prefix="/slack"))
assert scope.use_native is False
assert scope.date_str == "2026-04-10"
assert scope.channel_name == "general"
assert scope.channel_id == "C1"
def test_users_dir_not_native():
scope = detect_scope(_gs("/slack/users", prefix="/slack"))
assert scope.use_native is False
def test_users_file_not_native():
scope = detect_scope(_gs("/slack/users/alice__U1.json", prefix="/slack"))
assert scope.use_native is False
def test_unknown_root_not_native():
scope = detect_scope(_gs("/slack/whatever", prefix="/slack"))
assert scope.use_native is False
def test_dirname_without_id():
scope = detect_scope(_gs("/slack/channels/general", prefix="/slack"))
assert scope.use_native is True
assert scope.channel_name == "general"
assert scope.channel_id is None
def _spec(path: str, prefix: str = "/slack") -> PathSpec:
return PathSpec(resource_path=mount_key(path, prefix),
virtual=path,
directory=path)
def test_coalesce_concrete_jsonl_paths_same_channel():
paths = [
_spec(f"/slack/channels/general__C1/2026-01-{day:02d}/chat.jsonl")
for day in range(1, 8)
]
scope = coalesce_scopes(paths)
assert scope is not None
assert scope.use_native is True
assert scope.channel_name == "general"
assert scope.channel_id == "C1"
assert scope.container == "channels"
def test_coalesce_returns_none_for_mixed_channels():
paths = [
_spec("/slack/channels/general__C1/2026-01-01/chat.jsonl"),
_spec("/slack/channels/random__C2/2026-01-01/chat.jsonl"),
]
assert coalesce_scopes(paths) is None
def test_coalesce_returns_none_for_mixed_containers():
paths = [
_spec("/slack/channels/general__C1/2026-01-01/chat.jsonl"),
_spec("/slack/dms/alice__D1/2026-01-01/chat.jsonl"),
]
assert coalesce_scopes(paths) is None
def test_coalesce_single_path_delegates_to_detect_scope():
p = _spec("/slack/channels/general__C1/2026-01-01/chat.jsonl")
scope = coalesce_scopes([p])
assert scope is not None
assert scope.use_native is True
assert scope.channel_name == "general"
def test_coalesce_empty_list_returns_none():
assert coalesce_scopes([]) is None
def test_date_dir():
scope = detect_scope(
_gs("/slack/channels/general__C1/2026-04-10", prefix="/slack"))
assert scope.use_native is True
assert scope.date_str == "2026-04-10"
assert scope.channel_name == "general"
assert scope.channel_id == "C1"
assert scope.target == "date"
def test_files_dir():
scope = detect_scope(
_gs("/slack/channels/general__C1/2026-04-10/files", prefix="/slack"))
assert scope.use_native is True
assert scope.date_str == "2026-04-10"
assert scope.target == "files"
def test_specific_file_blob():
scope = detect_scope(
_gs("/slack/channels/general__C1/2026-04-10/files/report__F1.pdf",
prefix="/slack"))
assert scope.use_native is False
assert scope.date_str == "2026-04-10"
assert scope.target == "files"
assert scope.channel_name == "general"
assert scope.channel_id == "C1"
def test_glob_files_in_day():
spec = PathSpec(
resource_path=mount_key(
"/slack/channels/general__C1/2026-04-10/files/*.pdf", "/slack"),
virtual="/slack/channels/general__C1/2026-04-10/files/*.pdf",
directory="/slack/channels/general__C1/2026-04-10/files/",
pattern="*.pdf",
resolved=False,
)
scope = detect_scope(spec)
assert scope.use_native is True
assert scope.target == "files"
def test_chat_jsonl_target():
scope = detect_scope(
_gs("/slack/channels/general__C1/2026-04-10/chat.jsonl",
prefix="/slack"))
assert scope.target == "messages"
def test_depth3_non_date_falls_through():
scope = detect_scope(
_gs("/slack/channels/general__C1/notadate", prefix="/slack"))
assert scope.use_native is False
assert scope.target is None
def test_depth4_unknown_leaf_under_date_not_native():
scope = detect_scope(
_gs("/slack/channels/general__C1/2026-04-10/random.txt",
prefix="/slack"))
assert scope.use_native is False
assert scope.target is None
+82
View File
@@ -0,0 +1,82 @@
# ========= 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.slack.formatters import format_grep_results
from mirage.core.slack.scope import SlackScope
from mirage.core.slack.search import search_messages
from mirage.resource.slack.config import SlackConfig
@pytest.mark.asyncio
async def test_search_messages_defaults_include_page_1():
cfg = SlackConfig(token="xoxp-test")
with patch(
"mirage.core.slack.search.slack_get",
new=AsyncMock(return_value={"ok": True}),
) as fake_get:
await search_messages(cfg, "hello")
params = fake_get.call_args.kwargs["params"]
assert params["query"] == "hello"
assert params["count"] == 20
assert params["page"] == 1
assert params["sort"] == "timestamp"
@pytest.mark.asyncio
async def test_search_messages_forwards_explicit_count_and_page():
cfg = SlackConfig(token="xoxp-test")
with patch(
"mirage.core.slack.search.slack_get",
new=AsyncMock(return_value={"ok": True}),
) as fake_get:
await search_messages(cfg, "hello", count=50, page=3)
params = fake_get.call_args.kwargs["params"]
assert params["count"] == 50
assert params["page"] == 3
def test_format_grep_results_path_uses_chat_jsonl():
raw_payload = {
"messages": {
"matches": [
{
"channel": {
"id": "C001",
"name": "general"
},
"user": "U1",
"ts": "1712707200.0",
"text": "hello",
},
],
},
}
raw = json.dumps(raw_payload).encode()
scope = SlackScope(
use_native=True,
container="channels",
channel_name="general",
channel_id="C001",
target="messages",
)
lines = format_grep_results(raw, scope, "/slack")
assert len(lines) == 1
line = lines[0]
assert line.startswith(
"/slack/channels/general__C001/2024-04-10/chat.jsonl:"), line
@@ -0,0 +1,157 @@
# ========= 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.slack.formatters import build_query, format_file_grep_results
from mirage.core.slack.scope import SlackScope
from mirage.core.slack.search import search_files
from mirage.resource.slack.config import SlackConfig
@pytest.mark.asyncio
async def test_search_files_calls_correct_endpoint():
config = SlackConfig(token="xoxb", search_token="xoxp")
fake_response = {
"ok": True,
"files": {
"matches": []
},
}
with patch("mirage.core.slack.search.slack_get",
new_callable=AsyncMock,
return_value=fake_response) as mock:
await search_files(config, "report")
args, kwargs = mock.call_args
assert args[1] == "search.files"
assert kwargs["params"]["query"] == "report"
assert "token" not in kwargs
def test_format_file_grep_results_renders_paths():
raw_payload = {
"files": {
"matches": [
{
"id": "F1ABC",
"name": "report.pdf",
"title": "Q4 Report",
"filetype": "pdf",
"channels": ["C001"],
"timestamp": 1712707200,
},
],
},
}
raw = json.dumps(raw_payload).encode()
scope = SlackScope(use_native=True,
container="channels",
channel_name="general",
channel_id="C001",
target="files")
lines = format_file_grep_results(raw, scope, "/slack")
assert len(lines) == 1
line = lines[0]
assert "files/" in line
assert "F1ABC" in line
assert "[file]" in line
assert "Q4 Report" in line
def test_build_query_unchanged_for_files():
scope = SlackScope(use_native=True,
container="channels",
channel_name="eng",
channel_id="C1",
target="files")
assert build_query("foo", scope) == "in:#eng foo"
def test_format_file_grep_results_emits_exact_path():
raw_payload = {
"files": {
"matches": [{
"id": "F1ABC",
"name": "report.pdf",
"title": "Q4 Report",
"filetype": "pdf",
"channels": ["C001"],
"timestamp": 1712707200,
}],
},
}
raw = json.dumps(raw_payload).encode()
scope = SlackScope(use_native=True,
container="channels",
channel_name="general",
channel_id="C001",
target="files")
lines = format_file_grep_results(raw, scope, "/slack")
assert len(lines) == 1
expected_path = ("/slack/channels/general__C001/2024-04-10/files/"
"report__F1ABC.pdf")
assert lines[0].startswith(expected_path + ":"), lines[0]
def test_format_file_grep_results_skips_when_no_scope_channel():
raw_payload = {
"files": {
"matches": [{
"id": "F1ABC",
"name": "report.pdf",
"title": "Q4 Report",
"channels": ["C001"],
"timestamp": 1712707200,
}],
},
}
raw = json.dumps(raw_payload).encode()
scope = SlackScope(use_native=True,
container="channels",
channel_name=None,
channel_id=None,
target="files")
lines = format_file_grep_results(raw, scope, "/slack")
assert lines == []
def test_format_file_grep_results_preserves_special_chars():
raw_payload = {
"files": {
"matches": [{
"id": "F1ABC",
"name": "Q4 Report (final).pdf",
"title": "Q4",
"channels": ["C001"],
"timestamp": 1712707200,
}],
},
}
raw = json.dumps(raw_payload).encode()
scope = SlackScope(
use_native=True,
container="channels",
channel_name="general",
channel_id="C001",
target="files",
)
lines = format_file_grep_results(raw, scope, "/slack")
assert len(lines) == 1
line = lines[0]
assert "F1ABC" in line
blob_segment = line.split("/files/")[1].split(":")[0]
assert blob_segment == "Q4 Report (final)__F1ABC.pdf"
+256
View File
@@ -0,0 +1,256 @@
# ========= 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.accessor.slack import SlackAccessor
from mirage.cache.index import IndexEntry, RAMIndexCacheStore
from mirage.core.slack.stat import stat
from mirage.resource.slack.config import SlackConfig
from mirage.types import FileType, PathSpec
@pytest.fixture
def config():
return SlackConfig(token="xoxb-test-token")
@pytest.fixture
def accessor(config):
return SlackAccessor(config=config)
@pytest.fixture
def index():
return RAMIndexCacheStore()
async def _populate_index(index: RAMIndexCacheStore) -> None:
await index.set_dir("/channels", [
(
"general__C001",
IndexEntry(
id="C001",
name="general",
resource_type="slack/channel",
remote_time="1609459200",
vfs_name="general__C001",
),
),
])
await index.set_dir("/users", [
(
"alice.json",
IndexEntry(
id="U001",
name="alice",
resource_type="slack/user",
vfs_name="alice.json",
),
),
])
@pytest.mark.asyncio
async def test_stat_root(accessor, index):
result = await stat(accessor,
PathSpec(resource_path="", virtual="/", directory="/"),
index=index)
assert result.type == FileType.DIRECTORY
assert result.name == "/"
@pytest.mark.asyncio
async def test_stat_channel(accessor, index):
await _populate_index(index)
result = await stat(accessor,
PathSpec(resource_path="channels/general__C001",
virtual="/channels/general__C001",
directory="/channels/general__C001"),
index=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_user(accessor, index):
await _populate_index(index)
result = await stat(accessor,
PathSpec(resource_path="users/alice.json",
virtual="/users/alice.json",
directory="/users/alice.json"),
index=index)
assert result.type == FileType.JSON
assert result.extra["user_id"] == "U001"
@pytest.mark.asyncio
async def test_stat_jsonl(accessor, index):
await _populate_index(index)
result = await stat(
accessor,
PathSpec(resource_path="channels/general__C001/2023-11-14/chat.jsonl",
virtual="/channels/general__C001/2023-11-14/chat.jsonl",
directory="/channels/general__C001/2023-11-14/chat.jsonl"),
index=index)
assert result.type == FileType.TEXT
assert result.name == "chat.jsonl"
@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=index)
@pytest.mark.asyncio
async def test_stat_date_dir(accessor, index):
await index.set_dir("/channels/general__C001", [
("2026-04-10",
IndexEntry(id="C001:2026-04-10",
name="2026-04-10",
resource_type="slack/date_dir",
vfs_name="2026-04-10")),
])
s = await stat(accessor,
PathSpec(resource_path="channels/general__C001/2026-04-10",
virtual="/channels/general__C001/2026-04-10",
directory="/channels/general__C001/2026-04-10"),
index=index)
assert s.type == FileType.DIRECTORY
@pytest.mark.asyncio
async def test_stat_non_date_dir_not_found(accessor, index):
with pytest.raises(FileNotFoundError):
await stat(accessor,
PathSpec(resource_path="channels/general__C001/notadate",
virtual="/channels/general__C001/notadate",
directory="/channels/general__C001/notadate"),
index=index)
@pytest.mark.asyncio
async def test_stat_chat_jsonl(accessor, index):
s = await stat(
accessor,
PathSpec(resource_path="channels/general__C001/2026-04-10/chat.jsonl",
virtual="/channels/general__C001/2026-04-10/chat.jsonl",
directory="/channels/general__C001/2026-04-10/chat.jsonl"),
index=index)
assert s.type == FileType.TEXT
@pytest.mark.asyncio
async def test_stat_files_dir(accessor, index):
s = await stat(accessor,
PathSpec(
resource_path="channels/general__C001/2026-04-10/files",
virtual="/channels/general__C001/2026-04-10/files",
directory="/channels/general__C001/2026-04-10/files"),
index=index)
assert s.type == FileType.DIRECTORY
@pytest.mark.asyncio
async def test_stat_file_blob_pdf(accessor, index):
await index.set_dir("/channels/general__C001/2026-04-10/files", [
("report__F1.pdf",
IndexEntry(id="F1",
name="report",
resource_type="slack/file",
vfs_name="report__F1.pdf",
size=4096,
extra={
"mimetype": "application/pdf",
"url_private_download": "u",
"channel_id": "C001",
"date": "2026-04-10"
})),
])
s = await stat(
accessor,
PathSpec(
resource_path=(
"/channels/general__C001/2026-04-10/files/report__F1.pdf"
).strip("/"),
virtual="/channels/general__C001/2026-04-10/files/report__F1.pdf",
directory="/channels/general__C001/2026-04-10/files/report__F1.pdf"
),
index=index)
assert s.type == FileType.PDF
assert s.size == 4096
@pytest.mark.asyncio
async def test_stat_file_blob_text(accessor, index):
await index.set_dir("/channels/general__C001/2026-04-10/files", [
("notes__F2.txt",
IndexEntry(id="F2",
name="notes",
resource_type="slack/file",
vfs_name="notes__F2.txt",
size=128,
extra={
"mimetype": "text/plain",
"url_private_download": "u",
"channel_id": "C001",
"date": "2026-04-10"
})),
])
s = await stat(
accessor,
PathSpec(
resource_path=(
"/channels/general__C001/2026-04-10/files/notes__F2.txt"
).strip("/"),
virtual="/channels/general__C001/2026-04-10/files/notes__F2.txt",
directory="/channels/general__C001/2026-04-10/files/notes__F2.txt"
),
index=index)
assert s.type == FileType.TEXT
@pytest.mark.asyncio
async def test_stat_file_blob_unknown_mimetype_is_binary(accessor, index):
await index.set_dir("/channels/general__C001/2026-04-10/files", [
("data__F3.bin",
IndexEntry(id="F3",
name="data",
resource_type="slack/file",
vfs_name="data__F3.bin",
size=2048,
extra={
"mimetype": "application/octet-stream",
"url_private_download": "u",
"channel_id": "C001",
"date": "2026-04-10"
})),
])
s = await stat(
accessor,
PathSpec(
resource_path=(
"/channels/general__C001/2026-04-10/files/data__F3.bin"
).strip("/"),
virtual="/channels/general__C001/2026-04-10/files/data__F3.bin",
directory="/channels/general__C001/2026-04-10/files/data__F3.bin"),
index=index)
assert s.type == FileType.BINARY
assert s.size == 2048
+181
View File
@@ -0,0 +1,181 @@
# ========= 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.slack.users import get_user_profile, list_users, search_users
from mirage.resource.slack.config import SlackConfig
@pytest.fixture
def config():
return SlackConfig(token="xoxb-test-token")
@pytest.mark.asyncio
async def test_list_users(config):
mock_data = {
"ok":
True,
"members": [
{
"id": "U001",
"name": "alice",
"deleted": False,
"is_bot": False
},
{
"id": "U002",
"name": "bot-helper",
"deleted": False,
"is_bot": True
},
{
"id": "U003",
"name": "gone",
"deleted": True,
"is_bot": False
},
{
"id": "USLACKBOT",
"name": "slackbot",
"deleted": False,
"is_bot": False
},
{
"id": "U004",
"name": "bob",
"deleted": False,
"is_bot": False
},
],
}
with patch(
"mirage.core.slack.paginate.slack_get",
new_callable=AsyncMock,
return_value=mock_data,
):
result = await list_users(config)
assert len(result) == 2
names = [u["name"] for u in result]
assert "alice" in names
assert "bob" in names
assert "bot-helper" not in names
assert "gone" not in names
assert "slackbot" not in names
@pytest.mark.asyncio
async def test_search_users(config):
mock_data = {
"ok":
True,
"members": [
{
"id": "U001",
"name": "alice",
"real_name": "Alice Smith",
"deleted": False,
"is_bot": False,
"profile": {
"email": "alice@example.com"
}
},
{
"id": "U002",
"name": "bob",
"real_name": "Bob Jones",
"deleted": False,
"is_bot": False,
"profile": {
"email": "bob@example.com"
}
},
],
}
with patch(
"mirage.core.slack.paginate.slack_get",
new_callable=AsyncMock,
return_value=mock_data,
):
result = await search_users(config, "alice")
assert len(result) == 1
assert result[0]["name"] == "alice"
@pytest.mark.asyncio
async def test_search_users_by_email(config):
mock_data = {
"ok":
True,
"members": [
{
"id": "U001",
"name": "alice",
"real_name": "Alice Smith",
"deleted": False,
"is_bot": False,
"profile": {
"email": "alice@example.com"
}
},
{
"id": "U002",
"name": "bob",
"real_name": "Bob Jones",
"deleted": False,
"is_bot": False,
"profile": {
"email": "bob@example.com"
}
},
],
}
with patch(
"mirage.core.slack.paginate.slack_get",
new_callable=AsyncMock,
return_value=mock_data,
):
result = await search_users(config, "bob@example")
assert len(result) == 1
assert result[0]["name"] == "bob"
@pytest.mark.asyncio
async def test_get_user_profile(config):
mock_data = {
"ok": True,
"user": {
"id": "U001",
"name": "alice",
"real_name": "Alice Smith",
"profile": {
"email": "alice@example.com"
},
},
}
with patch(
"mirage.core.slack.users.slack_get",
new_callable=AsyncMock,
return_value=mock_data,
):
result = await get_user_profile(config, "U001")
assert result["id"] == "U001"
assert result["name"] == "alice"
@@ -0,0 +1,82 @@
# ========= 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 patch
import pytest
from mirage.core.slack.users import list_users_stream
from mirage.resource.slack.config import SlackConfig
@pytest.mark.asyncio
async def test_list_users_stream_walks_pages_and_filters():
cfg = SlackConfig(token="xoxb-t")
pages = [
{
"members": [
{
"id": "U1",
"name": "alice",
"deleted": False,
"is_bot": False
},
{
"id": "U2",
"name": "bot",
"deleted": False,
"is_bot": True
},
],
"response_metadata": {
"next_cursor": "cur1"
},
},
{
"members": [
{
"id": "USLACKBOT",
"name": "slackbot",
"deleted": False,
"is_bot": False
},
{
"id": "U3",
"name": "bob",
"deleted": False,
"is_bot": False
},
],
"response_metadata": {
"next_cursor": ""
},
},
]
calls = []
async def fake_get(_cfg, method, params=None, token=None):
assert method == "users.list"
calls.append(dict(params or {}))
return pages[len(calls) - 1]
with patch("mirage.core.slack.paginate.slack_get", new=fake_get):
seen = []
async for page in list_users_stream(cfg):
seen.append(page)
assert len(seen) == 2
assert [u["name"] for u in seen[0]] == ["alice"]
assert [u["name"] for u in seen[1]] == ["bob"]
assert calls[0]["limit"] == 200
assert calls[1]["cursor"] == "cur1"