fix(voice): persist WhatsApp last route
parent
7dab927260
commit
6e9d3092a7
|
|
@ -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 () => {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue