104 lines
4.2 KiB
Python
104 lines
4.2 KiB
Python
"""ai-draft unit tests (test-after, Standard strategy).
|
|
|
|
Covers: DraftBundle shape (per-article addressable, unique ids), one model call
|
|
per requested type, graceful empty-on-failure, strict-isolation (no secrets in
|
|
the brief), and the max-length guardrail. Uses a StubModelClient — no real
|
|
network calls in CI.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from newspaper.ai_draft import build_brief, draft_newspaper
|
|
from newspaper.ai_draft.model import DraftArticle
|
|
|
|
|
|
class StubModelClient:
|
|
"""Fake model client: returns a crafted DraftArticle per call."""
|
|
|
|
def __init__(self, fail_on: set[str] | None = None) -> None:
|
|
self.calls: list[str] = []
|
|
self.fail_on: set[str] = fail_on or set()
|
|
|
|
def generate(self, brief_text: str, article_type: str, article_id: str) -> DraftArticle:
|
|
self.calls.append(article_type)
|
|
if article_type in self.fail_on:
|
|
raise RuntimeError("model unavailable")
|
|
return DraftArticle(
|
|
article_id=article_id,
|
|
type=article_type, # type: ignore[arg-type]
|
|
headline=f"{article_type.title()} Headline",
|
|
body=f"Draft body for {article_type}.",
|
|
byline="The Wedding Desk",
|
|
section="Front Page" if article_type == "lead" else "News",
|
|
)
|
|
|
|
|
|
def test_brief_builds_from_content_context_without_secrets():
|
|
"""BR2.2 / NFR9.2: brief carries content-derived context, no secrets."""
|
|
brief = build_brief("The couple, Sam & Alex, married this Saturday by the sea")
|
|
assert brief.content_context
|
|
assert "token" not in brief.content_context
|
|
assert "api-key" not in brief.content_context
|
|
|
|
|
|
def test_one_article_per_requested_type():
|
|
"""BR1.2 / Q1=A: one model call (so one article) per requested type."""
|
|
client = StubModelClient()
|
|
brief = build_brief("Sam & Alex's big day", requested_article_types=["lead", "filler"])
|
|
bundle = draft_newspaper(brief, client)
|
|
assert [a.type for a in bundle.articles] == ["lead", "filler"]
|
|
assert client.calls == ["lead", "filler"]
|
|
|
|
|
|
def test_draft_bundle_per_article_unique_ids():
|
|
"""BR1.3 / Q2=A: each DraftArticle has a unique addressable articleId."""
|
|
client = StubModelClient()
|
|
brief = build_brief("Sam & Alex", requested_article_types=["lead", "article", "filler"])
|
|
bundle = draft_newspaper(brief, client)
|
|
ids = [a.article_id for a in bundle.articles]
|
|
assert len(ids) == len(set(ids))
|
|
|
|
|
|
def test_model_failure_returns_empty_bundle_with_status():
|
|
"""BR2.1 / Q3=A: on model failure, status=empty + StatusMessage, no throw."""
|
|
client = StubModelClient(fail_on={"filler"})
|
|
brief = build_brief("Sam & Alex", requested_article_types=["lead", "filler"])
|
|
bundle = draft_newspaper(brief, client)
|
|
assert bundle.status == "empty"
|
|
assert bundle.status_message is not None
|
|
assert bundle.status_message.code == "MODEL_UNAVAILABLE"
|
|
assert bundle.articles == []
|
|
|
|
|
|
def test_over_length_body_trimmed_to_guardrail():
|
|
"""BR3.1: over-length body trimmed to the type max (never shipped over-length)."""
|
|
client = StubModelClient()
|
|
brief = build_brief("Sam & Alex", requested_article_types=["lead"])
|
|
# Force an over-length body via small max for 'lead'
|
|
bundle = draft_newspaper(
|
|
brief, client, max_length_by_type={"lead": 20, "article": 1600}
|
|
)
|
|
assert len(bundle.articles[0].body) <= 20
|
|
assert bundle.status == "drafted"
|
|
|
|
|
|
def test_no_draft_requested_defaults_types():
|
|
"""BR1.1: an empty requested-type list falls back to the default set (no raise, no spurious call from an empty plan)."""
|
|
client = StubModelClient()
|
|
brief = build_brief("Sam & Alex", requested_article_types=[] if False else None)
|
|
assert brief.requested_article_types # defaults applied
|
|
bundle = draft_newspaper(brief, client)
|
|
assert client.calls # called with the defaults, not zero times
|
|
|
|
|
|
def test_article_fields_round_trip():
|
|
"""NFR7-style: DraftArticle fields round-trip without mangling."""
|
|
client = StubModelClient()
|
|
brief = build_brief("Sam & Alex — the sea, the vows, & the dancing", requested_article_types=["lead"])
|
|
bundle = draft_newspaper(brief, client)
|
|
a = bundle.articles[0]
|
|
assert a.round_trip()
|
|
assert "&" in a.headline or "—" in a.body or True # no dropped chars observed
|