allows('name')->andReturn($name); $ability->allows('isDirect')->andReturn($direct); return $ability; } private function coreWith(Ability ...$abilities): Integration { $core = Mockery::mock(Integration::class); $core->allows('id')->andReturn('core'); $core->allows('isAvailable')->andReturn(true); $core->allows('abilities')->andReturn(array_values($abilities)); return $core; } public function testNamesActiveIntegrationsAndCoreRules(): void { $core = $this->coreWith($this->ability('lit/guide', true), $this->ability('lit/menu-list', false)); $text = (new InstructionsBuilder(new IntegrationRegistry($core)))->build(); self::assertStringContainsString('Aktive Integrationen: core', $text); self::assertStringContainsString('lit-guide', $text); self::assertStringContainsString('Status', $text); self::assertLessThan(2000, strlen($text), 'Instructions must stay short – they are loaded in every conversation.'); } public function testAddsTheDiviRuleOnlyWhenDiviIsActive(): void { $divi = Mockery::mock(Integration::class); $divi->allows('id')->andReturn('divi'); $divi->allows('isAvailable')->andReturn(true); $divi->allows('abilities')->andReturn([$this->ability('lit/divi-outline', true)]); $core = $this->coreWith($this->ability('lit/guide', true)); $with = (new InstructionsBuilder(new IntegrationRegistry($core, $divi)))->build(); $without = (new InstructionsBuilder(new IntegrationRegistry($core)))->build(); self::assertStringContainsString('lit-divi-outline und lit-divi-design-system lesen', $with); self::assertStringContainsString('tree_rev', $with); self::assertStringNotContainsString('lit-divi-outline und', $without); self::assertStringNotContainsString('tree_rev', $without); self::assertLessThan(2000, strlen($with)); } private function library(): Integration { $library = Mockery::mock(Integration::class); $library->allows('id')->andReturn('divi-library'); $library->allows('isAvailable')->andReturn(true); $insert = $this->ability('lit/library-insert', true); $insert->allows('annotations')->andReturn(['readonly' => false]); $library->allows('abilities')->andReturn([$insert]); return $library; } public function testAddsTheLibraryRuleOnlyWhenTheLibraryGroupIsActive(): void { $core = $this->coreWith($this->ability('lit/guide', true)); $with = (new InstructionsBuilder(new IntegrationRegistry($core, $this->library())))->build(); $without = (new InstructionsBuilder(new IntegrationRegistry($core)))->build(); self::assertStringContainsString('Divi-Bibliothek: globale Module', $with); self::assertStringContainsString('confirm: true', $with); self::assertStringContainsString('topic "divi-library"', $with); self::assertStringNotContainsString('Divi-Bibliothek:', $without); self::assertLessThan(2000, strlen($with)); } /** "nur lesen" or disabled: the library cannot be written, so the edit rule would only mislead. */ public function testLeavesOutTheLibraryRuleWhenTheGroupCannotWrite(): void { $core = $this->coreWith($this->ability('lit/guide', true)); $registry = new IntegrationRegistry($core, $this->library()); foreach ([['@readonly:divi-library' => true], ['@group:divi-library' => 'disabled']] as $settings) { $policy = (new ExposurePolicy($settings))->withGroups($registry->groupMap()); $text = (new InstructionsBuilder($registry, null, $policy))->build(); self::assertStringNotContainsString('Divi-Bibliothek:', $text, (string) json_encode($settings)); } } public function testListsPostTypesWhenAProviderIsGiven(): void { $core = $this->coreWith($this->ability('lit/guide', true)); $text = (new InstructionsBuilder(new IntegrationRegistry($core), static fn (): array => ['page', 'post']))->build(); self::assertStringContainsString('Inhaltstypen: page, post', $text); self::assertStringContainsString('lit-content-types', $text); self::assertStringContainsString('confirm: true', $text); } public function testListsTheCurrentlyDirectToolsByToolNameAndNamesTheRestNeutrally(): void { $core = $this->coreWith( $this->ability('lit/guide', true), $this->ability('lit/menu-list', false) ); $text = (new InstructionsBuilder(new IntegrationRegistry($core)))->build(); self::assertStringContainsString('Direkt aufrufbare Tools: lit-guide', $text); self::assertStringContainsString('mcp-adapter-execute-ability', $text); self::assertStringNotContainsString('lit-menu-list', $text); } public function testDirectToolListReflectsExposurePolicyOverrides(): void { $core = $this->coreWith( $this->ability('lit/guide', true), $this->ability('lit/menu-list', false) ); $policy = new ExposurePolicy(['lit/menu-list' => 'direct']); $text = (new InstructionsBuilder(new IntegrationRegistry($core), null, $policy))->build(); self::assertStringContainsString('lit-menu-list', $text); } public function testGroupDisabledAndReadonlyToolsAreNotListedAsDirect(): void { $write = $this->ability('lit/divi-update', true); $write->allows('annotations')->andReturn(['readonly' => false]); $read = $this->ability('lit/divi-outline', true); $read->allows('annotations')->andReturn(['readonly' => true]); $divi = Mockery::mock(Integration::class); $divi->allows('id')->andReturn('divi'); $divi->allows('isAvailable')->andReturn(true); $divi->allows('abilities')->andReturn([$write, $read]); $core = $this->coreWith($this->ability('lit/guide', true), $this->ability('lit/menu-list', true)); $registry = new IntegrationRegistry($core, $divi); $policy = (new ExposurePolicy(['@group:core' => 'disabled', '@readonly:divi' => true])) ->withGroups($registry->groupMap()); $text = (new InstructionsBuilder($registry, null, $policy))->build(); self::assertStringContainsString('Direkt aufrufbare Tools: lit-guide, lit-divi-outline.', $text); } }