
























@@ -314,14 +314,24 @@ describe("isHeartbeatEnabledForAgent", () => {
314314expect(isHeartbeatEnabledForAgent(cfg, "ops")).toBe(true);
315315});
316316317-it("falls back to default agent when no explicit heartbeat entries", () => {
317+it("uses global heartbeat defaults for all agents when no explicit heartbeat entries exist", () => {
318318const cfg: OpenClawConfig = {
319319agents: {
320320defaults: { heartbeat: { every: "30m" } },
321321list: [{ id: "main" }, { id: "ops" }],
322322},
323323};
324324expect(isHeartbeatEnabledForAgent(cfg, "main")).toBe(true);
325+expect(isHeartbeatEnabledForAgent(cfg, "ops")).toBe(true);
326+});
327+328+it("falls back to default agent when no heartbeat config exists", () => {
329+const cfg: OpenClawConfig = {
330+agents: {
331+list: [{ id: "main" }, { id: "ops" }],
332+},
333+};
334+expect(isHeartbeatEnabledForAgent(cfg, "main")).toBe(true);
325335expect(isHeartbeatEnabledForAgent(cfg, "ops")).toBe(false);
326336});
327337});
@@ -1404,6 +1414,78 @@ describe("runHeartbeatOnce", () => {
14041414}
14051415});
140614161417+it("keeps non-task HEARTBEAT.md context while stripping blank-line-separated task blocks", async () => {
1418+const tmpDir = await createCaseDir("openclaw-hb-tasks-context");
1419+const storePath = path.join(tmpDir, "sessions.json");
1420+const workspaceDir = path.join(tmpDir, "workspace");
1421+await fs.mkdir(workspaceDir, { recursive: true });
1422+await fs.writeFile(
1423+path.join(workspaceDir, "HEARTBEAT.md"),
1424+`# Keep this header
1425+1426+Remember escalation policy.
1427+1428+tasks:
1429+ - name: inbox
1430+ interval: 5m
1431+ prompt: Check urgent inbox items
1432+1433+ - name: calendar
1434+ interval: 5m
1435+ prompt: Check calendar changes
1436+1437+Some global directive after tasks.
1438+`,
1439+"utf-8",
1440+);
1441+1442+const cfg: OpenClawConfig = {
1443+agents: {
1444+defaults: {
1445+workspace: workspaceDir,
1446+heartbeat: { every: "5m", target: "whatsapp" },
1447+},
1448+},
1449+channels: { whatsapp: { allowFrom: ["*"] } },
1450+session: { store: storePath },
1451+};
1452+await fs.writeFile(
1453+storePath,
1454+JSON.stringify({
1455+[resolveMainSessionKey(cfg)]: {
1456+sessionId: "sid",
1457+updatedAt: Date.now(),
1458+lastChannel: "whatsapp",
1459+lastTo: "120363401234567890@g.us",
1460+},
1461+}),
1462+);
1463+const replySpy = vi.fn().mockResolvedValue({ text: "Handled due heartbeat tasks" });
1464+const sendWhatsApp = vi
1465+.fn<
1466+(to: string, text: string, opts?: unknown) => Promise<{ messageId: string; toJid: string }>
1467+>()
1468+.mockResolvedValue({ messageId: "m1", toJid: "jid" });
1469+1470+const res = await runHeartbeatOnce({
1471+ cfg,
1472+deps: createHeartbeatDeps(sendWhatsApp, { getReplyFromConfig: replySpy }),
1473+});
1474+1475+expect(res.status).toBe("ran");
1476+expect(replySpy).toHaveBeenCalledTimes(1);
1477+const calledCtx = replySpy.mock.calls[0]?.[0] as { Body?: string };
1478+expect(calledCtx.Body).toContain("- inbox: Check urgent inbox items");
1479+expect(calledCtx.Body).toContain("- calendar: Check calendar changes");
1480+expect(calledCtx.Body).toContain("Additional context from HEARTBEAT.md");
1481+expect(calledCtx.Body).toContain("# Keep this header");
1482+expect(calledCtx.Body).toContain("Remember escalation policy.");
1483+expect(calledCtx.Body).toContain("Some global directive after tasks.");
1484+expect(calledCtx.Body).not.toContain("name: inbox");
1485+expect(calledCtx.Body).not.toContain("name: calendar");
1486+replySpy.mockReset();
1487+});
1488+14071489it("applies HEARTBEAT.md gating rules across file states and triggers", async () => {
14081490const cases: Array<{
14091491name: string;
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。