Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changes/unreleased/fix-calibration-default-profile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Bugfixes

- **Fix Calibration Default Profile**: restore the default-calibration-profile backfill dropped when calibration ownership moved to the daemon, so fresh installs (and any install with an explicit empty `calibration_profiles` list) get a usable profile and active calibration ID instead of a permanently disabled calibration flow.
30 changes: 27 additions & 3 deletions crates/skill-daemon/src/routes/settings_calibration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,10 @@ mod tests {

fn mk_state() -> (TempDir, AppState) {
let td = TempDir::new().unwrap();
// Write default settings with a default calibration profile.
let mut settings = skill_settings::UserSettings::default();
settings.calibration_profiles = vec![CalibrationProfile::default()];
// Write default settings. calibration_profiles is left at its
// Vec::new() default — load_settings()'s backfill (restored from
// 3abfaf60) is what populates it, not this fixture.
let settings = skill_settings::UserSettings::default();
let path = skill_settings::settings_path(td.path());
if let Some(p) = path.parent() {
std::fs::create_dir_all(p).unwrap();
Expand All @@ -216,6 +217,29 @@ mod tests {
(td, state)
}

/// Regression test for the backfill dropped by 3abfaf60: a settings.json
/// with `"calibration_profiles": []` explicitly present (not merely
/// missing) must still yield exactly one profile, and its id must equal
/// active_calibration_id (M-1 — the other half of the deleted block).
/// Written as raw JSON text so this cannot silently stop covering the
/// explicit-`[]` case if the struct ever gains `skip_serializing_if`.
#[tokio::test]
async fn list_profiles_backfills_when_explicitly_empty() {
let td = TempDir::new().unwrap();
let path = skill_settings::settings_path(td.path());
if let Some(p) = path.parent() {
std::fs::create_dir_all(p).unwrap();
}
std::fs::write(&path, r#"{"calibration_profiles": [], "active_calibration_id": ""}"#).unwrap();
let state = AppState::new("token".into(), td.path().to_path_buf());

let res = list_profiles(State(state.clone())).await.0;
assert_eq!(res.len(), 1);

let active = get_active_profile_id(State(state)).await.0;
assert_eq!(active["value"], res[0].id);
}

#[tokio::test]
async fn list_profiles_returns_defaults() {
let (_td, state) = mk_state();
Expand Down
14 changes: 14 additions & 0 deletions crates/skill-settings/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1291,6 +1291,20 @@ pub fn load_settings(skill_dir: &Path) -> UserSettings {
s.settings_shortcut = default_settings_shortcut();
}

// ── Calibration migration: 3abfaf60 moved calibration ownership to the
// daemon and deleted the backfill that ran in the Tauri client
// (v0.0.121:src-tauri/src/setup.rs:370-382). Nothing re-established it
// daemon-side. Restore it where settings are now loaded. In-memory only,
// deliberately — this re-applies on every load and lands on disk
// incidentally, the first time any route calls modify_settings_blocking,
// matching the shortcut migrations directly above.
if s.calibration_profiles.is_empty() {
s.calibration_profiles = vec![CalibrationProfile::from_legacy(&s.calibration)];
}
if s.active_calibration_id.is_empty() {
s.active_calibration_id = s.calibration_profiles.first().map(|p| p.id.clone()).unwrap_or_default();
}

// ── Secret migration: plaintext JSON → system keychain ───────────────
//
// If the JSON file still contains non-empty secret values (from a
Expand Down
Loading