From ba326ac0bce153d687bef02f28417e2a567cbefb Mon Sep 17 00:00:00 2001 From: soyuka Date: Wed, 4 Mar 2026 17:56:54 +0100 Subject: [PATCH] fix: clear() wiping dynamically registered tools by tracking discovered state on references clear() previously removed all non-manual elements, which destroyed dynamically registered tools when setDiscoveryState() was called on the next request cycle. Add isDiscovered flag to ElementReference so clear() only removes elements that were imported via setDiscoveryState(), preserving both manual and dynamic registrations. --- src/Capability/Registry.php | 60 +-- src/Capability/Registry/ElementReference.php | 1 + src/Capability/Registry/PromptReference.php | 3 +- src/Capability/Registry/ResourceReference.php | 3 +- .../Registry/ResourceTemplateReference.php | 3 +- src/Capability/Registry/ToolReference.php | 3 +- ...mbinedRegistrationTest-resources_list.json | 4 +- ...onTest-resources_read-config_priority.json | 2 +- .../Unit/Capability/Registry/RegistryTest.php | 431 ++++++++++++++++++ tests/Unit/Capability/RegistryTest.php | 75 +-- 10 files changed, 509 insertions(+), 76 deletions(-) create mode 100644 tests/Unit/Capability/Registry/RegistryTest.php diff --git a/src/Capability/Registry.php b/src/Capability/Registry.php index 2a327ae4..0e579536 100644 --- a/src/Capability/Registry.php +++ b/src/Capability/Registry.php @@ -161,36 +161,10 @@ public function registerPrompt( public function clear(): void { - $clearCount = 0; - - foreach ($this->tools as $name => $tool) { - if (!$tool->isManual) { - unset($this->tools[$name]); - ++$clearCount; - } - } - foreach ($this->resources as $uri => $resource) { - if (!$resource->isManual) { - unset($this->resources[$uri]); - ++$clearCount; - } - } - foreach ($this->prompts as $name => $prompt) { - if (!$prompt->isManual) { - unset($this->prompts[$name]); - ++$clearCount; - } - } - foreach ($this->resourceTemplates as $uriTemplate => $template) { - if (!$template->isManual) { - unset($this->resourceTemplates[$uriTemplate]); - ++$clearCount; - } - } - - if ($clearCount > 0) { - $this->logger->debug(\sprintf('Removed %d discovered elements from internal registry.', $clearCount)); - } + $this->tools = array_filter($this->tools, static fn ($t) => !$t->isDiscovered); + $this->resources = array_filter($this->resources, static fn ($r) => !$r->isDiscovered); + $this->prompts = array_filter($this->prompts, static fn ($p) => !$p->isDiscovered); + $this->resourceTemplates = array_filter($this->resourceTemplates, static fn ($t) => !$t->isDiscovered); } public function hasTools(): bool @@ -344,10 +318,10 @@ public function getPrompt(string $name): PromptReference public function getDiscoveryState(): DiscoveryState { return new DiscoveryState( - tools: array_filter($this->tools, static fn ($tool) => !$tool->isManual), - resources: array_filter($this->resources, static fn ($resource) => !$resource->isManual), - prompts: array_filter($this->prompts, static fn ($prompt) => !$prompt->isManual), - resourceTemplates: array_filter($this->resourceTemplates, static fn ($template) => !$template->isManual), + tools: array_filter($this->tools, static fn ($t) => $t->isDiscovered), + resources: array_filter($this->resources, static fn ($r) => $r->isDiscovered), + prompts: array_filter($this->prompts, static fn ($p) => $p->isDiscovered), + resourceTemplates: array_filter($this->resourceTemplates, static fn ($t) => $t->isDiscovered), ); } @@ -360,21 +334,29 @@ public function setDiscoveryState(DiscoveryState $state): void // Clear existing discovered elements $this->clear(); - // Import new discovered elements + // Import new discovered elements — skip any that conflict with manual or dynamic registrations foreach ($state->getTools() as $name => $tool) { - $this->tools[$name] = $tool; + if (!isset($this->tools[$name])) { + $this->tools[$name] = new ToolReference($tool->tool, $tool->handler, isDiscovered: true); + } } foreach ($state->getResources() as $uri => $resource) { - $this->resources[$uri] = $resource; + if (!isset($this->resources[$uri])) { + $this->resources[$uri] = new ResourceReference($resource->resource, $resource->handler, isDiscovered: true); + } } foreach ($state->getPrompts() as $name => $prompt) { - $this->prompts[$name] = $prompt; + if (!isset($this->prompts[$name])) { + $this->prompts[$name] = new PromptReference($prompt->prompt, $prompt->handler, completionProviders: $prompt->completionProviders, isDiscovered: true); + } } foreach ($state->getResourceTemplates() as $uriTemplate => $template) { - $this->resourceTemplates[$uriTemplate] = $template; + if (!isset($this->resourceTemplates[$uriTemplate])) { + $this->resourceTemplates[$uriTemplate] = new ResourceTemplateReference($template->resourceTemplate, $template->handler, completionProviders: $template->completionProviders, isDiscovered: true); + } } // Dispatch events for the imported elements diff --git a/src/Capability/Registry/ElementReference.php b/src/Capability/Registry/ElementReference.php index 6425ba13..9fd25764 100644 --- a/src/Capability/Registry/ElementReference.php +++ b/src/Capability/Registry/ElementReference.php @@ -24,6 +24,7 @@ class ElementReference public function __construct( public readonly \Closure|array|string $handler, public readonly bool $isManual = false, + public readonly bool $isDiscovered = false, ) { } } diff --git a/src/Capability/Registry/PromptReference.php b/src/Capability/Registry/PromptReference.php index 5de3a195..345ec03d 100644 --- a/src/Capability/Registry/PromptReference.php +++ b/src/Capability/Registry/PromptReference.php @@ -31,8 +31,9 @@ public function __construct( \Closure|array|string $handler, bool $isManual = false, public readonly array $completionProviders = [], + bool $isDiscovered = false, ) { - parent::__construct($handler, $isManual); + parent::__construct($handler, $isManual, $isDiscovered); } /** diff --git a/src/Capability/Registry/ResourceReference.php b/src/Capability/Registry/ResourceReference.php index d65f461e..3f717e81 100644 --- a/src/Capability/Registry/ResourceReference.php +++ b/src/Capability/Registry/ResourceReference.php @@ -29,8 +29,9 @@ public function __construct( public readonly Resource $resource, callable|array|string $handler, bool $isManual = false, + bool $isDiscovered = false, ) { - parent::__construct($handler, $isManual); + parent::__construct($handler, $isManual, $isDiscovered); } /** diff --git a/src/Capability/Registry/ResourceTemplateReference.php b/src/Capability/Registry/ResourceTemplateReference.php index ef2d915a..9483fb23 100644 --- a/src/Capability/Registry/ResourceTemplateReference.php +++ b/src/Capability/Registry/ResourceTemplateReference.php @@ -38,8 +38,9 @@ public function __construct( callable|array|string $handler, bool $isManual = false, public readonly array $completionProviders = [], + bool $isDiscovered = false, ) { - parent::__construct($handler, $isManual); + parent::__construct($handler, $isManual, $isDiscovered); $this->compileTemplate(); } diff --git a/src/Capability/Registry/ToolReference.php b/src/Capability/Registry/ToolReference.php index 9aa5a3c9..ba9af0bc 100644 --- a/src/Capability/Registry/ToolReference.php +++ b/src/Capability/Registry/ToolReference.php @@ -29,8 +29,9 @@ public function __construct( public readonly Tool $tool, callable|array|string $handler, bool $isManual = false, + bool $isDiscovered = false, ) { - parent::__construct($handler, $isManual); + parent::__construct($handler, $isManual, $isDiscovered); } /** diff --git a/tests/Inspector/Http/snapshots/HttpCombinedRegistrationTest-resources_list.json b/tests/Inspector/Http/snapshots/HttpCombinedRegistrationTest-resources_list.json index 2c7912e4..f3646e57 100644 --- a/tests/Inspector/Http/snapshots/HttpCombinedRegistrationTest-resources_list.json +++ b/tests/Inspector/Http/snapshots/HttpCombinedRegistrationTest-resources_list.json @@ -1,9 +1,9 @@ { "resources": [ { - "name": "priority_config_discovered", + "name": "priority_config_manual", "uri": "config://priority", - "description": "A resource discovered via attributes.\n\nThis will be overridden by a manual registration with the same URI." + "description": "Manually registered resource that overrides a discovered one." } ] } diff --git a/tests/Inspector/Http/snapshots/HttpCombinedRegistrationTest-resources_read-config_priority.json b/tests/Inspector/Http/snapshots/HttpCombinedRegistrationTest-resources_read-config_priority.json index 053dcceb..85eef65a 100644 --- a/tests/Inspector/Http/snapshots/HttpCombinedRegistrationTest-resources_read-config_priority.json +++ b/tests/Inspector/Http/snapshots/HttpCombinedRegistrationTest-resources_read-config_priority.json @@ -3,7 +3,7 @@ { "uri": "config://priority", "mimeType": "text/plain", - "text": "Discovered Priority Config: Low" + "text": "Manual Priority Config: HIGH (overrides discovered)" } ] } diff --git a/tests/Unit/Capability/Registry/RegistryTest.php b/tests/Unit/Capability/Registry/RegistryTest.php new file mode 100644 index 00000000..dd861bf9 --- /dev/null +++ b/tests/Unit/Capability/Registry/RegistryTest.php @@ -0,0 +1,431 @@ +logger = $this->createMock(LoggerInterface::class); + $this->registry = new Registry(null, $this->logger); + } + + public function testRegisterToolWithManualFlag(): void + { + $tool = $this->createValidTool('test_tool'); + $handler = fn () => 'result'; + + $this->registry->registerTool($tool, $handler, true); + + $toolRef = $this->registry->getTool('test_tool'); + $this->assertTrue($toolRef->isManual); + } + + public function testRegisterToolIgnoresDiscoveredWhenManualExists(): void + { + $manualTool = $this->createValidTool('test_tool'); + $discoveredTool = $this->createValidTool('test_tool'); + + $this->registry->registerTool($manualTool, fn () => 'manual', true); + + $this->logger + ->expects($this->once()) + ->method('debug') + ->with('Ignoring discovered tool "test_tool" as it conflicts with a manually registered one.'); + + $this->registry->registerTool($discoveredTool, fn () => 'discovered', false); + + $toolRef = $this->registry->getTool('test_tool'); + $this->assertTrue($toolRef->isManual); + } + + public function testRegisterToolOverridesDiscoveredWithManual(): void + { + $discoveredTool = $this->createValidTool('test_tool'); + $manualTool = $this->createValidTool('test_tool'); + + $this->registry->registerTool($discoveredTool, fn () => 'discovered', false); + $this->registry->registerTool($manualTool, fn () => 'manual', true); + + $toolRef = $this->registry->getTool('test_tool'); + $this->assertTrue($toolRef->isManual); + } + + public function testRegisterResourceWithManualFlag(): void + { + $resource = $this->createValidResource('test://resource'); + $handler = fn () => 'content'; + + $this->registry->registerResource($resource, $handler, true); + + $resourceRef = $this->registry->getResource('test://resource'); + $this->assertTrue($resourceRef->isManual); + } + + public function testRegisterResourceIgnoresDiscoveredWhenManualExists(): void + { + $manualResource = $this->createValidResource('test://resource'); + $discoveredResource = $this->createValidResource('test://resource'); + + $this->registry->registerResource($manualResource, fn () => 'manual', true); + + $this->logger + ->expects($this->once()) + ->method('debug') + ->with('Ignoring discovered resource "test://resource" as it conflicts with a manually registered one.'); + + $this->registry->registerResource($discoveredResource, fn () => 'discovered', false); + + $resourceRef = $this->registry->getResource('test://resource'); + $this->assertTrue($resourceRef->isManual); + } + + public function testRegisterResourceTemplateWithCompletionProviders(): void + { + $template = $this->createValidResourceTemplate('test://{id}'); + $completionProviders = ['id' => EnumCompletionProvider::class]; + + $this->registry->registerResourceTemplate($template, fn () => 'content', $completionProviders); + + $templateRef = $this->registry->getResourceTemplate('test://{id}'); + $this->assertEquals($completionProviders, $templateRef->completionProviders); + } + + public function testRegisterResourceTemplateIgnoresDiscoveredWhenManualExists(): void + { + $manualTemplate = $this->createValidResourceTemplate('test://{id}'); + $discoveredTemplate = $this->createValidResourceTemplate('test://{id}'); + + $this->registry->registerResourceTemplate($manualTemplate, fn () => 'manual', [], true); + + $this->logger + ->expects($this->once()) + ->method('debug') + ->with('Ignoring discovered template "test://{id}" as it conflicts with a manually registered one.'); + + $this->registry->registerResourceTemplate($discoveredTemplate, fn () => 'discovered', [], false); + + $templateRef = $this->registry->getResourceTemplate('test://{id}'); + $this->assertTrue($templateRef->isManual); + } + + public function testRegisterPromptWithCompletionProviders(): void + { + $prompt = $this->createValidPrompt('test_prompt'); + $completionProviders = ['param' => EnumCompletionProvider::class]; + + $this->registry->registerPrompt($prompt, fn () => [], $completionProviders); + + $promptRef = $this->registry->getPrompt('test_prompt'); + $this->assertEquals($completionProviders, $promptRef->completionProviders); + } + + public function testRegisterPromptIgnoresDiscoveredWhenManualExists(): void + { + $manualPrompt = $this->createValidPrompt('test_prompt'); + $discoveredPrompt = $this->createValidPrompt('test_prompt'); + + $this->registry->registerPrompt($manualPrompt, fn () => 'manual', [], true); + + $this->logger + ->expects($this->once()) + ->method('debug') + ->with('Ignoring discovered prompt "test_prompt" as it conflicts with a manually registered one.'); + + $this->registry->registerPrompt($discoveredPrompt, fn () => 'discovered', [], false); + + $promptRef = $this->registry->getPrompt('test_prompt'); + $this->assertTrue($promptRef->isManual); + } + + public function testClearRemovesOnlyDiscoveredElements(): void + { + $manualTool = $this->createValidTool('manual_tool'); + $discoveredTool = $this->createValidTool('discovered_tool'); + $manualResource = $this->createValidResource('test://manual'); + $discoveredResource = $this->createValidResource('test://discovered'); + $manualPrompt = $this->createValidPrompt('manual_prompt'); + $discoveredPrompt = $this->createValidPrompt('discovered_prompt'); + $manualTemplate = $this->createValidResourceTemplate('manual://{id}'); + $discoveredTemplate = $this->createValidResourceTemplate('discovered://{id}'); + + // Register manual elements directly + $this->registry->registerTool($manualTool, fn () => 'manual', true); + $this->registry->registerResource($manualResource, fn () => 'manual', true); + $this->registry->registerPrompt($manualPrompt, fn () => [], [], true); + $this->registry->registerResourceTemplate($manualTemplate, fn () => 'manual', [], true); + + // Import discovered elements via setDiscoveryState + $this->registry->setDiscoveryState(new DiscoveryState( + tools: ['discovered_tool' => new ToolReference($discoveredTool, fn () => 'discovered', false)], + resources: ['test://discovered' => new ResourceReference($discoveredResource, fn () => 'discovered', false)], + prompts: ['discovered_prompt' => new PromptReference($discoveredPrompt, fn () => [], false)], + resourceTemplates: ['discovered://{id}' => new ResourceTemplateReference($discoveredTemplate, fn () => 'discovered', false)], + )); + + $this->registry->clear(); + + // Manual elements survive + $this->assertNotNull($this->registry->getTool('manual_tool')); + $this->assertNotNull($this->registry->getResource('test://manual')); + $this->assertNotNull($this->registry->getPrompt('manual_prompt')); + $this->assertNotNull($this->registry->getResourceTemplate('manual://{id}')); + + // Discovered elements are gone + $this->assertException(ToolNotFoundException::class, fn () => $this->registry->getTool('discovered_tool')); + $this->assertException(ResourceNotFoundException::class, fn () => $this->registry->getResource('test://discovered', false)); + $this->assertException(PromptNotFoundException::class, fn () => $this->registry->getPrompt('discovered_prompt')); + $this->assertException(ResourceNotFoundException::class, fn () => $this->registry->getResourceTemplate('discovered://{id}')); + } + + public function testRegisterToolHandlesStringHandler(): void + { + $tool = $this->createValidTool('test_tool'); + $handler = 'TestClass::testMethod'; + + $this->registry->registerTool($tool, $handler); + + $toolRef = $this->registry->getTool('test_tool'); + $this->assertEquals($handler, $toolRef->handler); + } + + public function testRegisterToolHandlesArrayHandler(): void + { + $tool = $this->createValidTool('test_tool'); + $handler = ['TestClass', 'testMethod']; + + $this->registry->registerTool($tool, $handler); + + $toolRef = $this->registry->getTool('test_tool'); + $this->assertEquals($handler, $toolRef->handler); + } + + public function testRegisterResourceHandlesCallableHandler(): void + { + $resource = $this->createValidResource('test://resource'); + $handler = fn () => 'content'; + + $this->registry->registerResource($resource, $handler); + + $resourceRef = $this->registry->getResource('test://resource'); + $this->assertEquals($handler, $resourceRef->handler); + } + + public function testMultipleRegistrationsOfSameElementWithSameType(): void + { + $tool1 = $this->createValidTool('test_tool'); + $tool2 = $this->createValidTool('test_tool'); + + $this->registry->registerTool($tool1, fn () => 'first', false); + $this->registry->registerTool($tool2, fn () => 'second', false); + + // Second registration should override the first + $toolRef = $this->registry->getTool('test_tool'); + $this->assertEquals('second', ($toolRef->handler)()); + } + + public function testClearPreservesDynamicallyRegisteredElements(): void + { + // 1. Register a manual tool + $manualTool = $this->createValidTool('manual_tool'); + $this->registry->registerTool($manualTool, fn () => 'manual', true); + + // 2. Import discovered tools via setDiscoveryState + $discoveredTool = $this->createValidTool('discovered_tool'); + $this->registry->setDiscoveryState(new DiscoveryState( + tools: ['discovered_tool' => new ToolReference($discoveredTool, fn () => 'discovered', false)], + )); + + // 3. Register a dynamic tool (not manual, not discovered) + $dynamicTool = $this->createValidTool('dynamic_tool'); + $this->registry->registerTool($dynamicTool, fn () => 'dynamic', false); + + // 4. Restore discovery state again (simulates next HTTP request) + $discoveredTool2 = $this->createValidTool('discovered_tool_v2'); + $this->registry->setDiscoveryState(new DiscoveryState( + tools: ['discovered_tool_v2' => new ToolReference($discoveredTool2, fn () => 'discovered_v2', false)], + )); + + // Manual tool survives + $this->assertNotNull($this->registry->getTool('manual_tool')); + // Dynamic tool survives + $this->assertNotNull($this->registry->getTool('dynamic_tool')); + // Old discovered tool is gone + $this->assertException(ToolNotFoundException::class, fn () => $this->registry->getTool('discovered_tool')); + // New discovered tool is present + $this->assertNotNull($this->registry->getTool('discovered_tool_v2')); + } + + public function testGetDiscoveryStateExcludesDynamicTools(): void + { + // Import discovered tool via setDiscoveryState + $discoveredTool = $this->createValidTool('discovered_tool'); + $this->registry->setDiscoveryState(new DiscoveryState( + tools: ['discovered_tool' => new ToolReference($discoveredTool, fn () => 'discovered', false)], + )); + + // Register a dynamic tool + $dynamicTool = $this->createValidTool('dynamic_tool'); + $this->registry->registerTool($dynamicTool, fn () => 'dynamic', false); + + // Register a manual tool + $manualTool = $this->createValidTool('manual_tool'); + $this->registry->registerTool($manualTool, fn () => 'manual', true); + + $state = $this->registry->getDiscoveryState(); + + $this->assertArrayHasKey('discovered_tool', $state->getTools()); + $this->assertArrayNotHasKey('dynamic_tool', $state->getTools()); + $this->assertArrayNotHasKey('manual_tool', $state->getTools()); + } + + public function testSetDiscoveryStateRoundTrip(): void + { + // Import initial discovered state + $tool = $this->createValidTool('round_trip_tool'); + $resource = $this->createValidResource('test://round-trip'); + $prompt = $this->createValidPrompt('round_trip_prompt'); + $template = $this->createValidResourceTemplate('round-trip://{id}'); + + $initialState = new DiscoveryState( + tools: ['round_trip_tool' => new ToolReference($tool, fn () => 'result', false)], + resources: ['test://round-trip' => new ResourceReference($resource, fn () => 'content', false)], + prompts: ['round_trip_prompt' => new PromptReference($prompt, fn () => [], false)], + resourceTemplates: ['round-trip://{id}' => new ResourceTemplateReference($template, fn () => 'tpl', false)], + ); + + $this->registry->setDiscoveryState($initialState); + + // Round-trip: get and set again + $exportedState = $this->registry->getDiscoveryState(); + $this->registry->setDiscoveryState($exportedState); + + // All elements still present + $this->assertNotNull($this->registry->getTool('round_trip_tool')); + $this->assertNotNull($this->registry->getResource('test://round-trip')); + $this->assertNotNull($this->registry->getPrompt('round_trip_prompt')); + $this->assertNotNull($this->registry->getResourceTemplate('round-trip://{id}')); + + // Exported state matches + $reExportedState = $this->registry->getDiscoveryState(); + $this->assertCount(\count($exportedState->getTools()), $reExportedState->getTools()); + $this->assertCount(\count($exportedState->getResources()), $reExportedState->getResources()); + $this->assertCount(\count($exportedState->getPrompts()), $reExportedState->getPrompts()); + $this->assertCount(\count($exportedState->getResourceTemplates()), $reExportedState->getResourceTemplates()); + } + + public function testSetDiscoveryStateDoesNotOverwriteManualOrDynamicTools(): void + { + // Register a manual tool + $manualTool = $this->createValidTool('conflict_tool'); + $this->registry->registerTool($manualTool, fn () => 'manual_result', true); + + // Register a dynamic tool + $dynamicTool = $this->createValidTool('dynamic_conflict'); + $this->registry->registerTool($dynamicTool, fn () => 'dynamic_result', false); + + // Try to import discovered tools with same names + $discoveredConflict = $this->createValidTool('conflict_tool'); + $discoveredDynConflict = $this->createValidTool('dynamic_conflict'); + $this->registry->setDiscoveryState(new DiscoveryState( + tools: [ + 'conflict_tool' => new ToolReference($discoveredConflict, fn () => 'discovered_result', false), + 'dynamic_conflict' => new ToolReference($discoveredDynConflict, fn () => 'discovered_dyn_result', false), + ], + )); + + // Manual tool preserved with original handler + $manualRef = $this->registry->getTool('conflict_tool'); + $this->assertTrue($manualRef->isManual); + $this->assertEquals('manual_result', ($manualRef->handler)()); + + // Dynamic tool preserved with original handler + $dynamicRef = $this->registry->getTool('dynamic_conflict'); + $this->assertEquals('dynamic_result', ($dynamicRef->handler)()); + } + + private function createValidTool(string $name): Tool + { + return new Tool( + name: $name, + inputSchema: [ + 'type' => 'object', + 'properties' => [ + 'param' => ['type' => 'string'], + ], + 'required' => null, + ], + description: "Test tool: {$name}", + annotations: null, + ); + } + + private function createValidResource(string $uri): Resource + { + return new Resource( + uri: $uri, + name: 'test_resource', + description: 'Test resource', + mimeType: 'text/plain', + ); + } + + private function createValidResourceTemplate(string $uriTemplate): ResourceTemplate + { + return new ResourceTemplate( + uriTemplate: $uriTemplate, + name: 'test_template', + description: 'Test resource template', + mimeType: 'text/plain', + ); + } + + private function createValidPrompt(string $name): Prompt + { + return new Prompt( + name: $name, + description: "Test prompt: {$name}", + arguments: [], + ); + } + + private function assertException(string $exceptionClass, callable $callback): void + { + try { + $callback(); + $this->fail(\sprintf('Expected exception %s was not thrown.', $exceptionClass)); + } catch (\Throwable $e) { + $this->assertInstanceOf($exceptionClass, $e); + } + } +} diff --git a/tests/Unit/Capability/RegistryTest.php b/tests/Unit/Capability/RegistryTest.php index e8b19585..eb475c85 100644 --- a/tests/Unit/Capability/RegistryTest.php +++ b/tests/Unit/Capability/RegistryTest.php @@ -12,6 +12,7 @@ namespace Mcp\Tests\Unit\Capability; use Mcp\Capability\Completion\EnumCompletionProvider; +use Mcp\Capability\Discovery\DiscoveryState; use Mcp\Capability\Registry; use Mcp\Capability\Registry\PromptReference; use Mcp\Capability\Registry\ResourceReference; @@ -426,59 +427,73 @@ public function testClearRemovesOnlyDiscoveredElements(): void $manualTemplate = $this->createValidResourceTemplate('manual://{id}'); $discoveredTemplate = $this->createValidResourceTemplate('discovered://{id}'); + // Register manual elements directly $this->registry->registerTool($manualTool, static fn () => 'manual', true); - $this->registry->registerTool($discoveredTool, static fn () => 'discovered'); $this->registry->registerResource($manualResource, static fn () => 'manual', true); - $this->registry->registerResource($discoveredResource, static fn () => 'discovered'); $this->registry->registerPrompt($manualPrompt, static fn () => [], [], true); - $this->registry->registerPrompt($discoveredPrompt, static fn () => []); $this->registry->registerResourceTemplate($manualTemplate, static fn () => 'manual', [], true); - $this->registry->registerResourceTemplate($discoveredTemplate, static fn () => 'discovered'); - // Test that all elements exist - $this->registry->getTool('manual_tool'); - $this->registry->getResource('test://manual'); - $this->registry->getPrompt('manual_prompt'); - $this->registry->getResourceTemplate('manual://{id}'); - $this->registry->getTool('discovered_tool'); - $this->registry->getResource('test://discovered'); - $this->registry->getPrompt('discovered_prompt'); - $this->registry->getResourceTemplate('discovered://{id}'); + // Import discovered elements via setDiscoveryState + $this->registry->setDiscoveryState(new DiscoveryState( + tools: ['discovered_tool' => new ToolReference($discoveredTool, static fn () => 'discovered')], + resources: ['test://discovered' => new ResourceReference($discoveredResource, static fn () => 'discovered')], + prompts: ['discovered_prompt' => new PromptReference($discoveredPrompt, static fn () => [])], + resourceTemplates: ['discovered://{id}' => new ResourceTemplateReference($discoveredTemplate, static fn () => 'discovered')], + )); + + // All elements exist before clear + $this->assertInstanceOf(ToolReference::class, $this->registry->getTool('discovered_tool')); $this->registry->clear(); - // Manual elements should still exist - $this->registry->getTool('manual_tool'); - $this->registry->getResource('test://manual'); - $this->registry->getPrompt('manual_prompt'); - $this->registry->getResourceTemplate('manual://{id}'); + // Manual elements survive + $this->assertInstanceOf(ToolReference::class, $this->registry->getTool('manual_tool')); + $this->assertInstanceOf(ResourceReference::class, $this->registry->getResource('test://manual')); + $this->assertInstanceOf(PromptReference::class, $this->registry->getPrompt('manual_prompt')); + $this->assertInstanceOf(ResourceTemplateReference::class, $this->registry->getResourceTemplate('manual://{id}')); - // Test that all discovered elements throw exceptions + // Discovered elements are gone $this->expectException(ToolNotFoundException::class); $this->registry->getTool('discovered_tool'); + } + + public function testClearRemovesDiscoveredResources(): void + { + $discoveredResource = $this->createValidResource('test://discovered'); + $this->registry->setDiscoveryState(new DiscoveryState( + resources: ['test://discovered' => new ResourceReference($discoveredResource, static fn () => 'discovered')], + )); + + $this->registry->clear(); $this->expectException(ResourceNotFoundException::class); $this->registry->getResource('test://discovered'); + } + + public function testClearRemovesDiscoveredPrompts(): void + { + $discoveredPrompt = $this->createValidPrompt('discovered_prompt'); + $this->registry->setDiscoveryState(new DiscoveryState( + prompts: ['discovered_prompt' => new PromptReference($discoveredPrompt, static fn () => [])], + )); + + $this->registry->clear(); $this->expectException(PromptNotFoundException::class); $this->registry->getPrompt('discovered_prompt'); - - $this->expectException(ResourceNotFoundException::class); - $this->registry->getResourceTemplate('discovered://{id}'); } - public function testClearLogsNothingWhenNoDiscoveredElements(): void + public function testClearRemovesDiscoveredResourceTemplates(): void { - $manualTool = $this->createValidTool('manual_tool'); - $this->registry->registerTool($manualTool, static fn () => 'manual', true); - - $this->logger - ->expects($this->never()) - ->method('debug'); + $discoveredTemplate = $this->createValidResourceTemplate('discovered://{id}'); + $this->registry->setDiscoveryState(new DiscoveryState( + resourceTemplates: ['discovered://{id}' => new ResourceTemplateReference($discoveredTemplate, static fn () => 'discovered')], + )); $this->registry->clear(); - $this->registry->getTool('manual_tool'); + $this->expectException(ResourceNotFoundException::class); + $this->registry->getResourceTemplate('discovered://{id}'); } public function testRegisterToolHandlesStringHandler(): void