Skip to content

fix(config): refuse getSetting() before ctld.initialize(), clearly - #141

Merged
FullGas1 merged 5 commits into
developfrom
fix/config-not-loaded-guard
Aug 26, 2026
Merged

fix(config): refuse getSetting() before ctld.initialize(), clearly#141
FullGas1 merged 5 commits into
developfrom
fix/config-not-loaded-guard

Conversation

@FullGas1

@FullGas1 FullGas1 commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Closes Using a CTLD manager before ctld.initialize() crashes on a nil setting instead of saying so #125. A CTLD manager touched before ctld.initialize() used to crash on an unreadable arithmetic error deep inside src/, with a stack trace naming neither CTLD nor the missing init. Every setting read before init silently resolved to nil, and the first manager to do arithmetic on that nil (e.g. a refresh interval) is the one that crashed.
  • CTLDConfig:getSetting — the sole real read path, everything in src/ reads config through ctld.gs, which delegates straight to it — now refuses immediately with error(msg, 3), pointing the stack at the actual caller instead of at this line.
  • Chosen over the issue's own suggestion of a guard in each manager's getInstance() (~18 call sites) or a guard in ctld.gs: getSetting is the deepest single choke point, catching both the ctld.gs path and the documented-but-unused direct CTLDConfig.get():getSetting() API.
  • CTLD_i18n.lua's pre-init tr() tolerance (already wraps its read in pcall, falling back to a default language) is unaffected — verified with a new test, the pcall catches the new explicit error exactly as it caught the old implicit crash.
  • Grilled with the user 2026-08-26 (.backlog/FIX-CONFIG-NOT-LOADED-GUARD/PRD.md).

Test plan

  • TDD: new tests written red first (getSetting/ctld.gs raise before init), confirmed failing against the old code, then green after the fix.
  • Regression test confirms ctld.tr() still degrades gracefully pre-init (no raise) via the existing pcall site.
  • Full suite: busted --pattern=_spec --helper=tests/ci/helpers/init.lua tests/ci → 1359 passed / 0 failed / 1 pending (pre-existing, DCS-live gated).
  • luac -p CTLD.lua — Lua 5.1 syntax OK (rebuilt via merge_CTLD.ps1).
  • luacheck --config .luacheckrc src/CTLD_config.lua — 1 pre-existing warning (unused config local, already counted in the known 89), no new warning introduced.

Summary by Sourcery

Refuse CTLD configuration access before initialization with a clear diagnostic instead of allowing delayed, unreadable runtime failures.

Bug Fixes:

  • Make CTLD configuration reads fail immediately with a clear message naming ctld.initialize() when accessed before initialization.
  • Keep the loaded-state guard effective after malformed configuration loads and report premature companion asset checks instead of crashing.
  • Preserve graceful pre-initialization translation fallback behavior.

Documentation:

  • Add changelog and backlog documentation for the pre-initialization configuration-read fix.

Tests:

  • Add coverage for direct and delegated configuration reads before initialization, recovery after loading, malformed-load handling, and pre-initialization translation behavior.

Reading a CTLD setting before ctld.initialize() returns nil
silently, then usually crashes a few lines later on ordinary
arithmetic - a stack trace naming neither CTLD nor the missing
init. Grilled with the user 2026-08-26 following GitHub issue #125
(davidp57/Zip): a single guard in CTLDConfig:getSetting, the
deepest real choke point, rather than the issue's own suggestion of
18 per-manager getInstance() guards.
Single AFK ticket: the getSetting guard, tests extending the
existing config_spec.lua seam, CHANGELOG entry. Granularity
confirmed with the user.
A CTLD manager touched before ctld.initialize() used to crash on an
unreadable arithmetic error deep inside src/, with a stack trace
naming neither CTLD nor the missing init. Every setting read before
init silently resolved to nil, and the first manager to do
arithmetic on that nil (e.g. a refresh interval) is the one that
crashed.

CTLDConfig:getSetting - the sole real read path, everything in src/
reads config through ctld.gs, which delegates straight to it - now
refuses immediately with error(msg, 3), pointing the stack at the
actual caller instead of at this line. Chosen over the issue's own
suggestion of a guard in each manager's getInstance() (~18 call
sites) or a guard in ctld.gs: getSetting is the deepest single choke
point, catching both the ctld.gs path and the documented-but-unused
direct CTLDConfig.get():getSetting() API.

CTLD_i18n.lua's pre-init tr() tolerance (already wraps its read in
pcall, falling back to a default language) is unaffected - verified
with a new test, the pcall catches the new explicit error exactly
as it caught the old implicit crash.

FIX-CONFIG-NOT-LOADED-GUARD, grilled with the user 2026-08-26,
closes GitHub issue #125.
@FullGas1
FullGas1 requested a review from davidp57 as a code owner August 26, 2026 20:29
@sourcery-ai

sourcery-ai Bot commented Aug 26, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR changes pre-initialization configuration access from silent nil propagation to an immediate, caller-focused error at CTLDConfig:getSetting, while preserving ctld.tr()'s existing graceful fallback and adding targeted regression tests and documentation.

Sequence diagram for pre-initialization configuration access

sequenceDiagram
    participant Caller
    participant CTLDConfig
    participant CTLD_i18n

    Caller->>CTLDConfig: ctld.gs(key)
    CTLDConfig->>CTLDConfig: getSetting(key)
    alt configuration not loaded
        CTLDConfig-->>Caller: error(..., 3)
    else configuration loaded
        CTLDConfig-->>Caller: setting value
    end
    CTLD_i18n->>CTLDConfig: ctld.gs(i18n_lang)
    CTLDConfig-->>CTLD_i18n: error(..., 3)
    CTLD_i18n->>CTLD_i18n: pcall(...)
    CTLD_i18n-->>Caller: fallback language
Loading

Flow diagram for the configuration guard choke point

flowchart LR
    A["ctld.initialize()"] --> B["CTLDConfig:load()"]
    C["ctld.gs(key)"] --> D["CTLDConfig:getSetting(key)"]
    E["CTLDConfig.get():getSetting(key)"] --> D
    D -->|isLoaded| F["Return setting or default"]
    D -->|not loaded| G["Clear error naming ctld.initialize()"]
    H["ctld.tr()"] --> I["pcall around language lookup"]
    I -->|error caught| J["Fallback language"]
Loading

File-Level Changes

Change Details Files
Added an early initialization guard to the central configuration read path.
  • Rejects reads while configuration is unloaded with a clear message naming ctld.initialize().
  • Uses error(..., 3) so propagated failures identify the caller rather than the configuration implementation.
  • Covers both ctld.gs and direct CTLDConfig:getSetting() access without adding per-manager guards.
src/CTLD_config.lua
Added regression coverage for unloaded and loaded configuration behavior.
  • Verifies direct and delegated reads fail before initialization.
  • Verifies reads resume normally after load().
  • Verifies pre-initialization ctld.tr() continues to fall back without raising.
tests/ci/unit/config_spec.lua
tests/ci/unit/i18n_spec.lua
Documented the behavior change and implementation rationale.
  • Added an Unreleased changelog entry describing the clearer failure mode and preserved i18n tolerance.
  • Added the PRD, implementation ticket, and backlog tracking entry.
CHANGELOG.md
.backlog/FIX-CONFIG-NOT-LOADED-GUARD/PRD.md
.backlog/FIX-CONFIG-NOT-LOADED-GUARD/tickets/01-getsetting-guard.md
.backlog/README.md

Assessment against linked issues

Issue Objective Addressed Explanation
#125 Prevent CTLD setting and manager operations performed before ctld.initialize() from failing later on nil values or unreadable arithmetic errors.
#125 Provide a clear diagnostic telling users that CTLD must be initialized with ctld.initialize() before settings or managers are used.
#125 Preserve intended pre-initialization behavior for CTLD internationalization while documenting and testing the fix.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/CTLD_config.lua" line_range="111-113" />
<code_context>
+1. `src/CTLD_config.lua`, `CTLDConfig:getSetting` (~line 106): at the top, before reading
+   `self.settings[key]`, add:
+   ```lua
+   if not self.isLoaded then
+       error("CTLD configuration is not loaded — call ctld.initialize() before reading any "
+           .. "CTLD setting or using a CTLD manager.", 3)
+   end
+   ```
</code_context>
<issue_to_address>
**issue (broader_impact):** A failed `CTLDConfig:load()` leaves `self.isLoaded` set to true because `load()` marks it loaded before parsing and validation. Subsequent `getSetting()` calls therefore bypass this guard and return incomplete or nil values instead of refusing with the promised initialization error.

**Triggers:** When `ctld.configUser` is malformed or empty, or another error occurs during configuration loading.

**Suggested fix:** Set `isLoaded` only after loading completes successfully, or reset it in an error path before rethrowing the load failure.
</issue_to_address>

### Comment 2
<location path="tests/ci/unit/i18n_spec.lua" line_range="344-346" />
<code_context>
+        -- FIX-CONFIG-NOT-LOADED-GUARD: a pre-init tr() must still degrade gracefully, not raise
+        -- the new getSetting() guard error. _activeLang()'s own pcall is what makes this work.
+        it("a call before ctld.initialize() still returns a string, never raises", function()
+            CTLDConfig._instance = nil   -- fresh, never-loaded instance — no :load() call
+            assert.has_no_error(function()
+                assert.equals("hello", ctld.tr("__TESTKEY__"))
+            end)
+        end)
</code_context>
<issue_to_address>
**issue (testing):** The regression test replaces the global configuration singleton with a never-loaded instance and does not restore or load it afterward. Any later test or shared setup that calls `ctld.gs()` without reinitializing the singleton now raises the new guard error, making the suite order-dependent.

**Triggers:** When another spec runs after this test and relies on the existing loaded configuration state.

**Suggested fix:** Restore the prior singleton in an `after_each`, or call `CTLDConfig.get():load()` after the pre-init assertion.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/CTLD_config.lua Outdated
Comment thread tests/ci/unit/i18n_spec.lua
Two review findings on the getSetting() not-loaded guard:

- CTLDConfig:load() set isLoaded=true before validating a malformed
  ctld.configUser, so an aborted load left the new guard silently
  bypassed (settings stayed empty, but isLoaded already read true).
  isLoaded is now only set after validation passes.
- error(msg, 3) produced a positionless error for real tail-call
  sites (CTLD_aasystem.lua, CTLD_troop.lua): a Lua 5.1 tail call
  drops the caller's own stack frame, so a fixed level either
  mis-points or lands on a frame with no line info. Switched to
  error(msg, 0) - message-only, which is what actually carries the
  diagnosis.

Also: tools/companion/asset_check.lua now reports (via its existing
outText/log style) instead of crashing uncaught when the guard fires
during collection; config_spec.lua's new describe block gained the
after_each the sibling i18n_spec.lua block already had, plus a red
test for the load() ordering bug.

FIX-CONFIG-NOT-LOADED-GUARD, review remediation on PR #141.
@FullGas1
FullGas1 merged commit ad119a5 into develop Aug 26, 2026
9 checks passed
@FullGas1
FullGas1 deleted the fix/config-not-loaded-guard branch August 26, 2026 20:47
FullGas1 added a commit that referenced this pull request Aug 26, 2026
… index entry (#142)

The index line still described error(msg, 3); the fix landed as
error(msg, 0) after review remediation on PR #141 (tail calls break
a fixed stack level in Lua 5.1).
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.

Using a CTLD manager before ctld.initialize() crashes on a nil setting instead of saying so

2 participants