Files
odysseus/tests/test_group_character_dropdown.py
T
Hinode a5b60a34ee fix: group selection drop-downs recreation and repopulation logic (#3424)
* fix: include in-memory templates in group participant character list

_getCharacterList() only fetched user templates from the /api/presets/templates
endpoint. When a character was just created in the Character tab, the async
auto-save to the templates API might not have completed by the time the Group
tab loaded its participant dropdown — causing newly created characters to be
missing.

Now also merges the in-memory userTemplates array from presets.js as a
fallback. These are updated as soon as the async save completes (via the
loadUserTemplates callback), so they bridge the gap between character creation
and API persistence.

Fixes #3207

* fix: optimistic userTemplates update on character save

Update the in-memory userTemplates array immediately when saveCustomPreset()
succeeds, before the fire-and-forget templates API POST completes. This
bridges the timing gap where _getCharacterList() calls getUserTemplates()
and gets stale data because loadUserTemplates() hasn't been triggered yet.

* test: verify group participant dropdown merges in-memory templates

Source-level guards for the #3207 fix:
- group.js imports and calls getUserTemplates() to merge in-memory templates
- presets.js exports getUserTemplates and does optimistic in-memory update on save

5 tests ensuring the fix can't be silently reverted.

* fix: generate client-side id for optimistic update, return shallow copy from getUserTemplates

1. New characters now get a 'user-<hex>' id immediately on save, matching
   the server's convention (uuid.uuid4().hex[:8]). Previously the id was ''
   which the merge guard in _getCharacterList filtered as falsy.

2. getUserTemplates() now returns [...userTemplates] so callers cannot
   accidentally mutate module state.

* fix(group.js): fix selection drop-downs behavior

- add an identifier to the selection drop-downs
  based on what type it is.
- fix behavior of continuously adding a row
  when a user clicks the "Group" tab button.
- fix behavior of not repopulating existing
  selection drop-downs whenever a user
  clicks the "Group" tab button.

* fix(#3207): remove duplicate of latest persona

- fix the duplication of the latest persona
  or character being shown in selection
  drop-downs.
- remove unnecessary blocks of code in
  `_getCharacterList()`
- add functionality to show error toast if saving
  a preset template/character fails.
- add functionality to revert optimistic update
  of preset template/character if saving fails.

* chore(group.js,preset.js): fix test & format errors

remove trailing whitespaces in lines 230 and 232
in /static/group.js

add back the expected syntax from
tests/test_group_character_dropdown.py

* fix(presets.js,group.js): fix runtime errors

as stated in a comment by @alteixeira20,
runtime errors exist for the applied fixes.

fixes:

- missing ending `]`
  querySelectorAll("select.preset-input[data-selection-type=character")
  in `group.js`
- spelling error in `modelSelection.vale` in `group.js`
- fix the ordering logic error in optimistic rollback where `Object.assign` is called first before the clone happens in `saveCustomPreset` in `presets.js`.
- add tests for the cloning logic bug with the same format as previous tests by checking the order of LOC in `tests/test_group_character_dropdown.py`.

---------

Co-authored-by: michaelxer <michaelxer@users.noreply.github.com>
Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com>
2026-06-26 13:35:25 +01:00

84 lines
3.5 KiB
Python

"""Issue #3207 — newly created characters missing from Group participant dropdown.
The fix has two parts:
1. group.js _getCharacterList() merges in-memory userTemplates from presets.js
as a fallback (covers the gap while the async templates API save is in-flight).
2. presets.js saveCustomPreset() does an optimistic in-memory update of
userTemplates immediately on success (bridges the timing race where
loadUserTemplates hasn't been triggered yet).
These tests assert the source patterns exist so they can't be silently removed.
"""
from pathlib import Path
GROUP_JS = Path("static/js/group.js").read_text(encoding="utf-8")
PRESETS_JS = Path("static/js/presets.js").read_text(encoding="utf-8")
# --- group.js: in-memory template merge in _getCharacterList ---
def test_group_imports_getUserTemplates():
"""group.js must import getUserTemplates from presets.js."""
assert "getUserTemplates" in GROUP_JS
assert "from './presets.js'" in GROUP_JS or 'from "./presets.js"' in GROUP_JS
def test_group_merges_in_memory_templates():
"""_getCharacterList must call getUserTemplates() and merge results."""
assert "getUserTemplates()" in GROUP_JS
# The merge loop should check for duplicates by id
assert "!chars.find(c => c.id === t.id)" in GROUP_JS
# --- presets.js: optimistic in-memory update on save ---
def test_presets_exports_getUserTemplates():
"""getUserTemplates must be exported from presets.js."""
assert "export function getUserTemplates()" in PRESETS_JS
def test_presets_optimistic_update_on_save():
"""saveCustomPreset must update userTemplates in-memory before the async POST."""
# Find the optimistic update block
assert "Optimistically update the in-memory templates list" in PRESETS_JS
# Must push to userTemplates for new entries
assert "userTemplates.push(_entry)" in PRESETS_JS
# Must Object.assign for existing entries
assert "Object.assign(_existing, _entry)" in PRESETS_JS
def test_presets_getUserTemplates_returns_array():
"""getUserTemplates should return a shallow copy of userTemplates."""
assert "return [...userTemplates]" in PRESETS_JS
def test_presets_optimistic_id_not_empty():
"""Optimistic update must generate a client-side id for new characters (not empty string)."""
# The id generation uses 'user-' prefix matching server's uuid convention
assert "user-' + Math.random" in PRESETS_JS
# Must NOT use empty string as fallback (that was the bug)
assert "(_existing && _existing.id) || ''" not in PRESETS_JS
def test_presets_clone_happens_before_mutation():
"""Rollback snapshot must be taken before Object.assign mutates _existing."""
clone_idx = PRESETS_JS.find("clone = JSON.parse(JSON.stringify(_existing))")
assign_idx = PRESETS_JS.find("Object.assign(_existing, _entry)")
assert clone_idx != -1
assert assign_idx != -1
assert clone_idx < assign_idx
def test_presets_rollbak_restores_from_clone():
"""Failed save must restore the original object from the pre-mutation clone."""
assert "if (clone)" in PRESETS_JS
assert "Object.assign(_existing, clone)" in PRESETS_JS
def test_presets_clone_is_deep_copy():
"""Rollback snapshot must be a deep clone, not an alias."""
assert "clone = JSON.parse(JSON.stringify(_existing))" in PRESETS_JS
def test_presets_no_alias_clone():
"""Prevent accidental rollback breakage via reference assignment."""
assert "clone = _existing" not in PRESETS_JS
assert "const clone = _existing" not in PRESETS_JS
assert "let clone = _existing" not in PRESETS_JS