Repository navigation
feat(plans): per-plan health-score badge (closes #376 scope) - #1521
Conversation
📝 WalkthroughWalkthroughAdds backend plan health scoring from execution and schedule data, exposes scores and factors through the plans API, and renders frontend health badges with escaped tooltips. PostgreSQL aggregation, API behavior, compatibility handling, and scoring/rendering tests are included. ChangesPlan health scoring
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant PlansAPI
participant Store
participant HealthScorer
participant PlansUI
Client->>PlansAPI: request plan list
PlansAPI->>Store: count executions by plan and status
Store-->>PlansAPI: aggregated counts
PlansAPI->>HealthScorer: compute score and factors
HealthScorer-->>PlansAPI: health metadata
PlansAPI-->>PlansUI: plans with health fields
PlansUI->>PlansUI: render escaped health badge tooltip
Suggested labels: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Adversarial review of the health-score computationReviewed this PR specifically for the failure mode a computed score invites: a plausible-looking number that is wrong. Three rounds, each with an independent reviewer on the fix diff. Six confirmed defects, all fixed with regression tests that fail pre-fix. Commits Confirmed and fixed1. Truncated inputs produced confidently-healthy badges — The execution counts came from Replaced with a new 2. Fabricated perfect score on a store failure — When the executions fetch failed, every plan was assigned 3. Penalty derived from a missing input —
4. A single NULL
5. Counting window used the wrong column, making the tooltip false — A plan's executions are created up front for the whole ramp, so a pending row carries a 6. Unbounded counts, then a window justified by a claim that did not hold —
Checked and found clean (not changed)
One thing worth a human decision (not changed)A completed plan short-circuits to a perfect 100 regardless of how many failures it accumulated on the way. That is documented and deliberate in the original commit, and defensible (end-of-life noise is not actionable), so I left it. Flagging it because it means a badge can read 100 for a plan whose history was rocky. Separately, and pre-existing rather than introduced here: VerificationRoot module: Note for anyone reproducing locally: @coderabbitai full review |
|
🐇🔍 ✅ Action performedFull review finished. Your included review limit is currently reached under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 8 minutes. |
|
The previous full-review pass returned without posting findings (included-review limit). Re-requesting now that the window has reset. @coderabbitai full review |
|
You're currently rate limited under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. Your next review will be available in 55 minutes. |
follow-up, revives #376) Adds computePlanHealth, a pure 0-100 scoring function derived from a plan's ramp-schedule attributes and its recent failed/cancelled executions, with named penalty factors (overdue, failed_executions, cancelled_executions, stalled, behind_schedule, disabled_midway) so a bad score is actionable instead of opaque. listPlans now returns PlanWithHealth (PurchasePlan plus health_score/ health_factors) computed at read time; the score is never persisted. Reuses the existing GetExecutionsByStatuses call shape rather than adding a new store method. If that fetch fails, every plan defaults to a perfect score rather than a partial score or a 500.
…loses #340 follow-up, revives #376) Adds a colour-banded health badge (green >= 80, amber 50-79, red < 50) next to the status badge on each plan card, reusing the existing status-badge/badge-success/badge-warning/badge-danger palette instead of new CSS. The badge's title tooltip enumerates every penalty factor returned by the backend so a bad score is actionable. health_score/health_factors are optional on Plan/LocalPlan/BackendPlan so a cached response from before this feature shipped still renders the card without the badge. Every factor note goes through escapeHtmlAttr before it lands in the title attribute.
The per-plan health score could report a confident number built from data it never read. Three ways: 1. Truncated inputs. The failed/canceled execution counts came from GetExecutionsByStatuses, whose ORDER BY scheduled_date DESC + LIMIT page is capped at DefaultListLimit across ALL plans. Once system-wide execution volume exceeds that page, a plan's own failures fall outside it and the plan renders a green "healthy" badge while being unhealthy; the effect grows monotonically as newer rows push older ones out. Replaced with CountExecutionsByPlanAndStatus, a GROUP BY aggregate whose result is bounded by plans x statuses rather than by execution volume, so the counts are exact and need no limit. 2. Fabricated fallback. When the executions fetch failed, every plan was assigned HealthScore 100 -- a perfect score, indistinguishable from a measured one, on a number operators make purchasing decisions from. HealthScore is now *int: it stays nil, serializes as null, and the UI renders an explicit "Health: unknown" badge. The request still succeeds; the Plans page is not primarily about execution history. 3. Penalty derived from a missing input. scheduleFactor measured elapsed time from RampSchedule.StartDate without guarding the zero value, so a plan with no start date (JSONB rows predating the field, the same case RampSchedule.GetNextPurchaseDate already guards) was reported ~740000 days behind schedule and docked a real -20. Now skipped, like the StepIntervalDays <= 0 case next to it. Also skips the counts query entirely when the plan list is empty. Regression coverage: the zero-StartDate test fails pre-fix with "expected step 15250 by now"; listPlans asserts health_score serializes as an explicit null on a counts failure and AssertNotCalled pins that the capped page is never consulted; pgxmock asserts the aggregate SQL carries no LIMIT; the frontend asserts a null score renders "unknown" and never badge-success.
Follow-up to the previous commit, from an independent review of it. purchase_executions.plan_id has been nullable since migration 000033: direct-execute purchases from the Recommendations page have no originating plan, and deleting a plan SET NULLs its executions. The new GROUP BY therefore emits a NULL group as soon as one such row is failed or canceled, and scanning it into a plain string returns "cannot scan NULL into *string". That error propagated out of the aggregate, so attachPlanHealth blanked the score for EVERY plan: a single unrelated direct-execute failure would have switched the whole feature to "unknown" permanently. Those rows belong to no plan, so they are now excluded in SQL and the scan goes through sql.NullString as a second guard. scanExecutionRows in the same file already handled the column this way. The counts were also unbounded in time. CleanupOldExecutions purges terminal rows but never "failed" ones, so a plan that failed four times once would have been pinned at -40 for the rest of its life, with no number of subsequent clean runs able to clear it -- a badge saying "action needed" about a plan that needs none. Counts are now bounded to the trailing planHealthLookbackDays (90, matching the retention.execution_history_days default): past that horizon rows are not guaranteed to still exist, so an all-time count isn't a well-defined quantity to begin with. Every factor note names the window, so the number on screen is never ambiguous about what it covers. Also from the review: - a NaN/Infinity score off the wire now renders the "unknown" badge instead of dropping the badge, which reads as "deploy predates the feature"; - the no-LIMIT pgxmock assertion could pass vacuously if the query matcher was never invoked; it now requires the captured SQL to be non-empty first. Regression coverage: the NULL plan_id row test fails pre-fix (the row lands under an empty-string plan key); listPlans asserts the 90-day window reaches the store; pgxmock pins the scheduled_date bound in the SQL; the frontend asserts NaN renders "unknown".
From a second independent review. Two substantive corrections to the lookback window introduced in the previous commit. The window was applied to scheduled_date, which for the terminal statuses it counts points the wrong way. A plan's executions are created up front for the entire ramp, so a pending row carries a scheduled_date months in the future, and CancelExecutionAtomic leaves that date untouched when it flips the row to canceled. `scheduled_date >= since` with no upper bound therefore matched every future-dated cancellation, and the tooltip described a purchase cancelled today as one of "4 canceled execution(s) in the last N days" while pointing at next year's date. The window now uses updated_at, which CancelExecutionAtomic and TransitionExecutionStatus stamp explicitly and the update_purchase_executions_updated_at trigger backstops, so it means what the note says: when the row entered the status being counted. The 90-day constant was also justified by a claim that does not hold. retention.execution_history_days is read by nothing at runtime; the only CleanupOldExecutions caller hardcoded 30. So the window did not match retention at all, and since that sweep deletes canceled rows past the horizon but never failed ones, the two factors were being counted over different spans. Both now use config.DefaultExecutionTTLDays, the existing named constant for execution retention, and the cleanup caller uses it too instead of its own literal, so the two cannot drift apart. Also from the review: - openapi.yaml now documents that the two execution-based factor codes are windowed, and that clients must branch on `code` rather than parse `note`; - the non-finite-score tooltip no longer blames the database, since a garbled number is not evidence of a failed history read; - the NULL plan_id test comment now claims only what pgxmock can prove (a NULL group must not become an empty-string plan key); the production scan-error path is closed by the plan_id IS NOT NULL filter that the sibling test pins; - two doc comments referenced config.ConfigStore, which is not a type. Regression coverage: both pgxmock tests fail pre-fix (the SQL matcher requires updated_at >= $2), and the factor-note tests now assert the window is disclosed rather than leaving a bare count on screen.
45f351e to
8c448ba
Compare
|
Rebased onto Also corrected the PR body, which had drifted from the code: the scoring path now uses a new store method Note for the reviewer: no CodeRabbit review has ever been submitted on this PR (the earlier attempts were rate-limited), so please treat this as a first full pass over the whole diff rather than an incremental one. @coderabbitai full review |
|
✅ Action performedFull review finished. |
|
Rate-limit window has reset; re-requesting the full review. @coderabbitai full review |
|
You're currently rate limited under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. Your next review will be available in 55 minutes. |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
frontend/src/__tests__/plans.test.ts (1)
3087-3135: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCover the exact score-band boundaries.
The samples do not detect changing either inclusive comparison to exclusive. Add assertions for score
80usingbadge-successand score50usingbadge-warning.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/__tests__/plans.test.ts` around lines 3087 - 3135, Extend the health badge tests around the existing healthy and medium cases to explicitly cover scores 80 and 50. Assert that score 80 renders badge-success and score 50 renders badge-warning, preserving the existing score text assertions and test setup patterns.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/src/plans.ts`:
- Around line 909-911: Update the health_score validation in the plan badge
rendering logic to treat finite values below 0 or above 100 as unknown,
alongside null, non-number, NaN, and Infinity. Preserve valid rendering for
scores within the inclusive 0–100 range.
---
Nitpick comments:
In `@frontend/src/__tests__/plans.test.ts`:
- Around line 3087-3135: Extend the health badge tests around the existing
healthy and medium cases to explicitly cover scores 80 and 50. Assert that score
80 renders badge-success and score 50 renders badge-warning, preserving the
existing score text assertions and test setup patterns.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8f5fbe0e-bf91-4be7-98fd-496277a232dd
📒 Files selected for processing (20)
frontend/src/__tests__/plans.test.tsfrontend/src/api/index.tsfrontend/src/api/types.tsfrontend/src/plans.tsfrontend/src/types.tsinternal/analytics/collector_test.gointernal/api/handler_plans.gointernal/api/handler_plans_test.gointernal/api/handler_test.gointernal/api/openapi.yamlinternal/api/plan_health.gointernal/api/plan_health_test.gointernal/api/types.gointernal/config/interfaces.gointernal/config/store_postgres.gointernal/config/store_postgres_pgxmock_test.gointernal/config/types.gointernal/mocks/stores.gointernal/server/handler.gointernal/server/test_helpers_test.go
From CodeRabbit's first review pass. A finite score outside the 0-100 the API contracts to send (openapi.yaml gives health_score minimum 0, maximum 100) was banded like any other number, so -1 painted red and 101 painted green as though both had been measured. An out-of-range value is no more trustworthy than a NaN, and the badge already routes NaN/Infinity to "unknown"; range now joins that guard. The bounds are named constants pointing back at the schema rather than inline literals. Also adds band-boundary coverage, which the suite lacked: the existing cases sampled 90/65/30, all mid-band, so flipping either comparison from >= to > would have silently reclassified every plan sitting exactly on a threshold without failing anything. The new cases pin 80/79 and 50/49. Verified pre-fix: the three out-of-range cases fail against the previous implementation, and the 80 boundary case fails when the good-band comparison is flipped to exclusive.
CodeRabbit pass 1 addressed (
|
|
✅ Action performedReview finished.
|
|
Requesting one full pass over the final head ( @coderabbitai full review |
|
✅ Action performedFull review finished. Your included review limit is currently reached under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 36 minutes. |
|
Included-review window should have reset. Requesting the confirmation pass over final head @coderabbitai full review |
|
✅ Action performedFull review finished. Your included review limit is currently reached under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 55 minutes. |
|
Requesting a full review (not incremental) now that a window may be open. Triggers are serialised centrally across the queue because CodeRabbit is under an account-level adaptive throttle and parallel pings hold it in backoff. @coderabbitai full review |
|
✅ Action performedFull review finished. Your included review limit is currently reached under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 39 minutes. |
|
Requesting a full review (not incremental) now that a window may be open. Triggers are serialised centrally across the queue because CodeRabbit is under an account-level adaptive throttle and parallel pings hold it in backoff. @coderabbitai full review |
|
✅ Action performedFull review finished. Your included review limit is currently reached under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 24 minutes. |
|
Requesting a full review (not incremental) now that a window may be open. Triggers are serialised centrally across the queue because CodeRabbit is under an account-level adaptive throttle and parallel pings hold it in backoff. @coderabbitai full review |
|
✅ Action performedFull review finished. Your included review limit is currently reached under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 4 minutes. |
|
Requesting a full review (not incremental) now that a window may be open. Triggers are serialised centrally across the queue because CodeRabbit is under an account-level adaptive throttle and parallel pings hold it in backoff. @coderabbitai full review |
|
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/server/handler.go`:
- Around line 290-294: Update CleanupOldExecutions so canceled execution rows
are retained based on updated_at, while other statuses continue using
scheduled_date. Add a regression test covering a plan canceled today with a
scheduled date older than the retention window, verifying it is not deleted and
remains available for health scoring.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2a3cf3ec-1fca-47be-a7d4-9c69b4f4a296
📒 Files selected for processing (20)
frontend/src/__tests__/plans.test.tsfrontend/src/api/index.tsfrontend/src/api/types.tsfrontend/src/plans.tsfrontend/src/types.tsinternal/analytics/collector_test.gointernal/api/handler_plans.gointernal/api/handler_plans_test.gointernal/api/handler_test.gointernal/api/openapi.yamlinternal/api/plan_health.gointernal/api/plan_health_test.gointernal/api/types.gointernal/config/interfaces.gointernal/config/store_postgres.gointernal/config/store_postgres_pgxmock_test.gointernal/config/types.gointernal/mocks/stores.gointernal/server/handler.gointernal/server/test_helpers_test.go
| // Shared with the plan health score's lookback window | ||
| // (internal/api/plan_health.go): that score counts failed/canceled | ||
| // executions over exactly the period this sweep guarantees is | ||
| // retained, so the two must never drift apart. | ||
| deleted, err := app.Config.CleanupOldExecutions(ctx, config.DefaultExecutionTTLDays) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Retain canceled executions by updated_at, not scheduled_date.
CleanupOldExecutions deletes canceled rows using scheduled_date, while health counts use updated_at. A plan canceled today with a scheduled date older than 30 days is immediately purged and receives an inflated score. Split the cleanup predicate so canceled statuses retain on updated_at; add a regression case for this scenario.
Proposed cleanup predicate
- status IN ('completed', 'cancelled', 'canceled')
- AND scheduled_date < NOW() - INTERVAL '1 day' * $1
+ status = 'completed'
+ AND scheduled_date < NOW() - INTERVAL '1 day' * $1
+ OR (
+ status IN ('cancelled', 'canceled')
+ AND updated_at < NOW() - INTERVAL '1 day' * $1
+ )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/server/handler.go` around lines 290 - 294, Update
CleanupOldExecutions so canceled execution rows are retained based on
updated_at, while other statuses continue using scheduled_date. Add a regression
test covering a plan canceled today with a scheduled date older than the
retention window, verifying it is not deleted and remains available for health
scoring.
There was a problem hiding this comment.
Confirmed and fixed in d69ed8f, with a follow-up in 9166bc5. Leaving this thread open for you to re-review rather than resolving it.
The predicate is now split as you proposed, with the branches fully parenthesized so the AND/OR precedence cannot shift:
WHERE ( status = 'completed'
AND scheduled_date < NOW() - INTERVAL '1 day' * $1 )
OR ( status IN ('cancelled', 'canceled')
AND updated_at < NOW() - INTERVAL '1 day' * $1 )
OR ( status <> ALL($2)
AND expires_at IS NOT NULL
AND expires_at < NOW() - INTERVAL '1 day' * $1 )Two things go beyond the proposed diff, both found while verifying it.
The expires_at branch reintroduced the same purge. expires_at is written once at insert and never rewritten when a row later reaches a terminal status, so a row canceled today can carry a long-lapsed expires_at and be deleted by that branch on the same sweep, while the score is still counting it. Splitting only the first branch would have left the defect reachable, so branch 3 now skips the statuses the score counts.
It is not only canceled. The score also counts failed, at -10 each up to -40, which is twice canceled's worst case, so failed had the larger version of the same bug. Rather than maintain a second literal status list that can drift from the score's, the exclusion is parameterized as $2 from config.HealthScoredExecutionStatuses, the same slice plan_health.go passes to CountExecutionsByPlanAndStatus. Adding a status to the score now extends the sweep's exclusion in the same edit.
One correction to my own earlier claim, for the record: the expires_at arm is not reachable on rows current code writes. That column has exactly one writer, SavePurchaseExecution via timeFromTTL(execution.TTL), and no production path assigns PurchaseExecution.TTL, so it is NULL on every row created today and the branch is inert for them. The exclusion is defence in depth for legacy imported rows. Whether any such row carries a non-NULL expires_at needs a production query; it cannot be settled from code.
Regression coverage, as you asked for. The behavioural proof is an integration test against a real PostgreSQL rather than pgxmock, because the defect is in what the predicate means, not how it is spelled:
go test -tags=integration ./internal/config/ \
-run TestPostgresStoreDB_CleanupOldExecutions_RetainsCanceledInsideHealthWindow
The -tags=integration is load-bearing: without it the file is excluded by its build tag and the run reports "no tests to run" and exits 0, which looks like a pass.
It seeds nine rows across the status x timestamp matrix and asserts exactly which survive a 30-day sweep. Confirmed failing before each fix: pre-d69ed8fd the database deleted all three rows that must be retained (canceled-today with a 40-day-old scheduled_date, the same row with a lapsed expires_at, and the legacy spelling); pre-9166bc5d it deleted the failed-today row, which the suite had no case for at all. It also asserts that a genuinely old canceled row IS still deleted and that RowsAffected matches, so neither fix can pass by quietly disabling retention.
A user-visible consequence worth flagging on this thread: a canceled row with a future scheduled_date was previously retained forever, since scheduled_date < NOW() - retention never became true for it. It now leaves History 30 days after the cancellation instead of 30 days after the date it was scheduled for. That direction closes an unbounded-retention leak and matches what the health window already assumed, but it is a change in behaviour rather than a pure bug fix.
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Relevant files =="
fd -t f . internal | rg '(store_postgres|plan_health|execution.*cleanup|types|integration).*\.go$|interfaces\.go$' || true
echo
echo "== Current PR changes relevant to cleanup/health =="
git diff --name-only HEAD^ HEAD -- internal/config internal/api internal/server || true
git status --short
echo
echo "== Symbol locations =="
rg -n -C 4 '\b(CleanupOldExecutions|HealthScoredExecutionStatuses|CountExecutionsByPlanAndStatus|computePlanHealth)\b' internal \
-g '*.go' || trueLength of output: 67957
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Cleanup contract and predicate =="
sed -n '1698,1818p' internal/config/store_postgres.go
echo
echo "== Shared scored-status definition and score consumer =="
sed -n '396,432p' internal/config/types.go
sed -n '52,106p' internal/api/plan_health.go
echo
echo "== Integration regression test =="
sed -n '1250,1455p' internal/config/store_postgres_db_test.go
echo
echo "== Existing cleanup tests and failed-status retention references =="
rg -n -C 5 '(CleanupOldExecutions|StatusFailed|status = .failed.|failed.*expires_at|expires_at.*failed)' \
internal/config -g '*test.go' -g '*.go' | head -n 500Length of output: 43682
@cristim, re-reviewed and confirmed: the finding is addressed.
- Canceled executions now retain based on
updated_at, with both canonical and legacy status spellings covered. - The
expires_atbranch excludes the exact sharedconfig.HealthScoredExecutionStatusesslice that plan health queries, includingfailed, so it cannot purge a recently scored execution through the alternate path. - The PostgreSQL integration test exercises the relevant survivor/deletion matrix—including the stale-
expires_atand failed-execution cases—and verifiesRowsAffected.
The intentionally unbounded retention of failed rows is documented and consistent with the existing cleanup/health contract; health scoring itself remains limited to the shared 30-day updated_at window. I found no remaining issue in this thread.
🧬 Code Graph Analysis Results
internal/server/handler.go
Lines 180-224 (cleanup handler; explains coupling with plan-health lookback / retention window):
// handleCleanupExpiredRecords cleans up expired sessions and execution records.
//
// contract for the handler family registered in the task dispatch map; error is
// reserved for the failure modes the sibling handlers already surface.
//
//nolint:unparam // scheduled-task handler: (result, error) shape is the shared
func (app *Application) handleCleanupExpiredRecords(ctx context.Context) (map[string]int64, error) {
log.Println("Cleaning up expired records...")
result := map[string]int64{
"sessions_deleted": 0,
"executions_deleted": 0,
}
// Clean up expired sessions via auth service
if app.Auth != nil {
if err := app.Auth.CleanupExpiredSessions(ctx); err != nil {
log.Printf("Warning: failed to cleanup expired sessions: %v", err)
} else {
log.Println("Expired sessions cleaned up successfully")
}
}
// Clean up old execution records (30+ days)
if app.Config != nil {
// Shared with the plan health score's lookback window
// (internal/api/plan_health.go): that score counts failed/canceled
// executions over exactly the period this sweep guarantees is
// retained, so the two must never drift apart.
deleted, err := app.Config.CleanupOldExecutions(ctx, config.DefaultExecutionTTLDays)
if err != nil {
log.Printf("Warning: failed to cleanup old executions: %v", err)
} else {
result["executions_deleted"] = deleted
log.Printf("Cleaned up %d old execution records", deleted)
}
}
log.Printf("Cleanup complete: %d sessions, %d executions deleted", result["sessions_deleted"], result["executions_deleted"])
return result, nil
}internal/config/constants.go
Line 15 (retention default used by the cleanup handler):
DefaultExecutionTTLDays = 30internal/config/store_postgres.go
Lines 1782-1816 (the SQL DELETE predicate used by CleanupOldExecutions; parameters and status/TTL logic):
func (s *PostgresStore) CleanupOldExecutions(ctx context.Context, retentionDays int) (int64, error) {
// Branch 2 lists both spellings: 'cancelled' (legacy, pre-migration
// 000089 rows) and 'canceled' (canonical, written by new code after the
// follow-up to `#1277`). `#1278` will drop 'cancelled' once all rows are
// normalized.
//
// Branch 3's exclusion is parameterized from
// HealthScoredExecutionStatuses instead of repeating a literal list, so
// a status added to the plan health score extends this exclusion
// automatically and the two cannot drift. `status` is NOT NULL
// (migration 000001), so `<> ALL` is not exposed to three-valued logic.
query := `
DELETE FROM purchase_executions
WHERE (
status = 'completed'
AND scheduled_date < NOW() - INTERVAL '1 day' * $1
)
OR (
status IN ('cancelled', 'canceled')
AND updated_at < NOW() - INTERVAL '1 day' * $1
)
OR (
status <> ALL($2)
AND expires_at IS NOT NULL
AND expires_at < NOW() - INTERVAL '1 day' * $1
)
`
result, err := s.db.Exec(ctx, query, retentionDays, HealthScoredExecutionStatuses)
if err != nil {
return 0, fmt.Errorf("failed to cleanup old executions: %w", err)
}
return result.RowsAffected(), nil
}✏️ Learnings added
Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 1521
File: internal/server/handler.go:290-294
Timestamp: 2026-07-28T19:29:54.359Z
Learning: In the Go cleanup implementation at `internal/config/store_postgres.go`, `CleanupOldExecutions` intentionally never deletes executions whose status is `config.StatusFailed`. Plan health scoring in `internal/api/plan_health.go` counts failed executions only within `config.DefaultExecutionTTLDays` using `updated_at`; this bounded query window prevents old failures from affecting current health even though the records are retained.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
CleanupOldExecutions retained canceled rows on scheduled_date while
CountExecutionsByPlanAndStatus counts them on updated_at, and
CancelExecutionAtomic stamps updated_at while leaving scheduled_date
untouched. A plan's executions are created up front and
parseCreatePurchasesRequest validates only the date format, not that
the start date is in the future, so a purchase canceled today can carry
a scheduled_date from 40 days ago.
The next retention sweep deleted that row even though the health score
was still counting it, so the plan's badge silently gained up to 20
points overnight with no operator action and no way to tell what
changed. The expires_at branch reintroduced the same purge through a
sibling path: expires_at is fixed when the approval window opens and is
never rewritten on cancel, so a notified row whose token expired 40 days
ago and that an operator cancels today was purged the same day.
Split the cleanup predicate into three fully parenthesized branches:
completed retains on scheduled_date, the canceled spellings retain on
updated_at, and the expires_at branch now skips canceled rows so the
updated_at window governs them alone. Retention is unchanged for every
other status, and a genuinely old canceled row still has an old
updated_at, so nothing accumulates indefinitely.
Also clamp the behind_schedule expected step to TotalSteps. Elapsed
time keeps growing past the ramp's full duration, so the tooltip quoted
a step the plan does not have ("on step 2, expected step 12 by now" for
a 4-step plan). The penalty was always correct; the number shown to the
operator was fabricated by the arithmetic. The comparison and the note
are clamped together so they cannot desync.
Regression guards, each confirmed failing before the fix:
- an integration test against a real PostgreSQL asserting exactly which
rows survive a sweep. Pre-fix the database deleted all three rows
that must be retained (canceled-today with an old scheduled_date,
the same row with an expired token, and the legacy spelling), while
still asserting a genuinely old canceled row IS deleted so the fix
does not simply disable retention
- a pgxmock guard pinning each branch's retention column in the fast
unit suite
- note-text assertions on the clamped expected step, which the existing
tests over the same arithmetic never made
Addressed in d69ed8fFixed: retention purged canceled rows inside the health window (CR thread on
|
🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- changed files ---'
git diff --name-status origin/main...HEAD
printf '%s\n' '--- relevant diff ---'
git diff --unified=80 origin/main...HEAD -- \
internal/server/handler.go \
internal/server/handler_test.go \
internal/server/handler_integration_test.go \
internal/api/plan_health.go \
internal/api/plan_health_test.go \
internal/config/store_postgres_pgxmock_test.go \
internal/config/store_postgres_test.go
printf '%s\n' '--- candidate files ---'
fd -t f -a '.*(handler|plan_health|store_postgres).*_test\.go$|handler\.go$|plan_health\.go$' internal | sort
printf '%s\n' '--- code outlines ---'
ast-grep outline internal/server/handler.go --items all --type function --match 'CleanupOldExecutions|handleCleanupExpiredRecords' || true
ast-grep outline internal/api/plan_health.go --items all --type function --match 'scheduleFactor|computePlanHealth' || true
printf '%s\n' '--- focused symbol occurrences ---'
rg -n -C 5 -e '\bCleanupOldExecutions\s*\(' -e '\bscheduleFactor\s*\(' -e 'expectedStep' -e 'RetainsCanceled' internalLength of output: 78880 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- CleanupOldExecutions implementation ---'
sed -n '1725,1805p' internal/config/store_postgres.go
printf '%s\n' '--- retention integration test ---'
sed -n '1250,1435p' internal/config/store_postgres_db_test.go
printf '%s\n' '--- cancellation/transition timestamp writers ---'
rg -n -C 8 -e '\bCancelExecutionAtomic\s*\(' -e '\bTransitionExecutionStatus\s*\(' internal/config internal/purchase
printf '%s\n' '--- ramp schedule declarations and validation ---'
rg -n -C 8 -e 'type RampSchedule\b' -e 'TotalSteps' -e 'StepIntervalDays' internal/config | head -n 360
printf '%s\n' '--- commit-local changes for the reported fix ---'
git show --format=fuller --stat d69ed8fd
git show --format= --unified=30 d69ed8fd -- \
internal/config/store_postgres.go \
internal/config/store_postgres_db_test.go \
internal/config/store_postgres_pgxmock_test.go \
internal/api/plan_health.go \
internal/api/plan_health_test.goLength of output: 99386
No additional actionable findings in this update. ✏️ Learnings added
✅ Action performedReview finished.
|
|
Re-requesting review: the latest commit on this PR postdates your last review, so that review does not cover the current HEAD. Fixes landed since then were verified locally (build, tests, vet, gocyclo, and golangci-lint at the CI-pinned version), and CI on this PR is green across all workflows. Please review the current HEAD. @coderabbitai review |
|
✅ Action performedReview finished.
|
The expires_at branch of CleanupOldExecutions excluded only the canceled
spellings, but the plan health score also counts `failed`, at -10 each up
to -40, which is twice canceled's worst case. expires_at is written once
at insert and never rewritten when a row later reaches a terminal status,
so a row that failed today can carry a lapsed expires_at and was deleted
by that branch the same day, while the score was still counting it. That
is the same overnight score jump this PR exists to fix, one status over.
Source the exclusion from a single exported slice,
config.HealthScoredExecutionStatuses, consumed by plan_health.go for the
statuses it counts and passed to the DELETE as $2 for the statuses it
must not reap. Adding a status to the score now extends the sweep's
exclusion in the same edit rather than relying on someone remembering to
update a second literal list. `failed` deliberately still has no cleanup
branch of its own, which is what plan_health.go's lookback comment
already claims ("never deletes failed ones").
Correct three inaccurate claims in the same code:
- expires_at is the row's own TTL column, not the approval-token
deadline. The token deadline is approval_token_expires_at, a separate
column added by migration 000051.
- The doc said no transition code writes an 'expired' status and the
status CHECK excludes it. Both are false: migration 000089's
purchase_executions_status_check lists 'expired', and
expireStaleExecutionsSweep transitions pending/notified to it.
Behaviourally harmless, since an 'expired' row is unscored and so
still governed by the expires_at branch, but it was the stated
justification for that branch existing.
- expires_at has exactly one writer, SavePurchaseExecution via
timeFromTTL(execution.TTL), and no production path assigns
PurchaseExecution.TTL, so rows written by current code leave it NULL
and this branch is inert for them. The exclusion is defence in depth
for legacy imported rows, not a fix for a scenario reachable today;
the doc now says so instead of implying otherwise.
Also document a user-visible consequence of the previous commit: a
canceled row with a FUTURE scheduled_date used to be retained forever,
because `scheduled_date < NOW() - retention` was never true for it. It
now leaves History `retention` days after the cancellation, so a
purchase canceled today for a 2-year-out date disappears in a month
rather than in two years. That closes an unbounded-retention leak and
matches what the health window already assumed.
Regression guards, both confirmed failing before this change:
- a `failed` row with updated_at today and a 40-day-lapsed expires_at
must survive the sweep. The suite previously had no failed-row case at
all, so it stayed green with the defect present
- a pgxmock guard asserting the $2 argument IS
HealthScoredExecutionStatuses, not merely that the query says
`<> ALL($2)`, so passing a narrower list fails the build
Two existing pgxmock cases updated for the second query argument.
Round 2 — 9166bc5Independent verification of The The exclusion is now parameterized as Corrected an overstatement in my own earlier description. I presented the Two stale doc claims fixed. Behaviour change now disclosed in the PR body: a canceled row with a future Regression guards, both confirmed failing firstThe Verify with the tag — without it the file is excluded by its build tag and the run exits 0 having executed nothing: go test -tags=integration ./internal/config/ \
-run TestPostgresStoreDB_CleanupOldExecutions_RetainsCanceledInsideHealthWindowGates
Two existing pgxmock cases were updated for the second query argument. @coderabbitai full review |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- HEAD and PR diff summary ---'
git rev-parse --short HEAD
git status --short
git diff --name-status origin/main...HEAD
git log --oneline origin/main..HEAD
printf '%s\n' '--- relevant commit diff (if present) ---'
git show --format=fuller --stat 9166bc5d || true
git show --format= --unified=50 9166bc5d -- \
internal/config/store_postgres.go \
internal/config/store_postgres_pgxmock_test.go \
internal/config/store_postgres_db_test.go \
internal/api/plan_health.go \
internal/api/plan_health_test.go \
internal/config/types.go \
internal/config/interfaces.go || true
printf '%s\n' '--- symbol outlines ---'
ast-grep outline internal/config/store_postgres.go --items all --type function --match 'CleanupOldExecutions|SavePurchaseExecution|CountExecutionsByPlanAndStatus' || true
ast-grep outline internal/api/plan_health.go --items all --type function --match 'computePlanHealth|planHealthExecutionStatuses' || true
ast-grep outline internal/config/types.go --items all --type type,variable --match 'HealthScoredExecutionStatuses|PurchaseExecution' || true
printf '%s\n' '--- focused definitions, usage, and test locations ---'
rg -n -C 8 \
-e '\bHealthScoredExecutionStatuses\b' \
-e '\bCleanupOldExecutions\s*\(' \
-e '\bCountExecutionsByPlanAndStatus\s*\(' \
-e '\bexpires_at\b' \
-e '\btimeFromTTL\s*\(' \
-e '\bexpireStaleExecutionsSweep\s*\(' \
internal
printf '%s\n' '--- relevant test names ---'
rg -n -C 4 \
-e 'RetainsCanceledInsideHealthWindow' \
-e 'RetainsCanceledOnUpdatedAt' \
-e 'HealthScoredExecutionStatuses' \
internal/configLength of output: 201862 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- current PR commit/file summary ---'
git rev-parse --short HEAD
git diff --stat origin/main...HEAD
git diff --name-only origin/main...HEAD
printf '%s\n' '--- status-list declarations and every relevant call site ---'
rg -n -C 6 \
-e '^\s*(var|const)\s+planHealthExecutionStatuses\b' \
-e '^\s*(var|const)\s+HealthScoredExecutionStatuses\b' \
-e '\bCountExecutionsByPlanAndStatus\s*\(' \
-e '\bCleanupOldExecutions\s*\(' \
internal/api internal/config internal/server internal/mocks
printf '%s\n' '--- exact implementation regions ---'
sed -n '1,115p' internal/api/plan_health.go
sed -n '45,100p' internal/api/handler_plans.go
sed -n '1215,1275p' internal/config/store_postgres.go
sed -n '1710,1820p' internal/config/store_postgres.go
sed -n '385,435p' internal/config/types.go
printf '%s\n' '--- exact current incremental diff ---'
git diff --unified=30 d69ed8fd..HEAD -- \
internal/config/store_postgres.go \
internal/config/store_postgres_pgxmock_test.go \
internal/config/store_postgres_db_test.go \
internal/config/types.go \
internal/api/plan_health.go \
internal/api/handler_plans.go
printf '%s\n' '--- all direct cleanup callers, excluding declarations and test names ---'
rg -n -P '\.\s*CleanupOldExecutions\s*\(' \
-g '*.go' \
-g '!**/*_test.go' \
internalLength of output: 87604 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- diff hygiene ---'
git diff --check origin/main...HEAD
git diff --find-conflicts origin/main...HEAD || true
printf '%s\n' '--- changed production symbol map ---'
ast-grep outline internal/api/handler_plans.go --items all --type function --match 'listPlans|attachPlanHealth' || true
ast-grep outline internal/api/plan_health.go --items all --type function --match 'computePlanHealth|collectPlanHealthFactors|overdueFactor|failedExecutionsFactor|canceledExecutionsFactor|scheduleFactor|disabledMidwayFactor' || true
ast-grep outline frontend/src/plans.ts --items all --type function --match 'healthBadgeHtml|renderPlanCard' || true
printf '%s\n' '--- API/data-model and frontend rendering slices ---'
sed -n '1,135p' internal/api/types.go
rg -n -C 8 -e '^ PlanWithHealth:' -e '^ PlanHealthFactor:' -e '^ PlansResponse:' internal/api/openapi.yaml
sed -n '1,230p' frontend/src/plans.ts
sed -n '1,150p' frontend/src/api/types.ts
sed -n '1,130p' frontend/src/types.ts
printf '%s\n' '--- health-score test case index and assertions ---'
rg -n -C 3 \
-e '^func TestComputePlanHealth' \
-e '^func TestHandler_listPlans_Health' \
-e "describe\\('plan health-score badge'" \
internal/api/plan_health_test.go \
internal/api/handler_plans_test.go \
frontend/src/__tests__/plans.test.tsLength of output: 46119
No additional actionable findings. Confirmed in particular:
🐇🔎 [full_review] ✏️ Learnings added
|
Summary
Revives the per-plan health-score badge from PR #376 (closed stale,
never merged), implemented fresh against current
main. Adds a 0-100"Plan health" badge to every purchase plan card on the Plans tab.
Green >= 80, amber 50-79, red < 50. The badge's tooltip enumerates
exactly which factors subtracted from 100 so a bad score is actionable
instead of opaque.
Historical context: issue #340 (closed, unrelated broader UI-revamp
scope) is where PR #376's body originally noted this as a deferred
follow-up idea. This PR does not close either #340 or #376 -- #340 is
out of scope and already closed, and #376 is a PR, not an issue.
Scoring
Pure backend function
computePlanHealth(internal/api/plan_health.go).Starts at 100, subtracts weighted penalties, clamps to [0, 100]:
overduenext_execution_dateis in the pastfailed_executionscanceled_executionsstalledcurrent_step == 0behind_schedulecurrent_steplags the step implied by start_date + intervaldisabled_midway0 < current_step < total_stepsA completed plan (
RampSchedule.IsComplete(), guarded against thedegenerate
total_steps == 0case) short-circuits to 100: post-completionfailure rows and a toggle-off are expected end-of-life noise, not
something an operator should chase.
Scoring details worth calling out:
whose
updated_atfalls within the trailingplanHealthLookbackDays,which is deliberately the same constant as
config.DefaultExecutionTTLDays(30 days) that the execution-retentionsweep uses. That sweep deletes canceled rows past the horizon but never
failed ones, so counting over any longer span would measure the two
factors over different periods and permanently penalize a plan for
failures years ago that no number of clean runs could clear. Every
factor note names the window, so the number on screen is unambiguous.
stalledandbehind_scheduleare mutually exclusive -- at mostone of the pair ever applies.
step_interval_days <= 0(immediate ramps have no schedule position to fall behind on, and this
is the only divisor) or when
start_dateis zero (measuring elapsedtime from a zero
time.Timewould fabricate a ~740000-day penalty outof a missing input;
RampSchedule.GetNextPurchaseDateguards the samefield the same way).
config.StatusCanceledandconfig.LegacyStatusCanceled) so pre-#1278 rows are not invisible toscoring.
Implementation notes
config.StoreInterface.CountExecutionsByPlanAndStatus(ctx, statuses, since)returns exact per-plan counts via
GROUP BY plan_id, status. It isdeliberately NOT derived from
GetExecutionsByStatuses: that method iscapped by
limitacross ALL plans, so counting per-plan rows out of itsilently understates any plan whose executions fall outside the newest
page -- rendering a confidently-healthy badge for an unhealthy plan. The
result set is bounded by plans x statuses, not by execution volume.
updated_at, notscheduled_date. Executions arecreated up front for the whole ramp, so a pending row carries a
scheduled_datemonths in the future, andCancelExecutionAtomicleaves that date untouched. Windowing on
scheduled_datewould count apurchase canceled today under a date next year.
updated_atis stampedby
CancelExecutionAtomic/TransitionExecutionStatusand backstoppedby the
update_purchase_executions_updated_attrigger, so it is whenthe row actually entered the counted status.
plan_idrows excluded.plan_idhas been nullable sincemigration 000033 (direct-execute purchases from the Recommendations
page have no originating plan; deleting a plan SET NULLs its
executions). Those rows belong to no plan and are filtered in SQL; the
scan still goes through
sql.NullStringso a NULL can never turn into ascan error that blanks the score for every plan.
the counts fetch fails,
health_scoreis left nil, serializes as"health_score": null, and renders as an explicit "unknown" badge. Ascore is a number operators make purchasing decisions from, so an
uncomputable one must stay absent -- substituting 100 ("healthy") or 0
("broken") would be indistinguishable from a real value. The request
itself still succeeds; the Plans page is not primarily about execution
history, so a 500 would be the worse trade.
PlanWithHealthresponse envelope wrapsconfig.PurchasePlanwith
health_score(nullable) /health_factors, so neither fieldpersists into the DB-backed struct.
response) -> no badge at all;
nullor non-finite -> explicit unknownbadge; a number -> colored badge.
titleattribute) is built fromescapeHtmlAttr-escaped factor notes, per this repo'sall-API-fields-through-an-escaper convention.
status-badge/badge-success/badge-warning/badge-dangerCSS classes already used by the Overdue badge on thesame card -- no new CSS needed.
Test plan
Verified on the rebased head (branch rebased onto
mainto pick up thegovulncheck fix for GO-2026-6061, grpc v1.80.0 -> v1.82.1; the rebase was
clean, no conflicts, and touched no dependency files):
gofmt -l .(exit 0, no output)go vet ./...(exit 0)go build ./...(exit 0)go test ./internal/api/... ./internal/config/... ./internal/mocks/... ./internal/server/... ./internal/analytics/... -count=1(exit 0)gocyclo -over 10on every touched non-test Go file (exit 0, noviolations -- this repo's pre-commit gate is stricter than
golangci's min-complexity 15)
cd frontend && npx tsc --noEmit(exit 0)cd frontend && npx jest(exit 0; 86/86 suites, 2736 passed, 1skipped). Note: under a
de_DEshell locale, 8 currency-formattingtests in
utils.test.ts/approval-details.test.ts/riexchange.test.tsfail on the thousands separator ($1.000vs$1,000). That is anIntl.NumberFormatlocale artifact in filesthis branch does not touch; under
LC_ALL=en_US.UTF-8(what CIruns) the full suite is green.
Retention / health-window fixes (d69ed8f, 9166bc5)
The score counts execution rows over a 30-day window on
updated_at, so theretention sweep must not delete a row while that window still covers it. It did.
CleanupOldExecutionspurged rows inside their own health window. Itretained canceled rows on
scheduled_date, whileCountExecutionsByPlanAndStatuscounts them on
updated_at, andCancelExecutionAtomicstampsupdated_atwhile leaving
scheduled_dateuntouched. A plan's executions are created upfront and
parseCreatePurchasesRequestvalidates only the date format, notthat the start date is in the future, so a purchase canceled today can carry a
40-day-old
scheduled_date. The next sweep deleted it while the score was stillcounting it, and the plan's badge gained up to 20 points overnight with nothing
having changed about the plan.
The same purge was reachable through the
expires_atbranch, and not only forcanceled: the score also counts
failed, at -10 each up to -40.expires_atiswritten once at insert and never rewritten when a row later reaches a terminal
status, so a row that failed or was canceled today can carry a lapsed
expires_atand be deleted by that branch on the same sweep.The predicate is now three fully parenthesized branches:
completedretains onscheduled_date, the canceled spellings retain onupdated_at, and theexpires_atbranch excludes every status the score counts. That exclusion isparameterized from
config.HealthScoredExecutionStatuses, the same sliceplan_health.gopasses toCountExecutionsByPlanAndStatus, so a status addedto the score extends the sweep's exclusion in the same edit instead of drifting
from a second literal list.
behind_schedulequoted a step the plan does not have.expectedStepwasnever clamped to
TotalSteps, so a 4-step weekly ramp started 90 days agorendered "on step 2, expected step 12 by now". The -20 penalty was correct; the
number the operator was asked to reconcile against was fabricated by the
arithmetic.
Reachability of the
expires_atarm - correcting an earlier claimAn earlier version of this description presented the
expires_atarm as a livedefect. That overstates it.
purchase_executions.expires_athas exactly onewriter,
SavePurchaseExecutionviatimeFromTTL(execution.TTL), and noproduction path assigns
PurchaseExecution.TTL, so the column is NULL on everyrow current code creates and the branch is inert for them. The exclusion is
defence in depth, not a fix for a scenario reachable today.
Whether legacy DynamoDB-era imported rows carry a non-NULL
expires_atis anopen question that needs a production query; it cannot be settled from code, and
this PR asserts nothing either way. Note also that
expires_atis the row's ownTTL column, not the approval-token deadline - that is
approval_token_expires_at,a separate column added by migration 000051. The doc comment said otherwise and
has been corrected, along with a stale claim that no code writes an
'expired'status (migration 000089's CHECK lists it, and
expireStaleExecutionsSweepwrites it).
Behaviour change worth knowing about
A canceled row with a future
scheduled_datewas previously retainedforever, because
scheduled_date < NOW() - retentionnever became true for it.It now leaves History 30 days after the cancellation rather than 30 days after
the date it was scheduled for, so a purchase canceled today for a 2-year-out
date disappears in a month instead of in two years. The direction is right - it
closes an unbounded-retention leak and matches what the health window already
assumed - but it is user-visible rather than a pure bug fix.
failedrows are deliberately still never deleted by any branch, which is whatplan_health.go's lookback comment already claimed and the score's windowdepends on.
Verifying the retention fix
The behavioural proof is an integration test against a real PostgreSQL, because
the defect is in what the predicate means, not how it is spelled:
go test -tags=integration ./internal/config/ \ -run TestPostgresStoreDB_CleanupOldExecutions_RetainsCanceledInsideHealthWindow-tags=integrationis load-bearing. Without it the file is excluded by itsbuild tag, the run reports "no tests to run" and exits 0, and that false green is
indistinguishable from a pass.
It seeds nine rows across the status x timestamp matrix and asserts exactly which
survive a 30-day sweep, in both directions. Confirmed failing before each fix:
pre-
d69ed8fdthe database deleted all three rows that must be retained;pre-
9166bc5dit deleted thefailed-today row, which the suite had no case forat all. It also asserts a genuinely old canceled row IS still deleted and that
RowsAffectedmatches, so neither fix can pass by quietly disabling retention.Unit-level guards:
TestPGXMock_CleanupOldExecutions_RetainsCanceledOnUpdatedAtpins each branch's retention column, and
TestPGXMock_CleanupOldExecutions_ExcludesEveryHealthScoredStatuspins the$2argument itself so a narrower list fails the build.
TestComputePlanHealth_BehindScheduleNoteClampsExpectedStepToTotalStepsassertsthe rendered note text, which the pre-existing test over the same arithmetic
never did.
Not changed
Executions can reach
partially_completed, which the score counts as neitherfailed nor canceled, so a plan whose executions consistently partially fail still
scores 100. That is a scoring-policy decision rather than a correctness bug and is
left for a separate call.
Summary by CodeRabbit