mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +00:00
fix(gateway): clear stale Slack socket state after disconnect (#39083)
* fix(gateway): restore stale-socket recovery * test(slack): cover clean socket disconnect status
This commit is contained in:
@@ -174,7 +174,7 @@ describe("evaluateChannelHealth", () => {
|
||||
},
|
||||
{
|
||||
channelId: "slack",
|
||||
now: 100_000,
|
||||
now: 75_000,
|
||||
channelConnectGraceMs: 10_000,
|
||||
staleEventThresholdMs: 30_000,
|
||||
},
|
||||
@@ -194,13 +194,33 @@ describe("evaluateChannelHealth", () => {
|
||||
},
|
||||
{
|
||||
channelId: "slack",
|
||||
now: 100_000,
|
||||
now: 75_000,
|
||||
channelConnectGraceMs: 10_000,
|
||||
staleEventThresholdMs: 30_000,
|
||||
},
|
||||
);
|
||||
expect(evaluation).toEqual({ healthy: true, reason: "healthy" });
|
||||
});
|
||||
|
||||
it("flags inherited event timestamps after the lifecycle exceeds the stale threshold", () => {
|
||||
const evaluation = evaluateChannelHealth(
|
||||
{
|
||||
running: true,
|
||||
connected: true,
|
||||
enabled: true,
|
||||
configured: true,
|
||||
lastStartAt: 50_000,
|
||||
lastEventAt: 10_000,
|
||||
},
|
||||
{
|
||||
channelId: "slack",
|
||||
now: 140_000,
|
||||
channelConnectGraceMs: 10_000,
|
||||
staleEventThresholdMs: 30_000,
|
||||
},
|
||||
);
|
||||
expect(evaluation).toEqual({ healthy: false, reason: "stale-socket" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveChannelRestartReason", () => {
|
||||
|
||||
@@ -109,7 +109,11 @@ export function evaluateChannelHealth(
|
||||
snapshot.lastEventAt != null
|
||||
) {
|
||||
if (lastStartAt != null && snapshot.lastEventAt < lastStartAt) {
|
||||
return { healthy: true, reason: "healthy" };
|
||||
const lifecycleEventGap = Math.max(0, policy.now - lastStartAt);
|
||||
if (lifecycleEventGap <= policy.staleEventThresholdMs) {
|
||||
return { healthy: true, reason: "healthy" };
|
||||
}
|
||||
return { healthy: false, reason: "stale-socket" };
|
||||
}
|
||||
const eventAge = policy.now - snapshot.lastEventAt;
|
||||
if (eventAge > policy.staleEventThresholdMs) {
|
||||
|
||||
@@ -38,6 +38,38 @@ describe("slack socket reconnect helpers", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("clears connected state when socket mode disconnects", () => {
|
||||
const setStatus = vi.fn();
|
||||
const err = new Error("dns down");
|
||||
|
||||
__testing.publishSlackDisconnectedStatus(setStatus, err);
|
||||
|
||||
expect(setStatus).toHaveBeenCalledTimes(1);
|
||||
expect(setStatus).toHaveBeenCalledWith({
|
||||
connected: false,
|
||||
lastDisconnect: {
|
||||
at: expect.any(Number),
|
||||
error: "dns down",
|
||||
},
|
||||
lastError: "dns down",
|
||||
});
|
||||
});
|
||||
|
||||
it("clears connected state without error when socket mode disconnects cleanly", () => {
|
||||
const setStatus = vi.fn();
|
||||
|
||||
__testing.publishSlackDisconnectedStatus(setStatus);
|
||||
|
||||
expect(setStatus).toHaveBeenCalledTimes(1);
|
||||
expect(setStatus).toHaveBeenCalledWith({
|
||||
connected: false,
|
||||
lastDisconnect: {
|
||||
at: expect.any(Number),
|
||||
},
|
||||
lastError: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves disconnect waiter on socket disconnect event", async () => {
|
||||
const client = new FakeEmitter();
|
||||
const app = { receiver: { client } };
|
||||
|
||||
@@ -77,6 +77,22 @@ function publishSlackConnectedStatus(setStatus?: (next: Record<string, unknown>)
|
||||
});
|
||||
}
|
||||
|
||||
function publishSlackDisconnectedStatus(
|
||||
setStatus?: (next: Record<string, unknown>) => void,
|
||||
error?: unknown,
|
||||
) {
|
||||
if (!setStatus) {
|
||||
return;
|
||||
}
|
||||
const at = Date.now();
|
||||
const message = error ? formatUnknownError(error) : undefined;
|
||||
setStatus({
|
||||
connected: false,
|
||||
lastDisconnect: message ? { at, error: message } : { at },
|
||||
lastError: message ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
const cfg = opts.config ?? loadConfig();
|
||||
const runtime: RuntimeEnv = opts.runtime ?? createNonExitingRuntime();
|
||||
@@ -440,6 +456,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
if (opts.abortSignal?.aborted) {
|
||||
break;
|
||||
}
|
||||
publishSlackDisconnectedStatus(opts.setStatus, disconnect.error);
|
||||
|
||||
// Bail immediately on non-recoverable auth errors during reconnect too.
|
||||
if (disconnect.error && isNonRecoverableSlackAuthError(disconnect.error)) {
|
||||
@@ -495,6 +512,7 @@ export { isNonRecoverableSlackAuthError } from "./reconnect-policy.js";
|
||||
|
||||
export const __testing = {
|
||||
publishSlackConnectedStatus,
|
||||
publishSlackDisconnectedStatus,
|
||||
resolveSlackRuntimeGroupPolicy: resolveOpenProviderRuntimeGroupPolicy,
|
||||
resolveDefaultGroupPolicy,
|
||||
getSocketEmitter,
|
||||
|
||||
Reference in New Issue
Block a user