Artifex/tests/test_venture_discovery_v0.py
2026-08-16 15:35:55 +07:00

265 lines
14 KiB
Python

from __future__ import annotations
import json
from decimal import Decimal
from agents.venture_discovery import PITCH_SECTIONS, SCORE_DIMENSIONS, VentureDiscoveryService
from control_plane.ventures.models import CapabilityPriority, CapabilityStatus, CompanyProposal, CompanyProposalStatus, ICDecisionType, VentureArtifact
from graph.bootstrap import champion_venture_discovery_graph_v1
from graph.langgraph_runtime import LangGraphRuntime
from graph.models import GraphRun, GraphRunStatus
from graph.venture_discovery import venture_discovery_registry
from model_router.router import ModelProvider, ModelRequestContract, ModelResponseContract, ModelRouter
class SolOneCompanyProvider(ModelProvider):
provider_name = "sol-test"
def complete(self, request: ModelRequestContract) -> ModelResponseContract:
return ModelResponseContract(
model="sol",
content=json.dumps(
{
"title": "LaunchLens",
"one_line_thesis": "A paid validation audit helps solo technical founders avoid wasting weeks on unvalidated AI microbusinesses.",
"description": "A fixed-scope validation and launch-readiness report for one startup idea.",
"problem": "Builders overbuild before proving demand.",
"target_customer": "Solo technical founders and small service operators considering an AI-assisted microbusiness.",
"proposed_solution": "A productized audit covering ICP, first-dollar path, validation gates, build plan, risks, and capability gaps.",
"business_model": "Productized service first, optional software later.",
"pricing_hypothesis": "$49-$99 per audit.",
"acquisition_strategy": "Compliant founder community posts and personal network conversations after approval.",
"validation_plan": "Collect 5 credible target-customer responses or 1 willingness-to-pay signal before build/spend.",
"capital_requested": "50",
"time_to_first_dollar_estimate": "3-10 days after outreach approval",
"expected_margin": "70-85% gross margin",
"build_complexity": "LOW",
"market_evidence": [{"type": "reasoning", "source": "sol", "summary": "Service-led validation minimizes build risk."}],
"differentiation": "IC-style diligence plus Artifex execution/capability-gap awareness.",
"major_risks": ["Demand unproven", "Distribution may fail", "Generic consulting competition"],
"confidence": 0.66,
}
),
metadata={"usage": {"prompt_tokens": 1, "completion_tokens": 1}},
)
def health(self) -> str:
return "AVAILABLE"
class ResearchProvider(ModelProvider):
provider_name = "luna-test"
def __init__(self, complete: bool = True) -> None:
self.complete_coverage = complete
def complete(self, request: ModelRequestContract) -> ModelResponseContract:
categories = ["competitors", "pricing", "customer_pain", "market_alternatives", "regulatory_platform_risks"] if self.complete_coverage else ["competitors"]
return ModelResponseContract(
model="luna",
content=json.dumps(
{
"sources": [{"url": f"https://example.com/{category}", "title": category.replace("_", " ").title(), "category": category, "summary": f"Source-linked evidence for {category}."} for category in categories],
"findings": {category: f"Finding for {category}." for category in categories},
"coverage": {category: category in categories for category in ["competitors", "pricing", "customer_pain", "market_alternatives", "regulatory_platform_risks"]},
}
),
metadata={},
)
def health(self) -> str:
return "AVAILABLE"
class FakeSearchClient:
def search(self, query: str, *, category: str = "general", limit: int = 5) -> list[dict[str, object]]:
return [
{
"type": "public_web",
"source": "searxng",
"url": f"https://search.example/{category}",
"title": f"{category} source",
"category": category,
"summary": f"Search result for {query}",
"fallback_evidence": False,
}
]
class FakePageFetcher:
def fetch_many(self, sources: list[dict[str, object]], *, max_pages: int = 8) -> list[dict[str, object]]:
return [{"url": source["url"], "category": source["category"], "source_title": source["title"], "text": f"Fetched page text for {source['category']} with pricing, alternatives, and customer pain evidence.", "fetched_chars": 80} for source in sources[:max_pages]]
def service(*, web_research_available: bool = False, complete_research: bool = True) -> VentureDiscoveryService:
return VentureDiscoveryService(ModelRouter({"sol": SolOneCompanyProvider(), "luna": ResearchProvider(complete_research)}), web_research_available=web_research_available)
def test_venture_model_policy_overrides_ideation_and_research(monkeypatch) -> None:
monkeypatch.setenv("ARTIFEX_VENTURE_IDEATION_MODEL", "terra")
monkeypatch.setenv("ARTIFEX_VENTURE_RESEARCH_MODEL", "qwen")
router = ModelRouter({"terra": SolOneCompanyProvider(), "qwen": ResearchProvider()})
svc = VentureDiscoveryService(router, web_research_available=True)
proposal = svc.generate_single_company(svc.create_v0_mandate())
research = svc.conduct_market_research(proposal)
assert proposal.metadata["generation_source"] == "terra"
assert proposal.metadata["fallback_evidence"] is False
assert research["provider"] == "qwen"
def test_company_proposal_lifecycle_mandate_limits_and_pitch_schema() -> None:
svc = service()
mandate = svc.create_v0_mandate()
proposal = svc.generate_single_company(mandate)
assert mandate.max_validation_capital == Decimal("50")
assert mandate.target_net_new_cash == Decimal("500")
assert mandate.target_window_days == 30
assert mandate.constraints["no_real_spend_in_v0"] is True
assert mandate.constraints["no_real_customer_outreach_in_v0"] is True
assert CompanyProposal.objects.count() == 1
assert proposal.status == CompanyProposalStatus.SUBMITTED
assert proposal.capital_requested <= Decimal("50")
assert all(section in proposal.pitch for section in PITCH_SECTIONS)
assert proposal.metadata["generation_source"] == "sol"
assert proposal.metadata["real_spend"] == 0
assert proposal.metadata["real_customer_outreach"] is False
def test_board_review_ic_questions_responses_and_bounded_diligence() -> None:
svc = service()
proposal = svc.generate_single_company(svc.create_v0_mandate())
board = svc.board_review(proposal)
diligence = svc.start_ic_diligence(proposal)
questions = svc.generate_ic_questions(diligence)
responses = svc.answer_questions(diligence)
challenge = svc.red_team(diligence)
final = svc.final_company_response(diligence)
assert set(board.observations) == {"CEO", "CTO", "CFO", "CRO", "Independent Director"}
assert board.metadata["company_does_not_grade_itself"] is True
assert diligence.rounds == ["Initial Pitch", "Diligence Round 1", "Final Challenge", "Decision"]
assert diligence.metadata["bounded_rounds"] is True
assert len(questions) == 8
assert all(q.evidence_required for q in questions)
assert len(responses) == len(questions)
assert all(r.metadata["no_customer_outreach"] and r.metadata["no_spend"] for r in responses)
assert challenge["recommendation"] == "continue_to_final_response"
assert final["kill_criteria"]
def test_ic_scoring_decision_capability_gap_and_memo_artifact() -> None:
svc = service(web_research_available=True)
proposal = svc.generate_single_company(svc.create_v0_mandate())
research = svc.conduct_market_research(proposal)
svc.board_review(proposal)
diligence = svc.start_ic_diligence(proposal)
svc.generate_ic_questions(diligence)
svc.answer_questions(diligence)
svc.red_team(diligence)
svc.final_company_response(diligence)
decision = svc.score_and_decide(diligence)
gap = svc.capability_analysis(proposal)
memo = svc.produce_investment_memo(diligence, gap)
assert decision.decision in ICDecisionType.values
assert set(SCORE_DIMENSIONS) == set(decision.component_scores)
assert decision.decision == ICDecisionType.CONDITIONAL_FUND
assert decision.initial_tranche in {Decimal("10.00"), Decimal("20.00")}
assert "5 credible target-customer responses" in decision.validation_condition
assert decision.metadata["no_actual_funding"] is True
assert len(research["sources"]) == 5
assert "WEB_MARKET_RESEARCH" in gap.available
assert gap.metadata["web_market_research_status"] == "AVAILABLE"
assert {item["priority"] for item in gap.ranked_missing}.issuperset({CapabilityPriority.BEFORE_VALIDATION, CapabilityPriority.BEFORE_FIRST_CUSTOMER})
assert memo.artifact_type == "FINAL_INVESTMENT_MEMO"
assert memo.content["Company"] == proposal.title
assert "Capability gap" in memo.content
def test_searxng_search_and_page_fetch_sources_can_supply_market_research_evidence() -> None:
svc = VentureDiscoveryService(ModelRouter({}), web_research_available=True, search_client=FakeSearchClient(), page_fetcher=FakePageFetcher())
proposal = svc.generate_single_company(svc.create_v0_mandate())
research = svc.conduct_market_research(proposal)
assert research["search_provider"] == "searxng"
assert len(research["sources"]) == 5
assert research["page_fetch_count"] == 5
assert "Fetched page text" in research["page_corpus"][0]["text"]
assert proposal.metadata["research"]["page_fetch_count"] == 5
assert all(research["coverage"].values())
def test_fallback_is_marked_and_reduces_confidence_when_research_unavailable() -> None:
svc = VentureDiscoveryService(web_research_available=False)
proposal = svc.generate_single_company(svc.create_v0_mandate())
gap = svc.capability_analysis(proposal)
assert proposal.metadata["generation_source"] == "deterministic_fallback"
assert proposal.metadata["fallback_evidence"] is True
assert proposal.confidence <= 0.58
assert any(item.get("fallback_evidence") for item in proposal.market_evidence)
assert "WEB_MARKET_RESEARCH" in gap.missing
def test_venture_discovery_v1_graph_lineage_and_no_automatic_execution() -> None:
version = champion_venture_discovery_graph_v1()
graph_run = GraphRun.objects.create(execution_graph_version=version, current_node=version.graph_spec["entry"])
LangGraphRuntime(venture_discovery_registry(service())).run_until_terminal_or_paused(graph_run)
graph_run.refresh_from_db()
assert graph_run.status == GraphRunStatus.COMPLETE
assert graph_run.execution_graph_version.graph.name == "venture_discovery"
assert graph_run.execution_graph_version.version == 1
assert CompanyProposal.objects.count() == 1
proposal = CompanyProposal.objects.get()
assert proposal.metadata["real_spend"] == 0
assert proposal.metadata["real_customer_outreach"] is False
assert graph_run.node_runs.count() == 14
assert graph_run.edge_traversals.count() == 14
artifact_types = set(VentureArtifact.objects.values_list("artifact_type", flat=True))
assert {"STANDARDIZED_COMPANY_PITCH", "MARKET_RESEARCH", "COMPANY_BOARD_REVIEW", "IC_QUESTIONS", "IC_RESPONSES", "IC_RED_TEAM", "IC_FINAL_RESPONSE", "IC_FINAL_SCORE", "CAPABILITY_GAP_REPORT", "FINAL_INVESTMENT_MEMO"}.issubset(artifact_types)
decision = proposal.ic_diligence.get().decision
svc = service()
svc.request_human_approval(decision, "approve_for_validation")
decision.refresh_from_db()
assert decision.metadata["real_spend_still_blocked"] is True
def test_materially_different_pitches_receive_different_scores_and_tranches() -> None:
strong = service(web_research_available=True, complete_research=True)
strong_proposal = strong.generate_single_company(strong.create_v0_mandate())
strong.conduct_market_research(strong_proposal)
strong_diligence = strong.start_ic_diligence(strong_proposal)
strong.generate_ic_questions(strong_diligence)
strong.answer_questions(strong_diligence)
strong.red_team(strong_diligence)
strong.final_company_response(strong_diligence)
strong_decision = strong.score_and_decide(strong_diligence)
weak = service(web_research_available=True, complete_research=False)
weak_proposal = weak.generate_single_company(weak.create_v0_mandate())
weak_proposal.title = "Enterprise RegTech Platform"
weak_proposal.business_model = "SaaS platform requiring integrations and long enterprise sales cycles."
weak_proposal.build_complexity = "HIGH"
weak_proposal.expected_margin = "35-45% gross margin before support burden"
weak_proposal.validation_plan = "Build a prototype and then seek feedback."
weak_proposal.pitch = {**weak_proposal.pitch, "Company name": weak_proposal.title, "Business model": weak_proposal.business_model, "Validation plan": weak_proposal.validation_plan}
weak_proposal.save(update_fields=["title", "business_model", "build_complexity", "expected_margin", "validation_plan", "pitch", "updated_at"])
weak.conduct_market_research(weak_proposal)
weak_diligence = weak.start_ic_diligence(weak_proposal)
weak.generate_ic_questions(weak_diligence)
weak.answer_questions(weak_diligence)
weak.red_team(weak_diligence)
weak.final_company_response(weak_diligence)
weak_decision = weak.score_and_decide(weak_diligence)
assert strong_decision.component_scores["Demand Evidence"] > weak_decision.component_scores["Demand Evidence"]
assert strong_decision.component_scores["Build Simplicity"] > weak_decision.component_scores["Build Simplicity"]
assert strong_decision.composite_score - weak_decision.composite_score >= 10
assert strong_decision.probability_500_within_30_days > weak_decision.probability_500_within_30_days
assert strong_decision.initial_tranche != weak_decision.initial_tranche