2026-07-03 19:24:00 +02:00
|
|
|
"""XEngine coverage: release-post drafting + mentions poll, held/deduped/capped.
|
|
|
|
|
|
|
|
|
|
Mirrors the release-manager engine tests: flag-gated, no-creds no-ops, drafts
|
|
|
|
|
via the local model (mocked here), enforces the 280-char limit, dedupes
|
|
|
|
|
mentions by id, and caps origination per cycle + rolling open count. Never
|
|
|
|
|
posts — asserted against a real Postgres DB.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
|
from unittest.mock import AsyncMock
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
from roboco.config import settings as cfg
|
2026-07-04 07:34:40 +02:00
|
|
|
from roboco.db.tables import AgentTable, ProjectTable, XSeenFeatureTable
|
2026-07-03 19:24:00 +02:00
|
|
|
from roboco.foundation import identity as _foundation
|
|
|
|
|
from roboco.foundation.policy.content import markers
|
|
|
|
|
from roboco.models.base import AgentRole, AgentStatus, Team
|
|
|
|
|
from roboco.models.base import TaskStatus as TS
|
|
|
|
|
from roboco.services import x_engine as x_engine_module
|
2026-07-04 07:34:40 +02:00
|
|
|
from roboco.services.company_goals import get_company_goals_service
|
|
|
|
|
from roboco.services.task import (
|
|
|
|
|
X_FEATURE_EXPLORATION_SOURCE,
|
|
|
|
|
X_FEATURE_SOURCE,
|
|
|
|
|
X_POST_SOURCE,
|
|
|
|
|
X_REPLY_SOURCE,
|
|
|
|
|
get_task_service,
|
|
|
|
|
)
|
2026-07-03 19:24:00 +02:00
|
|
|
from roboco.services.x_client import MAX_TWEET_CHARS, XClient, XMention, XPostResult
|
|
|
|
|
from sqlalchemy import select
|
|
|
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
|
|
|
|
SYSTEM_UUID = _foundation.AGENTS["system"].uuid
|
|
|
|
|
SECRETARY_UUID = _foundation.AGENTS["secretary-1"].uuid
|
2026-07-04 07:34:40 +02:00
|
|
|
HOM_UUID = _foundation.AGENTS["head-marketing"].uuid
|
2026-07-03 19:24:00 +02:00
|
|
|
SLUG = "roboco"
|
|
|
|
|
ONE = 1
|
|
|
|
|
TWO = 2
|
|
|
|
|
_VERSION = "0.17.0"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _FakeClient(XClient):
|
|
|
|
|
"""A configured stub — never touches the network."""
|
|
|
|
|
|
|
|
|
|
def __init__(self, mentions: list[XMention] | None = None) -> None:
|
|
|
|
|
self._mentions = mentions or []
|
|
|
|
|
self.posted: list[str] = []
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def configured(self) -> bool:
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
async def post_tweet(self, text: str) -> XPostResult:
|
|
|
|
|
self.posted.append(text)
|
|
|
|
|
return XPostResult(posted=True, tweet_id="1", detail="ok")
|
|
|
|
|
|
|
|
|
|
async def fetch_mentions(
|
|
|
|
|
self, since_id: str | None, max_results: int
|
|
|
|
|
) -> list[XMention]:
|
|
|
|
|
_ = (since_id, max_results)
|
|
|
|
|
return self._mentions
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _NullClient(XClient):
|
|
|
|
|
@property
|
|
|
|
|
def configured(self) -> bool:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
async def post_tweet(self, text: str) -> XPostResult:
|
|
|
|
|
_ = text
|
|
|
|
|
return XPostResult(posted=False, tweet_id=None, detail="no creds")
|
|
|
|
|
|
|
|
|
|
async def fetch_mentions(
|
|
|
|
|
self, since_id: str | None, max_results: int
|
|
|
|
|
) -> list[XMention]:
|
|
|
|
|
_ = (since_id, max_results)
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _seed(session: AsyncSession) -> None:
|
2026-07-04 07:34:40 +02:00
|
|
|
for uuid, slug, role, team in (
|
|
|
|
|
(SYSTEM_UUID, "system", AgentRole.SYSTEM, None),
|
|
|
|
|
(SECRETARY_UUID, "secretary-1", AgentRole.SECRETARY, None),
|
|
|
|
|
(HOM_UUID, "head-marketing", AgentRole.HEAD_MARKETING, Team.BOARD),
|
2026-07-03 19:24:00 +02:00
|
|
|
):
|
|
|
|
|
if await session.get(AgentTable, uuid) is None:
|
|
|
|
|
session.add(
|
|
|
|
|
AgentTable(
|
|
|
|
|
id=uuid,
|
|
|
|
|
name=slug,
|
|
|
|
|
slug=slug,
|
|
|
|
|
role=role,
|
2026-07-04 07:34:40 +02:00
|
|
|
team=team,
|
2026-07-03 19:24:00 +02:00
|
|
|
status=AgentStatus.ACTIVE,
|
|
|
|
|
model_config={},
|
|
|
|
|
system_prompt="x",
|
|
|
|
|
capabilities=[],
|
|
|
|
|
permissions={},
|
|
|
|
|
metrics={},
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
await session.flush()
|
|
|
|
|
existing = await session.execute(
|
|
|
|
|
select(ProjectTable).where(ProjectTable.slug == SLUG)
|
|
|
|
|
)
|
|
|
|
|
if existing.scalar_one_or_none() is None:
|
|
|
|
|
session.add(
|
|
|
|
|
ProjectTable(
|
|
|
|
|
name="RoboCo",
|
|
|
|
|
slug=SLUG,
|
|
|
|
|
git_url="https://github.com/x/roboco.git",
|
|
|
|
|
default_branch="master",
|
|
|
|
|
protected_branches=["master"],
|
|
|
|
|
assigned_cell=Team.BACKEND,
|
|
|
|
|
created_by=SYSTEM_UUID,
|
|
|
|
|
is_active=True,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
await session.flush()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _enable(monkeypatch: pytest.MonkeyPatch, **overrides: object) -> None:
|
|
|
|
|
monkeypatch.setattr(cfg, "x_engine_enabled", True)
|
|
|
|
|
monkeypatch.setattr(cfg, "x_replies_enabled", True)
|
|
|
|
|
monkeypatch.setattr(cfg, "self_heal_project_slug", SLUG)
|
|
|
|
|
monkeypatch.setattr(cfg, "x_max_open_posts", 10)
|
|
|
|
|
monkeypatch.setattr(cfg, "x_mentions_max_per_cycle", 5)
|
|
|
|
|
monkeypatch.setattr(cfg, "x_mentions_min_engagement", 0)
|
|
|
|
|
for key, value in overrides.items():
|
|
|
|
|
monkeypatch.setattr(cfg, key, value)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _mock_local_model(monkeypatch: pytest.MonkeyPatch, reply: str | None) -> AsyncMock:
|
|
|
|
|
mock = AsyncMock(return_value=reply)
|
|
|
|
|
monkeypatch.setattr(x_engine_module, "_chat", mock)
|
|
|
|
|
return mock
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
# Release-post drafting
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_disabled_drafts_no_release_post(
|
|
|
|
|
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
await _seed(db_session)
|
|
|
|
|
monkeypatch.setattr(cfg, "x_engine_enabled", False)
|
|
|
|
|
engine = x_engine_module.XEngine(db_session, client=_FakeClient())
|
|
|
|
|
task = await engine.draft_release_post(version=_VERSION, highlights=["feat: x"])
|
|
|
|
|
assert task is None
|
|
|
|
|
assert await get_task_service(db_session).list_open_x_posts() == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_no_credentials_drafts_no_release_post(
|
|
|
|
|
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
await _seed(db_session)
|
|
|
|
|
_enable(monkeypatch)
|
|
|
|
|
engine = x_engine_module.XEngine(db_session, client=_NullClient())
|
|
|
|
|
task = await engine.draft_release_post(version=_VERSION, highlights=["feat: x"])
|
|
|
|
|
assert task is None
|
|
|
|
|
assert await get_task_service(db_session).list_open_x_posts() == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_draft_release_post_holds_one_proposal(
|
|
|
|
|
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
await _seed(db_session)
|
|
|
|
|
_enable(monkeypatch)
|
|
|
|
|
_mock_local_model(monkeypatch, "RoboCo just shipped a great new feature!")
|
|
|
|
|
engine = x_engine_module.XEngine(db_session, client=_FakeClient())
|
|
|
|
|
task = await engine.draft_release_post(
|
|
|
|
|
version=_VERSION, highlights=["feat: new thing"]
|
|
|
|
|
)
|
|
|
|
|
assert task is not None
|
|
|
|
|
assert task.status == TS.PENDING
|
|
|
|
|
assert task.confirmed_by_human is False
|
|
|
|
|
assert task.assigned_to == SECRETARY_UUID
|
|
|
|
|
assert task.source == X_POST_SOURCE
|
|
|
|
|
assert markers.get_x_release_version(task) == _VERSION
|
|
|
|
|
body = markers.get_x_draft_body(task)
|
|
|
|
|
assert body is not None
|
|
|
|
|
assert "RoboCo" in body
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_release_post_body_enforces_280_chars(
|
|
|
|
|
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
await _seed(db_session)
|
|
|
|
|
_enable(monkeypatch)
|
|
|
|
|
_mock_local_model(monkeypatch, "x" * 500) # a runaway local-model draft
|
|
|
|
|
engine = x_engine_module.XEngine(db_session, client=_FakeClient())
|
|
|
|
|
task = await engine.draft_release_post(version=_VERSION, highlights=[])
|
|
|
|
|
assert task is not None
|
|
|
|
|
body = markers.get_x_draft_body(task)
|
|
|
|
|
assert body is not None
|
|
|
|
|
assert len(body) <= MAX_TWEET_CHARS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_release_post_falls_back_when_local_model_fails(
|
|
|
|
|
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
await _seed(db_session)
|
|
|
|
|
_enable(monkeypatch)
|
|
|
|
|
mock = AsyncMock(side_effect=RuntimeError("ollama down"))
|
|
|
|
|
monkeypatch.setattr(x_engine_module, "_chat", mock)
|
|
|
|
|
engine = x_engine_module.XEngine(db_session, client=_FakeClient())
|
|
|
|
|
task = await engine.draft_release_post(
|
|
|
|
|
version=_VERSION, highlights=["feat: still works"]
|
|
|
|
|
)
|
|
|
|
|
assert task is not None
|
|
|
|
|
body = markers.get_x_draft_body(task)
|
|
|
|
|
assert body is not None
|
|
|
|
|
assert _VERSION in body
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_draft_release_post_dedupes_same_version(
|
|
|
|
|
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
await _seed(db_session)
|
|
|
|
|
_enable(monkeypatch)
|
|
|
|
|
_mock_local_model(monkeypatch, "shipped!")
|
|
|
|
|
engine = x_engine_module.XEngine(db_session, client=_FakeClient())
|
|
|
|
|
await engine.draft_release_post(version=_VERSION, highlights=[])
|
|
|
|
|
await engine.draft_release_post(version=_VERSION, highlights=[])
|
|
|
|
|
open_posts = await get_task_service(db_session).list_open_x_posts()
|
|
|
|
|
assert len(open_posts) == ONE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_draft_release_post_respects_open_cap(
|
|
|
|
|
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
await _seed(db_session)
|
|
|
|
|
_enable(monkeypatch, x_max_open_posts=1)
|
|
|
|
|
_mock_local_model(monkeypatch, "shipped!")
|
|
|
|
|
engine = x_engine_module.XEngine(db_session, client=_FakeClient())
|
|
|
|
|
await engine.draft_release_post(version="1.0.0", highlights=[])
|
|
|
|
|
task = await engine.draft_release_post(version="2.0.0", highlights=[])
|
|
|
|
|
assert task is None
|
|
|
|
|
open_posts = await get_task_service(db_session).list_open_x_posts()
|
|
|
|
|
assert len(open_posts) == ONE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
# Mentions poll
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _mention(
|
|
|
|
|
mid: str, text: str = "great work @roboco", engagement: int = 1
|
|
|
|
|
) -> XMention:
|
|
|
|
|
return XMention(
|
|
|
|
|
id=mid,
|
|
|
|
|
author_id="author-1",
|
|
|
|
|
text=text,
|
|
|
|
|
like_count=engagement,
|
|
|
|
|
reply_count=0,
|
|
|
|
|
retweet_count=0,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_disabled_run_cycle_originates_nothing(
|
|
|
|
|
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
await _seed(db_session)
|
|
|
|
|
monkeypatch.setattr(cfg, "x_engine_enabled", False)
|
|
|
|
|
engine = x_engine_module.XEngine(
|
|
|
|
|
db_session, client=_FakeClient(mentions=[_mention("m1")])
|
|
|
|
|
)
|
|
|
|
|
result = await engine.run_cycle()
|
|
|
|
|
assert result == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_run_cycle_noop_when_replies_disabled(
|
|
|
|
|
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Engine on but the mention-reply sub-switch off: the poll cycle drafts
|
|
|
|
|
nothing (release posting is a separate, still-enabled path)."""
|
|
|
|
|
await _seed(db_session)
|
|
|
|
|
_enable(monkeypatch, x_replies_enabled=False)
|
|
|
|
|
_mock_local_model(monkeypatch, "Thanks!")
|
|
|
|
|
engine = x_engine_module.XEngine(
|
|
|
|
|
db_session, client=_FakeClient(mentions=[_mention("m1")])
|
|
|
|
|
)
|
|
|
|
|
result = await engine.run_cycle()
|
|
|
|
|
assert result == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_release_post_holds_even_when_replies_disabled(
|
|
|
|
|
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Release posting is decoupled from the mention-reply sub-switch: a
|
|
|
|
|
release draft is still held with replies off."""
|
|
|
|
|
await _seed(db_session)
|
|
|
|
|
_enable(monkeypatch, x_replies_enabled=False)
|
|
|
|
|
_mock_local_model(monkeypatch, "RoboCo shipped something great!")
|
|
|
|
|
engine = x_engine_module.XEngine(db_session, client=_FakeClient())
|
|
|
|
|
task = await engine.draft_release_post(version=_VERSION, highlights=["feat: x"])
|
|
|
|
|
assert task is not None
|
|
|
|
|
assert task.source == X_POST_SOURCE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_no_credentials_run_cycle_originates_nothing(
|
|
|
|
|
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
await _seed(db_session)
|
|
|
|
|
_enable(monkeypatch)
|
|
|
|
|
engine = x_engine_module.XEngine(db_session, client=_NullClient())
|
|
|
|
|
result = await engine.run_cycle()
|
|
|
|
|
assert result == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_meaningful_mention_holds_reply_proposal(
|
|
|
|
|
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
await _seed(db_session)
|
|
|
|
|
_enable(monkeypatch)
|
|
|
|
|
_mock_local_model(monkeypatch, "Thanks so much for the shoutout!")
|
|
|
|
|
engine = x_engine_module.XEngine(
|
|
|
|
|
db_session, client=_FakeClient(mentions=[_mention("m1")])
|
|
|
|
|
)
|
|
|
|
|
result = await engine.run_cycle()
|
|
|
|
|
assert len(result) == ONE
|
|
|
|
|
task = result[0]
|
|
|
|
|
assert task.source == X_REPLY_SOURCE
|
|
|
|
|
assert task.confirmed_by_human is False
|
|
|
|
|
assert task.assigned_to == SECRETARY_UUID
|
|
|
|
|
ref = markers.get_x_mention_ref(task)
|
|
|
|
|
assert ref is not None
|
|
|
|
|
assert ref["id"] == "m1"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_bot_like_mention_is_filtered_out(
|
|
|
|
|
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
await _seed(db_session)
|
|
|
|
|
_enable(monkeypatch)
|
|
|
|
|
_mock_local_model(monkeypatch, "reply")
|
|
|
|
|
engine = x_engine_module.XEngine(
|
|
|
|
|
db_session, client=_FakeClient(mentions=[_mention("m1", text="RT spam")])
|
|
|
|
|
)
|
|
|
|
|
result = await engine.run_cycle()
|
|
|
|
|
assert result == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_below_engagement_floor_is_filtered_out(
|
|
|
|
|
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
await _seed(db_session)
|
|
|
|
|
_enable(monkeypatch, x_mentions_min_engagement=5)
|
|
|
|
|
_mock_local_model(monkeypatch, "reply")
|
|
|
|
|
engine = x_engine_module.XEngine(
|
|
|
|
|
db_session, client=_FakeClient(mentions=[_mention("m1", engagement=1)])
|
|
|
|
|
)
|
|
|
|
|
result = await engine.run_cycle()
|
|
|
|
|
assert result == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_mentions_dedupe_across_cycles(
|
|
|
|
|
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
await _seed(db_session)
|
|
|
|
|
_enable(monkeypatch)
|
|
|
|
|
_mock_local_model(monkeypatch, "reply")
|
|
|
|
|
client = _FakeClient(mentions=[_mention("m1")])
|
|
|
|
|
engine = x_engine_module.XEngine(db_session, client=client)
|
|
|
|
|
first = await engine.run_cycle()
|
|
|
|
|
second = await engine.run_cycle()
|
|
|
|
|
assert len(first) == ONE
|
|
|
|
|
assert second == [] # same mention id -> already seen, not re-proposed
|
|
|
|
|
open_posts = await get_task_service(db_session).list_open_x_posts()
|
|
|
|
|
assert len(open_posts) == ONE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_mentions_per_cycle_cap(
|
|
|
|
|
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
await _seed(db_session)
|
|
|
|
|
_enable(monkeypatch, x_mentions_max_per_cycle=2)
|
|
|
|
|
_mock_local_model(monkeypatch, "reply")
|
|
|
|
|
mentions = [_mention(f"m{i}") for i in range(5)]
|
|
|
|
|
engine = x_engine_module.XEngine(db_session, client=_FakeClient(mentions=mentions))
|
|
|
|
|
result = await engine.run_cycle()
|
|
|
|
|
assert len(result) == TWO
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_mentions_respect_open_cap(
|
|
|
|
|
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
await _seed(db_session)
|
|
|
|
|
_enable(monkeypatch, x_max_open_posts=1, x_mentions_max_per_cycle=5)
|
|
|
|
|
_mock_local_model(monkeypatch, "reply")
|
|
|
|
|
mentions = [_mention(f"cap{i}") for i in range(3)]
|
|
|
|
|
engine = x_engine_module.XEngine(db_session, client=_FakeClient(mentions=mentions))
|
|
|
|
|
result = await engine.run_cycle()
|
|
|
|
|
assert len(result) == ONE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_reply_body_enforces_280_chars(
|
|
|
|
|
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
await _seed(db_session)
|
|
|
|
|
_enable(monkeypatch)
|
|
|
|
|
_mock_local_model(monkeypatch, "y" * 400)
|
|
|
|
|
engine = x_engine_module.XEngine(
|
|
|
|
|
db_session, client=_FakeClient(mentions=[_mention("m1")])
|
|
|
|
|
)
|
|
|
|
|
result = await engine.run_cycle()
|
|
|
|
|
assert len(result) == ONE
|
|
|
|
|
body = markers.get_x_draft_body(result[0])
|
|
|
|
|
assert body is not None
|
|
|
|
|
assert len(body) <= MAX_TWEET_CHARS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_engine_never_calls_post_tweet(
|
|
|
|
|
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Neither responsibility ever posts — that is XPostService's job only."""
|
|
|
|
|
await _seed(db_session)
|
|
|
|
|
_enable(monkeypatch)
|
|
|
|
|
_mock_local_model(monkeypatch, "reply")
|
|
|
|
|
client = _FakeClient(mentions=[_mention("m1")])
|
|
|
|
|
engine = x_engine_module.XEngine(db_session, client=client)
|
|
|
|
|
await engine.run_cycle()
|
|
|
|
|
await engine.draft_release_post(version=_VERSION, highlights=[])
|
|
|
|
|
assert client.posted == []
|
2026-07-04 07:34:40 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
# Feature-spotlight exploration (Head of Marketing)
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_feature_spotlight_disabled_creates_no_exploration(
|
|
|
|
|
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
await _seed(db_session)
|
|
|
|
|
monkeypatch.setattr(cfg, "x_engine_enabled", False)
|
|
|
|
|
engine = x_engine_module.XEngine(db_session, client=_FakeClient())
|
|
|
|
|
task = await engine.open_feature_spotlight_exploration()
|
|
|
|
|
assert task is None
|
|
|
|
|
assert await get_task_service(db_session).list_open_feature_explorations() == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_feature_spotlight_subswitch_off_creates_no_exploration(
|
|
|
|
|
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
"""x_engine_enabled on but x_feature_spotlight_enabled off: no exploration —
|
|
|
|
|
the engine still drafts release posts/mention replies via the other paths."""
|
|
|
|
|
await _seed(db_session)
|
|
|
|
|
_enable(monkeypatch, x_feature_spotlight_enabled=False)
|
|
|
|
|
engine = x_engine_module.XEngine(db_session, client=_FakeClient())
|
|
|
|
|
task = await engine.open_feature_spotlight_exploration()
|
|
|
|
|
assert task is None
|
|
|
|
|
assert await get_task_service(db_session).list_open_feature_explorations() == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_feature_spotlight_no_credentials_creates_no_exploration(
|
|
|
|
|
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
await _seed(db_session)
|
|
|
|
|
_enable(monkeypatch, x_feature_spotlight_enabled=True)
|
|
|
|
|
engine = x_engine_module.XEngine(db_session, client=_NullClient())
|
|
|
|
|
task = await engine.open_feature_spotlight_exploration()
|
|
|
|
|
assert task is None
|
|
|
|
|
assert await get_task_service(db_session).list_open_feature_explorations() == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_feature_spotlight_dedupe_one_open_cycle(
|
|
|
|
|
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
await _seed(db_session)
|
|
|
|
|
_enable(monkeypatch, x_feature_spotlight_enabled=True)
|
|
|
|
|
engine = x_engine_module.XEngine(db_session, client=_FakeClient())
|
|
|
|
|
first = await engine.open_feature_spotlight_exploration()
|
|
|
|
|
second = await engine.open_feature_spotlight_exploration()
|
|
|
|
|
assert first is not None
|
|
|
|
|
assert second is None
|
|
|
|
|
open_cycles = await get_task_service(db_session).list_open_feature_explorations()
|
|
|
|
|
assert len(open_cycles) == ONE
|
|
|
|
|
cycle = open_cycles[0]
|
|
|
|
|
assert cycle.status == TS.PENDING
|
|
|
|
|
assert cycle.confirmed_by_human is False # HELD; board-dispatched only
|
|
|
|
|
assert cycle.assigned_to == HOM_UUID
|
|
|
|
|
assert cycle.team == Team.BOARD
|
|
|
|
|
assert cycle.source == X_FEATURE_EXPLORATION_SOURCE
|
|
|
|
|
assert "spotlight" in cycle.title.lower()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_feature_spotlight_respects_open_post_cap(
|
|
|
|
|
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
await _seed(db_session)
|
|
|
|
|
_enable(monkeypatch, x_feature_spotlight_enabled=True, x_max_open_posts=1)
|
|
|
|
|
_mock_local_model(monkeypatch, "shipped!")
|
|
|
|
|
engine = x_engine_module.XEngine(db_session, client=_FakeClient())
|
|
|
|
|
# Fill the shared open-post cap with an unrelated release draft first.
|
|
|
|
|
await engine.draft_release_post(version="1.0.0", highlights=[])
|
|
|
|
|
task = await engine.open_feature_spotlight_exploration()
|
|
|
|
|
assert task is None
|
|
|
|
|
assert await get_task_service(db_session).list_open_feature_explorations() == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_feature_spotlight_unresolvable_project_no_cycle(
|
|
|
|
|
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
await _seed(db_session)
|
|
|
|
|
_enable(monkeypatch, x_feature_spotlight_enabled=True)
|
|
|
|
|
monkeypatch.setattr(cfg, "self_heal_project_slug", "no-such-project")
|
|
|
|
|
engine = x_engine_module.XEngine(db_session, client=_FakeClient())
|
|
|
|
|
task = await engine.open_feature_spotlight_exploration()
|
|
|
|
|
assert task is None
|
|
|
|
|
assert await get_task_service(db_session).list_open_feature_explorations() == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_feature_spotlight_exploration_carries_seen_features_marker(
|
|
|
|
|
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
await _seed(db_session)
|
|
|
|
|
db_session.add(XSeenFeatureTable(feature_slug="old-feature-1"))
|
|
|
|
|
db_session.add(XSeenFeatureTable(feature_slug="old-feature-2"))
|
|
|
|
|
await db_session.flush()
|
|
|
|
|
_enable(monkeypatch, x_feature_spotlight_enabled=True)
|
|
|
|
|
engine = x_engine_module.XEngine(db_session, client=_FakeClient())
|
|
|
|
|
task = await engine.open_feature_spotlight_exploration()
|
|
|
|
|
assert task is not None
|
|
|
|
|
assert set(markers.get_x_seen_features(task)) == {
|
|
|
|
|
"old-feature-1",
|
|
|
|
|
"old-feature-2",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_materialize_feature_spotlight_holds_draft_and_completes_exploration(
|
|
|
|
|
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
await _seed(db_session)
|
|
|
|
|
_enable(monkeypatch, x_feature_spotlight_enabled=True)
|
|
|
|
|
engine = x_engine_module.XEngine(db_session, client=_FakeClient())
|
|
|
|
|
exploration = await engine.open_feature_spotlight_exploration()
|
|
|
|
|
assert exploration is not None
|
|
|
|
|
|
|
|
|
|
draft = await engine.materialize_feature_spotlight(
|
|
|
|
|
exploration_task=exploration,
|
|
|
|
|
feature_slug="org-memory",
|
|
|
|
|
feature_title="Organizational Memory Loop",
|
|
|
|
|
body="Did you know RoboCo agents learn from every completed task?",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert draft.source == X_FEATURE_SOURCE
|
|
|
|
|
assert draft.assigned_to == SECRETARY_UUID
|
|
|
|
|
assert draft.confirmed_by_human is False
|
|
|
|
|
assert draft.status == TS.PENDING
|
|
|
|
|
ref = markers.get_x_feature_ref(draft)
|
|
|
|
|
assert ref is not None
|
|
|
|
|
assert ref["slug"] == "org-memory"
|
|
|
|
|
assert ref["title"] == "Organizational Memory Loop"
|
|
|
|
|
body = markers.get_x_draft_body(draft)
|
|
|
|
|
assert body is not None
|
|
|
|
|
assert "RoboCo" in body
|
|
|
|
|
|
|
|
|
|
# The exploration task itself is completed as a side effect...
|
|
|
|
|
assert exploration.status == TS.COMPLETED
|
|
|
|
|
# ...and therefore excluded from the open-cycle list on the next query.
|
|
|
|
|
still_open = await get_task_service(db_session).list_open_feature_explorations()
|
|
|
|
|
assert still_open == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_materialize_feature_spotlight_marks_feature_seen(
|
|
|
|
|
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
await _seed(db_session)
|
|
|
|
|
_enable(monkeypatch, x_feature_spotlight_enabled=True)
|
|
|
|
|
engine = x_engine_module.XEngine(db_session, client=_FakeClient())
|
|
|
|
|
exploration = await engine.open_feature_spotlight_exploration()
|
|
|
|
|
assert exploration is not None
|
|
|
|
|
|
|
|
|
|
assert await engine.is_feature_seen("sandboxed-dev-db") is False
|
|
|
|
|
await engine.materialize_feature_spotlight(
|
|
|
|
|
exploration_task=exploration,
|
|
|
|
|
feature_slug="sandboxed-dev-db",
|
|
|
|
|
feature_title="Sandboxed Dev DB/Redis",
|
|
|
|
|
body="Every agent now gets a throwaway Postgres + Redis sandbox.",
|
|
|
|
|
)
|
|
|
|
|
assert await engine.is_feature_seen("sandboxed-dev-db") is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_materialize_feature_spotlight_enforces_280_chars(
|
|
|
|
|
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
await _seed(db_session)
|
|
|
|
|
_enable(monkeypatch, x_feature_spotlight_enabled=True)
|
|
|
|
|
engine = x_engine_module.XEngine(db_session, client=_FakeClient())
|
|
|
|
|
exploration = await engine.open_feature_spotlight_exploration()
|
|
|
|
|
assert exploration is not None
|
|
|
|
|
|
|
|
|
|
draft = await engine.materialize_feature_spotlight(
|
|
|
|
|
exploration_task=exploration,
|
|
|
|
|
feature_slug="runaway-body",
|
|
|
|
|
feature_title="Runaway Body",
|
|
|
|
|
body="z" * 500, # a runaway HoM-authored draft
|
|
|
|
|
)
|
|
|
|
|
body = markers.get_x_draft_body(draft)
|
|
|
|
|
assert body is not None
|
|
|
|
|
assert len(body) <= MAX_TWEET_CHARS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
# Voice guide (feeds release/reply prompts + the HoM identity's briefing claim)
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_voice_guide_falls_back_when_brand_voice_unset(
|
|
|
|
|
db_session: AsyncSession,
|
|
|
|
|
) -> None:
|
|
|
|
|
await get_company_goals_service(db_session).upsert({"brand_voice": ""})
|
|
|
|
|
engine = x_engine_module.XEngine(db_session, client=_FakeClient())
|
|
|
|
|
voice = await engine._voice_guide()
|
|
|
|
|
assert voice == x_engine_module._HOM_VOICE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_voice_guide_appends_brand_voice_when_set(
|
|
|
|
|
db_session: AsyncSession,
|
|
|
|
|
) -> None:
|
|
|
|
|
await get_company_goals_service(db_session).upsert(
|
|
|
|
|
{"brand_voice": "Dry wit, never an exclamation point."}
|
|
|
|
|
)
|
|
|
|
|
engine = x_engine_module.XEngine(db_session, client=_FakeClient())
|
|
|
|
|
voice = await engine._voice_guide()
|
|
|
|
|
assert x_engine_module._HOM_VOICE in voice
|
|
|
|
|
assert "Dry wit, never an exclamation point." in voice
|