mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
Service-level tests now exercise provider, permissions, project, journal, messaging, work_session, metrics, kanban, extraction, learning, notification, dashboard, llm_routing, a2a, task, repository_base, audit, db_seed, branch_name, indexed_document, query_helpers, agent. API route tests cover provider, journal, project, sessions, dashboard, work_session, tasks, a2a, groups, notifications, agents, channels, messages, kanban, api_resources. Pure-function helpers covered: handlers, deps_helpers, middleware, middleware_docs, transcription, pr templates, agents_config, errors, logging, journal/notification/channel/a2a access, task_lifecycle, streaming, converters, crypto, schemas (common + websocket), events, permissions extras. pyproject ruff per-file-ignores extended for tests so PLR2004 (status code magic values), PLC0415 (lazy imports), PLR0913 (fixture params), ARG001 (unused fixture deps), SIM105, and E501 don't fight test idioms.
1235 lines
41 KiB
Python
1235 lines
41 KiB
Python
"""MessagingService coverage — channels, groups, sessions, task links."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
import pytest_asyncio
|
|
from roboco.db.tables import AgentTable, ProjectTable, TaskTable
|
|
from roboco.models import AgentRole, AgentStatus, Team
|
|
from roboco.models.base import (
|
|
ChannelType,
|
|
SessionStatus,
|
|
TaskNature,
|
|
TaskStatus,
|
|
TaskType,
|
|
)
|
|
from roboco.models.messaging import (
|
|
ChannelCreateRequest,
|
|
GroupCreateRequest,
|
|
SessionCreateRequest,
|
|
)
|
|
from roboco.services.base import NotFoundError
|
|
from roboco.services.messaging import MessagingService
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import AsyncIterator
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def msg_setup(
|
|
db_session: AsyncSession,
|
|
) -> AsyncIterator[dict]:
|
|
agent = AgentTable(
|
|
id=uuid4(),
|
|
name="Dev",
|
|
slug=f"be-dev-{uuid4().hex[:8]}",
|
|
role=AgentRole.DEVELOPER,
|
|
team=Team.BACKEND,
|
|
status=AgentStatus.ACTIVE,
|
|
model_config={},
|
|
system_prompt="dev",
|
|
capabilities=[],
|
|
permissions={},
|
|
metrics={},
|
|
)
|
|
db_session.add(agent)
|
|
await db_session.flush()
|
|
project = ProjectTable(
|
|
id=uuid4(),
|
|
name="M-Proj",
|
|
slug=f"m-proj-{uuid4().hex[:8]}",
|
|
git_url="https://example.com/r.git",
|
|
assigned_cell=Team.BACKEND,
|
|
created_by=agent.id,
|
|
)
|
|
db_session.add(project)
|
|
await db_session.flush()
|
|
task = TaskTable(
|
|
id=uuid4(),
|
|
title="t",
|
|
description="d",
|
|
acceptance_criteria=["ac"],
|
|
status=TaskStatus.PENDING,
|
|
priority=2,
|
|
task_type=TaskType.CODE,
|
|
nature=TaskNature.TECHNICAL,
|
|
project_id=project.id,
|
|
created_by=agent.id,
|
|
team=Team.BACKEND,
|
|
)
|
|
db_session.add(task)
|
|
await db_session.flush()
|
|
yield {
|
|
"svc": MessagingService(db_session),
|
|
"agent_id": agent.id,
|
|
"task_id": task.id,
|
|
}
|
|
|
|
|
|
def _channel_req(slug_suffix: str) -> ChannelCreateRequest:
|
|
return ChannelCreateRequest(
|
|
name=f"Channel {slug_suffix}",
|
|
slug=f"ch-{slug_suffix}",
|
|
channel_type=ChannelType.CELL,
|
|
description="desc",
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Channels
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_channel(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
assert ch.id is not None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_channel_duplicate_slug_raises(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
req = _channel_req(uuid4().hex[:6])
|
|
await svc.create_channel(req)
|
|
with pytest.raises(ValueError, match="already exists"):
|
|
await svc.create_channel(req)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_channel(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
fetched = await svc.get_channel(ch.id)
|
|
assert fetched is not None
|
|
assert fetched.id == ch.id
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_channel_returns_none_for_missing(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
assert await svc.get_channel(uuid4()) is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_channel_or_raise_raises(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
with pytest.raises(NotFoundError):
|
|
await svc.get_channel_or_raise(uuid4())
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_channel_by_slug(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
req = _channel_req(uuid4().hex[:6])
|
|
await svc.create_channel(req)
|
|
found = await svc.get_channel_by_slug(req.slug)
|
|
assert found is not None
|
|
# Hash prefix is preserved
|
|
found_hash = await svc.get_channel_by_slug(f"#{req.slug}")
|
|
assert found_hash is not None
|
|
assert found_hash.id == found.id
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_archive_channel(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
archived = await svc.archive_channel(ch.id)
|
|
assert archived.is_archived is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_and_remove_channel_member(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
aid = msg_setup["agent_id"]
|
|
updated = await svc.add_channel_member(ch.id, aid, can_write=True)
|
|
assert aid in updated.members
|
|
assert aid in updated.writers
|
|
removed = await svc.remove_channel_member(ch.id, aid)
|
|
assert aid not in removed.members
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_channel_member_or_raise(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
aid = msg_setup["agent_id"]
|
|
await svc.add_channel_member_or_raise(
|
|
channel_id=ch.id, member_id=aid, can_write=True
|
|
)
|
|
refreshed = await svc.get_channel(ch.id)
|
|
assert refreshed is not None
|
|
assert aid in refreshed.members
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_remove_channel_member_or_raise(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
aid = msg_setup["agent_id"]
|
|
await svc.add_channel_member(ch.id, aid)
|
|
await svc.remove_channel_member_or_raise(channel_id=ch.id, member_id=aid)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_channels_for_agent(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
aid = msg_setup["agent_id"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
await svc.add_channel_member(ch.id, aid)
|
|
channels = await svc.list_channels_for_agent(aid)
|
|
assert ch.id in {c.id for c in channels}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_channel_fields(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
updated = await svc.update_channel_fields(
|
|
channel_id=ch.id, fields={"name": "new", "description": "newdesc"}
|
|
)
|
|
assert updated.name == "new"
|
|
assert updated.description == "newdesc"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_channels_paginated(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
req = _channel_req(uuid4().hex[:6])
|
|
await svc.create_channel(req)
|
|
rows, total = await svc.list_channels_paginated(
|
|
accessible_slugs=[req.slug],
|
|
include_archived=False,
|
|
page=1,
|
|
page_size=10,
|
|
)
|
|
assert total >= 1
|
|
assert any(r.slug == req.slug for r in rows)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Groups
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_group(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc.create_group(
|
|
GroupCreateRequest(name="g1", channel_id=ch.id, hierarchy_level=4)
|
|
)
|
|
assert grp.id is not None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_group_missing_channel_raises(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
with pytest.raises(ValueError, match="not found"):
|
|
await svc.create_group(
|
|
GroupCreateRequest(name="g1", channel_id=uuid4(), hierarchy_level=4)
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_group_returns_none(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
assert await svc.get_group(uuid4()) is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_groups_in_channel(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
await svc.create_group(GroupCreateRequest(name="g2", channel_id=ch.id))
|
|
groups = await svc.list_groups_in_channel(ch.id)
|
|
assert len(groups) >= 2
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Sessions
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_session(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
sess = await svc.create_session(SessionCreateRequest(group_id=grp.id))
|
|
assert sess.id is not None
|
|
assert sess.status == SessionStatus.ACTIVE
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_session_missing_group_raises(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
with pytest.raises(ValueError, match="not found"):
|
|
await svc.create_session(SessionCreateRequest(group_id=uuid4()))
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_session_replaces_active(msg_setup: dict) -> None:
|
|
"""Second create_session against same group still produces an ACTIVE session."""
|
|
svc = msg_setup["svc"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
await svc.create_session(SessionCreateRequest(group_id=grp.id))
|
|
second = await svc.create_session(SessionCreateRequest(group_id=grp.id))
|
|
assert second.status == SessionStatus.ACTIVE
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_session_returns_none(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
assert await svc.get_session(uuid4()) is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_close_session_returns_none_when_missing(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
assert await svc.close_session(uuid4()) is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_close_session_idempotent_when_already_closed(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
sess = await svc.create_session(SessionCreateRequest(group_id=grp.id))
|
|
await svc.close_session(sess.id)
|
|
again = await svc.close_session(sess.id)
|
|
assert again is not None
|
|
assert again.status == SessionStatus.CLOSED
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_session_or_raise(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
with pytest.raises(NotFoundError):
|
|
await svc.get_session_or_raise(uuid4())
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_close_session_or_raise_missing(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
with pytest.raises(NotFoundError):
|
|
await svc.close_session_or_raise(uuid4())
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Session-task links
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_link_session_to_task(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
aid = msg_setup["agent_id"]
|
|
tid = msg_setup["task_id"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
sess = await svc.create_session(SessionCreateRequest(group_id=grp.id))
|
|
link = await svc.link_session_to_task(sess.id, tid, aid)
|
|
assert link.session_id == sess.id
|
|
assert link.task_id == tid
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_link_session_to_task_idempotent(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
aid = msg_setup["agent_id"]
|
|
tid = msg_setup["task_id"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
sess = await svc.create_session(SessionCreateRequest(group_id=grp.id))
|
|
a = await svc.link_session_to_task(sess.id, tid, aid)
|
|
b = await svc.link_session_to_task(sess.id, tid, aid)
|
|
assert a.id == b.id
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_link_session_to_task_missing_session(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
aid = msg_setup["agent_id"]
|
|
tid = msg_setup["task_id"]
|
|
with pytest.raises(NotFoundError):
|
|
await svc.link_session_to_task(uuid4(), tid, aid)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unlink_session_from_task(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
aid = msg_setup["agent_id"]
|
|
tid = msg_setup["task_id"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
sess = await svc.create_session(SessionCreateRequest(group_id=grp.id))
|
|
await svc.link_session_to_task(sess.id, tid, aid)
|
|
result = await svc.unlink_session_from_task(sess.id, tid)
|
|
assert result is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unlink_session_from_task_returns_false_when_missing(
|
|
msg_setup: dict,
|
|
) -> None:
|
|
svc = msg_setup["svc"]
|
|
assert await svc.unlink_session_from_task(uuid4(), uuid4()) is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_sessions_for_task(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
aid = msg_setup["agent_id"]
|
|
tid = msg_setup["task_id"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
sess = await svc.create_session(SessionCreateRequest(group_id=grp.id))
|
|
await svc.link_session_to_task(sess.id, tid, aid)
|
|
links = await svc.get_sessions_for_task(tid)
|
|
# Returns SessionTaskTable (links), not SessionTable.
|
|
assert sess.id in {ln.session_id for ln in links}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sweep_timed_out_sessions_no_op(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
closed = await svc.sweep_timed_out_sessions()
|
|
assert closed >= 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_or_create_active_session_returns_active(
|
|
msg_setup: dict,
|
|
) -> None:
|
|
svc = msg_setup["svc"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
a = await svc.get_or_create_active_session(grp.id)
|
|
assert a.status == SessionStatus.ACTIVE
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_or_create_active_session_missing_group(
|
|
msg_setup: dict,
|
|
) -> None:
|
|
svc = msg_setup["svc"]
|
|
with pytest.raises(ValueError, match="not found"):
|
|
await svc.get_or_create_active_session(uuid4())
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_channel_by_slug_or_raise(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
with pytest.raises(NotFoundError):
|
|
await svc.get_channel_by_slug_or_raise("ghost-slug")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_channel_with_groups_or_raise(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
fetched = await svc.get_channel_with_groups_or_raise(ch.id)
|
|
assert fetched.id == ch.id
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_channel_with_groups_or_raise_missing(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
with pytest.raises(NotFoundError):
|
|
await svc.get_channel_with_groups_or_raise(uuid4())
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_archive_channel_missing_raises(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
with pytest.raises(ValueError, match="not found"):
|
|
await svc.archive_channel(uuid4())
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_channel_member_missing_raises(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
with pytest.raises(ValueError, match="not found"):
|
|
await svc.add_channel_member(uuid4(), uuid4())
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_tasks_for_session(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
aid = msg_setup["agent_id"]
|
|
tid = msg_setup["task_id"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
sess = await svc.create_session(SessionCreateRequest(group_id=grp.id))
|
|
await svc.link_session_to_task(sess.id, tid, aid)
|
|
tasks = await svc.get_tasks_for_session(sess.id)
|
|
assert tid in {t.task_id for t in tasks}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_primary_session_for_task_returns_none(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
assert await svc.get_primary_session_for_task(uuid4()) is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_primary_session_for_task_returns_link(
|
|
msg_setup: dict,
|
|
) -> None:
|
|
svc = msg_setup["svc"]
|
|
aid = msg_setup["agent_id"]
|
|
tid = msg_setup["task_id"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
sess = await svc.create_session(SessionCreateRequest(group_id=grp.id))
|
|
await svc.link_session_to_task(sess.id, tid, aid, is_primary=True)
|
|
link = await svc.get_primary_session_for_task(tid)
|
|
assert link is not None
|
|
assert link.session_id == sess.id
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_walk_task_ancestors_empty_when_no_parent(
|
|
msg_setup: dict,
|
|
) -> None:
|
|
svc = msg_setup["svc"]
|
|
ancestors = await svc._walk_task_ancestors(msg_setup["task_id"])
|
|
assert ancestors == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_or_create_channel_by_slug_returns_none_for_unknown(
|
|
msg_setup: dict,
|
|
) -> None:
|
|
svc = msg_setup["svc"]
|
|
assert await svc.get_or_create_channel_by_slug("ghost-channel") is None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Content validation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_assert_content_rejects_empty(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
with pytest.raises(ValueError, match="EMPTY_MESSAGE"):
|
|
svc._assert_content("")
|
|
with pytest.raises(ValueError, match="EMPTY_MESSAGE"):
|
|
svc._assert_content(" \n\n ")
|
|
with pytest.raises(ValueError, match="EMPTY_MESSAGE"):
|
|
svc._assert_content(None)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_assert_content_rejects_oversized(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
with pytest.raises(ValueError, match="MESSAGE_TOO_LONG"):
|
|
svc._assert_content("x" * 20_000)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_assert_content_accepts_valid(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
# No exception.
|
|
svc._assert_content("hello")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Default group resolution
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_default_group_for_channel_creates_one_if_none(
|
|
msg_setup: dict,
|
|
) -> None:
|
|
svc = msg_setup["svc"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc._default_group_for_channel(ch)
|
|
assert grp is not None
|
|
assert grp.channel_id == ch.id
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_default_group_for_channel_returns_existing(
|
|
msg_setup: dict,
|
|
) -> None:
|
|
svc = msg_setup["svc"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
explicit = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
found = await svc._default_group_for_channel(ch)
|
|
assert found.id == explicit.id
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# check_session_boundaries
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_check_session_boundaries_within_limits(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
sess = await svc.create_session(SessionCreateRequest(group_id=grp.id))
|
|
assert svc._check_session_boundaries(sess) is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_check_session_boundaries_exceeded(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
sess = await svc.create_session(SessionCreateRequest(group_id=grp.id))
|
|
sess.message_count = sess.max_message_count or 100
|
|
assert svc._check_session_boundaries(sess) is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Messages
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
from roboco.models.messaging import MessageCreateRequest # noqa: E402
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_message_to_session(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
aid = msg_setup["agent_id"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
sess = await svc.create_session(SessionCreateRequest(group_id=grp.id))
|
|
msg = await svc.send_message(
|
|
MessageCreateRequest(
|
|
agent_id=aid,
|
|
session_id=sess.id,
|
|
content="hello world",
|
|
)
|
|
)
|
|
assert msg.id is not None
|
|
assert msg.content == "hello world"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_message_rejects_empty(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
aid = msg_setup["agent_id"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
sess = await svc.create_session(SessionCreateRequest(group_id=grp.id))
|
|
with pytest.raises(ValueError, match="EMPTY_MESSAGE"):
|
|
await svc.send_message(
|
|
MessageCreateRequest(
|
|
agent_id=aid,
|
|
session_id=sess.id,
|
|
content="",
|
|
)
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_message_returns_none_for_missing(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
assert await svc.get_message(uuid4()) is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_message_or_raise(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
with pytest.raises(NotFoundError):
|
|
await svc.get_message_or_raise(uuid4())
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_messages_empty(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
sess = await svc.create_session(SessionCreateRequest(group_id=grp.id))
|
|
messages, has_more = await svc.get_messages(sess.id)
|
|
assert isinstance(messages, list)
|
|
assert has_more is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_edit_message_by_author(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
aid = msg_setup["agent_id"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
sess = await svc.create_session(SessionCreateRequest(group_id=grp.id))
|
|
msg = await svc.send_message(
|
|
MessageCreateRequest(agent_id=aid, session_id=sess.id, content="original")
|
|
)
|
|
edited = await svc.edit_message(msg.id, aid, "edited", edit_reason="typo")
|
|
assert edited.content == "edited"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_edit_message_by_non_author_raises(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
aid = msg_setup["agent_id"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
sess = await svc.create_session(SessionCreateRequest(group_id=grp.id))
|
|
msg = await svc.send_message(
|
|
MessageCreateRequest(agent_id=aid, session_id=sess.id, content="original")
|
|
)
|
|
with pytest.raises(ValueError, match="author"):
|
|
await svc.edit_message(msg.id, uuid4(), "edited")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_message_by_author(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
aid = msg_setup["agent_id"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
sess = await svc.create_session(SessionCreateRequest(group_id=grp.id))
|
|
msg = await svc.send_message(
|
|
MessageCreateRequest(agent_id=aid, session_id=sess.id, content="original")
|
|
)
|
|
assert await svc.delete_message(msg.id, aid) is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_message_by_non_author_raises(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
aid = msg_setup["agent_id"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
sess = await svc.create_session(SessionCreateRequest(group_id=grp.id))
|
|
msg = await svc.send_message(
|
|
MessageCreateRequest(agent_id=aid, session_id=sess.id, content="original")
|
|
)
|
|
with pytest.raises(ValueError, match="author"):
|
|
await svc.delete_message(msg.id, uuid4())
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_or_create_channel_by_slug_creates_from_seed(
|
|
msg_setup: dict,
|
|
) -> None:
|
|
"""Auto-create from DEFAULT_CHANNELS when DB has no row but slug is known."""
|
|
svc = msg_setup["svc"]
|
|
# backend-cell is in DEFAULT_CHANNELS.
|
|
ch = await svc.get_or_create_channel_by_slug("backend-cell")
|
|
assert ch is not None
|
|
assert ch.slug == "backend-cell"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_post_to_channel_unknown_slug_raises(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
with pytest.raises(NotFoundError):
|
|
await svc.post_to_channel(
|
|
agent_id=msg_setup["agent_id"],
|
|
channel_slug="ghost-channel",
|
|
content="hi",
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# list_messages_for_session
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_messages_for_session_unknown(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
with pytest.raises(NotFoundError):
|
|
await svc.list_messages_for_session(
|
|
session_id=uuid4(),
|
|
before=None,
|
|
after=None,
|
|
message_type=None,
|
|
limit=10,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_messages_for_session_returns_empty(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
sess = await svc.create_session(SessionCreateRequest(group_id=grp.id))
|
|
msgs, has_more = await svc.list_messages_for_session(
|
|
session_id=sess.id,
|
|
before=None,
|
|
after=None,
|
|
message_type=None,
|
|
limit=10,
|
|
)
|
|
assert msgs == []
|
|
assert has_more is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# edit_message_or_raise / delete_message_or_raise
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_edit_message_or_raise_not_found(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
with pytest.raises(NotFoundError):
|
|
await svc.edit_message_or_raise(
|
|
message_id=uuid4(),
|
|
agent_id=uuid4(),
|
|
new_content="x",
|
|
edit_reason=None,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_message_or_raise_not_found(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
with pytest.raises(NotFoundError):
|
|
await svc.delete_message_or_raise(message_id=uuid4(), agent_id=uuid4())
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# list_group_sessions_for_agent + create_session_with_access_check
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_group_sessions_unknown_group(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
aid = msg_setup["agent_id"]
|
|
with pytest.raises(NotFoundError):
|
|
await svc.list_group_sessions_for_agent(
|
|
group_id=uuid4(),
|
|
agent_id=aid,
|
|
status_filter=None,
|
|
limit=10,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_group_sessions_unauthorized(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
# Random agent who's not in the channel.
|
|
with pytest.raises(PermissionError):
|
|
await svc.list_group_sessions_for_agent(
|
|
group_id=grp.id,
|
|
agent_id=uuid4(),
|
|
status_filter=None,
|
|
limit=10,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_session_with_access_check_unknown_group(
|
|
msg_setup: dict,
|
|
) -> None:
|
|
from roboco.services.messaging import ApiSessionCreate
|
|
|
|
svc = msg_setup["svc"]
|
|
with pytest.raises(NotFoundError):
|
|
await svc.create_session_with_access_check(
|
|
agent_id=msg_setup["agent_id"],
|
|
request=ApiSessionCreate(
|
|
group_id=uuid4(),
|
|
max_time_window_minutes=30,
|
|
max_message_count=100,
|
|
max_content_length=10000,
|
|
timeout_seconds=300,
|
|
),
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_session_with_access_check_unauthorized(
|
|
msg_setup: dict,
|
|
) -> None:
|
|
from roboco.services.messaging import ApiSessionCreate
|
|
|
|
svc = msg_setup["svc"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
with pytest.raises(PermissionError):
|
|
await svc.create_session_with_access_check(
|
|
agent_id=uuid4(), # Not in channel.writers.
|
|
request=ApiSessionCreate(
|
|
group_id=grp.id,
|
|
max_time_window_minutes=30,
|
|
max_message_count=100,
|
|
max_content_length=10000,
|
|
timeout_seconds=300,
|
|
),
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# create_session_for_tasks
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_session_for_tasks_unknown_channel(
|
|
msg_setup: dict,
|
|
) -> None:
|
|
from roboco.models.session import SessionForTasksCreate, SessionScope
|
|
|
|
svc = msg_setup["svc"]
|
|
with pytest.raises(NotFoundError):
|
|
await svc.create_session_for_tasks(
|
|
SessionForTasksCreate(
|
|
task_ids=[msg_setup["task_id"]],
|
|
channel_slug="ghost-channel",
|
|
scope=SessionScope.TASK,
|
|
),
|
|
pm_agent_id=msg_setup["agent_id"],
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_session_for_tasks_creates_session(
|
|
msg_setup: dict,
|
|
) -> None:
|
|
from roboco.models.session import SessionForTasksCreate, SessionScope
|
|
|
|
svc = msg_setup["svc"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
sess, links = await svc.create_session_for_tasks(
|
|
SessionForTasksCreate(
|
|
task_ids=[msg_setup["task_id"]],
|
|
channel_slug=ch.slug,
|
|
scope=SessionScope.TASK,
|
|
),
|
|
pm_agent_id=msg_setup["agent_id"],
|
|
)
|
|
assert sess.id is not None
|
|
assert len(links) == 1
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Walking ancestors with parent
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_session_with_access_check_member_can_write(
|
|
msg_setup: dict,
|
|
) -> None:
|
|
"""Channel writer can create a session via access-checked path."""
|
|
from roboco.services.messaging import ApiSessionCreate
|
|
|
|
svc = msg_setup["svc"]
|
|
aid = msg_setup["agent_id"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
# Add agent to writers list.
|
|
await svc.add_channel_member(ch.id, aid, can_write=True)
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
sess = await svc.create_session_with_access_check(
|
|
agent_id=aid,
|
|
request=ApiSessionCreate(
|
|
group_id=grp.id,
|
|
max_time_window_minutes=30,
|
|
max_message_count=100,
|
|
max_content_length=10000,
|
|
timeout_seconds=300,
|
|
),
|
|
)
|
|
assert sess.id is not None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_group_sessions_for_agent_member(
|
|
msg_setup: dict,
|
|
) -> None:
|
|
"""Channel member can list sessions in their group."""
|
|
svc = msg_setup["svc"]
|
|
aid = msg_setup["agent_id"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
await svc.add_channel_member(ch.id, aid)
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
await svc.create_session(SessionCreateRequest(group_id=grp.id))
|
|
sessions = await svc.list_group_sessions_for_agent(
|
|
group_id=grp.id, agent_id=aid, status_filter=None, limit=10
|
|
)
|
|
assert len(sessions) >= 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_group_sessions_with_status_filter(
|
|
msg_setup: dict,
|
|
) -> None:
|
|
svc = msg_setup["svc"]
|
|
aid = msg_setup["agent_id"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
await svc.add_channel_member(ch.id, aid)
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
await svc.create_session(SessionCreateRequest(group_id=grp.id))
|
|
sessions = await svc.list_group_sessions_for_agent(
|
|
group_id=grp.id,
|
|
agent_id=aid,
|
|
status_filter=SessionStatus.ACTIVE,
|
|
limit=10,
|
|
)
|
|
assert all(s.status == SessionStatus.ACTIVE for s in sessions)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sweep_timed_out_sessions_closes_idle_session(
|
|
msg_setup: dict, db_session: AsyncSession
|
|
) -> None:
|
|
from datetime import UTC, datetime, timedelta
|
|
|
|
svc = msg_setup["svc"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
sess = await svc.create_session(
|
|
SessionCreateRequest(group_id=grp.id, timeout_seconds=1)
|
|
)
|
|
# Force last_activity_at into the past so sweeper closes it.
|
|
sess.last_activity_at = datetime.now(UTC) - timedelta(seconds=120)
|
|
await db_session.flush()
|
|
closed = await svc.sweep_timed_out_sessions()
|
|
assert closed >= 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_edit_message_or_raise_succeeds(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
aid = msg_setup["agent_id"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
sess = await svc.create_session(SessionCreateRequest(group_id=grp.id))
|
|
msg = await svc.send_message(
|
|
MessageCreateRequest(agent_id=aid, session_id=sess.id, content="original")
|
|
)
|
|
edited = await svc.edit_message_or_raise(
|
|
message_id=msg.id,
|
|
agent_id=aid,
|
|
new_content="edited content",
|
|
edit_reason=None,
|
|
)
|
|
assert edited.content == "edited content"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_message_or_raise_succeeds(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
aid = msg_setup["agent_id"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
sess = await svc.create_session(SessionCreateRequest(group_id=grp.id))
|
|
msg = await svc.send_message(
|
|
MessageCreateRequest(
|
|
agent_id=aid, session_id=sess.id, content="will be deleted"
|
|
)
|
|
)
|
|
await svc.delete_message_or_raise(message_id=msg.id, agent_id=aid)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_message_with_mentions(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
aid = msg_setup["agent_id"]
|
|
other = uuid4()
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
sess = await svc.create_session(SessionCreateRequest(group_id=grp.id))
|
|
msg = await svc.send_message(
|
|
MessageCreateRequest(
|
|
agent_id=aid,
|
|
session_id=sess.id,
|
|
content="hi @other",
|
|
mentions=[other],
|
|
)
|
|
)
|
|
assert other in msg.mentions
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_message_reply_target_unknown_raises(
|
|
msg_setup: dict,
|
|
) -> None:
|
|
svc = msg_setup["svc"]
|
|
aid = msg_setup["agent_id"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
sess = await svc.create_session(SessionCreateRequest(group_id=grp.id))
|
|
with pytest.raises((ValueError, NotFoundError)):
|
|
await svc.send_message(
|
|
MessageCreateRequest(
|
|
agent_id=aid,
|
|
session_id=sess.id,
|
|
content="reply",
|
|
reply_to=uuid4(), # Bogus reply target.
|
|
)
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_messages_with_filters(msg_setup: dict) -> None:
|
|
"""get_messages with before/after/type filters."""
|
|
from datetime import UTC, datetime, timedelta
|
|
|
|
from roboco.models import MessageType
|
|
|
|
svc = msg_setup["svc"]
|
|
aid = msg_setup["agent_id"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
sess = await svc.create_session(SessionCreateRequest(group_id=grp.id))
|
|
await svc.send_message(
|
|
MessageCreateRequest(agent_id=aid, session_id=sess.id, content="msg1")
|
|
)
|
|
cutoff = datetime.now(UTC) - timedelta(hours=1)
|
|
msgs, _ = await svc.get_messages(
|
|
sess.id,
|
|
before=datetime.now(UTC) + timedelta(hours=1),
|
|
after=cutoff,
|
|
message_type=MessageType.DIALOGUE,
|
|
)
|
|
assert isinstance(msgs, list)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_messages_with_limit(msg_setup: dict) -> None:
|
|
svc = msg_setup["svc"]
|
|
aid = msg_setup["agent_id"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
sess = await svc.create_session(SessionCreateRequest(group_id=grp.id))
|
|
for i in range(5):
|
|
await svc.send_message(
|
|
MessageCreateRequest(agent_id=aid, session_id=sess.id, content=f"msg-{i}")
|
|
)
|
|
msgs, has_more = await svc.get_messages(sess.id, limit=2)
|
|
assert len(msgs) == 2
|
|
assert has_more is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_message_context_redirects_when_session_closed(
|
|
msg_setup: dict,
|
|
) -> None:
|
|
"""If session is closed, _get_message_context should redirect to active session."""
|
|
svc = msg_setup["svc"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
sess = await svc.create_session(SessionCreateRequest(group_id=grp.id))
|
|
# Close the session.
|
|
await svc.close_session(sess.id)
|
|
# Now request its context — should redirect to a fresh active session.
|
|
new_sess, new_grp, new_ch = await svc._get_message_context(sess.id)
|
|
assert new_grp.id == grp.id
|
|
assert new_ch.id == ch.id
|
|
assert new_sess.status == SessionStatus.ACTIVE
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_message_context_unknown_session_raises(
|
|
msg_setup: dict,
|
|
) -> None:
|
|
svc = msg_setup["svc"]
|
|
with pytest.raises(ValueError, match="not found"):
|
|
await svc._get_message_context(uuid4())
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_validate_reply_target_unknown_message_raises(
|
|
msg_setup: dict,
|
|
) -> None:
|
|
svc = msg_setup["svc"]
|
|
with pytest.raises(ValueError, match="not found"):
|
|
await svc._validate_reply_target(uuid4(), uuid4())
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_validate_reply_target_wrong_session_raises(
|
|
msg_setup: dict,
|
|
) -> None:
|
|
svc = msg_setup["svc"]
|
|
aid = msg_setup["agent_id"]
|
|
ch = await svc.create_channel(_channel_req(uuid4().hex[:6]))
|
|
grp = await svc.create_group(GroupCreateRequest(name="g1", channel_id=ch.id))
|
|
sess1 = await svc.create_session(SessionCreateRequest(group_id=grp.id))
|
|
msg = await svc.send_message(
|
|
MessageCreateRequest(agent_id=aid, session_id=sess1.id, content="msg")
|
|
)
|
|
sess2 = await svc.create_session(SessionCreateRequest(group_id=grp.id))
|
|
with pytest.raises(ValueError, match="not found in this session"):
|
|
await svc._validate_reply_target(msg.id, sess2.id)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_walk_task_ancestors_with_parent(
|
|
msg_setup: dict, db_session: AsyncSession
|
|
) -> None:
|
|
"""Smoke-test ancestry walk via direct DB seeding."""
|
|
from roboco.db.tables import TaskTable
|
|
from roboco.models.base import TaskNature, TaskStatus, TaskType
|
|
|
|
svc = msg_setup["svc"]
|
|
parent_id = uuid4()
|
|
child_id = uuid4()
|
|
# Need to fetch project_id and aid from msg_setup.
|
|
result = await db_session.execute(
|
|
__import__("sqlalchemy")
|
|
.select(TaskTable)
|
|
.where(TaskTable.id == msg_setup["task_id"])
|
|
)
|
|
base_task = result.scalar_one()
|
|
|
|
parent = TaskTable(
|
|
id=parent_id,
|
|
title="parent",
|
|
description="d",
|
|
acceptance_criteria=["ac"],
|
|
status=TaskStatus.PENDING,
|
|
priority=2,
|
|
task_type=TaskType.CODE,
|
|
nature=TaskNature.TECHNICAL,
|
|
project_id=base_task.project_id,
|
|
created_by=msg_setup["agent_id"],
|
|
team=base_task.team,
|
|
)
|
|
child = TaskTable(
|
|
id=child_id,
|
|
title="child",
|
|
description="d",
|
|
acceptance_criteria=["ac"],
|
|
status=TaskStatus.PENDING,
|
|
priority=2,
|
|
task_type=TaskType.CODE,
|
|
nature=TaskNature.TECHNICAL,
|
|
project_id=base_task.project_id,
|
|
created_by=msg_setup["agent_id"],
|
|
team=base_task.team,
|
|
parent_task_id=parent_id,
|
|
)
|
|
db_session.add_all([parent, child])
|
|
await db_session.flush()
|
|
ancestors = await svc._walk_task_ancestors(child_id)
|
|
assert len(ancestors) >= 1
|
|
assert ancestors[0].id == parent_id
|