Repository navigation
fix(api): keep an equivalent ramp when editing a non-preset weekly/monthly plan - #1198
Conversation
…nthly plan The UI can send only the two presets or "custom", so Edit resubmits a stored non-preset weekly or monthly ramp (e.g. weekly 20%/7d, created through the API) as the equal custom one. rampScheduleShapeUnchanged compared the type label, so that counted as a ramp change: an in-progress plan was refused with 409 even for a bare rename, and a not-started one had its type rewritten to custom and its start and next-execution dates rebuilt. weekly, monthly and custom are labels for the same step-based behavior; only "immediate" is branched on downstream. Treat them as equivalent when percent, interval and step count match, and keep the stored type along with the progress fields. "immediate" and unknown labels still have to match exactly, and a real change of percent, interval or step count is still refused once a step has run. Closes LeanerCloud#1184 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reached
This review includes 2 billable files and costs up to $0.50. Or wait 58 minutes for your next included review. View limit details
Comment |
|
Independent review 2 (claude-fleet, cold): APPROVE 00e7430 Root cause of #1184:
Findings (none blocking):
Attacked:
Verified locally, on
Reviewed by an independent agent with no author context (second reviewer). Review only: nothing was pushed, and the GitHub review state is unchanged. |
|
VERDICT: APPROVE 00e7430 Rebase check: Findings
Equivalence and money safety
Attacked
|
|
Local verification at
|
|
Marked ready and merged by IAmJSD's agent, at IAmJSD's request, after 2 independent reviews and local verification (see the review and verification comments above, all at |
Root cause
The UI can only send the two presets or
custom. Since #1183, Edit pre-fills a plan whose stored ramp is a non-presetweeklyormonthly(for example weekly 20%/7d, created through the API) as Custom with the same percent and interval, so Save sendstype: custom, 20%, 7 days.rampScheduleShapeUnchanged(internal/api/handler_plans.go) compared the type label, soweeklyvscustomcounted as a ramp change even with identical percent, interval and step count:CurrentStep > 0:preserveRampProgressrefused the update with 409 "cannot change the ramp schedule", even for a bare rename.CurrentStep == 0: the save went through, but the stored type becamecustomandStartDate/NextExecutionDatewere rebuilt from "now". The type change also made the store'srampBaselineStale(which includes the type) clear the plan's ramp baseline.Fix
The issue offers two options: match shapes whatever the type label is, or keep the stored type when an equal custom ramp is submitted. This PR does both, limited to the step-based types:
rampTypesEquivalent:weekly,monthlyandcustomcount as the same type. They only label the same step-based behavior; the only ramp type anything downstream branches on isimmediate(CumulativePercent,GetCurrentCoverage, next-execution calculation, step validation).immediateand any unknown label still have to match exactly.preserveRampProgressnow keeps the storedTypealong withCurrentStep,StartDate,NextExecutionDate,DatesManagedandAllocationMode. Resubmitting an equal ramp leaves the stored schedule exactly as it was, in progress or not, and the ramp baseline is no longer cleared for nothing.immediateand a step-based type.The change is in the handler only: no store, schema or migration change, and no frontend change. The update keeps its existing
expected_updated_atoptimistic check and store path, so this adds no new concurrency window.Verification
Unit tests with a mocked store, driving
PUT /api/plans/{id}throughHandleRequest(internal/api/handler_plans_ramp_shape_test.go):TestUpdatePlan_EquivalentCustomRampKeepsStoredSchedulecovers 5 cases: in-progress weekly 20%/7d (step 3) and monthly 5%/30d (step 1) renamed viacustom, an in-progress custom 25%/7d renamed via the equalweekly-25pctpreset, and not-started (step 0) weekly 20% and monthly 5% renamed viacustom. Each case asserts 200 and that the savedRampScheduleequals the stored one (type, step, start date and all), plus the sameNextExecutionDate.TestUpdatePlan_RealRampChangeOnNonPresetStillRefusedcovers 6 cases on in-progress plans, each expecting 409 with nothing reaching the store: different percent and step count, different interval, different percent with the same step count (30% vs 33%), weekly to immediate, immediate to custom 100%, and custom 100%/0d/1 step to immediate (the same numbers as immediate, so only the type guard refuses it).TestUpdatePlan_RampShapeChangestill passes unchanged.Fail before, pass after:
origin/main(7e6ec71) with only the new tests added, all 5EquivalentCustomRampKeepsStoredSchedulecases fail: the 3 in-progress cases get 409 instead of 200, and the 2 step-0 cases getType:"custom"with a rebuilt start date. The 6 refusal cases pass, since they guard behavior that must not regress.Mutation probes, each reverted afterwards:
Typecarry-over fails all 5 equivalence cases.rampTypesEquivalentalways return true fails "custom 100% to immediate".Commands, all run with
AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/nullbecause this machine's default AWS profile is invalid:All of this is unit or mock evidence. No real Postgres or cloud run was needed, since the change is pure handler logic over the request and the stored plan.
Out of scope / notes
internal/api/handler_plans.gowas already 851 lines on main, over the 500-line guideline; this PR adds 23. I didn't split it here.weekly/monthlyplan resubmitted with a different custom shape before its first step still becomescustom, as before. That is a genuine schedule change.Frontend follow-up for the UI owner
None needed. With #1183 the Edit form already sends the equal custom ramp, and the backend now accepts it and keeps the stored type.
Closes #1184
Maintainers: please mirror the issue's triage labels onto this PR:
severity/medium,triaged,priority/p3,urgency/this-quarter,impact/few,effort/s,type/bug.🤖 Generated with Claude Code