fix(listeners): method_exists() cannot see OpenRegister's magic accessors — the slug resolver was dead - #185
Conversation
…sors — the slug resolver was dead ObjectSchemaSlugResolver guarded all three of its accessor calls with method_exists(). OpenRegister's ObjectEntity, Schema and Register extend Nextcloud's Entity, which serves every column accessor through __call() and declares it as an @method docblock only. method_exists() is therefore false for getSchema(), getRegister() and getSlug() on the real classes, so every guard took its false branch, schemaSlug() returned '' for every real entity, and isOpenBuildSchema() was false even for openbuild's own objects. Measured on this instance: method_exists false / is_callable true for those three, against the concrete control getObject() which is true for both. That disabled the two listeners the resolver exists to serve — ProductionVersionGuardListener and AutomationCleanupListener. Both sit behind ListenerSlugContract, which defaults off, so default behaviour does not change here: what changes is that enabling the flag now actually wakes them instead of silently keeping them off. Waking them remains a rollout decision, unchanged. The file's own header already documented the neighbouring trap (getSchemaSlug() does not exist and the ids are not slugs) while all three of its guards carried this one, so the fix is applied at the single private seam they funnel into rather than at the reported line. is_callable() is not a membership test on a __call class — it is true for any name — so readAccessor() is exception-safe; Entity throws BadFunctionCallException for a column it does not have. ProductionVersionGuardListener::extractUuid() carried the same defect on getUuid() and is fixed the same way. Why the suite never caught it: tests/stubs/openregister-stubs.php declares these accessors concretely, for a real reason (PHPUnit 10 removed addMethods(), so createMock()->method('getUuid') cannot configure a magic method). That stub inverts the exact predicate under test — method_exists is true in the suite and false in production. The new test therefore extends the real OCP\AppFramework\Db\Entity so its accessors are reached the way production reaches them, and carries a concrete-method control so a false cannot come from a broken fixture. Negative control, predicted before running: reverting the guard at readAccessor() turns exactly 3 of the 7 new tests red. Observed exactly those 3 (testSchemaSlugResolvesThroughMagicAccessor, testRegisterSlugResolvesThroughMagicAccessor, testIsOpenBuildSchemaMatchesOpenBuildsOwnObject); the other 4 stayed green. Suite 797 -> 804 tests, all green. lint/phpcs/phpmd/psalm/phpstan clean.
Quality Report — ConductionNL/openbuild @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| check-gitignore | ✅ | ||||
| check-nc-floor | ✅ | ||||
| composer | ✅ | ✅ 101/101 | |||
| npm | ✅ | ✅ 654/654 | |||
| PHPUnit | ✅ | ||||
| Newman | ⏭️ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-11 22:36 UTC
Download the full PDF report from the workflow artifacts.
|
Merging with E2E and Quality Report red. Both are pre-existing and neither is caused by this diff. The E2E job fails on exactly two tests, and the same two fail identically on development runs 31520347084, 31459739568 and 31431360507 — same spec, same line numbers, same test ordinals, same locator timeout. They are the REQ-AUTD-008 RBAC scenarios already tracked in #173, which records that automation CRUD is admin-only at the OR layer so neither an editor nor an owner can author or enable one. openbuild's E2E has been red on development for eight consecutive runs; the most recent one failed even earlier, at Checkout additional apps, so it ran no tests at all. Quality Report is a pure aggregator over the other jobs and carries no independent signal — it is red because E2E is. Everything this change could plausibly affect is green: Hydra Gates pass, all four PHPUnit cells pass, phpcs, phpmd, psalm, phpstan, both Security jobs and both Vue Quality jobs pass. Read the gates green narrowly. COVERAGE: 34 of 64 declared gates reported a result, 34 of 34 applicable ran, over a diff of 3 changed files against origin/development. That is a real verdict on this change and not a statement about the repository. |
What
ObjectSchemaSlugResolverguarded all three of its accessor calls withmethod_exists(). OpenRegister'sObjectEntity,SchemaandRegisterextend Nextcloud'sEntity, which serves every column accessor through__call()and declares it as an@methoddocblock only. So the probe was false forgetSchema(),getRegister()andgetSlug()on the real classes,schemaSlug()returned an empty string for every real entity, andisOpenBuildSchema()was false even for openbuild's own objects.Measured on this instance: the probe is false while
is_callableis true for those three, against the concrete controlgetObject()which is true for both.That silently disabled the two listeners the resolver exists to serve —
ProductionVersionGuardListenerandAutomationCleanupListener.Blast radius is unchanged by this PR
Both listeners sit behind
ListenerSlugContract, which defaults off. Default behaviour is therefore identical. What changes is that turning the flag on now actually wakes them, instead of silently keeping them off — which is the opposite of what a rollout flag is for. Waking them remains a rollout decision and is not taken here.Whole file, not the reported line
The file's own header already documented the neighbouring trap — that
getSchemaSlug()does not exist and that the event carries ids rather than slugs — while all three of its own guards carried this one. The fix is applied at the single private seam the three call sites funnel into.is_callable()is not a membership test on a__callclass; it is true for any name. So the helper is exception-safe, becauseEntityraises for a column it does not have.ProductionVersionGuardListener::extractUuid()carried the same defect ongetUuid()and is fixed the same way.Why the suite never caught it
tests/stubs/openregister-stubs.phpdeclares these accessors concretely, for a real reason: PHPUnit 10 removedaddMethods(), so a generated mock cannot configure a magic method. That stub inverts the exact predicate under test — the probe is true in the suite and false in production. The stub is left alone, because its reason holds.The new test instead extends the real
OCP\AppFramework\Db\Entity, so its accessors are reached the way production reaches them, and it carries a concrete-method control so a false result cannot come from a broken fixture.Prediction and control
Predicted before running: reverting the guard at the shared seam turns exactly three of the seven new tests red. Observed exactly those three; the other four stayed green, including the fail-closed test that rejects a same-named schema in another register.
Suite goes from 797 to 804 tests, all green. lint, phpcs, phpmd, psalm and phpstan clean. Gates were re-run after committing, so the diff-scoped gates inspected a real three-file scope rather than an empty one.
Also found while here, deliberately not fixed
AutomationApprovalTriggerListener::schemaOf()andDocumentGenerationListener::schemaOf()carry the same probe, but fixing it alone would not wake them:getSchema()returns the numeric schema id, and both compare it against a slug literal. Routing them through this resolver is the real fix and is a rollout decision of the same kind the contract flag governs, so it is filed rather than guessed at.