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:
Kristian Wind
2026-08-20 14:56:48 +02:00
co-authored by Claude Opus 5
parent e46aaa462a
commit 6b4448b245
5 changed files with 152 additions and 3 deletions
+5 -1
View File
@@ -195,10 +195,14 @@ func TestAGmailConnectedBeforeTheMoveSurvivesIt(t *testing.T) {
}
values := `{"refresh_token":"` + sealed + `","trigger":"label","label":"Verdande",` +
`"email":"kw@nolimit.dk","seen":["abc","def"],"project_id":"p-1"}`
// The conflict target is (user_id, scope) since 0025 — before that the table
// held one row per person and the upsert renamed its scope, which is the bug
// 0025 exists to undo. Nothing conflicts here; the clause only has to be legal
// against the schema this test is running on.
if _, err := db.ExecContext(context.Background(), `
INSERT INTO user_settings (user_id, scope, values_json, updated_at)
VALUES (?, 'gmail', ?, 0)
ON CONFLICT (user_id) DO UPDATE SET scope = 'gmail', values_json = excluded.values_json`,
ON CONFLICT (user_id, scope) DO UPDATE SET values_json = excluded.values_json`,
userID, values); err != nil {
t.Fatal(err)
}