File size: 10,311 Bytes
a6a5d8e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# SPDX-License-Identifier: Apache-2.0
# © 2026 Lutar, Stephen P. — SZL Holdings
# Doctrine v11 LOCKED 749/14/163. Authored by Yachay (CTO).
# DCO: Signed-off-by: Yachay <yachay@szlholdings.dev>
# Co-Authored-By: Perplexity Computer Agent
"""
tests/test_v4_agent_voters.py — pytest tests for the a11oy v4 multi-LLM ensemble.

Tests:
  1. Voter with missing env var returns {status: "unavailable"} — not an error.
  2. /api/a11oy/v4/agent/voters returns all 13 voters.
  3. /api/a11oy/v4/agent/ask with explicit voters list respects the filter.

All tests are hermetic: no real LLM calls, no real env vars needed.
"""
from __future__ import annotations

import asyncio
import json
import os
import sys
from pathlib import Path
from typing import Any, Dict, List
from unittest.mock import AsyncMock, patch

import pytest

# Ensure repo root is on path
_REPO = Path(__file__).parent.parent.resolve()
if str(_REPO) not in sys.path:
    sys.path.insert(0, str(_REPO))

# ---------------------------------------------------------------------------
# Imports under test
# ---------------------------------------------------------------------------
from packages.inference.src.voters import (
    get_all_voters,
    get_voter,
    resolve_voters,
    VOTER_COUNT,
)
from packages.inference.src.voters.deepseek_r1 import DeepSeekR1Voter
from packages.inference.src.voters.kimi_k2 import KimiK2Voter
from packages.inference.src.voters.nemotron_super_49b import NemotronSuper49BVoter


# ---------------------------------------------------------------------------
# Test 1: Voter with missing env var returns {status: "unavailable"}
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_missing_env_var_returns_unavailable():
    """A cloud voter with no env var must return status='unavailable', not raise."""
    # Use DeepSeekR1Voter — requires DEEPSEEK_API_KEY or HF_TOKEN
    voter = DeepSeekR1Voter()

    # Guarantee both env vars are absent for this test
    env_patch = {k: None for k in voter.ENV_VARS}
    with patch.dict(os.environ, {}, clear=False):
        for k in voter.ENV_VARS:
            os.environ.pop(k, None)

        assert not voter.is_available(), "voter should be unavailable without env vars"

        result = await voter.vote(prompt="hello world")

    assert result["status"] == "unavailable", (
        f"Expected 'unavailable', got {result['status']!r}"
    )
    assert result["voter_id"] == "deepseek-r1"
    assert result["text"] is None
    assert "token_not_present" in result["reason"]
    # latency_ms is None when unavailable
    assert result["latency_ms"] is None


@pytest.mark.asyncio
async def test_multiple_voters_missing_env_return_unavailable():
    """Several new voters with no env vars must all return 'unavailable', not raise."""
    voters_to_check = [
        KimiK2Voter(),
        NemotronSuper49BVoter(),
    ]
    for voter in voters_to_check:
        for k in voter.ENV_VARS:
            os.environ.pop(k, None)

        result = await voter.vote(prompt="test")
        assert result["status"] == "unavailable", (
            f"{voter.VOTER_ID}: expected 'unavailable', got {result['status']!r}"
        )
        assert result["text"] is None
        assert "token_not_present" in result["reason"]


# ---------------------------------------------------------------------------
# Test 2: /api/a11oy/v4/agent/voters returns all 13 voters
# ---------------------------------------------------------------------------

def test_voter_count_is_13():
    """Registry must contain exactly 13 voters."""
    assert VOTER_COUNT == 13, f"Expected 13 voters, got {VOTER_COUNT}"


def test_all_voters_have_required_fields():
    """Every voter must expose required metadata fields."""
    voters = get_all_voters()
    assert len(voters) == 13

    required_fields = {"voter_id", "status", "provider", "license", "context_window", "env_vars", "model_id"}
    for v in voters:
        meta = v.metadata()
        for field in required_fields:
            assert field in meta, f"{v.VOTER_ID}: missing metadata field '{field}'"
        assert meta["status"] in ("available", "token_required"), (
            f"{v.VOTER_ID}: invalid status {meta['status']!r}"
        )


def test_sovereign_default_always_available():
    """qwen-local (sovereign-default) must always be available regardless of env vars."""
    qwen = get_voter("qwen-local")
    assert qwen is not None, "qwen-local voter not found in registry"
    assert qwen.is_available(), "qwen-local (sovereign-default) must always be available"
    assert qwen.metadata()["status"] == "available"


def test_all_13_voter_ids_present():
    """All 13 voter IDs must be in the registry."""
    expected_ids = {
        # Original 4
        "qwen-local",
        "hf-inference-llama",
        "hf-inference-mistral",
        "hf-inference-qwen",
        # 9 new voters
        "deepseek-r1",
        "kimi-k2",
        "glm-4.6",
        "hermes-4-405b",
        "minimax-m2",
        "step-3.7-flash",
        "nomos-1",
        "nemotron-super-49b",
        "xiaomi-mimo",
    }
    actual_ids = {v.VOTER_ID for v in get_all_voters()}
    assert actual_ids == expected_ids, (
        f"Voter ID mismatch.\nExpected: {sorted(expected_ids)}\nGot: {sorted(actual_ids)}"
    )


def test_voters_endpoint_response_shape():
    """Simulate the /agent/voters endpoint response and validate its shape."""
    import a11oy_v4_agent as agent_mod
    from packages.inference.src.voters import VOTER_INPUT_SCHEMA, VOTER_OUTPUT_SCHEMA

    voters = get_all_voters()
    response = {
        "count": len(voters),
        "voters": [v.metadata() for v in voters],
        "sovereign_default": "qwen-local",
        "doctrine": {"version": "v11", "declarations": 749, "axioms": 14, "sorries": 163},
    }

    assert response["count"] == 13
    assert response["sovereign_default"] == "qwen-local"
    assert len(response["voters"]) == 13
    # Doctrine pins intact
    assert response["doctrine"]["declarations"] == 749
    assert response["doctrine"]["axioms"] == 14
    assert response["doctrine"]["sorries"] == 163


# ---------------------------------------------------------------------------
# Test 3: /agent/ask with explicit voters list respects the filter
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_ask_respects_voters_filter():
    """/agent/ask with voters=["qwen-local","deepseek-r1"] runs exactly those two."""
    from packages.inference.src.voters import resolve_voters

    # Only request two voters
    requested = ["qwen-local", "deepseek-r1"]
    voters = resolve_voters(requested)

    voter_ids = [v.VOTER_ID for v in voters]
    assert "qwen-local" in voter_ids, "sovereign-default qwen-local must always be included"
    assert "deepseek-r1" in voter_ids, "explicitly requested deepseek-r1 must be included"
    # Other voters must not be present
    for vid in voter_ids:
        assert vid in requested, f"Unexpected voter {vid!r} in resolved list"


@pytest.mark.asyncio
async def test_ask_sovereign_always_present_even_without_list():
    """When no voters list is given, only qwen-local (sovereign-default) runs."""
    voters = resolve_voters(None)
    assert len(voters) == 1
    assert voters[0].VOTER_ID == "qwen-local"


@pytest.mark.asyncio
async def test_ask_deepseek_unavailable_when_no_key():
    """When deepseek-r1 is in voters list but DEEPSEEK_API_KEY absent →
    vote returns 'unavailable', not an exception."""
    from packages.inference.src.voters import resolve_voters

    # Clear env vars
    for k in ["DEEPSEEK_API_KEY", "HF_TOKEN"]:
        os.environ.pop(k, None)

    requested = ["qwen-local", "deepseek-r1"]
    voters = resolve_voters(requested)

    # Run only deepseek (skip qwen to avoid vLLM stub noise)
    ds_voter = next(v for v in voters if v.VOTER_ID == "deepseek-r1")
    result = await ds_voter.vote(prompt="test prompt")

    assert result["status"] == "unavailable"
    assert result["voter_id"] == "deepseek-r1"
    assert "token_not_present" in result["reason"]


@pytest.mark.asyncio
async def test_unknown_voter_id_silently_skipped():
    """An unknown voter ID in the voters list is silently skipped (no KeyError)."""
    voters = resolve_voters(["qwen-local", "nonexistent-voter-xyz"])
    voter_ids = [v.VOTER_ID for v in voters]
    assert "qwen-local" in voter_ids
    assert "nonexistent-voter-xyz" not in voter_ids


@pytest.mark.asyncio
async def test_lambda_aggregator_no_ok_votes():
    """Λ-aggregator must handle zero ok-status votes gracefully."""
    import a11oy_v4_agent as agent_mod

    votes = [
        {"voter_id": "deepseek-r1", "status": "unavailable", "text": None, "reason": "token_not_present"},
        {"voter_id": "kimi-k2", "status": "unavailable", "text": None, "reason": "token_not_present"},
    ]
    agg = agent_mod._lambda_aggregate(votes)
    assert agg["winner_id"] is None
    assert agg["lambda_score"] == 0.0


@pytest.mark.asyncio
async def test_lambda_aggregator_single_ok_vote():
    """Λ-aggregator with one ok vote returns that voter as winner."""
    import a11oy_v4_agent as agent_mod

    votes = [
        {"voter_id": "qwen-local", "status": "ok", "text": "Hello from qwen-local!", "reason": None},
        {"voter_id": "deepseek-r1", "status": "unavailable", "text": None, "reason": "token_not_present"},
    ]
    agg = agent_mod._lambda_aggregate(votes)
    assert agg["winner_id"] == "qwen-local"
    assert 0.0 < agg["lambda_score"] <= 1.0
    assert len(agg["provenance_entries"]) == 1


@pytest.mark.asyncio
async def test_qwen_local_returns_stub_when_vllm_unreachable():
    """qwen-local must return a stub (not raise) when local vLLM is unreachable."""
    qwen = get_voter("qwen-local")
    assert qwen is not None

    # Override QWEN_LOCAL_URL to an unreachable endpoint
    with patch.dict(os.environ, {"QWEN_LOCAL_URL": "http://127.0.0.1:19999/v1/chat/completions"}):
        result = await qwen.vote(prompt="test")

    # qwen-local never fails — it degrades to a stub
    assert result["status"] == "ok"
    assert result["text"] is not None
    assert "STUB" in result["text"] or "stub" in result["text"].lower() or len(result["text"]) > 0