The action was registered + the block declared, but the automation
builder's per-action config renderer didn't have a case for
`personalized_pipeline` so users only saw the bare card with the
generic delay-minutes input — no way to select which playlists to
sync. This commit adds the multi-select picker.
Backend:
- `core/personalized/api.list_kinds(manager=...)` now optionally
takes a manager and includes the resolved variant list per kind
(calls each spec's variant_resolver(deps) when present). Singleton
kinds get an empty `variants` list. Variant-bearing kinds
(time_machine / genre_playlist / daily_mix / seasonal_mix) get
their full enumerated set.
- `web_server.py` `/api/personalized/kinds` route now passes a built
manager so the variants list lands in the response.
Frontend:
- `webui/static/stats-automations.js` `_renderBlockConfigFields`
gains a `personalized_pipeline` branch that renders a scrollable
multi-select picker:
- Singletons (Hidden Gems, Discovery Shuffle, Popular Picks,
Fresh Tape, The Archives) = one checkbox row per kind
- Variant kinds = a section header + one checkbox row per variant
(e.g. Time Machine: 1960s/1970s/.../2020s; Seasonal: halloween/
christmas/valentines/summer/spring/autumn)
- Pre-checks rows that match the existing `kinds` config on edit
- New `_autoLoadPersonalizedKinds(slotKey)` fetches `/api/personalized/kinds`
(cached after first load), renders the picker DOM, and pre-checks
saved selections via `data-kind` / `data-variant` attributes on
the checkboxes.
- `_renderBuilderCanvas` calls the loader for any `cfg-*-kinds-picker`
it finds in the freshly-rendered slots.
- The save-time `_collectActionConfig` walks the picker's checked
inputs (matched by `data-kind` attribute) and emits
`{kinds: [{kind, variant?}, ...], refresh_first, skip_wishlist}`
in the same shape the handler expects.
Tests:
- `tests/automation/test_automation_blocks.py::_FIELD_TYPES` adds
'personalized_playlist_select' so the block-shape regression test
accepts the new field type. (Test was failing because it whitelists
every field type used across all blocks.)
- 189 automation + personalized API tests pass; full suite intact.
83 lines
2.8 KiB
Python
83 lines
2.8 KiB
Python
"""Tests for core/automation/blocks.py — static block definitions for the builder UI.
|
|
|
|
Catches accidental schema regressions in the builder block list (missing
|
|
`type`/`label`, malformed config_fields options, etc.).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from core.automation import blocks
|
|
|
|
|
|
def _shape_check(items, allowed_types):
|
|
"""Every item has type+label+description, plus type-specific shape rules."""
|
|
seen_types = set()
|
|
for item in items:
|
|
assert 'type' in item, item
|
|
assert 'label' in item, item
|
|
assert isinstance(item.get('available'), bool), item
|
|
# No duplicate types within a list
|
|
assert item['type'] not in seen_types, f"Duplicate type {item['type']!r}"
|
|
seen_types.add(item['type'])
|
|
|
|
if 'config_fields' in item:
|
|
for field in item['config_fields']:
|
|
assert 'key' in field
|
|
assert 'type' in field
|
|
assert field['type'] in allowed_types, f"Unknown field type {field['type']!r} in {item['type']}"
|
|
if field['type'] == 'select':
|
|
assert 'options' in field
|
|
for opt in field['options']:
|
|
assert 'value' in opt
|
|
assert 'label' in opt
|
|
|
|
|
|
_FIELD_TYPES = {
|
|
'number', 'select', 'time', 'multi_select', 'checkbox', 'text',
|
|
'mirrored_playlist_select', 'personalized_playlist_select',
|
|
'signal_input', 'script_select',
|
|
}
|
|
|
|
|
|
def test_triggers_shape():
|
|
_shape_check(blocks.TRIGGERS, _FIELD_TYPES)
|
|
|
|
|
|
def test_actions_shape():
|
|
_shape_check(blocks.ACTIONS, _FIELD_TYPES)
|
|
|
|
|
|
def test_notifications_shape():
|
|
_shape_check(blocks.NOTIFICATIONS, _FIELD_TYPES)
|
|
|
|
|
|
def test_signal_received_trigger_present():
|
|
types = {t['type'] for t in blocks.TRIGGERS}
|
|
assert 'signal_received' in types
|
|
|
|
|
|
def test_fire_signal_notification_present():
|
|
types = {n['type'] for n in blocks.NOTIFICATIONS}
|
|
assert 'fire_signal' in types
|
|
|
|
|
|
def test_run_script_in_both_actions_and_notifications():
|
|
"""run_script can be either an action or a then-action — both lists own it."""
|
|
action_types = {a['type'] for a in blocks.ACTIONS}
|
|
notif_types = {n['type'] for n in blocks.NOTIFICATIONS}
|
|
assert 'run_script' in action_types
|
|
assert 'run_script' in notif_types
|
|
|
|
|
|
def test_schedule_trigger_default_unit_is_hours():
|
|
schedule = next(t for t in blocks.TRIGGERS if t['type'] == 'schedule')
|
|
unit_field = next(f for f in schedule['config_fields'] if f['key'] == 'unit')
|
|
assert unit_field['default'] == 'hours'
|
|
|
|
|
|
def test_event_triggers_with_conditions_have_condition_fields():
|
|
for t in blocks.TRIGGERS:
|
|
if t.get('has_conditions'):
|
|
assert 'condition_fields' in t, f"{t['type']} marked has_conditions but no condition_fields"
|
|
assert isinstance(t['condition_fields'], list)
|
|
assert len(t['condition_fields']) > 0
|