calendar: a Russian summary keeps its letters in the fact key (V-443)
safeKey kept ASCII only, so "Встреча с Аней" and "Обед с мамой" both reduced to "--" and shared one key on one day. The second event of the day overwrote the first, silently, and his calendar is Russian. Letters and digits in any script now pass. Migration #18 deletes the rows written under the old rule instead of rewriting them: a calendar fact is derived, the next poll writes the day again, and a stale row reads as an extra meeting.
This commit is contained in:
@@ -208,6 +208,17 @@ ALTER TABLE reminders ADD COLUMN next_fire_ts INTEGER;`, // #2
|
||||
// list_tasks into something that writes without the row changing by one
|
||||
// byte. The fingerprint is the declared shape at approval time, so a
|
||||
// redefinition is a re-approval instead of a silent upgrade.
|
||||
`DELETE FROM facts
|
||||
WHERE key LIKE 'calendar_event_%'
|
||||
AND replace(substr(key, 25), '-', '') = '';`,
|
||||
// #18 — drop the calendar keys written while safeKey dropped Cyrillic
|
||||
// (Vikunja #443). Everything after the date prefix was punctuation, so
|
||||
// every Russian event on one day shared one key and only the last one
|
||||
// survived. Deleting rather than rewriting: a calendar fact is derived
|
||||
// data, the next poll writes the day again under keys that identify the
|
||||
// event, and the old rows would otherwise be recited as extra meetings.
|
||||
// The filter is exact — it keeps any key whose summary part still has a
|
||||
// letter or a digit in it.
|
||||
}
|
||||
|
||||
// migrate applies every migration with a number greater than the DB's current
|
||||
|
||||
@@ -47,3 +47,36 @@ func TestMigrateAppliesOnceAndIsIdempotent(t *testing.T) {
|
||||
t.Fatalf("after re-migrate user_version = %d, want %d", v, want)
|
||||
}
|
||||
}
|
||||
|
||||
// Migration #18 clears the calendar keys written while safeKey dropped
|
||||
// Cyrillic. Those rows are indistinguishable from real events on read, so
|
||||
// leaving them would recite one meeting as several (Vikunja #443).
|
||||
func TestCollapsedCalendarKeysAreDropped(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := newTestStore(t)
|
||||
|
||||
rows := []string{
|
||||
"calendar_event_20260804_--", // "Встреча с Аней" under the old rule
|
||||
"calendar_event_20260804_", // a one-word Russian summary
|
||||
"calendar_event_20260804_Встреча-с-Аней", // the new format
|
||||
"calendar_event_20260804_Standup", // an ASCII summary, always fine
|
||||
}
|
||||
for _, key := range rows {
|
||||
if _, err := s.db.ExecContext(ctx,
|
||||
`INSERT INTO facts (ts, kind, key, value, source, confidence) VALUES (0, 'env', ?, 'x', 'poll:caldav', 1.0)`,
|
||||
key); err != nil {
|
||||
t.Fatalf("seed %q: %v", key, err)
|
||||
}
|
||||
}
|
||||
if _, err := s.db.ExecContext(ctx, migrations[17]); err != nil {
|
||||
t.Fatalf("migration 18: %v", err)
|
||||
}
|
||||
|
||||
var got int
|
||||
if err := s.db.QueryRowContext(ctx, `SELECT count(*) FROM facts WHERE key LIKE 'calendar_event_%'`).Scan(&got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != 2 {
|
||||
t.Fatalf("%d calendar rows left, want the 2 that identify their event", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user