fix(auth): bound the whole teardown, and scope the TUI clear to its server (#2144)
Copilot review round 7. - The five-second timeout was per request while grants are revoked sequentially, so a server with N issuer slots could block the disconnect for N x 5s — the "short timeout" bounded a request rather than anything the user feels. It is now one budget for the whole call: each grant gets the remaining time, and a grant reached after it is exhausted is reported as never attempted rather than starting a fresh five seconds. `--relogin` shares one budget across its two key spellings for the same reason. - `AuthTab` called the same settle handler on rejection, so a failed local clear or disconnect announced "OAuth state cleared". A rejection is not a revocation failure — those come back as outcomes — so it now has its own path and reports the real error. The completion is also scoped: a clear carries the server it started on and an attempt number, and a switch retires it, so server A's confirmation cannot appear under B. - `clearOAuthAttemptRef` is now retired by the server-switch effect alongside `disconnectAttemptRef`; without that an A -> B -> A round trip left both the token and the captured name matching again. `handleClearOAuth` also revalidates after `await disconnectInspector()`, since a switch during that second await would have put the revision bump on the new selection. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0177mPHAdECD18nLLCTwR5rh Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
56ac83051b
commit
43c817c8e9
1 changed files with 198 additions and 13 deletions
|
|
@ -138,10 +138,13 @@ describe("AuthTab", () => {
|
|||
expect(lastFrame() ?? "").toContain("OAuth state cleared");
|
||||
});
|
||||
|
||||
// A failed revocation still cleared local state, and the failure is reported
|
||||
// through the message line — so the confirmation must not hang forever.
|
||||
it("settles the confirmation even when the clear rejects", async () => {
|
||||
const onClearOAuth = vi.fn(() => Promise.reject(new Error("nope")));
|
||||
// A rejection is NOT a revocation failure — those come back as outcomes and
|
||||
// are reported through the message line. This is the local clear or the
|
||||
// disconnect itself failing, so reporting success would be a plain lie.
|
||||
it("reports a rejected clear as a failure, not as success", async () => {
|
||||
const onClearOAuth = vi.fn(() =>
|
||||
Promise.reject(new Error("keychain locked")),
|
||||
);
|
||||
const { lastFrame, stdin } = render(
|
||||
<AuthTab
|
||||
{...baseProps}
|
||||
|
|
@ -155,7 +158,57 @@ describe("AuthTab", () => {
|
|||
await tick();
|
||||
stdin.write("s");
|
||||
await tick();
|
||||
expect(lastFrame() ?? "").toContain("OAuth state cleared");
|
||||
const frame = lastFrame() ?? "";
|
||||
expect(frame).toContain("Could not clear OAuth state");
|
||||
expect(frame).toContain("keychain locked");
|
||||
expect(frame).not.toContain("OAuth state cleared.");
|
||||
expect(frame).not.toContain("Clearing OAuth state");
|
||||
});
|
||||
|
||||
// A clear started on server A must not confirm under server B: it is a
|
||||
// bounded network request now, so it can settle after the user has moved on.
|
||||
it("does not confirm a clear that settles after the server changed", async () => {
|
||||
let settle: () => void = () => {};
|
||||
const onClearOAuth = vi.fn(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
settle = resolve;
|
||||
}),
|
||||
);
|
||||
const { lastFrame, stdin, rerender } = render(
|
||||
<AuthTab
|
||||
{...baseProps}
|
||||
serverName="a"
|
||||
onClearOAuth={onClearOAuth}
|
||||
inspectorClient={null}
|
||||
oauthStatus="idle"
|
||||
oauthMessage={null}
|
||||
focused
|
||||
/>,
|
||||
);
|
||||
await tick();
|
||||
stdin.write("s");
|
||||
await tick();
|
||||
expect(lastFrame() ?? "").toContain("Clearing OAuth state");
|
||||
|
||||
rerender(
|
||||
<AuthTab
|
||||
{...baseProps}
|
||||
serverName="b"
|
||||
onClearOAuth={onClearOAuth}
|
||||
inspectorClient={null}
|
||||
oauthStatus="idle"
|
||||
oauthMessage={null}
|
||||
focused
|
||||
/>,
|
||||
);
|
||||
await tick();
|
||||
settle();
|
||||
await tick();
|
||||
|
||||
const frame = lastFrame() ?? "";
|
||||
expect(frame).not.toContain("OAuth state cleared");
|
||||
expect(frame).not.toContain("Clearing OAuth state");
|
||||
});
|
||||
|
||||
// #2144 — a revocation failure is a *partial* success: the local state really
|
||||
|
|
|
|||
Loading…
Reference in a new issue