/
/
/
1"""
2Regression tests for domain-less core config blocks (issue #6278).
3
4The background tasks controller persists its scheduler state with a raw path write to
5``core/tasks/scheduled_task_states``. On a fresh install that block does not exist yet,
6so the underlying ``set`` helper created it on the fly and left a stub without the
7``domain`` key that ``CoreConfig`` requires. Listing the core configs then crashed with
8"Field 'domain' of type str is missing in CoreConfig instance".
9
10Two complementary fixes are covered here. The write path creates a valid CoreConfig base
11object first, so a raw write can no longer leave a domain-less stub behind (root cause).
12The read path fills in a missing ``domain``, so installs that already carry such a stub
13on disk render their core config again.
14"""
15
16from __future__ import annotations
17
18from music_assistant.constants import CONF_CORE, CONFIGURABLE_CORE_CONTROLLERS
19from music_assistant.mass import MusicAssistant
20
21
22async def test_fresh_install_leaves_a_parsable_tasks_core_config(mass: MusicAssistant) -> None:
23 """
24 A first start on empty storage must not leave a domain-less core/tasks block behind.
25
26 This is the reported scenario. No settings.json exists yet, so the repair in
27 ``migrate()`` never runs and only the write path can keep the block valid.
28 """
29 raw_conf = mass.config.get(f"{CONF_CORE}/tasks")
30 assert raw_conf["domain"] == "tasks"
31 # the startup task registrations have persisted their state into the same block
32 assert raw_conf["scheduled_task_states"]
33
34 configs = await mass.config.get_core_configs()
35 assert {config.domain for config in configs} == set(CONFIGURABLE_CORE_CONTROLLERS)
36
37
38async def test_persisting_task_state_keeps_the_core_config_valid(mass: MusicAssistant) -> None:
39 """A raw task-state write into an absent core block leaves a parsable CoreConfig."""
40 mass.config.remove(f"{CONF_CORE}/tasks")
41 assert mass.config.get(f"{CONF_CORE}/tasks") is None
42
43 mass.tasks._set_persisted_task_states({"some_task": {"status": "idle"}})
44
45 raw_conf = mass.config.get(f"{CONF_CORE}/tasks")
46 assert raw_conf["domain"] == "tasks"
47 assert raw_conf["scheduled_task_states"] == {"some_task": {"status": "idle"}}
48 # the state write must not have clobbered the config values structure
49 assert raw_conf["values"] == {}
50
51
52async def test_persisting_task_state_repairs_an_existing_stub(mass: MusicAssistant) -> None:
53 """A stub written by an older version is repaired by the next task-state write."""
54 mass.config.set(f"{CONF_CORE}/tasks", {"scheduled_task_states": {"old_task": {}}})
55
56 mass.tasks._set_persisted_task_states({"some_task": {"status": "idle"}})
57
58 assert mass.config.get(f"{CONF_CORE}/tasks/domain") == "tasks"
59
60
61async def test_get_core_configs_survives_a_domain_less_stub(mass: MusicAssistant) -> None:
62 """Listing the core configs works even when a stored block has no 'domain' key."""
63 mass.config.set(f"{CONF_CORE}/tasks", {"scheduled_task_states": {"some_task": {}}})
64
65 configs = await mass.config.get_core_configs()
66
67 by_domain = {config.domain for config in configs}
68 assert by_domain == set(CONFIGURABLE_CORE_CONTROLLERS)
69
70
71async def test_get_core_config_survives_a_domain_less_stub(mass: MusicAssistant) -> None:
72 """Reading a single core config works even when its stored block has no 'domain' key."""
73 mass.config.set(f"{CONF_CORE}/tasks", {"scheduled_task_states": {"some_task": {}}})
74
75 config = await mass.config.get_core_config("tasks")
76
77 assert config.domain == "tasks"
78