Repository navigation
fix(model): struct-form enum() validates the stored value, matching scopes and is*() checkers - #3020
Conversation
…copes and is*() checkers
When `enum(property="priority", values={low: 0, medium: 1, high: 2})` is
declared, the auto-registered `validatesInclusionOf` previously checked
the column against the NAME keys ("low,medium,high") while the
auto-generated scopes and `is<Name>()` checkers compared against the
STORED values (0/1/2). No row value could satisfy both sides — saving
the stored value tripped validation; saving the name skipped every
scope/checker. Derive the inclusion list from the stored values so the
three sides agree. The list-form path is unaffected because names ==
values there.
Refs #3014
Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Wheels Bot — No doc updatesReviewed this PR's diff and found no docs that need updating (bug fix restoring behavior the documentation already described — the MDX enum guide already promised that |
There was a problem hiding this comment.
Wheels Bot — Reviewer A
TL;DR: The logic fix is correct — building the inclusion list from stored values rather than name keys is exactly right, and the new spec catches the regression from the other direction. One meaningful test gap needs to be closed before merge: the spec never asserts that the enum name string (e.g. "low") is now rejected for the struct form, which was the specific invariant this PR inverts. There is also a multi-line comment block that violates the project style rule. Both should be addressed before marking ready for review.
Tests
Missing assertion: struct-form enum NAME is now rejected
vendor/wheels/tests/specs/model/structEnumValidationSpec.cfc, lines 33–40
it("rejects values that are neither names nor stored values", () => {
var m = g.model("author")
m.enum(property="lastName", values={low: 0, medium: 1, high: 2})
var instance = m.new(firstName="Test")
instance.lastName = "bogus"
expect(instance.valid()).toBeFalse()
})This test only verifies that an arbitrary unrelated string ("bogus") fails validation. It does not verify that the enum NAME itself ("low") is now rejected — yet that is literally the behavior the fix inverts. Before the fix, validatesInclusionOf accepted "low" (from StructKeyList) and rejected 0. After the fix it must accept 0 and reject "low". The "accepts 0" half is covered by tests 1, 3, and 4. The "rejects "low"" half is nowhere in the suite.
Without this assertion, a future regression to the old code path would pass all five tests while silently re-introducing the bug.
Suggested addition:
it("rejects the enum NAME string (struct form) — stored value is what matters", () => {
var m = g.model("author")
m.enum(property="lastName", values={low: 0, medium: 1, high: 2})
var instance = m.new(firstName="Test")
instance.lastName = "low" // the name key, not the stored value
expect(instance.valid()).toBeFalse()
})Conventions
Multi-line comment block in production code
vendor/wheels/model/properties.cfc, lines 919–926
// Auto-register inclusion validation for this property.
// The list must derive from the STORED VALUES (not the name keys), so that
// rows hold values the auto-generated scope filters on and the is<Name>()
// checkers compare against. For the list form, names == values so the
// behaviour is unchanged. For the struct form `enum(property="priority",
// values={low: 0, medium: 1, high: 2})`, validation now accepts 0/1/2 —
// the same values the scope WHERE clause and isLow()/isMedium()/isHigh()
// already reference. See issue ##3014.CLAUDE.md §Tone: "Never write multi-paragraph docstrings or multi-line comment blocks — one short line max." Collapse to one line, e.g.:
// Build inclusion from stored values (not name keys) so valid() agrees with scopes and is*() — ##3014.The same rule applies to the multi-line inline comment blocks in structEnumValidationSpec.cfc (lines 17–21, 42–45, 55–60, 69–71). They explain what each test case does rather than why a non-obvious constraint exists, so they can simply be removed.
Correctness (no blocking findings)
The core logic is correct. ListToArray(local.enumDef.names) iterates the names returned by StructKeyList(), and local.enumDef.values[local.enumName] resolves the stored value via a case-insensitive struct lookup — so the inclusion list is built correctly regardless of whether Adobe CF uppercases struct keys vs. Lucee preserving case. ListAppend converts integer stored values (e.g. 0) to "0" as expected, and validatesInclusionOf does a loose comparison, so numeric stored values round-trip cleanly. The list form is unaffected because names == values there.
Cross-engine (no blocking findings)
The new for (local.enumName in ListToArray(...)) loop is standard CFML that runs identically on Lucee 5/6/7, Adobe CF 2018–2025, and BoxLang. No member-function collision, no closure scope concern, no bracket-notation call. The struct-literal argument in the spec (values={low: 0, medium: 1, high: 2}) is supported on all targeted engines. The spec follows the established g = application.wo pattern (assignment to variables scope, not local) so closure access to g inside it() and beforeEach()/afterEach() bodies is correct.
Commits
fix(model): struct-form enum() validates the stored value, matching scopes and is*() checkers — 95 chars, valid type, valid scope, not ALL-CAPS. DCO sign-off box ticked. ✓
Docs
Changelog fragment changelog.d/struct-enum-validation-mismatch.fixed.md is present and correctly uses the .fixed.md extension. CLAUDE.md and AI reference doc updates are deferred to the docs bot. ✓
Wheels Bot — Reviewer B (round 1)A's review is accurate and well-reasoned. Both findings check out against the actual diff: the missing name-rejection assertion is a real test gap, and the multi-line comment blocks genuinely violate CLAUDE.md. Converging on changes-needed. SycophancyNone detected. A required concrete changes before marking ready for review and gave no unearned approval. False positivesNone detected.
Missed issuesNone detected. I re-scanned the diff for:
Verdict alignmentA's COMMENTED verdict with an explicit "both should be addressed before marking ready for review" is consistent with the two actionable findings. ConvergenceAligned. A identified a real test gap (no assertion that the enum name string |
- Add spec assertion that struct-form enum NAME ("low") is rejected by
valid() — closes the regression-detection gap A and B both flagged
(vendor/wheels/tests/specs/model/structEnumValidationSpec.cfc).
- Collapse the 8-line comment block in vendor/wheels/model/properties.cfc
(lines 919–926) to one line per CLAUDE.md §Tone.
- Strip the explanatory multi-line inline comment blocks from the
struct-enum spec (lines 17–21, 42–45, 55–60, 69–71) per the same rule.
Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Wheels Bot — Address Review (round 1)Applied consensus findings from Reviewer A and Reviewer B's convergence (round 1):
Test run note: local sandbox has no The new commit will trigger a fresh Reviewer A run on the updated SHA. Convergence loop continues until reviewers align on |
There was a problem hiding this comment.
Wheels Bot — Reviewer A
TL;DR: This is the round-2 push addressing the two consensus findings from the first-round review. Both are fully resolved: the missing "rejects enum NAME string" assertion is now present as test 2 (instance.lastName = "low" → toBeFalse()), and all multi-line inline comment blocks have been removed from the spec and the implementation comment in properties.cfc is correctly single-line. The fix logic and all six test cases are correct. The only remaining nit is the fixup commit's subject line, which references the review process rather than the code change — a CLAUDE.md style note, and moot for a squash-merge workflow where the PR title is the landing commit. Verdict: comment.
Conventions
Commit 2 subject references the review process, not the code change
c816da647 fix(model): address Reviewer A/B consensus findings (round 1)
CLAUDE.md § "Commit Message Conventions": "Do not reference the current task, fix, or callers … since those belong in the PR description and rot as the codebase evolves." A subject like add name-rejection test and collapse multi-line inline comments would describe what actually changed in that commit. This is moot for the landing squash-merge commit (the PR title fix(model): struct-form enum() validates the stored value, matching scopes and is*() checkers is correct and clean), but worth noting for branch-history hygiene.
Correctness (no findings)
The core change in vendor/wheels/model/properties.cfc lines 920–922 correctly iterates local.enumDef.names (set from StructKeyList(arguments.values) at line 907) and resolves each stored value via a case-insensitive struct lookup local.enumDef.values[local.enumName]. ListAppend coerces integer stored values (e.g. 0) to "0", which round-trips cleanly through validatesInclusionOf's loose CFML string comparison. The list-form code path is unaffected (names == values there).
Cross-engine (no findings)
for (local.enumName in ListToArray(local.enumDef.names)) is standard CFML with no member-function collision, no closure-scope concern, and no bracket-notation parser crash. The struct lookup local.enumDef.values[local.enumName] is case-insensitive on all targeted engines, so the fix is correct regardless of whether Adobe CF uppercases struct literal keys or Lucee/BoxLang preserves them.
Tests (no findings)
All six test cases are present and correctly scoped:
- Stored value
0accepted byvalid()witherrorsOn("lastName")confirming zero errors - Enum name string
"low"rejected — the previously missing assertion, now present - Unrelated string
"bogus"rejected - The passing value is exactly the value
isLow()returnstruefor - The passing value matches
scopes.low.whereParams[1].value(scope-alignment proof) - List-form regression guard (
names == valuespath still correct)
g = application.wo is a bare (unqualified) assignment inside function run(), so it lands in the CFC's variables scope — not local — and is correctly visible inside all it() / beforeEach() closures. The $clearModelInitializationCache() guard in beforeEach/afterEach prevents enum registrations from leaking across tests.
Docs (no findings)
changelog.d/struct-enum-validation-mismatch.fixed.md present with the correct .fixed.md extension. No direct CHANGELOG.md edit was made.
Wheels Bot — Reviewer B (round 1)A's review is accurate on substance: the fix is correct, the tests exercise what they claim, and the cross-engine reasoning holds. One verdict inconsistency worth noting — A called its only finding explicitly moot, then chose SycophancyNone detected. Every positive claim A made is backed by specific code lines or reasoning (the False positivesNone detected.
Missed issues
// Build inclusion from stored values (not name keys) so valid() agrees with scopes and is*() — ##3014.Inside a Verdict alignmentA found zero actionable issues and explicitly called its one nit "moot for a squash-merge workflow," yet chose ConvergenceAligned. A's analysis of the fix, cross-engine safety, and test coverage is accurate. The |
Summary
For the struct form of
enum()— e.g.enum(property="priority", values={low: 0, medium: 1, high: 2})— the auto-registered inclusion validation was checking the property against the NAME keys ("low,medium,high") while the auto-generated scopes andis<Name>()checkers compared against the STORED values (0/1/2). No value a row could hold satisfied both sides: saving the stored value tripped validation, saving the name skipped every scope and checker. This was masked in the suite because the only enum fixture (PostWithEnum.cfc) used the list form, where names and values are identical.The fix builds the inclusion list from the stored values (iterating
local.enumDef.valuesby name), so validation now accepts the same values the scopes filter on and theis*()checkers compare against. The list form path is unaffected becausenames == valuesthere.Related Issue
Refs #3014
Type of Change
Feature Completeness Checklist
Signed-off-by:(usegit commit -s); see CONTRIBUTING.mdvendor/wheels/tests/specs/model/structEnumValidationSpec.cfccovers the struct form across validation, scope, andis<Name>()agreement; includes a list-form regression guard. Failed pre-fix (3 of 5 expectations), passes post-fix.changelog.d/struct-enum-validation-mismatch.fixed.mdwheels.tests.specs.modeldirectory: 888 pass / 0 fail / 0 error post-fix (vs. 885 pass / 3 fail / 0 error pre-fix); same 175 suites.Test Plan
bash tools/test-local.sh model— confirm 888/0/0.vendor/wheels/tests/specs/model/structEnumValidationSpec.cfc— five expectations covering:valid()accepts the stored value (0)valid()rejects neither-name-nor-value ("bogus")is<Name>()returns true forvendor/wheels/tests/specs/model/enumSpec.cfc) still green.Screenshots / Output
n/a