bcbd1bdb22
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
324 lines
9.8 KiB
Python
324 lines
9.8 KiB
Python
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
|
|
|
import asyncio
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import pytest
|
|
|
|
from mirage.cache.index import IndexEntry
|
|
from mirage.fuse.fs import MirageFS
|
|
from mirage.ops import Ops
|
|
from mirage.ops.config import OpsMount
|
|
from mirage.resource.discord import DiscordConfig, DiscordResource
|
|
from mirage.types import FileType, MountMode
|
|
|
|
GUILD = "TestGuild"
|
|
CHANNEL = "general"
|
|
DATE = "2024-01-15"
|
|
FILE = "chat.jsonl"
|
|
GUILD_PATH = f"{GUILD}"
|
|
CHANNEL_PATH = f"{GUILD}/channels/{CHANNEL}"
|
|
DATE_DIR_PATH = f"{GUILD}/channels/{CHANNEL}/{DATE}"
|
|
FILE_PATH = f"{GUILD}/channels/{CHANNEL}/{DATE}/{FILE}"
|
|
MEMBER_PATH = f"{GUILD}/members/alice.json"
|
|
|
|
PREFIX = "/discord"
|
|
|
|
FAKE_JSONL = (b'{"id":"1","content":"hello","author":{"username":"alice"}}\n'
|
|
b'{"id":"2","content":"world","author":{"username":"bob"}}\n')
|
|
|
|
FAKE_MEMBER = b'{"user":{"id":"U1","username":"alice"},"roles":[]}'
|
|
|
|
|
|
def _run(coro):
|
|
return asyncio.run(coro)
|
|
|
|
|
|
def _make_resource() -> DiscordResource:
|
|
config = DiscordConfig(token="test-token")
|
|
resource = DiscordResource(config=config)
|
|
index = resource.index
|
|
_run(
|
|
index.put(
|
|
f"{PREFIX}/{GUILD}",
|
|
IndexEntry(id="G1",
|
|
name=GUILD,
|
|
resource_type="discord/guild",
|
|
vfs_name=GUILD)))
|
|
_run(
|
|
index.put(
|
|
f"{PREFIX}/{CHANNEL_PATH}",
|
|
IndexEntry(id="C1",
|
|
name=CHANNEL,
|
|
resource_type="discord/channel",
|
|
vfs_name=CHANNEL)))
|
|
_run(
|
|
index.put(
|
|
f"{PREFIX}/{DATE_DIR_PATH}",
|
|
IndexEntry(id="C1:2024-01-15",
|
|
name="2024-01-15",
|
|
resource_type="discord/history",
|
|
vfs_name=DATE)))
|
|
_run(
|
|
index.put(
|
|
f"{PREFIX}/{FILE_PATH}",
|
|
IndexEntry(id="C1:2024-01-15:chat",
|
|
name="chat.jsonl",
|
|
resource_type="discord/chat_jsonl",
|
|
vfs_name=FILE)))
|
|
_run(
|
|
index.put(
|
|
f"{PREFIX}/{MEMBER_PATH}",
|
|
IndexEntry(id="U1",
|
|
name="alice",
|
|
resource_type="discord/member",
|
|
vfs_name="alice.json")))
|
|
_run(
|
|
index.put(
|
|
f"{PREFIX}/{GUILD}/members",
|
|
IndexEntry(id="G1:members",
|
|
name="members",
|
|
resource_type="discord/virtual_dir")))
|
|
_run(
|
|
index.set_dir(f"{PREFIX}/{GUILD}/channels", [
|
|
(CHANNEL,
|
|
IndexEntry(id="C1",
|
|
name=CHANNEL,
|
|
resource_type="discord/channel",
|
|
vfs_name=CHANNEL)),
|
|
]))
|
|
_run(
|
|
index.set_dir(f"{PREFIX}/{CHANNEL_PATH}", [
|
|
(DATE,
|
|
IndexEntry(id="C1:2024-01-15",
|
|
name="2024-01-15",
|
|
resource_type="discord/history",
|
|
vfs_name=DATE)),
|
|
]))
|
|
_run(
|
|
index.set_dir(f"{PREFIX}/{DATE_DIR_PATH}", [
|
|
(FILE,
|
|
IndexEntry(id="C1:2024-01-15:chat",
|
|
name="chat.jsonl",
|
|
resource_type="discord/chat_jsonl",
|
|
vfs_name=FILE)),
|
|
]))
|
|
_run(
|
|
index.set_dir(f"{PREFIX}/{GUILD}/members", [
|
|
("alice.json",
|
|
IndexEntry(id="U1",
|
|
name="alice",
|
|
resource_type="discord/member",
|
|
vfs_name="alice.json")),
|
|
]))
|
|
return resource
|
|
|
|
|
|
def _make_ops(resource: DiscordResource) -> Ops:
|
|
mount = OpsMount(
|
|
prefix=f"{PREFIX}/",
|
|
resource_type=resource.name,
|
|
accessor=resource.accessor,
|
|
index=resource.index,
|
|
mode=MountMode.READ,
|
|
ops=resource.ops_list(),
|
|
)
|
|
return Ops([mount])
|
|
|
|
|
|
@pytest.fixture
|
|
def resource():
|
|
return _make_resource()
|
|
|
|
|
|
@pytest.fixture
|
|
def ops(resource):
|
|
return _make_ops(resource)
|
|
|
|
|
|
@pytest.fixture
|
|
def fs(ops):
|
|
return MirageFS(ops)
|
|
|
|
|
|
# ── ops.readdir ──────────────────────────────────
|
|
|
|
|
|
def test_ops_readdir_guild(ops):
|
|
entries = _run(ops.readdir(f"{PREFIX}/{GUILD}/"))
|
|
names = [e.rsplit("/", 1)[-1] for e in entries]
|
|
assert "channels" in names
|
|
assert "members" in names
|
|
|
|
|
|
def test_ops_readdir_channels(ops):
|
|
entries = _run(ops.readdir(f"{PREFIX}/{GUILD}/channels/"))
|
|
names = [e.rsplit("/", 1)[-1] for e in entries]
|
|
assert CHANNEL in names
|
|
|
|
|
|
def test_ops_readdir_dates(ops):
|
|
entries = _run(ops.readdir(f"{PREFIX}/{CHANNEL_PATH}/"))
|
|
names = [e.rsplit("/", 1)[-1] for e in entries]
|
|
assert DATE in names
|
|
|
|
|
|
def test_ops_readdir_members(ops):
|
|
entries = _run(ops.readdir(f"{PREFIX}/{GUILD}/members/"))
|
|
names = [e.rsplit("/", 1)[-1] for e in entries]
|
|
assert "alice.json" in names
|
|
|
|
|
|
# ── ops.stat ─────────────────────────────────────
|
|
|
|
|
|
def test_ops_stat_guild(ops):
|
|
s = _run(ops.stat(f"{PREFIX}/{GUILD}"))
|
|
assert s.type == FileType.DIRECTORY
|
|
|
|
|
|
def test_ops_stat_channel(ops):
|
|
s = _run(ops.stat(f"{PREFIX}/{CHANNEL_PATH}"))
|
|
assert s.type == FileType.DIRECTORY
|
|
|
|
|
|
def test_ops_stat_file(ops):
|
|
s = _run(ops.stat(f"{PREFIX}/{FILE_PATH}"))
|
|
assert s.type == FileType.TEXT
|
|
assert s.name == FILE
|
|
|
|
|
|
# ── ops.read ─────────────────────────────────────
|
|
|
|
|
|
def test_ops_read_jsonl(ops):
|
|
with patch("mirage.core.discord.read.get_history_jsonl",
|
|
new_callable=AsyncMock,
|
|
return_value=FAKE_JSONL):
|
|
data = _run(ops.read(f"{PREFIX}/{FILE_PATH}"))
|
|
assert data == FAKE_JSONL
|
|
assert b"hello" in data
|
|
|
|
|
|
def test_ops_read_member(ops):
|
|
with patch("mirage.core.discord.read.list_members",
|
|
new_callable=AsyncMock,
|
|
return_value=[{
|
|
"user": {
|
|
"id": "U1",
|
|
"username": "alice"
|
|
}
|
|
}]):
|
|
data = _run(ops.read(f"{PREFIX}/{MEMBER_PATH}"))
|
|
assert b"alice" in data
|
|
|
|
|
|
# ── ops.read prefix in index keys ────────────────
|
|
|
|
|
|
def test_ops_read_uses_prefix_for_index(ops):
|
|
with patch("mirage.core.discord.read.get_history_jsonl",
|
|
new_callable=AsyncMock,
|
|
return_value=b"data") as mock_get:
|
|
data = _run(ops.read(f"{PREFIX}/{FILE_PATH}"))
|
|
assert data == b"data"
|
|
mock_get.assert_called_once()
|
|
|
|
|
|
# ── FUSE getattr ─────────────────────────────────
|
|
|
|
|
|
def test_fuse_getattr_root(fs):
|
|
attr = fs.getattr("/")
|
|
assert attr["st_mode"] & 0o170000 == 0o040000
|
|
|
|
|
|
def test_fuse_getattr_mount_prefix(fs):
|
|
attr = fs.getattr("/discord")
|
|
assert attr["st_mode"] & 0o170000 == 0o040000
|
|
|
|
|
|
def test_fuse_getattr_guild(fs):
|
|
attr = fs.getattr(f"{PREFIX}/{GUILD}")
|
|
assert attr["st_mode"] & 0o170000 == 0o040000
|
|
|
|
|
|
def test_fuse_getattr_file(fs):
|
|
attr = fs.getattr(f"{PREFIX}/{FILE_PATH}")
|
|
assert attr["st_mode"] & 0o170000 == 0o100000
|
|
|
|
|
|
def test_fuse_getattr_unknown_size(fs):
|
|
# Unknown size before open() — direct_io ensures read() still works
|
|
attr = fs.getattr(f"{PREFIX}/{FILE_PATH}")
|
|
assert attr["st_size"] == 0
|
|
|
|
|
|
# ── FUSE readdir ─────────────────────────────────
|
|
|
|
|
|
def test_fuse_readdir_root(fs):
|
|
entries = fs.readdir("/", None)
|
|
assert "discord" in entries
|
|
|
|
|
|
def test_fuse_readdir_guild(fs):
|
|
entries = fs.readdir(f"{PREFIX}/{GUILD}", None)
|
|
assert "channels" in entries
|
|
assert "members" in entries
|
|
|
|
|
|
def test_fuse_readdir_channels(fs):
|
|
entries = fs.readdir(f"{PREFIX}/{GUILD}/channels", None)
|
|
assert CHANNEL in entries
|
|
|
|
|
|
def test_fuse_readdir_dates(fs):
|
|
entries = fs.readdir(f"{PREFIX}/{CHANNEL_PATH}", None)
|
|
assert DATE in entries
|
|
|
|
|
|
# ── FUSE open + read ─────────────────────────────
|
|
|
|
|
|
def test_fuse_read_file(fs):
|
|
with patch("mirage.core.discord.read.get_history_jsonl",
|
|
new_callable=AsyncMock,
|
|
return_value=FAKE_JSONL):
|
|
fh = fs.open(f"{PREFIX}/{FILE_PATH}", 0)
|
|
data = fs.read(f"{PREFIX}/{FILE_PATH}", 4096, 0, fh)
|
|
fs.release(f"{PREFIX}/{FILE_PATH}", fh)
|
|
assert data == FAKE_JSONL
|
|
|
|
|
|
def test_fuse_read_offset(fs):
|
|
with patch("mirage.core.discord.read.get_history_jsonl",
|
|
new_callable=AsyncMock,
|
|
return_value=FAKE_JSONL):
|
|
fh = fs.open(f"{PREFIX}/{FILE_PATH}", 0)
|
|
data = fs.read(f"{PREFIX}/{FILE_PATH}", 10, 0, fh)
|
|
fs.release(f"{PREFIX}/{FILE_PATH}", fh)
|
|
assert data == FAKE_JSONL[:10]
|
|
|
|
|
|
def test_fuse_read_beyond_eof(fs):
|
|
with patch("mirage.core.discord.read.get_history_jsonl",
|
|
new_callable=AsyncMock,
|
|
return_value=FAKE_JSONL):
|
|
fh = fs.open(f"{PREFIX}/{FILE_PATH}", 0)
|
|
data = fs.read(f"{PREFIX}/{FILE_PATH}", 4096, len(FAKE_JSONL), fh)
|
|
fs.release(f"{PREFIX}/{FILE_PATH}", fh)
|
|
assert data == b""
|