mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
428 lines
12 KiB
Python
428 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",
|
|
)
|