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 { syncGroupsForAccount } from "../whatsapp/group-sync.js";
|
||||||
import { writeAuditLog } from "../audit.js";
|
import { writeAuditLog } from "../audit.js";
|
||||||
import { pgNotifyWeb } from "./notify.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 PAIR_TIMEOUT_MS = 5 * 60 * 1000;
|
||||||
const offByAccount = new Map<string, () => void>();
|
const offByAccount = new Map<string, () => void>();
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import {
|
|||||||
decideOnPairClose,
|
decideOnPairClose,
|
||||||
decideOnPairTimeout,
|
decideOnPairTimeout,
|
||||||
decidePairListenerOnClose,
|
decidePairListenerOnClose,
|
||||||
|
nextWarmingUpAfterEvent,
|
||||||
shouldAutoReconnect,
|
shouldAutoReconnect,
|
||||||
} from "./pair-state.js";
|
} from "./pair-state.js";
|
||||||
|
|
||||||
@ -125,3 +126,81 @@ describe("decidePairListenerOnClose (back→re-pair flicker regression)", () =>
|
|||||||
).toBe("ignore-leaked-close");
|
).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";
|
if (input.restartRequired) return "post-pair-restart";
|
||||||
return "treat-as-timeout";
|
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