Compare commits

...

4 Commits

Author SHA1 Message Date
Teknium
6c56d60d82 fix(tests): also add api_key where missing (AIAgent needs BOTH for direct path)
My last fix added base_url but not api_key. AIAgent.__init__ takes the
direct-construction path only when BOTH are set — with only base_url
it still calls resolve_provider_client and fails in hermetic CI.

Same 31 call sites, now with both kwargs.
2026-04-17 07:41:49 -07:00
Teknium
5d179b9777 fix(tests): pass base_url to 31 more AIAgent() calls across run_agent tests
Same root cause as previous commit — tests that construct AIAgent()
without base_url rely on provider-resolver fallback state that doesn't
exist in hermetic CI / shard-split runs. Previously hidden because
other tests in the same xdist worker happened to prime module state.

Covered by the previous fix: calls that passed api_key but not base_url.
This covers calls that pass NEITHER (model=... only) — test_streaming.py
especially (24 call sites). Plus test_860_dedup, test_compression_
persistence, test_create_openai_client_*, test_provider_parity.

One call site (test_none_base_url_passed_as_none) remains explicitly
unmodified — it asserts None/empty base_url behavior, so adding base_url
would defeat the test's intent.

Validation:
- tests/run_agent/: 760 passed, 0 failed (local)
- Matrix shard 3 subset: 3098 passed, 0 failed, 1m49s (local)
2026-04-17 07:17:54 -07:00
Teknium
c2559b80fa fix(tests): pass base_url explicitly in AIAgent constructor calls
Tests that construct AIAgent(api_key=..., ...) without base_url were
relying on provider-resolver fallback state from other tests in the
same xdist worker. When matrix-split distributed them to different
shards, the resolver found no env vars and no config and raised
'No LLM provider configured'.

Fix: add base_url='https://openrouter.ai/api/v1' to every AIAgent
construction that passes api_key. AIAgent.__init__ with both args set
takes the direct-construction path (line 960 in run_agent.py) and
skips resolver fallback entirely, making these tests self-contained.

7 files, 16 call sites updated via AST-based fixup. One call site
(test_none_base_url_passed_as_none) left alone — that test's
intent is to verify base_url=None behavior, so adding base_url
defeats the test.

Validation:
- tests/run_agent/ full run: 760 passed, 0 failed (was 1 failure
  under the AST script's over-application, now clean)
- Matrix shard 3 local run: 3083 passed, 0 failed, 1m44s
2026-04-17 06:59:35 -07:00
Teknium
50f23ea522 ci: split Tests workflow into 4 parallel shards via pytest-split
Target: <2min CI test wall time.

Runs the Tests workflow as a 4-way matrix instead of one job. Each
shard runs ~3,000 tests on its own ubuntu-latest runner (4 cores) with
-n auto xdist inside. Total effective parallelism: 16 workers across
4 machines (vs 4 workers on 1 machine today).

Was previously tried in #11566 and closed — shard 3 hung at 97% complete
for 100+ seconds with dozens of E/F markers. Root cause was cross-test
pollution exposed by splitting test files across shards (e.g. the three
test files that mutated sys.modules['dotenv'] at import time poisoned
whichever shard they landed in). That's now fixed by #11453 and #11577:
conftest is hermetic, the dotenv stub bombs are removed, and tests no
longer depend on each other's env-var side effects.

Changes:
- pyproject.toml: add pytest-split>=0.9,<1 to dev extras
- .github/workflows/tests.yml: 'test' job becomes matrix-split into 4
  groups with fail-fast: false. Runs 'pytest --splits 4 --group N'.
  pytest-split composes with -n auto from pyproject addopts.

e2e job is unchanged (already small, 20s).

Expected timing:
  Before: ~4m total (243s test step + ~25s setup)
  After:  ~90-115s total (shard wall time ~60-90s + ~25s setup)

Hash-based split is deterministic; no .test_durations file needed yet.
Can add one later via --store-durations for better shard balance.
2026-04-17 06:59:35 -07:00
15 changed files with 93 additions and 4 deletions

View File

@@ -16,8 +16,13 @@ concurrency:
jobs:
test:
name: test (${{ matrix.group }}/4)
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
group: [1, 2, 3, 4]
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
@@ -37,10 +42,11 @@ jobs:
source .venv/bin/activate
uv pip install -e ".[all,dev]"
- name: Run tests
- name: Run tests (shard ${{ matrix.group }}/4)
run: |
source .venv/bin/activate
python -m pytest tests/ -q --ignore=tests/integration --ignore=tests/e2e --tb=short -n auto
python -m pytest tests/ -q --ignore=tests/integration --ignore=tests/e2e --tb=short \
--splits 4 --group ${{ matrix.group }}
env:
# Ensure tests don't accidentally call real APIs
OPENROUTER_API_KEY: ""

View File

@@ -39,7 +39,7 @@ dependencies = [
[project.optional-dependencies]
modal = ["modal>=1.0.0,<2"]
daytona = ["daytona>=0.148.0,<1"]
dev = ["debugpy>=1.8.0,<2", "pytest>=9.0.2,<10", "pytest-asyncio>=1.3.0,<2", "pytest-xdist>=3.0,<4", "mcp>=1.2.0,<2"]
dev = ["debugpy>=1.8.0,<2", "pytest>=9.0.2,<10", "pytest-asyncio>=1.3.0,<2", "pytest-xdist>=3.0,<4", "pytest-split>=0.9,<1", "mcp>=1.2.0,<2"]
messaging = ["python-telegram-bot[webhooks]>=22.6,<23", "discord.py[voice]>=2.7.1,<3", "aiohttp>=3.13.3,<4", "slack-bolt>=1.18.0,<2", "slack-sdk>=3.27.0,<4", "qrcode>=7.0,<8"]
cron = ["croniter>=6.0.0,<7"]
slack = ["slack-bolt>=1.18.0,<2", "slack-sdk>=3.27.0,<4"]

View File

@@ -32,6 +32,7 @@ class TestGeneric400Heuristic:
from run_agent import AIAgent
a = AIAgent(
api_key="test-key-12345",
base_url="https://openrouter.ai/api/v1",
quiet_mode=True,
skip_context_files=True,
skip_memory=True,

View File

@@ -69,6 +69,7 @@ def agent():
):
a = AIAgent(
api_key="test-key-1234567890",
base_url="https://openrouter.ai/api/v1",
quiet_mode=True,
skip_context_files=True,
skip_memory=True,

View File

@@ -29,6 +29,8 @@ class TestFlushDeduplication:
with patch.dict(os.environ, {"OPENROUTER_API_KEY": "test-key"}):
from run_agent import AIAgent
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
session_db=session_db,
@@ -271,6 +273,8 @@ class TestFlushIdxInit:
with patch.dict(os.environ, {"OPENROUTER_API_KEY": "test-key"}):
from run_agent import AIAgent
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,
@@ -283,6 +287,8 @@ class TestFlushIdxInit:
with patch.dict(os.environ, {"OPENROUTER_API_KEY": "test-key"}):
from run_agent import AIAgent
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,

View File

@@ -37,6 +37,8 @@ class TestFlushAfterCompression:
with patch.dict(os.environ, {"OPENROUTER_API_KEY": "test-key"}):
from run_agent import AIAgent
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
session_db=session_db,

View File

@@ -19,6 +19,8 @@ from run_agent import AIAgent
def test_create_openai_client_does_not_mutate_input_kwargs(mock_openai):
mock_openai.return_value = MagicMock()
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,

View File

@@ -23,6 +23,8 @@ from run_agent import AIAgent
def _make_agent():
return AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,

View File

@@ -36,6 +36,7 @@ def _make_agent(fallback_model=None):
):
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
quiet_mode=True,
skip_context_files=True,
skip_memory=True,

View File

@@ -45,6 +45,7 @@ def test_plugin_engine_gets_context_length_on_init():
agent = AIAgent(
api_key="test-key-1234567890",
base_url="https://openrouter.ai/api/v1",
quiet_mode=True,
skip_context_files=True,
skip_memory=True,
@@ -75,6 +76,7 @@ def test_plugin_engine_update_model_args():
agent = AIAgent(
model="openrouter/auto",
api_key="test-key-1234567890",
base_url="https://openrouter.ai/api/v1",
quiet_mode=True,
skip_context_files=True,
skip_memory=True,

View File

@@ -19,6 +19,7 @@ def _make_agent(fallback_model=None):
):
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
quiet_mode=True,
skip_context_files=True,
skip_memory=True,

View File

@@ -60,6 +60,9 @@ def _make_agent(monkeypatch, provider, api_mode="chat_completions", base_url="ht
)
if model:
kwargs["model"] = model
base_url="https://openrouter.ai/api/v1",
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
return AIAgent(**kwargs)

View File

@@ -55,6 +55,7 @@ def agent():
):
a = AIAgent(
api_key="test-key-1234567890",
base_url="https://openrouter.ai/api/v1",
quiet_mode=True,
skip_context_files=True,
skip_memory=True,
@@ -76,6 +77,7 @@ def agent_with_memory_tool():
):
a = AIAgent(
api_key="test-k...7890",
base_url="https://openrouter.ai/api/v1",
quiet_mode=True,
skip_context_files=True,
skip_memory=True,
@@ -112,12 +114,14 @@ def test_aiagent_reuses_existing_errors_log_handler():
):
AIAgent(
api_key="test-k...7890",
base_url="https://openrouter.ai/api/v1",
quiet_mode=True,
skip_context_files=True,
skip_memory=True,
)
AIAgent(
api_key="test-k...7890",
base_url="https://openrouter.ai/api/v1",
quiet_mode=True,
skip_context_files=True,
skip_memory=True,
@@ -491,6 +495,7 @@ class TestInit:
):
a = AIAgent(
api_key="test-key-1234567890",
base_url="https://openrouter.ai/api/v1",
model="openai/gpt-4o",
quiet_mode=True,
skip_context_files=True,
@@ -542,6 +547,7 @@ class TestInit:
):
a = AIAgent(
api_key="test-key-1234567890",
base_url="https://openrouter.ai/api/v1",
quiet_mode=True,
skip_context_files=True,
skip_memory=True,
@@ -557,6 +563,7 @@ class TestInit:
):
a = AIAgent(
api_key="test-key-1234567890",
base_url="https://openrouter.ai/api/v1",
quiet_mode=True,
skip_context_files=True,
skip_memory=True,
@@ -694,6 +701,7 @@ class TestBuildSystemPrompt:
):
agent = AIAgent(
api_key="test-k...7890",
base_url="https://openrouter.ai/api/v1",
quiet_mode=True,
skip_context_files=True,
skip_memory=True,
@@ -726,6 +734,7 @@ class TestToolUseEnforcementConfig:
a = AIAgent(
model=model,
api_key="test-key-1234567890",
base_url="https://openrouter.ai/api/v1",
quiet_mode=True,
skip_context_files=True,
skip_memory=True,
@@ -822,6 +831,7 @@ class TestToolUseEnforcementConfig:
):
a = AIAgent(
api_key="test-key-1234567890",
base_url="https://openrouter.ai/api/v1",
quiet_mode=True,
skip_context_files=True,
skip_memory=True,
@@ -3433,7 +3443,7 @@ class TestAnthropicBaseUrlPassthrough:
):
mock_build.return_value = MagicMock()
a = AIAgent(
api_key="sk-ant-api03-test1234567890",
api_key="sk-ant...7890",
api_mode="anthropic_messages",
quiet_mode=True,
skip_context_files=True,
@@ -3457,6 +3467,7 @@ class TestAnthropicCredentialRefresh:
mock_build.side_effect = [old_client, new_client]
agent = AIAgent(
api_key="sk-ant-oat01-stale-token",
base_url="https://openrouter.ai/api/v1",
api_mode="anthropic_messages",
quiet_mode=True,
skip_context_files=True,
@@ -3487,6 +3498,7 @@ class TestAnthropicCredentialRefresh:
):
agent = AIAgent(
api_key="sk-ant-oat01-same-token",
base_url="https://openrouter.ai/api/v1",
api_mode="anthropic_messages",
quiet_mode=True,
skip_context_files=True,
@@ -3514,6 +3526,7 @@ class TestAnthropicCredentialRefresh:
):
agent = AIAgent(
api_key="sk-ant-oat01-current-token",
base_url="https://openrouter.ai/api/v1",
api_mode="anthropic_messages",
quiet_mode=True,
skip_context_files=True,

View File

@@ -80,6 +80,8 @@ class TestStreamingAccumulator:
mock_create.return_value = mock_client
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,
@@ -120,6 +122,8 @@ class TestStreamingAccumulator:
mock_create.return_value = mock_client
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,
@@ -167,6 +171,8 @@ class TestStreamingAccumulator:
mock_create.return_value = mock_client
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,
@@ -205,6 +211,8 @@ class TestStreamingAccumulator:
mock_create.return_value = mock_client
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,
@@ -245,6 +253,8 @@ class TestStreamingCallbacks:
mock_create.return_value = mock_client
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,
@@ -277,6 +287,8 @@ class TestStreamingCallbacks:
mock_create.return_value = mock_client
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,
@@ -308,6 +320,8 @@ class TestStreamingCallbacks:
mock_create.return_value = mock_client
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,
@@ -346,6 +360,8 @@ class TestStreamingCallbacks:
mock_create.return_value = mock_client
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,
@@ -381,6 +397,8 @@ class TestStreamingCallbacks:
mock_create.return_value = mock_client
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,
@@ -428,6 +446,8 @@ class TestStreamingFallback:
mock_create.return_value = mock_client
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,
@@ -455,6 +475,8 @@ class TestStreamingFallback:
mock_create.return_value = mock_client
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,
@@ -477,6 +499,8 @@ class TestStreamingFallback:
mock_create.return_value = mock_client
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,
@@ -500,6 +524,8 @@ class TestStreamingFallback:
mock_create.return_value = mock_client
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,
@@ -542,6 +568,8 @@ class TestStreamingFallback:
mock_create.return_value = mock_client
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,
@@ -577,6 +605,8 @@ class TestStreamingFallback:
mock_create.return_value = mock_client
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,
@@ -619,6 +649,8 @@ class TestReasoningStreaming:
mock_create.return_value = mock_client
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,
@@ -646,6 +678,8 @@ class TestHasStreamConsumers:
def test_no_consumers(self):
from run_agent import AIAgent
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,
@@ -656,6 +690,8 @@ class TestHasStreamConsumers:
def test_delta_callback_set(self):
from run_agent import AIAgent
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,
@@ -667,6 +703,8 @@ class TestHasStreamConsumers:
def test_stream_callback_set(self):
from run_agent import AIAgent
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,
@@ -688,6 +726,8 @@ class TestCodexStreamCallbacks:
deltas = []
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,
@@ -729,6 +769,8 @@ class TestCodexStreamCallbacks:
from run_agent import AIAgent
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,
@@ -792,6 +834,8 @@ class TestCodexStreamCallbacks:
)
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,
@@ -810,6 +854,8 @@ class TestCodexStreamCallbacks:
from run_agent import AIAgent
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,
@@ -861,6 +907,8 @@ class TestAnthropicStreamCallbacks:
from run_agent import AIAgent
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
model="test/model",
quiet_mode=True,
skip_context_files=True,

View File

@@ -22,6 +22,7 @@ def _make_agent(session_db, *, platform: str):
):
agent = AIAgent(
api_key="test-key",
base_url="https://openrouter.ai/api/v1",
quiet_mode=True,
skip_context_files=True,
skip_memory=True,