fix(frontend): round 2 — the popover latch reset read a stale, negative count

The reset I added guards on `hidden === 0`, and `hidden` can be NEGATIVE.
`useChipOverflowFit` keeps `visible` in state and re-decides in a layout effect,
so the render in which the chip set SHRINKS still carries the previous
measurement — delete three tags under an open "+N" and `chips.length - visible`
goes to -2, the equality never matches, and the flag survives the one window the
reset exists to close. The next re-fit brings the menu back on its own, which is
the original defect with an extra step. Clamped at all three lanes, with a test
that drives the shrink through the stale value.

Also: the Scheduler characterization banner said "the four tests below" and now
sits above six, the last two of which are genuine regression guards for the
range-refusal fix. A future mutation check would have read it, seen them go red,
and concluded the harness was broken rather than that the guard worked.
This commit is contained in:
2026-09-21 23:59:59 +02:00
parent aa51644f5f
commit d924c665f8
4 changed files with 48 additions and 8 deletions
@@ -80,11 +80,13 @@ afterEach(() => {
const intervalBox = () => screen.queryByRole("spinbutton", { name: "" });
const batchBox = () => screen.getByLabelText("scheduler.maintenance.batchLabel");
// The four tests below are CHARACTERIZATION, not regression guards for E27 S2: the old JobRow's
// re-sync effect was already guarded by `if (!editing)` and the mirror only rendered while editing,
// so every one of them passes against the pre-sweep code too. They are here because the editor had
// no tests at all. The editor that genuinely lost the user's typing to a poll is the maintenance
// batch box below — its old effect had no guard — and that one has its own test.
// The FIRST FOUR tests below are CHARACTERIZATION, not regression guards for E27 S2: the old
// JobRow's re-sync effect was already guarded by `if (!editing)` and the mirror only rendered while
// editing, so each of those four passes against the pre-sweep code too. They are here because the
// editor had no tests at all. The two after them — the range refusal and the focus return — ARE
// regression guards, for the silent-discard fix, and they fail against the code this file shipped
// with. The editor that genuinely lost the user's typing to a poll is the maintenance batch box
// below: its old effect had no guard, and that one has its own test.
describe("Scheduler · the job interval editor", () => {
it("opens on the current interval and commits a new one", async () => {
const update = vi.spyOn(api, "updateSchedulerJob").mockResolvedValue(undefined as never);