php-sdk/tests/Unit/Capability
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Christopher Hertel c5dbfb64e5
[Server][Capability] Do not announce an externally loaded registry as changed (#490)
* [Capability] Do not announce an externally loaded registry as changed

`Registry` suppresses its `*ListChangedEvent`s while it is loading — `dispatch()`
returns early on the `loading` guard, so the elements a loader registers are the
registry's initial contents rather than a change to them.
`testListChangedEventsAreSuppressedDuringTheDeferredLoad` pins that.

The guard only covers `Registry::load()`, which needs the loader the constructor
took. A registry the caller built cannot be given one that way, so
`Builder::resolve()` loads it from the outside instead — by calling
`$chainLoader->load($registry)` directly, which never sets the guard. Every
element then dispatches on the way in.

With a notification bus configured that is not quiet. `PublishingEventDispatcher`
turns each event into a published notification, so every `build()` puts one
`list_changed` per element on the bus for a registry that did not change. Under
PHP-FPM, where the server is built per request and `Psr16NotificationBus` is
shared and persistent, every request broadcasts its whole element list to every
open `subscriptions/listen` stream and consumes the 256-entry backlog — after
which a reader that fell behind silently skips real notifications.

Split the guarded body of `load()` into `loadFrom(LoaderInterface $loader)` and
route the custom-registry branch through it.

`loadFrom()` takes only the `loading` guard, not the `loaded` bookkeeping, which
stays with `load()`. The loader it runs belongs to the caller, so it cannot stand
in for the one the registry was constructed with: marking the registry loaded
would retire a constructor loader that never ran, and would make a second
`loadFrom()` — one registry handed to two builders — a silent no-op. Keeping
`loaded` out of it also keeps `load()`'s promise that a transient failure is
retried on the next read, including when the failing loader reads the registry
during its own run.

`RegistryInterface` declares no `load()`, so this stays on the concrete
`Registry`, and a third-party implementation keeps the path it has today.

* Apply batched suggestions from code review

Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
2026-08-30 01:10:56 +02:00
..
Attribute [Schema][Server] Add top-level title to Tool (#288) 2026-04-26 15:37:40 +02:00
Discovery [Server] Keep injectable parameters out of the published inputSchema (#482) 2026-08-25 01:12:29 +02:00
Formatter [Capability] Harden structuredContent extraction (#417) 2026-08-15 04:03:20 +02:00
Logger style: apply php-cs-fixer and PHPStan fixes (#289) 2026-04-20 20:49:15 +02:00
Registry [Capability] Cast variadic tool parameters element-by-element (#465) 2026-08-29 15:46:40 +02:00
Tool Add tool name validation for SEP-986 (#140) 2025-11-13 21:42:30 +01:00
RegistryTest.php [Server][Capability] Do not announce an externally loaded registry as changed (#490) 2026-08-30 01:10:56 +02:00