diff --git a/Mimante/Tests/SettingsMigrationTests.cs b/Mimante/Tests/SettingsMigrationTests.cs
new file mode 100644
index 0000000..587d016
--- /dev/null
+++ b/Mimante/Tests/SettingsMigrationTests.cs
@@ -0,0 +1,90 @@
+using AutoBidder.Utilities;
+using Xunit;
+
+namespace AutoBidder.Tests;
+
+///
+/// Migrazione delle impostazioni salvate da una versione precedente.
+///
+/// La proprieta' da difendere: un predefinito nuovo non arriva a chi ha gia' un
+/// file salvato, perche' il valore su disco vince. Va bene per le scelte dell'utente, non
+/// per due valori che erano conseguenze di misure sbagliate — il tetto di 40 richieste al
+/// secondo e l'anticipo di 1000 ms erano stati tarati quando il tetto stesso accodava le
+/// chiamate e gonfiava il ping. Senza migrazione l'aggiornamento sarebbe arrivato con le
+/// correzioni gia' annullate dal file dell'utente.
+///
+/// L'altra proprieta', altrettanto importante: chi quei valori li aveva gia' cambiati
+/// di suo se li tiene. Quella e' una scelta, e non spetta a un aggiornamento ribaltarla.
+///
+public class SettingsMigrationTests
+{
+ private static AppSettings Vecchie() => new()
+ {
+ SettingsSchemaVersion = 0,
+ MaxRequestsPerSecond = 40,
+ DefaultBidBeforeDeadlineMs = 1000
+ };
+
+ [Fact]
+ public void I_valori_vecchi_vengono_portati_avanti()
+ {
+ var s = Vecchie();
+
+ Assert.True(SettingsManager.Migra(s));
+
+ Assert.Equal(0, s.MaxRequestsPerSecond); // nessun tetto
+ Assert.Equal(500, s.DefaultBidBeforeDeadlineMs);
+ Assert.Equal(SettingsManager.SchemaCorrente, s.SettingsSchemaVersion);
+ }
+
+ [Fact]
+ public void Una_scelta_dell_utente_non_viene_ribaltata()
+ {
+ var s = new AppSettings
+ {
+ SettingsSchemaVersion = 0,
+ MaxRequestsPerSecond = 15, // scelto apposta, non il vecchio predefinito
+ DefaultBidBeforeDeadlineMs = 2000 // idem
+ };
+
+ SettingsManager.Migra(s);
+
+ Assert.Equal(15, s.MaxRequestsPerSecond);
+ Assert.Equal(2000, s.DefaultBidBeforeDeadlineMs);
+ }
+
+ [Fact]
+ public void Gira_una_volta_sola()
+ {
+ var s = Vecchie();
+
+ Assert.True(SettingsManager.Migra(s));
+ Assert.False(SettingsManager.Migra(s));
+
+ // E se poi l'utente rimette 40 di suo, non glielo si toglie al giro dopo.
+ s.MaxRequestsPerSecond = 40;
+ Assert.False(SettingsManager.Migra(s));
+ Assert.Equal(40, s.MaxRequestsPerSecond);
+ }
+
+ [Fact]
+ public void Impostazioni_nuove_di_zecca_sono_gia_allo_schema_corrente()
+ {
+ // Un file che non esiste ancora non e' un file vecchio: nasce gia' aggiornato,
+ // e non deve far scattare nessuna migrazione.
+ var s = new AppSettings { SettingsSchemaVersion = SettingsManager.SchemaCorrente };
+
+ Assert.False(SettingsManager.Migra(s));
+ }
+
+ [Fact]
+ public void I_predefiniti_di_fabbrica_sono_quelli_misurati_oggi()
+ {
+ var s = new AppSettings();
+
+ Assert.Equal(0, s.MaxRequestsPerSecond);
+ Assert.Equal(500, s.DefaultBidBeforeDeadlineMs);
+ Assert.True(s.QuietHoursEnabled);
+ Assert.True(s.AutoBidDuelWithdrawEnabled);
+ }
+}
diff --git a/Mimante/Utilities/SettingsManager.cs b/Mimante/Utilities/SettingsManager.cs
index 6c446a8..8837a2d 100644
--- a/Mimante/Utilities/SettingsManager.cs
+++ b/Mimante/Utilities/SettingsManager.cs
@@ -565,6 +565,17 @@ namespace AutoBidder.Utilities
// Vedi BiddingHours per i numeri: alle 0 e alle 9 la stessa asta costa quasi il
// doppio che fra le 10 e le 13.
+ ///
+ /// Versione dello schema delle impostazioni, per le migrazioni una tantum.
+ ///
+ /// Un file scritto da una versione precedente non ha questo campo e vale
+ /// quindi 0. Serve perché un predefinito nuovo non arriva a chi ha già un
+ /// file salvato: il valore su disco vince sempre, ed è giusto che sia così, tranne
+ /// quando il vecchio valore veniva da una misura che si è poi rivelata sbagliata.
+ /// Vedi .
+ ///
+ public int SettingsSchemaVersion { get; set; }
+
///
/// Sospendere le puntate nella fascia indicata. Acceso di serie.
/// L'asta resta comunque Attiva e riprende da sola: vedi .
@@ -833,6 +844,36 @@ namespace AutoBidder.Utilities
private const int CACHE_TTL_MS = 2000;
+ /// Schema corrente. Alzarlo fa girare una volta sola.
+ public const int SchemaCorrente = 1;
+
+ ///
+ /// Porta avanti un file scritto da una versione precedente. Restituisce true se
+ /// qualcosa è cambiato, così chi chiama sa che va risalvato.
+ ///
+ /// Perché serve. Il valore su disco vince sui predefiniti, ed è
+ /// giusto: sono scelte dell'utente. Ma due di quei valori non erano scelte, erano
+ /// conseguenze di misure sbagliate — il tetto di 40 richieste al secondo e
+ /// l'anticipo di 1000 ms erano stati tarati quando il tetto stesso accodava le
+ /// chiamate e gonfiava il ping a 444 ms. Senza migrazione, chi aggiorna si ritrova
+ /// le correzioni di oggi già annullate dal proprio file, e conclude che non
+ /// funzionano.
+ ///
+ /// Si tocca solo chi ha ancora esattamente il vecchio valore. Chi lo aveva
+ /// già cambiato di suo se lo tiene: quella è una scelta, e non spetta a un
+ /// aggiornamento ribaltarla.
+ ///
+ public static bool Migra(AppSettings s)
+ {
+ if (s == null || s.SettingsSchemaVersion >= SchemaCorrente) return false;
+
+ if (Math.Abs(s.MaxRequestsPerSecond - 40) < 0.001) s.MaxRequestsPerSecond = 0;
+ if (s.DefaultBidBeforeDeadlineMs == 1000) s.DefaultBidBeforeDeadlineMs = 500;
+
+ s.SettingsSchemaVersion = SchemaCorrente;
+ return true;
+ }
+
public static AppSettings Load()
{
// Percorso comune: nessun lucchetto, nessuna allocazione.
@@ -848,11 +889,20 @@ namespace AutoBidder.Utilities
return cached;
AppSettings loaded;
+ var daRisalvare = false;
+
try
{
- loaded = File.Exists(_file)
+ var esiste = File.Exists(_file);
+
+ loaded = esiste
? JsonSerializer.Deserialize(File.ReadAllText(_file)) ?? new AppSettings()
: new AppSettings();
+
+ // Un file nuovo nasce già allo schema corrente: la migrazione riguarda
+ // solo chi arriva da una versione precedente.
+ if (!esiste) loaded.SettingsSchemaVersion = SchemaCorrente;
+ else daRisalvare = Migra(loaded);
}
catch
{
@@ -860,6 +910,15 @@ namespace AutoBidder.Utilities
loaded = _cached ?? new AppSettings();
}
+ // Scritto qui e non dentro Save: Load è sul percorso caldo e non deve
+ // toccare il disco, ma una migrazione capita una volta sola nella vita
+ // del file, e lasciarla non salvata la farebbe ripetere a ogni avvio.
+ if (daRisalvare)
+ {
+ try { File.WriteAllText(_file, JsonSerializer.Serialize(loaded, new JsonSerializerOptions { WriteIndented = true })); }
+ catch { /* si riproverà al prossimo avvio */ }
+ }
+
_cached = loaded;
Interlocked.Exchange(ref _cacheExpiryTicks, DateTime.UtcNow.AddMilliseconds(CACHE_TTL_MS).Ticks);
return loaded;