chore: address Copilot review round 1 (#2192)

- `useEmaIdpLoginState`: the effect-triggered `refresh()` was discarded with
  `void`, but it does not own its failures — `getEmaIdpLoginState` awaits
  `storage.getIdpSession`, which rejects when the backend is unreachable.
  Terminate with `.catch()` and leave `loginState` untouched, as `logout`
  already does. Covered by a new test that also asserts no unhandled
  rejection escapes.
- Correct the history: the plugin was *client-local*, declared by both
  `clients/web` and `clients/tui`. The root could reach neither, since Node
  resolution walks up rather than down — that, not web-exclusivity, is why
  `core/react/` went unlinted.
- Correct the enforcement claim: `clients/tui` registers the plugin but
  deliberately enables only `rules-of-hooks` and `exhaustive-deps`, so
  `set-state-in-effect` and `refs` hold for `clients/web` and `core/react/`
  only. Says so in AGENTS.md and in the Copilot mirror, which would
  otherwise have reviewers cite rules the TUI gate does not run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hzwzS8UvBsr4yZm7o7sev
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-29 23:50:18 -04:00
commit 7e6d6c0bc1

View file

@ -189,4 +189,47 @@ describe("useEmaIdpLoginState", () => {
).not.toHaveBeenCalled();
expect(result.current.loginState).toBe("none");
});
it("swallows a session-read rejection and leaves loginState unchanged", async () => {
const exp = Math.floor(Date.now() / 1000) + 3600;
const payload = btoa(JSON.stringify({ exp }))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
vi.mocked(storage.getIdpSession).mockResolvedValue({
idToken: `h.${payload}.s`,
});
const { result, rerender } = renderHook(
({ issuer }: { issuer: string }) =>
useEmaIdpLoginState(storage, issuer, true),
{ initialProps: { issuer: "https://idp.test" } },
);
await waitFor(() => {
expect(result.current.loginState).toBe("logged_in");
});
// The storage backend goes away. The effect-triggered refresh must not
// surface an unhandled rejection, and must leave the last state we
// actually read in place rather than falsely reporting signed-out.
const unhandled = vi.fn();
process.on("unhandledRejection", unhandled);
try {
vi.mocked(storage.getIdpSession).mockRejectedValue(
new Error("storage unreachable"),
);
await act(async () => {
rerender({ issuer: "https://other-idp.test" });
await new Promise((resolve) => setTimeout(resolve, 0));
});
} finally {
process.off("unhandledRejection", unhandled);
}
expect(storage.getIdpSession).toHaveBeenCalledWith(
"https://other-idp.test",
);
expect(unhandled).not.toHaveBeenCalled();
expect(result.current.loginState).toBe("logged_in");
});
});