fix(gate-14): an AppHost generic the leaf registers itself is not an unreachable route - #199
Merged
Merged
Conversation
…unreachable route
Bootstrap::register() is not the only way an ADR-040 generic reaches a
route. A leaf that needs the generic constructed with its OWN collaborators
registers it by hand, under the standard controller class name — the name
NC's App::main synthesises from a plain route slug:
// openconnector lib/AppInfo/Application.php
$context->registerService(
'OCA\OpenConnector\Controller\GenericPreferencesController',
static fn ($c) => new GenericPreferencesController(
appName: self::APP_ID, ...
)
);
`appName` is the point: it keeps the `pref_` user-value namespace scoped to
openconnector instead of to OpenRegister. The class resolves at request time
and has no file in the leaf repo — the same legitimate absence
`_apphost_serves` already covers, arrived at a different way.
The five-slug list could not see it. It keys on the slugs Bootstrap aliases
(`preferences`), and the route has to be named `genericPreferences` so that
App::main builds the class name the service is registered under. So gate-14
reported `genericPreferences#getPreference` and `#setPreference` as
controller-class-not-found — two findings on an endpoint that works, on
every openconnector run.
`_di_binds_controller` asks for evidence rather than for a category: the
exact fully-qualified class name must appear as a literal within a few lines
of a registerService call in the app's own Application.php. Not a wildcard,
and not "this app adopts AppHost, so absences are fine" — a controller that
is simply missing still fails, which is the invariant the gate exists for.
The needle goes through the environment, not `awk -v`: -v runs escape
processing over the value, so the `\\` pairs a PHP class literal is made of
arrive as single backslashes and match nothing. That is a silent failure —
the helper answers "no such binding" for every controller, which reads
exactly like a correct verdict. Caught by testing the helper directly
before wiring it in.
Fixture `di-registered-generic` carries its own control in the same
directory: `gadget#run` names a class that is neither on disk nor
registered, and gate-14 must still raise it. Verified by renaming the
registration away and watching the generic come back as a finding — the
exemption does work rather than blanket-passing. gate-5 also stops calling
these UNRESOLVED-for-unknown-reasons and names AppHost as the reason.
This was referenced Aug 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bootstrap::register()is not the only way an ADR-040 AppHost generic reaches a route.A leaf that needs the generic constructed with its own collaborators registers it by hand, under the standard controller class name — the name NC's
App::mainsynthesises from a plain route slug:appNameis the point: it keeps thepref_user-value namespace scoped to openconnector rather than to OpenRegister. The class resolves at request time and has no file in the leaf repo — the same legitimate absence_apphost_servesalready covers, arrived at a different way.Why the slug list could not see it
_HYDRA_APPHOST_SLUGSis keyed on the five slugs Bootstrap aliases (preferences). This route has to be namedgenericPreferences, because that is the slug from whichApp::mainbuilds the class name the service is registered under. So gate-14 reportedgenericPreferences#getPreferenceand#setPreferenceascontroller-class-not-found— two findings on a working endpoint, on every openconnector run.The fix asks for evidence, not for a category
_di_binds_controllerrequires the exact fully-qualified class name to appear as a literal within a few lines of aregisterServicecall in the app's ownApplication.php.Not a wildcard, and deliberately not "this app adopts AppHost, so absences are fine" — a controller that is simply missing still fails, which is the invariant the gate exists for.
One thing worth calling out
The needle goes through the environment, not
awk -v.-vruns escape processing over the value, so the\\pairs a PHP class literal is made of arrive as single backslashes and match nothing. That failure is silent: the helper answers "no such binding" for every controller, which reads exactly like a correct verdict. Caught by exercising the helper directly before wiring it in.Tests
New fixture
di-registered-generic, carrying its own control in the same directory so the two cannot drift apart:gadget#run— a class neither on disk nor registered — must still be reportedVerified by renaming the registration away and watching the generic come back as a finding, so the exemption is doing work rather than blanket-passing.
test_gate_route_auth.sh: 30 assertions, 0 failures. Full helper-suite run: 26 passed, 2 quarantined as documented, 0 failed.