test(cron): cover delivery context edge cases

This commit is contained in:
Peter Steinberger
2026-04-26 02:20:39 +01:00
parent e309fd485e
commit 00d2fbfda4
6 changed files with 100 additions and 4 deletions

View File

@@ -140,7 +140,7 @@ forever.
| `webhook` | POST finished event payload to a URL |
| `none` | No runner fallback delivery |
Use `--announce --channel telegram --to "-1001234567890"` for channel delivery. For Telegram forum topics, use `-1001234567890:topic:123`. Slack/Discord/Mattermost targets should use explicit prefixes (`channel:<id>`, `user:<id>`).
Use `--announce --channel telegram --to "-1001234567890"` for channel delivery. For Telegram forum topics, use `-1001234567890:topic:123`. Slack/Discord/Mattermost targets should use explicit prefixes (`channel:<id>`, `user:<id>`). Matrix room IDs are case-sensitive; use the exact room ID or `room:!room:server` form from Matrix.
For isolated jobs, chat delivery is shared. If a chat route is available, the
agent can use the `message` tool even when the job uses `--no-deliver`. If the
@@ -148,6 +148,11 @@ agent sends to the configured/current target, OpenClaw skips the fallback
announce. Otherwise `announce`, `webhook`, and `none` only control what the
runner does with the final reply after the agent turn.
When an agent creates an isolated reminder from an active chat, OpenClaw stores
the preserved live delivery target for the fallback announce route. Internal
session keys may be lowercase; provider delivery targets are not reconstructed
from those keys when current chat context is available.
Failure notifications follow a separate destination path:
- `cron.failureDestination` sets a global default for failure notifications.
@@ -418,6 +423,9 @@ openclaw doctor
- Delivery mode `none` means no runner fallback send is expected. The agent can
still send directly with the `message` tool when a chat route is available.
- Delivery target missing/invalid (`channel`/`to`) means outbound was skipped.
- For Matrix, copied or legacy jobs with lowercased `delivery.to` room IDs can
fail because Matrix room IDs are case-sensitive. Edit the job to the exact
`!room:server` or `room:!room:server` value from Matrix.
- Channel auth errors (`unauthorized`, `Forbidden`) mean delivery was blocked by credentials.
- If the isolated run returns only the silent token (`NO_REPLY` / `no_reply`),
OpenClaw suppresses direct outbound delivery and also suppresses the fallback

View File

@@ -883,6 +883,11 @@ Matrix accepts these target forms anywhere OpenClaw asks you for a room or user
- Rooms: `!room:server`, `room:!room:server`, or `matrix:room:!room:server`
- Aliases: `#alias:server`, `channel:#alias:server`, or `matrix:channel:#alias:server`
Matrix room IDs are case-sensitive. Use the exact room ID casing from Matrix
when configuring explicit delivery targets, cron jobs, bindings, or allowlists.
OpenClaw keeps internal session keys canonical for storage, so those lowercase
keys are not a reliable source for Matrix delivery IDs.
Live directory lookup uses the logged-in Matrix account:
- User lookups query the Matrix user directory on that homeserver.

View File

@@ -138,6 +138,10 @@ Delivery ownership note:
- `announce` fallback-delivers the final reply only when the agent did not send
directly to the resolved target. `webhook` posts the finished payload to a URL.
`none` disables runner fallback delivery.
- Reminders created from an active chat preserve the live chat delivery target
for fallback announce delivery. Internal session keys may be lowercase; do not
use them as a source of truth for case-sensitive provider IDs such as Matrix
room IDs.
## Common admin commands

View File

@@ -199,4 +199,28 @@ describe("createOpenClawTools cron context wiring", () => {
},
});
});
it("uses agent route context when auto-threading context is unavailable", async () => {
const { createOpenClawTools } = await import("./openclaw-tools.js");
createOpenClawTools({
agentSessionKey: "agent:main:matrix:channel:!abcdef1234567890:example.org",
agentChannel: "matrix",
agentAccountId: "bot-a",
agentTo: "room:!FallbackRoom:Example.Org",
agentThreadId: "$FallbackThread:Example.Org",
disableMessageTool: true,
disablePluginTools: true,
});
expect(mocks.createCronToolOptions).toHaveBeenCalledWith({
agentSessionKey: "agent:main:matrix:channel:!abcdef1234567890:example.org",
currentDeliveryContext: {
channel: "matrix",
to: "room:!FallbackRoom:Example.Org",
accountId: "bot-a",
threadId: "$FallbackThread:Example.Org",
},
});
});
});

View File

@@ -15,6 +15,14 @@ vi.mock("../agent-scope.js", async () => {
import { createCronTool } from "./cron-tool.js";
describe("cron tool", () => {
type TestDelivery = {
mode?: string;
channel?: string;
to?: string;
accountId?: string;
threadId?: string | number;
};
function createTestCronTool(
opts?: Parameters<typeof createCronTool>[0],
): ReturnType<typeof createCronTool> {
@@ -64,7 +72,7 @@ describe("cron tool", () => {
currentDeliveryContext?: NonNullable<
Parameters<typeof createCronTool>[0]
>["currentDeliveryContext"];
delivery?: { mode?: string; channel?: string; to?: string } | null;
delivery?: TestDelivery | null;
}) {
const tool = createTestCronTool({
agentSessionKey: params.agentSessionKey,
@@ -79,7 +87,7 @@ describe("cron tool", () => {
});
const call = callGatewayMock.mock.calls[0]?.[0] as {
params?: { delivery?: { mode?: string; channel?: string; to?: string } };
params?: { delivery?: TestDelivery };
};
return call?.params?.delivery;
}
@@ -464,6 +472,53 @@ describe("cron tool", () => {
});
});
it("keeps explicit delivery account and thread while filling target from context", async () => {
expect(
await executeAddAndReadDelivery({
callId: "call-explicit-delivery-fields-win",
agentSessionKey: "agent:main:matrix:channel:!abcdef1234567890:example.org",
currentDeliveryContext: {
channel: "matrix",
to: "!AbCdEf1234567890:example.org",
accountId: "context-bot",
threadId: "$ContextThread:Example.Org",
},
delivery: {
mode: "announce",
accountId: "explicit-bot",
threadId: "$ExplicitThread:Example.Org",
},
}),
).toEqual({
mode: "announce",
channel: "matrix",
to: "!AbCdEf1234567890:example.org",
accountId: "explicit-bot",
threadId: "$ExplicitThread:Example.Org",
});
});
it("trims current context fields without changing provider target casing", async () => {
expect(
await executeAddAndReadDelivery({
callId: "call-trim-current-context",
agentSessionKey: "agent:main:matrix:channel:!abcdef1234567890:example.org",
currentDeliveryContext: {
channel: " Matrix ",
to: " !AbCdEf1234567890:Example.Org ",
accountId: " Bot-A ",
threadId: " $RootEvent:Example.Org ",
},
}),
).toEqual({
mode: "announce",
channel: "matrix",
to: "!AbCdEf1234567890:Example.Org",
accountId: "bot-a",
threadId: "$RootEvent:Example.Org",
});
});
it("infers delivery from current context even when no session key is available", async () => {
expect(
await executeAddAndReadDelivery({

View File

@@ -644,8 +644,8 @@ Use jobId as the canonical identifier; id is accepted for compatibility. Use con
inferDeliveryFromSessionKey(opts.agentSessionKey);
if (inferred) {
(job as { delivery?: unknown }).delivery = {
...delivery,
...inferred,
...delivery,
} satisfies CronDelivery;
}
}