test: update health/status and legacy migration coverage
This commit is contained in:
@@ -30,7 +30,7 @@ const probeGateway = vi.fn(async ({ url }: { url: string }) => {
|
|||||||
close: null,
|
close: null,
|
||||||
health: { ok: true },
|
health: { ok: true },
|
||||||
status: {
|
status: {
|
||||||
linkProvider: {
|
linkChannel: {
|
||||||
id: "whatsapp",
|
id: "whatsapp",
|
||||||
label: "WhatsApp",
|
label: "WhatsApp",
|
||||||
linked: false,
|
linked: false,
|
||||||
@@ -60,7 +60,7 @@ const probeGateway = vi.fn(async ({ url }: { url: string }) => {
|
|||||||
close: null,
|
close: null,
|
||||||
health: { ok: true },
|
health: { ok: true },
|
||||||
status: {
|
status: {
|
||||||
linkProvider: {
|
linkChannel: {
|
||||||
id: "whatsapp",
|
id: "whatsapp",
|
||||||
label: "WhatsApp",
|
label: "WhatsApp",
|
||||||
linked: true,
|
linked: true,
|
||||||
|
|||||||
@@ -35,10 +35,12 @@ describe("healthCommand (coverage)", () => {
|
|||||||
durationMs: 5,
|
durationMs: 5,
|
||||||
channels: {
|
channels: {
|
||||||
whatsapp: {
|
whatsapp: {
|
||||||
|
accountId: "default",
|
||||||
linked: true,
|
linked: true,
|
||||||
authAgeMs: 5 * 60_000,
|
authAgeMs: 5 * 60_000,
|
||||||
},
|
},
|
||||||
telegram: {
|
telegram: {
|
||||||
|
accountId: "default",
|
||||||
configured: true,
|
configured: true,
|
||||||
probe: {
|
probe: {
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -48,6 +50,7 @@ describe("healthCommand (coverage)", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
discord: {
|
discord: {
|
||||||
|
accountId: "default",
|
||||||
configured: false,
|
configured: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -58,6 +61,29 @@ describe("healthCommand (coverage)", () => {
|
|||||||
discord: "Discord",
|
discord: "Discord",
|
||||||
},
|
},
|
||||||
heartbeatSeconds: 60,
|
heartbeatSeconds: 60,
|
||||||
|
defaultAgentId: "main",
|
||||||
|
agents: [
|
||||||
|
{
|
||||||
|
agentId: "main",
|
||||||
|
isDefault: true,
|
||||||
|
heartbeat: {
|
||||||
|
enabled: true,
|
||||||
|
every: "1m",
|
||||||
|
everyMs: 60_000,
|
||||||
|
prompt: "hi",
|
||||||
|
target: "last",
|
||||||
|
ackMaxChars: 160,
|
||||||
|
},
|
||||||
|
sessions: {
|
||||||
|
path: "/tmp/sessions.json",
|
||||||
|
count: 2,
|
||||||
|
recent: [
|
||||||
|
{ key: "main", updatedAt: Date.now() - 60_000, age: 60_000 },
|
||||||
|
{ key: "foo", updatedAt: null, age: null },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
sessions: {
|
sessions: {
|
||||||
path: "/tmp/sessions.json",
|
path: "/tmp/sessions.json",
|
||||||
count: 2,
|
count: 2,
|
||||||
|
|||||||
@@ -30,10 +30,6 @@ vi.mock("../web/auth-store.js", () => ({
|
|||||||
logWebSelfId: vi.fn(),
|
logWebSelfId: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../web/reconnect.js", () => ({
|
|
||||||
resolveHeartbeatSeconds: vi.fn(() => 60),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe("getHealthSnapshot", () => {
|
describe("getHealthSnapshot", () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.unstubAllGlobals();
|
vi.unstubAllGlobals();
|
||||||
@@ -229,4 +225,32 @@ describe("getHealthSnapshot", () => {
|
|||||||
expect(telegram.probe?.ok).toBe(false);
|
expect(telegram.probe?.ok).toBe(false);
|
||||||
expect(telegram.probe?.error).toMatch(/network down/i);
|
expect(telegram.probe?.error).toMatch(/network down/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("disables heartbeat for agents without heartbeat blocks", async () => {
|
||||||
|
testConfig = {
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
heartbeat: {
|
||||||
|
every: "30m",
|
||||||
|
target: "last",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
list: [
|
||||||
|
{ id: "main", default: true },
|
||||||
|
{ id: "ops", heartbeat: { every: "1h", target: "whatsapp" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
testStore = {};
|
||||||
|
|
||||||
|
const snap = await getHealthSnapshot({ timeoutMs: 10, probe: false });
|
||||||
|
const byAgent = new Map(snap.agents.map((agent) => [agent.agentId, agent] as const));
|
||||||
|
const main = byAgent.get("main");
|
||||||
|
const ops = byAgent.get("ops");
|
||||||
|
|
||||||
|
expect(main?.heartbeat.everyMs).toBeNull();
|
||||||
|
expect(main?.heartbeat.every).toBe("disabled");
|
||||||
|
expect(ops?.heartbeat.everyMs).toBeTruthy();
|
||||||
|
expect(ops?.heartbeat.every).toBe("1h");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import type { HealthSummary } from "./health.js";
|
import type { HealthSummary } from "./health.js";
|
||||||
import { healthCommand } from "./health.js";
|
import { formatHealthChannelLines, healthCommand } from "./health.js";
|
||||||
|
|
||||||
const runtime = {
|
const runtime = {
|
||||||
log: vi.fn(),
|
log: vi.fn(),
|
||||||
@@ -20,14 +20,23 @@ describe("healthCommand", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("outputs JSON from gateway", async () => {
|
it("outputs JSON from gateway", async () => {
|
||||||
|
const agentSessions = {
|
||||||
|
path: "/tmp/sessions.json",
|
||||||
|
count: 1,
|
||||||
|
recent: [{ key: "+1555", updatedAt: Date.now(), age: 0 }],
|
||||||
|
};
|
||||||
const snapshot: HealthSummary = {
|
const snapshot: HealthSummary = {
|
||||||
ok: true,
|
ok: true,
|
||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
durationMs: 5,
|
durationMs: 5,
|
||||||
channels: {
|
channels: {
|
||||||
whatsapp: { linked: true, authAgeMs: 5000 },
|
whatsapp: { accountId: "default", linked: true, authAgeMs: 5000 },
|
||||||
telegram: { configured: true, probe: { ok: true, elapsedMs: 1 } },
|
telegram: {
|
||||||
discord: { configured: false },
|
accountId: "default",
|
||||||
|
configured: true,
|
||||||
|
probe: { ok: true, elapsedMs: 1 },
|
||||||
|
},
|
||||||
|
discord: { accountId: "default", configured: false },
|
||||||
},
|
},
|
||||||
channelOrder: ["whatsapp", "telegram", "discord"],
|
channelOrder: ["whatsapp", "telegram", "discord"],
|
||||||
channelLabels: {
|
channelLabels: {
|
||||||
@@ -36,11 +45,23 @@ describe("healthCommand", () => {
|
|||||||
discord: "Discord",
|
discord: "Discord",
|
||||||
},
|
},
|
||||||
heartbeatSeconds: 60,
|
heartbeatSeconds: 60,
|
||||||
sessions: {
|
defaultAgentId: "main",
|
||||||
path: "/tmp/sessions.json",
|
agents: [
|
||||||
count: 1,
|
{
|
||||||
recent: [{ key: "+1555", updatedAt: Date.now(), age: 0 }],
|
agentId: "main",
|
||||||
|
isDefault: true,
|
||||||
|
heartbeat: {
|
||||||
|
enabled: true,
|
||||||
|
every: "1m",
|
||||||
|
everyMs: 60_000,
|
||||||
|
prompt: "hi",
|
||||||
|
target: "last",
|
||||||
|
ackMaxChars: 160,
|
||||||
},
|
},
|
||||||
|
sessions: agentSessions,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sessions: agentSessions,
|
||||||
};
|
};
|
||||||
callGatewayMock.mockResolvedValueOnce(snapshot);
|
callGatewayMock.mockResolvedValueOnce(snapshot);
|
||||||
|
|
||||||
@@ -60,9 +81,9 @@ describe("healthCommand", () => {
|
|||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
durationMs: 5,
|
durationMs: 5,
|
||||||
channels: {
|
channels: {
|
||||||
whatsapp: { linked: false, authAgeMs: null },
|
whatsapp: { accountId: "default", linked: false, authAgeMs: null },
|
||||||
telegram: { configured: false },
|
telegram: { accountId: "default", configured: false },
|
||||||
discord: { configured: false },
|
discord: { accountId: "default", configured: false },
|
||||||
},
|
},
|
||||||
channelOrder: ["whatsapp", "telegram", "discord"],
|
channelOrder: ["whatsapp", "telegram", "discord"],
|
||||||
channelLabels: {
|
channelLabels: {
|
||||||
@@ -71,6 +92,22 @@ describe("healthCommand", () => {
|
|||||||
discord: "Discord",
|
discord: "Discord",
|
||||||
},
|
},
|
||||||
heartbeatSeconds: 60,
|
heartbeatSeconds: 60,
|
||||||
|
defaultAgentId: "main",
|
||||||
|
agents: [
|
||||||
|
{
|
||||||
|
agentId: "main",
|
||||||
|
isDefault: true,
|
||||||
|
heartbeat: {
|
||||||
|
enabled: true,
|
||||||
|
every: "1m",
|
||||||
|
everyMs: 60_000,
|
||||||
|
prompt: "hi",
|
||||||
|
target: "last",
|
||||||
|
ackMaxChars: 160,
|
||||||
|
},
|
||||||
|
sessions: { path: "/tmp/sessions.json", count: 0, recent: [] },
|
||||||
|
},
|
||||||
|
],
|
||||||
sessions: { path: "/tmp/sessions.json", count: 0, recent: [] },
|
sessions: { path: "/tmp/sessions.json", count: 0, recent: [] },
|
||||||
} satisfies HealthSummary);
|
} satisfies HealthSummary);
|
||||||
|
|
||||||
@@ -79,4 +116,61 @@ describe("healthCommand", () => {
|
|||||||
expect(runtime.exit).not.toHaveBeenCalled();
|
expect(runtime.exit).not.toHaveBeenCalled();
|
||||||
expect(runtime.log).toHaveBeenCalled();
|
expect(runtime.log).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("formats per-account probe timings", () => {
|
||||||
|
const summary: HealthSummary = {
|
||||||
|
ok: true,
|
||||||
|
ts: Date.now(),
|
||||||
|
durationMs: 5,
|
||||||
|
channels: {
|
||||||
|
telegram: {
|
||||||
|
accountId: "main",
|
||||||
|
configured: true,
|
||||||
|
probe: { ok: true, elapsedMs: 196, bot: { username: "pinguini_ugi_bot" } },
|
||||||
|
accounts: {
|
||||||
|
main: {
|
||||||
|
accountId: "main",
|
||||||
|
configured: true,
|
||||||
|
probe: { ok: true, elapsedMs: 196, bot: { username: "pinguini_ugi_bot" } },
|
||||||
|
},
|
||||||
|
flurry: {
|
||||||
|
accountId: "flurry",
|
||||||
|
configured: true,
|
||||||
|
probe: { ok: true, elapsedMs: 190, bot: { username: "flurry_ugi_bot" } },
|
||||||
|
},
|
||||||
|
poe: {
|
||||||
|
accountId: "poe",
|
||||||
|
configured: true,
|
||||||
|
probe: { ok: true, elapsedMs: 188, bot: { username: "poe_ugi_bot" } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
channelOrder: ["telegram"],
|
||||||
|
channelLabels: { telegram: "Telegram" },
|
||||||
|
heartbeatSeconds: 60,
|
||||||
|
defaultAgentId: "main",
|
||||||
|
agents: [
|
||||||
|
{
|
||||||
|
agentId: "main",
|
||||||
|
isDefault: true,
|
||||||
|
heartbeat: {
|
||||||
|
enabled: true,
|
||||||
|
every: "1m",
|
||||||
|
everyMs: 60_000,
|
||||||
|
prompt: "hi",
|
||||||
|
target: "last",
|
||||||
|
ackMaxChars: 160,
|
||||||
|
},
|
||||||
|
sessions: { path: "/tmp/sessions.json", count: 0, recent: [] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sessions: { path: "/tmp/sessions.json", count: 0, recent: [] },
|
||||||
|
};
|
||||||
|
|
||||||
|
const lines = formatHealthChannelLines(summary, { accountMode: "all" });
|
||||||
|
expect(lines).toContain(
|
||||||
|
"Telegram: ok (@pinguini_ugi_bot:main:196ms, @flurry_ugi_bot:flurry:190ms, @poe_ugi_bot:poe:188ms)",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,6 +32,12 @@ const mocks = vi.hoisted(() => ({
|
|||||||
configSnapshot: null,
|
configSnapshot: null,
|
||||||
}),
|
}),
|
||||||
callGateway: vi.fn().mockResolvedValue({}),
|
callGateway: vi.fn().mockResolvedValue({}),
|
||||||
|
listAgentsForGateway: vi.fn().mockReturnValue({
|
||||||
|
defaultId: "main",
|
||||||
|
mainKey: "agent:main:main",
|
||||||
|
scope: "per-sender",
|
||||||
|
agents: [{ id: "main", name: "Main" }],
|
||||||
|
}),
|
||||||
runSecurityAudit: vi.fn().mockResolvedValue({
|
runSecurityAudit: vi.fn().mockResolvedValue({
|
||||||
ts: 0,
|
ts: 0,
|
||||||
summary: { critical: 1, warn: 1, info: 2 },
|
summary: { critical: 1, warn: 1, info: 2 },
|
||||||
@@ -154,12 +160,7 @@ vi.mock("../gateway/call.js", async (importOriginal) => {
|
|||||||
return { ...actual, callGateway: mocks.callGateway };
|
return { ...actual, callGateway: mocks.callGateway };
|
||||||
});
|
});
|
||||||
vi.mock("../gateway/session-utils.js", () => ({
|
vi.mock("../gateway/session-utils.js", () => ({
|
||||||
listAgentsForGateway: () => ({
|
listAgentsForGateway: mocks.listAgentsForGateway,
|
||||||
defaultId: "main",
|
|
||||||
mainKey: "agent:main:main",
|
|
||||||
scope: "per-sender",
|
|
||||||
agents: [{ id: "main", name: "Main" }],
|
|
||||||
}),
|
|
||||||
}));
|
}));
|
||||||
vi.mock("../infra/clawdbot-root.js", () => ({
|
vi.mock("../infra/clawdbot-root.js", () => ({
|
||||||
resolveClawdbotPackageRoot: vi.fn().mockResolvedValue("/tmp/clawdbot"),
|
resolveClawdbotPackageRoot: vi.fn().mockResolvedValue("/tmp/clawdbot"),
|
||||||
@@ -234,7 +235,7 @@ describe("statusCommand", () => {
|
|||||||
const payload = JSON.parse((runtime.log as vi.Mock).mock.calls[0][0]);
|
const payload = JSON.parse((runtime.log as vi.Mock).mock.calls[0][0]);
|
||||||
expect(payload.linkChannel.linked).toBe(true);
|
expect(payload.linkChannel.linked).toBe(true);
|
||||||
expect(payload.sessions.count).toBe(1);
|
expect(payload.sessions.count).toBe(1);
|
||||||
expect(payload.sessions.path).toBe("/tmp/sessions.json");
|
expect(payload.sessions.paths).toContain("/tmp/sessions.json");
|
||||||
expect(payload.sessions.defaults.model).toBeTruthy();
|
expect(payload.sessions.defaults.model).toBeTruthy();
|
||||||
expect(payload.sessions.defaults.contextTokens).toBeGreaterThan(0);
|
expect(payload.sessions.defaults.contextTokens).toBeGreaterThan(0);
|
||||||
expect(payload.sessions.recent[0].percentUsed).toBe(50);
|
expect(payload.sessions.recent[0].percentUsed).toBe(50);
|
||||||
@@ -335,4 +336,62 @@ describe("statusCommand", () => {
|
|||||||
expect(logs.join("\n")).toMatch(/gateway:/i);
|
expect(logs.join("\n")).toMatch(/gateway:/i);
|
||||||
expect(logs.join("\n")).toMatch(/WARN/);
|
expect(logs.join("\n")).toMatch(/WARN/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("includes sessions across agents in JSON output", async () => {
|
||||||
|
const originalAgents = mocks.listAgentsForGateway.getMockImplementation();
|
||||||
|
const originalResolveStorePath = mocks.resolveStorePath.getMockImplementation();
|
||||||
|
const originalLoadSessionStore = mocks.loadSessionStore.getMockImplementation();
|
||||||
|
|
||||||
|
mocks.listAgentsForGateway.mockReturnValue({
|
||||||
|
defaultId: "main",
|
||||||
|
mainKey: "agent:main:main",
|
||||||
|
scope: "per-sender",
|
||||||
|
agents: [
|
||||||
|
{ id: "main", name: "Main" },
|
||||||
|
{ id: "ops", name: "Ops" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
mocks.resolveStorePath.mockImplementation((_store, opts) =>
|
||||||
|
opts?.agentId === "ops" ? "/tmp/ops.json" : "/tmp/main.json",
|
||||||
|
);
|
||||||
|
mocks.loadSessionStore.mockImplementation((storePath) => {
|
||||||
|
if (storePath === "/tmp/ops.json") {
|
||||||
|
return {
|
||||||
|
"agent:ops:main": {
|
||||||
|
updatedAt: Date.now() - 120_000,
|
||||||
|
inputTokens: 1_000,
|
||||||
|
outputTokens: 1_000,
|
||||||
|
contextTokens: 10_000,
|
||||||
|
model: "pi:opus",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"+1000": {
|
||||||
|
updatedAt: Date.now() - 60_000,
|
||||||
|
verboseLevel: "on",
|
||||||
|
thinkingLevel: "low",
|
||||||
|
inputTokens: 2_000,
|
||||||
|
outputTokens: 3_000,
|
||||||
|
contextTokens: 10_000,
|
||||||
|
model: "pi:opus",
|
||||||
|
sessionId: "abc123",
|
||||||
|
systemSent: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await statusCommand({ json: true }, runtime as never);
|
||||||
|
const payload = JSON.parse((runtime.log as vi.Mock).mock.calls.at(-1)?.[0]);
|
||||||
|
expect(payload.sessions.count).toBe(2);
|
||||||
|
expect(payload.sessions.paths.length).toBe(2);
|
||||||
|
expect(payload.sessions.recent.some((sess: { key?: string }) => sess.key === "agent:ops:main"))
|
||||||
|
.toBe(true);
|
||||||
|
|
||||||
|
if (originalAgents) mocks.listAgentsForGateway.mockImplementation(originalAgents);
|
||||||
|
if (originalResolveStorePath)
|
||||||
|
mocks.resolveStorePath.mockImplementation(originalResolveStorePath);
|
||||||
|
if (originalLoadSessionStore)
|
||||||
|
mocks.loadSessionStore.mockImplementation(originalLoadSessionStore);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -338,6 +338,43 @@ describe("legacy config detection", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
it("auto-migrates bindings[].match.accountID on load and writes back", async () => {
|
||||||
|
await withTempHome(async (home) => {
|
||||||
|
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
|
||||||
|
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||||
|
await fs.writeFile(
|
||||||
|
configPath,
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
bindings: [{ agentId: "main", match: { channel: "telegram", accountID: "work" } }],
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
|
||||||
|
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||||
|
vi.resetModules();
|
||||||
|
try {
|
||||||
|
const { loadConfig } = await import("./config.js");
|
||||||
|
const cfg = loadConfig();
|
||||||
|
expect(cfg.bindings?.[0]?.match?.accountId).toBe("work");
|
||||||
|
|
||||||
|
const raw = await fs.readFile(configPath, "utf-8");
|
||||||
|
const parsed = JSON.parse(raw) as {
|
||||||
|
bindings?: Array<{ match?: { accountId?: string; accountID?: string } }>;
|
||||||
|
};
|
||||||
|
expect(parsed.bindings?.[0]?.match?.accountId).toBe("work");
|
||||||
|
expect(parsed.bindings?.[0]?.match?.accountID).toBeUndefined();
|
||||||
|
expect(
|
||||||
|
warnSpy.mock.calls.some(([msg]) => String(msg).includes("Auto-migrated config")),
|
||||||
|
).toBe(true);
|
||||||
|
} finally {
|
||||||
|
warnSpy.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
it("auto-migrates session.sendPolicy.rules[].match.provider on load and writes back", async () => {
|
it("auto-migrates session.sendPolicy.rules[].match.provider on load and writes back", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
|
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
resolveStorePath,
|
resolveStorePath,
|
||||||
} from "../config/sessions.js";
|
} from "../config/sessions.js";
|
||||||
import {
|
import {
|
||||||
|
isHeartbeatEnabledForAgent,
|
||||||
resolveHeartbeatIntervalMs,
|
resolveHeartbeatIntervalMs,
|
||||||
resolveHeartbeatPrompt,
|
resolveHeartbeatPrompt,
|
||||||
runHeartbeatOnce,
|
runHeartbeatOnce,
|
||||||
@@ -81,6 +82,30 @@ describe("resolveHeartbeatPrompt", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("isHeartbeatEnabledForAgent", () => {
|
||||||
|
it("enables only explicit heartbeat agents when configured", () => {
|
||||||
|
const cfg: ClawdbotConfig = {
|
||||||
|
agents: {
|
||||||
|
defaults: { heartbeat: { every: "30m" } },
|
||||||
|
list: [{ id: "main" }, { id: "ops", heartbeat: { every: "1h" } }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(isHeartbeatEnabledForAgent(cfg, "main")).toBe(false);
|
||||||
|
expect(isHeartbeatEnabledForAgent(cfg, "ops")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to default agent when no explicit heartbeat entries", () => {
|
||||||
|
const cfg: ClawdbotConfig = {
|
||||||
|
agents: {
|
||||||
|
defaults: { heartbeat: { every: "30m" } },
|
||||||
|
list: [{ id: "main" }, { id: "ops" }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(isHeartbeatEnabledForAgent(cfg, "main")).toBe(true);
|
||||||
|
expect(isHeartbeatEnabledForAgent(cfg, "ops")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("resolveHeartbeatDeliveryTarget", () => {
|
describe("resolveHeartbeatDeliveryTarget", () => {
|
||||||
const baseEntry = {
|
const baseEntry = {
|
||||||
sessionId: "sid",
|
sessionId: "sid",
|
||||||
@@ -214,6 +239,21 @@ describe("resolveHeartbeatDeliveryTarget", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("runHeartbeatOnce", () => {
|
describe("runHeartbeatOnce", () => {
|
||||||
|
it("skips when agent heartbeat is not enabled", async () => {
|
||||||
|
const cfg: ClawdbotConfig = {
|
||||||
|
agents: {
|
||||||
|
defaults: { heartbeat: { every: "30m" } },
|
||||||
|
list: [{ id: "main" }, { id: "ops", heartbeat: { every: "1h" } }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await runHeartbeatOnce({ cfg, agentId: "main" });
|
||||||
|
expect(res.status).toBe("skipped");
|
||||||
|
if (res.status === "skipped") {
|
||||||
|
expect(res.reason).toBe("disabled");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("uses the last non-empty payload for delivery", async () => {
|
it("uses the last non-empty payload for delivery", async () => {
|
||||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-hb-"));
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-hb-"));
|
||||||
const storePath = path.join(tmpDir, "sessions.json");
|
const storePath = path.join(tmpDir, "sessions.json");
|
||||||
|
|||||||
Reference in New Issue
Block a user