Skip to content

Commit 53ef9b1

Browse files
committed
Add budget-window semi-integration seam test
1 parent 165c99c commit 53ef9b1

1 file changed

Lines changed: 241 additions & 0 deletions

File tree

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
"""Semi-integration coverage for budget-window gateway and worker seams."""
2+
3+
from __future__ import annotations
4+
5+
from dataclasses import dataclass, field
6+
7+
import pytest
8+
from fastapi.testclient import TestClient
9+
10+
import src.modal.budget_window_batch as batch_module
11+
import src.modal.budget_window_scheduler as scheduler_module
12+
import src.modal.budget_window_state as state_module
13+
from fixtures.gateway.shared import create_gateway_app
14+
from src.modal.gateway import endpoints
15+
16+
17+
@dataclass
18+
class SemiIntegrationRuntime:
19+
dicts: dict[str, dict] = field(default_factory=dict)
20+
calls: dict[str, object] = field(default_factory=dict)
21+
child_payloads: list[dict] = field(default_factory=list)
22+
current_parent_call_id: str | None = None
23+
next_parent_call_id: str = "parent-batch-123"
24+
active_child_calls: set[str] = field(default_factory=set)
25+
max_active_child_calls: int = 0
26+
27+
def child_result_for_year(self, simulation_year: str) -> dict:
28+
offset = int(simulation_year) - 2025
29+
return {
30+
"budget": {
31+
"tax_revenue_impact": offset * 100,
32+
"state_tax_revenue_impact": offset * 10,
33+
"benefit_spending_impact": offset + 4,
34+
"budgetary_impact": offset * 100 - (offset + 4),
35+
}
36+
}
37+
38+
def child_started(self, object_id: str) -> None:
39+
self.active_child_calls.add(object_id)
40+
self.max_active_child_calls = max(
41+
self.max_active_child_calls,
42+
len(self.active_child_calls),
43+
)
44+
45+
def child_finished(self, object_id: str) -> None:
46+
self.active_child_calls.discard(object_id)
47+
48+
49+
class MockDict:
50+
def __init__(self, data: dict):
51+
self._data = data
52+
53+
def __getitem__(self, key: str):
54+
return self._data[key]
55+
56+
def __setitem__(self, key: str, value):
57+
self._data[key] = value
58+
59+
def get(self, key: str, default=None):
60+
return self._data.get(key, default)
61+
62+
63+
class MockChildCall:
64+
def __init__(
65+
self, runtime: SemiIntegrationRuntime, *, object_id: str, result: dict
66+
):
67+
self.runtime = runtime
68+
self.object_id = object_id
69+
self.result = result
70+
self.runtime.child_started(object_id)
71+
72+
def get(self, timeout: int = 0):
73+
self.runtime.child_finished(self.object_id)
74+
return self.result
75+
76+
77+
class MockParentBatchCall:
78+
def __init__(self, runtime: SemiIntegrationRuntime, *, payload: dict):
79+
self.runtime = runtime
80+
self.object_id = runtime.next_parent_call_id
81+
self.payload = payload
82+
self._polls = 0
83+
self._result = None
84+
85+
def get(self, timeout: int = 0):
86+
if self._result is not None:
87+
return self._result
88+
if self._polls == 0:
89+
self._polls += 1
90+
raise TimeoutError()
91+
92+
previous = self.runtime.current_parent_call_id
93+
self.runtime.current_parent_call_id = self.object_id
94+
try:
95+
self._result = batch_module.run_budget_window_batch_impl(self.payload)
96+
finally:
97+
self.runtime.current_parent_call_id = previous
98+
return self._result
99+
100+
101+
class MockFunction:
102+
def __init__(
103+
self, runtime: SemiIntegrationRuntime, *, app_name: str, func_name: str
104+
):
105+
self.runtime = runtime
106+
self.app_name = app_name
107+
self.func_name = func_name
108+
109+
def spawn(self, payload: dict):
110+
if self.func_name == "run_budget_window_batch":
111+
call = MockParentBatchCall(self.runtime, payload=payload)
112+
self.runtime.calls[call.object_id] = call
113+
return call
114+
115+
simulation_year = payload["time_period"]
116+
self.runtime.child_payloads.append(payload)
117+
call = MockChildCall(
118+
self.runtime,
119+
object_id=f"child-{simulation_year}",
120+
result=self.runtime.child_result_for_year(simulation_year),
121+
)
122+
self.runtime.calls[call.object_id] = call
123+
return call
124+
125+
126+
@pytest.fixture
127+
def budget_window_semi_integration_client(
128+
monkeypatch,
129+
) -> tuple[TestClient, SemiIntegrationRuntime]:
130+
runtime = SemiIntegrationRuntime()
131+
runtime.dicts["simulation-api-us-versions"] = {
132+
"latest": "1.500.0",
133+
"1.500.0": "policyengine-simulation-us1-500-0-uk2-66-0",
134+
}
135+
136+
class MockModalDict:
137+
@staticmethod
138+
def from_name(name: str, create_if_missing: bool = False):
139+
if create_if_missing and name not in runtime.dicts:
140+
runtime.dicts[name] = {}
141+
if name not in runtime.dicts:
142+
raise KeyError(name)
143+
return MockDict(runtime.dicts[name])
144+
145+
class MockModalFunction:
146+
@staticmethod
147+
def from_name(app_name: str, func_name: str):
148+
return MockFunction(runtime, app_name=app_name, func_name=func_name)
149+
150+
class MockModalFunctionCall:
151+
@classmethod
152+
def from_id(cls, object_id: str):
153+
return runtime.calls[object_id]
154+
155+
class MockModal:
156+
Dict = MockModalDict
157+
Function = MockModalFunction
158+
FunctionCall = MockModalFunctionCall
159+
160+
@staticmethod
161+
def current_function_call_id():
162+
if runtime.current_parent_call_id is None:
163+
raise RuntimeError("No active parent batch call")
164+
return runtime.current_parent_call_id
165+
166+
monkeypatch.setattr(endpoints, "modal", MockModal)
167+
monkeypatch.setattr(state_module, "modal", MockModal)
168+
monkeypatch.setattr(scheduler_module, "modal", MockModal)
169+
monkeypatch.setattr(batch_module, "modal", MockModal)
170+
monkeypatch.setattr(scheduler_module.time, "sleep", lambda _: None)
171+
172+
return TestClient(create_gateway_app()), runtime
173+
174+
175+
def test_budget_window_submit_and_poll_exercise_gateway_worker_seams(
176+
budget_window_semi_integration_client,
177+
):
178+
client, runtime = budget_window_semi_integration_client
179+
180+
submit_response = client.post(
181+
"/simulate/economy/budget-window",
182+
json={
183+
"country": "us",
184+
"region": "us",
185+
"scope": "macro",
186+
"reform": {},
187+
"start_year": "2026",
188+
"window_size": 3,
189+
"max_parallel": 2,
190+
"_telemetry": {
191+
"run_id": "batch-run-123",
192+
"process_id": "proc-123",
193+
"capture_mode": "disabled",
194+
},
195+
},
196+
)
197+
198+
assert submit_response.status_code == 200
199+
assert submit_response.json()["batch_job_id"] == "parent-batch-123"
200+
201+
first_poll = client.get("/budget-window-jobs/parent-batch-123")
202+
assert first_poll.status_code == 202
203+
assert first_poll.json()["status"] == "submitted"
204+
assert first_poll.json()["queued_years"] == ["2026", "2027", "2028"]
205+
206+
second_poll = client.get("/budget-window-jobs/parent-batch-123")
207+
assert second_poll.status_code == 200
208+
body = second_poll.json()
209+
210+
assert body["status"] == "complete"
211+
assert body["progress"] == 100
212+
assert body["completed_years"] == ["2026", "2027", "2028"]
213+
assert body["result"]["kind"] == "budgetWindow"
214+
assert body["result"]["startYear"] == "2026"
215+
assert body["result"]["endYear"] == "2028"
216+
assert [row["year"] for row in body["result"]["annualImpacts"]] == [
217+
"2026",
218+
"2027",
219+
"2028",
220+
]
221+
assert body["result"]["totals"] == {
222+
"year": "Total",
223+
"taxRevenueImpact": 600.0,
224+
"federalTaxRevenueImpact": 540.0,
225+
"stateTaxRevenueImpact": 60.0,
226+
"benefitSpendingImpact": 18.0,
227+
"budgetaryImpact": 582.0,
228+
}
229+
230+
assert runtime.max_active_child_calls == 2
231+
assert len(runtime.child_payloads) == 3
232+
assert [payload["time_period"] for payload in runtime.child_payloads] == [
233+
"2026",
234+
"2027",
235+
"2028",
236+
]
237+
assert all("target" not in payload for payload in runtime.child_payloads)
238+
assert all("start_year" not in payload for payload in runtime.child_payloads)
239+
assert all("window_size" not in payload for payload in runtime.child_payloads)
240+
assert all("max_parallel" not in payload for payload in runtime.child_payloads)
241+
assert all("_metadata" not in payload for payload in runtime.child_payloads)

0 commit comments

Comments
 (0)