2026-03-23 17:19:15 +08:00
|
|
|
"""Gateway internal decision/plan builder tests."""
|
|
|
|
|
|
|
|
|
|
import asyncio
|
|
|
|
|
import base64
|
|
|
|
|
import json
|
|
|
|
|
import time
|
|
|
|
|
from types import SimpleNamespace
|
|
|
|
|
from typing import Any
|
|
|
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
from fastapi import BackgroundTasks, FastAPI
|
|
|
|
|
from fastapi.responses import JSONResponse, StreamingResponse
|
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
from sqlalchemy import create_engine
|
|
|
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
|
|
|
|
|
|
from src.api.internal.gateway import (
|
|
|
|
|
CONTROL_ACTION_HEADER,
|
|
|
|
|
CONTROL_ACTION_PROXY_PUBLIC,
|
|
|
|
|
CONTROL_EXECUTED_HEADER,
|
|
|
|
|
GatewayAuthContext,
|
|
|
|
|
GatewayExecutionDecisionResponse,
|
|
|
|
|
GatewayExecuteRequest,
|
|
|
|
|
GatewayExecutionPlanResponse,
|
|
|
|
|
GatewayResolveRequest,
|
|
|
|
|
GatewayStreamReportRequest,
|
|
|
|
|
GatewaySyncReportRequest,
|
|
|
|
|
_dispatch_gateway_sync_telemetry,
|
|
|
|
|
_build_gateway_sync_error_payload,
|
|
|
|
|
_build_gateway_sync_telemetry_writer,
|
|
|
|
|
_run_gateway_stream_report_background,
|
|
|
|
|
_run_gateway_sync_report_background,
|
|
|
|
|
_stream_executor_requires_python_rewrite,
|
|
|
|
|
_build_claude_chat_sync_decision,
|
|
|
|
|
_build_claude_chat_stream_decision,
|
|
|
|
|
_build_claude_cli_sync_decision,
|
|
|
|
|
_build_claude_cli_stream_decision,
|
|
|
|
|
_build_gemini_files_download_stream_decision,
|
|
|
|
|
_build_gemini_files_proxy_sync_decision,
|
|
|
|
|
_build_gemini_chat_sync_decision,
|
|
|
|
|
_build_gemini_chat_stream_decision,
|
|
|
|
|
_build_gemini_cli_sync_decision,
|
|
|
|
|
_build_gemini_cli_stream_decision,
|
|
|
|
|
_build_openai_chat_sync_decision,
|
|
|
|
|
_build_openai_chat_stream_decision,
|
|
|
|
|
_build_openai_cli_stream_decision,
|
|
|
|
|
_build_openai_video_content_stream_decision,
|
|
|
|
|
_extract_gateway_sync_error_message,
|
|
|
|
|
_record_gateway_direct_candidate_graph,
|
|
|
|
|
_resolve_gateway_sync_error_status_code,
|
|
|
|
|
_build_openai_chat_sync_plan,
|
|
|
|
|
_build_openai_cli_sync_decision,
|
|
|
|
|
_build_openai_cli_stream_plan,
|
|
|
|
|
_build_openai_cli_sync_plan,
|
|
|
|
|
_is_streaming_sync_payload,
|
|
|
|
|
_resolve_auth_context,
|
|
|
|
|
_resolve_gateway_sync_adapter,
|
|
|
|
|
classify_gateway_route,
|
|
|
|
|
router,
|
|
|
|
|
)
|
|
|
|
|
from src.database import get_db
|
|
|
|
|
from src.models.database import Base, RequestCandidate
|
|
|
|
|
from src.services.orchestration.candidate_resolver import CandidateResolver
|
|
|
|
|
from src.services.scheduling.schemas import PoolCandidate, ProviderCandidate
|
2026-04-03 14:59:58 +08:00
|
|
|
from src.services.request.execution_runtime_plan import ExecutionPlan, ExecutionPlanBody, PreparedExecutionPlan
|
2026-03-23 17:19:15 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _wait_until(predicate: Any, *, timeout: float = 1.0, interval: float = 0.01) -> None:
|
|
|
|
|
deadline = time.time() + timeout
|
|
|
|
|
while time.time() < deadline:
|
|
|
|
|
if predicate():
|
|
|
|
|
return
|
|
|
|
|
time.sleep(interval)
|
|
|
|
|
assert predicate()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_stream_executor_requires_python_rewrite_allows_antigravity_same_format() -> None:
|
|
|
|
|
class AntigravityEnvelope:
|
|
|
|
|
name = "antigravity:v1internal"
|
|
|
|
|
|
|
|
|
|
def force_stream_rewrite(self) -> bool:
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
envelope = AntigravityEnvelope()
|
|
|
|
|
assert (
|
|
|
|
|
_stream_executor_requires_python_rewrite(
|
|
|
|
|
envelope=envelope,
|
|
|
|
|
needs_conversion=False,
|
|
|
|
|
provider_api_format="gemini:cli",
|
|
|
|
|
client_api_format="gemini:cli",
|
|
|
|
|
)
|
|
|
|
|
is False
|
|
|
|
|
)
|
|
|
|
|
assert (
|
|
|
|
|
_stream_executor_requires_python_rewrite(
|
|
|
|
|
envelope=envelope,
|
|
|
|
|
needs_conversion=False,
|
|
|
|
|
provider_api_format="gemini:chat",
|
|
|
|
|
client_api_format="gemini:chat",
|
|
|
|
|
)
|
|
|
|
|
is False
|
|
|
|
|
)
|
|
|
|
|
assert (
|
|
|
|
|
_stream_executor_requires_python_rewrite(
|
|
|
|
|
envelope=envelope,
|
|
|
|
|
needs_conversion=False,
|
|
|
|
|
provider_api_format="gemini:cli",
|
|
|
|
|
client_api_format="openai:cli",
|
|
|
|
|
)
|
|
|
|
|
is True
|
|
|
|
|
)
|
|
|
|
|
assert (
|
|
|
|
|
_stream_executor_requires_python_rewrite(
|
|
|
|
|
envelope=envelope,
|
|
|
|
|
needs_conversion=True,
|
|
|
|
|
provider_api_format="gemini:chat",
|
|
|
|
|
client_api_format="openai:chat",
|
|
|
|
|
)
|
|
|
|
|
is False
|
|
|
|
|
)
|
|
|
|
|
assert (
|
|
|
|
|
_stream_executor_requires_python_rewrite(
|
|
|
|
|
envelope=envelope,
|
|
|
|
|
needs_conversion=True,
|
|
|
|
|
provider_api_format="gemini:cli",
|
|
|
|
|
client_api_format="openai:cli",
|
|
|
|
|
)
|
|
|
|
|
is False
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_stream_executor_requires_python_rewrite_allows_kiro_same_format() -> None:
|
|
|
|
|
class KiroEnvelope:
|
|
|
|
|
name = "kiro:generateAssistantResponse"
|
|
|
|
|
|
|
|
|
|
def force_stream_rewrite(self) -> bool:
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
envelope = KiroEnvelope()
|
|
|
|
|
assert (
|
|
|
|
|
_stream_executor_requires_python_rewrite(
|
|
|
|
|
envelope=envelope,
|
|
|
|
|
needs_conversion=False,
|
|
|
|
|
provider_api_format="claude:cli",
|
|
|
|
|
client_api_format="claude:cli",
|
|
|
|
|
)
|
|
|
|
|
is False
|
|
|
|
|
)
|
|
|
|
|
assert (
|
|
|
|
|
_stream_executor_requires_python_rewrite(
|
|
|
|
|
envelope=envelope,
|
|
|
|
|
needs_conversion=False,
|
|
|
|
|
provider_api_format="claude:chat",
|
|
|
|
|
client_api_format="claude:chat",
|
|
|
|
|
)
|
|
|
|
|
is True
|
|
|
|
|
)
|