






















@@ -0,0 +1,147 @@
1+import fs from "node:fs";
2+import os from "node:os";
3+import path from "node:path";
4+import { afterEach, beforeEach, describe, expect, it } from "vitest";
5+import { resolveStorePath } from "../config/sessions/paths.js";
6+import type { OpenClawConfig } from "../config/types.openclaw.js";
7+import { describeHeartbeatSessionTargetIssues } from "./doctor-heartbeat-session-target.js";
8+9+describe("describeHeartbeatSessionTargetIssues", () => {
10+let tmpDir: string;
11+12+beforeEach(() => {
13+tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-heartbeat-doctor-"));
14+});
15+16+afterEach(() => {
17+fs.rmSync(tmpDir, { recursive: true, force: true });
18+});
19+20+function cfgWithSession(session: string, target: string | null = "slack"): OpenClawConfig {
21+const heartbeat = target === null ? { session } : { session, target };
22+return {
23+session: {
24+mainKey: "work",
25+store: path.join(tmpDir, "agents", "{agentId}", "sessions", "sessions.json"),
26+},
27+agents: {
28+list: [
29+{
30+id: "ops",
31+ heartbeat,
32+},
33+],
34+},
35+} as OpenClawConfig;
36+}
37+38+function cfgWithDefaultHeartbeat(
39+session: string,
40+target: string | null = "slack",
41+): OpenClawConfig {
42+const heartbeat = target === null ? { session } : { session, target };
43+return {
44+session: {
45+mainKey: "work",
46+store: path.join(tmpDir, "agents", "{agentId}", "sessions", "sessions.json"),
47+},
48+agents: {
49+defaults: {
50+ heartbeat,
51+},
52+list: [
53+{
54+id: "ops",
55+},
56+],
57+},
58+} as OpenClawConfig;
59+}
60+61+function writeStore(cfg: OpenClawConfig, entries: Record<string, unknown>) {
62+const storePath = resolveStorePath(cfg.session?.store, { agentId: "ops" });
63+fs.mkdirSync(path.dirname(storePath), { recursive: true });
64+fs.writeFileSync(storePath, JSON.stringify(entries, null, 2));
65+}
66+67+it("uses runtime session canonicalization before warning", () => {
68+const cfg = cfgWithSession("agent:ops:main");
69+writeStore(cfg, {
70+"agent:ops:work": {
71+sessionId: "agent:ops:work",
72+updatedAt: Date.now(),
73+},
74+});
75+76+expect(describeHeartbeatSessionTargetIssues(cfg)).toEqual([]);
77+});
78+79+it("warns when the resolved heartbeat session is missing", () => {
80+const cfg = cfgWithSession("slack:channel:c123");
81+writeStore(cfg, {});
82+83+const warnings = describeHeartbeatSessionTargetIssues(cfg);
84+85+expect(warnings).toHaveLength(1);
86+expect(warnings[0]).toContain("resolved to agent:ops:slack:channel:c123");
87+expect(warnings[0]).toContain('reason="no-target"');
88+});
89+90+it("does not warn when an explicit heartbeat recipient does not need session history", () => {
91+const cfg = cfgWithSession("slack:channel:c123");
92+const agent = cfg.agents?.list?.[0];
93+if (!agent?.heartbeat) {
94+throw new Error("expected test config to include heartbeat config");
95+}
96+agent.heartbeat.target = "telegram";
97+agent.heartbeat.to = "-100123";
98+writeStore(cfg, {});
99+100+expect(describeHeartbeatSessionTargetIssues(cfg)).toEqual([]);
101+});
102+103+it("does not warn when the heartbeat cadence is disabled", () => {
104+const cfg = cfgWithSession("slack:channel:c123");
105+const agent = cfg.agents?.list?.[0];
106+if (!agent?.heartbeat) {
107+throw new Error("expected test config to include heartbeat config");
108+}
109+agent.heartbeat.every = "0m";
110+writeStore(cfg, {});
111+112+expect(describeHeartbeatSessionTargetIssues(cfg)).toEqual([]);
113+});
114+115+it("warns when a default-only heartbeat session is missing", () => {
116+const cfg = cfgWithDefaultHeartbeat("slack:channel:c123");
117+writeStore(cfg, {});
118+119+const warnings = describeHeartbeatSessionTargetIssues(cfg);
120+121+expect(warnings).toHaveLength(1);
122+expect(warnings[0]).toContain("Agent ops heartbeat.session pins slack:channel:c123");
123+expect(warnings[0]).toContain("resolved to agent:ops:slack:channel:c123");
124+});
125+126+it("warns when an explicit heartbeat inherits a default session", () => {
127+const cfg = cfgWithDefaultHeartbeat("slack:channel:c123");
128+const agent = cfg.agents?.list?.[0];
129+if (!agent) {
130+throw new Error("expected test config to include an agent");
131+}
132+agent.heartbeat = {};
133+writeStore(cfg, {});
134+135+const warnings = describeHeartbeatSessionTargetIssues(cfg);
136+137+expect(warnings).toHaveLength(1);
138+expect(warnings[0]).toContain("resolved to agent:ops:slack:channel:c123");
139+});
140+141+it("does not warn when target is omitted because runtime defaults to none", () => {
142+const cfg = cfgWithSession("slack:channel:c123", null);
143+writeStore(cfg, {});
144+145+expect(describeHeartbeatSessionTargetIssues(cfg)).toEqual([]);
146+});
147+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。