fix(contextchat): OCP\ContextChat is NC 33+, but this app supports NC 28+ - #2372
Merged
Conversation
… 28+
Two classes named an `OCP\ContextChat` type in their CONSTRUCTOR, which is
fatal on every Nextcloud below 33 — `lib/public/ContextChat/` does not exist
on stable31 or stable32, and appinfo/info.xml declares min-version="28".
The load happens in the container, not at class declaration:
`SimpleContainer::resolve()` calls `new ReflectionClass()` on each parameter
type. That call is the load.
ContextChatReindexCommand listed in appinfo/info.xml, so
Console\Application::loadCommandsFromInfoXml()
reflects it on EVERY occ invocation. Observed
in CI on NC 31.0.14.1 during `occ app:enable`:
Interface "OCP\ContextChat\IContentProvider"
not found. It is logged at level 3 and the
enable still reports success — loud in the log,
invisible in the exit code, which is how it
survived.
ContextChatSubmissionListener registered for ObjectCreatedEvent,
ObjectUpdatedEvent and ObjectDeletedEvent, so
it is resolved on EVERY object write. On
NC 28-32 that made every create, update and
delete throw ReflectionException, for a
feature the instance is not even using. Its
docblock claimed IContentManager was "always
resolvable via DI"; it is not.
Both now resolve through the container inside the method body, behind an
interface_exists() guard, and both call sites in the listener are guarded
separately — submitContentItem() and removeContentItem() each read
ContentProvider:: constants, and loading THAT class is fatal too (its header
implements the missing interface).
ContentProviderRegistrationListener is deliberately unchanged: it is only ever
resolved by dispatching ContentProviderRegisterEvent, which cannot fire on a
server without Context Chat. Registering it is safe because `::class` on an
FQCN is compile-time and never autoloads.
VERIFIED with a probe that maps OCP\ContextChat\* to nothing, exactly as those
servers do, then mimics SimpleContainer::resolve():
ContextChatReindexCommand before FATAL Interface "OCP\ContextChat\IContentProvider" not found
after OK
ContextChatSubmissionListener before FATAL Class "OCP\ContextChat\IContentManager" does not exist
after OK
ContentProvider (control) still FATAL — proves the probe really does
simulate NC < 33 rather than passing
everything
The first probe I wrote proved nothing: it read parameter types via
getType()->getName(), which returns a string WITHOUT loading the class, so the
unfixed command "passed". The fatal only appears once each parameter type is
itself reflected.
TESTS. tests/Unit/ContextChat/ContextChatVersionGuardTest.php — 4 tests
pinning the shape, since a machine where OCP\ContextChat resolves cannot
reproduce the fatal.
Its first version had the same class of hole as the probe: it checked only
whether the parameter TYPE NAME contained OCP\ContextChat, so re-adding
`ContentProvider $contentProvider` to the command — the original defect,
verbatim — still passed, because ContentProvider is an OCA class. It now walks
class_implements()/class_parents() as well. Mutation-verified after the fix:
re-add ContentProvider to the command's constructor 1 failure
drop one of the listener's two guards 1 failure
restored OK (4 tests)
The existing ContextChatSubmissionListenerTest passes unchanged in substance —
only the constructor wiring moved to a container mock returning the same
IContentManager mock, so all 26 assertions still observe what they did before.
Contributor
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ❌ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 173/173 | |||
| npm | ✅ | ✅ 713/713 | |||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-06 13:05 UTC
Download the full PDF report from the workflow artifacts.
phpstan failed with:
Method ContextChatSubmissionListener::contentManager()
has invalid return type OCP\ContextChat\IContentManager.
which is itself confirmation of the defect this branch fixes: openregister's
own nextcloud/ocp stubs ship NO ContextChat at all, exactly as NC 28-32 does
not.
phpstan.neon already ignores the OCP\ContextChat class as a parameter or
property — but it words a parameter/property as 'has invalid type' and a
return as 'has invalid RETURN type', and only the former was listed. The new
private accessor returns ?IContentManager, so it needed the second wording.
Not a new kind of suppression: the OCA\Bookmarks block directly above is the
same shape — an optional peer app resolved lazily via \OCP\Server::get()
behind a class_exists guard — and already carries its own
'has invalid return type OCA\Bookmarks\' entry for this exact reason. The
block's existing comment explains why ignoring beats baselining here: a
baseline entry goes stale the moment the optional component IS present.
Verified narrow rather than blanket: replacing the return type with an
unrelated non-existent class still produces 'Found 1 error', and phpstan is
[OK] No errors once restored.
Contributor
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 173/173 | |||
| npm | ✅ | ✅ 713/713 | |||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-06 13:33 UTC
Download the full PDF report from the workflow artifacts.
gate-46 spec-anchor-existence failed with 10 unresolved @SPEC targets, all in the two files this branch touches. They are pre-existing — development does not fail gate-46 because the gate is diff-scoped and these files were not in any recent diff — but they became mine to fix the moment I edited the files. Every anchor named a requirement heading that was never written. The spec has three requirements; the code cited four inventions of them: #requirement-only-opted-in-schemas-must-have-their-objects-submitted-… #requirement-only-published-objects-must-be-submitted-to-context-chat #requirement-object-deletion-must-remove-submitted-content-from-… all three describe ONE requirement, "Only opted-in, published objects are submitted to Context Chat", whose body also covers deletion ("Deleted objects … SHALL have their content removed … rather than left stale"). Repointed to its real slug. #requirement-initial-import-must-walk-opted-in-schemas-in-batches-and-… is "getItemUrl and initial import reuse existing OpenRegister infrastructure", whose body carries the batching and occ-scoping language. Repointed likewise. Nothing in the spec was changed: the requirements already say what the code claims, the citations were simply written from memory rather than from the headings. Verified: gate-46 PASS after the rewrite, and FAIL — 1 unresolved when a deliberately bogus anchor is injected, so the pass is a verdict rather than the gate having gone quiet.
Contributor
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 173/173 | |||
| npm | ✅ | ✅ 713/713 | |||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-06 13:52 UTC
Download the full PDF report from the workflow artifacts.
rubenvdlinde
added a commit
that referenced
this pull request
Aug 7, 2026
…r repos (#2380) #2378 raised min-version 28 -> 32. Its premise was correct WHEN WRITTEN and is no longer: "the registration listener cannot prevent it, because the failure happens when PHP reads the class header, not when the provider is registered" That was true, and #2372 is what changed it. #2372 removed every EAGER reference to ContentProvider — the command's constructor typehint and the listener's — so the class is now only loaded from inside interface_exists('OCP\ContextChat\IContentManager') guards. On a server without OCP\ContextChat the class header is never read: the feature is inert, and nothing logs. WHAT THE FLOOR ACTUALLY COSTS. min-version is enforced at INSTALL time, so 32 makes `occ app:enable openregister` refuse on 31. Eight fleet repos install openregister as an additional-app while testing stable31 — procest, shillinq, portaliq, openbuild, decidesk, doriath, hermiq, larpingapp. Each now fails its seed with {"success":false,"message":"OpenRegister is not installed or enabled"} and then a 404 from /api/configurations/import, because the fallback importer belongs to the app that did not install. Observed on procest#758, where it looked for a while like a manifest bug in that PR. EVIDENCE THAT 31 WORKS. procest's e2e installs openregister at `development` and runs stable31. Its run at 2026-08-06T14:04Z — six minutes AFTER #2372 merged at 13:58Z — passed. The app demonstrably boots, enables and serves on NC 31 with that fix in place. The second reason the CI compose gave for a 32 floor does not hold either: OCP\DB\Types::DATETIME_IMMUTABLE is present in stable31 as well as stable32. Checked directly against both branches. ALSO CORRECTS MY OWN ERROR. #2372 and its test say "NC 33" throughout. That came from a local lookup against refs this checkout does not have, which answers ABSENT for everything — the failure mode a positive control exists to catch, and I did not run one at the time. Verified properly against raw.githubusercontent, with IUserManager.php as the control: stable31 IContentProvider 404 control 200 stable32 IContentProvider 200 control 200 So the interfaces arrive in 32, not 33. Every "NC 33" claim in the listener, the command and the guard test is corrected to 32. 28-31 remain untested by this repo's own CI, exactly as #2378 observed. That is a real coverage gap and worth closing, but it is a different thing from declaring those versions unsupported — the eight repos above ARE the coverage, and they were green on 31 until the floor moved. 26 ContextChat tests pass; phpcs clean.
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.
Two classes named an
OCP\ContextChattype in their constructor, which is fatal on every Nextcloud below 33 —lib/public/ContextChat/does not exist onstable31orstable32, andappinfo/info.xmldeclaresmin-version="28".The load happens in the container, not at class declaration:
SimpleContainer::resolve()callsnew ReflectionClass()on each parameter type. That call is the load.ContextChatReindexCommandinfo.xml→loadCommandsFromInfoXml()occinvocationContextChatSubmissionListenerThe command failure is already visible in CI on NC 31.0.14.1 during
occ app:enable:Interface "OCP\ContextChat\IContentProvider" not found— logged at level 3 while the enable still reports success. Loud in the log, invisible in the exit code, which is how it survived. The listener's docblock claimedIContentManagerwas "always resolvable via DI"; it is not.Both now resolve through the container inside the method body, behind an
interface_exists()guard. Both listener call sites are guarded separately —submitContentItem()andremoveContentItem()each readContentProvider::constants, and loading that class is fatal too, since its header implements the missing interface.ContentProviderRegistrationListeneris deliberately unchanged: it is only ever resolved by dispatchingContentProviderRegisterEvent, which cannot fire without Context Chat.Verified with a probe that simulates NC < 33
ContextChatReindexCommandInterface "OCP\ContextChat\IContentProvider" not foundContextChatSubmissionListenerClass "OCP\ContextChat\IContentManager" does not existContentProvider(control)The control matters: it proves the probe really simulates NC < 33 rather than passing everything.
The first probe I wrote proved nothing. It read parameter types via
getType()->getName(), which returns a string without loading the class — so the unfixed command "passed". The fatal only appears once each parameter type is itself reflected.Tests — and the same hole, twice
tests/Unit/ContextChat/ContextChatVersionGuardTest.phppins the shape, since a machine whereOCP\ContextChatresolves cannot reproduce the fatal.Its first version had the same class of hole as the probe: it checked only whether the parameter type name contained
OCP\ContextChat, so re-addingContentProvider $contentProviderto the command — the original defect, verbatim — still passed, becauseContentProvideris anOCAclass. It now walksclass_implements()/class_parents()too.ContentProviderto the command's constructorThe existing
ContextChatSubmissionListenerTestpasses unchanged in substance — only the constructor wiring moved to a container mock returning the sameIContentManagermock, so all 26 assertions still observe what they did before.