fix(e24): review round 3 — the holes round 2's own fixes left

Round 3 reviewed the round-2 commit across six dimensions. Fifteen findings, five survived
three adversarial refuters each; three died on 2+ refutations (a keyboard-focus claim that
is a property of `ui/Button` at all six of its `loading` call sites, a contract-test
false-pass that needs a deliberate revert plus a self-incriminating comment, and a
MAX_EXCLUDE coupling whose slice is unreachable from the real client).

- `rerolling` was still wrong for a FAILED press. `data === undefined` is true forever for a
  key whose request failed (`retry: false`), so the next unrelated mutation re-disabled the
  reroll button — the same defect round 2 reported, one branch over. `!isFetched` is the
  flag that remembers a key settled; `!isError` would not have worked, because query-core
  resets a data-less query to "pending" for the duration of a refetch, hiding the error
  exactly when it matters.
- The hero fallback used `??`, which cannot tell "this budget's last pick resolved to NO
  hero" from "nothing remembered". So a press that failed after an empty one resurrected the
  card the page had already excluded as seen and just said it had nothing beyond. The tagged
  slot is now authoritative for its own budget.
- `setPage`'s boolean — the input the whole burial rule reads — was asserted by nothing.
  The type fences the SHAPE, so deleting a `return` fails tsc, but swapping the two values
  passed typecheck and all 822 tests. NavigationProvider gets its first render test, on the
  value AND on the effects the early return skips.
- `closeThenNavigate`'s door into `buryUnder` was equally unpinned: reverting that one call
  site to the old unconditional burial left the suite green. It has the mirror case now.
- The comment claiming one rule serves both entry points was overstated. `buryUnder` fails
  OPEN, and `CommandItem.run` is typed `() => void`, so only the palette's bare
  `() => setPage(page)` rows forward the boolean at all. Nothing regressed — that path buried
  unconditionally before — but the prose said more than the code does, so it now says what is
  true and points at the deferred fix. Same for a "the only assertion in the suite" claim
  that the file it cites as precedent also makes.

Every new test mutation-checked red for the right reason, both edges where there are two.
Gate: ruff, ruff format, 3x tsc, eslint (0 errors), prettier, knip, vitest 828, pytest 1005.
This commit is contained in:
2026-09-09 00:39:37 +02:00
parent e4ffd59ca7
commit 59efc8f2e8
6 changed files with 241 additions and 12 deletions
+72
View File
@@ -413,6 +413,78 @@ describe("Home", () => {
expect(rerollBtn().getAttribute("aria-busy")).toBeNull();
});
it("a press that FAILED does not leave the button hostage to the next mutation", async () => {
// Round 3's correction to the case above. A key whose request failed never gets data of its own
// (`retry: false` makes it terminal), so a predicate reading `data === undefined` stayed true
// forever and every later background refetch re-disabled the button — the same defect, one
// branch over. `isFetched` remembers that the key settled, an error counting as settled.
const rerollBtn = () =>
screen.getByText("home.hero.reroll").closest("button") as HTMLButtonElement;
let failPick!: (e: Error) => void;
discoverPick.mockReturnValueOnce(
new Promise((_resolve, reject) => {
failPick = reject;
}),
);
const { qc } = mountWithClient();
await screen.findByText("Hero v1");
fireEvent.click(screen.getByText("home.hero.reroll"));
await waitFor(() => expect(discoverPick).toHaveBeenCalledTimes(1));
await act(async () => {
failPick(new Error("boom"));
await new Promise((r) => setTimeout(r, 0));
});
expect(rerollBtn().disabled).toBe(false); // the failure itself releases the button
// Now an unrelated action refetches that errored key in the background.
discoverPick.mockReturnValue(new Promise(() => {}));
act(() => {
void qc.invalidateQueries({ queryKey: qk.discover() });
});
await waitFor(() => expect(discoverPick).toHaveBeenCalledTimes(2));
expect(rerollBtn().disabled).toBe(false);
expect(rerollBtn().getAttribute("aria-busy")).toBeNull();
});
it("keeps the honest empty answer when a press fails after an empty one", async () => {
// The tagged slot is authoritative for its own budget, including when what it holds is "no hero
// at all". Under a `??` chain that read as "nothing remembered" and fell through to the loaded
// payload, so a failed press resurrected the card the page had already excluded as seen and just
// said it had nothing beyond (round 3).
discover.mockResolvedValue({
budget: "short",
hero: { tier: "resume", card: ytCard("v1", "Hero v1") },
continue_watching: [],
comfort: [],
});
mount();
await screen.findByText("Hero v1");
fireEvent.click(screen.getByText("home.budget.short")); // the empty-state branch needs a band
await waitFor(() => expect(discover).toHaveBeenLastCalledWith("short", 0));
// A press that RESOLVES with nothing left: the honest empty state.
discoverPick.mockResolvedValueOnce({ budget: "short", hero: null });
fireEvent.click(screen.getByText("home.hero.reroll"));
await screen.findByText("home.hero.emptyBudget");
expect(screen.queryByText("Hero v1")).toBeNull();
// …and a press from there that fails must not undo it.
let failPick!: (e: Error) => void;
discoverPick.mockReturnValueOnce(
new Promise((_resolve, reject) => {
failPick = reject;
}),
);
fireEvent.click(screen.getByText("home.hero.reroll"));
await waitFor(() => expect(discoverPick).toHaveBeenCalledTimes(2));
await act(async () => {
failPick(new Error("boom"));
await new Promise((r) => setTimeout(r, 0));
});
expect(screen.getByText("home.hero.emptyBudget")).toBeTruthy();
expect(screen.queryByText("Hero v1")).toBeNull();
});
it("refreshes the payload the bookmark is read from", async () => {
mount();
await screen.findByText("Second thing");