Files
openclaw/src/cron/service/wake.test.ts
Kaspre 072fa9b174 fix(wake): handle relative + agent-prefixed session keys consistently in cron adapter
Address review findings from successive codex rounds:

1. next-heartbeat + sessionKey now fires a targeted immediate wake.
   The regularly-scheduled heartbeat fires for the agent's main session,
   not the supplied sessionKey, so an event queued for a non-main session
   would sit stranded indefinitely; an "event"-intent wake is also
   deferred as not-due by the heartbeat runner and not retried, so
   neither path delivers without an explicit immediate wake.

2. resolveCronWakeTarget now always runs through resolveCronAgent, both
   for agent-prefixed session keys (so non-default agents are honored)
   and relative keys (so the configured default agent is used instead
   of the hardcoded "main" returned by resolveAgentIdFromSessionKey).
   Mirrors the matching fix in the enqueueSystemEvent adapter so wake
   and enqueue resolve to the same target.

3. Generated Swift `WakeParams` models now expose the new optional
   `sessionkey` field (codingKey "sessionKey") in both the macOS and
   shared OpenClawKit copies. Locally regenerated from agent.ts via
   protocol:gen + protocol:gen:swift would have produced this; the
   environment couldn't run the generators (fs-safe transitive
   typecheck errors), so the diff was applied by hand to match what
   pnpm protocol:check would output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:24:30 +01:00

100 lines
3.5 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { wake } from "./timer.js";
function createState() {
const enqueueSystemEvent = vi.fn();
const requestHeartbeat = vi.fn();
return {
state: {
deps: {
enqueueSystemEvent,
requestHeartbeat,
},
} as unknown as Parameters<typeof wake>[0],
enqueueSystemEvent,
requestHeartbeat,
};
}
describe("wake (cron timer)", () => {
it("returns ok:false on empty text without enqueueing or waking", () => {
const { state, enqueueSystemEvent, requestHeartbeat } = createState();
expect(wake(state, { mode: "now", text: " " })).toEqual({ ok: false });
expect(enqueueSystemEvent).not.toHaveBeenCalled();
expect(requestHeartbeat).not.toHaveBeenCalled();
});
it("enqueues without sessionKey when omitted", () => {
const { state, enqueueSystemEvent, requestHeartbeat } = createState();
expect(wake(state, { mode: "now", text: "ping" })).toEqual({ ok: true });
expect(enqueueSystemEvent).toHaveBeenCalledWith("ping", undefined);
expect(requestHeartbeat).toHaveBeenCalledWith({
source: "manual",
intent: "immediate",
reason: "wake",
});
});
it("threads sessionKey to both enqueue and heartbeat on mode=now", () => {
const { state, enqueueSystemEvent, requestHeartbeat } = createState();
expect(
wake(state, {
mode: "now",
text: "ping",
sessionKey: "agent:main:telegram:dm:42",
}),
).toEqual({ ok: true });
expect(enqueueSystemEvent).toHaveBeenCalledWith("ping", {
sessionKey: "agent:main:telegram:dm:42",
});
expect(requestHeartbeat).toHaveBeenCalledWith({
source: "manual",
intent: "immediate",
reason: "wake",
sessionKey: "agent:main:telegram:dm:42",
});
});
it("threads sessionKey to enqueue and fires a targeted immediate wake on mode=next-heartbeat", () => {
// next-heartbeat + sessionKey collapses to immediate-targeted behavior:
// the regularly-scheduled heartbeat fires for agent-main and never peeks
// a non-main session queue, and an "event"-intent wake is not retried by
// the heartbeat runner. Targeted immediate is the only reliable path.
const { state, enqueueSystemEvent, requestHeartbeat } = createState();
expect(
wake(state, {
mode: "next-heartbeat",
text: "ping",
sessionKey: "agent:main:slack:42",
}),
).toEqual({ ok: true });
expect(enqueueSystemEvent).toHaveBeenCalledWith("ping", {
sessionKey: "agent:main:slack:42",
});
expect(requestHeartbeat).toHaveBeenCalledWith({
source: "manual",
intent: "immediate",
reason: "wake",
sessionKey: "agent:main:slack:42",
});
});
it("does not fire a wake on mode=next-heartbeat when no sessionKey is supplied", () => {
const { state, enqueueSystemEvent, requestHeartbeat } = createState();
expect(wake(state, { mode: "next-heartbeat", text: "ping" })).toEqual({ ok: true });
expect(enqueueSystemEvent).toHaveBeenCalledWith("ping", undefined);
expect(requestHeartbeat).not.toHaveBeenCalled();
});
it("treats whitespace-only sessionKey as omitted", () => {
const { state, enqueueSystemEvent, requestHeartbeat } = createState();
wake(state, { mode: "now", text: "ping", sessionKey: " " });
expect(enqueueSystemEvent).toHaveBeenCalledWith("ping", undefined);
expect(requestHeartbeat).toHaveBeenCalledWith({
source: "manual",
intent: "immediate",
reason: "wake",
});
});
});