33from __future__ import annotations
44
55import json
6+ from contextlib import nullcontext
67from unittest .mock import patch
78
89from ucode .agents import pi
@@ -267,9 +268,13 @@ def _setup(self, tmp_path, monkeypatch):
267268 monkeypatch .setattr (config_io_mod , "APP_DIR" , tmp_path )
268269 config_file = tmp_path / "models.json"
269270 backup_file = tmp_path / "pi-backup.json"
271+ settings_file = tmp_path / "settings.json"
272+ settings_backup_file = tmp_path / "pi-settings-backup.json"
270273 monkeypatch .setattr (pi_mod , "PI_CONFIG_PATH" , config_file )
274+ monkeypatch .setattr (pi_mod , "PI_SETTINGS_PATH" , settings_file )
271275 monkeypatch .setattr (pi_mod , "PI_BACKUP_PATH" , backup_file )
272- return pi_mod , config_file
276+ monkeypatch .setattr (pi_mod , "PI_SETTINGS_BACKUP_PATH" , settings_backup_file )
277+ return pi_mod , config_file , settings_file , settings_backup_file
273278
274279 def _state (self , ** overrides ) -> dict :
275280 state = {
@@ -284,7 +289,7 @@ def _state(self, **overrides) -> dict:
284289 return state
285290
286291 def test_stale_managed_providers_removed_before_merge (self , tmp_path , monkeypatch ):
287- pi_mod , config_file = self ._setup (tmp_path , monkeypatch )
292+ pi_mod , config_file , _ , _ = self ._setup (tmp_path , monkeypatch )
288293
289294 stale = {
290295 "providers" : {
@@ -312,7 +317,7 @@ def test_legacy_providers_removed_on_upgrade(self, tmp_path, monkeypatch):
312317 """Earlier ucode versions wrote `databricks-anthropic`, `databricks-codex`,
313318 and `databricks-oss` providers. They must be stripped on the next write
314319 so users don't end up with stale entries pointing at routes that 400."""
315- pi_mod , config_file = self ._setup (tmp_path , monkeypatch )
320+ pi_mod , config_file , _ , _ = self ._setup (tmp_path , monkeypatch )
316321
317322 config_file .write_text (
318323 json .dumps (
@@ -339,7 +344,7 @@ def test_legacy_providers_removed_on_upgrade(self, tmp_path, monkeypatch):
339344 assert "databricks-claude" in written_providers
340345
341346 def test_config_written_with_correct_model_and_token (self , tmp_path , monkeypatch ):
342- pi_mod , config_file = self ._setup (tmp_path , monkeypatch )
347+ pi_mod , config_file , _ , _ = self ._setup (tmp_path , monkeypatch )
343348
344349 with (
345350 patch ("ucode.agents.pi.get_databricks_token" , return_value = "tok" ),
@@ -350,3 +355,61 @@ def test_config_written_with_correct_model_and_token(self, tmp_path, monkeypatch
350355 written = json .loads (config_file .read_text ())
351356 assert written ["model" ] == "databricks-claude/claude-sonnet"
352357 assert written ["providers" ]["databricks-claude" ]["apiKey" ] == "tok"
358+
359+ def test_settings_pins_default_provider_and_model (self , tmp_path , monkeypatch ):
360+ # Without this, Pi's `findInitialModel` can fall through to a built-in
361+ # provider when an unrelated env var (e.g. HF_TOKEN) makes one look
362+ # auth-configured. Pinning the default keeps Pi on our provider.
363+ pi_mod , _ , settings_file , _ = self ._setup (tmp_path , monkeypatch )
364+
365+ with (
366+ patch ("ucode.agents.pi.get_databricks_token" , return_value = "tok" ),
367+ patch ("ucode.agents.pi.save_state" ),
368+ ):
369+ pi_mod .write_tool_config (self ._state (), "claude-sonnet" , token = "tok" )
370+
371+ settings = json .loads (settings_file .read_text ())
372+ assert settings ["defaultProvider" ] == "databricks-claude"
373+ assert settings ["defaultModel" ] == "claude-sonnet"
374+
375+ def test_pre_existing_settings_are_backed_up_before_first_write (self , tmp_path , monkeypatch ):
376+ pi_mod , _ , settings_file , settings_backup_file = self ._setup (tmp_path , monkeypatch )
377+
378+ original = '{"theme": "Default Dark", "defaultProvider": "openai"}'
379+ settings_file .parent .mkdir (parents = True , exist_ok = True )
380+ settings_file .write_text (original , encoding = "utf-8" )
381+
382+ with (
383+ patch ("ucode.agents.pi.get_databricks_token" , return_value = "tok" ),
384+ patch ("ucode.agents.pi.save_state" ),
385+ ):
386+ pi_mod .write_tool_config (self ._state (), "claude-sonnet" , token = "tok" )
387+
388+ assert settings_backup_file .read_text (encoding = "utf-8" ) == original
389+ # The on-disk settings still get the ucode pin applied via deep_merge.
390+ merged = json .loads (settings_file .read_text ())
391+ assert merged ["defaultProvider" ] == "databricks-claude"
392+ assert merged ["theme" ] == "Default Dark"
393+
394+
395+ class TestValidateAllToolsPiRollback :
396+ def test_failed_pi_validation_rolls_back_settings (self , tmp_path , monkeypatch ):
397+ import ucode .agents as agents_mod
398+ import ucode .agents .pi as pi_mod
399+
400+ settings_file = tmp_path / "settings.json"
401+ settings_file .write_text ("{}" , encoding = "utf-8" )
402+ monkeypatch .setattr (pi_mod , "PI_SETTINGS_PATH" , settings_file )
403+ monkeypatch .setattr (pi_mod , "PI_SETTINGS_BACKUP_PATH" , tmp_path / "settings.backup.json" )
404+ # Keep the generic models.json rollback off the user's real config dir.
405+ monkeypatch .setitem (agents_mod .TOOL_SPECS ["pi" ], "config_path" , tmp_path / "models.json" )
406+ monkeypatch .setitem (
407+ agents_mod .TOOL_SPECS ["pi" ], "backup_path" , tmp_path / "models.backup.json"
408+ )
409+ monkeypatch .setattr (agents_mod , "validate_tool" , lambda tool : (False , "boom" ))
410+ monkeypatch .setattr (agents_mod , "save_state" , lambda s : None )
411+ monkeypatch .setattr (agents_mod , "spinner" , lambda * _a , ** _kw : nullcontext ())
412+
413+ agents_mod .validate_all_tools ({"available_tools" : ["pi" ], "managed_configs" : {"pi" : True }})
414+
415+ assert not settings_file .exists ()
0 commit comments