156 lines
8.2 KiB
Python
156 lines
8.2 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"
|
|
|
|
|
|
def service(*, web_research_available: bool = False) -> VentureDiscoveryService:
|
|
return VentureDiscoveryService(ModelRouter({"sol": SolOneCompanyProvider()}), web_research_available=web_research_available)
|
|
|
|
|
|
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()
|
|
proposal = svc.generate_single_company(svc.create_v0_mandate())
|
|
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 == Decimal("10.00")
|
|
assert "5 credible target-customer responses" in decision.validation_condition
|
|
assert decision.metadata["no_actual_funding"] is True
|
|
assert "WEB_MARKET_RESEARCH" in gap.missing
|
|
assert gap.metadata["web_market_research_status"] == "MISSING"
|
|
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_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() == 13
|
|
assert graph_run.edge_traversals.count() == 13
|
|
artifact_types = set(VentureArtifact.objects.values_list("artifact_type", flat=True))
|
|
assert {"STANDARDIZED_COMPANY_PITCH", "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
|