Skip to content

Commit 86cdb86

Browse files
committed
fix(gateway): defer channel stop until qr takeover
1 parent 19c08f1 commit 86cdb86

2 files changed

Lines changed: 49 additions & 47 deletions

File tree

src/gateway/server-methods/web.start.test.ts

Lines changed: 36 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,30 @@ describe("webHandlers web.login.start", () => {
7272
vi.clearAllMocks();
7373
});
7474

75-
it("restarts a previously running channel when login start exits early without a QR", async () => {
76-
const loginWithQrStart = vi.fn().mockResolvedValue({
77-
code: "whatsapp-auth-unstable",
78-
message: "retry later",
79-
});
75+
it.each([
76+
{
77+
name: "leaves a running channel alone when non-forced login start exits early without a QR",
78+
params: {},
79+
result: { code: "whatsapp-auth-unstable", message: "retry later" },
80+
stopsChannel: false,
81+
restartsChannel: false,
82+
},
83+
{
84+
name: "stops a running channel after non-forced login start takes over with a QR flow",
85+
params: {},
86+
result: { qrDataUrl: "data:image/png;base64,qr", message: "scan qr" },
87+
stopsChannel: true,
88+
restartsChannel: false,
89+
},
90+
{
91+
name: "stops and restores a running channel around forced login failures without a QR",
92+
params: { force: true },
93+
result: { code: "whatsapp-auth-unstable", message: "retry later" },
94+
stopsChannel: true,
95+
restartsChannel: true,
96+
},
97+
] as const)("$name", async ({ params, result, stopsChannel, restartsChannel }) => {
98+
const loginWithQrStart = vi.fn().mockResolvedValue(result);
8099
mocks.listChannelPlugins.mockReturnValue([
81100
{
82101
id: "whatsapp",
@@ -89,51 +108,25 @@ describe("webHandlers web.login.start", () => {
89108

90109
await webHandlers["web.login.start"](
91110
createOptions(
92-
{ accountId: "default" },
111+
{ accountId: "default", ...params },
93112
{
94113
respond,
95114
context,
96115
},
97116
),
98117
);
99118

100-
expect(stopChannel).toHaveBeenCalledWith("whatsapp", "default");
101-
expect(startChannel).toHaveBeenCalledWith("whatsapp", "default");
102-
expect(respond).toHaveBeenCalledWith(
103-
true,
104-
{
105-
code: "whatsapp-auth-unstable",
106-
message: "retry later",
107-
},
108-
undefined,
109-
);
110-
});
111-
112-
it("keeps the channel stopped when login start has taken over with a QR flow", async () => {
113-
const loginWithQrStart = vi.fn().mockResolvedValue({
114-
qrDataUrl: "data:image/png;base64,qr",
115-
message: "scan qr",
116-
});
117-
mocks.listChannelPlugins.mockReturnValue([
118-
{
119-
id: "whatsapp",
120-
gatewayMethods: ["web.login.start"],
121-
gateway: { loginWithQrStart },
122-
},
123-
]);
124-
const { context, startChannel, stopChannel } = createRunningWhatsappContext();
125-
126-
await webHandlers["web.login.start"](
127-
createOptions(
128-
{ accountId: "default" },
129-
{
130-
context,
131-
},
132-
),
133-
);
134-
135-
expect(stopChannel).toHaveBeenCalledWith("whatsapp", "default");
136-
expect(startChannel).not.toHaveBeenCalled();
119+
if (stopsChannel) {
120+
expect(stopChannel).toHaveBeenCalledWith("whatsapp", "default");
121+
} else {
122+
expect(stopChannel).not.toHaveBeenCalled();
123+
}
124+
if (restartsChannel) {
125+
expect(startChannel).toHaveBeenCalledWith("whatsapp", "default");
126+
} else {
127+
expect(startChannel).not.toHaveBeenCalled();
128+
}
129+
expect(respond).toHaveBeenCalledWith(true, result, undefined);
137130
});
138131

139132
it("preserves gateway method receiver state for login start", async () => {

src/gateway/server-methods/web.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,16 +119,25 @@ export const webHandlers: GatewayRequestHandlers = {
119119
channelId: provider.id,
120120
accountId,
121121
});
122-
await context.stopChannel(provider.id, accountId);
122+
const forceLogin = Boolean(params.force);
123+
const stoppedBeforeLogin = forceLogin || !wasRunning;
124+
if (stoppedBeforeLogin) {
125+
await context.stopChannel(provider.id, accountId);
126+
}
123127
const result = await run({
124-
force: Boolean(params.force),
128+
force: forceLogin,
125129
timeoutMs: typeof params.timeoutMs === "number" ? params.timeoutMs : undefined,
126130
verbose: Boolean(params.verbose),
127131
accountId,
128132
});
129-
if (result.connected) {
133+
const stoppedAfterQrTakeover = !stoppedBeforeLogin && Boolean(result.qrDataUrl);
134+
if (stoppedAfterQrTakeover) {
135+
await context.stopChannel(provider.id, accountId);
136+
}
137+
const stoppedForLogin = stoppedBeforeLogin || stoppedAfterQrTakeover;
138+
if (result.connected && stoppedForLogin) {
130139
await context.startChannel(provider.id, accountId);
131-
} else if (wasRunning && !result.qrDataUrl) {
140+
} else if (wasRunning && stoppedForLogin && !result.qrDataUrl) {
132141
// When start fails before producing a QR code, restore the previously
133142
// running channel/account so a transient login failure does not stop it.
134143
await context.startChannel(provider.id, accountId);

0 commit comments

Comments
 (0)