Covers model_registry.py without requiring a running service or LLM:
Empty/fresh state: no files, missing user dir
Save/load: round-trip, corrupt file fallback
Migration: v1 hosts/models, v1 no active, v0 flat, v0 empty url,
distill_backend_mid=local → distill role, saves file after migrate
Built-in resolution: claude_cli, gemini_api, gemini_cli, unknown → None
User model resolution: local_openai merges host, missing host → None
get_model_for_role: registry primary, built-in from registry, skips missing,
walks full backup chain, .env fallback, hardcoded fallback,
custom roles
get_best_local_model: prefers role chain, falls back to first local, None if no local
Host CRUD: create, update, unknown ID creates new, remove + cascades to models
Model CRUD: create, update, remove + clears role refs
set_role: assign model, assign built-in, clear with None, invalid slot,
unknown model ID, creates new role key
get_defined_roles: returns all settings roles, fills gaps with {}
Multi-user isolation: registries don't bleed across users
All tests use tmp_path + patch.object(config.settings, ...) — no real files touched.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>