Let an upgraded binary start against the settings it wrote before
The deployed service crash-looped 83 times on "config.toml:3: unknown setting \"include_amount\"". The classification redesign retired that preference from both the reader and the writer, but /var/lib/finance-duck/config.toml was written by the previous binary and still names it, and Open refuses any key its switch does not recognise. So the new binary would not start against its own settings file: nixos-rebuild switched successfully, systemd restarted the unit until it gave up, and the updater's health check failed - a deployment error whose cause was neither the build nor the code that was deployed. Retired settings are now read and discarded, and the next SaveSettings rewrites the file without them. An unrecognised key is still refused, because a misspelled preference that loads silently is a preference the user believes is in force. Every future removal adds its key to the same list rather than stranding the deployments that already hold it. Verified by running the built binary against a config.toml carrying exactly the line the host has: it starts and /api/health answers 200, where the previous binary exited 1. The regression test fails with "retired setting must not stop startup" before the change, and it still requires classify_on_imports to be rejected.
This commit is contained in:
@@ -7,6 +7,8 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
@@ -57,6 +59,43 @@ func seed(t *testing.T, a *App, s State) State {
|
||||
}
|
||||
return result.State
|
||||
}
|
||||
|
||||
// A released binary wrote include_amount into config.toml. Refusing it on
|
||||
// startup made every upgraded deployment crash-loop against its own settings
|
||||
// file, so a retired key must load and then disappear on the next save.
|
||||
func TestRetiredSettingLoadsAndIsRewrittenAwayButTyposStillFail(t *testing.T) {
|
||||
t.Setenv("OPENROUTER_API_KEY", "")
|
||||
t.Setenv("ENABLEBANKING_APP_ID", "")
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.toml")
|
||||
if err := os.WriteFile(path, []byte("classification_model = \"old/model\"\ninclude_amount = true\nclassify_on_import = false\n"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a, err := Open(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("retired setting must not stop startup: %v", err)
|
||||
}
|
||||
defer a.Close()
|
||||
if a.settings.Model != "old/model" || a.settings.ClassifyOnImport {
|
||||
t.Fatalf("surrounding settings lost: %#v", a.settings)
|
||||
}
|
||||
if _, err := a.SaveSettings(context.Background(), Settings{Model: "new/model", ClassifyOnImport: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
written, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(written), "include_amount") {
|
||||
t.Fatalf("retired setting survived a save: %s", written)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte("classify_on_imports = true\n"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := Open(dir); err == nil {
|
||||
t.Fatal("a misspelled setting must still be refused")
|
||||
}
|
||||
}
|
||||
func TestFailedClassificationStillImportsAndRetryIsIdempotent(t *testing.T) {
|
||||
a, s := testApp(t)
|
||||
mock := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusServiceUnavailable) }))
|
||||
|
||||
Reference in New Issue
Block a user