2828import sys
2929import tempfile
3030import threading
31+ from collections .abc import Iterator
32+ from contextlib import contextmanager
3133from http .server import BaseHTTPRequestHandler , HTTPServer
34+ from typing import Any
3235
3336import yaml
3437
3538REPO_ROOT = os .path .dirname (os .path .dirname (os .path .abspath (__file__ )))
3639sys .path .insert (0 , os .path .join (REPO_ROOT , "src" ))
3740sys .path .insert (0 , REPO_ROOT )
3841
39- from ogx .core .library_client import OGXAsLibraryClient # noqa: E402
40- from ogx .core .stack import get_stack_run_config_from_distro # noqa: E402
41- from ogx .core .testing_context import set_test_context # noqa: E402
42- from tests .integration .inference .store_disabled_constants import ( # noqa: E402
42+ from ogx .core .library_client import OGXAsLibraryClient # noqa: E402 # Requires local checkout path setup above.
43+ from ogx .core .testing_context import set_test_context # noqa: E402 # Requires local checkout path setup above.
44+ from tests .integration .inference .store_disabled_support import ( # noqa: E402 # Requires path setup above.
4345 NON_STREAMING_PROMPT ,
4446 RECORDING_TEST_IDS ,
4547 STREAMING_PROMPT ,
4648 TEXT_MODEL ,
49+ build_inference_store_disabled_run_config ,
4750)
4851
4952RECORDINGS_DIR = os .path .join (REPO_ROOT , "tests" , "integration" , "inference" , "recordings" )
5558MOCK_PORT = 0 # ephemeral
5659
5760
58- def _completion_body (model : str , prompt : str , completion_id : str ) -> dict :
61+ def _completion_body (model : str , prompt : str , completion_id : str ) -> dict [ str , Any ] :
5962 return {
6063 "id" : completion_id ,
6164 "object" : "chat.completion" ,
@@ -72,7 +75,7 @@ def _completion_body(model: str, prompt: str, completion_id: str) -> dict:
7275 }
7376
7477
75- def _stream_chunks (model : str , completion_id : str ):
78+ def _stream_chunks (model : str , completion_id : str ) -> list [ dict [ str , Any ]] :
7679 return [
7780 {
7881 "id" : completion_id ,
@@ -105,7 +108,7 @@ def _stream_chunks(model: str, completion_id: str):
105108
106109
107110class MockHandler (BaseHTTPRequestHandler ):
108- def do_GET (self ): # noqa: N802 Function name `do_GET` should be lowercase
111+ def do_GET (self ) -> None : # noqa: N802 Function name `do_GET` should be lowercase
109112 if self .path .endswith ("/models" ):
110113 body = json .dumps ({"object" : "list" , "data" : [{"id" : "gpt-4o" , "object" : "model" }]}).encode ()
111114 self .send_response (200 )
@@ -117,13 +120,14 @@ def do_GET(self): # noqa: N802 Function name `do_GET` should be lowercase
117120 self .send_response (404 )
118121 self .end_headers ()
119122
120- def do_POST (self ): # noqa: N802 Function name `do_POST` should be lowercase
123+ def do_POST (self ) -> None : # noqa: N802 Function name `do_POST` should be lowercase
121124 length = int (self .headers .get ("Content-Length" , "0" ))
122125 raw = self .rfile .read (length ) if length else b""
123126 try :
124- payload = json .loads (raw ) if raw else {}
125- except Exception :
126- payload = {}
127+ decoded_payload = json .loads (raw ) if raw else {}
128+ except (json .JSONDecodeError , UnicodeDecodeError ):
129+ decoded_payload = {}
130+ payload = decoded_payload if isinstance (decoded_payload , dict ) else {}
127131 stream = payload .get ("stream" , False )
128132 model = payload .get ("model" , "gpt-4o" )
129133 completion_id = "chatcmpl-mock-recording"
@@ -132,8 +136,8 @@ def do_POST(self): # noqa: N802 Function name `do_POST` should be lowercase
132136 self .send_response (200 )
133137 self .send_header ("Content-Type" , "text/event-stream" )
134138 self .end_headers ()
135- for ch in chunks :
136- self .wfile .write (f"data: { json .dumps (ch )} \n \n " .encode ())
139+ for chunk in chunks :
140+ self .wfile .write (f"data: { json .dumps (chunk )} \n \n " .encode ())
137141 self .wfile .flush ()
138142 self .wfile .write (b"data: [DONE]\n \n " )
139143 self .wfile .flush ()
@@ -146,18 +150,24 @@ def do_POST(self): # noqa: N802 Function name `do_POST` should be lowercase
146150 self .end_headers ()
147151 self .wfile .write (data )
148152
149- def log_message (self , * args ) : # silence
153+ def log_message (self , format : str , * args : Any ) -> None : # silence
150154 pass
151155
152156
153- def start_mock_server () -> HTTPServer :
157+ @contextmanager
158+ def _mock_server () -> Iterator [HTTPServer ]:
154159 server = HTTPServer ((MOCK_HOST , MOCK_PORT ), MockHandler )
155160 thread = threading .Thread (target = server .serve_forever , daemon = True )
156161 thread .start ()
157- return server
162+ try :
163+ yield server
164+ finally :
165+ server .shutdown ()
166+ server .server_close ()
167+ thread .join ()
158168
159169
160- async def run (test_id : str , config_path : str ) -> None :
170+ async def _run (test_id : str , config_path : str ) -> None :
161171 set_test_context (test_id )
162172 client = OGXAsLibraryClient (config_path , skip_logger_removal = True )
163173 try :
@@ -177,70 +187,72 @@ async def run(test_id: str, config_path: str) -> None:
177187 client .shutdown ()
178188
179189
180- def main () -> None :
181- os .environ ["OGX_TEST_INFERENCE_MODE" ] = "record"
182- os .environ ["OGX_LOGGING" ] = "all=warning"
183- os .environ ["OPENAI_API_KEY" ] = "fake-key-for-replay"
184- sqlite_dir = tempfile .mkdtemp (prefix = "ogx-record-" )
185- os .environ ["SQLITE_STORE_DIR" ] = sqlite_dir
186-
187- server = start_mock_server ()
188- port = server .server_address [1 ]
189- mock_base = f"http://{ MOCK_HOST } :{ port } /v1"
190- os .environ ["OPENAI_BASE_URL" ] = mock_base
191-
192- run_config = get_stack_run_config_from_distro ("ci-tests" )
193- run_config .storage .stores .inference = None
194- run_config .vector_stores = None
195-
196- config_file = os .path .join (tempfile .mkdtemp (), "run.yaml" )
197- with open (config_file , "w" ) as f :
198- yaml .dump (run_config .model_dump (mode = "json" ), f )
199-
200- # Isolate recording so the shared inference recordings directory is untouched.
201- # The recorder always writes into the test file's ``recordings/`` dir (relative
202- # to CWD) when a test context is set, so move the real directory aside while
203- # recording and merge only the chat-completion recordings back afterwards.
204- backup = None
205- if os .path .isdir (RECORDINGS_DIR ):
206- backup = RECORDINGS_DIR + ".bak"
207- shutil .move (RECORDINGS_DIR , backup )
208-
209- try :
210- for test_id in TEST_NODE_IDS :
211- print (f"recording for { test_id } ..." )
212- asyncio .run (run (test_id , config_file ))
213- finally :
214- server .shutdown ()
215-
216- # Collect the freshly recorded chat-completion recordings (skip models-list
217- # recordings -- the test does not need them in replay mode) and rewrite their
218- # mock-host URLs to the canonical provider URL. The recording hash ignores
219- # the host, so replay works against the real provider URL.
220- staged = tempfile .mkdtemp (prefix = "ogx-staged-" )
190+ def _stage_recordings (staged_dir : str ) -> int :
191+ """Copy generated chat recordings to staging and normalize their provider URLs."""
221192 for name in os .listdir (RECORDINGS_DIR ):
222193 if not name .endswith (".json" ) or name .startswith ("models-" ):
223194 continue
224- src = os .path .join (RECORDINGS_DIR , name )
225- with open (src ) as f :
226- data = json .load (f )
195+ source = os .path .join (RECORDINGS_DIR , name )
196+ with open (source , encoding = "utf-8" ) as file :
197+ data = json .load (file )
227198 url = data .get ("request" , {}).get ("url" , "" )
228199 if re .search (r"\d+\.\d+\.\d+\.\d+:\d+" , url ):
229200 data ["request" ]["url" ] = "https://api.openai.com/v1" + url .split ("/v1" , 1 )[1 ]
230- with open (os .path .join (staged , name ), "w" ) as f :
231- json .dump (data , f , indent = 2 )
232- f .write ("\n " )
233-
234- # Restore the original recordings directory and drop in the new recordings.
235- shutil .rmtree (RECORDINGS_DIR , ignore_errors = True )
236- if backup is not None :
237- shutil .move (backup , RECORDINGS_DIR )
238- else :
201+ with open (os .path .join (staged_dir , name ), "w" , encoding = "utf-8" ) as file :
202+ json .dump (data , file , indent = 2 )
203+ file .write ("\n " )
204+ return len (os .listdir (staged_dir ))
205+
206+
207+ def _generate_recordings (config_file : str ) -> int :
208+ """Generate recordings while preserving the repository's existing fixtures."""
209+ recordings_parent = os .path .dirname (RECORDINGS_DIR )
210+ with (
211+ tempfile .TemporaryDirectory (prefix = ".recordings-backup-" , dir = recordings_parent ) as backup_dir ,
212+ tempfile .TemporaryDirectory (prefix = "ogx-staged-" ) as staged_dir ,
213+ ):
214+ original_recordings = os .path .join (backup_dir , "recordings" )
215+ had_original_recordings = os .path .isdir (RECORDINGS_DIR )
216+ if had_original_recordings :
217+ shutil .move (RECORDINGS_DIR , original_recordings )
218+
219+ try :
220+ for test_id in TEST_NODE_IDS :
221+ print (f"recording for { test_id } ..." )
222+ asyncio .run (_run (test_id , config_file ))
223+ n_written = _stage_recordings (staged_dir )
224+ finally :
225+ shutil .rmtree (RECORDINGS_DIR , ignore_errors = True )
226+ if had_original_recordings :
227+ shutil .move (original_recordings , RECORDINGS_DIR )
228+
239229 os .makedirs (RECORDINGS_DIR , exist_ok = True )
240- n_written = len (os .listdir (staged ))
241- for name in os .listdir (staged ):
242- shutil .copy2 (os .path .join (staged , name ), os .path .join (RECORDINGS_DIR , name ))
243- shutil .rmtree (staged , ignore_errors = True )
230+ for name in os .listdir (staged_dir ):
231+ shutil .copy2 (os .path .join (staged_dir , name ), os .path .join (RECORDINGS_DIR , name ))
232+ return n_written
233+
234+
235+ def main () -> None :
236+ os .environ ["OGX_TEST_INFERENCE_MODE" ] = "record"
237+ os .environ ["OGX_LOGGING" ] = "all=warning"
238+ os .environ ["OPENAI_API_KEY" ] = "fake-key-for-replay"
239+
240+ with (
241+ tempfile .TemporaryDirectory (prefix = "ogx-record-" ) as sqlite_dir ,
242+ tempfile .TemporaryDirectory (prefix = "ogx-config-" ) as config_dir ,
243+ _mock_server () as server ,
244+ ):
245+ os .environ ["SQLITE_STORE_DIR" ] = sqlite_dir
246+ port = server .server_address [1 ]
247+ os .environ ["OPENAI_BASE_URL" ] = f"http://{ MOCK_HOST } :{ port } /v1"
248+
249+ run_config = build_inference_store_disabled_run_config ()
250+ config_file = os .path .join (config_dir , "run.yaml" )
251+ with open (config_file , "w" , encoding = "utf-8" ) as file :
252+ yaml .safe_dump (run_config .model_dump (mode = "json" ), file )
253+
254+ n_written = _generate_recordings (config_file )
255+
244256 print (f"wrote { n_written } recordings" )
245257 print ("done" )
246258
0 commit comments