feat(ai): add bounded model-aware decision fallback
Some checks failed
CD Pipeline / deploy (push) Has been cancelled
Some checks failed
CD Pipeline / deploy (push) Has been cancelled
This commit is contained in:
@@ -3,6 +3,8 @@ import json
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class _Response:
|
||||
def __init__(self, payload, status_code=200):
|
||||
@@ -19,12 +21,18 @@ class _Response:
|
||||
return self._payload
|
||||
|
||||
|
||||
def _tags(service, *, digest=None):
|
||||
def _tags(service, *, model=None, digest=None):
|
||||
resolved_model = model or service.NEMOTRON_OLLAMA_MODEL
|
||||
resolved_digest = digest or (
|
||||
service.NEMOTRON_OLLAMA_FALLBACK_EXPECTED_DIGEST
|
||||
if resolved_model == service.NEMOTRON_OLLAMA_FALLBACK_MODEL
|
||||
else service.NEMOTRON_OLLAMA_EXPECTED_DIGEST
|
||||
)
|
||||
return {
|
||||
"models": [{
|
||||
"name": service.NEMOTRON_OLLAMA_MODEL,
|
||||
"model": service.NEMOTRON_OLLAMA_MODEL,
|
||||
"digest": digest or service.NEMOTRON_OLLAMA_EXPECTED_DIGEST,
|
||||
"name": resolved_model,
|
||||
"model": resolved_model,
|
||||
"digest": resolved_digest,
|
||||
"details": {
|
||||
"parameter_size": "14.8B",
|
||||
"quantization_level": "Q4_K_M",
|
||||
@@ -33,6 +41,22 @@ def _tags(service, *, digest=None):
|
||||
}
|
||||
|
||||
|
||||
def _valid_canary_tool_call():
|
||||
return {
|
||||
"function": {
|
||||
"name": "trigger_price_alert",
|
||||
"arguments": {
|
||||
"sku": "CANARY-NEMO-001",
|
||||
"name": "NemoTron 受控決策驗證品",
|
||||
"gap_pct": 22.4,
|
||||
"sales_delta": -35.0,
|
||||
"action": "建立價格告警候選",
|
||||
"confidence": 0.91,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_nemotron_canary_readback_never_calls_network(tmp_path, monkeypatch):
|
||||
from services import nemotron_decision_canary_service as service
|
||||
|
||||
@@ -113,7 +137,8 @@ def test_nemotron_canary_executes_decision_only_on_verified_secondary(
|
||||
assert posted[0][0].endswith("/api/chat")
|
||||
assert posted[0][1]["keep_alive"] == 0
|
||||
assert posted[0][1]["think"] is False
|
||||
assert posted[0][2] == 45
|
||||
assert posted[0][1]["options"]["num_predict"] == 2048
|
||||
assert posted[0][2] == pytest.approx(45, abs=0.01)
|
||||
assert payload["latest_execution"]["canary_passed"] is True
|
||||
|
||||
acknowledged = service.acknowledge_nemotron_decision_canary(
|
||||
@@ -157,12 +182,263 @@ def test_nemotron_canary_blocks_model_digest_drift_before_model_call(
|
||||
|
||||
assert payload["status"] == "canary_failed"
|
||||
assert payload["execution"]["model_call_performed"] is False
|
||||
assert payload["execution"]["error"] == (
|
||||
"no_approved_gcp_host_with_expected_nemotron_digest"
|
||||
assert payload["execution"]["error"] == "all_approved_model_candidates_failed"
|
||||
assert payload["execution"]["attempt_count"] == 3
|
||||
assert all(
|
||||
attempt["status"] == "identity_failed"
|
||||
for attempt in payload["execution"]["runtime_attempts"]
|
||||
)
|
||||
assert model_calls == []
|
||||
|
||||
|
||||
def test_nemotron_canary_malformed_primary_tags_falls_through_secondary(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
from services import nemotron_decision_canary_service as service
|
||||
|
||||
def fake_get(url, timeout):
|
||||
if service.OLLAMA_HOST_PRIMARY in url:
|
||||
return _Response({
|
||||
"models": [{
|
||||
"name": service.NEMOTRON_OLLAMA_MODEL,
|
||||
"model": service.NEMOTRON_OLLAMA_MODEL,
|
||||
"digest": service.NEMOTRON_OLLAMA_EXPECTED_DIGEST,
|
||||
"details": [],
|
||||
}]
|
||||
})
|
||||
return _Response(_tags(service))
|
||||
|
||||
monkeypatch.setattr(service.requests, "get", fake_get)
|
||||
monkeypatch.setattr(
|
||||
service.requests,
|
||||
"post",
|
||||
lambda *_args, **_kwargs: _Response({
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [_valid_canary_tool_call()],
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
payload = service.run_nemotron_decision_canary(
|
||||
output_root=tmp_path,
|
||||
execute=True,
|
||||
write_receipt=False,
|
||||
timeout_sec=45,
|
||||
)
|
||||
|
||||
execution = payload["execution"]
|
||||
assert payload["status"] == "canary_passed"
|
||||
assert execution["selected_host"] == service.OLLAMA_HOST_SECONDARY
|
||||
assert execution["runtime_attempts"][0]["status"] == "identity_failed"
|
||||
assert execution["runtime_attempts"][0]["error"] == "malformed_model_details"
|
||||
|
||||
|
||||
def test_nemotron_canary_uses_exact_digest_111_fallback_after_gcp_timeout(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
import requests
|
||||
from services import nemotron_decision_canary_service as service
|
||||
from services.ollama_service import OLLAMA_HOST_FALLBACK
|
||||
|
||||
def fake_get(url, timeout):
|
||||
assert timeout == service.DEFAULT_MODEL_IDENTITY_TIMEOUT_SEC
|
||||
if service.OLLAMA_HOST_PRIMARY in url:
|
||||
raise requests.ConnectionError("primary unavailable")
|
||||
if service.OLLAMA_HOST_SECONDARY in url:
|
||||
return _Response(_tags(service))
|
||||
if OLLAMA_HOST_FALLBACK in url:
|
||||
return _Response(
|
||||
_tags(service, model=service.NEMOTRON_OLLAMA_FALLBACK_MODEL)
|
||||
)
|
||||
raise AssertionError(f"unexpected identity URL: {url}")
|
||||
|
||||
posted = []
|
||||
|
||||
def fake_post(url, json, timeout):
|
||||
posted.append((url, json, timeout))
|
||||
if service.OLLAMA_HOST_SECONDARY in url:
|
||||
raise requests.Timeout("secondary saturated")
|
||||
return _Response({
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [{
|
||||
"function": {
|
||||
"name": "trigger_price_alert",
|
||||
"arguments": {
|
||||
"sku": "CANARY-NEMO-001",
|
||||
"name": "NemoTron 受控決策驗證品",
|
||||
"gap_pct": 22.4,
|
||||
"sales_delta": -35.0,
|
||||
"action": "建立價格告警候選",
|
||||
"confidence": 0.91,
|
||||
},
|
||||
}
|
||||
}],
|
||||
},
|
||||
"prompt_eval_count": 100,
|
||||
"eval_count": 20,
|
||||
})
|
||||
|
||||
monkeypatch.setattr(service.requests, "get", fake_get)
|
||||
monkeypatch.setattr(service.requests, "post", fake_post)
|
||||
|
||||
payload = service.run_nemotron_decision_canary(
|
||||
output_root=tmp_path,
|
||||
execute=True,
|
||||
write_receipt=True,
|
||||
timeout_sec=90,
|
||||
run_id="nemotron-canary-fallback-test-001",
|
||||
)
|
||||
|
||||
assert payload["status"] == "canary_passed_degraded_fallback"
|
||||
execution = payload["execution"]
|
||||
assert execution["selected_host"] == OLLAMA_HOST_FALLBACK
|
||||
assert execution["model"] == service.NEMOTRON_OLLAMA_FALLBACK_MODEL
|
||||
assert execution["fallback_used"] is True
|
||||
assert execution["degraded_runtime"] is True
|
||||
assert execution["selected_candidate_label"] == "ollama_111_fallback"
|
||||
assert execution["attempt_count"] == 3
|
||||
assert posted[0][2] == 60
|
||||
assert posted[1][2] == 45
|
||||
assert posted[1][1]["model"] == service.NEMOTRON_OLLAMA_FALLBACK_MODEL
|
||||
assert posted[1][1]["options"]["num_ctx"] == 4096
|
||||
assert posted[1][1]["options"]["num_predict"] == 512
|
||||
assert payload["next_machine_action"] == "restore_gcp_decision_runtime_capacity"
|
||||
assert payload["latest_execution"]["fallback_used"] is True
|
||||
|
||||
|
||||
def test_nemotron_canary_timeout_is_one_total_deadline(tmp_path, monkeypatch):
|
||||
import requests
|
||||
from services import nemotron_decision_canary_service as service
|
||||
|
||||
clock = {"now": 50.0}
|
||||
monkeypatch.setattr(service.time, "monotonic", lambda: clock["now"])
|
||||
monkeypatch.setattr(
|
||||
service,
|
||||
"inspect_nemotron_model_identity",
|
||||
lambda candidate, **_kwargs: {
|
||||
**candidate.as_public_dict(),
|
||||
"ok": True,
|
||||
"digest": candidate.expected_digest,
|
||||
"digest_matches": True,
|
||||
"elapsed_ms": 0,
|
||||
"error": None,
|
||||
},
|
||||
)
|
||||
posted = []
|
||||
|
||||
def timeout_post(url, json, timeout):
|
||||
posted.append((url, timeout))
|
||||
clock["now"] += float(timeout)
|
||||
raise requests.Timeout("simulated timeout")
|
||||
|
||||
monkeypatch.setattr(service.requests, "post", timeout_post)
|
||||
|
||||
payload = service.run_nemotron_decision_canary(
|
||||
output_root=tmp_path,
|
||||
execute=True,
|
||||
write_receipt=False,
|
||||
timeout_sec=30,
|
||||
)
|
||||
|
||||
execution = payload["execution"]
|
||||
assert payload["status"] == "canary_failed"
|
||||
assert execution["error"] == "decision_total_deadline_exhausted"
|
||||
assert execution["deadline_exhausted"] is True
|
||||
assert execution["total_elapsed_ms"] == 30000
|
||||
assert len(posted) == 1
|
||||
assert posted[0][1] == 30
|
||||
assert execution["attempt_count"] == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("tool_calls", "error_fragment"),
|
||||
[
|
||||
(
|
||||
[
|
||||
_valid_canary_tool_call(),
|
||||
{
|
||||
"function": {
|
||||
"name": "delete_product",
|
||||
"arguments": {"sku": "CANARY-NEMO-001"},
|
||||
}
|
||||
},
|
||||
],
|
||||
"tool_call_count_mismatch",
|
||||
),
|
||||
(
|
||||
[_valid_canary_tool_call(), _valid_canary_tool_call()],
|
||||
"tool_call_count_mismatch",
|
||||
),
|
||||
(
|
||||
[{
|
||||
"function": {
|
||||
"name": "delete_product",
|
||||
"arguments": {"sku": "CANARY-NEMO-001"},
|
||||
}
|
||||
}],
|
||||
"unknown_tool",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_nemotron_canary_rejects_extra_duplicate_or_unknown_tool_calls(
|
||||
tmp_path, monkeypatch, tool_calls, error_fragment
|
||||
):
|
||||
from services import nemotron_decision_canary_service as service
|
||||
from services.ollama_service import OLLAMA_HOST_FALLBACK
|
||||
|
||||
def fake_get(url, timeout):
|
||||
model = (
|
||||
service.NEMOTRON_OLLAMA_FALLBACK_MODEL
|
||||
if OLLAMA_HOST_FALLBACK in url
|
||||
else service.NEMOTRON_OLLAMA_MODEL
|
||||
)
|
||||
return _Response(_tags(service, model=model))
|
||||
|
||||
monkeypatch.setattr(service.requests, "get", fake_get)
|
||||
monkeypatch.setattr(
|
||||
service.requests,
|
||||
"post",
|
||||
lambda *_args, **_kwargs: _Response({
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": tool_calls,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
payload = service.run_nemotron_decision_canary(
|
||||
output_root=tmp_path,
|
||||
execute=True,
|
||||
write_receipt=False,
|
||||
timeout_sec=90,
|
||||
)
|
||||
|
||||
execution = payload["execution"]
|
||||
assert payload["status"] == "canary_failed"
|
||||
assert execution["canary_passed"] is False
|
||||
assert execution["tool_execution_count"] == 0
|
||||
assert execution["database_call_performed"] is False
|
||||
model_attempts = [
|
||||
attempt
|
||||
for attempt in execution["runtime_attempts"]
|
||||
if attempt["model_call_performed"]
|
||||
]
|
||||
assert len(model_attempts) == 3
|
||||
assert all(
|
||||
attempt["checks"]["decision_contract_valid"] is False
|
||||
for attempt in model_attempts
|
||||
)
|
||||
assert all(
|
||||
error_fragment in str(attempt.get("contract_error") or "")
|
||||
for attempt in model_attempts
|
||||
)
|
||||
|
||||
|
||||
def test_nemotron_canary_route_is_read_only():
|
||||
from routes import system_public_routes as routes
|
||||
|
||||
|
||||
@@ -62,7 +62,10 @@ def test_send_telegram_attaches_decision_envelope_to_event_router(monkeypatch):
|
||||
"severity": "P2",
|
||||
"guardrails": {"can_auto_execute": False},
|
||||
}
|
||||
module.NemotronDispatcher()._send_telegram("hello price alert", decision_envelope=envelope)
|
||||
outcome = module.NemotronDispatcher()._send_telegram(
|
||||
"hello price alert",
|
||||
decision_envelope=envelope,
|
||||
)
|
||||
|
||||
assert len(captured) == 1
|
||||
event = captured[0]
|
||||
@@ -71,6 +74,25 @@ def test_send_telegram_attaches_decision_envelope_to_event_router(monkeypatch):
|
||||
assert event["decision_envelope"] == envelope
|
||||
assert event["payload"]["decision_envelope"] == envelope
|
||||
assert event["payload"]["raw_message"] == "hello price alert"
|
||||
assert outcome["durable"] is False
|
||||
|
||||
|
||||
def test_send_telegram_requires_delivery_or_durable_queue(monkeypatch):
|
||||
import services.nemoton_dispatcher_service as module
|
||||
|
||||
fake_event_router = types.ModuleType("services.event_router")
|
||||
fake_event_router.dispatch_sync = lambda event: {
|
||||
"delivered": False,
|
||||
"queued": False,
|
||||
"errors": ["telegram unavailable"],
|
||||
}
|
||||
monkeypatch.setitem(sys.modules, "services.event_router", fake_event_router)
|
||||
|
||||
outcome = module.NemotronDispatcher()._send_telegram("test")
|
||||
|
||||
assert outcome["durable"] is False
|
||||
assert outcome["delivered"] is False
|
||||
assert outcome["queued"] is False
|
||||
|
||||
|
||||
def test_exec_trigger_price_alert_persists_decision_envelope(monkeypatch):
|
||||
@@ -82,15 +104,18 @@ def test_exec_trigger_price_alert_persists_decision_envelope(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
dispatcher,
|
||||
"_send_telegram",
|
||||
lambda message, decision_envelope=None: sent.append((message, decision_envelope)),
|
||||
lambda message, decision_envelope=None: (
|
||||
sent.append((message, decision_envelope))
|
||||
or {"durable": True, "delivered": True, "queued": False}
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
dispatcher,
|
||||
"_sink_insight_to_km",
|
||||
lambda *args, **kwargs: stored.append((args, kwargs)),
|
||||
lambda *args, **kwargs: (stored.append((args, kwargs)) or True),
|
||||
)
|
||||
|
||||
dispatcher._exec_trigger_price_alert(
|
||||
outcome = dispatcher._exec_trigger_price_alert(
|
||||
sku="SKU-1",
|
||||
name="測試商品",
|
||||
gap_pct=12.5,
|
||||
@@ -115,3 +140,4 @@ def test_exec_trigger_price_alert_persists_decision_envelope(monkeypatch):
|
||||
assert envelope["subject"]["competitor_product_id"] == "PC-1"
|
||||
assert stored
|
||||
assert stored[0][1]["metadata"]["decision_envelope"] == envelope
|
||||
assert outcome["durable"] is True
|
||||
|
||||
281
tests/test_nemotron_dispatch_reservation_service.py
Normal file
281
tests/test_nemotron_dispatch_reservation_service.py
Normal file
@@ -0,0 +1,281 @@
|
||||
import json
|
||||
import multiprocessing
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def _process_reserve(state_path, start_event, result_queue):
|
||||
from services.nemotron_dispatch_reservation_service import (
|
||||
SharedFileReservationStore,
|
||||
)
|
||||
|
||||
store = SharedFileReservationStore(
|
||||
state_path,
|
||||
committed_ttl_sec=3600,
|
||||
lease_ttl_sec=60,
|
||||
)
|
||||
start_event.wait(timeout=5)
|
||||
result_queue.put(store.reserve("SKU-PROCESS-SHARED"))
|
||||
|
||||
|
||||
def test_shared_store_coordinates_independent_instances_and_ttl(tmp_path):
|
||||
from services.nemotron_dispatch_reservation_service import (
|
||||
SharedFileReservationStore,
|
||||
)
|
||||
|
||||
clock = {"now": 1000.0}
|
||||
path = tmp_path / "dedupe" / "state.json"
|
||||
first = SharedFileReservationStore(
|
||||
path,
|
||||
committed_ttl_sec=100,
|
||||
lease_ttl_sec=20,
|
||||
clock=lambda: clock["now"],
|
||||
)
|
||||
second = SharedFileReservationStore(
|
||||
path,
|
||||
committed_ttl_sec=100,
|
||||
lease_ttl_sec=20,
|
||||
clock=lambda: clock["now"],
|
||||
)
|
||||
|
||||
token = first.reserve("SKU-SHARED")
|
||||
assert token
|
||||
assert second.reserve("SKU-SHARED") is None
|
||||
assert second.is_duplicate("SKU-SHARED") is True
|
||||
assert "SKU-SHARED" not in path.read_text(encoding="utf-8")
|
||||
assert second.release("SKU-SHARED", "stale-token") is False
|
||||
assert first.refresh("SKU-SHARED", token) is True
|
||||
assert first.commit("SKU-SHARED", token) is True
|
||||
assert second.reserve("SKU-SHARED") is None
|
||||
|
||||
clock["now"] += 101
|
||||
next_token = second.reserve("SKU-SHARED")
|
||||
assert next_token and next_token != token
|
||||
assert second.release("SKU-SHARED", next_token) is True
|
||||
|
||||
|
||||
def test_begin_side_effect_persists_extended_quarantine(tmp_path):
|
||||
from services.nemotron_dispatch_reservation_service import (
|
||||
SharedFileReservationStore,
|
||||
)
|
||||
|
||||
clock = {"now": 1000.0}
|
||||
path = tmp_path / "state.json"
|
||||
first = SharedFileReservationStore(
|
||||
path,
|
||||
committed_ttl_sec=100,
|
||||
lease_ttl_sec=20,
|
||||
clock=lambda: clock["now"],
|
||||
)
|
||||
second = SharedFileReservationStore(
|
||||
path,
|
||||
committed_ttl_sec=100,
|
||||
lease_ttl_sec=20,
|
||||
clock=lambda: clock["now"],
|
||||
)
|
||||
|
||||
token = first.reserve("SKU-SIDE-EFFECT")
|
||||
assert token
|
||||
assert first.begin_side_effect("SKU-SIDE-EFFECT", token) is True
|
||||
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
lease = next(iter(payload["leases"].values()))
|
||||
assert lease == {
|
||||
"token": token,
|
||||
"expires_at": 1100.0,
|
||||
"phase": "side_effect_started",
|
||||
}
|
||||
|
||||
clock["now"] += 21
|
||||
assert second.reserve("SKU-SIDE-EFFECT") is None
|
||||
clock["now"] += 80
|
||||
assert second.reserve("SKU-SIDE-EFFECT")
|
||||
|
||||
|
||||
def test_shared_store_is_atomic_across_processes(tmp_path):
|
||||
state_path = str(tmp_path / "process" / "state.json")
|
||||
context = multiprocessing.get_context("fork")
|
||||
start_event = context.Event()
|
||||
result_queue = context.Queue()
|
||||
processes = [
|
||||
context.Process(
|
||||
target=_process_reserve,
|
||||
args=(state_path, start_event, result_queue),
|
||||
)
|
||||
for _index in range(6)
|
||||
]
|
||||
for process in processes:
|
||||
process.start()
|
||||
start_event.set()
|
||||
tokens = [result_queue.get(timeout=10) for _process in processes]
|
||||
for process in processes:
|
||||
process.join(timeout=10)
|
||||
assert process.exitcode == 0
|
||||
|
||||
owners = [token for token in tokens if token]
|
||||
assert len(owners) == 1
|
||||
|
||||
|
||||
def test_shared_store_corruption_fails_closed(tmp_path):
|
||||
from services.nemotron_dispatch_reservation_service import (
|
||||
SharedFileReservationStore,
|
||||
)
|
||||
|
||||
path = tmp_path / "state.json"
|
||||
path.write_text("{not-json", encoding="utf-8")
|
||||
store = SharedFileReservationStore(
|
||||
path,
|
||||
committed_ttl_sec=3600,
|
||||
lease_ttl_sec=900,
|
||||
)
|
||||
|
||||
assert store.reserve("SKU-CORRUPT") is None
|
||||
assert store.refresh("SKU-CORRUPT", "token") is False
|
||||
assert store.commit("SKU-CORRUPT", "token") is False
|
||||
assert store.release("SKU-CORRUPT", "token") is False
|
||||
assert store.is_duplicate("SKU-CORRUPT") is True
|
||||
assert path.read_text(encoding="utf-8") == "{not-json"
|
||||
|
||||
|
||||
def test_atomic_write_fsyncs_file_and_parent_directory(tmp_path, monkeypatch):
|
||||
from services import nemotron_dispatch_reservation_service as service
|
||||
|
||||
fsynced = []
|
||||
real_fsync = service.os.fsync
|
||||
|
||||
def record_fsync(fd):
|
||||
fsynced.append(fd)
|
||||
return real_fsync(fd)
|
||||
|
||||
monkeypatch.setattr(service.os, "fsync", record_fsync)
|
||||
store = service.SharedFileReservationStore(
|
||||
tmp_path / "nested" / "state.json",
|
||||
committed_ttl_sec=3600,
|
||||
lease_ttl_sec=900,
|
||||
)
|
||||
|
||||
assert store.reserve("SKU-FSYNC")
|
||||
assert len(fsynced) >= 2
|
||||
|
||||
|
||||
def test_missing_process_shared_lock_fails_closed(monkeypatch, tmp_path):
|
||||
from services import nemotron_dispatch_reservation_service as service
|
||||
|
||||
monkeypatch.setattr(service, "fcntl", None)
|
||||
monkeypatch.delenv("FLASK_ENV", raising=False)
|
||||
monkeypatch.delenv("USE_POSTGRESQL", raising=False)
|
||||
|
||||
assert service.build_production_shared_reservation_store(
|
||||
committed_ttl_sec=3600,
|
||||
lease_ttl_sec=900,
|
||||
) is None
|
||||
|
||||
store = service.SharedFileReservationStore(
|
||||
tmp_path / "state.json",
|
||||
committed_ttl_sec=3600,
|
||||
lease_ttl_sec=900,
|
||||
)
|
||||
assert store.reserve("SKU-NO-FCNTL") is None
|
||||
assert store.is_duplicate("SKU-NO-FCNTL") is True
|
||||
|
||||
|
||||
def test_production_builder_forces_canonical_shared_data_path(monkeypatch):
|
||||
from services import nemotron_dispatch_reservation_service as service
|
||||
|
||||
monkeypatch.setenv("FLASK_ENV", "production")
|
||||
store = service.build_production_shared_reservation_store(
|
||||
committed_ttl_sec=3600,
|
||||
lease_ttl_sec=900,
|
||||
)
|
||||
|
||||
assert store is not None
|
||||
assert store.state_path == Path(
|
||||
"/app/data/ai_automation/nemotron_dispatch_dedupe_state.json"
|
||||
)
|
||||
assert store.lock_path == Path(
|
||||
"/app/data/ai_automation/nemotron_dispatch_dedupe_state.json.lock"
|
||||
)
|
||||
|
||||
|
||||
def test_postgres_runtime_cannot_fall_back_to_process_local_memory(monkeypatch):
|
||||
from services import nemotron_dispatch_reservation_service as service
|
||||
|
||||
monkeypatch.delenv("FLASK_ENV", raising=False)
|
||||
monkeypatch.setenv("USE_POSTGRESQL", "true")
|
||||
|
||||
assert service.production_shared_reservation_required() is True
|
||||
assert service.build_production_shared_reservation_store(
|
||||
committed_ttl_sec=3600,
|
||||
lease_ttl_sec=900,
|
||||
) is not None
|
||||
|
||||
|
||||
def test_shared_state_contains_only_hashed_sku_and_timing_metadata(tmp_path):
|
||||
from services.nemotron_dispatch_reservation_service import (
|
||||
SharedFileReservationStore,
|
||||
)
|
||||
|
||||
path = tmp_path / "state.json"
|
||||
store = SharedFileReservationStore(
|
||||
path,
|
||||
committed_ttl_sec=3600,
|
||||
lease_ttl_sec=900,
|
||||
)
|
||||
token = store.reserve("RAW-SKU-MUST-NOT-APPEAR")
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
assert token
|
||||
assert "RAW-SKU-MUST-NOT-APPEAR" not in path.read_text(encoding="utf-8")
|
||||
assert set(payload) == {"version", "leases", "committed"}
|
||||
assert len(next(iter(payload["leases"]))) == 64
|
||||
|
||||
|
||||
def test_production_containers_share_data_mount_and_force_shared_backend():
|
||||
compose = (
|
||||
Path(__file__).resolve().parents[1] / "docker-compose.yml"
|
||||
).read_text(encoding="utf-8")
|
||||
|
||||
assert compose.count("- ./data:/app/data") >= 3
|
||||
assert compose.count("- FLASK_ENV=production") >= 3
|
||||
|
||||
|
||||
def test_shared_reservation_canary_has_no_business_side_effects(tmp_path):
|
||||
from services.nemotron_dispatch_reservation_service import (
|
||||
run_shared_reservation_canary,
|
||||
)
|
||||
|
||||
payload = run_shared_reservation_canary(
|
||||
state_path=tmp_path / "canary" / "state.json",
|
||||
)
|
||||
|
||||
assert payload["success"] is True
|
||||
assert payload["check_pass_count"] == payload["check_count"] == 6
|
||||
assert payload["controlled_apply"] == {
|
||||
"writes_shared_reservation_state": True,
|
||||
"terminal_state_clear": True,
|
||||
"writes_database": False,
|
||||
"sends_telegram": False,
|
||||
"executes_business_tool": False,
|
||||
"reads_secret": False,
|
||||
}
|
||||
|
||||
|
||||
def test_shared_reservation_canary_cli_is_machine_readable(tmp_path):
|
||||
completed = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"scripts/ops/run_nemotron_dispatch_reservation_canary.py",
|
||||
"--state-path",
|
||||
str(tmp_path / "cli" / "state.json"),
|
||||
],
|
||||
cwd=Path(__file__).resolve().parents[1],
|
||||
capture_output=True,
|
||||
check=False,
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert completed.returncode == 0, completed.stderr
|
||||
payload = json.loads(completed.stdout)
|
||||
assert payload["status"] == "shared_reservation_canary_passed"
|
||||
assert payload["controlled_apply"]["writes_database"] is False
|
||||
@@ -9,9 +9,11 @@ def _force_legacy_nim_first(monkeypatch):
|
||||
import services.nemoton_dispatcher_service as module
|
||||
|
||||
module._ALERT_CACHE.clear()
|
||||
module._ALERT_INFLIGHT.clear()
|
||||
monkeypatch.setattr(module, "NEMOTRON_OLLAMA_FIRST", False)
|
||||
yield
|
||||
module._ALERT_CACHE.clear()
|
||||
module._ALERT_INFLIGHT.clear()
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -33,6 +35,7 @@ def _patch_execution_methods(monkeypatch, dispatcher):
|
||||
def record(kind):
|
||||
def _inner(*args, **kwargs):
|
||||
calls.append({"kind": kind, "args": args, "kwargs": kwargs})
|
||||
return {"durable": True}
|
||||
return _inner
|
||||
|
||||
monkeypatch.setattr(dispatcher, "_exec_trigger_price_alert", record("price_alert"))
|
||||
@@ -56,6 +59,207 @@ def test_dispatch_falls_back_to_hermes_rules_without_nim_api_key(monkeypatch):
|
||||
assert calls[0]["kind"] == "price_alert"
|
||||
|
||||
|
||||
def test_forced_review_failure_is_counted_as_skipped(monkeypatch):
|
||||
import services.nemoton_dispatcher_service as module
|
||||
|
||||
monkeypatch.setattr(module, "NIM_API_KEY", "")
|
||||
dispatcher = module.NemotronDispatcher()
|
||||
monkeypatch.setattr(
|
||||
dispatcher,
|
||||
"_exec_flag_for_human_review",
|
||||
lambda *_args, **_kwargs: {"durable": False},
|
||||
)
|
||||
threat = FakeThreat("SKU-FORCED", "斷崖品")
|
||||
threat.sales_7d_delta_pct = -100.0
|
||||
|
||||
result = dispatcher.dispatch([threat], hermes_stats={"duration_sec": 1})
|
||||
|
||||
assert result["dispatched"] == 0
|
||||
assert result["skipped"] == 1
|
||||
assert len(result["errors"]) == 1
|
||||
assert "durable_side_effect_not_confirmed" in result["errors"][0]
|
||||
|
||||
|
||||
def test_hermes_failure_is_counted_as_skipped(monkeypatch):
|
||||
import services.nemoton_dispatcher_service as module
|
||||
|
||||
monkeypatch.setattr(module, "NIM_API_KEY", "")
|
||||
dispatcher = module.NemotronDispatcher()
|
||||
monkeypatch.setattr(
|
||||
dispatcher,
|
||||
"_exec_trigger_price_alert",
|
||||
lambda *_args, **_kwargs: {"durable": False},
|
||||
)
|
||||
|
||||
result = dispatcher.dispatch(
|
||||
[FakeThreat("SKU-HERMES-FAIL", "告警品")],
|
||||
hermes_stats={"duration_sec": 1},
|
||||
)
|
||||
|
||||
assert result["dispatched"] == 0
|
||||
assert result["skipped"] == 1
|
||||
assert len(result["errors"]) == 1
|
||||
assert "durable_side_effect_not_confirmed" in result["errors"][0]
|
||||
|
||||
|
||||
def test_forced_review_commit_failure_keeps_durable_result_quarantined(monkeypatch):
|
||||
import services.nemoton_dispatcher_service as module
|
||||
|
||||
monkeypatch.setattr(module, "NIM_API_KEY", "")
|
||||
dispatcher = module.NemotronDispatcher()
|
||||
monkeypatch.setattr(
|
||||
dispatcher,
|
||||
"_exec_flag_for_human_review",
|
||||
lambda *_args, **_kwargs: {"durable": True},
|
||||
)
|
||||
monkeypatch.setattr(module, "_mark_alert_dispatched", lambda *_args: False)
|
||||
released = []
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"_release_alert_reservation",
|
||||
lambda *args: (released.append(args) or True),
|
||||
)
|
||||
threat = FakeThreat("SKU-FORCED-COMMIT", "斷崖品")
|
||||
threat.sales_7d_delta_pct = -100.0
|
||||
|
||||
result = dispatcher.dispatch([threat], hermes_stats={"duration_sec": 1})
|
||||
|
||||
assert result["dispatched"] == 1
|
||||
assert result["skipped"] == 0
|
||||
assert result["errors"] == [
|
||||
"dedupe_commit_unverified:flag_for_human_review(SKU-FORCED-COMMIT)"
|
||||
]
|
||||
assert released == []
|
||||
|
||||
|
||||
def test_hermes_commit_failure_keeps_durable_result_quarantined(monkeypatch):
|
||||
import services.nemoton_dispatcher_service as module
|
||||
|
||||
monkeypatch.setattr(module, "NIM_API_KEY", "")
|
||||
dispatcher = module.NemotronDispatcher()
|
||||
_patch_execution_methods(monkeypatch, dispatcher)
|
||||
monkeypatch.setattr(module, "_mark_alert_dispatched", lambda *_args: False)
|
||||
released = []
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"_release_alert_reservation",
|
||||
lambda *args: (released.append(args) or True),
|
||||
)
|
||||
|
||||
result = dispatcher.dispatch(
|
||||
[FakeThreat("SKU-HERMES-COMMIT", "告警品")],
|
||||
hermes_stats={"duration_sec": 1},
|
||||
)
|
||||
|
||||
assert result["dispatched"] == 1
|
||||
assert result["skipped"] == 0
|
||||
assert result["errors"] == [
|
||||
"dedupe_commit_unverified:hermes_rule_fallback(SKU-HERMES-COMMIT)"
|
||||
]
|
||||
assert released == []
|
||||
|
||||
|
||||
def test_mixed_batch_cleanup_never_releases_forced_durable_quarantine(monkeypatch):
|
||||
import services.nemoton_dispatcher_service as module
|
||||
|
||||
monkeypatch.setattr(module, "NIM_API_KEY", "test-key")
|
||||
monkeypatch.setattr(module, "_check_nim_quota", lambda: True)
|
||||
dispatcher = module.NemotronDispatcher()
|
||||
_patch_execution_methods(monkeypatch, dispatcher)
|
||||
forced = FakeThreat("SKU-FORCED-MIXED", "斷崖品")
|
||||
forced.sales_7d_delta_pct = -100.0
|
||||
candidate = FakeThreat("SKU-NIM-MIXED", "模型候選")
|
||||
monkeypatch.setattr(
|
||||
dispatcher,
|
||||
"_call_nim",
|
||||
lambda _threats, **_kwargs: (
|
||||
[{
|
||||
"tool": "trigger_price_alert",
|
||||
"args": {
|
||||
"sku": candidate.sku,
|
||||
"name": candidate.name,
|
||||
"gap_pct": candidate.gap_pct,
|
||||
"sales_delta": candidate.sales_7d_delta_pct,
|
||||
"action": candidate.recommended_action,
|
||||
"confidence": candidate.confidence,
|
||||
},
|
||||
}],
|
||||
{"provider": "nim", "total_tokens": 1},
|
||||
),
|
||||
)
|
||||
real_mark = module._mark_alert_dispatched
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"_mark_alert_dispatched",
|
||||
lambda sku, token: (
|
||||
False if str(sku) == forced.sku else real_mark(sku, token)
|
||||
),
|
||||
)
|
||||
|
||||
result = dispatcher.dispatch(
|
||||
[forced, candidate],
|
||||
hermes_stats={"duration_sec": 1},
|
||||
)
|
||||
|
||||
assert result["dispatched"] == 2
|
||||
assert result["skipped"] == 0
|
||||
assert result["errors"] == [
|
||||
"dedupe_commit_unverified:flag_for_human_review(SKU-FORCED-MIXED)"
|
||||
]
|
||||
assert module._is_duplicate_alert(forced.sku) is True
|
||||
assert module._is_duplicate_alert(candidate.sku) is True
|
||||
|
||||
|
||||
def test_mixed_batch_counts_failed_model_candidate_as_skipped(monkeypatch):
|
||||
import services.nemoton_dispatcher_service as module
|
||||
|
||||
monkeypatch.setattr(module, "NIM_API_KEY", "test-key")
|
||||
monkeypatch.setattr(module, "_check_nim_quota", lambda: True)
|
||||
dispatcher = module.NemotronDispatcher()
|
||||
forced = FakeThreat("SKU-FORCED-COUNT", "斷崖品")
|
||||
forced.sales_7d_delta_pct = -100.0
|
||||
candidate = FakeThreat("SKU-NIM-COUNT", "模型候選")
|
||||
monkeypatch.setattr(
|
||||
dispatcher,
|
||||
"_exec_flag_for_human_review",
|
||||
lambda *_args, **_kwargs: {"durable": True},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
dispatcher,
|
||||
"_exec_trigger_price_alert",
|
||||
lambda *_args, **_kwargs: {"durable": False},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
dispatcher,
|
||||
"_call_nim",
|
||||
lambda _threats, **_kwargs: (
|
||||
[{
|
||||
"tool": "trigger_price_alert",
|
||||
"args": {
|
||||
"sku": candidate.sku,
|
||||
"name": candidate.name,
|
||||
"gap_pct": candidate.gap_pct,
|
||||
"sales_delta": candidate.sales_7d_delta_pct,
|
||||
"action": candidate.recommended_action,
|
||||
"confidence": candidate.confidence,
|
||||
},
|
||||
}],
|
||||
{"provider": "nim", "total_tokens": 1},
|
||||
),
|
||||
)
|
||||
|
||||
result = dispatcher.dispatch(
|
||||
[forced, candidate],
|
||||
hermes_stats={"duration_sec": 1},
|
||||
)
|
||||
|
||||
assert result["dispatched"] == 1
|
||||
assert result["skipped"] == 1
|
||||
assert len(result["errors"]) == 1
|
||||
assert "durable_side_effect_not_confirmed" in result["errors"][0]
|
||||
assert result["dispatched"] + result["skipped"] == 2
|
||||
|
||||
|
||||
def test_dispatch_routes_non_exact_match_to_human_review(monkeypatch):
|
||||
import services.nemoton_dispatcher_service as module
|
||||
|
||||
@@ -83,7 +287,7 @@ def test_dispatch_falls_back_to_hermes_rules_on_nim_timeout(monkeypatch):
|
||||
monkeypatch.setattr(module, "_check_nim_quota", lambda: True)
|
||||
dispatcher = module.NemotronDispatcher()
|
||||
calls = _patch_execution_methods(monkeypatch, dispatcher)
|
||||
monkeypatch.setattr(dispatcher, "_call_nim", lambda threats: (_ for _ in ()).throw(requests.Timeout("timeout")))
|
||||
monkeypatch.setattr(dispatcher, "_call_nim", lambda threats, **_kwargs: (_ for _ in ()).throw(requests.Timeout("timeout")))
|
||||
|
||||
result = dispatcher.dispatch([FakeThreat("SKU-2", "測試品")], hermes_stats={"duration_sec": 1})
|
||||
|
||||
@@ -101,7 +305,7 @@ def test_dispatch_falls_back_to_hermes_rules_on_zero_tool_calls(monkeypatch):
|
||||
monkeypatch.setattr(module, "_check_nim_quota", lambda: True)
|
||||
dispatcher = module.NemotronDispatcher()
|
||||
calls = _patch_execution_methods(monkeypatch, dispatcher)
|
||||
monkeypatch.setattr(dispatcher, "_call_nim", lambda threats: ([], {"total_tokens": 10}))
|
||||
monkeypatch.setattr(dispatcher, "_call_nim", lambda threats, **_kwargs: ([], {"total_tokens": 10}))
|
||||
|
||||
result = dispatcher.dispatch([FakeThreat("SKU-3", "測試品")], hermes_stats={"duration_sec": 1})
|
||||
|
||||
|
||||
@@ -39,6 +39,20 @@ class FakeThreat:
|
||||
sales_7d_prev_amount: float = 120000.0
|
||||
|
||||
|
||||
def _valid_price_alert_call(sku="SKU-Q1"):
|
||||
return {
|
||||
"tool": "trigger_price_alert",
|
||||
"args": {
|
||||
"sku": sku,
|
||||
"name": "qwen3 測試品",
|
||||
"gap_pct": 22.4,
|
||||
"sales_delta": -35.0,
|
||||
"action": "跟進降價",
|
||||
"confidence": 0.85,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class _FakeResp:
|
||||
def __init__(self, payload: dict, status: int = 200):
|
||||
self._payload = payload
|
||||
@@ -59,6 +73,7 @@ def _noop_log_ai_call(*args, **kwargs):
|
||||
class _Ctx:
|
||||
def set_tokens(self, **_kw): pass
|
||||
def set_provider(self, *_a, **_kw): pass
|
||||
def set_model(self, *_a, **_kw): pass
|
||||
def set_error(self, *_a, **_kw): pass
|
||||
def fallback_to_caller(self, *_a, **_kw): pass
|
||||
def set_cache_hit(self, *_a, **_kw): pass
|
||||
@@ -76,11 +91,13 @@ def _reset_global_state():
|
||||
import services.nemoton_dispatcher_service as _nem
|
||||
import services.ollama_service as _oss
|
||||
_nem._ALERT_CACHE.clear()
|
||||
_nem._ALERT_INFLIGHT.clear()
|
||||
_oss._unhealthy_marks.clear()
|
||||
_oss._resolved_host_cache['host'] = None
|
||||
_oss._resolved_host_cache['ts'] = 0
|
||||
yield
|
||||
_nem._ALERT_CACHE.clear()
|
||||
_nem._ALERT_INFLIGHT.clear()
|
||||
_oss._unhealthy_marks.clear()
|
||||
_oss._resolved_host_cache['host'] = None
|
||||
_oss._resolved_host_cache['ts'] = 0
|
||||
@@ -93,6 +110,7 @@ def _patch_execution_methods(monkeypatch, dispatcher):
|
||||
def record(kind):
|
||||
def _inner(*args, **kwargs):
|
||||
calls.append({"kind": kind, "args": args, "kwargs": kwargs})
|
||||
return {"durable": True}
|
||||
return _inner
|
||||
|
||||
monkeypatch.setattr(dispatcher, "_exec_trigger_price_alert", record("price_alert"))
|
||||
@@ -102,14 +120,27 @@ def _patch_execution_methods(monkeypatch, dispatcher):
|
||||
|
||||
|
||||
def _enable_qwen3_path(monkeypatch, module):
|
||||
"""打開 NEMOTRON_OLLAMA_FIRST + 旁路 mcp/log_ai_call/resolve_host 等副作用"""
|
||||
"""打開 Ollama-first,並隔離 MCP、logger 與 model identity 網路副作用。"""
|
||||
monkeypatch.setattr(module, "NEMOTRON_OLLAMA_FIRST", True)
|
||||
monkeypatch.setattr(module, "log_ai_call", _noop_log_ai_call)
|
||||
monkeypatch.setattr(module, "build_mcp_context", lambda: "MCP-MOCK")
|
||||
# 確保即使未被呼叫,import 路徑可解析
|
||||
import services.ollama_service as ollama_module
|
||||
import services.nemotron_runtime_candidate_service as runtime_module
|
||||
|
||||
monkeypatch.setattr(ollama_module, "resolve_ollama_host", lambda: "http://34.87.90.216:11434")
|
||||
monkeypatch.setattr(ollama_module, "mark_unhealthy", lambda *a, **kw: None)
|
||||
monkeypatch.setattr(
|
||||
runtime_module,
|
||||
"inspect_nemotron_model_identity",
|
||||
lambda candidate, **_kwargs: {
|
||||
**candidate.as_public_dict(),
|
||||
"ok": True,
|
||||
"digest": candidate.expected_digest,
|
||||
"digest_matches": True,
|
||||
"error": None,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
@@ -178,12 +209,7 @@ def test_qwen3_retries_secondary_when_primary_chat_fails(monkeypatch):
|
||||
import services.ollama_service as ollama_module
|
||||
|
||||
_enable_qwen3_path(monkeypatch, module)
|
||||
hosts = iter([
|
||||
"http://34.87.90.216:11434",
|
||||
"http://34.21.145.224:11434",
|
||||
])
|
||||
marked = []
|
||||
monkeypatch.setattr(ollama_module, "resolve_ollama_host", lambda: next(hosts))
|
||||
monkeypatch.setattr(ollama_module, "mark_unhealthy", lambda host: marked.append(host))
|
||||
|
||||
fake_body = {
|
||||
@@ -227,6 +253,519 @@ def test_qwen3_retries_secondary_when_primary_chat_fails(monkeypatch):
|
||||
assert calls[0]["kind"] == "price_alert"
|
||||
|
||||
|
||||
def test_qwen3_uses_bounded_111_model_after_both_gcp_candidates_fail(monkeypatch):
|
||||
"""兩台 GCP 都逾時時,正式 dispatcher 應改用 exact-digest 111 qwen3:8b。"""
|
||||
import requests
|
||||
import services.nemoton_dispatcher_service as module
|
||||
|
||||
_enable_qwen3_path(monkeypatch, module)
|
||||
posted = []
|
||||
fake_body = {
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [{
|
||||
"function": {
|
||||
"name": "trigger_price_alert",
|
||||
"arguments": {
|
||||
"sku": "SKU-Q1",
|
||||
"name": "qwen3 測試品",
|
||||
"gap_pct": 22.4,
|
||||
"sales_delta": -35.0,
|
||||
"action": "跟進降價",
|
||||
"confidence": 0.85,
|
||||
},
|
||||
}
|
||||
}],
|
||||
},
|
||||
"prompt_eval_count": 180,
|
||||
"eval_count": 36,
|
||||
}
|
||||
|
||||
def fake_post(url, json, timeout):
|
||||
posted.append((url, json, timeout))
|
||||
if len(posted) <= 2:
|
||||
raise requests.Timeout("GCP saturated")
|
||||
return _FakeResp(fake_body)
|
||||
|
||||
monkeypatch.setattr(module.requests, "post", fake_post)
|
||||
dispatcher = module.NemotronDispatcher()
|
||||
calls = _patch_execution_methods(monkeypatch, dispatcher)
|
||||
|
||||
result = dispatcher.dispatch([FakeThreat()], hermes_stats={"duration_sec": 1.0})
|
||||
|
||||
stats = result["nim_stats"]
|
||||
assert result["dispatched"] == 1
|
||||
assert stats["provider"] == "ollama_111"
|
||||
assert stats["model"] == "qwen3:8b"
|
||||
assert stats["candidate_label"] == "ollama_111_fallback"
|
||||
assert stats["fallback_used"] is True
|
||||
assert [attempt["status"] for attempt in stats["attempted_candidates"]] == [
|
||||
"failed",
|
||||
"failed",
|
||||
"selected",
|
||||
]
|
||||
assert posted[2][1]["think"] is False
|
||||
assert posted[2][1]["options"]["num_predict"] == 512
|
||||
assert posted[2][2] == 45
|
||||
assert calls[0]["kind"] == "price_alert"
|
||||
assert "qwen3:8b (111 備援)" in calls[0]["kwargs"]["footprint"]
|
||||
assert "降級備援" in calls[0]["kwargs"]["footprint"]
|
||||
|
||||
structured = module._build_footprint_json(None, stats)
|
||||
assert structured["dispatcher"]["platform"] == "Ollama"
|
||||
assert structured["dispatcher"]["model"] == "qwen3:8b"
|
||||
assert structured["dispatcher"]["fallback_used"] is True
|
||||
assert [
|
||||
attempt["label"]
|
||||
for attempt in structured["dispatcher"]["candidate_attempts"]
|
||||
] == ["gcp_primary", "gcp_secondary", "ollama_111_fallback"]
|
||||
assert all(
|
||||
"host" not in attempt and "error" not in attempt
|
||||
for attempt in structured["dispatcher"]["candidate_attempts"]
|
||||
)
|
||||
|
||||
|
||||
def test_invalid_primary_tool_contract_retries_verified_secondary(monkeypatch):
|
||||
import services.nemoton_dispatcher_service as module
|
||||
|
||||
_enable_qwen3_path(monkeypatch, module)
|
||||
valid_body = {
|
||||
"message": {
|
||||
"tool_calls": [{"function": {
|
||||
"name": "trigger_price_alert",
|
||||
"arguments": _valid_price_alert_call()["args"],
|
||||
}}],
|
||||
"content": "",
|
||||
},
|
||||
"prompt_eval_count": 100,
|
||||
"eval_count": 20,
|
||||
}
|
||||
invalid_body = {
|
||||
"message": {
|
||||
"tool_calls": [{"function": {
|
||||
"name": "delete_product",
|
||||
"arguments": {"sku": "SKU-Q1"},
|
||||
}}],
|
||||
"content": "",
|
||||
},
|
||||
}
|
||||
responses = [_FakeResp(invalid_body), _FakeResp(valid_body)]
|
||||
monkeypatch.setattr(
|
||||
module.requests,
|
||||
"post",
|
||||
lambda *_args, **_kwargs: responses.pop(0),
|
||||
)
|
||||
dispatcher = module.NemotronDispatcher()
|
||||
calls = _patch_execution_methods(monkeypatch, dispatcher)
|
||||
|
||||
result = dispatcher.dispatch([FakeThreat()], hermes_stats={"duration_sec": 1.0})
|
||||
|
||||
attempts = result["nim_stats"]["attempted_candidates"]
|
||||
assert [attempt["status"] for attempt in attempts] == ["failed", "selected"]
|
||||
assert "unknown_tool:delete_product" in attempts[0]["error"]
|
||||
assert result["nim_stats"]["provider"] == "ollama_secondary"
|
||||
assert result["dispatched"] == 1
|
||||
assert calls[0]["kind"] == "price_alert"
|
||||
|
||||
|
||||
def test_malformed_primary_tags_schema_falls_through_to_verified_secondary(
|
||||
monkeypatch,
|
||||
):
|
||||
import services.nemoton_dispatcher_service as module
|
||||
import services.nemotron_runtime_candidate_service as runtime_module
|
||||
import services.ollama_service as ollama_module
|
||||
|
||||
monkeypatch.setattr(module, "NEMOTRON_OLLAMA_FIRST", True)
|
||||
monkeypatch.setattr(module, "log_ai_call", _noop_log_ai_call)
|
||||
monkeypatch.setattr(module, "build_mcp_context", lambda: "MCP-MOCK")
|
||||
monkeypatch.setattr(ollama_module, "mark_unhealthy", lambda *_args: None)
|
||||
|
||||
def fake_get(url, timeout):
|
||||
if runtime_module.OLLAMA_HOST_PRIMARY in url:
|
||||
return _FakeResp({
|
||||
"models": [
|
||||
None,
|
||||
{
|
||||
"name": runtime_module.PRIMARY_MODEL,
|
||||
"model": runtime_module.PRIMARY_MODEL,
|
||||
"digest": runtime_module.PRIMARY_EXPECTED_DIGEST,
|
||||
"details": {},
|
||||
},
|
||||
]
|
||||
})
|
||||
return _FakeResp({
|
||||
"models": [{
|
||||
"name": runtime_module.PRIMARY_MODEL,
|
||||
"model": runtime_module.PRIMARY_MODEL,
|
||||
"digest": runtime_module.PRIMARY_EXPECTED_DIGEST,
|
||||
"details": {
|
||||
"parameter_size": "14.8B",
|
||||
"quantization_level": "Q4_K_M",
|
||||
},
|
||||
}]
|
||||
})
|
||||
|
||||
valid_body = {
|
||||
"message": {
|
||||
"tool_calls": [{"function": {
|
||||
"name": "trigger_price_alert",
|
||||
"arguments": _valid_price_alert_call()["args"],
|
||||
}}],
|
||||
"content": "",
|
||||
},
|
||||
"prompt_eval_count": 100,
|
||||
"eval_count": 20,
|
||||
}
|
||||
monkeypatch.setattr(module.requests, "get", fake_get)
|
||||
monkeypatch.setattr(
|
||||
module.requests,
|
||||
"post",
|
||||
lambda *_args, **_kwargs: _FakeResp(valid_body),
|
||||
)
|
||||
dispatcher = module.NemotronDispatcher()
|
||||
calls = _patch_execution_methods(monkeypatch, dispatcher)
|
||||
|
||||
result = dispatcher.dispatch([FakeThreat()], hermes_stats={"duration_sec": 1})
|
||||
|
||||
attempts = result["nim_stats"]["attempted_candidates"]
|
||||
assert [attempt["status"] for attempt in attempts] == [
|
||||
"identity_failed",
|
||||
"selected",
|
||||
]
|
||||
assert attempts[0]["error"] == "malformed_models_schema"
|
||||
assert result["nim_stats"]["provider"] == "ollama_secondary"
|
||||
assert result["dispatched"] == 1
|
||||
assert calls[0]["kind"] == "price_alert"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("tool_calls", "threats", "error_fragment"),
|
||||
[
|
||||
([{"tool": "delete_product", "args": {"sku": "SKU-Q1"}}], [FakeThreat()], "unknown_tool"),
|
||||
([_valid_price_alert_call("OTHER")], [FakeThreat()], "sku_not_in_request"),
|
||||
([{"tool": "trigger_price_alert", "args": {"sku": "SKU-Q1"}}], [FakeThreat()], "missing_required_args"),
|
||||
([_valid_price_alert_call()], [FakeThreat(), FakeThreat(sku="SKU-Q2")], "tool_call_count_mismatch"),
|
||||
(
|
||||
[_valid_price_alert_call(), _valid_price_alert_call()],
|
||||
[FakeThreat(), FakeThreat(sku="SKU-Q2")],
|
||||
"duplicate_sku_tool_call",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_tool_contract_rejects_unbound_incomplete_or_duplicate_calls(
|
||||
tool_calls, threats, error_fragment
|
||||
):
|
||||
import services.nemoton_dispatcher_service as module
|
||||
|
||||
with pytest.raises(module.ToolCallContractError, match=error_fragment):
|
||||
module._validate_tool_call_contract(tool_calls, threats)
|
||||
|
||||
|
||||
def test_failed_handler_does_not_poison_alert_dedupe(monkeypatch):
|
||||
import services.nemoton_dispatcher_service as module
|
||||
|
||||
dispatcher = module.NemotronDispatcher()
|
||||
monkeypatch.setattr(
|
||||
dispatcher,
|
||||
"_exec_trigger_price_alert",
|
||||
lambda **_kwargs: (_ for _ in ()).throw(RuntimeError("send failed")),
|
||||
)
|
||||
first_token = module._reserve_alert("SKU-Q1")
|
||||
assert first_token
|
||||
first = dispatcher._execute_tool_calls(
|
||||
tool_calls=[_valid_price_alert_call()],
|
||||
threats=[FakeThreat()],
|
||||
hermes_stats=None,
|
||||
nim_stats={"provider": "nim", "total_tokens": 1},
|
||||
reservation_tokens={"SKU-Q1": first_token},
|
||||
)
|
||||
|
||||
assert first["dispatched"] == 0
|
||||
assert module._is_duplicate_alert("SKU-Q1") is False
|
||||
|
||||
sent = []
|
||||
monkeypatch.setattr(
|
||||
dispatcher,
|
||||
"_exec_trigger_price_alert",
|
||||
lambda **kwargs: (sent.append(kwargs) or {"durable": True}),
|
||||
)
|
||||
second_token = module._reserve_alert("SKU-Q1")
|
||||
assert second_token
|
||||
second = dispatcher._execute_tool_calls(
|
||||
tool_calls=[_valid_price_alert_call()],
|
||||
threats=[FakeThreat()],
|
||||
hermes_stats=None,
|
||||
nim_stats={"provider": "nim", "total_tokens": 1},
|
||||
reservation_tokens={"SKU-Q1": second_token},
|
||||
)
|
||||
|
||||
assert second["dispatched"] == 1
|
||||
assert sent
|
||||
assert module._is_duplicate_alert("SKU-Q1") is True
|
||||
|
||||
|
||||
def test_non_durable_alert_outcome_is_failed_and_releases_reservation(monkeypatch):
|
||||
import services.nemoton_dispatcher_service as module
|
||||
|
||||
dispatcher = module.NemotronDispatcher()
|
||||
monkeypatch.setattr(
|
||||
dispatcher,
|
||||
"_send_telegram",
|
||||
lambda *_args, **_kwargs: {
|
||||
"durable": False,
|
||||
"delivered": False,
|
||||
"queued": False,
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(dispatcher, "_sink_insight_to_km", lambda **_kwargs: True)
|
||||
token = module._reserve_alert("SKU-Q1")
|
||||
assert token
|
||||
|
||||
result = dispatcher._execute_tool_calls(
|
||||
tool_calls=[_valid_price_alert_call()],
|
||||
threats=[FakeThreat()],
|
||||
hermes_stats=None,
|
||||
nim_stats={"provider": "nim", "total_tokens": 1},
|
||||
reservation_tokens={"SKU-Q1": token},
|
||||
)
|
||||
|
||||
assert result["dispatched"] == 0
|
||||
assert result["skipped"] == 1
|
||||
assert "durable_side_effect_not_confirmed" in result["errors"][0]
|
||||
assert module._is_duplicate_alert("SKU-Q1") is False
|
||||
|
||||
|
||||
def test_recommendation_requires_db_write_or_durable_notification(monkeypatch):
|
||||
import services.nemoton_dispatcher_service as module
|
||||
|
||||
dispatcher = module.NemotronDispatcher(engine=None)
|
||||
monkeypatch.setattr(
|
||||
dispatcher,
|
||||
"_send_telegram",
|
||||
lambda *_args, **_kwargs: {"durable": False},
|
||||
)
|
||||
monkeypatch.setattr(dispatcher, "_sink_insight_to_km", lambda **_kwargs: True)
|
||||
token = module._reserve_alert("SKU-Q1")
|
||||
assert token
|
||||
|
||||
result = dispatcher._execute_tool_calls(
|
||||
tool_calls=[{
|
||||
"tool": "add_to_recommendation",
|
||||
"args": {
|
||||
"sku": "SKU-Q1",
|
||||
"name": "qwen3 測試品",
|
||||
"reason": "具價格競爭力",
|
||||
"confidence": 0.85,
|
||||
},
|
||||
}],
|
||||
threats=[FakeThreat()],
|
||||
hermes_stats=None,
|
||||
nim_stats={"provider": "nim", "total_tokens": 1},
|
||||
reservation_tokens={"SKU-Q1": token},
|
||||
)
|
||||
|
||||
assert result["dispatched"] == 0
|
||||
assert "durable_side_effect_not_confirmed" in result["errors"][0]
|
||||
assert module._is_duplicate_alert("SKU-Q1") is False
|
||||
|
||||
|
||||
def test_km_route_requires_durable_insight_write(monkeypatch):
|
||||
import services.nemoton_dispatcher_service as module
|
||||
|
||||
dispatcher = module.NemotronDispatcher()
|
||||
monkeypatch.setattr(dispatcher, "_sink_insight_to_km", lambda **_kwargs: False)
|
||||
token = module._reserve_alert("SKU-Q1")
|
||||
assert token
|
||||
|
||||
result = dispatcher._execute_tool_calls(
|
||||
tool_calls=[{
|
||||
"tool": "route_to_km",
|
||||
"args": {
|
||||
"sku": "SKU-Q1",
|
||||
"name": "qwen3 測試品",
|
||||
"km_domain": "price_competition",
|
||||
"summary": "競價洞察",
|
||||
"confidence": 0.85,
|
||||
},
|
||||
}],
|
||||
threats=[FakeThreat()],
|
||||
hermes_stats=None,
|
||||
nim_stats={"provider": "nim", "total_tokens": 1},
|
||||
reservation_tokens={"SKU-Q1": token},
|
||||
)
|
||||
|
||||
assert result["dispatched"] == 0
|
||||
assert "durable_side_effect_not_confirmed" in result["errors"][0]
|
||||
assert module._is_duplicate_alert("SKU-Q1") is False
|
||||
|
||||
|
||||
def test_alert_reservation_is_atomic_and_releasable():
|
||||
import services.nemoton_dispatcher_service as module
|
||||
|
||||
first_token = module._reserve_alert("SKU-Q1")
|
||||
assert first_token
|
||||
assert module._reserve_alert("SKU-Q1") is None
|
||||
assert module._release_alert_reservation("SKU-Q1", first_token) is True
|
||||
second_token = module._reserve_alert("SKU-Q1")
|
||||
assert second_token and second_token != first_token
|
||||
assert module._mark_alert_dispatched("SKU-Q1", second_token) is True
|
||||
assert module._reserve_alert("SKU-Q1") is None
|
||||
|
||||
|
||||
def test_expired_reservation_token_cannot_release_or_commit_new_owner(monkeypatch):
|
||||
import services.nemoton_dispatcher_service as module
|
||||
|
||||
clock = {"now": 1000.0}
|
||||
monkeypatch.setattr(module.time, "time", lambda: clock["now"])
|
||||
first_token = module._reserve_alert("SKU-Q1")
|
||||
assert first_token
|
||||
|
||||
clock["now"] += module._ALERT_INFLIGHT_TTL_SEC + 1
|
||||
assert module._mark_alert_dispatched("SKU-Q1", first_token) is False
|
||||
second_token = module._reserve_alert("SKU-Q1")
|
||||
assert second_token and second_token != first_token
|
||||
|
||||
assert module._release_alert_reservation("SKU-Q1", first_token) is False
|
||||
assert module._mark_alert_dispatched("SKU-Q1", first_token) is False
|
||||
assert module._refresh_alert_reservation("SKU-Q1", first_token) is False
|
||||
assert module._refresh_alert_reservation("SKU-Q1", second_token) is True
|
||||
assert module._mark_alert_dispatched("SKU-Q1", second_token) is True
|
||||
assert module._is_duplicate_alert("SKU-Q1") is True
|
||||
|
||||
|
||||
def test_concurrent_reservation_has_exactly_one_owner():
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from threading import Barrier
|
||||
|
||||
import services.nemoton_dispatcher_service as module
|
||||
|
||||
barrier = Barrier(8)
|
||||
|
||||
def reserve_once():
|
||||
barrier.wait()
|
||||
return module._reserve_alert("SKU-CONCURRENT")
|
||||
|
||||
with ThreadPoolExecutor(max_workers=8) as pool:
|
||||
tokens = list(pool.map(lambda _index: reserve_once(), range(8)))
|
||||
|
||||
owners = [token for token in tokens if token is not None]
|
||||
assert len(owners) == 1
|
||||
assert module._release_alert_reservation("SKU-CONCURRENT", owners[0]) is True
|
||||
|
||||
|
||||
def test_side_effect_boundary_extends_lease_through_slow_handler(monkeypatch):
|
||||
import services.nemoton_dispatcher_service as module
|
||||
|
||||
clock = {"now": 5000.0}
|
||||
monkeypatch.setattr(module.time, "time", lambda: clock["now"])
|
||||
old_token = module._reserve_alert("SKU-Q1")
|
||||
assert old_token
|
||||
new_owner = {}
|
||||
dispatcher = module.NemotronDispatcher()
|
||||
|
||||
def delayed_handler(**_kwargs):
|
||||
clock["now"] += module._ALERT_INFLIGHT_TTL_SEC + 1
|
||||
new_owner["token"] = module._reserve_alert("SKU-Q1")
|
||||
return {"durable": True}
|
||||
|
||||
monkeypatch.setattr(dispatcher, "_exec_trigger_price_alert", delayed_handler)
|
||||
result = dispatcher._execute_tool_calls(
|
||||
tool_calls=[_valid_price_alert_call()],
|
||||
threats=[FakeThreat()],
|
||||
hermes_stats=None,
|
||||
nim_stats={"provider": "nim", "total_tokens": 1},
|
||||
reservation_tokens={"SKU-Q1": old_token},
|
||||
)
|
||||
|
||||
assert new_owner["token"] is None
|
||||
assert result["dispatched"] == 1
|
||||
assert result["errors"] == []
|
||||
assert module._is_duplicate_alert("SKU-Q1") is True
|
||||
|
||||
|
||||
def test_durable_tool_commit_failure_never_releases_quarantine(monkeypatch):
|
||||
import services.nemoton_dispatcher_service as module
|
||||
|
||||
dispatcher = module.NemotronDispatcher()
|
||||
monkeypatch.setattr(
|
||||
dispatcher,
|
||||
"_exec_trigger_price_alert",
|
||||
lambda **_kwargs: {"durable": True},
|
||||
)
|
||||
token = module._reserve_alert("SKU-Q1")
|
||||
released = []
|
||||
monkeypatch.setattr(module, "_mark_alert_dispatched", lambda *_args: False)
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"_release_alert_reservation",
|
||||
lambda *args: (released.append(args) or True),
|
||||
)
|
||||
|
||||
result = dispatcher._execute_tool_calls(
|
||||
tool_calls=[_valid_price_alert_call()],
|
||||
threats=[FakeThreat()],
|
||||
hermes_stats=None,
|
||||
nim_stats={"provider": "nim", "total_tokens": 1},
|
||||
reservation_tokens={"SKU-Q1": token},
|
||||
)
|
||||
|
||||
assert result["dispatched"] == 1
|
||||
assert result["skipped"] == 0
|
||||
assert result["errors"] == [
|
||||
"dedupe_commit_unverified:trigger_price_alert(SKU-Q1)"
|
||||
]
|
||||
assert released == []
|
||||
assert module._is_duplicate_alert("SKU-Q1") is True
|
||||
|
||||
|
||||
def test_dispatch_total_deadline_bounds_ollama_and_nim_chain(monkeypatch):
|
||||
import requests
|
||||
import services.nemoton_dispatcher_service as module
|
||||
|
||||
_enable_qwen3_path(monkeypatch, module)
|
||||
monkeypatch.setattr(module, "NEMOTRON_DISPATCH_TOTAL_TIMEOUT_SEC", 30)
|
||||
monkeypatch.setattr(module, "NIM_API_KEY", "fake-key")
|
||||
monkeypatch.setattr(module, "_check_nim_quota", lambda: True)
|
||||
clock = {"now": 100.0}
|
||||
monkeypatch.setattr(module.time, "monotonic", lambda: clock["now"])
|
||||
posted = []
|
||||
|
||||
def timeout_post(url, json=None, timeout=None, **_kwargs):
|
||||
posted.append((url, timeout))
|
||||
clock["now"] += float(timeout or 0)
|
||||
raise requests.Timeout("simulated timeout")
|
||||
|
||||
monkeypatch.setattr(module.requests, "post", timeout_post)
|
||||
dispatcher = module.NemotronDispatcher()
|
||||
calls = _patch_execution_methods(monkeypatch, dispatcher)
|
||||
|
||||
result = dispatcher.dispatch([FakeThreat()], hermes_stats={"duration_sec": 1.0})
|
||||
|
||||
upstream = result["nim_stats"]["upstream_ollama_failure"]
|
||||
assert len(posted) == 1
|
||||
assert posted[0][1] == 30
|
||||
assert clock["now"] == 130.0
|
||||
assert upstream["deadline_exhausted"] is True
|
||||
assert upstream["attempt_count"] == 2
|
||||
assert result["nim_stats"]["provider"] == "hermes_rule_engine"
|
||||
assert "Ollama 候選鏈" in calls[0]["kwargs"]["footprint"]
|
||||
assert "gcp_primary | primary | qwen3:14b | failed | digest=ok | Timeout" in (
|
||||
calls[0]["kwargs"]["footprint"]
|
||||
)
|
||||
structured = module._build_footprint_json(None, result["nim_stats"])
|
||||
attempts = structured["dispatcher"]["candidate_attempts"]
|
||||
assert attempts[0] == {
|
||||
"label": "gcp_primary",
|
||||
"tier": "primary",
|
||||
"model": "qwen3:14b",
|
||||
"status": "failed",
|
||||
"digest_matches": True,
|
||||
"error_class": "Timeout",
|
||||
}
|
||||
assert result["dispatched"] == 1
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# T2. qwen3 沒回 tool_calls 但 content 含 JSON list → fallback 解析
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
@@ -252,7 +791,7 @@ def test_qwen3_content_only_fallback_parsing(monkeypatch):
|
||||
calls = _patch_execution_methods(monkeypatch, dispatcher)
|
||||
monkeypatch.setattr(
|
||||
dispatcher, "_call_nim",
|
||||
lambda threats: (_ for _ in ()).throw(AssertionError("NIM 不應被呼叫")),
|
||||
lambda threats, **_kwargs: (_ for _ in ()).throw(AssertionError("NIM 不應被呼叫")),
|
||||
)
|
||||
|
||||
result = dispatcher.dispatch([FakeThreat(confidence=0.45)], hermes_stats={"duration_sec": 1.0})
|
||||
@@ -325,7 +864,7 @@ def test_qwen3_connection_error_falls_back_to_nim(monkeypatch):
|
||||
calls = _patch_execution_methods(monkeypatch, dispatcher)
|
||||
nim_invoked = {"v": False}
|
||||
|
||||
def _fake_nim(threats):
|
||||
def _fake_nim(threats, **_kwargs):
|
||||
nim_invoked["v"] = True
|
||||
return (
|
||||
[{
|
||||
@@ -369,7 +908,7 @@ def test_qwen3_and_nim_both_fail_falls_back_to_hermes_rules(monkeypatch):
|
||||
# 攔 _call_nim 也擲 timeout
|
||||
monkeypatch.setattr(
|
||||
dispatcher, "_call_nim",
|
||||
lambda threats: (_ for _ in ()).throw(requests.Timeout("NIM timeout")),
|
||||
lambda threats, **_kwargs: (_ for _ in ()).throw(requests.Timeout("NIM timeout")),
|
||||
)
|
||||
|
||||
# 攔住規則引擎內部呼叫的 _exec_*,記錄 concern / reason 文字驗證 🟡 標記
|
||||
@@ -392,6 +931,7 @@ def test_qwen3_and_nim_both_fail_falls_back_to_hermes_rules(monkeypatch):
|
||||
'revenue_loss_7d', 'recommended_price'],
|
||||
args, kwargs)
|
||||
captured.append(("human_review", merged))
|
||||
return {"durable": True}
|
||||
|
||||
def record_alert(*args, **kwargs):
|
||||
merged = _merge_positional(
|
||||
@@ -400,9 +940,11 @@ def test_qwen3_and_nim_both_fail_falls_back_to_hermes_rules(monkeypatch):
|
||||
'revenue_loss_7d', 'recommended_price'],
|
||||
args, kwargs)
|
||||
captured.append(("price_alert", merged))
|
||||
return {"durable": True}
|
||||
|
||||
def record_reco(*args, **kwargs):
|
||||
captured.append(("recommendation", kwargs))
|
||||
return {"durable": True}
|
||||
|
||||
monkeypatch.setattr(dispatcher, "_exec_flag_for_human_review", record_review)
|
||||
monkeypatch.setattr(dispatcher, "_exec_trigger_price_alert", record_alert)
|
||||
@@ -448,7 +990,7 @@ def test_flag_false_preserves_nim_first_emergency_path(monkeypatch):
|
||||
calls = _patch_execution_methods(monkeypatch, dispatcher)
|
||||
monkeypatch.setattr(
|
||||
dispatcher, "_call_nim",
|
||||
lambda threats: (
|
||||
lambda threats, **_kwargs: (
|
||||
[{
|
||||
"tool": "trigger_price_alert",
|
||||
"args": {
|
||||
|
||||
205
tests/test_nemotron_runtime_candidate_service.py
Normal file
205
tests/test_nemotron_runtime_candidate_service.py
Normal file
@@ -0,0 +1,205 @@
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
|
||||
class _Response:
|
||||
def __init__(self, payload):
|
||||
self._payload = payload
|
||||
|
||||
def raise_for_status(self):
|
||||
return None
|
||||
|
||||
def json(self):
|
||||
return self._payload
|
||||
|
||||
|
||||
def test_candidate_chain_is_gcp_first_with_exact_digest_111_fallback():
|
||||
from services import nemotron_runtime_candidate_service as service
|
||||
|
||||
candidates = service.build_nemotron_runtime_candidates()
|
||||
|
||||
assert [candidate.label for candidate in candidates] == [
|
||||
"gcp_primary",
|
||||
"gcp_secondary",
|
||||
"ollama_111_fallback",
|
||||
]
|
||||
assert [candidate.model for candidate in candidates] == [
|
||||
"qwen3:14b",
|
||||
"qwen3:14b",
|
||||
"qwen3:8b",
|
||||
]
|
||||
assert candidates[0].is_fallback is False
|
||||
assert candidates[2].is_fallback is True
|
||||
assert candidates[0].request_timeout_sec == 60
|
||||
assert candidates[2].request_timeout_sec == 45
|
||||
assert candidates[0].num_predict == 2048
|
||||
assert candidates[2].num_predict == 512
|
||||
assert candidates[0].num_ctx is None
|
||||
assert candidates[2].num_ctx == 4096
|
||||
|
||||
|
||||
def test_model_identity_fails_closed_on_digest_drift():
|
||||
from services import nemotron_runtime_candidate_service as service
|
||||
|
||||
candidate = service.build_nemotron_runtime_candidates()[0]
|
||||
identity = service.inspect_nemotron_model_identity(
|
||||
candidate,
|
||||
request_get=lambda *_args, **_kwargs: _Response({
|
||||
"models": [{
|
||||
"name": candidate.model,
|
||||
"model": candidate.model,
|
||||
"digest": "deadbeef",
|
||||
"details": {},
|
||||
}]
|
||||
}),
|
||||
)
|
||||
|
||||
assert identity["ok"] is False
|
||||
assert identity["digest_matches"] is False
|
||||
assert identity["error"] == "model_digest_mismatch"
|
||||
|
||||
|
||||
def test_model_identity_reports_network_failure_without_loading_model():
|
||||
from services import nemotron_runtime_candidate_service as service
|
||||
|
||||
candidate = service.build_nemotron_runtime_candidates()[2]
|
||||
|
||||
def fail(*_args, **_kwargs):
|
||||
raise requests.Timeout("offline")
|
||||
|
||||
identity = service.inspect_nemotron_model_identity(
|
||||
candidate,
|
||||
request_get=fail,
|
||||
)
|
||||
|
||||
assert identity["ok"] is False
|
||||
assert identity["digest"] is None
|
||||
assert identity["error"].startswith("Timeout: offline")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("payload", "error_fragment"),
|
||||
[
|
||||
(None, "malformed_tags_payload"),
|
||||
({"models": {}}, "malformed_models_schema"),
|
||||
({"models": [None]}, "malformed_models_schema"),
|
||||
({"models": [{"name": "other", "details": None}]}, "malformed_model_details"),
|
||||
],
|
||||
)
|
||||
def test_model_identity_fails_closed_on_malformed_tags_schema(
|
||||
payload, error_fragment
|
||||
):
|
||||
from services import nemotron_runtime_candidate_service as service
|
||||
|
||||
candidate = service.build_nemotron_runtime_candidates()[0]
|
||||
identity = service.inspect_nemotron_model_identity(
|
||||
candidate,
|
||||
request_get=lambda *_args, **_kwargs: _Response(payload),
|
||||
)
|
||||
|
||||
assert identity["ok"] is False
|
||||
assert identity["digest_matches"] is False
|
||||
assert error_fragment in str(identity["error"])
|
||||
|
||||
|
||||
def test_model_identity_rejects_malformed_entry_mixed_with_valid_target():
|
||||
from services import nemotron_runtime_candidate_service as service
|
||||
|
||||
candidate = service.build_nemotron_runtime_candidates()[0]
|
||||
identity = service.inspect_nemotron_model_identity(
|
||||
candidate,
|
||||
request_get=lambda *_args, **_kwargs: _Response({
|
||||
"models": [
|
||||
None,
|
||||
{
|
||||
"name": candidate.model,
|
||||
"model": candidate.model,
|
||||
"digest": candidate.expected_digest,
|
||||
"details": {},
|
||||
},
|
||||
]
|
||||
}),
|
||||
)
|
||||
|
||||
assert identity["ok"] is False
|
||||
assert identity["digest_matches"] is False
|
||||
assert identity["error"] == "malformed_models_schema"
|
||||
|
||||
|
||||
def test_candidate_attempt_timeout_honors_env_but_111_resources_stay_fixed():
|
||||
env = os.environ.copy()
|
||||
env.update({
|
||||
"NEMOTRON_111_ATTEMPT_TIMEOUT_SEC": "33",
|
||||
"OLLAMA_111_NUM_CTX": "2048",
|
||||
"OLLAMA_111_NUM_PREDICT": "256",
|
||||
})
|
||||
completed = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-c",
|
||||
(
|
||||
"import json; "
|
||||
"from services.nemotron_runtime_candidate_service import "
|
||||
"build_nemotron_runtime_candidates; "
|
||||
"c=build_nemotron_runtime_candidates()[-1]; "
|
||||
"print(json.dumps(c.as_public_dict(), sort_keys=True))"
|
||||
),
|
||||
],
|
||||
cwd=os.path.dirname(os.path.dirname(__file__)),
|
||||
env=env,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert completed.returncode == 0, completed.stderr
|
||||
fallback = json.loads(completed.stdout.strip())
|
||||
assert fallback["request_timeout_sec"] == 33
|
||||
assert fallback["num_ctx"] == 4096
|
||||
assert fallback["num_predict"] == 512
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("num_ctx", "num_predict"),
|
||||
[
|
||||
("99999", "99999"),
|
||||
("-7", "-9"),
|
||||
("not-a-number", "invalid"),
|
||||
],
|
||||
)
|
||||
def test_candidate_resource_contract_cannot_be_weakened_by_environment(
|
||||
num_ctx, num_predict
|
||||
):
|
||||
env = os.environ.copy()
|
||||
env.update({
|
||||
"OLLAMA_111_NUM_CTX": num_ctx,
|
||||
"OLLAMA_111_NUM_PREDICT": num_predict,
|
||||
})
|
||||
completed = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-c",
|
||||
(
|
||||
"import json; "
|
||||
"from services.nemotron_runtime_candidate_service import "
|
||||
"build_nemotron_runtime_candidates; "
|
||||
"c=build_nemotron_runtime_candidates()[-1]; "
|
||||
"print(json.dumps(c.as_public_dict(), sort_keys=True))"
|
||||
),
|
||||
],
|
||||
cwd=os.path.dirname(os.path.dirname(__file__)),
|
||||
env=env,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert completed.returncode == 0, completed.stderr
|
||||
fallback = json.loads(completed.stdout.strip())
|
||||
assert fallback["num_ctx"] == 4096
|
||||
assert fallback["num_predict"] == 512
|
||||
@@ -132,8 +132,15 @@ def test_env_example_documents_runtime_and_ai_automation_variables():
|
||||
"N8N_USER",
|
||||
"N8N_WEBHOOK_BASE_URL",
|
||||
"NEMOTRON_OLLAMA_FIRST",
|
||||
"NEMOTRON_OLLAMA_EXPECTED_DIGEST",
|
||||
"NEMOTRON_OLLAMA_FALLBACK_EXPECTED_DIGEST",
|
||||
"NEMOTRON_OLLAMA_FALLBACK_MODEL",
|
||||
"NEMOTRON_OLLAMA_MODEL",
|
||||
"NEMOTRON_OLLAMA_TIMEOUT",
|
||||
"NEMOTRON_GCP_ATTEMPT_TIMEOUT_SEC",
|
||||
"NEMOTRON_111_ATTEMPT_TIMEOUT_SEC",
|
||||
"NEMOTRON_DISPATCH_TOTAL_TIMEOUT_SEC",
|
||||
"NEMOTRON_MODEL_IDENTITY_TIMEOUT_SEC",
|
||||
"OLLAMA_EMBED_TIMEOUT",
|
||||
"OPENCLAW_ADMIN_USER_IDS",
|
||||
"OPENCLAW_AGENT_DISPATCH",
|
||||
|
||||
@@ -7,6 +7,7 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
def test_qwen3_is_active_runtime_model_not_unused_ollama_weight():
|
||||
openclaw_source = (ROOT / "services" / "openclaw_strategist_service.py").read_text(encoding="utf-8")
|
||||
nemotron_source = (ROOT / "services" / "nemoton_dispatcher_service.py").read_text(encoding="utf-8")
|
||||
candidate_source = (ROOT / "services" / "nemotron_runtime_candidate_service.py").read_text(encoding="utf-8")
|
||||
router_source = (ROOT / "services" / "llm_model_router.py").read_text(encoding="utf-8")
|
||||
|
||||
assert "OPENCLAW_QA_OLLAMA_MODEL = os.getenv('OPENCLAW_QA_OLLAMA_MODEL', 'qwen3:14b')" in openclaw_source
|
||||
@@ -16,7 +17,11 @@ def test_qwen3_is_active_runtime_model_not_unused_ollama_weight():
|
||||
assert "OPENCLAW_STRATEGY_OLLAMA_KEEP_ALIVE = os.getenv('OPENCLAW_STRATEGY_OLLAMA_KEEP_ALIVE', '5m')" in openclaw_source
|
||||
assert "keep_alive=OPENCLAW_STRATEGY_OLLAMA_KEEP_ALIVE" in openclaw_source
|
||||
assert 'keep_alive="24h"' not in openclaw_source
|
||||
assert 'NEMOTRON_OLLAMA_MODEL = os.getenv("NEMOTRON_OLLAMA_MODEL", "qwen3:14b")' in nemotron_source
|
||||
assert "NEMOTRON_OLLAMA_MODEL = PRIMARY_MODEL" in nemotron_source
|
||||
assert "def _call_qwen3_dispatch(" in nemotron_source
|
||||
assert "for _attempt in range(3):" in nemotron_source
|
||||
assert "for candidate in build_nemotron_runtime_candidates(" in nemotron_source
|
||||
assert "inspect_nemotron_model_identity(" in nemotron_source
|
||||
assert '"qwen3:14b"' in candidate_source
|
||||
assert '"qwen3:8b"' in candidate_source
|
||||
assert 'label="ollama_111_fallback"' in candidate_source
|
||||
assert "'qwen3:14b'" in router_source
|
||||
|
||||
Reference in New Issue
Block a user