php-sdk/tests/Unit/Capability/Discovery/DiscoveryStateTest.php
Christopher Hertel 967c5cda4d
[Server] Untangle origin tracking from Registry (#302)
Drop the isManual flag from ElementReference and the discovery-state methods
from RegistryInterface. The Registry becomes a flat last-write-wins map;
DiscoveryLoader owns its own owned-set bookkeeping so re-running discovery
only removes elements it itself contributed. Adds ChainLoader for explicit
composition. Manual-over-discovered precedence is preserved via loader
ordering, not a flag on the reference.
2026-05-17 14:03:31 +02:00

164 lines
5.6 KiB
PHP

<?php
/*
* This file is part of the official PHP MCP SDK.
*
* A collaboration between Symfony and the PHP Foundation.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mcp\Tests\Unit\Capability\Discovery;
use Mcp\Capability\Discovery\DiscoveryState;
use Mcp\Capability\Registry\PromptReference;
use Mcp\Capability\Registry\ResourceReference;
use Mcp\Capability\Registry\ResourceTemplateReference;
use Mcp\Capability\Registry\ToolReference;
use Mcp\Schema\Prompt;
use Mcp\Schema\Resource;
use Mcp\Schema\ResourceTemplate;
use Mcp\Schema\Tool;
use PHPUnit\Framework\TestCase;
class DiscoveryStateTest extends TestCase
{
public function testObsoletedByReturnsEntriesAbsentFromNext(): void
{
$t1 = $this->tool('t1');
$t2 = $this->tool('t2');
$owned = new DiscoveryState(
tools: ['t1' => $t1, 't2' => $t2],
);
$next = new DiscoveryState(
tools: ['t2' => $this->tool('t2'), 't3' => $this->tool('t3')],
);
$obsolete = $owned->obsoletedBy($next);
$this->assertSame(['t1' => $t1], $obsolete->getTools());
}
public function testObsoletedByIsAsymmetricAndIgnoresValuesOnSharedKeys(): void
{
// Same key, different reference instance — must NOT be reported as obsolete.
$owned = new DiscoveryState(
tools: ['t' => $this->tool('t')],
);
$next = new DiscoveryState(
tools: ['t' => $this->tool('t')],
);
$this->assertTrue($owned->obsoletedBy($next)->isEmpty());
}
public function testObsoletedByOnEmptyNextReturnsAllOwned(): void
{
$owned = new DiscoveryState(
tools: ['t' => $this->tool('t')],
resources: ['r://x' => $this->resource('r://x')],
prompts: ['p' => $this->prompt('p')],
resourceTemplates: ['x://{id}' => $this->template('x://{id}')],
);
$obsolete = $owned->obsoletedBy(new DiscoveryState());
$this->assertSame(['t'], array_keys($obsolete->getTools()));
$this->assertSame(['r://x'], array_keys($obsolete->getResources()));
$this->assertSame(['p'], array_keys($obsolete->getPrompts()));
$this->assertSame(['x://{id}'], array_keys($obsolete->getResourceTemplates()));
}
public function testObsoletedByOnEmptyOwnedReturnsEmpty(): void
{
$next = new DiscoveryState(
tools: ['t' => $this->tool('t')],
);
$this->assertTrue((new DiscoveryState())->obsoletedBy($next)->isEmpty());
}
public function testObsoletedByKeepsKindsIndependent(): void
{
// A key shared across different kinds must not cancel out.
$owned = new DiscoveryState(
tools: ['shared' => $this->tool('shared')],
prompts: ['shared' => $this->prompt('shared')],
);
$next = new DiscoveryState(
tools: ['shared' => $this->tool('shared')],
// 'shared' prompt is gone.
);
$obsolete = $owned->obsoletedBy($next);
$this->assertSame([], $obsolete->getTools());
$this->assertSame(['shared'], array_keys($obsolete->getPrompts()));
}
public function testObsoletedByAcrossAllKindsAtOnce(): void
{
$owned = new DiscoveryState(
tools: ['keep_t' => $this->tool('keep_t'), 'drop_t' => $this->tool('drop_t')],
resources: ['r://keep' => $this->resource('r://keep'), 'r://drop' => $this->resource('r://drop')],
prompts: ['keep_p' => $this->prompt('keep_p'), 'drop_p' => $this->prompt('drop_p')],
resourceTemplates: ['keep://{id}' => $this->template('keep://{id}'), 'drop://{id}' => $this->template('drop://{id}')],
);
$next = new DiscoveryState(
tools: ['keep_t' => $this->tool('keep_t')],
resources: ['r://keep' => $this->resource('r://keep')],
prompts: ['keep_p' => $this->prompt('keep_p')],
resourceTemplates: ['keep://{id}' => $this->template('keep://{id}')],
);
$obsolete = $owned->obsoletedBy($next);
$this->assertSame(['drop_t'], array_keys($obsolete->getTools()));
$this->assertSame(['r://drop'], array_keys($obsolete->getResources()));
$this->assertSame(['drop_p'], array_keys($obsolete->getPrompts()));
$this->assertSame(['drop://{id}'], array_keys($obsolete->getResourceTemplates()));
}
private function tool(string $name): ToolReference
{
return new ToolReference(
new Tool(
name: $name,
title: null,
inputSchema: ['type' => 'object', 'properties' => [], 'required' => null],
description: null,
annotations: null,
icons: null,
meta: null,
outputSchema: null,
),
static fn () => null,
);
}
private function resource(string $uri): ResourceReference
{
return new ResourceReference(
new Resource(uri: $uri, name: 'r', description: null, mimeType: 'text/plain'),
static fn () => null,
);
}
private function prompt(string $name): PromptReference
{
return new PromptReference(
new Prompt(name: $name, description: null, arguments: []),
static fn () => [],
);
}
private function template(string $uriTemplate): ResourceTemplateReference
{
return new ResourceTemplateReference(
new ResourceTemplate(uriTemplate: $uriTemplate, name: 'tpl', description: null, mimeType: 'text/plain'),
static fn () => null,
);
}
}