mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 13:01:25 +00:00
refactor(test): share heartbeat sandbox
This commit is contained in:
@@ -33,15 +33,21 @@ describe("resolveHeartbeatIntervalMs", () => {
|
|||||||
async function seedSessionStore(
|
async function seedSessionStore(
|
||||||
storePath: string,
|
storePath: string,
|
||||||
sessionKey: string,
|
sessionKey: string,
|
||||||
session: { lastChannel: string; lastProvider: string; lastTo: string },
|
session: {
|
||||||
|
sessionId?: string;
|
||||||
|
updatedAt?: number;
|
||||||
|
lastChannel: string;
|
||||||
|
lastProvider: string;
|
||||||
|
lastTo: string;
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
storePath,
|
storePath,
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{
|
{
|
||||||
[sessionKey]: {
|
[sessionKey]: {
|
||||||
sessionId: "sid",
|
sessionId: session.sessionId ?? "sid",
|
||||||
updatedAt: Date.now(),
|
updatedAt: session.updatedAt ?? Date.now(),
|
||||||
...session,
|
...session,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -51,6 +57,24 @@ describe("resolveHeartbeatIntervalMs", () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function withTempHeartbeatSandbox<T>(
|
||||||
|
fn: (ctx: {
|
||||||
|
tmpDir: string;
|
||||||
|
storePath: string;
|
||||||
|
replySpy: ReturnType<typeof vi.spyOn>;
|
||||||
|
}) => Promise<T>,
|
||||||
|
) {
|
||||||
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hb-"));
|
||||||
|
const storePath = path.join(tmpDir, "sessions.json");
|
||||||
|
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||||
|
try {
|
||||||
|
return await fn({ tmpDir, storePath, replySpy });
|
||||||
|
} finally {
|
||||||
|
replySpy.mockRestore();
|
||||||
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function withTempTelegramHeartbeatSandbox<T>(
|
async function withTempTelegramHeartbeatSandbox<T>(
|
||||||
fn: (ctx: {
|
fn: (ctx: {
|
||||||
tmpDir: string;
|
tmpDir: string;
|
||||||
@@ -77,10 +101,7 @@ describe("resolveHeartbeatIntervalMs", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
it("respects ackMaxChars for heartbeat acks", async () => {
|
it("respects ackMaxChars for heartbeat acks", async () => {
|
||||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hb-"));
|
await withTempHeartbeatSandbox(async ({ tmpDir, storePath, replySpy }) => {
|
||||||
const storePath = path.join(tmpDir, "sessions.json");
|
|
||||||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
|
||||||
try {
|
|
||||||
const cfg: OpenClawConfig = {
|
const cfg: OpenClawConfig = {
|
||||||
agents: {
|
agents: {
|
||||||
defaults: {
|
defaults: {
|
||||||
@@ -97,22 +118,11 @@ describe("resolveHeartbeatIntervalMs", () => {
|
|||||||
};
|
};
|
||||||
const sessionKey = resolveMainSessionKey(cfg);
|
const sessionKey = resolveMainSessionKey(cfg);
|
||||||
|
|
||||||
await fs.writeFile(
|
await seedSessionStore(storePath, sessionKey, {
|
||||||
storePath,
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
[sessionKey]: {
|
|
||||||
sessionId: "sid",
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
lastChannel: "whatsapp",
|
lastChannel: "whatsapp",
|
||||||
lastProvider: "whatsapp",
|
lastProvider: "whatsapp",
|
||||||
lastTo: "+1555",
|
lastTo: "+1555",
|
||||||
},
|
});
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
replySpy.mockResolvedValue({ text: "HEARTBEAT_OK 🦞" });
|
replySpy.mockResolvedValue({ text: "HEARTBEAT_OK 🦞" });
|
||||||
const sendWhatsApp = vi.fn().mockResolvedValue({
|
const sendWhatsApp = vi.fn().mockResolvedValue({
|
||||||
@@ -132,17 +142,11 @@ describe("resolveHeartbeatIntervalMs", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(sendWhatsApp).toHaveBeenCalled();
|
expect(sendWhatsApp).toHaveBeenCalled();
|
||||||
} finally {
|
});
|
||||||
replySpy.mockRestore();
|
|
||||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sends HEARTBEAT_OK when visibility.showOk is true", async () => {
|
it("sends HEARTBEAT_OK when visibility.showOk is true", async () => {
|
||||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hb-"));
|
await withTempHeartbeatSandbox(async ({ tmpDir, storePath, replySpy }) => {
|
||||||
const storePath = path.join(tmpDir, "sessions.json");
|
|
||||||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
|
||||||
try {
|
|
||||||
const cfg: OpenClawConfig = {
|
const cfg: OpenClawConfig = {
|
||||||
agents: {
|
agents: {
|
||||||
defaults: {
|
defaults: {
|
||||||
@@ -158,22 +162,11 @@ describe("resolveHeartbeatIntervalMs", () => {
|
|||||||
};
|
};
|
||||||
const sessionKey = resolveMainSessionKey(cfg);
|
const sessionKey = resolveMainSessionKey(cfg);
|
||||||
|
|
||||||
await fs.writeFile(
|
await seedSessionStore(storePath, sessionKey, {
|
||||||
storePath,
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
[sessionKey]: {
|
|
||||||
sessionId: "sid",
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
lastChannel: "whatsapp",
|
lastChannel: "whatsapp",
|
||||||
lastProvider: "whatsapp",
|
lastProvider: "whatsapp",
|
||||||
lastTo: "+1555",
|
lastTo: "+1555",
|
||||||
},
|
});
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
replySpy.mockResolvedValue({ text: "HEARTBEAT_OK" });
|
replySpy.mockResolvedValue({ text: "HEARTBEAT_OK" });
|
||||||
const sendWhatsApp = vi.fn().mockResolvedValue({
|
const sendWhatsApp = vi.fn().mockResolvedValue({
|
||||||
@@ -194,17 +187,11 @@ describe("resolveHeartbeatIntervalMs", () => {
|
|||||||
|
|
||||||
expect(sendWhatsApp).toHaveBeenCalledTimes(1);
|
expect(sendWhatsApp).toHaveBeenCalledTimes(1);
|
||||||
expect(sendWhatsApp).toHaveBeenCalledWith("+1555", "HEARTBEAT_OK", expect.any(Object));
|
expect(sendWhatsApp).toHaveBeenCalledWith("+1555", "HEARTBEAT_OK", expect.any(Object));
|
||||||
} finally {
|
});
|
||||||
replySpy.mockRestore();
|
|
||||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("skips heartbeat LLM calls when visibility disables all output", async () => {
|
it("skips heartbeat LLM calls when visibility disables all output", async () => {
|
||||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hb-"));
|
await withTempHeartbeatSandbox(async ({ tmpDir, storePath, replySpy }) => {
|
||||||
const storePath = path.join(tmpDir, "sessions.json");
|
|
||||||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
|
||||||
try {
|
|
||||||
const cfg: OpenClawConfig = {
|
const cfg: OpenClawConfig = {
|
||||||
agents: {
|
agents: {
|
||||||
defaults: {
|
defaults: {
|
||||||
@@ -225,22 +212,11 @@ describe("resolveHeartbeatIntervalMs", () => {
|
|||||||
};
|
};
|
||||||
const sessionKey = resolveMainSessionKey(cfg);
|
const sessionKey = resolveMainSessionKey(cfg);
|
||||||
|
|
||||||
await fs.writeFile(
|
await seedSessionStore(storePath, sessionKey, {
|
||||||
storePath,
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
[sessionKey]: {
|
|
||||||
sessionId: "sid",
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
lastChannel: "whatsapp",
|
lastChannel: "whatsapp",
|
||||||
lastProvider: "whatsapp",
|
lastProvider: "whatsapp",
|
||||||
lastTo: "+1555",
|
lastTo: "+1555",
|
||||||
},
|
});
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const sendWhatsApp = vi.fn().mockResolvedValue({
|
const sendWhatsApp = vi.fn().mockResolvedValue({
|
||||||
messageId: "m1",
|
messageId: "m1",
|
||||||
@@ -261,17 +237,11 @@ describe("resolveHeartbeatIntervalMs", () => {
|
|||||||
expect(replySpy).not.toHaveBeenCalled();
|
expect(replySpy).not.toHaveBeenCalled();
|
||||||
expect(sendWhatsApp).not.toHaveBeenCalled();
|
expect(sendWhatsApp).not.toHaveBeenCalled();
|
||||||
expect(result).toEqual({ status: "skipped", reason: "alerts-disabled" });
|
expect(result).toEqual({ status: "skipped", reason: "alerts-disabled" });
|
||||||
} finally {
|
});
|
||||||
replySpy.mockRestore();
|
|
||||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("skips delivery for markup-wrapped HEARTBEAT_OK", async () => {
|
it("skips delivery for markup-wrapped HEARTBEAT_OK", async () => {
|
||||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hb-"));
|
await withTempHeartbeatSandbox(async ({ tmpDir, storePath, replySpy }) => {
|
||||||
const storePath = path.join(tmpDir, "sessions.json");
|
|
||||||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
|
||||||
try {
|
|
||||||
const cfg: OpenClawConfig = {
|
const cfg: OpenClawConfig = {
|
||||||
agents: {
|
agents: {
|
||||||
defaults: {
|
defaults: {
|
||||||
@@ -287,22 +257,11 @@ describe("resolveHeartbeatIntervalMs", () => {
|
|||||||
};
|
};
|
||||||
const sessionKey = resolveMainSessionKey(cfg);
|
const sessionKey = resolveMainSessionKey(cfg);
|
||||||
|
|
||||||
await fs.writeFile(
|
await seedSessionStore(storePath, sessionKey, {
|
||||||
storePath,
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
[sessionKey]: {
|
|
||||||
sessionId: "sid",
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
lastChannel: "whatsapp",
|
lastChannel: "whatsapp",
|
||||||
lastProvider: "whatsapp",
|
lastProvider: "whatsapp",
|
||||||
lastTo: "+1555",
|
lastTo: "+1555",
|
||||||
},
|
});
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
replySpy.mockResolvedValue({ text: "<b>HEARTBEAT_OK</b>" });
|
replySpy.mockResolvedValue({ text: "<b>HEARTBEAT_OK</b>" });
|
||||||
const sendWhatsApp = vi.fn().mockResolvedValue({
|
const sendWhatsApp = vi.fn().mockResolvedValue({
|
||||||
@@ -322,17 +281,11 @@ describe("resolveHeartbeatIntervalMs", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(sendWhatsApp).not.toHaveBeenCalled();
|
expect(sendWhatsApp).not.toHaveBeenCalled();
|
||||||
} finally {
|
});
|
||||||
replySpy.mockRestore();
|
|
||||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not regress updatedAt when restoring heartbeat sessions", async () => {
|
it("does not regress updatedAt when restoring heartbeat sessions", async () => {
|
||||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hb-"));
|
await withTempHeartbeatSandbox(async ({ tmpDir, storePath, replySpy }) => {
|
||||||
const storePath = path.join(tmpDir, "sessions.json");
|
|
||||||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
|
||||||
try {
|
|
||||||
const originalUpdatedAt = 1000;
|
const originalUpdatedAt = 1000;
|
||||||
const bumpedUpdatedAt = 2000;
|
const bumpedUpdatedAt = 2000;
|
||||||
const cfg: OpenClawConfig = {
|
const cfg: OpenClawConfig = {
|
||||||
@@ -350,22 +303,12 @@ describe("resolveHeartbeatIntervalMs", () => {
|
|||||||
};
|
};
|
||||||
const sessionKey = resolveMainSessionKey(cfg);
|
const sessionKey = resolveMainSessionKey(cfg);
|
||||||
|
|
||||||
await fs.writeFile(
|
await seedSessionStore(storePath, sessionKey, {
|
||||||
storePath,
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
[sessionKey]: {
|
|
||||||
sessionId: "sid",
|
|
||||||
updatedAt: originalUpdatedAt,
|
updatedAt: originalUpdatedAt,
|
||||||
lastChannel: "whatsapp",
|
lastChannel: "whatsapp",
|
||||||
lastProvider: "whatsapp",
|
lastProvider: "whatsapp",
|
||||||
lastTo: "+1555",
|
lastTo: "+1555",
|
||||||
},
|
});
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
replySpy.mockImplementationOnce(async () => {
|
replySpy.mockImplementationOnce(async () => {
|
||||||
const raw = await fs.readFile(storePath, "utf-8");
|
const raw = await fs.readFile(storePath, "utf-8");
|
||||||
@@ -395,17 +338,11 @@ describe("resolveHeartbeatIntervalMs", () => {
|
|||||||
{ updatedAt?: number } | undefined
|
{ updatedAt?: number } | undefined
|
||||||
>;
|
>;
|
||||||
expect(finalStore[sessionKey]?.updatedAt).toBe(bumpedUpdatedAt);
|
expect(finalStore[sessionKey]?.updatedAt).toBe(bumpedUpdatedAt);
|
||||||
} finally {
|
});
|
||||||
replySpy.mockRestore();
|
|
||||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("skips WhatsApp delivery when not linked or running", async () => {
|
it("skips WhatsApp delivery when not linked or running", async () => {
|
||||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hb-"));
|
await withTempHeartbeatSandbox(async ({ tmpDir, storePath, replySpy }) => {
|
||||||
const storePath = path.join(tmpDir, "sessions.json");
|
|
||||||
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
|
||||||
try {
|
|
||||||
const cfg: OpenClawConfig = {
|
const cfg: OpenClawConfig = {
|
||||||
agents: {
|
agents: {
|
||||||
defaults: {
|
defaults: {
|
||||||
@@ -418,22 +355,11 @@ describe("resolveHeartbeatIntervalMs", () => {
|
|||||||
};
|
};
|
||||||
const sessionKey = resolveMainSessionKey(cfg);
|
const sessionKey = resolveMainSessionKey(cfg);
|
||||||
|
|
||||||
await fs.writeFile(
|
await seedSessionStore(storePath, sessionKey, {
|
||||||
storePath,
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
[sessionKey]: {
|
|
||||||
sessionId: "sid",
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
lastChannel: "whatsapp",
|
lastChannel: "whatsapp",
|
||||||
lastProvider: "whatsapp",
|
lastProvider: "whatsapp",
|
||||||
lastTo: "+1555",
|
lastTo: "+1555",
|
||||||
},
|
});
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
replySpy.mockResolvedValue({ text: "Heartbeat alert" });
|
replySpy.mockResolvedValue({ text: "Heartbeat alert" });
|
||||||
const sendWhatsApp = vi.fn().mockResolvedValue({
|
const sendWhatsApp = vi.fn().mockResolvedValue({
|
||||||
@@ -455,10 +381,7 @@ describe("resolveHeartbeatIntervalMs", () => {
|
|||||||
expect(res.status).toBe("skipped");
|
expect(res.status).toBe("skipped");
|
||||||
expect(res).toMatchObject({ reason: "whatsapp-not-linked" });
|
expect(res).toMatchObject({ reason: "whatsapp-not-linked" });
|
||||||
expect(sendWhatsApp).not.toHaveBeenCalled();
|
expect(sendWhatsApp).not.toHaveBeenCalled();
|
||||||
} finally {
|
});
|
||||||
replySpy.mockRestore();
|
|
||||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
async function expectTelegramHeartbeatAccountId(params: {
|
async function expectTelegramHeartbeatAccountId(params: {
|
||||||
|
|||||||
Reference in New Issue
Block a user