Skip to content

test: put back the model class state four specs change - #4365

Merged
bpamiri merged 2 commits into
developfrom
peter/forms-spec-tag-validations
Oct 5, 2026
Merged

bpamiri merged 2 commits into
developfrom
peter/forms-spec-tag-validations

Conversation

@bpamiri

@bpamiri bpamiri commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Test-only. Four specs register a validation or callback on a shared model class, or clear its callbacks, without putting the class back. Model classes are shared for the whole test run, so the change follows every later spec that uses that model.

Spec What it left behind
view/formsSpec.cfc ("Tests that label") tag.validatesPresenceOf(properties = "name" / "virtual") → every later tag save needed both
model/hardener/ModelHardenerM2M8Spec.cfc (M2) newPost.validatesUniquenessOf(properties = "title", includeSoftDeletes = true) → a second uniqueness validation on every later post save
model/transactionsSpec.cfc ("rollback when error raised") tagModel.afterSave(methods = "crashMe") → every later tagWithDataCallbacks save ran crashMe
model/ModelSurfaceCoverageSpec.cfc (callback registration helpers) m.$clearCallbacks() with no type wiped every post callback. It's harmless today, because the registrars there are called without methods (they register nothing) and post's config() declares no callbacks, but it would silently drop any callback post gains.

How it was found. A spec in #4357 created a tag and expected an INSERT. It passed when its directory ran alone, but in a full run the create sent no INSERT: the tag class carried the formsSpec presence validations, and the create failed with Virtual property can't be empty.

Fix (same shape in each spec)

  • Snapshot the class's validations or callbacks with Duplicate() before the spec changes them.
  • Put them back in a catch-free finally.
  • Record the class's state when the bundle starts. A new check at the end confirms the class ends the bundle as it started.
  • The checks compare SerializeJSON($canonicalCacheValue(...)), which sorts struct keys at every depth, so struct key order can't make them flaky.

Tests

RED from a clean develop worktree, Lucee 7. Each spec was copied in with only its new check (the restore line removed):

  • formsSpec: keeps only the validations it was configured with: expected onSave: [], received the presence validations.
  • ModelHardenerM2M8Spec: leaves the post class with only the validations it was configured with: received an extra $validatesUniquenessOf.
  • transactionsSpec: leaves the tagWithDataCallbacks class with only the callbacks it was configured with: received crashMe in afterSave.
  • ModelSurfaceCoverageSpec: passes on develop too, for the reason in the table. That change is defensive.

GREEN, full core suite on SQLite, 546 bundles, 0 fail / 0 error:

Engine Pass
Lucee 7 6860
BoxLang 6860

Adobe and RustCFML are covered by the matrix (the change is test-only).

🤖 Generated with Claude Code

The label specs call validatesPresenceOf() on a tag object, which registers
on the shared tag class, so every later tag save in the run had to pass
presence checks on name and virtual. A full-suite run's tag create was
rejected for an empty virtual property and sent no INSERT. The label specs
now restore the class's validations after each spec, and a check confirms
the tag class ends the bundle with the validations it started with.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>
@bpamiri bpamiri added this to the v4.2.0 milestone Oct 5, 2026
@bpamiri

bpamiri commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

rev1-r3 review @ 6ac6ad9: code APPROVE (final sign-off after orch1's matrix run, or PR CI if orch1 treats this test-only change as PR-CI-gated)

  • Restoring validations is complete. validatesPresenceOf() → $registerValidation() only ever does ArrayAppend(variables.wheels.class.validations[when], …). It mutates no other class data, so restoring the class's validations struct alone undoes it completely. The beforeEach snapshot is a Duplicate, and the afterEach assigns a fresh Duplicate back through $classData(), which returns the class struct by reference on every engine. So each label spec starts from the configured validations and ends there.
  • The pin. beforeAll records the configured validations once, and the new "keeps only the validations it was configured with" case, declared after the label block, compares against it. RED on develop with exactly the leaked onSave presence check, green with the restore.

Not blocking, one robustness point: the pin compares SerializeJSON strings of a struct (onCreate / onSave / onUpdate → arrays of structs). On Lucee, struct iteration order is hash-bucket order rather than insertion order, and a Duplicated struct isn't guaranteed to serialize its keys in the same order as the original. It's green on full Lucee 7 and BoxLang runs today. If it ever flakes, compare per key (ArrayLen of each when array, or the validation properties lists) instead of the whole JSON string.

The three other shared-class leaks listed in the body (ModelHardenerM2M8Spec, ModelSurfaceCoverageSpec's untyped $clearCallbacks(), transactionsSpec's afterSave(crashMe)) are worth one follow-up PR of the same shape.

Three model specs register a validation or callback on a shared model class
(or clear all its callbacks) and never put it back:

- ModelHardenerM2M8Spec adds a uniqueness validation to post.
- transactionsSpec adds an afterSave crashMe callback to tagWithDataCallbacks.
- ModelSurfaceCoverageSpec cleared every post callback with $clearCallbacks().

Each now snapshots the class struct, restores it in a catch-free finally,
and checks the class ends the bundle as it started. The checks, including
the formsSpec one, compare a canonical form with struct keys sorted, so
struct key order can't make them flaky.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>
@bpamiri bpamiri changed the title test(view): put back the tag validations the label specs register test: put back the model class state four specs change Oct 5, 2026
@bpamiri

bpamiri commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

rev1-r3 re-review @ b79e874: code APPROVE (final sign-off after matrix run 37344929902 and PR CI)

Four specs now put back the shared model-class state they change, each with a no-leftover check compared through $canonicalCacheValue(), so key order can't make the check flaky.

  • Restoring one struct is enough in each case. Each spec saves and restores exactly the struct its registration call writes to, and those calls write nowhere else:
    • validatesPresenceOf and validatesUniquenessOf go through $registerValidation(), which only appends to class.validations[when];
    • afterSave and the other callback registrars go through $registerCallback(), which only creates or appends class.callbacks[type].
  • formsSpec (tag validations). beforeEach takes a Duplicate of the validations and afterEach puts it back, as reviewed at 6ac6ad9b3, with the pin now canonicalised.
  • ModelHardenerM2M8Spec (post uniqueness validation). It snapshots, then runs the original transaction/rollback body inside try/finally. So a failing expect inside the transaction still restores, and the validations come back after the rollback.
  • transactionsSpec (afterSave(crashMe) on tagWithDataCallbacks). The registration and the intentionally failing save() sit in an outer try/finally that restores callbacks. The inner try/catch still records results for the rollback assertion.
  • ModelSurfaceCoverageSpec (post callbacks). This replaces the untyped $clearCallbacks(), which would also wipe callbacks the class was configured with, with an exact restore. It's defensive today, since post configures none, as the body says, and its pin guards against that changing.
  • The pins. Each is declared after the spec that changes state and compares against a snapshot taken when that bundle starts. RED on develop with the restore removed: 3 of 4 fail; the defensive one is the expected exception.

These are test-harness files under /tests/, which TryCatchFinallyGuardSpec excludes, so the nested try/catch inside try/finally is fine there.

@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Wheels Test Results

224 116 tests   218 995 ✅  1h 27m 24s ⏱️
 17 472 suites    5 121 💤
     32 files          0 ❌

Results for commit b79e874.

@bpamiri

bpamiri commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

rev1-r3 review @ b79e874: APPROVE (final; matrix run 37344929902 read leg by leg, PR CI checked)

  • Matrix run 37344929902 is on this head, and all 32 legs plus rustcfml succeeded. From the junit artifact for each leg:
    • 6998–7015 tests per leg, with 0 failures and 0 errors on every leg.
    • All four changed specs pass on every leg, none skipped, including all six Adobe 2023 and all six Adobe 2025 legs and BoxLang: formsSpec 125, transactionsSpec 26, ModelHardenerM2M8Spec 15 and ModelSurfaceCoverageSpec 13, each including its new no-leftover check.
  • PR CI on this head: 18 pass, 3 skipped, 0 failing or pending.

@bpamiri
bpamiri merged commit 8d6d799 into develop Oct 5, 2026
62 checks passed
@bpamiri
bpamiri deleted the peter/forms-spec-tag-validations branch October 5, 2026 18:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant