Files

328 lines
11 KiB
Python
Raw Permalink Normal View History

"""TgCockpitService coverage: the /telegram/today aggregate composes
needs-you counts, fleet snapshot, spend, and ship state from seeded rows.
Assertions are DELTAS against a pre-seed baseline, never absolute counts —
CI runs the whole test tree in one process against one database, so other
suites' committed rows are visible here and an "empty company" cannot be
assumed.
"""
from __future__ import annotations
from datetime import UTC, date, datetime, timedelta
from typing import TYPE_CHECKING, cast
from uuid import uuid4
import pytest
from roboco.config import settings
from roboco.db.tables import AgentSpawnSessionTable, AgentTable, TaskTable
from roboco.foundation import identity as _foundation
from roboco.foundation.policy.content import markers
from roboco.models.base import (
AgentRole,
AgentStatus,
Complexity,
Team,
)
from roboco.models.base import TaskNature as TN
from roboco.models.base import TaskStatus as TS
from roboco.models.base import TaskType as TT
from roboco.services.task import (
RELEASE_MANAGER_SOURCE,
ROADMAP_SOURCE,
VIDEO_POST_SOURCE,
X_POST_SOURCE,
)
from roboco.services.tg_cockpit import get_tg_cockpit_service
if TYPE_CHECKING:
from uuid import UUID
from sqlalchemy.ext.asyncio import AsyncSession
SYSTEM_UUID = _foundation.AGENTS["system"].uuid
CI_WATCH_SOURCE = "ci_watch"
_COST_TOL = 0.005
_TOKENS_FLOOR = 1
def _spawn_session(
*, started_at: datetime, model: str, cost: float, tokens_input: int = 1000
) -> AgentSpawnSessionTable:
return AgentSpawnSessionTable(
id=uuid4(),
agent_slug=f"be-dev-{uuid4().hex[:6]}",
team="backend",
role="developer",
model=model,
task_id=None,
started_at=started_at,
ended_at=started_at + timedelta(minutes=5),
tokens_input=tokens_input,
tokens_output=0,
estimated_cost_usd=cost,
)
# 1 awaiting + 1 blocked + 4 held drafts (release/x/video/roadmap-item).
EXPECTED_NEEDS_YOU_TOTAL = 6
async def _seed_system_agent(session: AsyncSession) -> None:
if await session.get(AgentTable, SYSTEM_UUID) is None:
session.add(
AgentTable(
id=SYSTEM_UUID,
name="system",
slug="system",
role=AgentRole.SYSTEM,
team=None,
status=AgentStatus.ACTIVE,
model_config={},
system_prompt="x",
capabilities=[],
permissions={},
metrics={},
)
)
await session.flush()
def _task(
title: str,
task_status: TS,
*,
source: str | None = None,
team: Team = Team.BACKEND,
) -> TaskTable:
# priority 0 (critical) sorts seeded rows ahead of any leaked ones, so
# they stay inside the brief's per-section item caps.
return TaskTable(
id=uuid4(),
title=title,
description="A description long enough to satisfy any length floor.",
acceptance_criteria=["it is visible on the Today brief"],
status=task_status,
priority=0,
task_type=TT.ADMINISTRATIVE,
nature=TN.NON_TECHNICAL,
estimated_complexity=Complexity.LOW,
created_by=SYSTEM_UUID,
team=team,
source=source,
confirmed_by_human=True,
)
async def _seed_working_agent(session: AsyncSession, current_task_id: UUID) -> str:
slug = f"be-dev-{uuid4().hex[:6]}"
session.add(
AgentTable(
id=uuid4(),
name=slug,
slug=slug,
role=AgentRole.DEVELOPER,
team=Team.BACKEND,
status=AgentStatus.ACTIVE,
model_config={},
system_prompt="x",
capabilities=[],
permissions={},
metrics={},
current_task_id=current_task_id,
)
)
await session.flush()
return slug
@pytest.mark.asyncio
async def test_today_brief_shape(db_session: AsyncSession) -> None:
"""Structure + invariants that hold regardless of shared-DB residue."""
brief = await get_tg_cockpit_service(db_session).today()
assert set(brief) == {"needs_you", "fleet", "spend", "velocity", "ship"}
assert len(brief["spend"]["series"]) == 7 # noqa: PLR2004
assert len(brief["velocity"]["series"]) == 7 # noqa: PLR2004
needs = brief["needs_you"]
assert needs["total"] == (
needs["awaiting_ceo_count"]
+ needs["blocked_count"]
+ sum(needs["held_drafts"].values())
)
assert isinstance(brief["spend"]["tokens_today"], int)
assert isinstance(brief["spend"]["cost_today_usd"], float)
assert brief["ship"]["version"] == settings.app_version
@pytest.mark.asyncio
async def test_today_composes_needs_you_fleet_and_ship(
db_session: AsyncSession,
) -> None:
await _seed_system_agent(db_session)
baseline = await get_tg_cockpit_service(db_session).today()
awaiting = _task("Root PR ready", TS.AWAITING_CEO_APPROVAL)
blocked = _task("Stuck on infra", TS.BLOCKED)
x_draft = _task("X draft", TS.PENDING, source=X_POST_SOURCE, team=Team.BOARD)
video_draft = _task(
"Video draft", TS.PENDING, source=VIDEO_POST_SOURCE, team=Team.BOARD
)
release_prop = _task(
"Release 0.26.0", TS.PENDING, source=RELEASE_MANAGER_SOURCE, team=Team.BOARD
)
ci_fix = _task("Fix red CI", TS.PENDING, source=CI_WATCH_SOURCE)
cycle = _task("Roadmap cycle", TS.PENDING, source=ROADMAP_SOURCE, team=Team.BOARD)
for row in (
awaiting,
blocked,
x_draft,
video_draft,
release_prop,
ci_fix,
cycle,
):
db_session.add(row)
await db_session.flush()
markers.set_roadmap_cycle(
cycle,
{
"goal": "g",
"items": [
{"id": "item-0", "status": "proposed"},
{"id": "item-1", "status": "approved"},
],
},
)
agent_slug = await _seed_working_agent(db_session, cast("UUID", awaiting.id))
brief = await get_tg_cockpit_service(db_session).today()
needs, base_needs = brief["needs_you"], baseline["needs_you"]
assert needs["awaiting_ceo_count"] == base_needs["awaiting_ceo_count"] + 1
seeded = next(
item for item in needs["awaiting_ceo"] if item["title"] == "Root PR ready"
)
assert seeded["status"] == "awaiting_ceo_approval"
assert needs["blocked_count"] == base_needs["blocked_count"] + 1
for key in ("release_proposals", "x_posts", "video_posts", "roadmap_items"):
assert needs["held_drafts"][key] == base_needs["held_drafts"][key] + 1
assert needs["total"] == base_needs["total"] + EXPECTED_NEEDS_YOU_TOTAL
workers = {
agent["name"]: agent.get("task_title") for agent in brief["fleet"]["working"]
}
assert workers.get(agent_slug) == "Root PR ready"
assert brief["ship"]["open_release_proposal"] is True
assert brief["ship"]["ci_fix_tasks"] == baseline["ship"]["ci_fix_tasks"] + 1
# ---------------------------------------------------------------------------
# Display-timezone bucketing (Issue 2) — "today" is display_timezone-aware,
# not always the server's UTC day. `_session_metrics_by_day` is called
# directly with an explicit historical `days` window so the test is fully
# deterministic (disconnected from the real "now").
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_session_metrics_by_day_buckets_by_display_timezone(
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
) -> None:
"""23:30 UTC on the 15th is already 00:30 on the 16th in Europe/Berlin
(winter, CET = UTC+1) — the exact 'evening activity lands on the wrong
display day' bug this fix targets."""
started = datetime(2026, 1, 15, 23, 30, tzinfo=UTC)
session_row = _spawn_session(started_at=started, model="claude-sonnet-5", cost=1.23)
db_session.add(session_row)
await db_session.flush()
svc = get_tg_cockpit_service(db_session)
monkeypatch.setattr(settings, "display_timezone", "UTC")
cost_utc, tokens_utc, _ = await svc._session_metrics_by_day([date(2026, 1, 15)])
assert cost_utc.get(date(2026, 1, 15), 0.0) >= _COST_TOL
assert tokens_utc.get(date(2026, 1, 15), 0) >= _TOKENS_FLOOR
monkeypatch.setattr(settings, "display_timezone", "Europe/Berlin")
cost_berlin, tokens_berlin, _ = await svc._session_metrics_by_day(
[date(2026, 1, 16)]
)
assert cost_berlin.get(date(2026, 1, 16), 0.0) >= _COST_TOL
assert tokens_berlin.get(date(2026, 1, 16), 0) >= _TOKENS_FLOOR
# And the SAME row must NOT double-count into the UTC calendar day under
# the Berlin bucketing — the 15th should now come up empty for this row.
cost_berlin_15, _, _ = await svc._session_metrics_by_day([date(2026, 1, 15)])
assert cost_berlin_15.get(date(2026, 1, 15), 0.0) < _COST_TOL
@pytest.mark.asyncio
async def test_window_dates_shifts_with_display_timezone(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""`_window_dates` (real 'now') must reflect the configured display
timezone, not always UTC."""
svc = get_tg_cockpit_service(cast("AsyncSession", None))
monkeypatch.setattr(settings, "display_timezone", "UTC")
utc_dates = svc._window_dates()
monkeypatch.setattr(settings, "display_timezone", "Pacific/Kiritimati")
# UTC+14 — the furthest-ahead real timezone; "today" there is never
# earlier, and is later whenever UTC hasn't crossed its own midnight yet.
kiritimati_dates = svc._window_dates()
assert kiritimati_dates[-1] >= utc_dates[-1]
# ---------------------------------------------------------------------------
# Ollama Cloud honesty-labeling (Issue 1) — an ungrounded ':cloud' model's $0
# is flagged subscription_billed, never rendered as a bare misleading "$0".
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_today_spend_flags_ungrounded_ollama_cloud_as_subscription_billed(
db_session: AsyncSession,
) -> None:
now = datetime.now(UTC)
db_session.add(
_spawn_session(started_at=now, model="some-future-model:cloud", cost=0.0)
)
await db_session.flush()
summary = await get_tg_cockpit_service(db_session).today_spend()
assert summary["cost_today_usd"] == pytest.approx(0.0)
assert summary["subscription_billed"] is True
@pytest.mark.asyncio
async def test_today_spend_not_subscription_billed_when_priced(
db_session: AsyncSession,
) -> None:
"""A real per-token cost (even from a priced Ollama Cloud model like
GLM-5.2) is never mislabeled as an untracked subscription figure."""
now = datetime.now(UTC)
db_session.add(_spawn_session(started_at=now, model="glm-5.2:cloud", cost=2.5))
await db_session.flush()
summary = await get_tg_cockpit_service(db_session).today_spend()
assert summary["subscription_billed"] is False
@pytest.mark.asyncio
async def test_today_spend_not_subscription_billed_for_local_ollama(
db_session: AsyncSession,
) -> None:
"""A genuinely-free self-hosted model (no ':cloud' tag) at $0 is just
free, not an untracked subscription."""
now = datetime.now(UTC)
db_session.add(_spawn_session(started_at=now, model="ollama/llama3", cost=0.0))
await db_session.flush()
summary = await get_tg_cockpit_service(db_session).today_spend()
assert summary["subscription_billed"] is False