| Server IP : 3.147.158.171 / Your IP : 216.73.216.88 Web Server : Apache/2.4.67 (Amazon Linux) OpenSSL/3.5.5 System : Linux ip-172-31-2-178.us-east-2.compute.internal 6.1.172-216.329.amzn2023.x86_64 #1 SMP PREEMPT_DYNAMIC Wed May 20 06:31:34 UTC 2026 x86_64 User : ec2-user ( 1000) PHP Version : 8.4.21 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /tsai/repo/api/tests/unit/ |
Upload File : |
"""Unit tests for the Character Studio agent tools.
These drive the async tools synchronously via asyncio.run (no pytest-asyncio in
this suite) and mock the HTTP helpers / article service so nothing touches Drupal,
WordPress, or the network. The key guarantee under test: the test-article tool
generates an *unpublished draft*.
"""
import asyncio
from unittest.mock import AsyncMock, patch
from app.appTypes import (
ArticleCreationResult,
ArticleResponse,
LLModel,
LLMProvider,
SiteConfig,
)
from app.services.character_studio_tools import CharacterStudioToolset, _slugify
def make_site() -> SiteConfig:
return SiteConfig(
cb_host="chat.foo.test", api_host="http://api.foo.test", wp_host="http://foo.test",
wp_dir="foo", drupal_host="http://drupal.test", drupal_api="/jsonapi",
slug="foo", drupal_uid="user-uuid", drupal_target_id=5,
)
def make_char(**over) -> dict:
base = {
"slug": "nova", "name": "Nova", "email": "nova@example.com",
"description": "You are Nova.", "enabled": True, "date": "2026-07-10",
"researchCurrentEvents": False,
}
base.update(over)
return base
class FakeResp:
def __init__(self, status=200, payload=None, text=""):
self.status_code = status
self._payload = payload or {}
self.text = text
def json(self):
return self._payload
def test_slugify():
assert _slugify("Nova Sparks!") == "nova-sparks"
assert _slugify(" Multi Word ") == "multi-word"
def test_generate_test_article_uses_draft():
ts = CharacterStudioToolset(make_site())
ts._fetch_character = AsyncMock(return_value=make_char())
fake_result = ArticleCreationResult(
article=ArticleResponse(title="The Title", body="Body markdown", category="News", tags=[]),
post_id=42,
actual_model=LLModel(provider=LLMProvider.CLAUDE, name="claude-sonnet-4-6"),
permalink="http://foo.test/?p=42",
)
with patch(
"app.services.character_studio_tools.article_creation_service.create_article",
new=AsyncMock(return_value=fake_result),
) as mock_create:
out = asyncio.run(ts.generate_test_article("nova"))
kwargs = mock_create.call_args.kwargs
assert kwargs["post_status"] == "draft"
assert kwargs["generate_featured_image"] is False
assert kwargs["generate_secondary_image"] is False
assert kwargs["enable_fact_checking"] is False
assert kwargs["enable_corrections"] is False
assert len(ts.frames) == 1
frame = ts.frames[0]
assert frame["type"] == "article_preview"
assert frame["title"] == "The Title"
assert frame["postId"] == 42
assert "DRAFT" in out
def test_generate_test_article_missing_character():
ts = CharacterStudioToolset(make_site())
ts._fetch_character = AsyncMock(return_value=None)
out = asyncio.run(ts.generate_test_article("ghost"))
assert "No character found" in out
assert ts.frames == []
def test_create_character_emits_frame_and_registers_author():
ts = CharacterStudioToolset(make_site())
ts._send = AsyncMock(return_value=FakeResp(200, {"data": {"data": {"id": "node-uuid-1"}}}))
ts._fetch_character = AsyncMock(return_value=make_char())
out = asyncio.run(ts.create_character(name="Nova", description="You are Nova, a science writer."))
# POST /cms/character/ then POST /user/register/
called_paths = [c.args[1] for c in ts._send.call_args_list]
assert "/cms/character/" in called_paths
assert "/user/register/" in called_paths
assert any(f["type"] == "character_updated" for f in ts.frames)
assert "Created character 'Nova'" in out
def test_create_character_requires_search_query_when_research():
ts = CharacterStudioToolset(make_site())
ts._send = AsyncMock()
out = asyncio.run(ts.create_character(name="Nova", description="x", research=True))
assert "search_query" in out
ts._send.assert_not_called()
def test_update_character_appends_description():
ts = CharacterStudioToolset(make_site())
ts._fetch_character = AsyncMock(return_value=make_char())
ts._send = AsyncMock(return_value=FakeResp(200, {"data": make_char(description="merged")}))
asyncio.run(ts.update_character(slug="nova", append_description="Cite primary sources."))
body = ts._send.call_args.args[2] # (method, path, body)
sent_description = body["character"]["description"]
assert sent_description.startswith("You are Nova.")
assert "Cite primary sources." in sent_description
assert any(f["type"] == "character_updated" for f in ts.frames)