Skip to content

fix(frontend): a committee saves whatever the user list says - #1138

Merged
ExtraToast merged 1 commit into
mainfrom
fix/a-committee-save-waits-for-nothing
Sep 5, 2026
Merged

fix(frontend): a committee saves whatever the user list says#1138
ExtraToast merged 1 commit into
mainfrom
fix/a-committee-save-waits-for-nothing

Conversation

@ExtraToast

@ExtraToast ExtraToast commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Closes #1042.

Why

CommitteeForm judged every member row against the users list the page holds, and that list is one page of an unbounded table fetched after the form is open. Whether a save left the browser therefore depended on what the page happened to know at the moment the button was pressed. That is the shape #1042 keeps coming back in: a submit that sends no request, shows no refusal a request log can see, and leaves a board member on a form the api would have accepted.

Both earlier fixes narrowed the rule instead of removing it. 6c82c5b6c made an empty list absent evidence; dfeffc8eb made a member outside the page absent evidence and raised the page size to 500. Each left the rule holding a smaller sliver — after the second, the only refusal it can still produce is "the page's copy of this user says they are not a member" — and the failure shape survives in whatever sliver is left, because it is the reading of that list at all, not any particular reading, that decides whether the api is called.

Nothing on the api refuses a committee member who is not an association member: CommitteeService.reconcileMembers resolves the user by id and takes them, and CommitteeSeatRevocationListener gives the seat up when somebody stops being a member. So this was a frontend-only guard over a rule the api does not have, reading data the page cannot hold whole, cannot hold fresh, and does not have yet.

What this achieves

A committee saves on what was typed into it. The user list is what fills the picker, and nothing else. A board member who edits a committee before /users answers, or whose member sits outside the page it answered with, or whose copy of that user is stale, gets the save the api accepts.

How

committeeUserIsMember is gone. What replaces it, committeeMemberPicked, refuses only a row nobody has picked into — which has to be said explicitly, because 0 is what a fresh row carries and required reads a number as filled in. uniqueCommitteeMember stays: it judges the form against itself, not against a list. Both remaining refusals paint their message on the row they belong to.

Not in scope

findUsers({size: 500}) stays. With the rule gone it only decides what the picker can offer, and 500 offers more than the 20 it asked for before.

Worth a reviewer's attention

  • The premise that 500 is the whole list is false at scale, and it now breaks the picker rather than the save. Loading 5,410 users into a local CI stack makes creates committee from manager and updates committee members and committee roles fail deterministically — not on the save, but earlier, on CommitteeFormHelper.addMember waiting for a dropdown option for a user who is not in the first page. Say the word and I will file it; the fix is a picker that asks the api as you type rather than one that pretends to hold everything.
  • I could not reproduce the CI run itself. What I proved is that the mechanism the issue names can no longer refuse a save, and that the class it fails in is green against a real stack. Details under Verification.

Verification

Reproduction, at three levels:

  • Unit, deterministic, red before the change: saves an edited committee the user list contradicts in CommitteeManager.test.ts mounts the real page with real Vuetify, opens the edit form, types, and clicks the actual submit button. Against main it fails with expected "vi.fn()" to be called 1 times, but got 0 times — the CI symptom exactly, a click that sends nothing. It passes here.
  • Browser, mocked api: an e2e spec holding GET /users open across the click, swept across fourteen arrival offsets from 0 to 80 ms, with the CPU throttled 20×, with reduced motion off. All green on main — the empty and mid-flight cases are genuinely fixed, which is why the flake outlived those two fixes.
  • Real stack: CI images built locally, api on 8081, frontend on 5173, real MariaDB. Three probes drove the exact failing flow with GET /users?size=500 fulfilled empty, aborted, and answered with a page the member is not on. All three saved. That is the issue's stated cause, dead in the real system.

So the CI trace's failed=[] requests=[… +12642ms [committee submit click]] is a refusal I could not provoke on any of those paths — and after this change there is no path left by which the user list can produce one. The CommitteeManagerPageSystemTest class runs green 3× against the real stack with this build.

Local, each exit code checked: yarn test:unit 0, yarn lint 0, yarn typecheck 0. System tests: the one class, 3 runs, BUILD SUCCESSFUL; the rest of the suite was not run locally.


Diff breakdown added removed, scaled to the largest row.

frontend                                           +49    -14    3
  production         ████░░░░░░                     +7    -10    1
  unit tests         ████████████████████████░░    +42     -4    2

──────────────────────────────────────────────────────────────────
production                                          +7    -10
tests                                              +42     -4  6.00 test lines per prod line
total (hand-written)                               +49    -14  3 files

CommitteeForm judged every member row against the page's `users` list, and that
list is one page of an unbounded table fetched after the form is already open. A
save was therefore refused, and no request left the browser, on what the page
happened to know: #1042 fixed the empty list, #1099 the truncated one, and each
time the sliver that was left kept the same failure shape — a submit that sends
nothing, says nothing a request log can see, and leaves the board member looking
at a form the api would have accepted.

The rule is gone rather than narrowed again. Nothing on the api refuses a
committee member who is not an association member: CommitteeService resolves the
user by id, and CommitteeSeatRevocationListener gives the seat up when somebody
stops being a member. A frontend-only guard reading data the page cannot hold
whole, cannot hold fresh, and does not have yet is what produced three reports of
the same bug.

What is left is what the form can judge from itself: a row nobody has picked into
is refused, which has to be said explicitly because 0 is what a fresh row carries,
and a user twice in one committee is refused.
@ExtraToast
ExtraToast merged commit ec07271 into main Sep 5, 2026
41 of 42 checks passed
@ExtraToast
ExtraToast deleted the fix/a-committee-save-waits-for-nothing branch September 5, 2026 18:45
@github-actions github-actions Bot mentioned this pull request Sep 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Flaky: committee metadata save is refused while the user list is still loading

1 participant