Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
test:
name: pytest (py${{ matrix.python-version }})
runs-on: ubuntu-latest
# A hang must fail LOUDLY and fast, not sit burning runner time until the
# 6h default -- a job stuck at "in_progress" reads as "not done yet"
# rather than "broken", which is how a false green gets merged.
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
# requires-python = ">=3.11" -- cover the floor and a current minor.
python-version: ["3.11", "3.12"]
env:
# Placeholder, non-functional key. mount() only checks for presence
# (not validity) before constructing the provider -- without *some*
# key, mount() returns None and every behavioral/structural test
# that depends on a mounted provider fails/errors. Tests that need
# a real, working key are marked `live` and deselected below.
GOOGLE_API_KEY: test-placeholder-not-a-real-key-ci-only
steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: uv sync --all-extras --dev

- name: Run test suite
# Deselects tests marked `live`: the inherited
# test_list_models_returns_list makes a real network call to
# Google's models.list endpoint. Run locally with a real
# GOOGLE_API_KEY/GEMINI_API_KEY to validate.
run: uv run pytest -q -m "not live"
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,13 @@ allow-direct-references = true
testpaths = ["tests"]
addopts = "--import-mode=importlib"
asyncio_mode = "strict"
markers = [
"live: requires real network access / real provider credentials; deselected in CI via `-m \"not live\"`",
]

[dependency-groups]
dev = [
"amplifier-core>=1.0.10",
"amplifier-core>=1.6.0",
"google-api-core>=2.0.0",
"pytest>=9.0.3",
"pytest-asyncio>=1.2.0",
Expand Down
15 changes: 15 additions & 0 deletions tests/test_behavioral.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
Inherits authoritative tests from amplifier-core.
"""

import pytest

from amplifier_core.validation.behavioral import ProviderBehaviorTests


Expand All @@ -12,3 +14,16 @@ class TestGeminiProviderBehavior(ProviderBehaviorTests):
All tests from ProviderBehaviorTests run automatically.
Add module-specific tests below if needed.
"""

@pytest.mark.live
@pytest.mark.asyncio
async def test_list_models_returns_list(self, provider_module):
"""Override to mark this inherited test 'live'.

ProviderBehaviorTests.test_list_models_returns_list calls
provider_module.list_models(), which makes a real call to
Google's models.list endpoint -- it cannot pass in CI without a
genuine GOOGLE_API_KEY/GEMINI_API_KEY. Deselected in CI via
`-m "not live"`; run locally with real credentials to validate.
"""
await super().test_list_models_returns_list(provider_module)
7 changes: 7 additions & 0 deletions tests/test_image_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ def test_text_only_message_still_works(gemini_provider):
assert parts[0] == {"text": "Hello, how are you?"}


@pytest.mark.live
@pytest.mark.asyncio
async def test_image_vision_integration_with_real_api(test_image_base64):
"""Integration test: Verify ImageBlock works with real Gemini API.
Expand All @@ -190,6 +191,12 @@ async def test_image_vision_integration_with_real_api(test_image_base64):

Requires GOOGLE_API_KEY environment variable.
Skip if not available (unit tests still validate conversion logic).

Marked 'live' and deselected in CI via `-m "not live"`. CI sets a
placeholder GOOGLE_API_KEY so other behavioral/structural tests can
mount the provider, which defeats the plain `if not api_key: skip`
check below (a placeholder key is still truthy) -- the marker is the
real CI gate. Run this locally with a real key to validate.
"""
import os

Expand Down
34 changes: 18 additions & 16 deletions tests/test_pricing_and_vision.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,33 @@

import pytest

from amplifier_core.llm_errors import LLMError
from amplifier_module_provider_gemini import GeminiProvider


class TestFallbackModelsVision:
"""Verify hardcoded fallback models include vision capability."""
class TestNoCredentialsHardFails:
"""``list_models()`` must hard-fail without credentials, not fall back.

A hardcoded 5-model fallback list used to be returned here when no
live API call could be made. It was intentionally removed (see the
docstring on ``GeminiProvider.list_models``) because a stale hardcoded
list drifts out of sync with real Google releases and silently masks
API outages -- matching the anthropic/openai providers, which also
hard-fail rather than returning a fallback.

This replaces the old ``TestFallbackModelsVision`` tests, which
asserted on the removed fallback list's contents (vision/fast
capabilities) and could never pass again now that the list is gone.
"""

@pytest.fixture
def provider(self):
return GeminiProvider(api_key="test-key")

@pytest.mark.asyncio
async def test_fallback_all_models_have_vision(self, provider):
provider._client = None
provider._api_key = None

models = await provider.list_models()
for model in models:
assert "vision" in model.capabilities, f"{model.id} missing vision"

@pytest.mark.asyncio
async def test_fallback_flash_is_fast(self, provider):
async def test_list_models_without_credentials_raises(self, provider):
provider._client = None
provider._api_key = None

models = await provider.list_models()
flash_models = [m for m in models if "flash" in m.id]
for model in flash_models:
assert "fast" in model.capabilities, f"{model.id} should be fast"
with pytest.raises(LLMError, match="api_key must be provided"):
await provider.list_models()
33 changes: 9 additions & 24 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading