Compare commits

..

12 Commits

Author SHA1 Message Date
claude 14ea06712e Merge pull request 'Weather: the 6-city match table has no geocoder behind it' (#128) from task/421-weather-geocoder into master 2026-08-04 18:23:03 +02:00
claude 42d3feadd2 Merge pull request 'No read path for delivery_attempts — the outbox is durable but invisible' (#127) from task/390-no-read-path-for-delivery-attempts into master 2026-08-04 18:22:58 +02:00
claude 2a2f706b74 Merge pull request 'Recall fixture: filler note ids can collide with case ids and split the two backends' (#126) from task/386-recall-fixture-filler-note-ids into master 2026-08-04 18:22:52 +02:00
claude e082e06868 Merge pull request 'Bug: morning.Item has no required/optional flag, so behaviour 1 of task 280 cannot hold' (#125) from task/473-bug-morning-item-has-no-required-flag into master 2026-08-04 18:22:45 +02:00
claude 88dc4e1383 Merge pull request 'Bug: make simulate routes with an empty seed set, so a green run proves less than it looks' (#124) from task/465-bug-make-simulate-routes-with-an-empty into master 2026-08-04 18:22:37 +02:00
claude 60759a991e Merge pull request 'Bug: spoken task capture is dead — the router calls the marker an act, and capture only rides the note intent' (#123) from task/467-bug-spoken-task-capture-is-dead into master 2026-08-04 18:22:28 +02:00
claude 9537346441 Merge pull request 'Bug: a pending clarify is global, so one unanswerable question swallows the next three utterances from anybody' (#122) from task/466-bug-a-pending-clarify-is-global-so-one-u into master 2026-08-04 18:21:49 +02:00
claude c6be818f13 Merge pull request 'Bug: pattern.Detect has no minimum-interval floor, so four fast taps mint a permanent false routine' (#121) from task/468-bug-pattern-detect-has-no-minimum-interv into master 2026-08-04 18:21:38 +02:00
claude b5575a9402 Merge pull request 'Bug: CheckFeminine flags second-person masculine verbs as self-reference' (#120) from task/462-bug-checkfeminine-flags-second-person-ma into master 2026-08-04 18:21:27 +02:00
claude fcda5e3d2c Merge pull request 'safeKey drops Cyrillic, so Russian calendar events on one day collide' (#119) from task/443-safekey-drops-cyrillic-so-russian-calend into master 2026-08-04 18:21:15 +02:00
claude bdafc82e35 Merge pull request 'llama-server core-dumps on every SIGTERM, so each mavgpud yield writes a core file' (#116) from task/491-llama-server-core-dumps-on-every-sigterm into master 2026-08-04 14:09:25 +02:00
claude c69023c310 Merge PR #117 into task/491 (V-383) 2026-08-04 14:08:59 +02:00
2 changed files with 0 additions and 96 deletions
-29
View File
@@ -219,35 +219,6 @@ ALTER TABLE reminders ADD COLUMN next_fire_ts INTEGER;`, // #2
// 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.
// #19 — unstick the routines accepted before the fire-forever fix
// (Vikunja #377, follow-up to #366). Accepting used to leave accepted_ts
// NULL and a live one-shot reminder behind, and the tick loop skips a row
// with no accepted_ts, so every non-weekly routine accepted before that fix
// has been silent ever since.
//
// Three statements, in this order, per stuck row: adopt created_ts as the
// acceptance time, cancel the reminder that is still holding the schedule,
// then let go of it. Cancelling before clearing matters — clearing first
// loses the only pointer to the reminder and leaves it to fire on its own.
//
// created_ts rather than a fresh timestamp because a migration has no
// clock, and because the first interval should be measured from when he
// said yes. A routine whose interval has already elapsed nudges on the next
// tick, which is what being unstuck looks like.
//
// Weekly rows are included deliberately. Theirs was the case that kept
// working, because the cron reminder reschedules itself — so leaving them
// alone would give them both a cron reminder and a tick-loop schedule for
// one habit, and he would hear it twice.
`UPDATE reminders
SET status = 'cancelled'
WHERE status = 'pending'
AND id IN (SELECT reminder_id FROM proposed_routines
WHERE status = 'accepted' AND accepted_ts IS NULL AND reminder_id IS NOT NULL);
UPDATE proposed_routines
SET accepted_ts = created_ts, reminder_id = NULL
WHERE status = 'accepted' AND accepted_ts IS NULL;`,
}
// migrate applies every migration with a number greater than the DB's current
-67
View File
@@ -3,7 +3,6 @@ package store
import (
"context"
"testing"
"time"
)
func userVersion(t *testing.T, s *Store) int {
@@ -81,69 +80,3 @@ func TestCollapsedCalendarKeysAreDropped(t *testing.T) {
t.Fatalf("%d calendar rows left, want the 2 that identify their event", got)
}
}
// TestStuckRoutinesAreBackfilled — routines accepted before the fire-forever
// fix have accepted_ts NULL and a live reminder, so the tick loop skips them
// and they have been silent ever since (Vikunja #377). The migration touches
// live reminders, which is why it is tested against a real store.
func TestStuckRoutinesAreBackfilled(t *testing.T) {
ctx := context.Background()
s := newTestStore(t)
created := time.Date(2026, 7, 1, 9, 0, 0, 0, time.UTC)
rem, err := s.CreateReminder(ctx, created.Add(time.Hour), "полить цветы", "")
if err != nil {
t.Fatal(err)
}
healthy, err := s.CreateReminder(ctx, created.Add(2*time.Hour), "не трогать", "")
if err != nil {
t.Fatal(err)
}
if _, err := s.db.ExecContext(ctx,
`INSERT INTO proposed_routines (action, object, interval_days, status, created_ts, reminder_id, accepted_ts)
VALUES ('water', 'plants', 7, 'accepted', ?, ?, NULL)`,
created.UnixMilli(), rem); err != nil {
t.Fatal(err)
}
// An already-healthy accepted row, and a still-open proposal: neither is
// this migration's business.
if _, err := s.db.ExecContext(ctx,
`INSERT INTO proposed_routines (action, object, interval_days, status, created_ts, accepted_ts)
VALUES ('feed', 'cat', 1, 'accepted', ?, ?)`,
created.UnixMilli(), created.UnixMilli()); err != nil {
t.Fatal(err)
}
if _, err := s.db.ExecContext(ctx, migrations[18]); err != nil {
t.Fatalf("migration 19: %v", err)
}
accepted, err := s.ListAcceptedRoutines(ctx)
if err != nil || len(accepted) != 2 {
t.Fatalf("ListAcceptedRoutines = %d rows, err=%v, want 2", len(accepted), err)
}
stuck := accepted[0]
if stuck.Object != "plants" {
stuck = accepted[1]
}
if stuck.AcceptedTs == nil || !stuck.AcceptedTs.Equal(created) {
t.Fatalf("accepted_ts = %v, want the creation time", stuck.AcceptedTs)
}
if stuck.ReminderID != nil {
t.Fatalf("reminder_id = %v, want it let go", stuck.ReminderID)
}
// The reminder it was holding is cancelled, and nothing else is.
var status string
if err := s.db.QueryRowContext(ctx, `SELECT status FROM reminders WHERE id = ?`, rem).Scan(&status); err != nil {
t.Fatal(err)
}
if status != ReminderCancelled {
t.Fatalf("linked reminder status = %q, want cancelled", status)
}
if err := s.db.QueryRowContext(ctx, `SELECT status FROM reminders WHERE id = ?`, healthy).Scan(&status); err != nil {
t.Fatal(err)
}
if status != "pending" {
t.Fatalf("unrelated reminder status = %q, want it untouched", status)
}
}