Add community node protocol spec, Agent Skills, and make the failing tests hermetic
DevPlace CI / test (push) Failing after 31m42s
DevPlace CI / test (push) Failing after 31m42s
Community nodes (Phase 1 of plan.md): the protocol specification in services/nodes/spec.py as the single source of truth, GET /nodes/spec.json, a generated protocol docs page, the Community nodes API docs group, and the implementation plan plus the residential proxy research. The no-open-proxy rule is enforced in the spec (open_proxy cannot be set). Agent Skills: register the Skills API docs group, cascade skills on account deletion, add the nested CLAUDE.md, README and routes docs, and cover the parser, the fetcher (end to end against a mock origin), the data layer and the routes with 73 unit and 16 api tests. Tests made hermetic with the repo's existing patterns instead of ambient environment: embeddings tests set OPENROUTER_API_KEY via monkeypatch, the workspace tests install FakeBackend via runtime.set_backend, and the skills install tests use run_async. DockerCliBackend now returns 127 (command not found) when the CLI is missing instead of raising FileNotFoundError, so a host without docker answers with a clean error rather than a 500. The Providers tab empty state no longer names the Model routes tab. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -595,3 +595,106 @@ def test_fetch_provider_models_none_when_provider_has_no_base_url(local_db):
|
||||
assert run_async(r.fetch_provider_models("probeprovnourl")) is None
|
||||
finally:
|
||||
r.provider_store.remove("probeprovnourl")
|
||||
|
||||
|
||||
def test_provider_protocol_defaults_and_validates(local_db):
|
||||
assert r.ProviderIn(name="p1").protocol == "chat_completions"
|
||||
assert r.ProviderIn(name="p1", protocol="responses").protocol == "responses"
|
||||
assert r.ProviderIn(name="p1", protocol=" Responses ").protocol == "responses"
|
||||
with pytest.raises(ValidationError):
|
||||
r.ProviderIn(name="p1", protocol="soap")
|
||||
|
||||
|
||||
def test_provider_protocol_round_trips_through_store(local_db):
|
||||
r.provider_store.set(
|
||||
r.ProviderIn(
|
||||
name="protometa",
|
||||
base_url="https://api.meta.ai/v1/responses",
|
||||
api_key="sk-test",
|
||||
protocol="responses",
|
||||
)
|
||||
)
|
||||
try:
|
||||
saved = r.provider_store.get("protometa")
|
||||
assert saved is not None
|
||||
assert saved["protocol"] == "responses"
|
||||
assert saved["base_url"] == "https://api.meta.ai/v1/responses"
|
||||
finally:
|
||||
r.provider_store.remove("protometa")
|
||||
|
||||
|
||||
def test_chat_overlay_carries_provider_protocol(local_db):
|
||||
r.provider_store.set(
|
||||
r.ProviderIn(
|
||||
name="protoovl",
|
||||
base_url="https://api.meta.ai/v1/responses",
|
||||
api_key="sk-test",
|
||||
protocol="responses",
|
||||
)
|
||||
)
|
||||
r.model_store.set(
|
||||
r.ModelRouteIn(
|
||||
source_model="meta-route",
|
||||
provider="protoovl",
|
||||
target_model="muse-spark-1.3-contributor",
|
||||
kind="chat",
|
||||
)
|
||||
)
|
||||
try:
|
||||
overlay = r.chat_overlay("meta-route", {"gateway_model": "x"})
|
||||
assert overlay["gateway_upstream_protocol"] == "responses"
|
||||
assert (
|
||||
overlay["gateway_upstream_url"]
|
||||
== "https://api.meta.ai/v1/responses"
|
||||
)
|
||||
finally:
|
||||
_cleanup(["protoovl"], ["meta-route"])
|
||||
|
||||
|
||||
def test_responses_models_urls_probe_both_shapes():
|
||||
assert r._responses_models_urls("https://api.meta.ai/v1/responses") == [
|
||||
"https://api.meta.ai/v1/models",
|
||||
"https://api.meta.ai/v1/muse-code/models",
|
||||
]
|
||||
assert r._responses_models_urls("") == []
|
||||
|
||||
|
||||
def test_fetch_provider_models_tries_meta_listing_second(local_db, monkeypatch):
|
||||
r.provider_store.set(
|
||||
r.ProviderIn(
|
||||
name="probeprovmeta",
|
||||
base_url="https://api.meta.ai/v1/responses",
|
||||
protocol="responses",
|
||||
)
|
||||
)
|
||||
|
||||
class _SeqClient:
|
||||
def __init__(self):
|
||||
self.requested_urls = []
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *exc_info):
|
||||
return False
|
||||
|
||||
async def get(self, url):
|
||||
self.requested_urls.append(url)
|
||||
if url == "https://api.meta.ai/v1/models":
|
||||
return _FakeModelsResponse(404, {})
|
||||
return _FakeModelsResponse(
|
||||
200, {"data": [{"id": "muse-spark-1.3-contributor"}]}
|
||||
)
|
||||
|
||||
fake_client = _SeqClient()
|
||||
_patch_stealth_client(monkeypatch, fake_client)
|
||||
try:
|
||||
assert run_async(r.fetch_provider_models("probeprovmeta")) == [
|
||||
"muse-spark-1.3-contributor"
|
||||
]
|
||||
assert fake_client.requested_urls == [
|
||||
"https://api.meta.ai/v1/models",
|
||||
"https://api.meta.ai/v1/muse-code/models",
|
||||
]
|
||||
finally:
|
||||
r.provider_store.remove("probeprovmeta")
|
||||
|
||||
Reference in New Issue
Block a user