bot/src/ipc/pair-state.ts (NEW)
Pure helpers for the pairing-lifecycle decisions, lifted out of
pair-handler so the rules are testable without Baileys / Postgres:
- decideOnPairClose({ current, loggedOut })
- decideOnPairTimeout({ current })
- shouldAutoReconnect({ loggedOut, hasEverConnected })
bot/src/ipc/pair-state.test.ts (NEW, 7 tests)
Locks in the regressions we just fixed:
- Non-loggedOut close from `pending` MUST settle as `unpaired`
(the row used to stay `pending` and disappear from the overview).
- logged_out close → `logged_out`.
- pair-window timeout parks still-`pending` rows; ignores rows
that already moved on.
- Auto-reconnect only kicks in for accounts that have been linked
at least once — guards against the 5-second QR refresh loop on
a fresh pair.
web/src/components/accounts-list-view.test.tsx
+ Test that the overview renders accounts in transient states
(pending, unpaired, disconnected) alongside connected ones — the
`pending` row was being hidden by listAccounts before this fix.
Bot: 24 tests passing (+7).
Web: 99 tests passing (+1).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
70 lines
2.5 KiB
TypeScript
70 lines
2.5 KiB
TypeScript
/**
|
|
* Pure helpers for pairing-lifecycle status transitions. Extracted so
|
|
* the rules are unit-testable without spinning up Baileys / Postgres.
|
|
*
|
|
* Key invariant the tests guard:
|
|
* - A failed or abandoned pair MUST NOT leave the row stuck in
|
|
* `pending`. It transitions to `unpaired` so the operator can see
|
|
* the account on the list with a Re-pair affordance.
|
|
* - Successful pairing transitions to `connected` (set by the
|
|
* session-manager on the `open` event — not this helper's job).
|
|
* - Auto-reconnect for transient drops only applies to accounts
|
|
* that have been linked at least once (`lastConnectedAt` set).
|
|
*/
|
|
|
|
export type AccountStatus =
|
|
| "pending"
|
|
| "unpaired"
|
|
| "connected"
|
|
| "disconnected"
|
|
| "logged_out"
|
|
| "banned";
|
|
|
|
export interface PairCloseInput {
|
|
/** Status of the account row at the moment the close event fires. */
|
|
current: AccountStatus;
|
|
/** Did Baileys signal a logged-out close (vs an ephemeral close)? */
|
|
loggedOut: boolean;
|
|
}
|
|
|
|
export interface StatusUpdate {
|
|
next: AccountStatus;
|
|
/** Wipe the cached QR PNG when the pair window closes. */
|
|
clearQrPng: boolean;
|
|
}
|
|
|
|
/**
|
|
* Decide the status transition when the Baileys session closes during
|
|
* a pairing attempt (i.e. before the user has scanned the QR).
|
|
*
|
|
* - logged_out close → terminal: `logged_out`.
|
|
* - ephemeral close (refs exhausted, network blip, etc.) → park as
|
|
* `unpaired` so the row stays visible and the user can retry.
|
|
*/
|
|
export function decideOnPairClose({ current, loggedOut }: PairCloseInput): StatusUpdate {
|
|
if (loggedOut) {
|
|
return { next: "logged_out", clearQrPng: true };
|
|
}
|
|
// Whatever transient state we were in (most often `pending`), park
|
|
// the row as `unpaired` — anything else hides it from the operator.
|
|
return { next: "unpaired", clearQrPng: true };
|
|
}
|
|
|
|
/** Whether the session-manager should auto-reconnect after a non-loggedOut close. */
|
|
export function shouldAutoReconnect(args: {
|
|
loggedOut: boolean;
|
|
/** True if the account row has `last_connected_at` set (has been linked before). */
|
|
hasEverConnected: boolean;
|
|
}): boolean {
|
|
if (args.loggedOut) return false;
|
|
return args.hasEverConnected;
|
|
}
|
|
|
|
/** Decide what happens when the 5-min pair-window timeout fires. */
|
|
export function decideOnPairTimeout({ current }: { current: AccountStatus }): StatusUpdate | null {
|
|
// Only the still-pending rows need cleanup. Anything else has already
|
|
// moved on (connected, unpaired by an earlier close, etc.).
|
|
if (current !== "pending") return null;
|
|
return { next: "unpaired", clearQrPng: true };
|
|
}
|