Summary
LarpingApp's server-authoritative skill-requirement enforcement never runs for writes made through the OpenRegister REST objects API — the path the SPA and every REST client use. A character that violates a declared requiredSkills[] prerequisite is created and updated successfully.
CharacterRequirementListener, SkillRequirementService and SkillRequirementChecker (1001 lines) are correct and registered. The problem is upstream: OpenRegister does not dispatch the vetoable pre-write event on that write path, so the listener is never invoked.
This makes the spec's first scenario — "Direct API write bypassing the UI is rejected" — currently false, and 16 skill-requirement-enforcement scenarios untestable as written. They are the largest gate-19 cluster (50 findings total).
Reproduction (measured, not inferred)
Isolated rig: NC 34.0.0, openregister 0.2.17-unstable.36 (healthy 75-entry vendor/), larpingapp 0.1.39, register provisioned by this app's own tests/e2e/ci-seed.sh, firstrunwizard disabled.
Skill PROBE-adv declares requiredSkills: [PROBE-basic]. Character does not have PROBE-basic:
| write |
required by spec |
observed |
POST /apps/openregister/api/objects/{register}/{characterSchema} with skills:[adv] |
rejected before persistence |
HTTP 201, persisted |
PUT …/{characterSchema}/{id} adding skills:[adv] |
rejected before persistence |
HTTP 200, persisted |
The listener is not failing open either — it catches \Throwable and logs, and:
grep -c 'skill-requirement validation errored' data/nextcloud.log -> 0
It is never called at all.
Root cause
ObjectsController saves through objectService->saveObject(...). In openregister/lib/Service/Object/SaveObjects.php:
grep -c 'ObjectCreatingEvent|ObjectUpdatingEvent' -> 0 # the VETOABLE pre-write events
grep -n 'ObjectCreatedEvent|ObjectUpdatedEvent' -> 72,73,1687 # positive control: the grep works
grep -n '->insert(|->update(' -> (none)
The vetoable events are dispatched only from MagicMapper::insertObjectEntity() (l.6764) and the two ObjectUpdatingEvent sites (l.6919, l.7158), reached via MagicMapper::insert() (l.8803). SaveObjects never calls those.
Those dispatch sites do honour isPropagationStopped() and getErrors(), so the veto contract itself is implemented — the REST path simply does not traverse it. Note the asymmetry: the post-write events fire on both paths (SaveObjects.php:1687 and MagicMapper.php:8832), the pre-write events on only one.
Why this is easy to miss
- The app's unit tests exercise the service and the listener directly, so they are green.
gate-61 only inspects post-event listeners, so it cannot see this.
- Any e2e test written in the acceptance direction ("this valid assignment succeeds") passes with the enforcement layer entirely absent.
That last point is not hypothetical. I wrote 13 full-stack tests for these scenarios. 10 failed. The 3 that passed all asserted that a write is accepted — and would pass with enforcement removed, exactly the shallowness #301 describes. They were deliberately not shipped: anchoring all 13 would have moved gate-19 from 50 to 37 while proving nothing.
Suggested resolution
- Raise on OpenRegister:
SaveObjects must dispatch ObjectCreatingEvent/ObjectUpdatingEvent and honour stopPropagation() + getErrors(), as MagicMapper already does — otherwise the vetoable-event contract is not available to any consuming app on the REST path.
- Until then, the 16
skill-requirement-enforcement scenarios should stay as open gate-19 findings. They must not be closed with @e2e exclude: a browser can observe this perfectly well, and the reason would be "the feature does not work", which is a defect report, not an exemption.
- When it is fixed, the rejection-direction tests must land first — they are the only ones that can fail for the right reason.
Related
Summary
LarpingApp's server-authoritative skill-requirement enforcement never runs for writes made through the OpenRegister REST objects API — the path the SPA and every REST client use. A character that violates a declared
requiredSkills[]prerequisite is created and updated successfully.CharacterRequirementListener,SkillRequirementServiceandSkillRequirementChecker(1001 lines) are correct and registered. The problem is upstream: OpenRegister does not dispatch the vetoable pre-write event on that write path, so the listener is never invoked.This makes the spec's first scenario — "Direct API write bypassing the UI is rejected" — currently false, and 16
skill-requirement-enforcementscenarios untestable as written. They are the largest gate-19 cluster (50 findings total).Reproduction (measured, not inferred)
Isolated rig: NC 34.0.0,
openregister 0.2.17-unstable.36(healthy 75-entryvendor/),larpingapp 0.1.39, register provisioned by this app's owntests/e2e/ci-seed.sh,firstrunwizarddisabled.Skill
PROBE-advdeclaresrequiredSkills: [PROBE-basic]. Character does not havePROBE-basic:POST /apps/openregister/api/objects/{register}/{characterSchema}withskills:[adv]PUT …/{characterSchema}/{id}addingskills:[adv]The listener is not failing open either — it catches
\Throwableand logs, and:It is never called at all.
Root cause
ObjectsControllersaves throughobjectService->saveObject(...). Inopenregister/lib/Service/Object/SaveObjects.php:The vetoable events are dispatched only from
MagicMapper::insertObjectEntity()(l.6764) and the twoObjectUpdatingEventsites (l.6919, l.7158), reached viaMagicMapper::insert()(l.8803).SaveObjectsnever calls those.Those dispatch sites do honour
isPropagationStopped()andgetErrors(), so the veto contract itself is implemented — the REST path simply does not traverse it. Note the asymmetry: the post-write events fire on both paths (SaveObjects.php:1687andMagicMapper.php:8832), the pre-write events on only one.Why this is easy to miss
gate-61only inspects post-event listeners, so it cannot see this.That last point is not hypothetical. I wrote 13 full-stack tests for these scenarios. 10 failed. The 3 that passed all asserted that a write is accepted — and would pass with enforcement removed, exactly the shallowness #301 describes. They were deliberately not shipped: anchoring all 13 would have moved gate-19 from 50 to 37 while proving nothing.
Suggested resolution
SaveObjectsmust dispatchObjectCreatingEvent/ObjectUpdatingEventand honourstopPropagation()+getErrors(), asMagicMapperalready does — otherwise the vetoable-event contract is not available to any consuming app on the REST path.skill-requirement-enforcementscenarios should stay as open gate-19 findings. They must not be closed with@e2e exclude: a browser can observe this perfectly well, and the reason would be "the feature does not work", which is a defect report, not an exemption.Related
@e2etags, and the warning against retargeting tags onto tests that do not perform their scenario. Same standard applied here.31490125155).