Skip to content

fix(gate-14): an AppHost generic the leaf registers itself is not an unreachable route - #199

Merged
rubenvdlinde merged 1 commit into
mainfrom
fix/gate-14-di-registered-generic
Aug 7, 2026
Merged

fix(gate-14): an AppHost generic the leaf registers itself is not an unreachable route#199
rubenvdlinde merged 1 commit into
mainfrom
fix/gate-14-di-registered-generic

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

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::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 rather than 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.

Why the slug list could not see it

_HYDRA_APPHOST_SLUGS is keyed on the five slugs Bootstrap aliases (preferences). This route has to be named genericPreferences, because that is the slug from which App::main builds the class name the service is registered under. So gate-14 reported genericPreferences#getPreference and #setPreference as controller-class-not-foundtwo findings on a working endpoint, on every openconnector run.

The fix asks for evidence, not for a category

_di_binds_controller requires the exact fully-qualified class name to appear as a literal within a few lines of a registerService call in the app's own Application.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. -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 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:

  • the DI-registered generic must not be reported
  • gadget#run — a class neither on disk nor registered — must still be reported
  • gate-5 must state the generic was NOT JUDGED, naming AppHost as the reason, rather than dropping it

Verified 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.

…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant