Skip to content

feat(config-schema): versioned plugin config schemas + doctor extension (phase 1 of #80)#86

Open
rolling-codes wants to merge 3 commits into
mainfrom
feat/config-schema-phase1
Open

feat(config-schema): versioned plugin config schemas + doctor extension (phase 1 of #80)#86
rolling-codes wants to merge 3 commits into
mainfrom
feat/config-schema-phase1

Conversation

@rolling-codes

@rolling-codes rolling-codes commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Summary

Phase 1 of #80 — versioned plugin config schemas.

  • easycord/config_schema.py — new ConfigSchema class with apply(): pure function, no I/O. Handles absent/empty/non-dict sections, migration chains, missing-key backfill, _v version stamping, and unknown-key preservation.
  • PluginConfigManager.get_schema() — fast path (pure read) when section is already valid; heal path uses store.mutate() under the per-guild lock for atomic persist.
  • SuggestionsPlugin pilot_get_config() switches from get() to get_schema(SCHEMA), guaranteeing all default keys are present on every read. A missing enabled key is now impossible to observe.
  • easycord doctor extension — generic .tmp and corrupt-JSON detection (always); plugin config schema drift detection and --fix-configs healing (requires bot target).
  • 11 new tests in tests/test_config_schema.py (9 unit, 2 integration). Full suite: 1524 tests.

Test plan

  • pytest tests/test_config_schema.py -v — all 11 pass
  • pytest -q — full suite 1524 green, no regressions
  • ruff check easycord tests --select E9,F63,F7,F82 — clean
  • python scripts/verify_plugin_tests.py — all thresholds met
  • easycord doctor --help--fix-configs flag visible

Summary by Sourcery

Introduce versioned plugin configuration schemas with migration support and integrate them into plugin config management and diagnostics.

New Features:

  • Add ConfigSchema for declarative, versioned plugin config sections with migrations and default backfilling.
  • Extend PluginConfigManager with get_schema to read, heal, and persist config sections based on a schema.
  • Wire SuggestionsPlugin to use a ConfigSchema-backed config accessor ensuring all default keys are always present.
  • Add an easycord doctor --fix-configs mode to detect and heal plugin config schema drift across guild config files.

Enhancements:

  • Enhance doctor command with generic detection of leftover .tmp files and corrupt JSON config files, plus health reporting.
  • Log schema-based config healing operations for better observability.

Documentation:

  • Add config-schema documentation describing schema declaration, get_schema usage, migrations, doctor checks, and the apply() contract.

Tests:

  • Add unit and integration tests covering ConfigSchema.apply behaviours and PluginConfigManager.get_schema fast/heal paths.

…on (phase 1 of #80)

- Add ConfigSchema class (easycord/config_schema.py): pure apply() with migration
  chain, _v version stamping, missing-key backfill, non-dict replacement, and
  unknown-key preservation.
- Add PluginConfigManager.get_schema(): fast path (pure read) when section is
  already valid; heal path uses atomic store.mutate() under the per-guild lock.
- Pilot on SuggestionsPlugin: _get_config() now uses get_schema(SCHEMA) instead
  of get() + manual defaults, guaranteeing all keys are present on every read.
- Extend easycord doctor: generic .tmp and corrupt-JSON detection (always); plugin
  config schema drift detection (with bot target); --fix-configs flag applies
  sync healing via atomic JSON write (temp + rename).
- 11 new tests in tests/test_config_schema.py; full suite: 1524 tests.
@rolling-codes rolling-codes added the enhancement New feature or request label Jul 12, 2026
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@sourcery-ai

sourcery-ai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Reviewer's Guide

Introduces a declarative, versioned plugin configuration schema system with healing and migration support, wires it into the plugin config manager and SuggestionsPlugin, extends easycord doctor to detect and optionally fix schema drift and generic config file issues, and adds tests and docs for the new behavior.

Sequence diagram for PluginConfigManager.get_schema healing flow

sequenceDiagram
    participant Plugin as SuggestionsPlugin
    participant PCM as PluginConfigManager
    participant Store as ServerConfigStore
    participant Schema as ConfigSchema

    Plugin->>PCM: get_schema(guild_id, SCHEMA)
    PCM->>Store: load(guild_id)
    Store-->>PCM: cfg_obj
    PCM->>Schema: apply(cfg_obj.get_other(schema.key))
    Schema-->>PCM: healed_section, changes
    alt [changes is empty]
        PCM-->>Plugin: section (pure read)
    else [changes not empty]
        PCM->>Store: mutate(guild_id, _heal)
        Store-->>PCM: healed_section
        PCM-->>Plugin: healed_section
    end
Loading

Sequence diagram for easycord doctor schema drift detection and healing

sequenceDiagram
    actor Dev as Developer
    participant CLI as cmd_doctor
    participant Doctor as _doctor_report
    participant Bot as bot
    participant Plugin as Plugin
    participant Schema as ConfigSchema

    Dev->>CLI: easycord doctor --fix-configs target
    CLI->>Doctor: _doctor_report(target, fix_configs=True)
    Doctor->>Bot: iterate _plugins
    Doctor->>Plugin: inspect config.store._base
    Doctor->>Schema: apply(section)
    Schema-->>Doctor: healed_section, changes
    alt [changes present]
        Doctor->>Doctor: _apply_schema_fixes(schema_plugins)
        Doctor-->>CLI: report with healed count
    else [no changes]
        Doctor-->>CLI: report schemas healthy
    end
Loading

File-Level Changes

Change Details Files
Add ConfigSchema class to support versioned, migratable plugin config sections with healing semantics.
  • Implement ConfigSchema with key, version, defaults, and migration registry
  • Provide apply() to normalize sections, run migration chains, backfill missing keys, preserve unknown keys, and stamp _v
  • Ensure apply() is pure (no I/O, no mutation of input dict) and logs via easycord logger
easycord/config_schema.py
Extend PluginConfigManager with schema-aware read that heals and persists configs atomically when needed.
  • Add async get_schema() to read a section, run schema.apply(), and choose between fast pure-read path and healing path
  • Use store.load for fast path when no changes are needed
  • Use store.mutate with per-guild lock to apply healed section, persist via set_other, and log heal operations
easycord/plugins/_config_manager.py
Pilot the schema system in SuggestionsPlugin by declaring a schema and switching config reads to use it.
  • Declare module-level SCHEMA for suggestions config with version and defaults
  • Change _get_config() to call config.get_schema() instead of get() with defensive defaults
  • Update docstring to describe healing of missing keys via schema
easycord/plugins/suggestions.py
Enhance easycord doctor to detect plugin config schema drift, heal configs on demand, and report generic config file health issues.
  • Introduce _apply_schema_fixes() to iterate plugins with SCHEMA, heal guild JSON files via schema.apply(), and atomically rewrite files using .tmp rename
  • Extend _doctor_report() to collect schema_plugins, compute schema_issues via schema.apply(), and report either drift or healthy state
  • Add optional fix_configs flag to _doctor_report() and cmd_doctor; wire CLI flag --fix-configs that triggers healing
  • Add checks for leftover .tmp files and corrupt JSON in .easycord, reporting issues or overall config health
easycord/cli.py
Document the new plugin config schema mechanism and its usage, including migrations and doctor integration.
  • Add config-schema.md explaining declaration of ConfigSchema, using get_schema(), migration patterns, and doctor commands
  • Describe apply() contract, including behavior for None, empty, non-dict, partial, and outdated sections, and preservation of unknown keys
docs/config-schema.md
Add tests to validate ConfigSchema.apply behavior and PluginConfigManager.get_schema integration with the real store.
  • Create unit tests for apply() covering absent/empty sections, non-dict inputs, missing-key backfill, unknown-key preservation, purity, version stamping, and migration chains
  • Add async integration tests ensuring get_schema heals missing keys, persists healed sections, and uses fast path without calling mutate when section is already clean
tests/test_config_schema.py

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

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@rolling-codes, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 23 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 73f15023-49ad-480a-836f-f1ce7382ce9a

📥 Commits

Reviewing files that changed from the base of the PR and between 9aa4ef6 and fee494f.

📒 Files selected for processing (6)
  • docs/config-schema.md
  • easycord/cli.py
  • easycord/config_schema.py
  • easycord/plugins/_config_manager.py
  • easycord/plugins/suggestions.py
  • tests/test_config_schema.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/config-schema-phase1

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added documentation Improvements or additions to documentation plugin tests labels Jul 12, 2026
@codecov

codecov Bot commented Jul 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 61.42857% with 54 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
easycord/cli.py 18.46% 53 Missing ⚠️
easycord/config_schema.py 98.21% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Comment thread easycord/cli.py Fixed
Comment thread easycord/cli.py Fixed

@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 4 issues, and left some high level feedback:

  • The schema drift detection in _doctor_report and the healing logic in _apply_schema_fixes both walk plugin stores and JSON files separately; consider reusing the same discovered guild paths or passing the detected issues into the fixer to avoid redundant file I/O.
  • In _doctor_report, schema_plugins includes plugins even if their config.store._base is None, whereas _apply_schema_fixes iterates over all of them again and skips those without a base; you could pre-filter schema_plugins to only those with a valid store path to simplify both loops.
  • The .easycord root used for generic config health checks is hard-coded as Path('.easycord'); if there is already a single source of truth for the config root elsewhere, consider using that to avoid divergence if the root location changes.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The schema drift detection in `_doctor_report` and the healing logic in `_apply_schema_fixes` both walk plugin stores and JSON files separately; consider reusing the same discovered guild paths or passing the detected issues into the fixer to avoid redundant file I/O.
- In `_doctor_report`, `schema_plugins` includes plugins even if their `config.store._base` is `None`, whereas `_apply_schema_fixes` iterates over all of them again and skips those without a base; you could pre-filter `schema_plugins` to only those with a valid store path to simplify both loops.
- The `.easycord` root used for generic config health checks is hard-coded as `Path('.easycord')`; if there is already a single source of truth for the config root elsewhere, consider using that to avoid divergence if the root location changes.

## Individual Comments

### Comment 1
<location path="easycord/cli.py" line_range="423-424" />
<code_context>
+                tmp.write_text(json.dumps(data, indent=2), encoding="utf-8")
+                tmp.replace(guild_json)
+                fixed += 1
+            except (json.JSONDecodeError, OSError, ValueError):
+                pass
+    return fixed
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Avoid silently swallowing all JSON/IO/schema errors during schema heal

Catching `JSONDecodeError`, `OSError`, and `ValueError` and then `pass`-ing hides config/schema and filesystem problems, which makes failures in `--fix-configs` hard to understand. Please log at least a debug or warning message with the file path and exception, or gate logging behind a verbosity flag / once-per-file mechanism to avoid noise, rather than silently swallowing these errors.
</issue_to_address>

### Comment 2
<location path="easycord/cli.py" line_range="545-556" />
<code_context>
+                        pass
+
+            if schema_issues:
+                _fixed = _apply_schema_fixes(schema_plugins) if fix_configs else 0
+                add(
+                    "config.schema_health",
+                    "Plugin config schema drift",
+                    fix_configs and _fixed >= 0,
+                    f"{len(schema_issues)} guild config(s) need healing"
+                    if not fix_configs
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Success flag for schema heal always reports OK when --fix-configs is used

Because `ok` is computed as `fix_configs and _fixed >= 0`, it always evaluates to `True` whenever `--fix-configs` is used, even if no configs were actually healed. This can mask remaining schema drift after a supposed repair. Consider basing `ok` on the relationship between `_fixed` and `schema_issues` (e.g. `(_fixed == 0 and not schema_issues)` or `(_fixed >= len(schema_issues))`), or at least on `schema_issues` when `fix_configs` is true, so the result reflects the actual post-heal state.

```suggestion
            if schema_issues:
                _fixed = _apply_schema_fixes(schema_plugins) if fix_configs else 0
                _issue_count = len(schema_issues)
                add(
                    "config.schema_health",
                    "Plugin config schema drift",
                    (_fixed >= _issue_count) if fix_configs else False,
                    f"{_issue_count} guild config(s) need healing"
                    if not fix_configs
                    else f"{_fixed} of {_issue_count} guild config(s) healed",
                    severity="warning",
                    fix="" if fix_configs else f"Run: easycord doctor --fix-configs {target}",
                )
```
</issue_to_address>

### Comment 3
<location path="easycord/config_schema.py" line_range="90-96" />
<code_context>
+        result = dict(section)
+
+        # --- migrations ---------------------------------------------------------
+        current_v = result.get("_v")
+        if current_v is None:
+            current_v = 1  # pre-schema; treat as version 1
+            if self._version > 1:
+                changes.append("no _v stamp — treating as pre-schema v1")
+
+        if isinstance(current_v, int) and current_v < self._version:
+            v = current_v
+            while v < self._version:
</code_context>
<issue_to_address>
**issue:** Non-integer _v values skip migrations silently and remain inconsistent

When `_v` exists but isn’t an `int`, `isinstance(current_v, int)` causes all migrations to be skipped and leaves `_v` in a bad type, so future heals will also skip migrations. Consider treating non-int `_v` as invalid and either resetting it to a baseline (e.g. pre-schema v1) or overwriting it with `self._version` while recording that correction, so the version stamp and migration path stay consistent.
</issue_to_address>

### Comment 4
<location path="tests/test_config_schema.py" line_range="92-101" />
<code_context>
+    assert result["_v"] == 2
+
+
+def test_apply_runs_migration_chain() -> None:
+    schema = ConfigSchema(
+        key="test", version=3, defaults={**_DEFAULTS, "new_v3": "hi"}
+    )
+
+    @schema.migration(from_version=1)
+    def _v1_to_v2(s: dict) -> dict:
+        return {**s, "_migrated_v1": True}
+
+    @schema.migration(from_version=2)
+    def _v2_to_v3(s: dict) -> dict:
+        return {**s, "_migrated_v2": True}
+
+    result, changes = schema.apply({**_DEFAULTS, "_v": 1})
+    assert result["_migrated_v1"] is True
+    assert result["_migrated_v2"] is True
+    assert result["_v"] == 3
+    migration_changes = [c for c in changes if "migrated" in c]
+    assert len(migration_changes) == 2
+
+
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests for edge cases around `_v` handling and migration gaps in `ConfigSchema.apply`

The current tests only cover a linear 1→2→3 migration. Please also add coverage for:

- A section with `_v` greater than `schema.version` (forward/incompatible configs) to define whether `apply` leaves it unchanged and records no changes.
- A section with a non-int `_v` (e.g. string/float) to pin how `current_v` is interpreted (treated as pre-schema vs ignored).
- A section with `_v` < `schema.version` where a migration step is missing (e.g. `_v = 1`, `version = 3`, only `from_version=2` registered) to ensure we don’t silently produce a partially migrated config or inconsistent `_v`.

These tests will lock in the intended behavior for `_v` handling and guard against regressions in migration logic.

Suggested implementation:

```python
import copy
from pathlib import Path

import pytest
from easycord.config_schema import ConfigSchema

```

```python
_DEFAULTS: dict = {"enabled": True, "count": 0, "name": "default"}


def test_apply_ignores_forward_version_section() -> None:
    schema = ConfigSchema(key="test", version=2, defaults=_DEFAULTS)

    @schema.migration(from_version=1)
    def _v1_to_v2(s: dict) -> dict:
        return {**s, "extra": True}

    original = {**_DEFAULTS, "_v": 5, "marker": "future"}
    result, changes = schema.apply(copy.deepcopy(original))

    # Forward/incompatible configs should be left unchanged and produce no changes
    assert result == original
    assert changes == []


def test_apply_treats_non_int_version_as_unversioned() -> None:
    schema = ConfigSchema(key="test", version=2, defaults=_DEFAULTS)

    @schema.migration(from_version=1)
    def _v1_to_v2(s: dict) -> dict:
        return {**s, "extra": True}

    base_section = dict(_DEFAULTS)
    non_int_version_section = {**_DEFAULTS, "_v": "not-an-int"}

    result_base, changes_base = schema.apply(base_section)
    result_non_int, changes_non_int = schema.apply(non_int_version_section)

    # Non-int _v should behave the same as an unversioned section
    assert result_non_int == result_base
    assert changes_non_int == changes_base


def test_apply_raises_on_migration_gap() -> None:
    schema = ConfigSchema(key="test", version=3, defaults=_DEFAULTS)

    @schema.migration(from_version=2)
    def _v2_to_v3(s: dict) -> dict:
        return {**s, "extra": True}

    # Current version is 1, but there is no migration registered from 1 to 2
    section_v1 = {**_DEFAULTS, "_v": 1}

    with pytest.raises(ValueError):
        schema.apply(section_v1)

```
</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 easycord/cli.py Outdated
Comment thread easycord/cli.py
Comment on lines +545 to +556
if schema_issues:
_fixed = _apply_schema_fixes(schema_plugins) if fix_configs else 0
add(
"config.schema_health",
"Plugin config schema drift",
fix_configs and _fixed >= 0,
f"{len(schema_issues)} guild config(s) need healing"
if not fix_configs
else f"{_fixed} guild config(s) healed",
severity="warning",
fix="" if fix_configs else f"Run: easycord doctor --fix-configs {target}",
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): Success flag for schema heal always reports OK when --fix-configs is used

Because ok is computed as fix_configs and _fixed >= 0, it always evaluates to True whenever --fix-configs is used, even if no configs were actually healed. This can mask remaining schema drift after a supposed repair. Consider basing ok on the relationship between _fixed and schema_issues (e.g. (_fixed == 0 and not schema_issues) or (_fixed >= len(schema_issues))), or at least on schema_issues when fix_configs is true, so the result reflects the actual post-heal state.

Suggested change
if schema_issues:
_fixed = _apply_schema_fixes(schema_plugins) if fix_configs else 0
add(
"config.schema_health",
"Plugin config schema drift",
fix_configs and _fixed >= 0,
f"{len(schema_issues)} guild config(s) need healing"
if not fix_configs
else f"{_fixed} guild config(s) healed",
severity="warning",
fix="" if fix_configs else f"Run: easycord doctor --fix-configs {target}",
)
if schema_issues:
_fixed = _apply_schema_fixes(schema_plugins) if fix_configs else 0
_issue_count = len(schema_issues)
add(
"config.schema_health",
"Plugin config schema drift",
(_fixed >= _issue_count) if fix_configs else False,
f"{_issue_count} guild config(s) need healing"
if not fix_configs
else f"{_fixed} of {_issue_count} guild config(s) healed",
severity="warning",
fix="" if fix_configs else f"Run: easycord doctor --fix-configs {target}",
)

Comment thread easycord/config_schema.py
Comment on lines +90 to +96
current_v = result.get("_v")
if current_v is None:
current_v = 1 # pre-schema; treat as version 1
if self._version > 1:
changes.append("no _v stamp — treating as pre-schema v1")

if isinstance(current_v, int) and current_v < self._version:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue: Non-integer _v values skip migrations silently and remain inconsistent

When _v exists but isn’t an int, isinstance(current_v, int) causes all migrations to be skipped and leaves _v in a bad type, so future heals will also skip migrations. Consider treating non-int _v as invalid and either resetting it to a baseline (e.g. pre-schema v1) or overwriting it with self._version while recording that correction, so the version stamp and migration path stay consistent.

Comment on lines +92 to +101
def test_apply_runs_migration_chain() -> None:
schema = ConfigSchema(
key="test", version=3, defaults={**_DEFAULTS, "new_v3": "hi"}
)

@schema.migration(from_version=1)
def _v1_to_v2(s: dict) -> dict:
return {**s, "_migrated_v1": True}

@schema.migration(from_version=2)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Add tests for edge cases around _v handling and migration gaps in ConfigSchema.apply

The current tests only cover a linear 1→2→3 migration. Please also add coverage for:

  • A section with _v greater than schema.version (forward/incompatible configs) to define whether apply leaves it unchanged and records no changes.
  • A section with a non-int _v (e.g. string/float) to pin how current_v is interpreted (treated as pre-schema vs ignored).
  • A section with _v < schema.version where a migration step is missing (e.g. _v = 1, version = 3, only from_version=2 registered) to ensure we don’t silently produce a partially migrated config or inconsistent _v.

These tests will lock in the intended behavior for _v handling and guard against regressions in migration logic.

Suggested implementation:

import copy
from pathlib import Path

import pytest
from easycord.config_schema import ConfigSchema
_DEFAULTS: dict = {"enabled": True, "count": 0, "name": "default"}


def test_apply_ignores_forward_version_section() -> None:
    schema = ConfigSchema(key="test", version=2, defaults=_DEFAULTS)

    @schema.migration(from_version=1)
    def _v1_to_v2(s: dict) -> dict:
        return {**s, "extra": True}

    original = {**_DEFAULTS, "_v": 5, "marker": "future"}
    result, changes = schema.apply(copy.deepcopy(original))

    # Forward/incompatible configs should be left unchanged and produce no changes
    assert result == original
    assert changes == []


def test_apply_treats_non_int_version_as_unversioned() -> None:
    schema = ConfigSchema(key="test", version=2, defaults=_DEFAULTS)

    @schema.migration(from_version=1)
    def _v1_to_v2(s: dict) -> dict:
        return {**s, "extra": True}

    base_section = dict(_DEFAULTS)
    non_int_version_section = {**_DEFAULTS, "_v": "not-an-int"}

    result_base, changes_base = schema.apply(base_section)
    result_non_int, changes_non_int = schema.apply(non_int_version_section)

    # Non-int _v should behave the same as an unversioned section
    assert result_non_int == result_base
    assert changes_non_int == changes_base


def test_apply_raises_on_migration_gap() -> None:
    schema = ConfigSchema(key="test", version=3, defaults=_DEFAULTS)

    @schema.migration(from_version=2)
    def _v2_to_v3(s: dict) -> dict:
        return {**s, "extra": True}

    # Current version is 1, but there is no migration registered from 1 to 2
    section_v1 = {**_DEFAULTS, "_v": 1}

    with pytest.raises(ValueError):
        schema.apply(section_v1)

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request plugin tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants