@@ -44,3 +44,139 @@ def test_valid_schema_update_is_persisted(isolated_runtime_config):
4444 cfg .set_nested ("rate_limit" , "burst_limit" , 9 )
4545
4646 assert cfg .get_nested ("rate_limit" , "burst_limit" ) == 9
47+
48+
49+ def test_missing_schema_is_merged_and_preserved (tmp_path , monkeypatch ):
50+ schema_path = Path (__file__ ).resolve ().parents [1 ] / "config.schema.json"
51+ config_path = tmp_path / "config.json"
52+ config_path .write_text (
53+ """
54+ {
55+ "bot": {
56+ "name": "Custom Bot",
57+ "owner_jid": "12345@s.whatsapp.net"
58+ },
59+ "features": {
60+ "notes": false
61+ }
62+ }
63+ """ .strip (),
64+ encoding = "utf-8" ,
65+ )
66+
67+ monkeypatch .setattr (runtime_config_module , "CONFIG_FILE" , config_path )
68+ monkeypatch .setattr (runtime_config_module , "SCHEMA_FILE" , schema_path )
69+ monkeypatch .setattr (
70+ runtime_config_module , "OVERRIDES_FILE" , tmp_path / "runtime_overrides.json"
71+ )
72+ monkeypatch .setattr (
73+ runtime_config_module ,
74+ "OVERRIDES_MIGRATION_MARKER" ,
75+ tmp_path / ".runtime_overrides_migrated" ,
76+ )
77+ runtime_config_module .RuntimeConfig ._instance = None
78+
79+ cfg = runtime_config_module .RuntimeConfig ()
80+
81+ assert cfg .get_nested ("bot" , "name" ) == "Custom Bot"
82+ assert cfg .get_nested ("bot" , "owner_jid" ) == "12345@s.whatsapp.net"
83+ assert cfg .get_nested ("features" , "notes" ) is False
84+ assert cfg .get_nested ("features" , "anti_delete" ) is True
85+
86+ persisted = runtime_config_module .jsonc .load (config_path )
87+ assert persisted .get ("$schema" ) == runtime_config_module .DEFAULT_SCHEMA_PATH
88+ assert persisted .get ("bot" , {}).get ("name" ) == "Custom Bot"
89+ assert persisted .get ("features" , {}).get ("notes" ) is False
90+
91+ runtime_config_module .RuntimeConfig ._instance = None
92+
93+
94+ def test_invalid_config_does_not_overwrite_file (tmp_path , monkeypatch ):
95+ schema_path = Path (__file__ ).resolve ().parents [1 ] / "config.schema.json"
96+ config_path = tmp_path / "config.json"
97+ invalid_content = """
98+ {
99+ "bot": "not-an-object"
100+ }
101+ """ .strip ()
102+ config_path .write_text (invalid_content , encoding = "utf-8" )
103+
104+ monkeypatch .setattr (runtime_config_module , "CONFIG_FILE" , config_path )
105+ monkeypatch .setattr (runtime_config_module , "SCHEMA_FILE" , schema_path )
106+ monkeypatch .setattr (
107+ runtime_config_module , "OVERRIDES_FILE" , tmp_path / "runtime_overrides.json"
108+ )
109+ monkeypatch .setattr (
110+ runtime_config_module ,
111+ "OVERRIDES_MIGRATION_MARKER" ,
112+ tmp_path / ".runtime_overrides_migrated" ,
113+ )
114+ runtime_config_module .RuntimeConfig ._instance = None
115+
116+ cfg = runtime_config_module .RuntimeConfig ()
117+
118+ assert cfg .get_nested ("bot" , "name" ) == "Zero Ichi"
119+ assert config_path .read_text (encoding = "utf-8" ).strip () == invalid_content
120+
121+ runtime_config_module .RuntimeConfig ._instance = None
122+
123+
124+ def test_missing_default_keys_are_persisted_with_existing_schema (tmp_path , monkeypatch ):
125+ schema_path = Path (__file__ ).resolve ().parents [1 ] / "config.schema.json"
126+ config_path = tmp_path / "config.json"
127+ config_path .write_text (
128+ """
129+ {
130+ "$schema": "./config.schema.json",
131+ "bot": {
132+ "name": "Custom Bot",
133+ "prefix": "/",
134+ "login_method": "QR",
135+ "phone_number": "",
136+ "owner_jid": "owner@s.whatsapp.net",
137+ "auto_read": false,
138+ "auto_reload": true,
139+ "auto_react": false,
140+ "auto_react_emoji": "",
141+ "ignore_self_messages": true,
142+ "self_mode": false
143+ },
144+ "features": {
145+ "anti_delete": true,
146+ "anti_link": true,
147+ "welcome": true,
148+ "notes": false,
149+ "filters": true,
150+ "blacklist": true,
151+ "warnings": true,
152+ "automation_rules": true
153+ }
154+ }
155+ """ .strip (),
156+ encoding = "utf-8" ,
157+ )
158+
159+ monkeypatch .setattr (runtime_config_module , "CONFIG_FILE" , config_path )
160+ monkeypatch .setattr (runtime_config_module , "SCHEMA_FILE" , schema_path )
161+ monkeypatch .setattr (
162+ runtime_config_module , "OVERRIDES_FILE" , tmp_path / "runtime_overrides.json"
163+ )
164+ monkeypatch .setattr (
165+ runtime_config_module ,
166+ "OVERRIDES_MIGRATION_MARKER" ,
167+ tmp_path / ".runtime_overrides_migrated" ,
168+ )
169+ runtime_config_module .RuntimeConfig ._instance = None
170+
171+ cfg = runtime_config_module .RuntimeConfig ()
172+
173+ # Existing user values are preserved
174+ assert cfg .get_nested ("bot" , "name" ) == "Custom Bot"
175+ assert cfg .get_nested ("features" , "notes" ) is False
176+
177+ # Newly added keys are merged and persisted
178+ assert cfg .get_nested ("features" , "anti_spam" ) is False
179+ persisted = runtime_config_module .jsonc .load (config_path )
180+ assert persisted .get ("features" , {}).get ("anti_spam" ) is False
181+
182+ runtime_config_module .RuntimeConfig ._instance = None
0 commit comments