From d93231c7aca0b3fe0d43e44c72750e141b266885 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 18 Oct 2026 20:06:20 +0530 Subject: [PATCH] fix: match the server's NATS subject encoding for segments with dots The server now maps each URI segment to one subject token and percent-encodes every byte outside [A-Za-z0-9_-] with uppercase hex (priompt/priompt#33), so priompt://acme.corp/x/a.b publishes on priompt.acme%2Ecorp.x.a%2Eb. _subject() only replaced "/" with ".", so subscribe() listened on the wrong subject for any URI with a dot in a segment. Dot-free URIs keep the same subject. Fixes #10 Co-Authored-By: Claude Opus 5.5 --- priompt_sdk/client.py | 15 ++++++++++++++- tests/test_client.py | 17 +++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/priompt_sdk/client.py b/priompt_sdk/client.py index b649cac..8f32976 100644 --- a/priompt_sdk/client.py +++ b/priompt_sdk/client.py @@ -11,8 +11,21 @@ from priompt_sdk.v1 import prompt_pb2, prompt_pb2_grpc +_TOKEN_SAFE = frozenset( + b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-" +) + + def _subject(uri): - return "priompt." + uri.removeprefix("priompt://").replace("/", ".") + """Map a prompt URI to the server's NATS subject (pubsub.Subject): one + token per URI segment, every byte outside [A-Za-z0-9_-] percent-encoded + with uppercase hex, so "priompt://acme.corp/x/a.b" -> + "priompt.acme%2Ecorp.x.a%2Eb". Dot-free URIs map as before.""" + segs = uri.removeprefix("priompt://").split("/") + return "priompt." + ".".join( + "".join(chr(c) if c in _TOKEN_SAFE else f"%{c:02X}" for c in s.encode()) + for s in segs + ) def _parse_url(raw): diff --git a/tests/test_client.py b/tests/test_client.py index f5e5eac..591fb87 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -68,11 +68,28 @@ def make_client(cache_ttl=0): ("priompt://a", "priompt.a"), ("team/agent", "priompt.team.agent"), # no scheme prefix ("priompt://a/b/", "priompt.a.b."), # trailing slash preserved + # Server's encoding (priompt pubsub.Subject): bytes outside [A-Za-z0-9_-] + # are percent-encoded per segment, uppercase hex. + ("priompt://acme/support/agent-v2_x", "priompt.acme.support.agent-v2_x"), + ("priompt://acme.corp/x/a.b", "priompt.acme%2Ecorp.x.a%2Eb"), + ("priompt://a/*/>", "priompt.a.%2A.%3E"), # no wildcards + ("priompt://a/café", "priompt.a.caf%C3%A9"), # UTF-8 bytes ]) def test_subject(uri, expected): assert _subject(uri) == expected +@pytest.mark.parametrize("a,b", [ + # The server's collision cases (TestSubjectIsInjective): distinct URIs + # must get distinct subjects even when a segment contains a dot. + ("priompt://acme/a.b", "priompt://acme/a/b"), + ("priompt://acme/corp/x", "priompt://acme.corp/x"), + ("priompt://acme/a.b/c", "priompt://acme/a/b.c"), +]) +def test_subject_is_injective(a, b): + assert _subject(a) != _subject(b) + + # --- caching happy path --------------------------------------------------- def test_get_caches_within_ttl(clock):