feat(heartbeat): allow manual message and dry-run for web/twilio

This commit is contained in:
Peter Steinberger
2025-11-28 08:14:07 +01:00
parent 84f2595349
commit 12d7be7cad
5 changed files with 300 additions and 20 deletions

View File

@@ -351,6 +351,45 @@ describe("runWebHeartbeatOnce", () => {
expect(stored["+1999"]?.sessionId).toBe(sessionId);
expect(stored["+1999"]?.updatedAt).toBeDefined();
});
it("sends overrideBody directly and skips resolver", async () => {
const sender: typeof sendMessageWeb = vi
.fn()
.mockResolvedValue({ messageId: "m1", toJid: "jid" });
const resolver = vi.fn();
setLoadConfigMock({
inbound: { allowFrom: ["+1555"], reply: { mode: "command" } },
});
await runWebHeartbeatOnce({
to: "+1555",
verbose: false,
sender,
replyResolver: resolver,
overrideBody: "manual ping",
});
expect(sender).toHaveBeenCalledWith("+1555", "manual ping", {
verbose: false,
});
expect(resolver).not.toHaveBeenCalled();
});
it("dry-run overrideBody prints and skips send", async () => {
const sender: typeof sendMessageWeb = vi.fn();
const resolver = vi.fn();
setLoadConfigMock({
inbound: { allowFrom: ["+1555"], reply: { mode: "command" } },
});
await runWebHeartbeatOnce({
to: "+1555",
verbose: false,
sender,
replyResolver: resolver,
overrideBody: "dry",
dryRun: true,
});
expect(sender).not.toHaveBeenCalled();
expect(resolver).not.toHaveBeenCalled();
});
});
describe("web auto-reply", () => {

View File

@@ -81,8 +81,17 @@ export async function runWebHeartbeatOnce(opts: {
runtime?: RuntimeEnv;
sender?: typeof sendMessageWeb;
sessionId?: string;
overrideBody?: string;
dryRun?: boolean;
}) {
const { cfg: cfgOverride, to, verbose = false, sessionId } = opts;
const {
cfg: cfgOverride,
to,
verbose = false,
sessionId,
overrideBody,
dryRun = false,
} = opts;
const _runtime = opts.runtime ?? defaultRuntime;
const replyResolver = opts.replyResolver ?? getReplyFromConfig;
const sender = opts.sender ?? sendMessageWeb;
@@ -118,7 +127,38 @@ export async function runWebHeartbeatOnce(opts: {
);
}
if (overrideBody && overrideBody.trim().length === 0) {
throw new Error("Override body must be non-empty when provided.");
}
try {
if (overrideBody) {
if (dryRun) {
console.log(
success(
`[dry-run] web send -> ${to}: ${overrideBody.trim()} (manual message)`,
),
);
return;
}
const sendResult = await sender(to, overrideBody, { verbose });
heartbeatLogger.info(
{
to,
messageId: sendResult.messageId,
chars: overrideBody.length,
reason: "manual-message",
},
"manual heartbeat message sent",
);
console.log(
success(
`sent manual message to ${to} (web), id ${sendResult.messageId}`,
),
);
return;
}
const replyResult = await replyResolver(
{
Body: HEARTBEAT_PROMPT,
@@ -177,6 +217,17 @@ export async function runWebHeartbeatOnce(opts: {
}
const finalText = stripped.text || replyResult.text || "";
if (dryRun) {
heartbeatLogger.info(
{ to, reason: "dry-run", chars: finalText.length },
"heartbeat dry-run",
);
console.log(
success(`[dry-run] heartbeat -> ${to}: ${finalText.slice(0, 200)}`),
);
return;
}
const sendResult = await sender(to, finalText, { verbose });
heartbeatLogger.info(
{ to, messageId: sendResult.messageId, chars: finalText.length },