fix: address PR #814 reviewer follow-ups
- gateway_client: guard token cache with a lazy anyio.Lock so concurrent embed calls share one M2M token request instead of racing - status subscriber: distinguish idle fetch timeouts from real broker errors (log + 5s backoff) instead of swallowing all and spinning - nats: warn when the bus URL uses unencrypted transport (non-tls://) - collection_metadata: accept an optional shared httpx client, make TLS verify explicit, document the unauthenticated control-plane contract - replace python -O-stripped asserts with explicit ValueError in the bus status builder and the api metadata source - document why the nil-UUID sentinel point can't collide with content ids Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b5ed1e3b4d
commit
2d845cb70f
@@ -116,3 +116,47 @@ async def test_token_provider_caches_and_refreshes(monkeypatch):
|
||||
t3 = await tp.get_token()
|
||||
assert t3 == "tok2"
|
||||
assert calls["n"] == 2
|
||||
|
||||
|
||||
async def test_token_provider_concurrent_callers_issue_single_request(monkeypatch):
|
||||
"""Two concurrent get_token() calls must share one token request, not race."""
|
||||
import anyio
|
||||
|
||||
calls = {"n": 0}
|
||||
|
||||
async def handler(request: httpx.Request) -> httpx.Response:
|
||||
calls["n"] += 1
|
||||
# Hold the "network" open so a second caller arrives mid-flight.
|
||||
await anyio.sleep(0.05)
|
||||
return httpx.Response(
|
||||
200, json={"access_token": f"tok{calls['n']}", "expires_in": 3600}
|
||||
)
|
||||
|
||||
transport = httpx.MockTransport(handler)
|
||||
orig_async_client = httpx.AsyncClient
|
||||
|
||||
def _client(*args, **kwargs):
|
||||
kwargs["transport"] = transport
|
||||
return orig_async_client(*args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(httpx, "AsyncClient", _client)
|
||||
|
||||
tp = GatewayTokenProvider(
|
||||
token_url="https://idp.example/oauth2/token",
|
||||
client_id="cid",
|
||||
client_secret="sec",
|
||||
)
|
||||
|
||||
results: list[str] = []
|
||||
|
||||
async def _fetch():
|
||||
results.append(await tp.get_token())
|
||||
|
||||
async with anyio.create_task_group() as tg:
|
||||
tg.start_soon(_fetch)
|
||||
tg.start_soon(_fetch)
|
||||
|
||||
# The lock serialises the check-then-fetch cycle: only one HTTP request,
|
||||
# and both callers observe the same cached token.
|
||||
assert calls["n"] == 1
|
||||
assert results == ["tok1", "tok1"]
|
||||
|
||||
@@ -70,6 +70,29 @@ async def test_api_source(mocker):
|
||||
assert meta["embedding_identity"] == "amazon.titan-embed-text-v2:0"
|
||||
|
||||
|
||||
async def test_api_source_uses_shared_http_client():
|
||||
"""When a caller passes http_client, the read reuses it (no own client)."""
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
assert request.url.path == "/v1/qdrant-collections/col/metadata"
|
||||
return httpx.Response(200, json={"embedding_identity": "mistral-embed"})
|
||||
|
||||
shared = httpx.AsyncClient(transport=httpx.MockTransport(handler))
|
||||
async with shared:
|
||||
meta = await cm._read_from_api("https://cp", "col", client=shared)
|
||||
assert meta["embedding_identity"] == "mistral-embed"
|
||||
|
||||
|
||||
async def test_api_missing_url_falls_back_to_env(mocker):
|
||||
"""COLLECTION_METADATA_SOURCE=api with no URL must not crash the query path."""
|
||||
# Bypass Settings validation to simulate a -O / mutated-state edge case.
|
||||
settings = Settings()
|
||||
settings.collection_metadata_source = "api"
|
||||
settings.collection_metadata_api_url = None
|
||||
meta = await cm.read_collection_metadata(mocker.AsyncMock(), "col", settings)
|
||||
assert meta == cm.env_default_metadata(settings)
|
||||
|
||||
|
||||
async def test_upsert_sentinel_builds_point(mocker):
|
||||
client = mocker.AsyncMock()
|
||||
await cm.upsert_sentinel(
|
||||
|
||||
@@ -12,6 +12,7 @@ from nextcloud_mcp_server.vector.queue.nats import (
|
||||
NatsTaskProducer,
|
||||
_modified_at_rfc3339,
|
||||
msg_id,
|
||||
warn_if_insecure_nats_url,
|
||||
)
|
||||
from nextcloud_mcp_server.vector.queue.postgres import PostgresTaskProducer
|
||||
from nextcloud_mcp_server.vector.scanner import DocumentTask
|
||||
@@ -134,3 +135,21 @@ def test_transport_for(url, expected):
|
||||
async def test_postgres_producer_is_a_seam():
|
||||
with pytest.raises(NotImplementedError, match="documented seam"):
|
||||
await PostgresTaskProducer.connect(object())
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"url,should_warn",
|
||||
[
|
||||
("nats://nats:4222", True),
|
||||
("ws://nats:8080", True),
|
||||
("tls://nats:4222", False),
|
||||
("wss://nats:8080", False),
|
||||
],
|
||||
)
|
||||
def test_warn_if_insecure_nats_url(url, should_warn, caplog):
|
||||
import logging
|
||||
|
||||
with caplog.at_level(logging.WARNING):
|
||||
warn_if_insecure_nats_url(url)
|
||||
warned = any("unencrypted transport" in r.getMessage() for r in caplog.records)
|
||||
assert warned is should_warn
|
||||
|
||||
Reference in New Issue
Block a user