Add project tools and strict provider execution
This commit is contained in:
+215
-1
@@ -8,7 +8,7 @@ import pytest
|
||||
import yaml
|
||||
|
||||
from cmdforge.providers import (
|
||||
Provider, ProviderResult,
|
||||
Provider, ProviderResult, ProviderExecutionPolicy,
|
||||
load_providers, save_providers, get_provider,
|
||||
add_provider, delete_provider,
|
||||
call_provider, discover_installed_providers, mock_provider,
|
||||
@@ -60,6 +60,19 @@ class TestProvider:
|
||||
provider = Provider.from_dict(data)
|
||||
assert provider.description == ""
|
||||
|
||||
def test_legacy_policy_field_aliases_are_canonicalized(self):
|
||||
provider = Provider.from_dict({
|
||||
"name": "legacy-policy",
|
||||
"command": "provider",
|
||||
"model_id": "model-a",
|
||||
"max_input_tokens": 8192,
|
||||
})
|
||||
|
||||
assert provider.model == "model-a"
|
||||
assert provider.max_context_tokens == 8192
|
||||
assert provider.to_dict()["model"] == "model-a"
|
||||
assert provider.to_dict()["max_context_tokens"] == 8192
|
||||
|
||||
def test_roundtrip(self):
|
||||
original = Provider(
|
||||
name="custom",
|
||||
@@ -90,6 +103,13 @@ class TestProvider:
|
||||
fallback_chain=["backup", "mock"],
|
||||
api_key_env="TEST_API_KEY",
|
||||
pty_config={"prompt_pattern": ">"},
|
||||
locality="remote",
|
||||
capabilities=["text", "structured-json"],
|
||||
model_digest="sha256:abc",
|
||||
cost_class="low",
|
||||
latency_class="fast",
|
||||
max_context_tokens=131072,
|
||||
data_policy="internal",
|
||||
)
|
||||
|
||||
assert Provider.from_dict(original.to_dict()) == original
|
||||
@@ -516,6 +536,200 @@ class TestProviderFallback:
|
||||
# Fallback to mock should succeed
|
||||
assert result.success is True
|
||||
assert "[MOCK]" in result.text
|
||||
assert result.requested_provider == "primary"
|
||||
assert result.actual_provider == "mock"
|
||||
assert result.attempted_providers == ["primary", "mock"]
|
||||
assert result.fallback_used is True
|
||||
|
||||
def test_no_fallback_fails_closed(self, temp_providers_file):
|
||||
save_providers([
|
||||
Provider("primary", "nonexistent-cmd", fallback="mock"),
|
||||
Provider("mock", "mock"),
|
||||
])
|
||||
|
||||
result = call_provider(
|
||||
"primary", "Test prompt",
|
||||
execution_policy=ProviderExecutionPolicy(allow_fallback=False),
|
||||
)
|
||||
|
||||
assert result.success is False
|
||||
assert result.requested_provider == "primary"
|
||||
assert result.actual_provider is None
|
||||
assert result.attempted_providers == ["primary"]
|
||||
assert result.fallback_used is False
|
||||
|
||||
def test_private_data_requires_explicit_compatible_policy(
|
||||
self, temp_providers_file
|
||||
):
|
||||
save_providers([
|
||||
Provider(
|
||||
"local", "unused", locality="local",
|
||||
capabilities=["structured-json"],
|
||||
),
|
||||
])
|
||||
|
||||
result = call_provider(
|
||||
"local", "private packet",
|
||||
execution_policy=ProviderExecutionPolicy(
|
||||
allow_fallback=False,
|
||||
required_locality="local",
|
||||
required_capabilities=("structured-json",),
|
||||
data_classification="private",
|
||||
),
|
||||
)
|
||||
|
||||
assert result.success is False
|
||||
assert "data_policy is unspecified" in result.error
|
||||
|
||||
def test_require_local_rejects_ollama_redirected_to_remote_host(
|
||||
self, temp_providers_file, monkeypatch
|
||||
):
|
||||
save_providers([
|
||||
Provider(
|
||||
"ollama-private", "ollama run safe", locality="local",
|
||||
data_policy="private",
|
||||
),
|
||||
])
|
||||
monkeypatch.setenv("OLLAMA_HOST", "gpu.example.test:11434")
|
||||
|
||||
result = call_provider(
|
||||
"ollama-private", "private packet",
|
||||
execution_policy=ProviderExecutionPolicy(
|
||||
allow_fallback=False,
|
||||
required_locality="local",
|
||||
data_classification="private",
|
||||
),
|
||||
)
|
||||
|
||||
assert result.success is False
|
||||
assert "locality is remote" in result.error
|
||||
|
||||
def test_ollama_digest_is_observed_from_local_api(
|
||||
self, temp_providers_file, monkeypatch
|
||||
):
|
||||
save_providers([
|
||||
Provider(
|
||||
"ollama-safe", "ollama run safe:latest", model="safe:latest",
|
||||
locality="local", data_policy="private",
|
||||
),
|
||||
])
|
||||
monkeypatch.delenv("OLLAMA_HOST", raising=False)
|
||||
|
||||
class Response:
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
return False
|
||||
|
||||
def read(self):
|
||||
return (
|
||||
b'{"models":[{"name":"safe:latest","model":"safe:latest",'
|
||||
b'"digest":"abcdef"}]}'
|
||||
)
|
||||
|
||||
with (
|
||||
patch("urllib.request.urlopen", return_value=Response()),
|
||||
patch(
|
||||
"cmdforge.providers.call_provider_subprocess",
|
||||
return_value=ProviderResult(text="safe", success=True),
|
||||
),
|
||||
):
|
||||
result = call_provider(
|
||||
"ollama-safe", "private",
|
||||
execution_policy=ProviderExecutionPolicy(
|
||||
allow_fallback=False,
|
||||
required_locality="local",
|
||||
data_classification="private",
|
||||
require_model_identity=True,
|
||||
require_model_digest=True,
|
||||
),
|
||||
)
|
||||
|
||||
assert result.model == "safe:latest"
|
||||
assert result.model_digest == "sha256:abcdef"
|
||||
assert result.model_identity_source == "ollama-api"
|
||||
|
||||
def test_policy_eligible_provider_reports_verified_metadata(
|
||||
self, temp_providers_file
|
||||
):
|
||||
provider = Provider(
|
||||
"local", "local-ai", model="model-a",
|
||||
model_digest="sha256:123", locality="local",
|
||||
capabilities=["structured-json"], data_policy="private",
|
||||
)
|
||||
save_providers([provider])
|
||||
low_level = ProviderResult(text='{"ok": true}', success=True)
|
||||
|
||||
with patch(
|
||||
"cmdforge.providers.call_provider_subprocess",
|
||||
return_value=low_level,
|
||||
):
|
||||
result = call_provider(
|
||||
"local", "private packet",
|
||||
execution_policy=ProviderExecutionPolicy(
|
||||
allow_fallback=False,
|
||||
required_locality="local",
|
||||
required_capabilities=("structured-json",),
|
||||
data_classification="private",
|
||||
require_model_identity=True,
|
||||
require_model_digest=True,
|
||||
),
|
||||
)
|
||||
|
||||
assert result.success is True
|
||||
assert result.provenance() == {
|
||||
"success": True,
|
||||
"requested_provider": "local",
|
||||
"actual_provider": "local",
|
||||
"attempted_providers": ["local"],
|
||||
"fallback_used": False,
|
||||
"model": "model-a",
|
||||
"model_digest": "sha256:123",
|
||||
"locality": "local",
|
||||
"model_identity_source": "provider-config",
|
||||
}
|
||||
|
||||
def test_fallback_chain_skips_policy_ineligible_provider(
|
||||
self, temp_providers_file
|
||||
):
|
||||
save_providers([
|
||||
Provider(
|
||||
"primary", "primary", fallback_chain=["remote", "local"],
|
||||
locality="local", data_policy="private",
|
||||
),
|
||||
Provider(
|
||||
"remote", "remote", locality="remote", data_policy="private",
|
||||
),
|
||||
Provider(
|
||||
"local", "local", locality="local", data_policy="private",
|
||||
model="safe-model",
|
||||
),
|
||||
])
|
||||
|
||||
def invoke(provider, prompt, timeout, max_tokens):
|
||||
if provider.name == "primary":
|
||||
return ProviderResult(text="", success=False, error="offline")
|
||||
assert provider.name == "local"
|
||||
return ProviderResult(text="safe", success=True)
|
||||
|
||||
with patch(
|
||||
"cmdforge.providers.call_provider_subprocess", side_effect=invoke
|
||||
) as low_level:
|
||||
result = call_provider(
|
||||
"primary", "private packet",
|
||||
execution_policy=ProviderExecutionPolicy(
|
||||
required_locality="local",
|
||||
data_classification="private",
|
||||
),
|
||||
)
|
||||
|
||||
assert result.success is True
|
||||
assert result.actual_provider == "local"
|
||||
assert result.attempted_providers == ["primary", "remote", "local"]
|
||||
assert [call.args[0].name for call in low_level.call_args_list] == [
|
||||
"primary", "local"
|
||||
]
|
||||
|
||||
def test_fallback_chain(self, temp_providers_file):
|
||||
"""Fallback can chain to another provider with fallback."""
|
||||
|
||||
Reference in New Issue
Block a user