fix(voice): persist WhatsApp last route

main
Peter Steinberger 2025-12-12 17:27:48 +00:00
parent 7dab927260
commit 6e9d3092a7
2 changed files with 86 additions and 9 deletions

View File

@ -202,6 +202,58 @@ describe("partial reply gating", () => {
expect(reply).toHaveBeenCalledWith("final reply"); expect(reply).toHaveBeenCalledWith("final reply");
}); });
it("updates last-route for direct chats without senderE164", async () => {
const now = Date.now();
const store = await makeSessionStore({
main: { sessionId: "sid", updatedAt: now - 1 },
});
const replyResolver = vi.fn().mockResolvedValue(undefined);
const mockConfig: ClawdisConfig = {
inbound: {
allowFrom: ["*"],
reply: {
mode: "command",
session: { store: store.storePath, mainKey: "main" },
},
},
};
setLoadConfigMock(mockConfig);
await monitorWebProvider(
false,
async ({ onMessage }) => {
await onMessage({
id: "m1",
from: "+1000",
conversationId: "+1000",
to: "+2000",
body: "hello",
timestamp: now,
chatType: "direct",
chatId: "direct:+1000",
sendComposing: vi.fn().mockResolvedValue(undefined),
reply: vi.fn().mockResolvedValue(undefined),
sendMedia: vi.fn().mockResolvedValue(undefined),
});
return { close: vi.fn().mockResolvedValue(undefined) };
},
false,
replyResolver,
);
const stored = JSON.parse(await fs.readFile(store.storePath, "utf8")) as {
main?: { lastChannel?: string; lastTo?: string };
};
expect(stored.main?.lastChannel).toBe("whatsapp");
expect(stored.main?.lastTo).toBe("+1000");
resetLoadConfigMock();
await store.cleanup();
});
it("defaults to self-only when no config is present", async () => { it("defaults to self-only when no config is present", async () => {
const cfg: ClawdisConfig = { const cfg: ClawdisConfig = {
inbound: { inbound: {
@ -661,6 +713,10 @@ describe("web auto-reply", () => {
const originalMax = process.getMaxListeners(); const originalMax = process.getMaxListeners();
process.setMaxListeners?.(1); // force low to confirm bump process.setMaxListeners?.(1); // force low to confirm bump
const store = await makeSessionStore({
main: { sessionId: "sid", updatedAt: Date.now() },
});
const sendMedia = vi.fn(); const sendMedia = vi.fn();
const reply = vi.fn().mockResolvedValue(undefined); const reply = vi.fn().mockResolvedValue(undefined);
const sendComposing = vi.fn(); const sendComposing = vi.fn();
@ -684,7 +740,12 @@ describe("web auto-reply", () => {
.spyOn(commandQueue, "getQueueSize") .spyOn(commandQueue, "getQueueSize")
.mockImplementation(() => (queueBusy ? 1 : 0)); .mockImplementation(() => (queueBusy ? 1 : 0));
setLoadConfigMock(() => ({ inbound: { timestampPrefix: "UTC" } })); setLoadConfigMock(() => ({
inbound: {
timestampPrefix: "UTC",
reply: { mode: "command", session: { store: store.storePath } },
},
}));
await monitorWebProvider(false, listenerFactory, false, resolver); await monitorWebProvider(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined(); expect(capturedOnMessage).toBeDefined();
@ -713,7 +774,7 @@ describe("web auto-reply", () => {
// Let the queued batch flush once the queue is free // Let the queued batch flush once the queue is free
queueBusy = false; queueBusy = false;
vi.advanceTimersByTime(200); await vi.advanceTimersByTimeAsync(200);
expect(resolver).toHaveBeenCalledTimes(1); expect(resolver).toHaveBeenCalledTimes(1);
const args = resolver.mock.calls[0][0]; const args = resolver.mock.calls[0][0];
@ -730,6 +791,7 @@ describe("web auto-reply", () => {
queueSpy.mockRestore(); queueSpy.mockRestore();
process.setMaxListeners?.(originalMax); process.setMaxListeners?.(originalMax);
vi.useRealTimers(); vi.useRealTimers();
await store.cleanup();
}); });
it("falls back to text when media send fails", async () => { it("falls back to text when media send fails", async () => {

View File

@ -783,7 +783,7 @@ export async function monitorWebProvider(
const batch = pendingBatches.get(conversationId); const batch = pendingBatches.get(conversationId);
if (!batch || batch.messages.length === 0) return; if (!batch || batch.messages.length === 0) return;
if (getQueueSize() > 0) { if (getQueueSize() > 0) {
batch.timer = setTimeout(() => void processBatch(conversationId), 150); batch.timer = setTimeout(() => processBatch(conversationId), 150);
return; return;
} }
pendingBatches.delete(conversationId); pendingBatches.delete(conversationId);
@ -854,15 +854,25 @@ export async function monitorWebProvider(
const sessionCfg = cfg.inbound?.reply?.session; const sessionCfg = cfg.inbound?.reply?.session;
const mainKey = (sessionCfg?.mainKey ?? "main").trim() || "main"; const mainKey = (sessionCfg?.mainKey ?? "main").trim() || "main";
const storePath = resolveStorePath(sessionCfg?.store); const storePath = resolveStorePath(sessionCfg?.store);
const to = latest.senderE164 const to = (() => {
? normalizeE164(latest.senderE164) if (latest.senderE164) return normalizeE164(latest.senderE164);
: jidToE164(latest.from); // In direct chats, `latest.from` is already the canonical conversation id,
// which is an E.164 string (e.g. "+1555"). Only fall back to JID parsing
// when we were handed a JID-like string.
if (latest.from.includes("@")) return jidToE164(latest.from);
return normalizeE164(latest.from);
})();
if (to) { if (to) {
await updateLastRoute({ void updateLastRoute({
storePath, storePath,
sessionKey: mainKey, sessionKey: mainKey,
channel: "whatsapp", channel: "whatsapp",
to, to,
}).catch((err) => {
replyLogger.warn(
{ error: String(err), storePath, sessionKey: mainKey, to },
"failed updating last route",
);
}); });
} }
} }
@ -969,8 +979,7 @@ export async function monitorWebProvider(
if (getQueueSize() === 0) { if (getQueueSize() === 0) {
await processBatch(key); await processBatch(key);
} else { } else {
bucket.timer = bucket.timer = bucket.timer ?? setTimeout(() => processBatch(key), 150);
bucket.timer ?? setTimeout(() => void processBatch(key), 150);
} }
}; };
@ -1401,6 +1410,12 @@ export async function monitorWebProvider(
}, },
"web reconnect: max attempts reached; continuing in degraded mode", "web reconnect: max attempts reached; continuing in degraded mode",
); );
runtime.error(
danger(
`WhatsApp Web reconnect: max attempts reached (${reconnectAttempts}/${reconnectPolicy.maxAttempts}). Stopping web monitoring.`,
),
);
await closeListener();
break; break;
} }