Skip to content

feat(reconcile): validate preference values against the trait at plan time#1794

Merged
rohilsurana merged 3 commits into
mainfrom
fix/reconcile-preference-value-validation
Jul 23, 2026
Merged

feat(reconcile): validate preference values against the trait at plan time#1794
rohilsurana merged 3 commits into
mainfrom
fix/reconcile-preference-value-validation

Conversation

@rohilsurana

@rohilsurana rohilsurana commented Jul 23, 2026

Copy link
Copy Markdown
Member

What

Makes the Preference kind check a value against its trait during the plan, so a value the server would reject fails the plan up front instead of failing later at apply.

Why

Rule 2 of RFC 0001 says a plan never contains a change that cannot apply. A preference entry could set a value the server rejects: an empty value, or a value outside a checkbox or select trait's options. It passed validation and showed a set in the dry-run plan, then failed at apply. That was the one gap that kept Preference at Partial on the rules audit.

How

  • The diff now works from the full trait, not just its default. fetchTraits rebuilds each core/preference.Trait from the describe-preferences response (input type, hints, options, default), keyed by name.
  • A value is validated only when it becomes a set, that is, when it differs from the value already in effect. An invalid set fails the plan with a clear message. A value equal to the server's own value plans nothing and is left unchecked, so exporting a value stored under an older or looser trait definition still round-trips to zero ops (rule 5).
  • It also validates a trait default before planning a reset. If a trait would reject its own default, the default is not reachable, so the reset can never apply and the plan fails. That is the RFC's "a default must be reachable" clause.
  • A stored empty value counts as unset (already at its default), in both the reset step and export, so it is neither reset nor exported as an empty value the plan would reject.
  • It reuses the server's validators from core/preference, so the plan-time check matches the server's at the same version.

Testing

  • Test first throughout. Cases: an empty value, a value outside a trait's options, a reset to an unreachable default, the reconciler rejecting a bad value before applying, value validation over the wire (exercising the proto-to-core trait mapping, not just the text path), a value stored under a now-tighter trait round-tripping to zero ops, and stored-empty values being left alone by both the diff and export.
  • go build, go vet, go test ./internal/reconcile/... ./core/preference/..., gofmt, and golangci-lint all pass.

Known limitations

  • A trait that restricts scope (a non-empty allowed_scopes) is not expressible over the wire: the proto PreferenceTrait carries no allowed_scopes field, so the plan cannot pre-check a scope mismatch and that one case still surfaces at apply. Platform traits are global today, so this is a misconfiguration edge.
  • The plan-time and server-side checks share the core/preference validators, so they agree at the same version. A CLI built against a different server release could disagree if the validator rules changed between versions.

Status

Closes the last Rule 2 gap for the Preference kind on the rules-v2 audit, without weakening the Rule 5 export round trip. Targets main.

… time

A file entry could set a value the server rejects (an empty value, or a value outside a checkbox or select trait's options). It passed validation and showed a set in the plan, then failed at apply, breaking rule 2 (a plan never contains a change that cannot apply). The diff now rebuilds each trait from the describe-preferences response and runs its own validator over the desired value, and over a trait default before a reset, so an unappliable change fails the plan with a clear message. Reuses core/preference validators, so plan-time and server-side checks cannot drift.
@vercel

vercel Bot commented Jul 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
frontier Ready Ready Preview, Comment Jul 23, 2026 7:25am

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

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

Next review available in: 19 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fa78b83f-6e56-4f1c-8117-e5cb622c4af4

📥 Commits

Reviewing files that changed from the base of the PR and between d19cf3a and aec40aa.

📒 Files selected for processing (4)
  • internal/reconcile/preference.go
  • internal/reconcile/preference_reconciler.go
  • internal/reconcile/preference_reconciler_test.go
  • internal/reconcile/preference_test.go
📝 Walkthrough

Walkthrough

Preference reconciliation now loads complete trait definitions, validates requested values and reset defaults during planning, uses trait defaults for comparisons, and documents upfront validation behavior.

Changes

Preference trait validation

Layer / File(s) Summary
Fetch and reconstruct preference traits
internal/reconcile/preference_reconciler.go
PreferenceReconciler fetches full trait definitions, converts protobuf input metadata, and passes trait maps to reconciliation and export comparisons.
Validate desired values and reset defaults
internal/reconcile/preference.go
diffPreferences validates names and values, uses trait defaults, skips removed traits, and rejects invalid defaults during resets.
Reconcile validation coverage and documentation
internal/reconcile/preference_test.go, internal/reconcile/preference_reconciler_test.go, docs/content/docs/reconcile.mdx
Tests cover invalid values, invalid reset defaults, and no-apply failures; documentation describes plan-time validation.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • raystack/frontier#1762: Covers the same preference reconciliation paths and is refined here with trait-based validation.
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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.

@coveralls

coveralls commented Jul 23, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 29988651525

Warning

No base build found for commit c99f1f8 on main.
Coverage changes can't be calculated without a base build.
If a base build is processing, this comment will update automatically when it completes.

Coverage: 46.241%

Details

  • Patch coverage: 15 uncovered changes across 1 file (58 of 73 lines covered, 79.45%).

Uncovered Changes

File Changed Covered %
internal/reconcile/preference_reconciler.go 42 27 64.29%
Total (2 files) 73 58 79.45%

Coverage Regressions

Requires a base build to compare against. How to fix this →


Coverage Stats

Coverage Status
Relevant Lines: 38589
Covered Lines: 17844
Line Coverage: 46.24%
Coverage Strength: 14.1 hits per line

💛 - Coveralls

@rohilsurana
rohilsurana marked this pull request as ready for review July 23, 2026 07:18

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/reconcile/preference_reconciler.go (1)

134-190: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Trait-conversion branches largely untested — likely the source of Coveralls' 17 uncovered lines.

inputTypeFromPB's switch (Textarea/Select/Combobox/Multiselect/Number) and the InputOptions loop in traitFromPB aren't exercised by any test: platformTraits()/platformTraitPB in preference_reconciler_test.go only build checkbox-style traits with InputHints, so these branches are only implicitly covered by the default/empty-options path. A wrong mapping here would silently select the wrong validator without any test catching it.

Consider adding one or two trait fixtures (e.g. a select trait with InputOptions, a number/textarea trait) to platformTraits() or a dedicated test to exercise the remaining switch cases and the options loop.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6a9b2c03-f24e-4257-96ce-7b039c07e5a5

📥 Commits

Reviewing files that changed from the base of the PR and between c99f1f8 and d19cf3a.

📒 Files selected for processing (5)
  • docs/content/docs/reconcile.mdx
  • internal/reconcile/preference.go
  • internal/reconcile/preference_reconciler.go
  • internal/reconcile/preference_reconciler_test.go
  • internal/reconcile/preference_test.go

Comment thread internal/reconcile/preference_reconciler.go Outdated
The first cut validated every listed value, which broke the export round trip (rule 5): a value stored under an older or looser trait definition is exported verbatim, and re-reconciling it was rejected even though it equals the server's own value and plans no op. Validate only a value that differs from the one in effect, so a planned set is still appliable but a no-op set is left alone. Also treat a stored empty value as unset in both the reset loop and export, so it is not reset and not exported as an empty value the plan would reject. Adds tests for the stale-value round trip, the stored-empty cases, and value validation over the wire (exercising the proto-to-core trait mapping, not just the text path).
@rohilsurana
rohilsurana merged commit 9e063e3 into main Jul 23, 2026
8 checks passed
@rohilsurana
rohilsurana deleted the fix/reconcile-preference-value-validation branch July 23, 2026 07:48
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