Skip to content

Fix visualizer settings being clobbered by a minimal app override (breaks all rule runs) - #24

Merged
lmajano merged 5 commits into
developmentfrom
fix/visualizer-settings-deep-merge
Oct 3, 2026
Merged

lmajano merged 5 commits into
developmentfrom
fix/visualizer-settings-deep-merge

Conversation

@lmajano

@lmajano lmajano commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Description

Failing scenario. docs/guides/visualizer.md tells people to enable the visualizer with variables.moduleSettings = { rulebox = { visualizer = { enabled = true } } }. ColdBox merges an app's module settings over a module's defaults with a shallow append, so that replaces the whole default visualizer struct and metricsStore / datasourceName vanish. RuleEventBus.onDIComplete() then throws The key [metricsStore] was not found in the struct, the bus is never built, and every rule run fails with Cannot invoke method [publish()] on a null object because RuleBook injects the bus. The test harness hid this because test-harness/config/Coldbox.bx sets metricsStore explicitly. Apps leaving the visualizer off were unaffected.

Fix. ModuleConfig.bx is the single place that sets up and validates the visualizer settings:

  • visualizerDefaults() holds the defaults; configure() uses it.
  • onLoad() deep-merges the app's visualizer values over those defaults (app values win), then validates them. A value of the wrong shape throws RuleBox.InvalidSettingException naming the key, so a bad config fails on app start:
    • visualizer must be a struct
    • enabled must be a boolean
    • metricsStore and datasourceName must be non-blank
  • The handler, RuleEventBus and SQLiteMetricsStore are unchanged: they read the settings directly and trust them. No fallbacks are scattered across readers.

Verification. TestBox on BoxLang 1.18.0 (miniserver, private TestBox copy): VisualizerSettingsSpec 6/6 pass, full suite 132 passed, 0 failed, 6 skipped (pre-existing skips).

New spec test-harness/tests/specs/VisualizerSettingsSpec.bx: onLoad() back-fills a minimal override and keeps supplied values; it throws RuleBox.InvalidSettingException for a bad enabled, a blank metricsStore, a complex datasourceName and a non-struct visualizer; RuleEventBus resolves InMemoryMetricsStore@rulebox and survives an unresolvable store; a RuleBook run still succeeds and publishes an event under the minimal settings.

Issues

Release blocker found while preparing 2.0.0 (no separate issue filed).

Type of change

  • Bug Fix

Checklist

  • My code follows the style guidelines of this project cfformat
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation (the visualizer guide update is a follow-up docs PR)
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Follow-ups

🤖 Generated with Claude Code

https://claude.ai/code/session_01Yc5MDcm3tyR2yqPWs9RpgX

claude added 2 commits October 3, 2026 11:53
ColdBox merges an app's moduleSettings over a module's defaults with a
shallow append, so the documented minimal config
`visualizer = { enabled = true }` replaced the whole default visualizer
struct and dropped metricsStore/datasourceName. RuleEventBus.onDIComplete
then threw on the missing key (its catch block re-read the same key, so
the error escaped), the bus was never built, and every RuleBook run failed
with a null publish() call.

- ModuleConfig: single visualizerDefaults() source; onLoad() deep-merges
  the defaults under whatever the app supplied.
- RuleEventBus, Visualizer handler, SQLiteMetricsStore: tolerate missing
  visualizer keys (defence in depth); resolveMetricsStore() no longer reads
  settings inside its own catch block.
- Add VisualizerSettingsSpec.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Yc5MDcm3tyR2yqPWs9RpgX
ModuleConfig is now the single place that sets up and checks the visualizer
settings: onLoad() deep-merges the app's values over visualizerDefaults(), then
throws RuleBox.InvalidSettingException for a value of the wrong shape. The
handler, RuleEventBus and SQLiteMetricsStore go back to reading the settings
directly, with no scattered fallbacks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Yc5MDcm3tyR2yqPWs9RpgX
lmajano pushed a commit that referenced this pull request Oct 3, 2026
The handler reads visualizer.maxStreams directly; ModuleConfig fills the default
of 25 and rejects anything that is not a whole number of 1 or more at load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Yc5MDcm3tyR2yqPWs9RpgX
lmajano pushed a commit that referenced this pull request Oct 3, 2026
retentionDays, maxStoredEvents, circuitBreakerThreshold and
circuitBreakerCooldownSeconds get their defaults and whole-number validation in
ModuleConfig. SQLiteMetricsStore reads them directly; numericSetting() is gone.
Specs build store settings from the module's completed settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Yc5MDcm3tyR2yqPWs9RpgX
Comment thread ModuleConfig.bx Outdated
* The default `visualizer` settings: the single source of truth for both configure() and the
* onLoad() back-fill of keys an app's (shallow-merged) override left out.
*/
private struct function visualizerDefaults(){

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is int this static

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, it has no instance state. Made it private static struct function visualizerDefaults() in 58c7583 (same change on #35 and #36 so they still merge cleanly). Each call still returns a fresh struct, so the deep-merge in onLoad() never mutates shared defaults. Full suite: 132 passed, 0 failed.


Generated by Claude Code

Comment thread ModuleConfig.bx
claude added 2 commits October 3, 2026 15:00
The defaults do not depend on instance state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Yc5MDcm3tyR2yqPWs9RpgX
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Yc5MDcm3tyR2yqPWs9RpgX
Comment thread ModuleConfig.bx Outdated
* @return A new struct of defaults on every call, so callers can merge into it safely
*/
private static struct function visualizerDefaults(){
return {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why. A module is loaded once this should be a static variable

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. In db66c6d the defaults are a static VISUALIZER_DEFAULTS struct in a static { } block. configure() copies it once, and onLoad() fills the keys an app left out with append( static.VISUALIZER_DEFAULTS, false ), so the static is never written to. validateVisualizerSettings() is now void. Full suite: 132 passed, 0 failed. Merged into #35 and #36 as well.


Generated by Claude Code

A module is loaded once, so the defaults are a static struct instead of a
function. configure() copies it, and onLoad() fills the keys an app left out
with append( static.VISUALIZER_DEFAULTS, false ), so the static is never
written to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Yc5MDcm3tyR2yqPWs9RpgX
@lmajano
lmajano merged commit 8dd182a into development Oct 3, 2026
10 checks passed
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.

2 participants