fix(settings): AI, Gmail og kalenderen delte én række og slettede hinanden
Meldt ind som "AI-informationerne gemmes ikke". De blev gemt. De blev taget bagefter. `user_settings` blev oprettet med `user_id` som hele sin primærnøgle — én række per person, uanset scope — og upserten skrev `scope` om ved konflikt. Der er tre scopes i live: `ai`, `gmail` og `calendar`. De delte den ene række, så hver skrivning slettede de to andre, og hver eneste svarede 204. Det virker begge veje: at forbinde kalenderen slettede Gmail-forbindelsen, og Gmail slettede kalenderen. Fælden var set komme to gange. 0011 gav instansens indstillinger deres egen tabel delvis af den grund og skrev advarslen ned med rene ord; 0014 flyttede postkasserne ud og skrev, at advarslen "nær var trådt i for anden gang". Tredje gang var kalenderen, som lagde et `calendar`-scope ved siden af de to andre. Lektien værd at holde fast i: en advarsel i en migration bliver læst af den, der redigerer *den* migration, og det var ingen. Formen er rettet her, så der ikke er noget tilbage at advare om — `PRIMARY KEY (user_id, scope)`, og upserten rammer den nøgle. Tabellen bygges om, fordi SQLite ikke kan ændre en primærnøgle på plads. De rækker, der findes, kopieres som de er: hver person har højst én, og den holder det scope, der vandt sidst. Det, de tidligere skrivninger tog, er overskrevet og kan ikke hentes tilbage her — efter opdateringen skal AI-udbyderen sættes op igen, og Gmail-forbindelsen tjekkes. To prøver: én mod store-laget, hvor formen var fejlen, og én gennem handlerne, hvor den blev set. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e46aaa462a
commit
6b4448b245
@@ -562,6 +562,48 @@ func TestAIKeyIsNeverReturned(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Connecting something else must not delete the AI provider.
|
||||
//
|
||||
// Reported from use as "the AI settings are not saved": they were saved, and then
|
||||
// taken. `user_settings` had `user_id` as its whole primary key, and the upsert
|
||||
// renamed `scope` on conflict — so the `ai`, `gmail` and `calendar` scopes shared
|
||||
// one row and each write deleted the other two. Every write returned 204.
|
||||
//
|
||||
// Through the handlers, because that is where it was seen. The shape is asserted
|
||||
// against the store as well: see TestOnePersonCanHaveSettingsForMoreThanOneThing.
|
||||
func TestTheAIProviderSurvivesConnectingSomethingElse(t *testing.T) {
|
||||
ts := newTestServer(t)
|
||||
ts.bootstrap(t)
|
||||
|
||||
ts.do(t, "PUT", "/api/v1/ai/settings", map[string]any{
|
||||
"provider": "anthropic", "model": "claude-sonnet-5", "api_key": "sk-hemmelig-nøgle",
|
||||
})
|
||||
|
||||
user, err := ts.db.UserByEmail(t.Context(), "kristian@example.dk")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// What the calendar and the mailbox handlers do when a handshake starts: write
|
||||
// their own scope for the same person.
|
||||
for _, scope := range []string{"calendar", "gmail"} {
|
||||
if err := ts.db.SetUserSettings(t.Context(), user.ID, scope,
|
||||
map[string]any{"pkce_state": "s-" + scope}); err != nil {
|
||||
t.Fatalf("save %s: %v", scope, err)
|
||||
}
|
||||
}
|
||||
|
||||
_, body := ts.do(t, "GET", "/api/v1/ai/settings", nil)
|
||||
if body["provider"] != "anthropic" {
|
||||
t.Errorf("the provider is %v after connecting two other things, want anthropic", body["provider"])
|
||||
}
|
||||
if body["model"] != "claude-sonnet-5" {
|
||||
t.Errorf("the model is %v", body["model"])
|
||||
}
|
||||
if body["has_key"] != true {
|
||||
t.Error("the key was taken with it")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGmailSettings(t *testing.T) {
|
||||
ts := newTestServer(t)
|
||||
ts.bootstrap(t)
|
||||
|
||||
Reference in New Issue
Block a user