main
py 127 lines 5.55 KB
Raw
1 """Regression tests for global presentation vs. readiness provider-mapping trust alignment.
2
3 Canonical rule:
4 globalInstrumentId -> verified provider mapping -> provider symbol
5
6 `read_global_company_state` (presentation/summary) projects through
7 `_read_company_state_impl` -> `_resolve_profile` -> `_hydrate_verified_exchange_mappings`,
8 which RETAINS previously-verified provider mappings (it only adds trusted mappings
9 from the instrument and never removes stale ones). The readiness path
10 (`register_global_profile_metadata` -> `_refresh_profile_from_global_instrument`)
11 REPLACE`S` the mapping set with verified-only entries via `_trusted_provider_mapping`.
12
13 These projections must agree, so `_read_company_state_impl` now re-aligns the global
14 (provider-less) instrument through `_refresh_profile_from_global_instrument`. Portfolio
15 positions always carry a broker `provider`, so the `if not instrument.get("provider")`
16 guard leaves the portfolio retain path -- and broker-provenance identity -- untouched.
17 """
18 import asyncio
19 from uuid import UUID, uuid4
20
21 from app.portfolio_orchestration import PortfolioResearchOrchestrator
22 from app.repository import ResearchRepository
23 from app.settings import Settings
24
25
26 def _global_instrument_payload(global_id: UUID, yahoo_status: str = "VERIFIED") -> dict:
27 """A portfolio-service global-instrument master payload (no broker `provider`)."""
28 return {
29 "globalInstrumentId": str(global_id),
30 "canonicalName": "Venus Pipes And Fittings Limited",
31 "isin": "INE000V01010",
32 "assetType": "EQUITY",
33 "country": "IN",
34 "currency": "INR",
35 "primaryExchange": "NSE",
36 "primarySymbol": "VENUSPIPES",
37 "providerMappings": [
38 {"provider": "NSE", "providerSymbol": "VENUSPIPES", "status": "VERIFIED", "exchange": "NSE"},
39 {"provider": "YAHOO_FINANCE", "providerSymbol": "VENUSPIPES.NS", "status": yahoo_status, "exchange": "NSE"},
40 ],
41 }
42
43
44 def _no_yahoo_payload(global_id: UUID) -> dict:
45 """Global instrument whose master mappings contain no Yahoo entry at all."""
46 return {
47 "globalInstrumentId": str(global_id),
48 "canonicalName": "No Yahoo Company Limited",
49 "isin": "INE000N01010",
50 "assetType": "EQUITY",
51 "country": "IN",
52 "currency": "INR",
53 "primaryExchange": "NSE",
54 "primarySymbol": "NOYAHOO",
55 "providerMappings": [
56 {"provider": "NSE", "providerSymbol": "NOYAHOO", "status": "VERIFIED", "exchange": "NSE"},
57 ],
58 }
59
60
61 class _OfflineClient:
62 """Asserts the offline global presentation path performs no portfolio-service HTTP."""
63
64 async def get(self, *args, **kwargs):
65 raise AssertionError("read_global_company_state(metadata=) must not perform HTTP")
66
67 async def post(self, *args, **kwargs):
68 raise AssertionError("read_global_company_state(metadata=) must not perform HTTP")
69
70
71 def _orchestrator(repo: ResearchRepository) -> PortfolioResearchOrchestrator:
72 settings = Settings(
73 research_demo_enabled=False,
74 structured_provider_enabled=False,
75 portfolio_service_base_url="http://portfolio-service",
76 )
77 return PortfolioResearchOrchestrator(repo, settings, client=_OfflineClient())
78
79
80 def test_global_presentation_keeps_verified_yahoo_mapping() -> None:
81 global_id = uuid4()
82 repo = ResearchRepository(settings=Settings(research_demo_enabled=False))
83 orchestrator = _orchestrator(repo)
84 payload = _global_instrument_payload(global_id, yahoo_status="VERIFIED")
85
86 company = asyncio.run(orchestrator.read_global_company_state(global_id, metadata=payload))
87 assert company.verified_provider_mappings == {
88 "NSE": "VENUSPIPES",
89 "YAHOO_FINANCE": "VENUSPIPES.NS",
90 }
91
92
93 def test_global_presentation_drops_drifted_unverified_yahoo_mapping_from_summary_and_readiness() -> None:
94 global_id = uuid4()
95 repo = ResearchRepository(settings=Settings(research_demo_enabled=False))
96 orchestrator = _orchestrator(repo)
97 verified = _global_instrument_payload(global_id, yahoo_status="VERIFIED")
98 drifted = _global_instrument_payload(global_id, yahoo_status="INVALID")
99
100 # Seed a process-local profile whose Yahoo mapping was VERIFIED.
101 assert orchestrator.register_global_profile_metadata(global_id, verified) is True
102 assert repo.profile(global_id).provider_instrument_ids == {
103 "NSE": "VENUSPIPES",
104 "YAHOO_FINANCE": "VENUSPIPES.NS",
105 }
106
107 # The instrument now reports the Yahoo mapping as INVALID. The presentation
108 # projection (read_global_company_state) must not retain the stale, now-unverified
109 # Yahoo mapping as verified -- it must agree with the readiness projection.
110 company = asyncio.run(orchestrator.read_global_company_state(global_id, metadata=drifted))
111 assert "YAHOO_FINANCE" not in company.verified_provider_mappings
112 assert company.verified_provider_mappings == {"NSE": "VENUSPIPES"}
113
114 # The readiness projection agrees after its own refresh.
115 assert orchestrator.register_global_profile_metadata(global_id, drifted) is True
116 assert "YAHOO_FINANCE" not in repo.profile(global_id).provider_instrument_ids
117
118
119 def test_global_presentation_never_guesses_a_yahoo_symbol() -> None:
120 global_id = uuid4()
121 repo = ResearchRepository(settings=Settings(research_demo_enabled=False))
122 orchestrator = _orchestrator(repo)
123 payload = _no_yahoo_payload(global_id)
124
125 company = asyncio.run(orchestrator.read_global_company_state(global_id, metadata=payload))
126 assert "YAHOO_FINANCE" not in company.verified_provider_mappings
127 assert company.verified_provider_mappings == {"NSE": "NOYAHOO"}