Skip to content

Commit d510f01

Browse files
fix(miner-ui): reset the governor pause reason after a successful pause
GovernorControlSection keeps the pause-reason Input's text in a single `reason` state for the section's lifetime. The Input is unmounted while paused but the component instance persists, so after pause→resume the field reappeared pre-filled with the previous pause's reason — an operator could submit a stale reason into the persisted governor pause-state. Reset `reason` to empty the moment a pause succeeds (the polled result flips to paused), using React's render-phase 'info from previous renders' pattern rather than an effect. A failed pause leaves result.ok false and never sets paused, so the typed reason is preserved for a retry. Local-state-only; no callback/route changes. Adds a pause→resume→pause regression test plus the failed-pause negative case. Closes #7079
1 parent c060b2a commit d510f01

2 files changed

Lines changed: 47 additions & 1 deletion

File tree

apps/loopover-miner-ui/src/ledgers.test.tsx

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { describe, expect, it, vi } from "vitest";
33

44
import { fetchLedgers, LEDGERS_API_PATH, type LedgersResult, type LedgersSummary } from "./lib/ledgers";
55
import { type GovernorPauseState, type GovernorPauseStateResult } from "./lib/governor";
6-
import { LedgersPage, LedgersView } from "./routes/ledgers";
6+
import { GovernorControlSection, LedgersPage, LedgersView } from "./routes/ledgers";
77
import { handleLedgersRequest, type LedgersApiDeps } from "../vite-ledgers-api";
88

99
// Test-local fixture factories (were lib exports reachable only from tests; moved here per #6187).
@@ -493,3 +493,39 @@ describe("handleLedgersRequest (#4855)", () => {
493493
expect(handled).toEqual({ status: 500, body: JSON.stringify({ error: "sqlite locked" }) });
494494
});
495495
});
496+
497+
describe("GovernorControlSection pause-reason reset (#7079)", () => {
498+
const notPaused: GovernorPauseStateResult = { ok: true, pauseState: { paused: false, reason: null, pausedAt: null } };
499+
const pausedResult: GovernorPauseStateResult = {
500+
ok: true,
501+
pauseState: { paused: true, reason: "investigating flaky test", pausedAt: "2026-07-18T00:00:00.000Z" },
502+
};
503+
const control = (result: GovernorPauseStateResult, onPause = vi.fn()) => (
504+
<GovernorControlSection result={result} pending={false} onPause={onPause} onResume={vi.fn()} />
505+
);
506+
507+
it("clears the reason after a successful pause, so a later resume→pause starts from a blank input", () => {
508+
const onPause = vi.fn();
509+
const { rerender } = render(control(notPaused, onPause));
510+
fireEvent.change(screen.getByLabelText("Pause reason"), { target: { value: "investigating flaky test" } });
511+
fireEvent.click(screen.getByRole("button", { name: "Pause governor" }));
512+
expect(onPause).toHaveBeenCalledWith("investigating flaky test");
513+
514+
// The poll reflects the successful pause (paused: true), then the operator resumes back to the pause form.
515+
rerender(control(pausedResult, onPause));
516+
rerender(control(notPaused, onPause));
517+
expect((screen.getByLabelText("Pause reason") as HTMLInputElement).value).toBe("");
518+
});
519+
520+
it("preserves the typed reason after a FAILED pause, so it can be retried without retyping", () => {
521+
const onPause = vi.fn();
522+
const { rerender } = render(control(notPaused, onPause));
523+
fireEvent.change(screen.getByLabelText("Pause reason"), { target: { value: "investigating flaky test" } });
524+
fireEvent.click(screen.getByRole("button", { name: "Pause governor" }));
525+
526+
// The pause fails (ok: false): the reset must NOT fire. Re-render back to the form (a retry) and the reason stays.
527+
rerender(control({ ok: false, error: "governor state write failed" }, onPause));
528+
rerender(control(notPaused, onPause));
529+
expect((screen.getByLabelText("Pause reason") as HTMLInputElement).value).toBe("investigating flaky test");
530+
});
531+
});

apps/loopover-miner-ui/src/routes/ledgers.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,16 @@ export function GovernorControlSection({
336336
// Optional pause reason, mirroring the CLI's `governor pause [--reason <text>]`; an empty field
337337
// is passed through as `undefined` so it matches the CLI's own optional-flag behavior.
338338
const [reason, setReason] = useState("");
339+
// #7079: once a pause succeeds the polled `result` flips to paused; clear the reason so a later
340+
// resume→pause starts from a blank input. Done with React's "info from previous renders" pattern (a
341+
// render-phase reset when `paused` turns true) rather than an effect. A FAILED pause leaves `result.ok`
342+
// false and never sets `paused`, so the reason is preserved for a retry.
343+
const paused = result?.ok === true && result.pauseState.paused;
344+
const [wasPaused, setWasPaused] = useState(paused);
345+
if (paused !== wasPaused) {
346+
setWasPaused(paused);
347+
if (paused) setReason("");
348+
}
339349
const errorText = result !== null && !result.ok ? result.error : undefined;
340350
return (
341351
<section className="grid gap-3">

0 commit comments

Comments
 (0)