Summary
MergeOrganisatieService::repointBySelfOrganisation() reads the owning organisation with a method_exists() probe:
// lib/Service/MergeOrganisatieService.php:450
if (method_exists($entity, 'getOrganisation') === true) { $owningOrganisation = ...; }
...
if ($owningOrganisation !== $source) { continue; }
$entity is an OCA\OpenRegister\Db\ObjectEntity. getOrganisation() is an @method docblock served by Entity::__call(), so the probe is false, $owningOrganisation stays null, and the next line continues every object.
Measured
Live, against the server's own Entity.php:
| subject |
method_exists |
is_callable |
ObjectEntity::getOrganisation |
false |
true |
ObjectEntity::getObject — concrete control |
true |
true |
Consequence
repointBySelfOrganisation() always returns 0. Meanwhile tombstoneSource() still tombstones the source organisation, so contract and compliancy objects are left pointing at @self.organisation = <tombstoned org>. The merge reports success having moved nothing.
The parity check between dry-run and execute cannot catch this: both arms are equally broken, so they agree. This is the same shape as an A/B whose two arms share the defect.
Also in this repo, lower rank
ReviewService.php:371 and IntakeService.php:278 — same probe on getUuid, no working fallback (is_array() is false for an ObjectEntity), so both return null. submit() returns uuid: null to the client and writes ['uuid' => null] to the audit log.
Seven further getUuid probes in this repo (OrganizationContactSyncJob:221, MigrateContactsToNc:386, ModerationService:344, ReviewAggregateService:279, FederationService:659, EolSyncService:463) are dead code with no runtime effect — ObjectEntity::getObject() always injects the uuid under id, so their $data['id'] fallback already holds. Worth tidying, not worth prioritising.
Fix
is_callable() is not a membership test on a __call class — measured, it is true for any name and the call then raises BadFunctionCallException. So swapping the probe yields an always-true guard, and the call must be made exception-safe in the same edit.
The most robust route here is to read @self.organisation from jsonSerialize(), which is genuinely concrete.
Please reshape the double in the same PR
Whatever test currently covers the merge will be green because its double declares the accessor concretely — that is what happened in every other confirmed instance of this bug. A fix without reshaping the double proves nothing. Write down how many tests you expect to go red when the fix is reverted, then check.
decidesk/tests/Stubs/Db/ObjectEntity.php is the fleet's reference double: it extends Entity, declares only genuinely concrete methods, and documents why.
Fleet context and the full 18-repo table: fleet-board/findings/method-exists-sweep.md.
Summary
MergeOrganisatieService::repointBySelfOrganisation()reads the owning organisation with amethod_exists()probe:$entityis anOCA\OpenRegister\Db\ObjectEntity.getOrganisation()is an@methoddocblock served byEntity::__call(), so the probe is false,$owningOrganisationstaysnull, and the next linecontinues every object.Measured
Live, against the server's own
Entity.php:method_existsis_callableObjectEntity::getOrganisationObjectEntity::getObject— concrete controlConsequence
repointBySelfOrganisation()always returns 0. MeanwhiletombstoneSource()still tombstones the source organisation, socontractandcompliancyobjects are left pointing at@self.organisation = <tombstoned org>. The merge reports success having moved nothing.The parity check between dry-run and execute cannot catch this: both arms are equally broken, so they agree. This is the same shape as an A/B whose two arms share the defect.
Also in this repo, lower rank
ReviewService.php:371andIntakeService.php:278— same probe ongetUuid, no working fallback (is_array()is false for anObjectEntity), so both returnnull.submit()returnsuuid: nullto the client and writes['uuid' => null]to the audit log.Seven further
getUuidprobes in this repo (OrganizationContactSyncJob:221,MigrateContactsToNc:386,ModerationService:344,ReviewAggregateService:279,FederationService:659,EolSyncService:463) are dead code with no runtime effect —ObjectEntity::getObject()always injects the uuid underid, so their$data['id']fallback already holds. Worth tidying, not worth prioritising.Fix
is_callable()is not a membership test on a__callclass — measured, it is true for any name and the call then raisesBadFunctionCallException. So swapping the probe yields an always-true guard, and the call must be made exception-safe in the same edit.The most robust route here is to read
@self.organisationfromjsonSerialize(), which is genuinely concrete.Please reshape the double in the same PR
Whatever test currently covers the merge will be green because its double declares the accessor concretely — that is what happened in every other confirmed instance of this bug. A fix without reshaping the double proves nothing. Write down how many tests you expect to go red when the fix is reverted, then check.
decidesk/tests/Stubs/Db/ObjectEntity.phpis the fleet's reference double: it extendsEntity, declares only genuinely concrete methods, and documents why.Fleet context and the full 18-repo table:
fleet-board/findings/method-exists-sweep.md.