Skip to content

Commit 77876bd

Browse files
committed
test: share ghost reminder heartbeat fixtures
1 parent 805481c commit 77876bd

1 file changed

Lines changed: 86 additions & 114 deletions

File tree

src/infra/heartbeat-runner.ghost-reminder.test.ts

Lines changed: 86 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,43 @@ describe("Ghost reminder bug (issue #13317)", () => {
5959
return { cfg, sessionKey };
6060
};
6161

62+
const createLastTargetConfig = (params: {
63+
tmpDir: string;
64+
storePath: string;
65+
isolatedSession?: boolean;
66+
}): OpenClawConfig => ({
67+
agents: {
68+
defaults: {
69+
workspace: params.tmpDir,
70+
heartbeat: {
71+
every: "5m",
72+
target: "last",
73+
...(params.isolatedSession === true ? { isolatedSession: true } : {}),
74+
},
75+
},
76+
},
77+
channels: { telegram: { allowFrom: ["*"] } },
78+
session: { store: params.storePath },
79+
});
80+
81+
const writeTelegramSessionStore = async (
82+
storePath: string,
83+
sessionKey: string,
84+
overrides: Record<string, unknown>,
85+
): Promise<void> => {
86+
await fs.writeFile(
87+
storePath,
88+
JSON.stringify({
89+
[sessionKey]: {
90+
sessionId: "sid",
91+
updatedAt: Date.now(),
92+
lastChannel: "telegram",
93+
...overrides,
94+
},
95+
}),
96+
);
97+
};
98+
6299
const expectCronEventPrompt = (
63100
calledCtx: {
64101
Provider?: string;
@@ -144,6 +181,35 @@ describe("Ghost reminder bug (issue #13317)", () => {
144181
);
145182
};
146183

184+
const expectUntrustedEventOwnership = async (params: {
185+
tmpPrefix: string;
186+
reason: "hook:wake" | "interval";
187+
isolatedSession?: boolean;
188+
forceSenderIsOwnerFalse: boolean;
189+
}): Promise<void> => {
190+
const { result, sendTelegram, calledCtx } = await runHeartbeatCase({
191+
tmpPrefix: params.tmpPrefix,
192+
replyText: "Handled internally",
193+
reason: params.reason,
194+
target: "none",
195+
isolatedSession: params.isolatedSession,
196+
enqueue: (sessionKey) => {
197+
enqueueSystemEvent("GitHub issue opened: untrusted webhook content", {
198+
sessionKey,
199+
trusted: false,
200+
});
201+
},
202+
});
203+
204+
expect(result.status).toBe("ran");
205+
expect(calledCtx?.Provider).toBe("heartbeat");
206+
if (params.isolatedSession === true) {
207+
expect(calledCtx?.SessionKey).toContain(":heartbeat");
208+
}
209+
expect(calledCtx?.ForceSenderIsOwnerFalse).toBe(params.forceSenderIsOwnerFalse);
210+
expect(sendTelegram).not.toHaveBeenCalled();
211+
};
212+
147213
it("does not use CRON_EVENT_PROMPT when only a HEARTBEAT_OK event is present", async () => {
148214
const { result, sendTelegram, calledCtx, replyCallCount } = await runHeartbeatCase({
149215
tmpPrefix: "openclaw-ghost-",
@@ -330,87 +396,37 @@ describe("Ghost reminder bug (issue #13317)", () => {
330396
});
331397

332398
it("forces owner downgrade for untrusted hook:wake system events", async () => {
333-
const { result, sendTelegram, calledCtx } = await runHeartbeatCase({
399+
await expectUntrustedEventOwnership({
334400
tmpPrefix: "openclaw-hook-untrusted-",
335-
replyText: "Handled internally",
336401
reason: "hook:wake",
337-
target: "none",
338-
enqueue: (sessionKey) => {
339-
enqueueSystemEvent("GitHub issue opened: untrusted webhook content", {
340-
sessionKey,
341-
trusted: false,
342-
});
343-
},
402+
forceSenderIsOwnerFalse: true,
344403
});
345-
346-
expect(result.status).toBe("ran");
347-
expect(calledCtx?.Provider).toBe("heartbeat");
348-
expect(calledCtx?.ForceSenderIsOwnerFalse).toBe(true);
349-
expect(sendTelegram).not.toHaveBeenCalled();
350404
});
351405

352406
it("forces owner downgrade for untrusted interval events", async () => {
353-
const { result, sendTelegram, calledCtx } = await runHeartbeatCase({
407+
await expectUntrustedEventOwnership({
354408
tmpPrefix: "openclaw-interval-untrusted-",
355-
replyText: "Handled internally",
356409
reason: "interval",
357-
target: "none",
358-
enqueue: (sessionKey) => {
359-
enqueueSystemEvent("GitHub issue opened: untrusted webhook content", {
360-
sessionKey,
361-
trusted: false,
362-
});
363-
},
410+
forceSenderIsOwnerFalse: true,
364411
});
365-
366-
expect(result.status).toBe("ran");
367-
expect(calledCtx?.Provider).toBe("heartbeat");
368-
expect(calledCtx?.ForceSenderIsOwnerFalse).toBe(true);
369-
expect(sendTelegram).not.toHaveBeenCalled();
370412
});
371413

372414
it("does not force owner downgrade for untrusted hook:wake events with isolated sessions", async () => {
373-
const { result, sendTelegram, calledCtx } = await runHeartbeatCase({
415+
await expectUntrustedEventOwnership({
374416
tmpPrefix: "openclaw-hook-untrusted-isolated-",
375-
replyText: "Handled internally",
376417
reason: "hook:wake",
377-
target: "none",
378418
isolatedSession: true,
379-
enqueue: (sessionKey) => {
380-
enqueueSystemEvent("GitHub issue opened: untrusted webhook content", {
381-
sessionKey,
382-
trusted: false,
383-
});
384-
},
419+
forceSenderIsOwnerFalse: false,
385420
});
386-
387-
expect(result.status).toBe("ran");
388-
expect(calledCtx?.Provider).toBe("heartbeat");
389-
expect(calledCtx?.SessionKey).toContain(":heartbeat");
390-
expect(calledCtx?.ForceSenderIsOwnerFalse).toBe(false);
391-
expect(sendTelegram).not.toHaveBeenCalled();
392421
});
393422

394423
it("does not force owner downgrade for isolated interval runs with only base-session untrusted events", async () => {
395-
const { result, sendTelegram, calledCtx } = await runHeartbeatCase({
424+
await expectUntrustedEventOwnership({
396425
tmpPrefix: "openclaw-interval-untrusted-isolated-",
397-
replyText: "Handled internally",
398426
reason: "interval",
399-
target: "none",
400427
isolatedSession: true,
401-
enqueue: (sessionKey) => {
402-
enqueueSystemEvent("GitHub issue opened: untrusted webhook content", {
403-
sessionKey,
404-
trusted: false,
405-
});
406-
},
428+
forceSenderIsOwnerFalse: false,
407429
});
408-
409-
expect(result.status).toBe("ran");
410-
expect(calledCtx?.Provider).toBe("heartbeat");
411-
expect(calledCtx?.SessionKey).toContain(":heartbeat");
412-
expect(calledCtx?.ForceSenderIsOwnerFalse).toBe(false);
413-
expect(sendTelegram).not.toHaveBeenCalled();
414430
});
415431

416432
it("routes wake-triggered heartbeat replies using queued system-event delivery context", async () => {
@@ -475,32 +491,9 @@ describe("Ghost reminder bug (issue #13317)", () => {
475491

476492
it("does not reuse stale turn-source routing for isolated wake runs", async () => {
477493
await withTempHeartbeatSandbox(async ({ tmpDir, storePath, replySpy }) => {
478-
const cfg: OpenClawConfig = {
479-
agents: {
480-
defaults: {
481-
workspace: tmpDir,
482-
heartbeat: {
483-
every: "5m",
484-
target: "last",
485-
isolatedSession: true,
486-
},
487-
},
488-
},
489-
channels: { telegram: { allowFrom: ["*"] } },
490-
session: { store: storePath },
491-
};
494+
const cfg = createLastTargetConfig({ tmpDir, storePath, isolatedSession: true });
492495
const sessionKey = resolveMainSessionKey(cfg);
493-
await fs.writeFile(
494-
storePath,
495-
JSON.stringify({
496-
[sessionKey]: {
497-
sessionId: "sid",
498-
updatedAt: Date.now(),
499-
lastChannel: "telegram",
500-
lastTo: "-100155462274",
501-
},
502-
}),
503-
);
496+
await writeTelegramSessionStore(storePath, sessionKey, { lastTo: "-100155462274" });
504497

505498
const sendTelegram = vi.fn().mockResolvedValue({
506499
messageId: "m1",
@@ -609,38 +602,17 @@ describe("Ghost reminder bug (issue #13317)", () => {
609602

610603
it("keeps Telegram topic routing for isolated scheduled heartbeats", async () => {
611604
await withTempHeartbeatSandbox(async ({ tmpDir, storePath, replySpy }) => {
612-
const cfg: OpenClawConfig = {
613-
agents: {
614-
defaults: {
615-
workspace: tmpDir,
616-
heartbeat: {
617-
every: "5m",
618-
target: "last",
619-
isolatedSession: true,
620-
},
621-
},
622-
},
623-
channels: { telegram: { allowFrom: ["*"] } },
624-
session: { store: storePath },
625-
};
605+
const cfg = createLastTargetConfig({ tmpDir, storePath, isolatedSession: true });
626606
const sessionKey = resolveMainSessionKey(cfg);
627-
await fs.writeFile(
628-
storePath,
629-
JSON.stringify({
630-
[sessionKey]: {
631-
sessionId: "sid",
632-
updatedAt: Date.now(),
633-
lastChannel: "telegram",
634-
lastTo: "-100155462274",
635-
deliveryContext: {
636-
channel: "telegram",
637-
to: "-100155462274",
638-
threadId: 42,
639-
},
640-
chatType: "group",
641-
},
642-
}),
643-
);
607+
await writeTelegramSessionStore(storePath, sessionKey, {
608+
lastTo: "-100155462274",
609+
deliveryContext: {
610+
channel: "telegram",
611+
to: "-100155462274",
612+
threadId: 42,
613+
},
614+
chatType: "group",
615+
});
644616

645617
const sendTelegram = vi.fn().mockResolvedValue({
646618
messageId: "m1",

0 commit comments

Comments
 (0)