Conversation
The Form Type, Employment Status, and Pay Type selects briefly flickered disabled whenever any other field (e.g. Benefits) was being saved, because they were gated on the global isMutating flag. Track saving state per field via trackFieldMutation so a select only disables while its own save is in flight — preserving the same-field race protection without affecting unrelated fields. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bundle sizes [mpdx-react]Compared against fdfd564 No significant changes found |
wjames111
left a comment
There was a problem hiding this comment.
Multi-Agent Code Review — VERDICT: BLOCKERS_FOUND
6 agents · Architecture · Data Integrity · Testing · UX · Standards · Financial Reporting
Top blocker
Cross-field stale-write race (consensus from Data Integrity, UX, Financial Reporting). The PR narrows the in-flight save guard from "any mutation in flight" to "same field in flight," but UpdatePdsGoalCalculation responses carry the full PdsGoalCalculationFields fragment. A second save's response can land after a first save's, stamping stale field values (notably formType) into the Apollo cache — the exact silent goal-total miscalculation the in-line comment warns about. See inline comment on usePdsGoalAutoSave.ts.
Risk assessment
- Risk: 5/10 — MEDIUM
- Reviewer level recommended: mid-level / senior
Findings summary
| Tier | Count |
|---|---|
| Critical (9-10) | 0 |
| High Priority Blocker (8-8.9) | 1 |
| Important (7-7.9) | 2 |
| Medium (5-6.9) | 2 |
| Suggestions (<5) | ~10 |
Verdict reasoning
The per-field disable guard is correctly motivated for rapid same-field toggling, but is insufficient because (a) the server response carries the full fragment, so cross-field responses can interleave and stomp the cache, and (b) the Pay Type → payRate: null multi-field atomic save isn't protected on the Pay Rate text field (which has saveOnChange=false). Either widen the guard back for formType/salaryOrHourly, or drop the saveOnChange === true qualifier so blur-driven fields also disable during their own save.
Findings on Related / Pre-existing Code (Not in This PR's Diff)
[Suggestion] SetupStep.tsx:316-332 (Geographic Multiplier Autocomplete)
- Severity: 4.0/10
- Flagged by: UX, Data Integrity
- Note:
disabled={!calculation}only — no in-flight protection of any kind. Same race class as the blocker above. Pre-existing (not introduced by this PR). Worth a follow-up ticket, not a blocker here.
Standards & Quality summary
- Named exports: PASS
- TypeScript hygiene: PASS (no
any, no@ts-ignore, no!) - i18n: PASS (no new strings)
- Lint/TS clean for touched files: PASS
- Test colocation +
GqlMockedProvidertyped generics: PASS isMutatingis NOT dead code — page-levelSavingStatusstill consumes it viapages/.../pdsGoalCalculator/[pdsGoalId].page.tsx.
To resolve: address the blocker comment on usePdsGoalAutoSave.ts, then either fix or reply /dismiss: <reason> to severity < 7 findings you disagree with. Severity 7.0-7.9 findings cannot be dismissed but don't trigger BLOCKERS_FOUND on their own.
| /** Whether any mutations are currently in progress */ | ||
| isMutating: boolean; | ||
| /** Whether a save mutation tagged with the given field name is in flight */ | ||
| isFieldSaving: (fieldName: string) => boolean; |
There was a problem hiding this comment.
The new API accepts arbitrary strings:
isFieldSaving: (fieldName: string) => boolean(this line)trackFieldMutation: <T>(mutation: Promise<T>, fields: string[]) => Promise<T>(line 51-54)
But callers actually have keyof DesignationSupportCalculationUpdateInput available (usePdsGoalAutoSave.ts already enforces it). SetupStep.tsx:217 passes the bare literal 'salaryOrHourly' — a typo or rename would compile cleanly and silently leave the Pay Type select un-disabled.
Suggested patch:
import { DesignationSupportCalculationUpdateInput } from 'src/graphql/types.generated';
...
isFieldSaving: (
fieldName: keyof DesignationSupportCalculationUpdateInput,
) => boolean;
...
trackFieldMutation: <T>(
mutation: Promise<T>,
fields: Array<keyof DesignationSupportCalculationUpdateInput>,
) => Promise<T>;And in useSaveField.ts:48, isolate the Object.keys widening with one cast:
Object.keys(attributes) as Array<keyof DesignationSupportCalculationUpdateInput>,| await waitFor(() => | ||
| expect(mutationSpy).toHaveGraphqlOperation('UpdatePdsGoalCalculation'), | ||
| ); | ||
| expect(result.current.formType.disabled).toBe(false); |
There was a problem hiding this comment.
The final assertion runs after await waitFor(() => mutationSpy...). By that point, the mock mutation has resolved and savingFieldCounts.name is back to 0. The assertion passes both pre-fix (broad isMutating) and post-fix.
Compare with the working pattern at lines 124-145, which polls for true then false to catch the transition.
Suggested patch — sample synchronously mid-save:
act(() => {
result.current.name.onBlur();
});
+ // Sample synchronously mid-save: savingFieldCounts.name === 1 here, but
+ // the mock mutation has not yet resolved. Pre-fix (gating on isMutating),
+ // formType.disabled would have been true at this moment.
+ expect(result.current.formType.disabled).toBe(false);
+
await waitFor(() =>
expect(mutationSpy).toHaveGraphqlOperation('UpdatePdsGoalCalculation'),
);
expect(result.current.formType.disabled).toBe(false);| ); | ||
| }, | ||
| [trackMutation], | ||
| ); |
There was a problem hiding this comment.
| return next; | ||
| }); | ||
| return trackMutation( | ||
| mutation.finally(() => { |
There was a problem hiding this comment.
| // is in flight: rapid back-and-forth toggles can otherwise land out of | ||
| // order in the Apollo cache. formType is the load-bearing case — its | ||
| // value reshapes the goal calculation, so a stale final value silently | ||
| // understates the total. Other fields' saves do not affect this one. |
There was a problem hiding this comment.
Description
isMutatingflag from the calculator context.PdsGoalCalculatorContext: a newisFieldSaving(fieldName)selector and atrackFieldMutation(promise, fields)wrapper that tags the fields being written for the duration of the mutation.useSaveFieldnow usestrackFieldMutationand passesObject.keys(attributes), so each in-flight save tags only the fields it actually writes.usePdsGoalAutoSaveand the manual Pay Type select inSetupStepnow checkisFieldSaving(fieldName)instead ofisMutating. Same-field race protection onformType(and friends) is preserved — only the cross-field flicker is gone.saveOnChangeselect.Testing
salaryOrHourly + payRate: nullwrite still happens.Checklist:
/pr-reviewcommand locally and fixed any relevant suggestions🤖 Generated with Claude Code