mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-11 01:01:13 +00:00
refactor: lazy load heartbeat reply runtime
This commit is contained in:
committed by
Peter Steinberger
parent
bf3d1f85b8
commit
a78dba4396
@@ -1,6 +1,5 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import * as replyModule from "../auto-reply/reply.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveMainSessionKey } from "../config/sessions.js";
|
||||
import { runHeartbeatOnce } from "./heartbeat-runner.js";
|
||||
@@ -27,9 +26,7 @@ describe("Ghost reminder bug (issue #13317)", () => {
|
||||
messageId: "m1",
|
||||
chatId: "155462274",
|
||||
});
|
||||
const getReplySpy = vi
|
||||
.spyOn(replyModule, "getReplyFromConfig")
|
||||
.mockResolvedValue({ text: replyText });
|
||||
const getReplySpy = vi.fn().mockResolvedValue({ text: replyText });
|
||||
return { sendTelegram, getReplySpy };
|
||||
};
|
||||
|
||||
@@ -117,6 +114,7 @@ describe("Ghost reminder bug (issue #13317)", () => {
|
||||
agentId: "main",
|
||||
reason: params.reason,
|
||||
deps: {
|
||||
getReplyFromConfig: getReplySpy,
|
||||
telegram: sendTelegram,
|
||||
},
|
||||
});
|
||||
@@ -278,6 +276,7 @@ describe("Ghost reminder bug (issue #13317)", () => {
|
||||
agentId: "main",
|
||||
reason: "wake",
|
||||
deps: {
|
||||
getReplyFromConfig: replySpy,
|
||||
telegram: sendTelegram,
|
||||
},
|
||||
});
|
||||
@@ -340,6 +339,7 @@ describe("Ghost reminder bug (issue #13317)", () => {
|
||||
agentId: "main",
|
||||
reason: "wake",
|
||||
deps: {
|
||||
getReplyFromConfig: replySpy,
|
||||
telegram: sendTelegram,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import * as replyModule from "../auto-reply/reply.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveAgentMainSessionKey, resolveMainSessionKey } from "../config/sessions.js";
|
||||
import { runHeartbeatOnce } from "./heartbeat-runner.js";
|
||||
@@ -19,11 +18,12 @@ async function withHeartbeatFixture(
|
||||
run: (ctx: {
|
||||
tmpDir: string;
|
||||
storePath: string;
|
||||
replySpy: ReturnType<typeof vi.fn>;
|
||||
seedSession: (sessionKey: string, input: SeedSessionInput) => Promise<void>;
|
||||
}) => Promise<unknown>,
|
||||
): Promise<unknown> {
|
||||
return withTempHeartbeatSandbox(
|
||||
async ({ tmpDir, storePath }) => {
|
||||
async ({ tmpDir, storePath, replySpy }) => {
|
||||
const seedSession = async (sessionKey: string, input: SeedSessionInput) => {
|
||||
await seedSessionStore(storePath, sessionKey, {
|
||||
updatedAt: input.updatedAt,
|
||||
@@ -32,7 +32,7 @@ async function withHeartbeatFixture(
|
||||
lastTo: input.lastTo,
|
||||
});
|
||||
};
|
||||
return run({ tmpDir, storePath, seedSession });
|
||||
return run({ tmpDir, storePath, replySpy, seedSession });
|
||||
},
|
||||
{ prefix: "openclaw-hb-model-" },
|
||||
);
|
||||
@@ -47,27 +47,28 @@ describe("runHeartbeatOnce – heartbeat model override", () => {
|
||||
seedSession: (sessionKey: string, input: SeedSessionInput) => Promise<void>;
|
||||
cfg: OpenClawConfig;
|
||||
sessionKey: string;
|
||||
replySpy: ReturnType<typeof vi.fn>;
|
||||
agentId?: string;
|
||||
}) {
|
||||
await params.seedSession(params.sessionKey, { lastChannel: "whatsapp", lastTo: "+1555" });
|
||||
|
||||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||
replySpy.mockResolvedValue({ text: "HEARTBEAT_OK" });
|
||||
params.replySpy.mockResolvedValue({ text: "HEARTBEAT_OK" });
|
||||
|
||||
await runHeartbeatOnce({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
deps: {
|
||||
getReplyFromConfig: params.replySpy,
|
||||
getQueueSize: () => 0,
|
||||
nowMs: () => 0,
|
||||
},
|
||||
});
|
||||
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
expect(params.replySpy).toHaveBeenCalledTimes(1);
|
||||
return {
|
||||
ctx: replySpy.mock.calls[0]?.[0],
|
||||
opts: replySpy.mock.calls[0]?.[1],
|
||||
replySpy,
|
||||
ctx: params.replySpy.mock.calls[0]?.[0],
|
||||
opts: params.replySpy.mock.calls[0]?.[1],
|
||||
replySpy: params.replySpy,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -77,7 +78,7 @@ describe("runHeartbeatOnce – heartbeat model override", () => {
|
||||
lightContext?: boolean;
|
||||
isolatedSession?: boolean;
|
||||
}) {
|
||||
return withHeartbeatFixture(async ({ tmpDir, storePath, seedSession }) => {
|
||||
return withHeartbeatFixture(async ({ tmpDir, storePath, replySpy, seedSession }) => {
|
||||
const cfg: OpenClawConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
@@ -100,6 +101,7 @@ describe("runHeartbeatOnce – heartbeat model override", () => {
|
||||
seedSession,
|
||||
cfg,
|
||||
sessionKey,
|
||||
replySpy,
|
||||
});
|
||||
return result.opts;
|
||||
});
|
||||
@@ -137,7 +139,7 @@ describe("runHeartbeatOnce – heartbeat model override", () => {
|
||||
});
|
||||
|
||||
it("uses isolated session key when isolatedSession is enabled", async () => {
|
||||
await withHeartbeatFixture(async ({ tmpDir, storePath, seedSession }) => {
|
||||
await withHeartbeatFixture(async ({ tmpDir, storePath, replySpy, seedSession }) => {
|
||||
const cfg: OpenClawConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
@@ -157,6 +159,7 @@ describe("runHeartbeatOnce – heartbeat model override", () => {
|
||||
seedSession,
|
||||
cfg,
|
||||
sessionKey,
|
||||
replySpy,
|
||||
});
|
||||
|
||||
// Isolated heartbeat runs use a dedicated session key with :heartbeat suffix
|
||||
@@ -165,7 +168,7 @@ describe("runHeartbeatOnce – heartbeat model override", () => {
|
||||
});
|
||||
|
||||
it("uses main session key when isolatedSession is not set", async () => {
|
||||
await withHeartbeatFixture(async ({ tmpDir, storePath, seedSession }) => {
|
||||
await withHeartbeatFixture(async ({ tmpDir, storePath, replySpy, seedSession }) => {
|
||||
const cfg: OpenClawConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
@@ -184,6 +187,7 @@ describe("runHeartbeatOnce – heartbeat model override", () => {
|
||||
seedSession,
|
||||
cfg,
|
||||
sessionKey,
|
||||
replySpy,
|
||||
});
|
||||
|
||||
expect(result.ctx?.SessionKey).toBe(sessionKey);
|
||||
@@ -191,7 +195,7 @@ describe("runHeartbeatOnce – heartbeat model override", () => {
|
||||
});
|
||||
|
||||
it("passes per-agent heartbeat model override (merged with defaults)", async () => {
|
||||
await withHeartbeatFixture(async ({ tmpDir, storePath, seedSession }) => {
|
||||
await withHeartbeatFixture(async ({ tmpDir, storePath, replySpy, seedSession }) => {
|
||||
const cfg: OpenClawConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
@@ -222,6 +226,7 @@ describe("runHeartbeatOnce – heartbeat model override", () => {
|
||||
cfg,
|
||||
agentId: "ops",
|
||||
sessionKey,
|
||||
replySpy,
|
||||
});
|
||||
|
||||
expect(result.replySpy).toHaveBeenCalledWith(
|
||||
|
||||
@@ -78,7 +78,7 @@ describe("runHeartbeatOnce ack handling", () => {
|
||||
async function runTelegramHeartbeatWithDefaults(params: {
|
||||
tmpDir: string;
|
||||
storePath: string;
|
||||
replySpy: ReturnType<typeof vi.spyOn>;
|
||||
replySpy: ReturnType<typeof vi.fn>;
|
||||
replyText: string;
|
||||
messages?: Record<string, unknown>;
|
||||
telegramOverrides?: Record<string, unknown>;
|
||||
@@ -108,7 +108,10 @@ describe("runHeartbeatOnce ack handling", () => {
|
||||
const sendTelegram = createMessageSendSpy();
|
||||
await runHeartbeatOnce({
|
||||
cfg,
|
||||
deps: makeTelegramDeps({ sendTelegram }),
|
||||
deps: {
|
||||
...makeTelegramDeps({ sendTelegram }),
|
||||
getReplyFromConfig: params.replySpy,
|
||||
},
|
||||
});
|
||||
return sendTelegram;
|
||||
}
|
||||
@@ -170,7 +173,10 @@ describe("runHeartbeatOnce ack handling", () => {
|
||||
|
||||
await runHeartbeatOnce({
|
||||
cfg,
|
||||
deps: makeWhatsAppDeps({ sendWhatsApp }),
|
||||
deps: {
|
||||
...makeWhatsAppDeps({ sendWhatsApp }),
|
||||
getReplyFromConfig: replySpy,
|
||||
},
|
||||
});
|
||||
|
||||
expect(sendWhatsApp).toHaveBeenCalled();
|
||||
@@ -196,7 +202,10 @@ describe("runHeartbeatOnce ack handling", () => {
|
||||
|
||||
await runHeartbeatOnce({
|
||||
cfg,
|
||||
deps: makeWhatsAppDeps({ sendWhatsApp }),
|
||||
deps: {
|
||||
...makeWhatsAppDeps({ sendWhatsApp }),
|
||||
getReplyFromConfig: replySpy,
|
||||
},
|
||||
});
|
||||
|
||||
expect(sendWhatsApp).toHaveBeenCalledTimes(1);
|
||||
@@ -258,7 +267,10 @@ describe("runHeartbeatOnce ack handling", () => {
|
||||
|
||||
const result = await runHeartbeatOnce({
|
||||
cfg,
|
||||
deps: makeWhatsAppDeps({ sendWhatsApp }),
|
||||
deps: {
|
||||
...makeWhatsAppDeps({ sendWhatsApp }),
|
||||
getReplyFromConfig: replySpy,
|
||||
},
|
||||
});
|
||||
|
||||
expect(replySpy).not.toHaveBeenCalled();
|
||||
@@ -279,7 +291,10 @@ describe("runHeartbeatOnce ack handling", () => {
|
||||
|
||||
await runHeartbeatOnce({
|
||||
cfg,
|
||||
deps: makeWhatsAppDeps({ sendWhatsApp }),
|
||||
deps: {
|
||||
...makeWhatsAppDeps({ sendWhatsApp }),
|
||||
getReplyFromConfig: replySpy,
|
||||
},
|
||||
});
|
||||
|
||||
expect(sendWhatsApp).not.toHaveBeenCalled();
|
||||
@@ -317,7 +332,10 @@ describe("runHeartbeatOnce ack handling", () => {
|
||||
|
||||
await runHeartbeatOnce({
|
||||
cfg,
|
||||
deps: makeWhatsAppDeps(),
|
||||
deps: {
|
||||
...makeWhatsAppDeps(),
|
||||
getReplyFromConfig: replySpy,
|
||||
},
|
||||
});
|
||||
|
||||
const finalStore = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record<
|
||||
@@ -340,11 +358,14 @@ describe("runHeartbeatOnce ack handling", () => {
|
||||
|
||||
const res = await runHeartbeatOnce({
|
||||
cfg,
|
||||
deps: makeWhatsAppDeps({
|
||||
sendWhatsApp,
|
||||
webAuthExists: async () => false,
|
||||
hasActiveWebListener: () => false,
|
||||
}),
|
||||
deps: {
|
||||
...makeWhatsAppDeps({
|
||||
sendWhatsApp,
|
||||
webAuthExists: async () => false,
|
||||
hasActiveWebListener: () => false,
|
||||
}),
|
||||
getReplyFromConfig: replySpy,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status).toBe("skipped");
|
||||
@@ -376,7 +397,10 @@ describe("runHeartbeatOnce ack handling", () => {
|
||||
|
||||
await runHeartbeatOnce({
|
||||
cfg,
|
||||
deps: makeTelegramDeps({ sendTelegram }),
|
||||
deps: {
|
||||
...makeTelegramDeps({ sendTelegram }),
|
||||
getReplyFromConfig: replySpy,
|
||||
},
|
||||
});
|
||||
|
||||
expect(sendTelegram).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -3,7 +3,6 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { HEARTBEAT_PROMPT } from "../auto-reply/heartbeat.js";
|
||||
import * as replyModule from "../auto-reply/reply.js";
|
||||
import type { ChannelOutboundAdapter } from "../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
@@ -482,13 +481,17 @@ describe("runHeartbeatOnce", () => {
|
||||
text: string,
|
||||
opts?: unknown,
|
||||
) => Promise<{ messageId: string; toJid: string }>,
|
||||
nowMs = 0,
|
||||
options?: {
|
||||
nowMs?: number;
|
||||
getReplyFromConfig?: HeartbeatDeps["getReplyFromConfig"];
|
||||
},
|
||||
): HeartbeatDeps => ({
|
||||
whatsapp: sendWhatsApp,
|
||||
getQueueSize: () => 0,
|
||||
nowMs: () => nowMs,
|
||||
nowMs: () => options?.nowMs ?? 0,
|
||||
webAuthExists: async () => true,
|
||||
hasActiveWebListener: () => true,
|
||||
...(options?.getReplyFromConfig ? { getReplyFromConfig: options.getReplyFromConfig } : null),
|
||||
});
|
||||
|
||||
it("skips when agent heartbeat is not enabled", async () => {
|
||||
@@ -533,7 +536,7 @@ describe("runHeartbeatOnce", () => {
|
||||
it("uses the last non-empty payload for delivery", async () => {
|
||||
const tmpDir = await createCaseDir("hb-last-payload");
|
||||
const storePath = path.join(tmpDir, "sessions.json");
|
||||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||
const replySpy = vi.fn();
|
||||
try {
|
||||
const cfg: OpenClawConfig = {
|
||||
agents: {
|
||||
@@ -575,7 +578,7 @@ describe("runHeartbeatOnce", () => {
|
||||
|
||||
await runHeartbeatOnce({
|
||||
cfg,
|
||||
deps: createHeartbeatDeps(sendWhatsApp),
|
||||
deps: createHeartbeatDeps(sendWhatsApp, { getReplyFromConfig: replySpy }),
|
||||
});
|
||||
|
||||
expect(sendWhatsApp).toHaveBeenCalledTimes(1);
|
||||
@@ -585,14 +588,14 @@ describe("runHeartbeatOnce", () => {
|
||||
expect.any(Object),
|
||||
);
|
||||
} finally {
|
||||
replySpy.mockRestore();
|
||||
replySpy.mockReset();
|
||||
}
|
||||
});
|
||||
|
||||
it("uses per-agent heartbeat overrides and session keys", async () => {
|
||||
const tmpDir = await createCaseDir("hb-agent-overrides");
|
||||
const storePath = path.join(tmpDir, "sessions.json");
|
||||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||
const replySpy = vi.fn();
|
||||
try {
|
||||
const cfg: OpenClawConfig = {
|
||||
agents: {
|
||||
@@ -640,7 +643,7 @@ describe("runHeartbeatOnce", () => {
|
||||
await runHeartbeatOnce({
|
||||
cfg,
|
||||
agentId: "ops",
|
||||
deps: createHeartbeatDeps(sendWhatsApp),
|
||||
deps: createHeartbeatDeps(sendWhatsApp, { getReplyFromConfig: replySpy }),
|
||||
});
|
||||
expect(sendWhatsApp).toHaveBeenCalledTimes(1);
|
||||
expect(sendWhatsApp).toHaveBeenCalledWith(
|
||||
@@ -662,14 +665,14 @@ describe("runHeartbeatOnce", () => {
|
||||
cfg,
|
||||
);
|
||||
} finally {
|
||||
replySpy.mockRestore();
|
||||
replySpy.mockReset();
|
||||
}
|
||||
});
|
||||
|
||||
it("reuses non-default agent sessionFile from templated stores", async () => {
|
||||
const tmpDir = await createCaseDir("hb-templated-store");
|
||||
const storeTemplate = path.join(tmpDir, "agents", "{agentId}", "sessions", "sessions.json");
|
||||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||
const replySpy = vi.fn();
|
||||
const agentId = "ops";
|
||||
try {
|
||||
const cfg: OpenClawConfig = {
|
||||
@@ -726,7 +729,7 @@ describe("runHeartbeatOnce", () => {
|
||||
const result = await runHeartbeatOnce({
|
||||
cfg,
|
||||
agentId,
|
||||
deps: createHeartbeatDeps(sendWhatsApp),
|
||||
deps: createHeartbeatDeps(sendWhatsApp, { getReplyFromConfig: replySpy }),
|
||||
});
|
||||
|
||||
expect(result.status).toBe("ran");
|
||||
@@ -747,7 +750,7 @@ describe("runHeartbeatOnce", () => {
|
||||
cfg,
|
||||
);
|
||||
} finally {
|
||||
replySpy.mockRestore();
|
||||
replySpy.mockReset();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -779,7 +782,7 @@ describe("runHeartbeatOnce", () => {
|
||||
])(
|
||||
"resolves configured and forced session key overrides: $name",
|
||||
async ({ name, caseDir, peerKind, peerId, message, applyOverride, runOptions }) => {
|
||||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||
const replySpy = vi.fn();
|
||||
try {
|
||||
const tmpDir = await createCaseDir(caseDir);
|
||||
const storePath = path.join(tmpDir, "sessions.json");
|
||||
@@ -839,7 +842,7 @@ describe("runHeartbeatOnce", () => {
|
||||
await runHeartbeatOnce({
|
||||
cfg,
|
||||
...runOptions({ sessionKey: overrideSessionKey }),
|
||||
deps: createHeartbeatDeps(sendWhatsApp),
|
||||
deps: createHeartbeatDeps(sendWhatsApp, { getReplyFromConfig: replySpy }),
|
||||
});
|
||||
|
||||
expect(sendWhatsApp, name).toHaveBeenCalledTimes(1);
|
||||
@@ -855,7 +858,7 @@ describe("runHeartbeatOnce", () => {
|
||||
cfg,
|
||||
);
|
||||
} finally {
|
||||
replySpy.mockRestore();
|
||||
replySpy.mockReset();
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -863,7 +866,7 @@ describe("runHeartbeatOnce", () => {
|
||||
it("suppresses duplicate heartbeat payloads within 24h", async () => {
|
||||
const tmpDir = await createCaseDir("hb-dup-suppress");
|
||||
const storePath = path.join(tmpDir, "sessions.json");
|
||||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||
const replySpy = vi.fn();
|
||||
try {
|
||||
const cfg: OpenClawConfig = {
|
||||
agents: {
|
||||
@@ -904,12 +907,15 @@ describe("runHeartbeatOnce", () => {
|
||||
|
||||
await runHeartbeatOnce({
|
||||
cfg,
|
||||
deps: createHeartbeatDeps(sendWhatsApp, 60_000),
|
||||
deps: createHeartbeatDeps(sendWhatsApp, {
|
||||
nowMs: 60_000,
|
||||
getReplyFromConfig: replySpy,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(sendWhatsApp).toHaveBeenCalledTimes(0);
|
||||
} finally {
|
||||
replySpy.mockRestore();
|
||||
replySpy.mockReset();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -936,7 +942,7 @@ describe("runHeartbeatOnce", () => {
|
||||
)(
|
||||
"handles reasoning payload delivery variants: $name",
|
||||
async ({ name, caseDir, replies, expectedTexts }) => {
|
||||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||
const replySpy = vi.fn();
|
||||
try {
|
||||
const tmpDir = await createCaseDir(caseDir);
|
||||
const storePath = path.join(tmpDir, "sessions.json");
|
||||
@@ -983,7 +989,7 @@ describe("runHeartbeatOnce", () => {
|
||||
|
||||
await runHeartbeatOnce({
|
||||
cfg,
|
||||
deps: createHeartbeatDeps(sendWhatsApp),
|
||||
deps: createHeartbeatDeps(sendWhatsApp, { getReplyFromConfig: replySpy }),
|
||||
});
|
||||
|
||||
expect(sendWhatsApp, name).toHaveBeenCalledTimes(expectedTexts.length);
|
||||
@@ -996,7 +1002,7 @@ describe("runHeartbeatOnce", () => {
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
replySpy.mockRestore();
|
||||
replySpy.mockReset();
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -1004,7 +1010,7 @@ describe("runHeartbeatOnce", () => {
|
||||
it("loads the default agent session from templated stores", async () => {
|
||||
const tmpDir = await createCaseDir("openclaw-hb");
|
||||
const storeTemplate = path.join(tmpDir, "agents", "{agentId}", "sessions.json");
|
||||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||
const replySpy = vi.fn();
|
||||
try {
|
||||
const cfg: OpenClawConfig = {
|
||||
agents: {
|
||||
@@ -1048,7 +1054,7 @@ describe("runHeartbeatOnce", () => {
|
||||
|
||||
await runHeartbeatOnce({
|
||||
cfg,
|
||||
deps: createHeartbeatDeps(sendWhatsApp),
|
||||
deps: createHeartbeatDeps(sendWhatsApp, { getReplyFromConfig: replySpy }),
|
||||
});
|
||||
|
||||
expect(sendWhatsApp).toHaveBeenCalledTimes(1);
|
||||
@@ -1058,7 +1064,7 @@ describe("runHeartbeatOnce", () => {
|
||||
expect.any(Object),
|
||||
);
|
||||
} finally {
|
||||
replySpy.mockRestore();
|
||||
replySpy.mockReset();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1121,7 +1127,7 @@ describe("runHeartbeatOnce", () => {
|
||||
});
|
||||
}
|
||||
|
||||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||
const replySpy = vi.fn();
|
||||
replySpy.mockResolvedValue({ text: params.replyText ?? "Checked logs and PRs" });
|
||||
const sendWhatsApp = vi
|
||||
.fn<
|
||||
@@ -1131,7 +1137,7 @@ describe("runHeartbeatOnce", () => {
|
||||
const res = await runHeartbeatOnce({
|
||||
cfg,
|
||||
reason: params.reason,
|
||||
deps: createHeartbeatDeps(sendWhatsApp),
|
||||
deps: createHeartbeatDeps(sendWhatsApp, { getReplyFromConfig: replySpy }),
|
||||
});
|
||||
return { res, replySpy, sendWhatsApp, workspaceDir };
|
||||
}
|
||||
@@ -1262,7 +1268,7 @@ describe("runHeartbeatOnce", () => {
|
||||
expect(calledCtx.Body, name).toContain("scheduled reminder has been triggered");
|
||||
}
|
||||
} finally {
|
||||
replySpy.mockRestore();
|
||||
replySpy.mockReset();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1297,7 +1303,7 @@ describe("runHeartbeatOnce", () => {
|
||||
contextKey: "cron:rotate-logs",
|
||||
});
|
||||
|
||||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||
const replySpy = vi.fn();
|
||||
replySpy.mockResolvedValue({ text: "Handled internally" });
|
||||
const sendWhatsApp = vi
|
||||
.fn<
|
||||
@@ -1309,7 +1315,7 @@ describe("runHeartbeatOnce", () => {
|
||||
const res = await runHeartbeatOnce({
|
||||
cfg,
|
||||
reason: "interval",
|
||||
deps: createHeartbeatDeps(sendWhatsApp),
|
||||
deps: createHeartbeatDeps(sendWhatsApp, { getReplyFromConfig: replySpy }),
|
||||
});
|
||||
expect(res.status).toBe("ran");
|
||||
expect(sendWhatsApp).toHaveBeenCalledTimes(0);
|
||||
@@ -1318,7 +1324,7 @@ describe("runHeartbeatOnce", () => {
|
||||
expect(calledCtx.Body).toContain("Handle this reminder internally");
|
||||
expect(calledCtx.Body).not.toContain("Please relay this reminder to the user");
|
||||
} finally {
|
||||
replySpy.mockRestore();
|
||||
replySpy.mockReset();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1352,7 +1358,7 @@ describe("runHeartbeatOnce", () => {
|
||||
contextKey: "exec:backup",
|
||||
});
|
||||
|
||||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||
const replySpy = vi.fn();
|
||||
replySpy.mockResolvedValue({ text: "Handled internally" });
|
||||
const sendWhatsApp = vi
|
||||
.fn<
|
||||
@@ -1364,7 +1370,7 @@ describe("runHeartbeatOnce", () => {
|
||||
const res = await runHeartbeatOnce({
|
||||
cfg,
|
||||
reason: "exec-event",
|
||||
deps: createHeartbeatDeps(sendWhatsApp),
|
||||
deps: createHeartbeatDeps(sendWhatsApp, { getReplyFromConfig: replySpy }),
|
||||
});
|
||||
expect(res.status).toBe("ran");
|
||||
expect(sendWhatsApp).toHaveBeenCalledTimes(0);
|
||||
@@ -1378,7 +1384,7 @@ describe("runHeartbeatOnce", () => {
|
||||
expect(calledCtx.Body).toContain("Handle the result internally");
|
||||
expect(calledCtx.Body).not.toContain("Please relay the command output to the user");
|
||||
} finally {
|
||||
replySpy.mockRestore();
|
||||
replySpy.mockReset();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
1
src/infra/heartbeat-runner.runtime.ts
Normal file
1
src/infra/heartbeat-runner.runtime.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { getReplyFromConfig } from "../auto-reply/reply.js";
|
||||
@@ -44,6 +44,7 @@ describe("runHeartbeatOnce", () => {
|
||||
await runHeartbeatOnce({
|
||||
cfg,
|
||||
deps: {
|
||||
getReplyFromConfig: replySpy,
|
||||
slack: sendSlack,
|
||||
getQueueSize: () => 0,
|
||||
nowMs: () => 0,
|
||||
|
||||
@@ -2,7 +2,6 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { vi } from "vitest";
|
||||
import * as replyModule from "../auto-reply/reply.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveMainSessionKey } from "../config/sessions.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
@@ -48,7 +47,7 @@ export async function withTempHeartbeatSandbox<T>(
|
||||
fn: (ctx: {
|
||||
tmpDir: string;
|
||||
storePath: string;
|
||||
replySpy: ReturnType<typeof vi.spyOn>;
|
||||
replySpy: ReturnType<typeof vi.fn>;
|
||||
}) => Promise<T>,
|
||||
options?: {
|
||||
prefix?: string;
|
||||
@@ -58,7 +57,7 @@ export async function withTempHeartbeatSandbox<T>(
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), options?.prefix ?? "openclaw-hb-"));
|
||||
await fs.writeFile(path.join(tmpDir, "HEARTBEAT.md"), "- Check status\n", "utf-8");
|
||||
const storePath = path.join(tmpDir, "sessions.json");
|
||||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||
const replySpy = vi.fn().mockResolvedValue({ text: "ok" });
|
||||
const previousEnv = new Map<string, string | undefined>();
|
||||
for (const envName of options?.unsetEnvVars ?? []) {
|
||||
previousEnv.set(envName, process.env[envName]);
|
||||
@@ -67,7 +66,7 @@ export async function withTempHeartbeatSandbox<T>(
|
||||
try {
|
||||
return await fn({ tmpDir, storePath, replySpy });
|
||||
} finally {
|
||||
replySpy.mockRestore();
|
||||
replySpy.mockReset();
|
||||
for (const [envName, previousValue] of previousEnv.entries()) {
|
||||
if (previousValue === undefined) {
|
||||
delete process.env[envName];
|
||||
@@ -83,7 +82,7 @@ export async function withTempTelegramHeartbeatSandbox<T>(
|
||||
fn: (ctx: {
|
||||
tmpDir: string;
|
||||
storePath: string;
|
||||
replySpy: ReturnType<typeof vi.spyOn>;
|
||||
replySpy: ReturnType<typeof vi.fn>;
|
||||
}) => Promise<T>,
|
||||
options?: {
|
||||
prefix?: string;
|
||||
|
||||
@@ -70,7 +70,10 @@ describe("heartbeat transcript pruning", () => {
|
||||
agentId: undefined,
|
||||
reason: "test",
|
||||
cfg,
|
||||
deps: { sendTelegram: vi.fn() },
|
||||
deps: {
|
||||
sendTelegram: vi.fn(),
|
||||
getReplyFromConfig: replySpy,
|
||||
},
|
||||
});
|
||||
|
||||
const finalSize = (await fs.stat(transcriptPath)).size;
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
resolveHeartbeatPrompt as resolveHeartbeatPromptText,
|
||||
stripHeartbeatToken,
|
||||
} from "../auto-reply/heartbeat.js";
|
||||
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
||||
import { HEARTBEAT_TOKEN } from "../auto-reply/tokens.js";
|
||||
import type { ReplyPayload } from "../auto-reply/types.js";
|
||||
import { getChannelPlugin } from "../channels/plugins/index.js";
|
||||
@@ -84,12 +83,20 @@ import { peekSystemEventEntries, resolveSystemEventDeliveryContext } from "./sys
|
||||
|
||||
export type HeartbeatDeps = OutboundSendDeps &
|
||||
ChannelHeartbeatDeps & {
|
||||
getReplyFromConfig?: typeof import("./heartbeat-runner.runtime.js").getReplyFromConfig;
|
||||
runtime?: RuntimeEnv;
|
||||
getQueueSize?: (lane?: string) => number;
|
||||
nowMs?: () => number;
|
||||
};
|
||||
|
||||
const log = createSubsystemLogger("gateway/heartbeat");
|
||||
let heartbeatRunnerRuntimePromise: Promise<typeof import("./heartbeat-runner.runtime.js")> | null =
|
||||
null;
|
||||
|
||||
function loadHeartbeatRunnerRuntime() {
|
||||
heartbeatRunnerRuntimePromise ??= import("./heartbeat-runner.runtime.js");
|
||||
return heartbeatRunnerRuntimePromise;
|
||||
}
|
||||
|
||||
export { areHeartbeatsEnabled, setHeartbeatsEnabled };
|
||||
export {
|
||||
@@ -730,6 +737,8 @@ export async function runHeartbeatOnce(opts: {
|
||||
bootstrapContextMode,
|
||||
}
|
||||
: { isHeartbeat: true, suppressToolErrorWarnings, bootstrapContextMode };
|
||||
const getReplyFromConfig =
|
||||
opts.deps?.getReplyFromConfig ?? (await loadHeartbeatRunnerRuntime()).getReplyFromConfig;
|
||||
const replyResult = await getReplyFromConfig(ctx, replyOpts, cfg);
|
||||
const replyPayload = resolveHeartbeatReplyPayload(replyResult);
|
||||
const includeReasoning = heartbeat?.includeReasoning === true;
|
||||
|
||||
Reference in New Issue
Block a user