"""CPU contract/integration tests against upstream-shaped duplex boundaries. These use real PCM/JPEG encoding, Pydantic parsing and FastAPI HTTP handling, plus native adapter source-ledger and lifecycle logic. They do not claim CUDA. """ import base64 import io import json import threading import time from concurrent.futures import ThreadPoolExecutor import numpy as np import pytest from fastapi.testclient import TestClient from PIL import Image from pydantic import ValidationError from gpu_service.app import create_app from gpu_service.contracts import AVChunk from gpu_service.native import MODEL_LOCK, NativeDuplex, NativeContractError, prepare_text_context, text_only_duplex TOKEN = "test-only-token-no-credentials-12345" SESSION = "contract-session-1234" class UpstreamDecoder: def __init__(self): self.units = 0 self.generated_tokens = [] def get_window_stats(self): return {"unit_count": min(self.units, 64), "cache_length": min(self.units, 64) * 100} def get_cache_length(self): return min(self.units, 64) * 100 @property def _unit_history(self): return [None] * min(self.units, 64) def reset(self): self.units = 0 class UpstreamShapedDuplex: """Faithful signatures and first-chunk/core consumption from pinned source.""" def __init__(self): self.decoder = UpstreamDecoder() self.audio_buffer = np.zeros(0, dtype=np.float32) self.calls = [] self.current_turn_ended = True self.total_hidden = [] self.total_ids = [] self.prefill_schema_tokens = [] self.res_ids = [] self.stopped = False self.last_audio = None self.last_frames = None self.first = True self.generated_count = 0 self.model = None self.prompts = [] self.generate_options = [] self.speak_token_id = 1 self.pending_logits = np.zeros((1, 4), dtype=np.float32) def prepare(self, *, prefix_system_prompt): self.calls.append("prepare") assert "English" in prefix_system_prompt assert "chain-of-thought" in prefix_system_prompt self.prompts.append(prefix_system_prompt) self.stopped = False self.decoder.reset() self._reset_streaming_state() def _reset_streaming_state(self): self.audio_buffer = np.zeros(0, dtype=np.float32) self.first = True def set_session_stop(self): self.stopped = True self.calls.append("stop") def streaming_prefill(self, *, audio_waveform, frame_list, max_slice_nums, batch_vision_feed): self.calls.append("prefill") assert audio_waveform.dtype == np.float32 and audio_waveform.shape == (16000,) assert max_slice_nums == 2 and batch_vision_feed is True self.last_audio, self.last_frames = audio_waveform.copy(), frame_list self.pending_logits = np.zeros((1, 4), dtype=np.float32) self.audio_buffer = np.concatenate([self.audio_buffer, audio_waveform]) if self.first: self.audio_buffer = np.concatenate([np.zeros(560, dtype=np.float32), self.audio_buffer]) self.audio_buffer = self.audio_buffer[16480:] self.first = False else: self.audio_buffer = self.audio_buffer[16000:] return {"success": True, "cost_all": 0.0001} def streaming_generate(self, *, max_new_speak_tokens_per_chunk, decode_mode, listen_top_k=None, listen_prob_scale=1.0): self.calls.append("generate") assert 8 <= max_new_speak_tokens_per_chunk <= 64 and decode_mode == "sampling" self.generate_options.append((max_new_speak_tokens_per_chunk, listen_top_k, listen_prob_scale)) self.generated_count += 1 self.decoder.units += 1 self.total_hidden.append([np.ones(10)]) return {"is_listen": True, "text": "", "audio_waveform": np.zeros(24000), "end_of_turn": False, "current_time": self.generated_count, "cost_all": 0.0001} def payload(sequence=0, start_ms=None, count=16000, epoch=0): start = sequence * 1000 if start_ms is None else start_ms # Dense continuously nonzero signal, encoded as actual little-endian float32. signal = (np.sin(np.arange(count) * 2 * np.pi * 313 / 16000) * 0.7).astype("system\n" + prompt + "<|im_end|>" assert protected == [len(received)] and decoder.feed.call_count < 12 duplex.model.init_streaming_processor.assert_called_once() cancelled.set() forwards = decoder.feed.call_count with pytest.raises(NativeContractError, match="cancelled"): prepare_text_context(duplex, prompt, cancelled) assert decoder.feed.call_count == forwards def test_text_duplex_skips_unused_speech_renderer_and_restores_model_method(): from types import SimpleNamespace class Model: config = SimpleNamespace(init_vision=True, init_audio=True, init_tts=False) fail_after_init = False def init_tts(self, **kwargs): raise RuntimeError("Unused speech renderer must not be initialized") def as_duplex(self, *, generate_audio): self.init_tts(enable_float16=False, n_timesteps=10) if self.fail_after_init: raise ValueError("unrelated constructor failure") return SimpleNamespace(generate_audio=generate_audio, model=self) model = Model() with pytest.raises(RuntimeError, match="Unused speech renderer"): model.as_duplex(generate_audio=False) duplex = text_only_duplex(model) assert duplex.model is model and duplex.generate_audio is False assert model.config.init_vision and model.config.init_audio assert "init_tts" not in vars(model) model.fail_after_init = True with pytest.raises(ValueError, match="unrelated constructor failure"): text_only_duplex(model) assert "init_tts" not in vars(model) with pytest.raises(RuntimeError, match="Unused speech renderer"): model.init_tts() def test_eager_startup_error_identifies_stage_and_cause_without_secret_messages(monkeypatch): import importlib module = importlib.import_module("gpu_service.app") secret = "hf_" + "private-value-never-log" * 2 monkeypatch.setenv("HF_TOKEN", secret) monkeypatch.setenv("NATIVE_EAGER_LOAD", "1") class FailingLoad(NativeDuplex): def load(self): self.load_stage = "native_duplex_setup" try: raise ValueError("private download URL and credential: " + secret) except ValueError: raise ImportError("unhelpful upstream wrapper " + secret) from None monkeypatch.setattr(module, "NativeDuplex", FailingLoad) with pytest.raises(RuntimeError) as failure: with TestClient(module.create_app(token=TOKEN)): pass message = str(failure.value) assert "native_duplex_setup" in message assert "ImportError" in message and "ValueError" in message assert "test_gpu_contract.py:" in message assert secret not in message and "private download URL" not in message def test_portfolio_context_once_per_epoch_without_publication_cooldown(): from types import SimpleNamespace client, native = client_and_native() resets = [] native.model = SimpleNamespace(reset_session=lambda: resets.append(True)) holding = {"id": "copper-fund", "name": "User copper research holding", "ticker": "COPPER", "exchange": "User supplied", "currency": "CAD", "asset_type": "fund", "weight": 100} context = {"session_id": SESSION, "epoch": 0, "objective": "Evaluate source evidence relevant to copper demand.", "holdings": [holding], "output_max_tokens_per_unit": 32, "live_update_interval_seconds": 10} original = native.streaming_generate def first_speaks(**kwargs): result = original(**kwargs) if native.generated_count == 1: return {**result, "text": "Evidence: Demand is discussed. Holding: copper-fund", "is_listen": False, "end_of_turn": True} return result native.streaming_generate = first_speaks with client: assert client.post("/sessions", json=context).status_code == 200 assert client.post("/sessions", json=context).status_code == 200 assert client.post("/sessions", json={**context, "holdings": [{**holding, "name": "Changed name"}]}).status_code == 409 assert client.post(f"/sessions/{SESSION}/chunks", json=payload()).status_code == 200 assert client.post(f"/sessions/{SESSION}/chunks", json=payload(1)).status_code == 200 assert native.generate_options == [(32, None, 1.0), (32, None, 1.0)] assert len(native.prompts) == 1 and '"id":"copper-fund"' in native.prompts[0] assert len(resets) == 1 assert "Holding: the exact user holding ID" in native.prompts[0] assert client.post(f"/sessions/{SESSION}/reset", json={"epoch": 1}).status_code == 200 assert len(native.prompts) == 2 and native.prompts[1] == native.prompts[0] assert len(resets) >= 2 assert client.post(f"/sessions/{SESSION}/chunks", json=payload(0, start_ms=30000, epoch=1)).status_code == 200 assert native.generate_options[-1] == (32, None, 1.0) def test_native_silence_is_not_overridden_and_effective_settings_are_reported(): client, native = client_and_native() original = native.streaming_generate def decode(**kwargs): # Application must leave every pending logit unchanged, even after a # long silent stretch or the legacy interval setting. np.testing.assert_array_equal(native.pending_logits, np.zeros((1, 4))) return original(**kwargs) native.streaming_generate = decode with client: response = client.post('/sessions', json={'session_id': SESSION, 'epoch': 0, 'objective': 'Evaluate supplied media.', 'live_update_interval_seconds': 1}) assert response.status_code == 200 runtime = response.json()['runtime'] assert runtime['max_slice_nums'] == 2 and runtime['batch_vision_feed'] is True assert runtime['generate_audio'] is False and runtime['audio_understanding'] is True assert runtime['window_high_tokens'] == 8000 and runtime['window_low_tokens'] == 6000 for i in range(5): result = client.post(f'/sessions/{SESSION}/chunks', json=payload(i)).json() assert result['native_silence'] and result['text'] == '' timing = result['timing'] assert timing['new_source_ms'] == (995 if i == 0 else 1000) assert timing['processing_per_source_second'] == pytest.approx( (timing['preprocess_ms'] + timing['native_unit_ms']) / timing['new_source_ms']) assert native.calls.count('prepare') == 1 and native.calls.count('generate') == 5 assert client.post(f'/sessions/{SESSION}/finish', json={'epoch': 0}).status_code == 200 def test_real_pcm_jpeg_decode_and_native_request(monkeypatch): from gpu_service.contracts import Frame audio_decode, frame_decode = AVChunk.decode_audio, Frame.decode decoded = [] def audio_once(chunk): decoded.append(("audio", threading.current_thread().name)) return audio_decode(chunk) def frame_once(frame): decoded.append(("frame", threading.current_thread().name)) return frame_decode(frame) monkeypatch.setattr(AVChunk, "decode_audio", audio_once) monkeypatch.setattr(Frame, "decode", frame_once) client, native = client_and_native() with client: start(client) data = payload() second = io.BytesIO() Image.new("RGB", (32, 24), "blue").save(second, "JPEG") data["frames"].append({"source_time_ms": 750.125, "jpeg_b64": base64.b64encode(second.getvalue()).decode()}) response = client.post(f"/sessions/{SESSION}/chunks", json=data) assert response.status_code == 200, response.text result = response.json() assert native.last_frames[0].mode == "RGB" np.testing.assert_array_equal(native.last_audio, np.frombuffer(base64.b64decode(data["audio_b64"]), dtype="