fix(tui): forward the clear promise from App, so AuthTab can actually await it (#2144)
Copilot review round 12, and the best catch of the review.
`onClearOAuth` was wired as `() => { void handleClearOAuth(); }`, so the
promise `AuthTab` awaits resolved instantly. Everything round 6 through 9 built
on that await was therefore inert in production while still passing its own
tests: the pending line never showed, the repeat lock released immediately, and
a rejection went unhandled instead of reaching the failure line.
Passed directly now. The regression test drives the real App rather than
`AuthTab` in isolation — the wiring is the contract, and only an App-level test
can see it. Verified it fails when the void-ing arrow is put back.
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
e2cbf0de6d
commit
c0bcdc7e4a
2 changed files with 39 additions and 3 deletions
|
|
@ -1412,6 +1412,39 @@ describe("App (mid-session auth lifecycle events)", () => {
|
||||||
await expectFrame(r, "unreachable");
|
await expectFrame(r, "unreachable");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The wiring is the contract here, not just `AuthTab`'s own behavior: a
|
||||||
|
// `void`-ing arrow between them resolves instantly, which makes the pending
|
||||||
|
// state, the repeat lock and the rejection path all inert while revocation
|
||||||
|
// is still running. Driven through the real App so the arrow cannot come
|
||||||
|
// back (#2144).
|
||||||
|
it("holds the pending state until the clear actually settles", async () => {
|
||||||
|
let settle: () => void = () => {};
|
||||||
|
h.clientSpies.clearOAuthTokens.mockImplementation(
|
||||||
|
() =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
settle = () =>
|
||||||
|
resolve({ status: "skipped", reason: "no_endpoint" as const });
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const r = await mount(oneHttp());
|
||||||
|
await press(r, ["a", "s"]);
|
||||||
|
await waitUntil(() => h.clientSpies.clearOAuthTokens.mock.calls.length > 0);
|
||||||
|
await tick();
|
||||||
|
|
||||||
|
// Still running: the pending line is up and the confirmation is not.
|
||||||
|
expect(r.lastFrame() ?? "").toContain("Clearing OAuth state");
|
||||||
|
expect(r.lastFrame() ?? "").not.toContain("OAuth state cleared");
|
||||||
|
|
||||||
|
// And the repeat lock is live, so a second press does not start another.
|
||||||
|
await press(r, ["s"]);
|
||||||
|
expect(h.clientSpies.clearOAuthTokens).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
settle();
|
||||||
|
await waitUntil(() =>
|
||||||
|
(r.lastFrame() ?? "").includes("OAuth state cleared"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("says nothing when there was nothing to revoke", async () => {
|
it("says nothing when there was nothing to revoke", async () => {
|
||||||
const r = await mount(oneHttp());
|
const r = await mount(oneHttp());
|
||||||
await press(r, ["a", "s"]);
|
await press(r, ["a", "s"]);
|
||||||
|
|
|
||||||
|
|
@ -1899,9 +1899,12 @@ function App({
|
||||||
focused={
|
focused={
|
||||||
focus === "tabContentList" || focus === "tabContentDetails"
|
focus === "tabContentList" || focus === "tabContentDetails"
|
||||||
}
|
}
|
||||||
onClearOAuth={() => {
|
// Passed directly, NOT wrapped in a `void`-ing arrow: AuthTab
|
||||||
void handleClearOAuth();
|
// awaits this to hold its pending state, keep its repeat lock,
|
||||||
}}
|
// and route a rejection to the failure line. Dropping the
|
||||||
|
// promise here would resolve it instantly and make all three
|
||||||
|
// inert while revocation was still running (#2144).
|
||||||
|
onClearOAuth={handleClearOAuth}
|
||||||
connectionStatus={inspectorStatus}
|
connectionStatus={inspectorStatus}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue