A site running this module reports configuration that does not validate, and the cause is in the module: seven plugins store settings under a schema key nothing declares. An operator sees it on the configuration report; nothing in a build or a test run says a word about it.
Core keeps a permissive fallback for most of these families, which is what makes the gap quiet. field.formatter.settings.* and field.widget.settings.* are declared as a mapping with no keys, so an undeclared plugin saves without complaint and the site then reports every key the plugin actually stored as having no schema: two per entity view display for one formatter here. views.filter.* and views.field.* resolve to the base filter and field types, which cover the standard keys, so those two are correct today and would start reporting the moment either plugin defined an option of its own.
A views access plugin has no fallback at all, and that one is worse than untidy: the display's whole access.options mapping reports as having no schema, so a view the module ships is invalid out of the box.
The seven, with what each stores:
views.access.yoyaku_order_overview, no options: it defers to the route requirement the overview already carries. This is the one that invalidates a shipped view.field.formatter.settings.yoyaku_upcoming_slots: how many slots to list, and whether to show what is left.field.widget.settings.yoyaku_machine_name: the field the key is derived from.field.widget.settings.yoyaku_colorandfield.widget.settings.yoyaku_duration, no settings today.views.filter.yoyaku_booking_channel_offeredandviews.field.yoyaku_payment_order_link, no options today.
The rule this settles on is flat: a plugin the project ships in one of those families declares its own schema entry, whether or not it has settings yet. An entry with no keys is the honest answer for a plugin that stores none, it is what core writes for its own (views.access.none), and it means the day a setting is added the schema is already the place it goes. Eight plugins, eight entries, one of which was already there.
A unit test keeps it that way. It reads the plugin id out of every formatter, widget and views plugin the project ships, works out the schema key each one's settings belong under, and asserts a schema file declares it. Run against the code before the fix it fails seven times, once per gap, which is how the list above was checked rather than guessed. It walks only the project's own root and its modules/ tree, deliberately: CI builds the Drupal root inside the checkout, so a plain walk would take all of core and every contrib module for one of ours.
What this does not cover, and why a site may still report something. An existing site can also carry keys in its active configuration that no schema declares because the code that wrote them is gone: this project is pre-1.0 and ships no update hooks, so nothing removes them and reinstalling is the upgrade path. Those are site data rather than a defect here, and they are cleared by removing the keys. Worth knowing when reading a report, because the two look identical on the page.
And the browser tests were going red on a number, which had to be fixed here because nothing on this branch could go green until it was. Each red named a different scenario: the same commit, rerun untouched, passed the one that had failed and failed another. None is about anything under test. #3616893: Stop the offer stepper tests failing on a loaded runner instead of on a defect shipped AllowsForALoadedRunner for exactly this and its docblock says why, but it was adopted in a handful of call sites; everywhere else a test still wrote 10000, or took the ten seconds waitForElement(), waitForText() and assertJsCondition() default to. Half the trait was dead as well: it promises that a window the test itself holds open is ADDED to the budget, and keeps heldOpenFor to do it with, and nothing ever assigned that property. So no call site writes a wait any more, the trait grew the pieces that were missing, and holdTheAnswerBack() declares its window.
Routing the text waits turned up five assertions that could not fail: assertNotNull() around waitForText(), which answers a bool. Four are honest now and pass. The fifth is left exactly as it was found, with a comment saying why, because what it waits for does not happen: a full area is dropped from the choices of a booker holding none of it, so the card has no area to read a line from and the page announces that the choice was cleared instead. Two documented behaviors contradict each other there, and which one is right is a question about the page rather than about the test. Recorded here rather than decided: it wants its own issue and a maintainer's call.
One more real defect, found by sampling rather than by reading: the calendar's day() helper took a single findAll() snapshot of the grid and indexed into it. The grid is emptied and refilled on every answer, and the visitor's own holds arrive in a second one, so a snapshot can land on a bare grid and report a date as not being on screen. It polls for the cell it wants now.
What this does not claim. A green pipeline does not verify a flake fix, and the rate this reproduces at locally is high enough to sample: the count from repeated runs belongs on this issue before it is proposed for merge, not a single green badge.
AI-Generated: Yes (Claude Code was used to find the gaps, write the fix and the test, and draft this summary. I reviewed all of it before posting.)
Issue fork yoyaku-3618630
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #3
mably commentedComment #4
mably commentedComment #6
mably commented