fix(tui): retire the disconnect token on a server switch, not just clear
Copilot round 3 (PR #2039): clearing the banner when `selectedServer` changes is not enough. Start disconnecting A, switch A → B → A, and by the time the original request rejects both guards match again — the attempt token is unchanged and the selected server is A once more — so the stale failure lands on the freshly re-selected A. Bump the token on every switch, so a round trip invalidates prior disconnects the same way a one-way switch does. New test drives exactly that interleaving; it fails without the bump (the one-way test does not, which is why this needed its own). Also attaches the before/after TUI screenshots the review asked for — the review was right that "no UI change" stopped being true once the banner was added — and corrects the PR description accordingly. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
d830213796
commit
85e3215534
2 changed files with 29 additions and 1 deletions
|
|
@ -722,6 +722,30 @@ describe("App (foundation)", () => {
|
|||
expect(r.lastFrame() ?? "").not.toContain("stale-alpha-failure");
|
||||
});
|
||||
|
||||
it("drops a stale disconnect rejection across an A → B → A round trip", async () => {
|
||||
// The server-name check alone passes here: by the time the rejection
|
||||
// lands, alpha is selected again. Only retiring the attempt token on
|
||||
// every switch catches it.
|
||||
h.ctrl.status = "connected";
|
||||
let rejectDisconnect: (err: Error) => void = () => {};
|
||||
h.disconnect.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise<void>((_resolve, reject) => {
|
||||
rejectDisconnect = reject;
|
||||
}),
|
||||
);
|
||||
const r = await mount(stdioServer());
|
||||
r.stdin.write("d"); // alpha's disconnect starts, and hangs
|
||||
await tick();
|
||||
await press(r, [DOWN]); // alpha -> beta
|
||||
await expectFrame(r, "b.js");
|
||||
await press(r, [UP]); // beta -> alpha again
|
||||
await expectFrame(r, "s.js");
|
||||
rejectDisconnect(new Error("round-trip-failure"));
|
||||
await waitUntil(() => (r.lastFrame() ?? "").includes("round-trip-failure"));
|
||||
expect(r.lastFrame() ?? "").not.toContain("round-trip-failure");
|
||||
});
|
||||
|
||||
it("switches tabs via accelerator keys", async () => {
|
||||
const r = await mount(stdioServer());
|
||||
await press(r, ["t"]); // tools tab (server is auto-selected)
|
||||
|
|
|
|||
|
|
@ -446,7 +446,11 @@ function App({
|
|||
setOauthStatus("idle");
|
||||
setOauthMessage(null);
|
||||
// The header banner is server-scoped, so a failure from the server we just
|
||||
// left must not be read as this one's.
|
||||
// left must not be read as this one's. Clearing is not enough: a switch
|
||||
// away and back (A → B → A) would leave both the attempt token and the
|
||||
// server name matching again, so the stale rejection would land on the
|
||||
// re-selected A. Retiring the token on every switch is what closes that.
|
||||
disconnectAttemptRef.current++;
|
||||
setDisconnectError(null);
|
||||
const stepUp = pendingStepUpRef.current;
|
||||
if (stepUp && selectedServer && stepUp.serverName !== selectedServer) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue