feat(mcp) : add Slots, Machine Links, Structure, and Clone tools

- list_slots + update_slots for composant/piece slots
- list/add/update/remove machine links (component, piece, product)
- get_machine_structure with full hierarchy
- clone_machine with all links and custom fields
- 52 MCP tests pass total

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-03-16 14:49:55 +01:00
parent 2f173e766d
commit bd7259ed05
11 changed files with 1724 additions and 0 deletions

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace App\Tests\Mcp\Tool\Machine;
use App\Tests\AbstractApiTestCase;
/**
* @internal
*/
class MachineLinksToolTest extends AbstractApiTestCase
{
public function testListMachineLinks(): void
{
$machine = $this->createMachine(name: 'Machine Links Test');
$composant = $this->createComposant(name: 'Comp A');
$piece = $this->createPiece(name: 'Piece A');
$compLink = $this->createMachineComponentLink($machine, $composant);
$pieceLink = $this->createMachinePieceLink($machine, $piece, parentLink: $compLink, quantity: 3);
$session = $this->createMcpClient('ROLE_VIEWER');
$data = $this->callMcpTool($session, 'list_machine_links', ['machineId' => $machine->getId()]);
$this->assertArrayHasKey('_parsed', $data);
$parsed = $data['_parsed'];
$this->assertSame($machine->getId(), $parsed['machineId']);
$this->assertCount(1, $parsed['componentLinks']);
$this->assertCount(1, $parsed['pieceLinks']);
$this->assertSame($compLink->getId(), $parsed['componentLinks'][0]['id']);
$this->assertSame($pieceLink->getId(), $parsed['pieceLinks'][0]['id']);
$this->assertSame(3, $parsed['pieceLinks'][0]['quantity']);
}
public function testAddMachineComponentLink(): void
{
$machine = $this->createMachine(name: 'Machine Add Test');
$composant = $this->createComposant(name: 'Comp B');
$session = $this->createMcpClient('ROLE_GESTIONNAIRE');
$data = $this->callMcpTool($session, 'add_machine_links', [
'machineId' => $machine->getId(),
'links' => [
[
'type' => 'composant',
'entityId' => $composant->getId(),
'nameOverride' => 'Custom Name',
],
],
]);
$this->assertArrayHasKey('_parsed', $data);
$created = $data['_parsed']['created'];
$this->assertCount(1, $created);
$this->assertSame('composant', $created[0]['type']);
$this->assertSame($composant->getId(), $created[0]['entityId']);
$this->assertNotEmpty($created[0]['id']);
}
public function testRemoveMachineLink(): void
{
$machine = $this->createMachine(name: 'Machine Remove Test');
$composant = $this->createComposant(name: 'Comp C');
$link = $this->createMachineComponentLink($machine, $composant);
$session = $this->createMcpClient('ROLE_GESTIONNAIRE');
$data = $this->callMcpTool($session, 'remove_machine_link', [
'linkId' => $link->getId(),
'linkType' => 'composant',
]);
$this->assertArrayHasKey('_parsed', $data);
$this->assertTrue($data['_parsed']['deleted']);
// Verify the link is gone by listing
$listData = $this->callMcpTool($session, 'list_machine_links', ['machineId' => $machine->getId()]);
$this->assertCount(0, $listData['_parsed']['componentLinks']);
}
}

View File

@@ -0,0 +1,164 @@
<?php
declare(strict_types=1);
namespace App\Tests\Mcp\Tool\Machine;
use App\Tests\AbstractApiTestCase;
/**
* @internal
*/
class MachineStructureToolTest extends AbstractApiTestCase
{
public function testGetMachineStructure(): void
{
$site = $this->createSite(name: 'Site Structure');
$machine = $this->createMachine(name: 'Machine Structure', site: $site, reference: 'REF-STRUCT');
$composant = $this->createComposant(name: 'Composant Alpha');
$piece = $this->createPiece(name: 'Piece Alpha', reference: 'REF-P1');
$product = $this->createProduct(name: 'Product Alpha', reference: 'REF-PR1');
$componentLink = $this->createMachineComponentLink($machine, $composant);
$this->createMachinePieceLink($machine, $piece, $componentLink, quantity: 3);
$this->createMachineProductLink($machine, $product);
$session = $this->createMcpClient('ROLE_VIEWER');
$data = $this->callMcpTool($session, 'get_machine_structure', [
'machineId' => $machine->getId(),
]);
$this->assertArrayHasKey('_parsed', $data);
$parsed = $data['_parsed'];
// Machine info
$this->assertSame('Machine Structure', $parsed['machine']['name']);
$this->assertSame('REF-STRUCT', $parsed['machine']['reference']);
$this->assertSame($site->getId(), $parsed['machine']['siteId']);
// Component links
$this->assertCount(1, $parsed['componentLinks']);
$this->assertSame($composant->getId(), $parsed['componentLinks'][0]['composantId']);
$this->assertSame('Composant Alpha', $parsed['componentLinks'][0]['composant']['name']);
// Piece links
$this->assertCount(1, $parsed['pieceLinks']);
$this->assertSame($piece->getId(), $parsed['pieceLinks'][0]['pieceId']);
$this->assertSame('Piece Alpha', $parsed['pieceLinks'][0]['piece']['name']);
$this->assertSame(3, $parsed['pieceLinks'][0]['quantity']);
// Product links
$this->assertCount(1, $parsed['productLinks']);
$this->assertSame($product->getId(), $parsed['productLinks'][0]['productId']);
$this->assertSame('Product Alpha', $parsed['productLinks'][0]['product']['name']);
}
public function testGetMachineStructureNotFound(): void
{
$session = $this->createMcpClient('ROLE_VIEWER');
$data = $this->callMcpTool($session, 'get_machine_structure', [
'machineId' => 'nonexistent-id',
]);
$this->assertArrayHasKey('error', $data);
}
public function testGetMachineStructureWithOverrides(): void
{
$site = $this->createSite(name: 'Site Overrides');
$machine = $this->createMachine(name: 'Machine Overrides', site: $site);
$composant = $this->createComposant(name: 'Composant Override');
$componentLink = $this->createMachineComponentLink($machine, $composant);
$componentLink->setNameOverride('Custom Name');
$componentLink->setReferenceOverride('CUSTOM-REF');
$this->getEntityManager()->flush();
$session = $this->createMcpClient('ROLE_VIEWER');
$data = $this->callMcpTool($session, 'get_machine_structure', [
'machineId' => $machine->getId(),
]);
$this->assertArrayHasKey('_parsed', $data);
$overrides = $data['_parsed']['componentLinks'][0]['overrides'];
$this->assertSame('Custom Name', $overrides['name']);
$this->assertSame('CUSTOM-REF', $overrides['reference']);
}
public function testCloneMachine(): void
{
$site = $this->createSite(name: 'Site Source');
$targetSite = $this->createSite(name: 'Site Target');
$machine = $this->createMachine(name: 'Machine Source', site: $site, reference: 'REF-SRC');
$composant = $this->createComposant(name: 'Composant Clone');
$piece = $this->createPiece(name: 'Piece Clone');
$product = $this->createProduct(name: 'Product Clone');
$componentLink = $this->createMachineComponentLink($machine, $composant);
$this->createMachinePieceLink($machine, $piece, $componentLink, quantity: 2);
$this->createMachineProductLink($machine, $product);
$session = $this->createMcpClient('ROLE_GESTIONNAIRE');
$data = $this->callMcpTool($session, 'clone_machine', [
'machineId' => $machine->getId(),
'name' => 'Machine Cloned',
'siteId' => $targetSite->getId(),
'reference' => 'REF-CLN',
]);
$this->assertArrayHasKey('_parsed', $data);
$parsed = $data['_parsed'];
$this->assertSame('Machine Cloned', $parsed['name']);
$this->assertSame('REF-CLN', $parsed['reference']);
$this->assertSame($targetSite->getId(), $parsed['siteId']);
$this->assertSame($machine->getId(), $parsed['clonedFrom']);
$this->assertNotSame($machine->getId(), $parsed['id']);
// Verify the cloned machine has links by fetching its structure
$structureData = $this->callMcpTool($session, 'get_machine_structure', [
'machineId' => $parsed['id'],
]);
$this->assertArrayHasKey('_parsed', $structureData);
$structure = $structureData['_parsed'];
$this->assertCount(1, $structure['componentLinks']);
$this->assertSame($composant->getId(), $structure['componentLinks'][0]['composantId']);
$this->assertCount(1, $structure['pieceLinks']);
$this->assertCount(1, $structure['productLinks']);
}
public function testCloneMachineRequiresGestionnaire(): void
{
$site = $this->createSite(name: 'Site Forbidden');
$machine = $this->createMachine(name: 'Machine Forbidden', site: $site);
$session = $this->createMcpClient('ROLE_VIEWER');
$data = $this->callMcpTool($session, 'clone_machine', [
'machineId' => $machine->getId(),
'name' => 'Should Fail',
'siteId' => $site->getId(),
]);
$this->assertArrayHasKey('error', $data, 'Should fail with VIEWER role');
}
public function testCloneMachineNotFound(): void
{
$site = $this->createSite(name: 'Site Clone NF');
$session = $this->createMcpClient('ROLE_GESTIONNAIRE');
$data = $this->callMcpTool($session, 'clone_machine', [
'machineId' => 'nonexistent-id',
'name' => 'Should Fail',
'siteId' => $site->getId(),
]);
$this->assertArrayHasKey('error', $data);
}
}

View File

@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace App\Tests\Mcp\Tool\Slot;
use App\Tests\AbstractApiTestCase;
/**
* @internal
*/
class SlotsToolTest extends AbstractApiTestCase
{
public function testListSlotsForComposant(): void
{
$composant = $this->createComposant(name: 'Comp Slots');
$this->createComposantPieceSlot(composant: $composant, position: 0);
$this->createComposantProductSlot(composant: $composant, position: 1);
$this->createComposantSubcomponentSlot(composant: $composant, alias: 'Sub A', position: 2);
$session = $this->createMcpClient('ROLE_VIEWER');
$data = $this->callMcpTool($session, 'list_slots', [
'entityType' => 'composant',
'entityId' => $composant->getId(),
]);
$this->assertArrayHasKey('_parsed', $data);
$parsed = $data['_parsed'];
$this->assertSame('composant', $parsed['entityType']);
$this->assertSame(3, $parsed['total']);
$slotTypes = array_column($parsed['slots'], 'slotType');
$this->assertContains('piece', $slotTypes);
$this->assertContains('product', $slotTypes);
$this->assertContains('subcomponent', $slotTypes);
}
public function testListSlotsForPiece(): void
{
$piece = $this->createPiece(name: 'Piece Slots');
$this->createPieceProductSlot(piece: $piece, position: 0);
$this->createPieceProductSlot(piece: $piece, position: 1);
$session = $this->createMcpClient('ROLE_VIEWER');
$data = $this->callMcpTool($session, 'list_slots', [
'entityType' => 'piece',
'entityId' => $piece->getId(),
]);
$this->assertArrayHasKey('_parsed', $data);
$parsed = $data['_parsed'];
$this->assertSame('piece', $parsed['entityType']);
$this->assertSame(2, $parsed['total']);
foreach ($parsed['slots'] as $slot) {
$this->assertSame('product', $slot['slotType']);
}
}
public function testUpdateSlotSelectsPiece(): void
{
$composant = $this->createComposant(name: 'Comp Update');
$piece = $this->createPiece(name: 'Selected Piece');
$slot = $this->createComposantPieceSlot(composant: $composant, position: 0);
$session = $this->createMcpClient('ROLE_GESTIONNAIRE');
$data = $this->callMcpTool($session, 'update_slots', [
'slots' => [
[
'slotId' => $slot->getId(),
'slotType' => 'composant_piece',
'selectedPieceId' => $piece->getId(),
],
],
]);
$this->assertArrayHasKey('_parsed', $data);
$parsed = $data['_parsed'];
$this->assertSame(1, $parsed['total']);
$this->assertSame($piece->getId(), $parsed['updated'][0]['selectedEntityId']);
$this->assertSame('Selected Piece', $parsed['updated'][0]['selectedEntityName']);
}
}