diff --git a/CHANGELOG.md b/CHANGELOG.md index 223847c00..c8869a4d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -194,6 +194,16 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). usage and limit only when it refuses, so a row could not tell "near the limit" from "nothing measured". +- The governance link on a use-case pack page now carries the pack's + recommended profile. + + The page names the recommendation and then invites the operator to compare it + against what is in force. The link went to the readout with nothing selected, + so the profile had to be found again by hand, one screen after reading its + name. The readout has always honoured a `profile` query parameter; only the + link never sent one. + + ## [0.29.1] - 2026-08-13 ### Fixed diff --git a/Classes/Controller/Backend/UseCasePackController.php b/Classes/Controller/Backend/UseCasePackController.php index e7e315e46..ff117f3f6 100644 --- a/Classes/Controller/Backend/UseCasePackController.php +++ b/Classes/Controller/Backend/UseCasePackController.php @@ -114,7 +114,13 @@ public function showAction(string $pack = ''): ResponseInterface 'pack' => $useCasePack, 'plan' => $this->installer->plan($useCasePack), 'wizardUrl' => $this->wizardUrl(), - 'governanceUrl' => $this->routeUrl('nrllm_overview', 'Backend\\LlmModule', 'governance'), + // Carries the pack's recommended profile as the readout's `profile` + // query parameter, so the comparison the hint asks for is one click + // rather than a hunt through the profile buttons. The readout has + // always honoured it; only the link never sent it. + 'governanceUrl' => $this->routeUrl('nrllm_overview', 'Backend\\LlmModule', 'governance', [ + 'profile' => $useCasePack->recommendedGovernanceProfile->value, + ]), 'toolsUrl' => $this->routeUrl('nrllm_tools', 'Backend\\Tool', 'list'), // The Editor Action Center lives in the editor-facing module // (ADR-158), not in the admin tree. Linked because the plan names @@ -206,12 +212,15 @@ private function wizardUrl(): string return $this->routeUrl('nrllm_wizard', 'Backend\\SetupWizard', 'index'); } - private function routeUrl(string $route, string $controller, string $action): string + /** + * @param array $extra additional query parameters + */ + private function routeUrl(string $route, string $controller, string $action, array $extra = []): string { return (string)$this->backendUriBuilder->buildUriFromRoute($route, [ 'controller' => $controller, 'action' => $action, - ]); + ] + $extra); } private function enqueueFlashMessage(string $message, string $title, ContextualFeedbackSeverity $severity): void diff --git a/Tests/Functional/Controller/Backend/UseCasePackRenderTest.php b/Tests/Functional/Controller/Backend/UseCasePackRenderTest.php index 739764f4a..d6f1383f3 100644 --- a/Tests/Functional/Controller/Backend/UseCasePackRenderTest.php +++ b/Tests/Functional/Controller/Backend/UseCasePackRenderTest.php @@ -26,9 +26,11 @@ use Netresearch\NrLlm\Tests\Functional\AbstractFunctionalTestCase; use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\Attributes\Test; +use TYPO3\CMS\Backend\Routing\Route; use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder; use TYPO3\CMS\Core\Http\NormalizedParams; use TYPO3\CMS\Core\Http\ServerRequest; +use TYPO3\CMS\Core\Localization\LanguageServiceFactory; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\View\ViewFactoryData; use TYPO3\CMS\Core\View\ViewFactoryInterface; @@ -369,6 +371,64 @@ private function render(string $template, string $action, array $variables): str return $view->render(); } + #[Test] + public function theGovernanceLinkCarriesThePacksRecommendedProfile(): void + { + // The whole point of the link: the operator has just read the + // recommendation's name and wants it compared against what is in force. + // Asserted through a dispatched action rather than by handing the URL to + // the template, which would only prove the template prints what it is + // given (#778). + $this->importFixture('BeUsers.csv'); + $this->setUpBackendUser(1); + + // The request has to exist before the controller is resolved: Extbase's + // ConfigurationManager captures the ambient one when it is constructed, + // which is what theControllerIsRegisteredInTheContainer() already works + // around. + $request = $this->actionRequest('show', ['pack' => 'editorial-starter']); + $controller = $this->getService(UseCasePackController::class); + $body = (string)$controller->processRequest($request)->getBody(); + + self::assertStringContainsString('profile=controlled-cloud', $body); + } + + /** + * A request for one action of the real controller, with its arguments. + * + * The other tests here hand variables to a view, which can only prove the + * template prints what it is given. A URL the controller builds needs the + * controller to have built it. + * + * @param array $arguments + */ + private function actionRequest(string $action, array $arguments = []): ExtbaseRequest + { + $parameters = new ExtbaseRequestParameters(); + $parameters->setControllerName('Backend\\UseCasePack'); + $parameters->setControllerActionName($action); + $parameters->setControllerExtensionName('NrLlm'); + foreach ($arguments as $name => $value) { + $parameters->setArgument($name, $value); + } + + // BackendViewFactory resolves its template paths from the route's + // packageName, so a module action cannot be dispatched without one. + $route = new Route('/module/nrllm/usecase', ['packageName' => 'netresearch/nr-llm']); + + $serverRequest = (new ServerRequest('https://typo3-testing.local/typo3/', 'GET')) + ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE) + ->withAttribute('route', $route) + ->withAttribute('extbase', $parameters); + $serverRequest = $serverRequest->withAttribute('normalizedParams', NormalizedParams::createFromRequest($serverRequest)); + $GLOBALS['TYPO3_REQUEST'] = $serverRequest; + // ModuleTemplate translates its own chrome and reads $GLOBALS['LANG']. + $GLOBALS['LANG'] = $this->getService(LanguageServiceFactory::class) + ->createFromUserPreferences($GLOBALS['BE_USER'] ?? null); + + return new ExtbaseRequest($serverRequest); + } + /** * The Module layout renders f:flashMessages, which resolves its queue from * an extbase request. A plain PSR-7 request makes the LAYOUT fail before