diff --git a/CHANGELOG.md b/CHANGELOG.md index a5ddab84b5..54e4028508 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,7 +72,7 @@ Docs: https://docs.openclaw.ai - Sessions/Maintenance: archive transcripts when pruning stale sessions, clean expired media in subdirectories, and purge `.deleted` transcript archives after the prune window to prevent disk leaks. (#18538) - Infra/Fetch: ensure foreign abort-signal listener cleanup never masks original fetch successes/failures, while still preventing detached-finally unhandled rejection noise in `wrapFetchWithAbortSignal`. Thanks @Jackten. - Heartbeat: allow suppressing tool error warning payloads during heartbeat runs via a new heartbeat config flag. (#18497) Thanks @thewilloftheshadow. -- Heartbeat: include sender metadata (From/To/Provider) in heartbeat prompts so model context matches the delivery target. (#18532) +- Heartbeat: include sender metadata (From/To/Provider) in heartbeat prompts so model context matches the delivery target. (#18532) Thanks @dinakars777. - Heartbeat/Telegram: strip configured `responsePrefix` before heartbeat ack detection (with boundary-safe matching) so prefixed `HEARTBEAT_OK` replies are correctly suppressed instead of leaking into DMs. (#18602) ## 2026.2.15 diff --git a/src/infra/heartbeat-runner.model-override.test.ts b/src/infra/heartbeat-runner.model-override.test.ts index 434feee7d2..4b7f7db3db 100644 --- a/src/infra/heartbeat-runner.model-override.test.ts +++ b/src/infra/heartbeat-runner.model-override.test.ts @@ -2,12 +2,12 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../config/config.js"; import { telegramPlugin } from "../../extensions/telegram/src/channel.js"; import { setTelegramRuntime } from "../../extensions/telegram/src/runtime.js"; import { whatsappPlugin } from "../../extensions/whatsapp/src/channel.js"; import { setWhatsAppRuntime } from "../../extensions/whatsapp/src/runtime.js"; import * as replyModule from "../auto-reply/reply.js"; -import type { OpenClawConfig } from "../config/config.js"; import { resolveAgentMainSessionKey, resolveMainSessionKey } from "../config/sessions.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; import { createPluginRuntime } from "../plugins/runtime/index.js"; @@ -75,7 +75,10 @@ afterEach(() => { }); describe("runHeartbeatOnce – heartbeat model override", () => { - async function runDefaultsHeartbeat(params: { model?: string }) { + async function runDefaultsHeartbeat(params: { + model?: string; + suppressToolErrorWarnings?: boolean; + }) { return withHeartbeatFixture(async ({ tmpDir, storePath, seedSession }) => { const cfg: OpenClawConfig = { agents: { @@ -85,6 +88,7 @@ describe("runHeartbeatOnce – heartbeat model override", () => { every: "5m", target: "whatsapp", model: params.model, + suppressToolErrorWarnings: params.suppressToolErrorWarnings, }, }, }, @@ -121,6 +125,16 @@ describe("runHeartbeatOnce – heartbeat model override", () => { ); }); + it("passes suppressToolErrorWarnings when configured", async () => { + const replyOpts = await runDefaultsHeartbeat({ suppressToolErrorWarnings: true }); + expect(replyOpts).toEqual( + expect.objectContaining({ + isHeartbeat: true, + suppressToolErrorWarnings: true, + }), + ); + }); + it("passes per-agent heartbeat model override (merged with defaults)", async () => { await withHeartbeatFixture(async ({ storePath, seedSession }) => { const cfg: OpenClawConfig = { diff --git a/src/memory/search-manager.test.ts b/src/memory/search-manager.test.ts index 9e47e20162..8ab25ef92c 100644 --- a/src/memory/search-manager.test.ts +++ b/src/memory/search-manager.test.ts @@ -70,7 +70,8 @@ vi.mock("./manager.js", () => ({ import { QmdMemoryManager } from "./qmd-manager.js"; import { getMemorySearchManager } from "./search-manager.js"; -const createQmdManagerMock = vi.mocked(QmdMemoryManager.create.bind(QmdMemoryManager)); +// eslint-disable-next-line @typescript-eslint/unbound-method -- mocked static function +const createQmdManagerMock = vi.mocked(QmdMemoryManager.create); type SearchManagerResult = Awaited>; type SearchManager = NonNullable; diff --git a/src/process/exec.test.ts b/src/process/exec.test.ts index e9a9754e8e..7504977a3b 100644 --- a/src/process/exec.test.ts +++ b/src/process/exec.test.ts @@ -63,7 +63,8 @@ describe("runCommandWithTimeout", () => { }, ); - expect(result.code).toBe(0); + expect(result.signal).toBeNull(); + expect(result.code ?? 0).toBe(0); expect(result.termination).toBe("exit"); expect(result.noOutputTimedOut).toBe(false); expect(result.stdout.length).toBeGreaterThanOrEqual(2); diff --git a/src/telegram/webhook.test.ts b/src/telegram/webhook.test.ts index 538c065356..2c943a4be6 100644 --- a/src/telegram/webhook.test.ts +++ b/src/telegram/webhook.test.ts @@ -1,20 +1,23 @@ import { describe, expect, it, vi } from "vitest"; import { startTelegramWebhook } from "./webhook.js"; -const handlerSpy = vi.fn( - (_req: unknown, res: { writeHead: (status: number) => void; end: (body?: string) => void }) => { - res.writeHead(200); - res.end("ok"); - }, +const handlerSpy = vi.hoisted(() => + vi.fn( + (_req: unknown, res: { writeHead: (status: number) => void; end: (body?: string) => void }) => { + res.writeHead(200); + res.end("ok"); + }, + ), +); +const setWebhookSpy = vi.hoisted(() => vi.fn()); +const stopSpy = vi.hoisted(() => vi.fn()); +const webhookCallbackSpy = vi.hoisted(() => vi.fn(() => handlerSpy)); +const createTelegramBotSpy = vi.hoisted(() => + vi.fn(() => ({ + api: { setWebhook: setWebhookSpy }, + stop: stopSpy, + })), ); -const setWebhookSpy = vi.fn(); -const stopSpy = vi.fn(); -const webhookCallbackSpy = vi.fn(() => handlerSpy); - -const createTelegramBotSpy = vi.fn(() => ({ - api: { setWebhook: setWebhookSpy }, - stop: stopSpy, -})); vi.mock("grammy", async (importOriginal) => { const actual = await importOriginal();