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 LaunchLens validation audit serving solo technical founders with pricing, competitors, customer pain, market alternatives, regulatory compliance, tools, services, and subscription cost data 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