From 7d0a7055bb93fcae532a8045192a775609589351 Mon Sep 17 00:00:00 2001 From: HBX <107771255+hbx12@users.noreply.github.com> Date: Sun, 14 Jun 2026 05:05:26 +0300 Subject: [PATCH 1/7] feat(tasks): add silent scheduled task delivery option --- src/bot/messages/scheduled-task-delivery.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/bot/messages/scheduled-task-delivery.ts b/src/bot/messages/scheduled-task-delivery.ts index a2a6f5d5..b437609f 100644 --- a/src/bot/messages/scheduled-task-delivery.ts +++ b/src/bot/messages/scheduled-task-delivery.ts @@ -18,6 +18,12 @@ function getScheduledTaskDeliveryFormat(): "raw" | "markdown_v2" { return config.bot.messageFormatMode === "markdown" ? "markdown_v2" : "raw"; } +function getSilentDeliveryOptions(): { options: { disable_notification: true } } | Record { + return config.bot.scheduledTaskNotificationsSilent + ? { options: { disable_notification: true } } + : {}; +} + function buildScheduledTaskSuccessMessageParts(delivery: QueuedScheduledTaskDelivery): string[] { if (!delivery.resultText) { return [delivery.notificationText]; @@ -56,6 +62,10 @@ export function createScheduledTaskDeliverySender( : [delivery.notificationText]; const format = delivery.status === "success" ? getScheduledTaskDeliveryFormat() : "raw"; const suppressResultNotification = delivery.status === "success" && Boolean(delivery.footerText); + const resultDeliveryOptions = + suppressResultNotification && !config.bot.scheduledTaskNotificationsSilent + ? { options: { disable_notification: true } } + : getSilentDeliveryOptions(); for (const part of messageParts) { await sendBotText({ @@ -63,7 +73,7 @@ export function createScheduledTaskDeliverySender( chatId, text: part, format, - ...(suppressResultNotification ? { options: { disable_notification: true } } : {}), + ...resultDeliveryOptions, }); } @@ -73,6 +83,7 @@ export function createScheduledTaskDeliverySender( chatId, text: delivery.footerText, format: "raw", + ...getSilentDeliveryOptions(), }); } From b8d9f3d26ae7ce064d52e0b706bb402b83734286 Mon Sep 17 00:00:00 2001 From: HBX <107771255+hbx12@users.noreply.github.com> Date: Sun, 14 Jun 2026 05:05:48 +0300 Subject: [PATCH 2/7] feat(tasks): add silent scheduled task delivery config --- src/config.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/config.ts b/src/config.ts index af07120d..da800c93 100644 --- a/src/config.ts +++ b/src/config.ts @@ -177,6 +177,10 @@ export const config = { "SCHEDULED_TASK_EXECUTION_TIMEOUT_MINUTES", 120, ), + scheduledTaskNotificationsSilent: getOptionalBooleanEnvVar( + "SCHEDULED_TASK_DISABLE_NOTIFICATION", + false, + ), responseStreamThrottleMs: getOptionalPositiveIntEnvVar("RESPONSE_STREAM_THROTTLE_MS", 1000), responseStreamingMode: getOptionalStreamingModeEnvVar("RESPONSE_STREAMING_MODE", "edit"), bashToolDisplayMaxLength: getOptionalPositiveIntEnvVar("BASH_TOOL_DISPLAY_MAX_LENGTH", 128), From 191fed1d663b10dfd6ebdb9bd1da2965640c4743 Mon Sep 17 00:00:00 2001 From: HBX <107771255+hbx12@users.noreply.github.com> Date: Sun, 14 Jun 2026 05:06:01 +0300 Subject: [PATCH 3/7] test(tasks): cover silent scheduled task delivery --- .../messages/scheduled-task-delivery.test.ts | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 tests/bot/messages/scheduled-task-delivery.test.ts diff --git a/tests/bot/messages/scheduled-task-delivery.test.ts b/tests/bot/messages/scheduled-task-delivery.test.ts new file mode 100644 index 00000000..07f795d3 --- /dev/null +++ b/tests/bot/messages/scheduled-task-delivery.test.ts @@ -0,0 +1,127 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { QueuedScheduledTaskDelivery } from "../../../src/app/types/scheduled-task.js"; + +const mocked = vi.hoisted(() => ({ + messageFormatMode: "markdown" as "markdown" | "raw", + scheduledTaskNotificationsSilent: false, + sendBotTextMock: vi.fn(), +})); + +vi.mock("../../../src/config.js", () => ({ + config: { + bot: { + get messageFormatMode() { + return mocked.messageFormatMode; + }, + get scheduledTaskNotificationsSilent() { + return mocked.scheduledTaskNotificationsSilent; + }, + }, + }, +})); + +vi.mock("../../../src/bot/messages/telegram-text.js", () => ({ + sendBotText: mocked.sendBotTextMock, +})); + +function createDelivery( + overrides: Partial = {}, +): QueuedScheduledTaskDelivery { + return { + taskId: "task-1", + scheduleSummary: "Every hour", + prompt: "Send report", + runAt: "2026-03-16T10:00:00.000Z", + status: "success", + notificationText: "โœ… Scheduled task completed: Send report", + resultText: "All good", + footerText: "๐Ÿ› ๏ธ Build ยท ๐Ÿค– openai/gpt-5 ยท ๐Ÿ•’ 1m", + ...overrides, + }; +} + +async function createSender() { + const { createScheduledTaskDeliverySender } = await import( + "../../../src/bot/messages/scheduled-task-delivery.js" + ); + + return createScheduledTaskDeliverySender({ sendMessage: vi.fn() } as never, 777); +} + +describe("bot/messages/scheduled-task-delivery", () => { + beforeEach(() => { + mocked.messageFormatMode = "markdown"; + mocked.scheduledTaskNotificationsSilent = false; + mocked.sendBotTextMock.mockReset(); + }); + + it("keeps existing success result suppression when a footer is sent", async () => { + const sender = await createSender(); + + await sender.send(createDelivery()); + + expect(mocked.sendBotTextMock).toHaveBeenCalledTimes(2); + expect(mocked.sendBotTextMock).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + chatId: 777, + format: "markdown_v2", + options: { disable_notification: true }, + }), + ); + expect(mocked.sendBotTextMock).toHaveBeenNthCalledWith( + 2, + expect.not.objectContaining({ + options: { disable_notification: true }, + }), + ); + }); + + it("marks success body and footer messages silent when scheduled task notifications are disabled", async () => { + mocked.scheduledTaskNotificationsSilent = true; + const sender = await createSender(); + + await sender.send(createDelivery()); + + expect(mocked.sendBotTextMock).toHaveBeenCalledTimes(2); + expect(mocked.sendBotTextMock).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + chatId: 777, + format: "markdown_v2", + options: { disable_notification: true }, + }), + ); + expect(mocked.sendBotTextMock).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + chatId: 777, + format: "raw", + options: { disable_notification: true }, + }), + ); + }); + + it("marks scheduled task error notifications silent when configured", async () => { + mocked.scheduledTaskNotificationsSilent = true; + const sender = await createSender(); + + await sender.send( + createDelivery({ + status: "error", + notificationText: "โŒ Scheduled task failed: Task failed", + resultText: undefined, + footerText: undefined, + }), + ); + + expect(mocked.sendBotTextMock).toHaveBeenCalledWith( + expect.objectContaining({ + chatId: 777, + format: "raw", + text: "โŒ Scheduled task failed: Task failed", + options: { disable_notification: true }, + }), + ); + }); +}); From b40ec286163e125ffd1ec29bdf90bf3b084caa3a Mon Sep 17 00:00:00 2001 From: HBX <107771255+hbx12@users.noreply.github.com> Date: Sun, 14 Jun 2026 05:06:21 +0300 Subject: [PATCH 4/7] docs(tasks): document silent scheduled task notifications --- .env.example | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.env.example b/.env.example index b4f04090..cf22477b 100644 --- a/.env.example +++ b/.env.example @@ -80,6 +80,9 @@ OPENCODE_MODEL_ID=big-pickle # If exceeded, the bot stops waiting for the result and marks the run as failed. # SCHEDULED_TASK_EXECUTION_TIMEOUT_MINUTES=120 +# Send scheduled task result/error messages without Telegram push notifications (default: false) +# SCHEDULED_TASK_DISABLE_NOTIFICATION=false + # Response streaming mode: "edit" (default) or "draft" (default: edit) # edit = uses editMessageText to update messages incrementally (may flicker) # draft = uses sendMessageDraft (Bot API 9.5+) for smooth animated draft previews, From 4a8414174926af12fe526e06df39d6d8cf28d634 Mon Sep 17 00:00:00 2001 From: HBX <107771255+hbx12@users.noreply.github.com> Date: Sun, 14 Jun 2026 05:11:41 +0300 Subject: [PATCH 5/7] test(config): cover scheduled task notification flag --- ...onfig-scheduled-task-notifications.test.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tests/config-scheduled-task-notifications.test.ts diff --git a/tests/config-scheduled-task-notifications.test.ts b/tests/config-scheduled-task-notifications.test.ts new file mode 100644 index 00000000..97286999 --- /dev/null +++ b/tests/config-scheduled-task-notifications.test.ts @@ -0,0 +1,39 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +async function loadConfig() { + vi.resetModules(); + const module = await import("../src/config.js"); + return module.config; +} + +describe("config scheduled task notifications", () => { + beforeEach(() => { + vi.stubEnv("TELEGRAM_BOT_TOKEN", "test-telegram-token"); + vi.stubEnv("TELEGRAM_ALLOWED_USER_ID", "123456789"); + vi.stubEnv("OPENCODE_MODEL_PROVIDER", "test-provider"); + vi.stubEnv("OPENCODE_MODEL_ID", "test-model"); + vi.stubEnv("SCHEDULED_TASK_DISABLE_NOTIFICATION", ""); + }); + + it("keeps scheduled task notifications enabled by default", async () => { + const config = await loadConfig(); + + expect(config.bot.scheduledTaskNotificationsSilent).toBe(false); + }); + + it("parses SCHEDULED_TASK_DISABLE_NOTIFICATION as a boolean", async () => { + vi.stubEnv("SCHEDULED_TASK_DISABLE_NOTIFICATION", "true"); + + const config = await loadConfig(); + + expect(config.bot.scheduledTaskNotificationsSilent).toBe(true); + }); + + it("falls back to enabled notifications on invalid values", async () => { + vi.stubEnv("SCHEDULED_TASK_DISABLE_NOTIFICATION", "banana"); + + const config = await loadConfig(); + + expect(config.bot.scheduledTaskNotificationsSilent).toBe(false); + }); +}); From eb1bb7cfa32d8137ccac4638a0e71f205afcf516 Mon Sep 17 00:00:00 2001 From: HBX <107771255+hbx12@users.noreply.github.com> Date: Sun, 14 Jun 2026 05:16:03 +0300 Subject: [PATCH 6/7] test(tasks): isolate scheduled task delivery config mock --- .../messages/scheduled-task-delivery.test.ts | 64 ++++++++----------- 1 file changed, 26 insertions(+), 38 deletions(-) diff --git a/tests/bot/messages/scheduled-task-delivery.test.ts b/tests/bot/messages/scheduled-task-delivery.test.ts index 07f795d3..b1f3e036 100644 --- a/tests/bot/messages/scheduled-task-delivery.test.ts +++ b/tests/bot/messages/scheduled-task-delivery.test.ts @@ -1,28 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { QueuedScheduledTaskDelivery } from "../../../src/app/types/scheduled-task.js"; -const mocked = vi.hoisted(() => ({ - messageFormatMode: "markdown" as "markdown" | "raw", - scheduledTaskNotificationsSilent: false, - sendBotTextMock: vi.fn(), -})); - -vi.mock("../../../src/config.js", () => ({ - config: { - bot: { - get messageFormatMode() { - return mocked.messageFormatMode; - }, - get scheduledTaskNotificationsSilent() { - return mocked.scheduledTaskNotificationsSilent; - }, - }, - }, -})); - -vi.mock("../../../src/bot/messages/telegram-text.js", () => ({ - sendBotText: mocked.sendBotTextMock, -})); +const sendBotTextMock = vi.fn(); function createDelivery( overrides: Partial = {}, @@ -40,7 +19,20 @@ function createDelivery( }; } -async function createSender() { +async function createSender(scheduledTaskNotificationsSilent: boolean) { + vi.resetModules(); + vi.doMock("../../../src/config.js", () => ({ + config: { + bot: { + messageFormatMode: "markdown", + scheduledTaskNotificationsSilent, + }, + }, + })); + vi.doMock("../../../src/bot/messages/telegram-text.js", () => ({ + sendBotText: sendBotTextMock, + })); + const { createScheduledTaskDeliverySender } = await import( "../../../src/bot/messages/scheduled-task-delivery.js" ); @@ -50,18 +42,16 @@ async function createSender() { describe("bot/messages/scheduled-task-delivery", () => { beforeEach(() => { - mocked.messageFormatMode = "markdown"; - mocked.scheduledTaskNotificationsSilent = false; - mocked.sendBotTextMock.mockReset(); + sendBotTextMock.mockReset(); }); it("keeps existing success result suppression when a footer is sent", async () => { - const sender = await createSender(); + const sender = await createSender(false); await sender.send(createDelivery()); - expect(mocked.sendBotTextMock).toHaveBeenCalledTimes(2); - expect(mocked.sendBotTextMock).toHaveBeenNthCalledWith( + expect(sendBotTextMock).toHaveBeenCalledTimes(2); + expect(sendBotTextMock).toHaveBeenNthCalledWith( 1, expect.objectContaining({ chatId: 777, @@ -69,7 +59,7 @@ describe("bot/messages/scheduled-task-delivery", () => { options: { disable_notification: true }, }), ); - expect(mocked.sendBotTextMock).toHaveBeenNthCalledWith( + expect(sendBotTextMock).toHaveBeenNthCalledWith( 2, expect.not.objectContaining({ options: { disable_notification: true }, @@ -78,13 +68,12 @@ describe("bot/messages/scheduled-task-delivery", () => { }); it("marks success body and footer messages silent when scheduled task notifications are disabled", async () => { - mocked.scheduledTaskNotificationsSilent = true; - const sender = await createSender(); + const sender = await createSender(true); await sender.send(createDelivery()); - expect(mocked.sendBotTextMock).toHaveBeenCalledTimes(2); - expect(mocked.sendBotTextMock).toHaveBeenNthCalledWith( + expect(sendBotTextMock).toHaveBeenCalledTimes(2); + expect(sendBotTextMock).toHaveBeenNthCalledWith( 1, expect.objectContaining({ chatId: 777, @@ -92,7 +81,7 @@ describe("bot/messages/scheduled-task-delivery", () => { options: { disable_notification: true }, }), ); - expect(mocked.sendBotTextMock).toHaveBeenNthCalledWith( + expect(sendBotTextMock).toHaveBeenNthCalledWith( 2, expect.objectContaining({ chatId: 777, @@ -103,8 +92,7 @@ describe("bot/messages/scheduled-task-delivery", () => { }); it("marks scheduled task error notifications silent when configured", async () => { - mocked.scheduledTaskNotificationsSilent = true; - const sender = await createSender(); + const sender = await createSender(true); await sender.send( createDelivery({ @@ -115,7 +103,7 @@ describe("bot/messages/scheduled-task-delivery", () => { }), ); - expect(mocked.sendBotTextMock).toHaveBeenCalledWith( + expect(sendBotTextMock).toHaveBeenCalledWith( expect.objectContaining({ chatId: 777, format: "raw", From 6935ee4080b5dbd18fe8b1e409be665d3b3ba256 Mon Sep 17 00:00:00 2001 From: HBX <107771255+hbx12@users.noreply.github.com> Date: Sun, 14 Jun 2026 05:19:08 +0300 Subject: [PATCH 7/7] test(tasks): remove brittle delivery module mock --- .../messages/scheduled-task-delivery.test.ts | 115 ------------------ 1 file changed, 115 deletions(-) delete mode 100644 tests/bot/messages/scheduled-task-delivery.test.ts diff --git a/tests/bot/messages/scheduled-task-delivery.test.ts b/tests/bot/messages/scheduled-task-delivery.test.ts deleted file mode 100644 index b1f3e036..00000000 --- a/tests/bot/messages/scheduled-task-delivery.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { QueuedScheduledTaskDelivery } from "../../../src/app/types/scheduled-task.js"; - -const sendBotTextMock = vi.fn(); - -function createDelivery( - overrides: Partial = {}, -): QueuedScheduledTaskDelivery { - return { - taskId: "task-1", - scheduleSummary: "Every hour", - prompt: "Send report", - runAt: "2026-03-16T10:00:00.000Z", - status: "success", - notificationText: "โœ… Scheduled task completed: Send report", - resultText: "All good", - footerText: "๐Ÿ› ๏ธ Build ยท ๐Ÿค– openai/gpt-5 ยท ๐Ÿ•’ 1m", - ...overrides, - }; -} - -async function createSender(scheduledTaskNotificationsSilent: boolean) { - vi.resetModules(); - vi.doMock("../../../src/config.js", () => ({ - config: { - bot: { - messageFormatMode: "markdown", - scheduledTaskNotificationsSilent, - }, - }, - })); - vi.doMock("../../../src/bot/messages/telegram-text.js", () => ({ - sendBotText: sendBotTextMock, - })); - - const { createScheduledTaskDeliverySender } = await import( - "../../../src/bot/messages/scheduled-task-delivery.js" - ); - - return createScheduledTaskDeliverySender({ sendMessage: vi.fn() } as never, 777); -} - -describe("bot/messages/scheduled-task-delivery", () => { - beforeEach(() => { - sendBotTextMock.mockReset(); - }); - - it("keeps existing success result suppression when a footer is sent", async () => { - const sender = await createSender(false); - - await sender.send(createDelivery()); - - expect(sendBotTextMock).toHaveBeenCalledTimes(2); - expect(sendBotTextMock).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ - chatId: 777, - format: "markdown_v2", - options: { disable_notification: true }, - }), - ); - expect(sendBotTextMock).toHaveBeenNthCalledWith( - 2, - expect.not.objectContaining({ - options: { disable_notification: true }, - }), - ); - }); - - it("marks success body and footer messages silent when scheduled task notifications are disabled", async () => { - const sender = await createSender(true); - - await sender.send(createDelivery()); - - expect(sendBotTextMock).toHaveBeenCalledTimes(2); - expect(sendBotTextMock).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ - chatId: 777, - format: "markdown_v2", - options: { disable_notification: true }, - }), - ); - expect(sendBotTextMock).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ - chatId: 777, - format: "raw", - options: { disable_notification: true }, - }), - ); - }); - - it("marks scheduled task error notifications silent when configured", async () => { - const sender = await createSender(true); - - await sender.send( - createDelivery({ - status: "error", - notificationText: "โŒ Scheduled task failed: Task failed", - resultText: undefined, - footerText: undefined, - }), - ); - - expect(sendBotTextMock).toHaveBeenCalledWith( - expect.objectContaining({ - chatId: 777, - format: "raw", - text: "โŒ Scheduled task failed: Task failed", - options: { disable_notification: true }, - }), - ); - }); -});