Skip to content

Forward animation_handler split onto beta (#344 → beta)#367

Merged
dsarno merged 1 commit into
betafrom
claude/animation-handler-split-beta-NhT4x
May 5, 2026
Merged

Forward animation_handler split onto beta (#344 → beta)#367
dsarno merged 1 commit into
betafrom
claude/animation-handler-split-beta-NhT4x

Conversation

@dsarno

@dsarno dsarno commented May 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Forwards the animation_handler split from PR #344 (which I incorrectly merged to main instead of beta) onto beta, so the audit-stack history stays consistent with the umbrella's branching policy in #297. The maintainer-owned beta → main promotion will see the same patch on both sides.

Cherry-pick of d915c4d (#344) onto beta, with a one-file conflict resolved against #337's "scene-absolute target_path" changes:

  • _resolve_preset_target (now in animation_presets.gd) accepts both scene-absolute (/Main/Foo) and root_node-relative (World/Foo) target_path shapes, returns {node, kind, track_path_root}. Each preset_* consumes track_path_root (not the raw target_path) when building its track string.
  • The _player_root_node helper Accept scene-absolute target_path in animation presets (closes #328) #337 introduced is now AnimationValues.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 main directly. Per the umbrella issue's branching policy ("All audit follow-up PRs should target beta by default, not main"), it should have flowed through beta. This PR keeps beta consistent so the eventual beta → main promotion isn't the first time beta sees the split. The patch on both branches is functionally identical — git will collapse them at promotion time.

Validated locally

  • script/ci-check-gdscript — clean
  • ruff check src/ tests/ — clean
  • pytest -q767 passed (vs 722 on main; beta has more tests including the 6 test_preset_*_accepts_scene_absolute_target cases Accept scene-absolute target_path in animation presets (closes #328) #337 added)
  • SceneTree smoke — confirms handler instantiates its submodules, WeakRef back-pointers resolve, every static helper in AnimationValues round-trips, AnimationPresets._direction_offset returns the right axis convention for 2D and 3D, all 7 proxy methods exist on the handler

Test plan

Refs #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

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>
Copilot AI review requested due to automatic review settings May 5, 2026 14:57
@codecov

codecov Bot commented May 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copilot AI 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.

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.gd and wire it into animation_handler.gd via _presets proxies.
  • Extract read-only animation ops + shared coercion/serialization helpers into animation_values.gd and wire it via _values proxies / 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(":")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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 (rfindfind) 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

@dsarno dsarno merged commit aac5483 into beta May 5, 2026
15 checks passed
dsarno added a commit that referenced this pull request May 5, 2026
* 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>
dsarno added a commit that referenced this pull request May 6, 2026
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
@dsarno dsarno deleted the claude/animation-handler-split-beta-NhT4x branch May 7, 2026 14:53
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