whisper-money/tests/Feature/Ai
Víctor Falcón 05d4bae0af
fix(queue): raise retry_after above the longest job timeout (PHP-LARAVEL-2D) (#645)
## What & why

Fixes **PHP-LARAVEL-2D** — `MaxAttemptsExceededException:
CategorizeUncategorizedTransactionsJob has been attempted too many
times.` (recurring, still firing as of yesterday, ~18 events).

### Root cause
The `database` queue connection used `retry_after = 90s`, but several
jobs declare a `$timeout` far above it (up to **600s**). `retry_after`
is **per-connection**. When a job outlives its 90s reservation, the DB
queue driver hands it to a second worker. For a `tries = 1` job that
immediately raises `MaxAttemptsExceededException` — and, worse, the body
can execute twice. `CategorizeUncategorizedTransactionsJob` runs on the
`ai` queue, which the deploy runs with **two concurrent workers**
(`docker/supervisor/supervisord.conf`), so the double-run re-bills
Gemini and resets progress — the exact harm its `tries = 1` was meant to
prevent.

At `retry_after = 90` **8 jobs** violated the invariant (600s rule jobs,
300s AI jobs, 120s sync jobs).

## Changes (one concern per commit)
1. **fix(queue): raise `retry_after` above the longest job timeout** —
default `90 → 900s` (env-overridable via `DB_QUEUE_RETRY_AFTER`),
clearing the 600s longest job with margin. Adds `QueueConfigTest`
asserting every `app/Jobs` `$timeout` stays `< retry_after` so a new
long job can't silently re-break it.
2. **fix(ai): de-duplicate the backfill per user** — `retry_after` stops
one dispatch being re-reserved, but not a *duplicate* dispatch (double
"Enable AI" click / re-enable mid-run) on the 2-worker `ai` queue.
Implement `ShouldBeUnique` keyed on user id, mirroring the sibling
`RetryTransientAiCategorizationJob`.
3. **test(queue): widen the guard** to also scan queued Mailables
(`app/Mail`) and Notifications (`app/Notifications`), which share the
connection and honor `$timeout`.
4. **docs(queue): correct the `retry_after` comment** — per-job
`$timeout` takes precedence over the worker `--timeout` (which is
currently unset), so the earlier "must sit between" phrasing was
inaccurate.

## Verification
- `QueueConfigTest` fails at the old `retry_after=90` (8 offenders) and
passes at `900`.
- `vendor/bin/pint --test` clean; 131 tests across `tests/Feature/Ai`,
AI-consent flow, and queue config pass.
- Confirmed against the real deploy: 2 workers on the `ai` queue,
`pcntl` installed (SIGALRM enforced), workers set no `--timeout` so each
job's own `$timeout` bounds execution below 900s.

## Reviewed by two independent agents (architecture + product)
Both concluded **ship it**. The double-dispatch gap they flagged is
closed by change #2. Neither found a correctness regression.

## Operational notes (no code change needed, flagged for awareness)
- **Reclaim window:** `retry_after` is per-connection, so
`emails`/`default` also inherit the 90→900s window. A job orphaned by a
*hard* worker crash (OOM/SIGKILL) is now reclaimed after up to ~15 min
instead of ~90s. Graceful deploys (SIGTERM + `stopwaitsecs=3600`) don't
orphan jobs, so this only bites on hard crashes — an acceptable trade. A
dedicated connection for the long AI queue would remove it; left as a
possible follow-up.
- **Worker `--timeout`:** the fix relies on `pcntl` staying installed
and long jobs keeping a `$timeout`. Setting an explicit `--timeout` (<
900) on the supervisord `queue:work` commands would make it
self-enforcing; not done here to avoid changing short-job hang
detection.
- If prod sets `DB_QUEUE_RETRY_AFTER` explicitly, ensure it's above the
longest job timeout (≥ 600, ideally 900).
2026-07-05 09:31:23 +00:00
..
AiCategorizationGateTest.php AI auto-categorization: open to pro + consent, nudge free users (#561) 2026-06-19 14:08:40 +00:00
AiRuleLearnerTest.php fix(ai): surface learned-rule toast in edit modal and guard weak description keys (#635) 2026-07-04 11:00:15 +00:00
ApplyRuleSuggestionsTest.php feat(ai): suggest automation rules during onboarding (#523) 2026-06-13 22:51:15 +02:00
CategorizationOverrideRouteTest.php feat(ai): auto-categorize transactions with AI (behind flag) (#535) 2026-06-15 16:35:20 +02:00
CategorizationSchemaTest.php feat(ai): auto-categorize transactions with AI (behind flag) (#535) 2026-06-15 16:35:20 +02:00
CategorizeBackfillCommandTest.php feat(ai): auto-categorize transactions with AI (behind flag) (#535) 2026-06-15 16:35:20 +02:00
CategorizeOnboardingTransactionsJobTest.php AI auto-categorization: open to pro + consent, nudge free users (#561) 2026-06-19 14:08:40 +00:00
CategorizeTransactionWithAiListenerTest.php fix: stop double-dispatching transaction listeners (N+1 insert into jobs) (#620) 2026-07-02 13:26:45 +00:00
CategorizeTransactionWithAiTest.php AI auto-categorization: open to pro + consent, nudge free users (#561) 2026-06-19 14:08:40 +00:00
CategorizeTransactionsTest.php fix(ai): handle transient AI provider overloads — stop the Sentry noise and retry the dropped work (#595) 2026-06-26 20:07:06 +02:00
CategorizeUncategorizedTransactionsTest.php fix(queue): raise retry_after above the longest job timeout (PHP-LARAVEL-2D) (#645) 2026-07-05 09:31:23 +00:00
CategoryOverrideHandlerTest.php feat(ai): learn from category corrections so the AI stops repeating the same mistake (#608) 2026-06-29 19:12:15 +02:00
CategorySourceFilterTest.php feat(ai): auto-categorize transactions with AI (behind flag) (#535) 2026-06-15 16:35:20 +02:00
RetryTransientAiCategorizationJobTest.php fix(ai): handle transient AI provider overloads — stop the Sentry noise and retry the dropped work (#595) 2026-06-26 20:07:06 +02:00
RuleSuggestionAggregatorTest.php feat(ai): learn from category corrections so the AI stops repeating the same mistake (#608) 2026-06-29 19:12:15 +02:00
RuleSuggestionConsolidatorTest.php feat(ai): suggest automation rules during onboarding (#523) 2026-06-13 22:51:15 +02:00
RuleSuggestionControllerTest.php feat(ai): suggest automation rules during onboarding (#523) 2026-06-13 22:51:15 +02:00
RuleSuggestionGeneratorTest.php feat(ai): suggest automation rules during onboarding (#523) 2026-06-13 22:51:15 +02:00
RuleSuggestionGuardTest.php feat(ai): suggest automation rules during onboarding (#523) 2026-06-13 22:51:15 +02:00
SuggestRulesCommandTest.php feat(ai): suggest automation rules during onboarding (#523) 2026-06-13 22:51:15 +02:00