fix: accept the concluded-decision event under both of the decision app's namespaces - #1015
Merged
Merged
Conversation
…pp's namespaces The decision app renamed its PSR-4 root from OCA\Decidesk to OCA\Decidiq with no compatibility alias. Stackiq's OUTBOUND half already carries both spellings (ContractApprovalService::DECISION_REQUESTED_EVENTS). The INBOUND half did not, and that is the half that closes the loop. Application.php imported OCA\Decidesk\Event\DecisionConcludedEvent and registered `::class`, which resolves at COMPILE TIME to a string nothing dispatches any more, so the listener attached to a name that never fires. The listener then tested `instanceof` against that same name, so even a correctly registered event would have been rejected as somebody else's. Both fail in total silence: an event with no listener and a listener on no event are indistinguishable from here. The visible symptom is a contract approved in the decision app that never leaves `In onderhandeling`, with nothing logged and nothing thrown. Verified 2026-09-09 against decidiq development d72839c: OCA\Decidiq\Event\DecisionConcludedEvent EXISTS and publishes the whole getter surface this listener reads (getSourceApp, getSubjectId, getExternalReference, getDecisionId, getStatus). The old spelling is kept regardless — an instance pinned to a release from before that rename still dispatches it, and dropping it re-breaks the integration in the other direction. No new resolver class: this repo already had the pattern, one FQN-string constant per event beside the service that owns the boundary, so the conclusion event gets the sibling constant rather than a second way of doing the same thing. `instanceof` against an absent class is simply false — it neither autoloads nor errors — so naming both costs nothing on an instance running one. SOURCE_APP stays `stackiq`: it is persisted on the decision app's rows and the listener filters inbound events against it. tests/analysis-stubs/decidiq-events.stub.php mirrors the real signatures so phpstan and psalm see both spellings rather than proving the newer half dead. Analysis-only, and deliberately out of the tests/Stubs glob for the reason its decidesk sibling records.
Contributor
Quality Report — ConductionNL/stackiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| check-vue-demi | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 130/130 | |||
| npm | ✅ | ✅ 704/704 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ⏭️ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-09-09 11:21 UTC
Download the full PDF report from the workflow artifacts.
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.
What this is
The decision app renamed its PSR-4 root from
OCA\DecidesktoOCA\Decidiqwith no compatibility alias. This repo's outbound half already carries both spellings (ContractApprovalService::DECISION_REQUESTED_EVENTS, with a comment explaining exactly why). The inbound half did not, and that is the half that closes the loop.lib/AppInfo/Application.php:23+:812use OCA\Decidesk\Event\DecisionConcludedEventregistered as::classOCA\Decidiq\Event\DecisionConcludedEvent::classresolves at compile time to a string nothing dispatchesclass_exists-guardedlib/EventListener/DecisionConcludedListener.php:33+:74instanceofinstanceoffalse, method returns as if the event were somebody else'sTwo independent failures on the same path, either one sufficient. Both are silent by construction: an event with no listener and a listener on no event are indistinguishable from inside this app. The visible symptom is a contract approved in the decision app that never leaves
In onderhandeling, with nothing thrown and nothing logged.Target verification
Read from decidiq
developmentd72839c, not inferred.OCA\Decidiq\Event\DecisionConcludedEventexists and publishes the whole getter surface this listener reads:getSourceApp(),getSubjectId(),getExternalReference(),getDecisionId(),getStatus().OCA\Decidesk\Event\DecisionConcludedEventis gone from that repo.The old spelling is kept anyway. An instance pinned to a release from before that rename still dispatches it, and dropping it here re-breaks the integration in the other direction. Order is the contract: newest first.
Fix shape
No new resolver class, and no port of dossiq's
FleetAppId. This repo already had the pattern in the right place: one FQN-string constant per event, on the service that owns the delegation boundary. The conclusion event gets the sibling constant next to the request one, so the two halves of the same boundary are declared together and a future rename is one edit. Porting aSupport\FleetAppIdhere would have added a second way to say the same thing, plus a namespace map for nine apps stackiq has no binding to, and its id half would have had no caller at all.instanceofagainst a class that is not installed is simply false: it neither autoloads nor errors. So naming both spellings costs nothing on an instance that runs only one, which is why the listener can be typed against both imports rather than duck-typed throughmethod_exists.SOURCE_APPstaysstackiq. It is persisted on the decision app's Decision rows when an approval is raised, and the listener filters inbound events against it. Its existing docblock already says so.Static analysis
tests/analysis-stubs/decidiq-events.stub.phpis new, the sibling of thedecidesk-events.stub.phpthat was already here, registered with both phpstan (scanFiles) and psalm (<stubs>). Signatures mirror the real class rather than this app's assumptions about it: a stub written from the call site agrees with the call site by construction and cannot fail. It stays out of thetests/Stubs/glob for the reason its decidesk sibling documents, so it never shadows a real event at runtime.Tests
DecisionConcludedListenerNamespaceTestcovers three things: the outcome is projected for an event dispatched under either spelling, another consumer's decision is still ignored (widening the accepted classes must not widen thesourceAppfilter), and the registration list carries both spellings newest first.Mutation-checked, twice. Narrowing
handle()back to the singleinstanceoffails with "the listener ignored a real conclusion event dispatched as OCA\Decidiq\Event\DecisionConcludedEvent". Dropping the Decidiq entry from the constant fails the ordering assertion. Restored, 3/3 green.The event doubles live in
tests/Unit/EventListener/decision-conclusion-event-doubles.phpand load the twonextcloud/ocpbase declarations when nothing else has. That package ships them as real, self-contained classes but declares no autoload section, so in pure-unit modehandle(Event $event)could not be called at all and the one test that proves this fix would have had tomarkTestSkipped— the same as not having it. Every declaration is guarded, so on a booted Nextcloud the real classes win and the file is inert.Checks
lint, phpcs, phpmd, psalm and phpstan all exit 0.
test:allcannot build locally, before this branch as well as after:MigrateAppConfigKeysTestaborts the whole suite withInterface "OCP\Migration\IRepairStep" not foundbecause this checkout is not inside an installed Nextcloud root. Confirmed identical on unmodifieddevelopment. The new test was therefore run directly againsttests/bootstrap.php, which is also how it was mutation-checked; CI boots a real root and runs it with everything else.Note for whoever owns gate-114
It still reports
DecisionConcludedListener.php:33. That line is now one half of a two-line aliased import pair,use OCA\Decidiq\… as Decidiq…directly aboveuse OCA\Decidesk\… as Decidesk…, which is a dual-spelling list in every sense except that the statement-scoped exclusion cannot see across two statements. Ausepair is probably worth treating the way an array literal is.