Skip to content

fix(contextchat): OCP\ContextChat is NC 33+, but this app supports NC 28+ - #2372

Merged
rubenvdlinde merged 3 commits into
developmentfrom
fix/contextchat-nc33-guard
Aug 6, 2026
Merged

fix(contextchat): OCP\ContextChat is NC 33+, but this app supports NC 28+#2372
rubenvdlinde merged 3 commits into
developmentfrom
fix/contextchat-nc33-guard

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

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.

class how it is resolved consequence on NC 28-32
ContextChatReindexCommand listed in info.xmlloadCommandsFromInfoXml() throws on every occ invocation
ContextChatSubmissionListener ObjectCreated/Updated/Deleted events throws on every object write

The 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 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. Both listener call sites are guarded separatelysubmitContentItem() and removeContentItem() each read ContentProvider:: constants, and loading that class is fatal too, since its header implements the missing interface.

ContentProviderRegistrationListener is deliberately unchanged: it is only ever resolved by dispatching ContentProviderRegisterEvent, which cannot fire without Context Chat.

Verified with a probe that simulates NC < 33

class before after
ContextChatReindexCommand FATAL Interface "OCP\ContextChat\IContentProvider" not found OK
ContextChatSubmissionListener FATAL Class "OCP\ContextChat\IContentManager" does not exist OK
ContentProvider (control) FATAL still FATAL

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.php pins 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() too.

mutation result
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.

… 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.
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/openregister @ 3a32f78

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.
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/openregister @ 5cdad8a

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.
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/openregister @ 1c9a490

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
rubenvdlinde merged commit 42cfbd4 into development Aug 6, 2026
30 checks passed
@rubenvdlinde
rubenvdlinde deleted the fix/contextchat-nc33-guard branch August 6, 2026 13:58
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.
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