Forward animation_handler split onto beta (#344 → beta)#367
Conversation
The 1674-line animation_handler.gd had four clearly-separated domains (write ops, presets, read introspection, value coercion) that the audit in #297 flagged as a deferred split (finding #13). With the reliability work and characterization tests in place, extract: - animation_presets.gd (480 LOC): preset_fade / slide / shake / pulse, the _resolve_preset_target classifier, and the _direction_offset static helper. - animation_values.gd (454 LOC): list_animations, get_animation, validate_animation, plus the shared keyframe value coercion (coerce_value_for_track / resolve_track_prop_context / coerce_for_type), transition parsing, enum-to-string helpers, and serialize_value. Adds a player_root_node helper that DRYs up the root-node fallback that was open-coded in three places. - animation_handler.gd shrinks to 869 LOC (under the 900-LOC cap from the issue) and keeps every public op as the dispatcher's registration target. preset_* and read methods are thin proxies into the submodules so plugin.gd dispatcher entries and test_animation.gd's _handler.method(...) call sites need no changes. The submodules hold a WeakRef back to the handler. The handler owns them strongly via _presets / _values; the WeakRef breaks the cycle so plugin teardown's _handlers.clear() actually decrefs to zero. Both files follow the const X := preload(...) + no-bare-class_name convention from CLAUDE.md. Validated locally: ruff clean, all 722 Python tests pass, script/ci-check-gdscript clean, and a SceneTree smoke confirms the handler wires up its submodules, the WeakRef back-pointers resolve, and the static helpers + proxy methods all behave as expected. https://claude.ai/code/session_011QxzADbf9zHwfZicjzdvCw Co-authored-by: Claude <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Pull request overview
This PR forwards the animation handler split (presets + read/value helpers extracted into submodules) onto beta, keeping the animation_handler public surface stable via thin proxy methods while aligning with the existing *_handler.gd + *_presets.gd + *_values.gd pattern used elsewhere in the plugin.
Changes:
- Extract animation preset logic into
animation_presets.gdand wire it intoanimation_handler.gdvia_presetsproxies. - Extract read-only animation ops + shared coercion/serialization helpers into
animation_values.gdand wire it via_valuesproxies / static helpers. - Update the handler to call the new
AnimationValues.*statics for transition parsing and value coercion.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| plugin/addons/godot_ai/handlers/animation_handler.gd | Instantiates _presets/_values, routes preset/read APIs through proxies, and switches coercion/transition helpers to AnimationValues. |
| plugin/addons/godot_ai/handlers/animation_presets.gd | New preset module containing preset_* implementations and _resolve_preset_target. |
| plugin/addons/godot_ai/handlers/animation_values.gd | New values/read module containing list/get/validate plus shared coercion and serialization helpers. |
| plugin/addons/godot_ai/handlers/animation_presets.gd.uid | New UID for the presets script. |
| plugin/addons/godot_ai/handlers/animation_values.gd.uid | New UID for the values script. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| for i in anim.get_track_count(): | ||
| var track_path_str := str(anim.track_get_path(i)) | ||
| var colon := track_path_str.rfind(":") |
There was a problem hiding this comment.
Genuine bug — confirmed by reading the code. Target:modulate:a (the shape preset_fade produces, plus any position:y etc. subpath track) does get the property+subpath glued onto the node part by rfind(":"), so validate_animation reports those tracks as broken even when the target node exists.
But this is pre-existing on main and beta (the original animation_handler.gd:604 already uses rfind(":")), and PR #367 is a faithful cherry-pick of #344 onto beta — fixing it here would mean a behavior change inside what's supposed to be a 1:1 forward of the merge that should have gone to beta in the first place.
I'll open a separate follow-up against beta with the one-character fix (rfind → find) plus a validate_animation test that creates a Target:modulate:a track and asserts valid_count > 0. Better diff/review story than smuggling it into the cherry-pick.
Generated by Claude Code
* Fix validate_animation subpath track parsing (rfind → find)
validate_animation was extracting the node portion of each track path with
rfind(":"), so a subpath track like "Target:modulate:a" (the shape every
preset_* produces, plus any "position:y" / "modulate:a" / etc. subpath
track) had its property+subpath glued onto the node part. The validator
then looked up "Target:modulate" against root_node, found nothing, and
reported the track as broken even when the target node was perfectly
healthy.
Split on the FIRST colon — the node↔property boundary — instead. The rest
of the string is property+subpath, which validate_animation doesn't need
to inspect (it's only checking that the target node exists).
Add a regression test that creates a Sprite2D sibling of the player,
attaches a "Target:modulate:a" track, and asserts validate_animation
reports valid_count == 1 / broken_count == 0.
Surfaced by Copilot review on #367. Filing here so the cherry-pick stays
clean and the bug fix carries its own test.
https://claude.ai/code/session_011QxzADbf9zHwfZicjzdvCw
* Narrow comment claim: only preset_fade produces subpath tracks
Per Copilot review on #371: of the four motion presets, only preset_fade
emits a subpath track ("Target:modulate:a"). preset_slide / preset_shake /
preset_pulse use plain ":position" or ":scale" paths.
Tighten the comments in animation_values.gd and test_animation.gd so
future readers debugging validator behavior aren't misled.
https://claude.ai/code/session_011QxzADbf9zHwfZicjzdvCw
* Empty commit to retrigger CI (Godot tests / macOS flake)
* Retrigger CI (third attempt; Godot tests / macOS keeps flaking)
* Replace regression test with broken_tracks.node_path shape assertion
Godot's `get_node_or_null` strips the `:property` suffix of a NodePath
natively — so the original `rfind(":")` and the new `find(":")` produce
the same valid/broken classification at runtime. The bug Copilot flagged
on #371 turns out to only be visible in the broken_tracks[].node_path
field for missing-target subpath tracks: rfind surfaces
"MissingTarget:modulate" while find correctly surfaces "MissingTarget".
The previous test asserted valid_count == 1 on a healthy subpath track,
which passed equally with both rfind and find — i.e. it didn't actually
prove the fix. Replace it with a missing-target test that asserts the
broken_tracks[0].node_path field, which is the real behavior change.
The new test also no longer needs a sibling Sprite2D (the missing target
intentionally doesn't exist), so the test is smaller and avoids any
macOS-specific test-runner sensitivity that surfaced on the prior shape.
Comment in animation_values.gd narrowed to match — the fix matters for
diagnostic output, not for the broken/valid classification.
https://claude.ai/code/session_011QxzADbf9zHwfZicjzdvCw
---------
Co-authored-by: Claude <noreply@anthropic.com>
51 commits forward from main, headlined by two architecture audits: audit-v1 (issue #297, PRs #298-#315): scene-path ancestry guard, update/config data-loss safeguards, lifecycle reliability, characterization tests, plugin.gd extraction (McpPortResolver + McpServerLifecycleManager), state-model cleanup, McpUpdateManager extraction, Runtime Protocol deletion + DirectRuntime retype, narrowed meta-tool JSON coercion, self-update preload-alias hardening, locked FastMCP middleware order. audit-v2 (issue #343, PRs #369-#390): origin allowlist (DNS-rebinding guard, #345/#375), path-traversal guards on script_* / filesystem_* writes (#347/#369), errno.EADDRINUSE portability across all OSes (#348/#373), SessionRegistry RLock removal (#350/#370), Pydantic-validated WebSocket event payloads (#351/#378), sole-survivor auto-failover on active disconnect (#352/#379), 30s filesystem_changed watchdog during update reload (#353/#381), FAILED_MIXED self-update visibility via mixed_state in editor_state (#354/#382), 32/tick inbound packet-drain cap with spillover logging (#356/#383), error-code vocabulary enrichment (#365/#385: NODE_NOT_FOUND, PROPERTY_NOT_ON_CLASS, VALUE_OUT_OF_RANGE, MISSING_REQUIRED_PARAM cut INVALID_PARAMS sites 471 -> 97), resolve-or-error helper extraction (#364/#389: 38+ duplicate sites migrated), resource-form lint for meta-tool reads (#363/#386), LogViewer + PortPickerPanel extraction from mcp_dock.gd (#360/#390), LogViewer.tick() buffer-clear recovery (#392). Conflict resolutions (both intentional, both took beta's side): - plugin/addons/godot_ai/animation_handler.gd plus new submodules animation_presets.gd and animation_values.gd: beta's 4-domain split retained. Main's revert (#368) was CI-flake hygiene, not a structural rejection per its own commit message ("Empty commit to retrigger CI (flake on Godot tests / macOS)"). Beta's #367 is the canonical state the audit found wanted. - plugin/addons/godot_ai/clients/_toml_strategy.gd: beta's version retained. Beta handles inline comments after section headers (`[next_section] # note`) via _is_any_section_header(); main's later re-derived bare-key fix uses the simpler bracket check and would regress on commented headers. The four main-only Windows / TOML hotfixes (#302, #318, #319, #320) all landed on beta independently under different commit hashes and are content-equivalent or beta-improved; no cherry-pick was needed before this merge. Validation: - CI: green on beta tip 72b35d7 (and on PR #392 separately) across ubuntu-latest / macos-latest / windows-latest for Python tests, Godot tests, release-smoke, and game-capture-smoke. - Operator smoke (macOS, 2026-05-06): all 10 phases of the audit-v1+v2 post-landing runbook green. Report on issue #343. 47 GDScript suites + 903 Python tests passing. Phase 7 interactive self-update verified end-to-end with the local-self-update-smoke harness; plugin advanced 2.3.2 -> 2.3.3, no .ips, no _exit_tree leak, server stop/start clean. - Operator smoke (Windows 11, beta tip d5aa29f, 2026-05-06): Phase 7 self-update green (7/7; macOS-only .ips check correctly downgraded to SKIP). Path-traversal guard rejects backslash variant. Cursor client configure cycle round-trips cleanly. editor_state ping 109ms. Pre-existing pytest UnicodeDecodeError on Windows tracked separately as #397 (also on main, not introduced by this merge). # Conflicts: # test_project/tests/test_clients.gd
Summary
Forwards the animation_handler split from PR #344 (which I incorrectly merged to
maininstead ofbeta) ontobeta, so the audit-stack history stays consistent with the umbrella's branching policy in #297. The maintainer-ownedbeta → mainpromotion will see the same patch on both sides.Cherry-pick of
d915c4d(#344) ontobeta, with a one-file conflict resolved against #337's "scene-absolutetarget_path" changes:_resolve_preset_target(now inanimation_presets.gd) accepts both scene-absolute (/Main/Foo) and root_node-relative (World/Foo)target_pathshapes, returns{node, kind, track_path_root}. Eachpreset_*consumestrack_path_root(not the rawtarget_path) when building its track string._player_root_nodehelper Accept scene-absolute target_path in animation presets (closes #328) #337 introduced is nowAnimationValues.player_root_node(same logic, same call sites:validate_animation,resolve_track_prop_context,_resolve_preset_target).Why this exists separately
I merged #344 to
maindirectly. Per the umbrella issue's branching policy ("All audit follow-up PRs should targetbetaby default, notmain"), it should have flowed throughbeta. This PR keepsbetaconsistent so the eventualbeta → mainpromotion isn't the first timebetasees the split. The patch on both branches is functionally identical — git will collapse them at promotion time.Validated locally
script/ci-check-gdscript— cleanruff check src/ tests/— cleanpytest -q— 767 passed (vs 722 on main; beta has more tests including the 6test_preset_*_accepts_scene_absolute_targetcases Accept scene-absolute target_path in animation presets (closes #328) #337 added)AnimationValuesround-trips,AnimationPresets._direction_offsetreturns the right axis convention for 2D and 3D, all 7 proxy methods exist on the handlerTest plan
test_run suite=animationvia MCP against a live editor — full pass, including the scene-absolute-target tests added in Accept scene-absolute target_path in animation presets (closes #328) #337Refs #297 (audit umbrella, finding #13) — #342 (split tracker) — #344 (the version that landed on
main).https://claude.ai/code/session_011QxzADbf9zHwfZicjzdvCw
Generated by Claude Code