Task 008 implement boundary questions loop
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
BACKEND_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(BACKEND_ROOT))
|
||||
|
||||
from src.application.seed_data import seed_reference_data # noqa: E402
|
||||
from src.infrastructure.repositories import open_backend_repository # noqa: E402
|
||||
from src.presentation.dependencies import get_repository # noqa: E402
|
||||
from src.presentation.main import app # noqa: E402
|
||||
|
||||
|
||||
DEMO_EDITOR_EMAIL = "editor@example.com"
|
||||
DEMO_USER_EMAIL_HEADER = "X-Demo-User-Email"
|
||||
|
||||
|
||||
class BoundaryQuestionsPublicApiTest(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.tmp_dir = tempfile.TemporaryDirectory()
|
||||
dsn = f"sqlite:///{Path(self.tmp_dir.name) / 'boundary-questions.db'}"
|
||||
self.repository = open_backend_repository(dsn)
|
||||
self.repository.setup()
|
||||
seed_reference_data(self.repository)
|
||||
app.dependency_overrides[get_repository] = lambda: self.repository
|
||||
self.client = TestClient(app)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
app.dependency_overrides.clear()
|
||||
self.tmp_dir.cleanup()
|
||||
|
||||
def test_required_unanswered_questions_block_submission_then_submit_updates_workflow(
|
||||
self,
|
||||
) -> None:
|
||||
article_id = self._create_article()
|
||||
|
||||
blocked_plan_response = self.client.post(
|
||||
f"/api/articles/{article_id}/plans/generate",
|
||||
headers={DEMO_USER_EMAIL_HEADER: DEMO_EDITOR_EMAIL},
|
||||
)
|
||||
self.assertEqual(409, blocked_plan_response.status_code, blocked_plan_response.text)
|
||||
|
||||
generate_response = self.client.post(
|
||||
f"/api/articles/{article_id}/boundary-questions/generate",
|
||||
headers={DEMO_USER_EMAIL_HEADER: DEMO_EDITOR_EMAIL},
|
||||
)
|
||||
self.assertEqual(201, generate_response.status_code, generate_response.text)
|
||||
generated = generate_response.json()
|
||||
questions = generated["questions"]
|
||||
self.assertGreaterEqual(len(questions), 5)
|
||||
self.assertLessEqual(len(questions), 10)
|
||||
self.assertEqual("BOUNDARY_QUESTIONS", generated["job"]["job_type"])
|
||||
self.assertEqual("SUCCEEDED", generated["job"]["status"])
|
||||
|
||||
categories = {question["category"] for question in questions}
|
||||
for category in (
|
||||
"audience",
|
||||
"purpose",
|
||||
"reader_outcome",
|
||||
"depth",
|
||||
"tone",
|
||||
"excluded_topics",
|
||||
"primary_keyword",
|
||||
"competitor_angle",
|
||||
"evidence_standard",
|
||||
"visual_expectations",
|
||||
):
|
||||
self.assertIn(category, categories)
|
||||
|
||||
required_question = next(
|
||||
question for question in questions if question["is_required"]
|
||||
)
|
||||
optional_question = next(
|
||||
question for question in questions if not question["is_required"]
|
||||
)
|
||||
|
||||
patch_response = self.client.patch(
|
||||
f"/api/articles/{article_id}/boundary-questions/{required_question['id']}",
|
||||
headers={DEMO_USER_EMAIL_HEADER: DEMO_EDITOR_EMAIL},
|
||||
json={"answer": "Senior editors evaluating automated SEO content."},
|
||||
)
|
||||
self.assertEqual(200, patch_response.status_code, patch_response.text)
|
||||
self.assertEqual(
|
||||
"Senior editors evaluating automated SEO content.",
|
||||
patch_response.json()["question"]["answer"],
|
||||
)
|
||||
|
||||
optional_patch_response = self.client.patch(
|
||||
f"/api/articles/{article_id}/boundary-questions/{optional_question['id']}",
|
||||
headers={DEMO_USER_EMAIL_HEADER: DEMO_EDITOR_EMAIL},
|
||||
json={"answer": "No custom visual constraints."},
|
||||
)
|
||||
self.assertEqual(200, optional_patch_response.status_code, optional_patch_response.text)
|
||||
|
||||
submit_blocked_response = self.client.post(
|
||||
f"/api/articles/{article_id}/boundary-questions/submit",
|
||||
headers={DEMO_USER_EMAIL_HEADER: DEMO_EDITOR_EMAIL},
|
||||
)
|
||||
self.assertEqual(409, submit_blocked_response.status_code, submit_blocked_response.text)
|
||||
self.assertIn("required", submit_blocked_response.json()["detail"])
|
||||
|
||||
list_response = self.client.get(
|
||||
f"/api/articles/{article_id}/boundary-questions",
|
||||
headers={DEMO_USER_EMAIL_HEADER: DEMO_EDITOR_EMAIL},
|
||||
)
|
||||
self.assertEqual(200, list_response.status_code, list_response.text)
|
||||
saved_required = self._find_question(
|
||||
list_response.json()["questions"],
|
||||
required_question["id"],
|
||||
)
|
||||
self.assertEqual(
|
||||
"Senior editors evaluating automated SEO content.",
|
||||
saved_required["answer"],
|
||||
)
|
||||
|
||||
for question in list_response.json()["questions"]:
|
||||
if question["is_required"] and not question["answer"]:
|
||||
answer_response = self.client.patch(
|
||||
f"/api/articles/{article_id}/boundary-questions/{question['id']}",
|
||||
headers={DEMO_USER_EMAIL_HEADER: DEMO_EDITOR_EMAIL},
|
||||
json={"answer": f"Accepted answer for {question['category']}."},
|
||||
)
|
||||
self.assertEqual(200, answer_response.status_code, answer_response.text)
|
||||
|
||||
submit_response = self.client.post(
|
||||
f"/api/articles/{article_id}/boundary-questions/submit",
|
||||
headers={DEMO_USER_EMAIL_HEADER: DEMO_EDITOR_EMAIL},
|
||||
)
|
||||
self.assertEqual(200, submit_response.status_code, submit_response.text)
|
||||
self.assertEqual(
|
||||
"BOUNDARY_ANSWERS_SUBMITTED",
|
||||
submit_response.json()["article"]["status"],
|
||||
)
|
||||
|
||||
detail_response = self.client.get(
|
||||
f"/api/articles/{article_id}",
|
||||
headers={DEMO_USER_EMAIL_HEADER: DEMO_EDITOR_EMAIL},
|
||||
)
|
||||
self.assertEqual(200, detail_response.status_code, detail_response.text)
|
||||
detail = detail_response.json()
|
||||
self.assertEqual("BOUNDARY_ANSWERS_SUBMITTED", detail["article"]["status"])
|
||||
self.assertTrue(
|
||||
any(
|
||||
event["event_type"] == "BOUNDARY_ANSWERS_SUBMITTED"
|
||||
for event in detail["workflow_events"]
|
||||
)
|
||||
)
|
||||
|
||||
allowed_plan_response = self.client.post(
|
||||
f"/api/articles/{article_id}/plans/generate",
|
||||
headers={DEMO_USER_EMAIL_HEADER: DEMO_EDITOR_EMAIL},
|
||||
)
|
||||
self.assertEqual(202, allowed_plan_response.status_code, allowed_plan_response.text)
|
||||
|
||||
def _create_article(self) -> str:
|
||||
site_response = self.client.get(
|
||||
"/api/sites",
|
||||
headers={DEMO_USER_EMAIL_HEADER: DEMO_EDITOR_EMAIL},
|
||||
)
|
||||
self.assertEqual(200, site_response.status_code, site_response.text)
|
||||
target_site = site_response.json()[0]["site"]
|
||||
|
||||
response = self.client.post(
|
||||
"/api/articles",
|
||||
headers={DEMO_USER_EMAIL_HEADER: DEMO_EDITOR_EMAIL},
|
||||
json={
|
||||
"target_site_id": target_site["id"],
|
||||
"brief_description": "Build a precise comparison of AI content systems.",
|
||||
"working_title": "AI Content Systems",
|
||||
"content_type": "longform_guide",
|
||||
"primary_keyword": "AI content systems",
|
||||
},
|
||||
)
|
||||
self.assertEqual(201, response.status_code, response.text)
|
||||
return response.json()["article"]["id"]
|
||||
|
||||
def _find_question(
|
||||
self,
|
||||
questions: list[dict[str, Any]],
|
||||
question_id: str,
|
||||
) -> dict[str, Any]:
|
||||
for question in questions:
|
||||
if question["id"] == question_id:
|
||||
return question
|
||||
self.fail(f"Question not found: {question_id}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -87,6 +87,10 @@ class SchemaStorageContractsIntegrationTest(unittest.TestCase):
|
||||
"output_files",
|
||||
}.issubset(repository.schema.list_columns("agent_jobs"))
|
||||
)
|
||||
self.assertIn(
|
||||
"category",
|
||||
repository.schema.list_columns("boundary_questions"),
|
||||
)
|
||||
self.assertTrue(
|
||||
{
|
||||
"s3_prefix",
|
||||
|
||||
Reference in New Issue
Block a user