Skip to content

Commit bed86d1

Browse files
Rusty Conoverclaude
andcommitted
fix(meta_worker): let contents_* fixture workers opt out of unattached routing
2eb142d's contents_* catalogs re-declare main.double / vgi_sum / sequence (the example catalog's classes), so MetaWorker found four declarers for an attach-less main.double and raised "Cannot route", failing 44 client round-trip / HTTP upload / resumable-scan tests. New Worker class attribute route_unattached_calls (default True). MetaWorker's _candidates_for considers only workers that allow it; attach routing is unchanged. The four contents_* workers set it False, so they are reachable only through their own attach and the example worker stays the unambiguous owner of unattached calls. Object names are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 3fbb60f commit bed86d1

4 files changed

Lines changed: 66 additions & 4 deletions

File tree

‎tests/test_schema_scoped_functions.py‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,3 +239,40 @@ def test_an_unknown_name_reports_unknown_not_ambiguous(self) -> None:
239239
request = dataclasses.replace(self._request(None), function_name="no_such_function")
240240
with pytest.raises(ValueError, match="Unknown function"):
241241
meta._resolve_function(request)
242+
243+
244+
class TestAttachedOnlyWorkers:
245+
"""``route_unattached_calls = False`` keeps a sub-worker out of name routing.
246+
247+
The catalog_contents fixture catalogs re-declare ``main.double`` (the example
248+
catalog's class). Without the opt-out an attach-less ``main.double`` call
249+
would have four declarers and raise; with it the example worker stays the
250+
sole owner, while an attach still reaches the fixture catalog.
251+
"""
252+
253+
@staticmethod
254+
def _request(attach_opaque_data: bytes | None) -> BindRequest:
255+
return BindRequest(
256+
function_name="double",
257+
arguments=Arguments(positional=()),
258+
function_type=FunctionType.SCALAR,
259+
input_schema=_INPUT_SCHEMA,
260+
schema_path=["main"],
261+
attach_opaque_data=attach_opaque_data,
262+
)
263+
264+
def test_unattached_call_routes_to_the_routable_worker(self) -> None:
265+
"""Only the example worker is a candidate for an attach-less call."""
266+
from vgi._test_fixtures.catalog_contents import CONTENTS_WORKERS
267+
268+
meta = MetaWorker([ExampleWorker(), *(w() for w in CONTENTS_WORKERS)])
269+
assert meta._candidates_for("double", ["main"]) == [meta._workers[0]]
270+
assert meta._resolve_function(self._request(None)) is not None
271+
272+
def test_attach_still_reaches_an_attached_only_worker(self) -> None:
273+
"""Opting out of name routing does not affect attach routing."""
274+
from vgi._test_fixtures.catalog_contents import CATALOG_PROBE, ContentsProbeWorker
275+
276+
meta = MetaWorker([ExampleWorker(), ContentsProbeWorker()])
277+
attach = meta._workers[1]._seal_attach_with_catalog(b"\x00" * 16, CATALOG_PROBE)
278+
assert meta._maybe_worker_for_attach(attach) is meta._workers[1]

‎vgi/_test_fixtures/catalog_contents.py‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@
2626
macros), split over two schemas (``main`` and ``extra``), so RPC accounting in
2727
``vgi/test/sql/integration/catalog/catalog_contents*.test`` is deterministic.
2828
Served by ``vgi-fixture-worker`` and ``vgi-fixture-http`` (MetaWorker), so the
29-
same tests run over every transport.
29+
same tests run over every transport. Their functions re-use the example
30+
catalog's classes (``main.double`` …), so the workers set
31+
``route_unattached_calls = False``: they are reachable only through their own
32+
attach, and an unattached ``main.double`` still routes to the example worker.
3033
"""
3134

3235
from __future__ import annotations
@@ -155,6 +158,8 @@ def catalog_attach(self, **kwargs: Any) -> CatalogAttachResult:
155158
class ContentsProbeWorker(Worker):
156159
"""Serves ``contents_probe``."""
157160

161+
route_unattached_calls = False
162+
158163
catalog_interface = ContentsProbeCatalog
159164
catalog_name = CATALOG_PROBE
160165
catalog = _CATALOG_PROBE
@@ -163,6 +168,8 @@ class ContentsProbeWorker(Worker):
163168
class ContentsBrokenWorker(Worker):
164169
"""Serves ``contents_broken``."""
165170

171+
route_unattached_calls = False
172+
166173
catalog_interface = ContentsBrokenCatalog
167174
catalog_name = CATALOG_BROKEN
168175
catalog = _CATALOG_BROKEN
@@ -171,6 +178,8 @@ class ContentsBrokenWorker(Worker):
171178
class ContentsLegacyWorker(Worker):
172179
"""Serves ``contents_legacy``."""
173180

181+
route_unattached_calls = False
182+
174183
catalog_interface = ContentsLegacyCatalog
175184
catalog_name = CATALOG_LEGACY
176185
catalog = _CATALOG_LEGACY
@@ -230,6 +239,8 @@ def catalog_detach(self, *, attach_opaque_data: AttachOpaqueData) -> None:
230239
class ContentsMemoryWorker(Worker):
231240
"""Serves ``contents_memory``."""
232241

242+
route_unattached_calls = False
243+
233244
catalog_interface = ContentsMemoryCatalog
234245
catalog_name = CATALOG_MEMORY
235246

‎vgi/meta_worker.py‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -239,13 +239,18 @@ def _maybe_worker_for_attach(self, attach_opaque_data: bytes | None) -> Worker |
239239
return self._workers[idx]
240240

241241
def _candidates_for(self, function_name: str, schema_path: list[str] | None) -> list[Worker]:
242-
"""Every sub-worker declaring ``function_name``, most specific first."""
242+
"""Every sub-worker declaring ``function_name``, most specific first.
243+
244+
Only sub-workers that accept unattached calls (``route_unattached_calls``)
245+
are candidates; the rest are reachable solely through their own attach.
246+
"""
247+
workers = [w for w in self._workers if type(w).route_unattached_calls]
243248
if schema_path is not None:
244249
key = (schema_path_key(schema_path), function_name)
245-
scoped = [w for w in self._workers if key in type(w)._build_schema_registry()]
250+
scoped = [w for w in workers if key in type(w)._build_schema_registry()]
246251
if scoped:
247252
return scoped
248-
return [w for w in self._workers if function_name in type(w)._build_registry()]
253+
return [w for w in workers if function_name in type(w)._build_registry()]
249254

250255
def _worker_for_unattached(self, function_name: str, schema_path: list[str] | None) -> Worker | None:
251256
"""Pick a sub-worker for a call that carries no usable attach.

‎vgi/worker.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1145,6 +1145,9 @@ class Worker:
11451145
to use the auto-generated default (or disable the catalog).
11461146
catalog_name: Name of the default catalog; set to ``None`` to disable the
11471147
default catalog.
1148+
route_unattached_calls: Whether a [`MetaWorker`][]
1149+
may route a call carrying no attach to this worker by function name.
1150+
``False`` makes the worker reachable only through its own catalog.
11481151
catalog: Optional declarative `[`Catalog`][]` describing the worker's
11491152
schemas, tables, and views.
11501153
"""
@@ -1158,6 +1161,12 @@ class Worker:
11581161
protocol_class: ClassVar[type[VgiProtocol]] = VgiProtocol # type: ignore[type-abstract]
11591162
catalog_interface: type[CatalogInterface] | None = None
11601163
catalog_name: str | None = "functions" # Set to None to disable default catalog
1164+
# Whether a MetaWorker may route a call that carries no attach (a
1165+
# non-catalog caller) to this worker by function name. A worker whose
1166+
# functions are only meant to be reached through its own attached catalog
1167+
# sets this False, so re-declaring a name another sub-worker owns does not
1168+
# make that name ambiguous for unattached callers.
1169+
route_unattached_calls: ClassVar[bool] = True
11611170
catalog: Catalog | None = None
11621171
_registry: dict[str, list[type[Function]]] | None = None
11631172
# (lowercased structural schema path, function name) -> classes declared there.

0 commit comments

Comments
 (0)