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.
426 lines
12 KiB
Python
426 lines
12 KiB
Python
"""JournalService coverage — get/create journals + entries + queries."""
|
|
|
|
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 (
|
|
JournalEntryType,
|
|
TaskNature,
|
|
TaskStatus,
|
|
TaskType,
|
|
)
|
|
from roboco.models.journal import (
|
|
DecisionLogParams,
|
|
JournalEntryCreate,
|
|
LearningEntryParams,
|
|
ListEntriesFilter,
|
|
StruggleEntryParams,
|
|
TaskReflectionParams,
|
|
)
|
|
from roboco.services.journal import JournalService
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import AsyncIterator
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def journal_setup(
|
|
db_session: AsyncSession,
|
|
) -> AsyncIterator[dict]:
|
|
"""Seed an agent so we can create a journal for them."""
|
|
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="J-Proj",
|
|
slug=f"j-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": JournalService(db_session),
|
|
"agent_id": agent.id,
|
|
"agent": agent,
|
|
"task_id": task.id,
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_or_create_journal_creates_new(journal_setup: dict) -> None:
|
|
svc = journal_setup["svc"]
|
|
journal = await svc.get_or_create_journal(journal_setup["agent_id"])
|
|
assert journal is not None
|
|
assert journal.agent_id == journal_setup["agent_id"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_or_create_journal_idempotent(journal_setup: dict) -> None:
|
|
svc = journal_setup["svc"]
|
|
a = await svc.get_or_create_journal(journal_setup["agent_id"])
|
|
b = await svc.get_or_create_journal(journal_setup["agent_id"])
|
|
assert a.id == b.id
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_journal_by_agent(journal_setup: dict) -> None:
|
|
svc = journal_setup["svc"]
|
|
created = await svc.get_or_create_journal(journal_setup["agent_id"])
|
|
fetched = await svc.get_journal_by_agent(journal_setup["agent_id"])
|
|
assert fetched is not None
|
|
assert fetched.id == created.id
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_journal_by_agent_returns_none_when_missing(
|
|
journal_setup: dict,
|
|
) -> None:
|
|
svc = journal_setup["svc"]
|
|
assert await svc.get_journal_by_agent(uuid4()) is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_entry(journal_setup: dict) -> None:
|
|
svc = journal_setup["svc"]
|
|
journal = await svc.get_or_create_journal(journal_setup["agent_id"])
|
|
entry = await svc.create_entry(
|
|
JournalEntryCreate(
|
|
journal_id=journal.id,
|
|
type=JournalEntryType.GENERAL,
|
|
title="First entry",
|
|
content="Some content here",
|
|
)
|
|
)
|
|
assert entry is not None
|
|
assert entry.title == "First entry"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_entry(journal_setup: dict) -> None:
|
|
svc = journal_setup["svc"]
|
|
journal = await svc.get_or_create_journal(journal_setup["agent_id"])
|
|
created = await svc.create_entry(
|
|
JournalEntryCreate(
|
|
journal_id=journal.id,
|
|
type=JournalEntryType.LEARNING,
|
|
title="learn",
|
|
content="x",
|
|
)
|
|
)
|
|
assert created is not None
|
|
fetched = await svc.get_entry(created.id)
|
|
assert fetched is not None
|
|
assert fetched.id == created.id
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_entry_returns_none_when_missing(journal_setup: dict) -> None:
|
|
svc = journal_setup["svc"]
|
|
assert await svc.get_entry(uuid4()) is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_entries(journal_setup: dict) -> None:
|
|
svc = journal_setup["svc"]
|
|
journal = await svc.get_or_create_journal(journal_setup["agent_id"])
|
|
for i in range(3):
|
|
await svc.create_entry(
|
|
JournalEntryCreate(
|
|
journal_id=journal.id,
|
|
type=JournalEntryType.GENERAL,
|
|
title=f"e{i}",
|
|
content="x",
|
|
)
|
|
)
|
|
entries = await svc.list_entries(journal.id)
|
|
assert len(entries) >= 3
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_entries_filtered_by_type(journal_setup: dict) -> None:
|
|
svc = journal_setup["svc"]
|
|
journal = await svc.get_or_create_journal(journal_setup["agent_id"])
|
|
await svc.create_entry(
|
|
JournalEntryCreate(
|
|
journal_id=journal.id,
|
|
type=JournalEntryType.LEARNING,
|
|
title="L",
|
|
content="x",
|
|
)
|
|
)
|
|
await svc.create_entry(
|
|
JournalEntryCreate(
|
|
journal_id=journal.id,
|
|
type=JournalEntryType.STRUGGLE,
|
|
title="S",
|
|
content="y",
|
|
)
|
|
)
|
|
learning_only = await svc.list_entries(
|
|
journal.id, ListEntriesFilter(entry_type=JournalEntryType.LEARNING)
|
|
)
|
|
assert all(e.type == JournalEntryType.LEARNING for e in learning_only)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_entry(journal_setup: dict) -> None:
|
|
svc = journal_setup["svc"]
|
|
journal = await svc.get_or_create_journal(journal_setup["agent_id"])
|
|
entry = await svc.create_entry(
|
|
JournalEntryCreate(
|
|
journal_id=journal.id,
|
|
type=JournalEntryType.GENERAL,
|
|
title="del",
|
|
content="x",
|
|
)
|
|
)
|
|
assert entry is not None
|
|
deleted = await svc.delete_entry(entry.id)
|
|
assert deleted is True
|
|
assert await svc.get_entry(entry.id) is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_entry_returns_false_for_missing(journal_setup: dict) -> None:
|
|
svc = journal_setup["svc"]
|
|
assert await svc.delete_entry(uuid4()) is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_resolve_agent_id_by_slug(journal_setup: dict) -> None:
|
|
svc = journal_setup["svc"]
|
|
resolved = await svc.resolve_agent_id(journal_setup["agent"].slug)
|
|
assert resolved == journal_setup["agent_id"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_resolve_agent_id_by_uuid(journal_setup: dict) -> None:
|
|
svc = journal_setup["svc"]
|
|
resolved = await svc.resolve_agent_id(str(journal_setup["agent_id"]))
|
|
assert resolved == journal_setup["agent_id"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_resolve_agent_id_returns_none_for_unknown(
|
|
journal_setup: dict,
|
|
) -> None:
|
|
svc = journal_setup["svc"]
|
|
assert await svc.resolve_agent_id("unknown-slug") is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_agent_slug(journal_setup: dict) -> None:
|
|
svc = journal_setup["svc"]
|
|
slug = await svc.get_agent_slug(journal_setup["agent_id"])
|
|
assert slug == journal_setup["agent"].slug
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_agent_slug_returns_none_for_unknown(
|
|
journal_setup: dict,
|
|
) -> None:
|
|
svc = journal_setup["svc"]
|
|
assert await svc.get_agent_slug(uuid4()) is None
|
|
|
|
|
|
def _reflection(tid) -> TaskReflectionParams:
|
|
return TaskReflectionParams(
|
|
task_id=tid,
|
|
title="r",
|
|
what_done="d",
|
|
what_learned="l",
|
|
what_struggled="s",
|
|
next_steps=["n"],
|
|
)
|
|
|
|
|
|
def _decision(tid) -> DecisionLogParams:
|
|
return DecisionLogParams(
|
|
title="d",
|
|
context="ctx",
|
|
options=[{"name": "a", "rationale": "r"}],
|
|
chosen="a",
|
|
rationale="r",
|
|
consequences=["c"],
|
|
task_id=tid,
|
|
)
|
|
|
|
|
|
def _learning(tid) -> LearningEntryParams:
|
|
return LearningEntryParams(title="l", what_learned="x", task_id=tid)
|
|
|
|
|
|
def _struggle(tid) -> StruggleEntryParams:
|
|
return StruggleEntryParams(
|
|
title="s",
|
|
what_struggled="x",
|
|
attempted_solutions=["try1"],
|
|
task_id=tid,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_helper_add_methods(journal_setup: dict) -> None:
|
|
"""add_task_reflection / add_decision_log / add_learning / add_struggle."""
|
|
svc = journal_setup["svc"]
|
|
aid = journal_setup["agent_id"]
|
|
tid = journal_setup["task_id"]
|
|
refl = await svc.add_task_reflection(aid, _reflection(tid))
|
|
assert refl is not None
|
|
dec = await svc.add_decision_log(aid, _decision(tid))
|
|
assert dec is not None
|
|
lrn = await svc.add_learning(aid, _learning(tid))
|
|
assert lrn is not None
|
|
strug = await svc.add_struggle(aid, _struggle(tid))
|
|
assert strug is not None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_has_decision_learning_reflect_for_task(journal_setup: dict) -> None:
|
|
svc = journal_setup["svc"]
|
|
aid = journal_setup["agent_id"]
|
|
tid = journal_setup["task_id"]
|
|
|
|
assert await svc.has_decision_for_task(aid, tid) is False
|
|
await svc.add_decision_log(aid, _decision(tid))
|
|
assert await svc.has_decision_for_task(aid, tid) is True
|
|
|
|
assert await svc.has_learning_for_task(aid, tid) is False
|
|
await svc.add_learning(aid, _learning(tid))
|
|
assert await svc.has_learning_for_task(aid, tid) is True
|
|
|
|
assert await svc.has_reflect_for_task(aid, tid) is False
|
|
await svc.add_task_reflection(aid, _reflection(tid))
|
|
assert await svc.has_reflect_for_task(aid, tid) is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_journal_stats(journal_setup: dict) -> None:
|
|
svc = journal_setup["svc"]
|
|
journal = await svc.get_or_create_journal(journal_setup["agent_id"])
|
|
await svc.create_entry(
|
|
JournalEntryCreate(
|
|
journal_id=journal.id,
|
|
type=JournalEntryType.LEARNING,
|
|
title="L",
|
|
content="x",
|
|
)
|
|
)
|
|
stats = await svc.get_journal_stats(journal.id)
|
|
assert stats is not None
|
|
assert stats.total_entries >= 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_journal_by_id(journal_setup: dict) -> None:
|
|
svc = journal_setup["svc"]
|
|
created = await svc.get_or_create_journal(journal_setup["agent_id"])
|
|
fetched = await svc.get_journal(created.id)
|
|
assert fetched is not None
|
|
assert fetched.id == created.id
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_journal_by_id_returns_none(journal_setup: dict) -> None:
|
|
svc = journal_setup["svc"]
|
|
assert await svc.get_journal(uuid4()) is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_growth_metrics_for_unknown_agent(journal_setup: dict) -> None:
|
|
svc = journal_setup["svc"]
|
|
assert await svc.get_growth_metrics(uuid4()) is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_growth_metrics_returns_metrics(journal_setup: dict) -> None:
|
|
svc = journal_setup["svc"]
|
|
aid = journal_setup["agent_id"]
|
|
journal = await svc.get_or_create_journal(aid)
|
|
await svc.create_entry(
|
|
JournalEntryCreate(
|
|
journal_id=journal.id,
|
|
type=JournalEntryType.LEARNING,
|
|
title="L",
|
|
content="x",
|
|
)
|
|
)
|
|
# Manually bump entries_by_type so growth_metrics has something to count.
|
|
metrics = await svc.get_growth_metrics(aid)
|
|
assert metrics is not None
|
|
assert hasattr(metrics, "total_learnings")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_write_struggle(journal_setup: dict) -> None:
|
|
svc = journal_setup["svc"]
|
|
aid = journal_setup["agent_id"]
|
|
tid = journal_setup["task_id"]
|
|
entry = await svc.write_struggle(
|
|
agent_id=aid,
|
|
task_id=tid,
|
|
content="Couldn't connect to the database.\nGave up after 3 hours.",
|
|
)
|
|
assert entry is not None
|
|
# Title is the first line truncated.
|
|
assert entry.title.startswith("Couldn't connect")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_write_entry_dispatches_by_scope(journal_setup: dict) -> None:
|
|
svc = journal_setup["svc"]
|
|
aid = journal_setup["agent_id"]
|
|
entry = await svc.write_entry(agent_id=aid, title="x", content="y", scope="note")
|
|
assert entry is not None
|
|
assert entry.type == JournalEntryType.GENERAL
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_write_entry_rejects_unknown_scope(journal_setup: dict) -> None:
|
|
svc = journal_setup["svc"]
|
|
with pytest.raises(ValueError, match="unknown scope"):
|
|
await svc.write_entry(
|
|
agent_id=journal_setup["agent_id"],
|
|
title="x",
|
|
content="y",
|
|
scope="bogus",
|
|
)
|