Files
roboco/tests/integration/test_journal_service.py
T
Renn F 64c48356d0 test: lift coverage 41% → 76% (+1068 tests across 36 files)
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.
2026-05-06 00:32:52 +02:00

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",
)