B2D-agentic-ai / tests /test_discovery.py
AMRYB's picture
Upload 91 files
287f3d3 verified
Raw
History Blame Contribute Delete
8.05 kB
"""Discovery agent / conversation loop tests."""
from __future__ import annotations
import json
import pytest
from agentic_core.orchestrator import OrchestrationError
from agentic_core.schemas import DiscoveryOutput, DiscoveryQuestion
from tests.helpers import discovery_output, build_handler
async def test_vague_idea_asks_for_clarification(provider, make_orchestrator, make_context):
provider.set_handler(build_handler(discovery_status="needs_clarification"))
orchestrator = make_orchestrator()
context = make_context("I want to build a booking platform for football fields.")
output = await orchestrator.discovery_turn(context, context.business_idea)
assert output.status == "needs_clarification"
assert output.questions
assert context.status == "discovery"
assert len(context.transcript) == 2 # user + agent
assert context.transcript[0].role == "user"
assert context.transcript[1].role == "agent"
async def test_complete_idea_reaches_ready(provider, make_orchestrator, make_context):
provider.set_handler(build_handler(discovery_status="ready"))
orchestrator = make_orchestrator()
context = make_context("Food delivery platform with online payment.")
output = await orchestrator.discovery_turn(context, context.business_idea)
assert output.status == "ready"
assert output.questions == []
assert context.status == "ready_for_confirmation"
assert context.target_users == ["Customers", "Restaurants"]
assert context.user_roles == ["Customer", "Restaurant Owner", "Admin"]
async def test_confirmation_requires_ready(provider, make_orchestrator, make_context):
provider.set_handler(build_handler(discovery_status="needs_clarification"))
orchestrator = make_orchestrator()
context = make_context("Something vague.")
await orchestrator.discovery_turn(context, context.business_idea)
with pytest.raises(OrchestrationError):
orchestrator.confirm(context)
provider.set_handler(build_handler(discovery_status="ready"))
context2 = make_context("A clear, complete idea.")
await orchestrator.discovery_turn(context2, context2.business_idea)
orchestrator.confirm(context2)
assert context2.status == "confirmed"
async def test_agent_never_loses_conversation_history(provider, make_orchestrator, make_context):
"""The discovery prompt must include prior turns so the agent never re-asks."""
responses = [
json.dumps(discovery_output("needs_clarification")),
json.dumps(discovery_output("ready")),
]
provider.set_responses(responses)
orchestrator = make_orchestrator()
context = make_context("Booking platform.")
await orchestrator.discovery_turn(context, context.business_idea)
await orchestrator.discovery_turn(context, "Players, field owners and admins.")
second_call_user = provider.calls[1][1]
assert "Booking platform." in second_call_user
assert "Players, field owners and admins." in second_call_user
assert context.transcript[-1].role == "agent"
assert context.status == "ready_for_confirmation"
async def test_contradictory_information_last_answer_wins(provider, make_orchestrator, make_context):
provider.set_handler(
lambda _s, _u: json.dumps(
discovery_output("needs_clarification", ) | {"known_information": {"target_users": ["Players"]}}
)
)
orchestrator = make_orchestrator()
context = make_context("Football field booking.")
await orchestrator.discovery_turn(context, context.business_idea)
assert context.target_users == ["Players"]
provider.set_handler(
lambda _s, _u: json.dumps(
discovery_output("ready") | {"known_information": {"target_users": ["Owners"]}}
)
)
await orchestrator.discovery_turn(context, "Actually owners only.")
assert context.target_users == ["Owners"]
async def test_unnecessary_information_not_forced(provider, make_orchestrator, make_context):
"""not_applicable fields produce no questions."""
payload = discovery_output("needs_clarification")
payload["missing_information"] = [
{"field": "payments", "importance": "not_applicable", "reason": "No payments in this project."}
]
payload["questions"] = []
provider.set_handler(lambda _s, _u: json.dumps(payload))
orchestrator = make_orchestrator()
context = make_context("Internal tool, no payments.")
output = await orchestrator.discovery_turn(context, context.business_idea)
assert output.status == "needs_clarification"
assert output.missing_information[0].importance == "not_applicable"
async def test_known_information_application_is_idempotent(provider, make_orchestrator, make_context):
def handler(_s, _u):
return json.dumps(
discovery_output("ready") | {"known_information": {"core_features": ["Booking", "Payments"]}}
)
provider.set_handler(handler)
orchestrator = make_orchestrator()
context = make_context("Football booking.")
context.core_features = ["Booking", "Payments"]
await orchestrator.discovery_turn(context, context.business_idea)
assert context.core_features == ["Booking", "Payments"]
async def test_discovery_questions_support_options(provider, make_orchestrator, make_context):
"""Questions can carry multiple-choice options; users pick one or type their own."""
payload = discovery_output("needs_clarification")
payload["questions"] = [
{
"id": "q1",
"question": "Which client should v1 ship on?",
"reason": "Defines the build target.",
"options": ["Web app only", "Mobile app only", "Both web and mobile"],
}
]
provider.set_handler(lambda _s, _u: json.dumps(payload))
orchestrator = make_orchestrator()
context = make_context("Booking platform.")
output = await orchestrator.discovery_turn(context, context.business_idea)
question = output.questions[0]
assert isinstance(question, DiscoveryQuestion)
assert question.options == ["Web app only", "Mobile app only", "Both web and mobile"]
assert output.status == "needs_clarification"
async def test_discovery_failure_raises(provider, make_orchestrator, make_context):
async def boom(_s, _u):
raise RuntimeError("provider down")
provider.set_handler(boom)
orchestrator = make_orchestrator()
context = make_context("Football booking.")
with pytest.raises(Exception):
await orchestrator.discovery_turn(context, context.business_idea)
async def test_discovery_retries_transient_failure(provider, make_orchestrator, make_context):
"""A single unparseable reply must not kill discovery; it retries once."""
calls = {"n": 0}
def flaky(_s, _u):
calls["n"] += 1
if calls["n"] <= 2: # first run: generate + repair both fail
return "sorry, no json here"
return json.dumps(discovery_output("ready"))
provider.set_handler(flaky)
orchestrator = make_orchestrator()
context = make_context("Football booking.")
output = await orchestrator.discovery_turn(context, context.business_idea)
assert calls["n"] == 3 # 2 (failed run) + 1 (successful retry run)
assert output.status == "ready"
assert context.status == "ready_for_confirmation"
async def test_discovery_fails_after_two_bad_replies(provider, make_orchestrator, make_context):
calls = {"n": 0}
def always_bad(_s, _u):
calls["n"] += 1
return "still not json"
provider.set_handler(always_bad)
orchestrator = make_orchestrator()
context = make_context("Football booking.")
with pytest.raises(Exception):
await orchestrator.discovery_turn(context, context.business_idea)
assert calls["n"] == 4 # two runs, each generate + repair