test(bot): cover post-pair-restart re-warming sequence
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d0db248460
commit
40d788302c
@ -10,7 +10,7 @@ import { renderQrPng } from "../whatsapp/qr-renderer.js";
|
||||
import { syncGroupsForAccount } from "../whatsapp/group-sync.js";
|
||||
import { writeAuditLog } from "../audit.js";
|
||||
import { pgNotifyWeb } from "./notify.js";
|
||||
import { decidePairListenerOnClose } from "./pair-state.js";
|
||||
import { decidePairListenerOnClose, nextWarmingUpAfterEvent } from "./pair-state.js";
|
||||
|
||||
const PAIR_TIMEOUT_MS = 5 * 60 * 1000;
|
||||
const offByAccount = new Map<string, () => void>();
|
||||
|
||||
@ -3,6 +3,7 @@ import {
|
||||
decideOnPairClose,
|
||||
decideOnPairTimeout,
|
||||
decidePairListenerOnClose,
|
||||
nextWarmingUpAfterEvent,
|
||||
shouldAutoReconnect,
|
||||
} from "./pair-state.js";
|
||||
|
||||
@ -125,3 +126,81 @@ describe("decidePairListenerOnClose (back→re-pair flicker regression)", () =>
|
||||
).toBe("ignore-leaked-close");
|
||||
});
|
||||
});
|
||||
|
||||
describe("nextWarmingUpAfterEvent (pair-listener flag transitions)", () => {
|
||||
it("first qr from the live session clears warming-up", () => {
|
||||
expect(nextWarmingUpAfterEvent({ warmingUp: true, event: "qr" })).toBe(false);
|
||||
expect(nextWarmingUpAfterEvent({ warmingUp: false, event: "qr" })).toBe(false);
|
||||
});
|
||||
|
||||
it("first open from the live session clears warming-up", () => {
|
||||
expect(nextWarmingUpAfterEvent({ warmingUp: true, event: "open" })).toBe(false);
|
||||
expect(nextWarmingUpAfterEvent({ warmingUp: false, event: "open" })).toBe(false);
|
||||
});
|
||||
|
||||
it("RE-ARMS warming-up on a restart-required close (post-pair-success)", () => {
|
||||
// The regression: after the user scans, Baileys closes with status
|
||||
// 515 and the session-manager schedules a stop().then(start())
|
||||
// reconnect. That cleanup-stop emits a SECOND close that arrives
|
||||
// before the new socket reopens. If warming-up isn't re-armed
|
||||
// between the two closes, the second one resolves to
|
||||
// 'treat-as-timeout' and detaches the listener right at the
|
||||
// moment the user actually paired successfully — UI never gets
|
||||
// session.connected.
|
||||
expect(
|
||||
nextWarmingUpAfterEvent({ warmingUp: false, event: "close", restartRequired: true }),
|
||||
).toBe(true);
|
||||
expect(
|
||||
nextWarmingUpAfterEvent({ warmingUp: true, event: "close", restartRequired: true }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("plain close leaves warming-up unchanged", () => {
|
||||
// The pair-handler decides what to DO with a non-restart close
|
||||
// separately (decidePairListenerOnClose). The warming-up flag
|
||||
// doesn't change as a side effect — the listener either detaches
|
||||
// (treat-as-timeout) or already returned (ignore-leaked-close).
|
||||
expect(
|
||||
nextWarmingUpAfterEvent({ warmingUp: false, event: "close" }),
|
||||
).toBe(false);
|
||||
expect(
|
||||
nextWarmingUpAfterEvent({ warmingUp: true, event: "close" }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("end-to-end successful-pair sequence: warming → qr → restart-required close → open", () => {
|
||||
// Full lifecycle the helper has to thread correctly so the user
|
||||
// sees 'Account connected!' instead of 'Pairing timed out'.
|
||||
let warming = true; // freshly attached listener after a re-pair
|
||||
|
||||
// First QR arrives — clears the leak-protection flag.
|
||||
warming = nextWarmingUpAfterEvent({ warmingUp: warming, event: "qr" });
|
||||
expect(warming).toBe(false);
|
||||
|
||||
// User scans → Baileys closes with restartRequired=true.
|
||||
// Re-arms because session-manager will run another stop+start.
|
||||
warming = nextWarmingUpAfterEvent({
|
||||
warmingUp: warming,
|
||||
event: "close",
|
||||
restartRequired: true,
|
||||
});
|
||||
expect(warming).toBe(true);
|
||||
|
||||
// The cleanup-stop's second close arrives. The CALLER decides via
|
||||
// decidePairListenerOnClose to ignore it (warmingUp=true wins).
|
||||
expect(
|
||||
decidePairListenerOnClose({ warmingUp: warming, restartRequired: false }),
|
||||
).toBe("ignore-leaked-close");
|
||||
// Flag stays armed because a plain close doesn't change it.
|
||||
warming = nextWarmingUpAfterEvent({
|
||||
warmingUp: warming,
|
||||
event: "close",
|
||||
restartRequired: false,
|
||||
});
|
||||
expect(warming).toBe(true);
|
||||
|
||||
// Fresh socket opens with the new credentials → success.
|
||||
warming = nextWarmingUpAfterEvent({ warmingUp: warming, event: "open" });
|
||||
expect(warming).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@ -114,3 +114,30 @@ export function decidePairListenerOnClose(input: {
|
||||
if (input.restartRequired) return "post-pair-restart";
|
||||
return "treat-as-timeout";
|
||||
}
|
||||
|
||||
/**
|
||||
* Step the pair-listener's warming-up flag forward through one Baileys
|
||||
* event. Captures three rules in one place so they're test-locked:
|
||||
*
|
||||
* - First `qr` / `open` from the live session clears warming-up
|
||||
* (we've seen real session activity, future closes are real).
|
||||
* - `close + restartRequired` (post-pair-success / status 515)
|
||||
* RE-ARMS warming-up. The session-manager will schedule a
|
||||
* `stop().then(start())` reconnect; that stop emits a second close
|
||||
* before the new socket reopens. Without re-arming, the leaked
|
||||
* close from the cleanup-stop reaches us with warming-up=false and
|
||||
* resolves to `treat-as-timeout` — detaching the listener right at
|
||||
* the moment the user actually paired successfully (regression).
|
||||
* - Any other `close` keeps warming-up unchanged (the listener
|
||||
* either ignored it because we're warming, or processed it as a
|
||||
* real timeout / restart and is leaving the loop anyway).
|
||||
*/
|
||||
export function nextWarmingUpAfterEvent(input: {
|
||||
warmingUp: boolean;
|
||||
event: "qr" | "open" | "close";
|
||||
restartRequired?: boolean;
|
||||
}): boolean {
|
||||
if (input.event === "qr" || input.event === "open") return false;
|
||||
if (input.event === "close" && input.restartRequired) return true;
|
||||
return input.warmingUp;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user