Files
openclaw/extensions/telegram/src/exec-approvals.test.ts
T
Peter Steinberger fc5265d685 improve: tighten newest regression ownership (#123606)
* test: tighten newest regression ownership

* test(ui): stabilize request-driven e2e waits

* fix(ci): stabilize lifecycle-bound test observations

* test(ci): pin current Telegram job cap

* test(ui): wait for terminal selection owner

* test(mac): use shared unread wait policy
2026-08-14 08:08:45 -07:00

577 lines
18 KiB
TypeScript

// Telegram tests cover exec approvals plugin behavior.
import path from "node:path";
import type {
OpenClawConfig,
TelegramAccountConfig,
TelegramExecApprovalConfig,
} from "openclaw/plugin-sdk/config-contracts";
import {
normalizeSessionDeliveryState,
upsertSessionEntry,
} from "openclaw/plugin-sdk/session-store-runtime";
import { closeOpenClawAgentDatabasesForTest } from "openclaw/plugin-sdk/sqlite-runtime-testing";
import {
resolvePreferredOpenClawTmpDir,
tempWorkspaceSync,
type TempWorkspaceSync,
} from "openclaw/plugin-sdk/temp-path";
import { afterEach, describe, expect, it } from "vitest";
import {
getTelegramExecApprovalApprovers,
isTelegramExecApprovalAuthorizedSender,
isTelegramExecApprovalApprover,
isTelegramExecApprovalClientEnabled,
isTelegramExecApprovalTargetRecipient,
resolveTelegramExecApprovalTarget,
shouldHandleTelegramExecApprovalRequest,
shouldInjectTelegramExecApprovalButtons,
} from "./exec-approvals.js";
const tempWorkspaces: TempWorkspaceSync[] = [];
type TelegramExecApprovalRequest = Parameters<
typeof shouldHandleTelegramExecApprovalRequest
>[0]["request"];
afterEach(() => {
closeOpenClawAgentDatabasesForTest();
for (const workspace of tempWorkspaces.splice(0)) {
workspace.cleanup();
}
});
function buildConfig(
execApprovals?: NonNullable<NonNullable<OpenClawConfig["channels"]>["telegram"]>["execApprovals"],
channelOverrides?: Partial<NonNullable<NonNullable<OpenClawConfig["channels"]>["telegram"]>>,
): OpenClawConfig {
return {
channels: {
telegram: {
botToken: "tok",
...channelOverrides,
execApprovals,
},
},
} as OpenClawConfig;
}
function telegramAccount(
accountId: string,
execApprovals: TelegramExecApprovalConfig,
overrides: Partial<TelegramAccountConfig> = {},
): TelegramAccountConfig {
return {
botToken: `tok-${accountId}`,
...overrides,
execApprovals,
};
}
function buildMultiAccountTelegramConfig(params: {
sessionStorePath?: string;
defaultExecApprovals?: TelegramExecApprovalConfig;
opsExecApprovals?: TelegramExecApprovalConfig;
defaultOverrides?: Partial<TelegramAccountConfig>;
opsOverrides?: Partial<TelegramAccountConfig>;
}): OpenClawConfig {
return {
...(params.sessionStorePath ? { session: { store: params.sessionStorePath } } : {}),
channels: {
telegram: {
accounts: {
default: telegramAccount(
"default",
params.defaultExecApprovals ?? { enabled: true, approvers: ["123"] },
params.defaultOverrides,
),
ops: telegramAccount(
"ops",
params.opsExecApprovals ?? { enabled: true, approvers: ["123"] },
params.opsOverrides,
),
},
},
},
} as OpenClawConfig;
}
function makeChannelApprovalRequest(params: {
id: string;
sessionKey?: string;
turnSourceChannel?: string;
}): TelegramExecApprovalRequest {
return {
id: params.id,
request: {
command: "echo hi",
sessionKey: params.sessionKey ?? "agent:ops:missing",
turnSourceChannel: params.turnSourceChannel ?? "slack",
turnSourceTo: "channel:C123",
},
createdAtMs: 0,
expiresAtMs: 1000,
};
}
describe("telegram exec approvals", () => {
it("auto-enables when approvers resolve unless explicitly disabled", () => {
expect(isTelegramExecApprovalClientEnabled({ cfg: buildConfig() })).toBe(false);
expect(
isTelegramExecApprovalClientEnabled({
cfg: buildConfig({ enabled: true }),
}),
).toBe(false);
expect(
isTelegramExecApprovalClientEnabled({
cfg: buildConfig(undefined, { allowFrom: ["123"] }),
}),
).toBe(false);
expect(
isTelegramExecApprovalClientEnabled({
cfg: buildConfig(undefined, { defaultTo: 123 }),
}),
).toBe(false);
expect(
isTelegramExecApprovalClientEnabled({
cfg: buildConfig({ approvers: ["123"] }),
}),
).toBe(true);
expect(
isTelegramExecApprovalClientEnabled({
cfg: buildConfig({ enabled: "auto", approvers: ["123"] }),
}),
).toBe(true);
expect(
isTelegramExecApprovalClientEnabled({
cfg: buildConfig({ enabled: false, approvers: ["123"] }),
}),
).toBe(false);
});
it("matches approvers by normalized sender id", () => {
const cfg = buildConfig({ approvers: [123, "456"] });
expect(isTelegramExecApprovalApprover({ cfg, senderId: "123" })).toBe(true);
expect(isTelegramExecApprovalApprover({ cfg, senderId: "456" })).toBe(true);
expect(isTelegramExecApprovalApprover({ cfg, senderId: "789" })).toBe(false);
});
it("infers approvers from command owners", () => {
const cfg = {
...buildConfig(),
commands: {
ownerAllowFrom: ["telegram:12345", "tg:67890", "discord:ignored", "-100999"],
},
} as OpenClawConfig;
expect(getTelegramExecApprovalApprovers({ cfg })).toEqual(["12345", "67890"]);
expect(isTelegramExecApprovalClientEnabled({ cfg })).toBe(true);
expect(isTelegramExecApprovalApprover({ cfg, senderId: "12345" })).toBe(true);
expect(isTelegramExecApprovalApprover({ cfg, senderId: "67890" })).toBe(true);
expect(
shouldHandleTelegramExecApprovalRequest({
cfg,
request: makeChannelApprovalRequest({
id: "command-owner-inference",
turnSourceChannel: "telegram",
}),
}),
).toBe(true);
});
it("does not infer approvers from Telegram chat allowlists", () => {
const cfg = buildConfig(
{ enabled: true },
{
allowFrom: ["12345", "-100999", "@ignored"],
defaultTo: 67890,
},
);
expect(getTelegramExecApprovalApprovers({ cfg })).toStrictEqual([]);
expect(isTelegramExecApprovalClientEnabled({ cfg })).toBe(false);
expect(isTelegramExecApprovalApprover({ cfg, senderId: "12345" })).toBe(false);
expect(isTelegramExecApprovalApprover({ cfg, senderId: "67890" })).toBe(false);
});
it("defaults target to dm", () => {
expect(
resolveTelegramExecApprovalTarget({ cfg: buildConfig({ enabled: true, approvers: ["1"] }) }),
).toBe("dm");
});
it("matches agent filters from the Telegram session key when request.agentId is absent", () => {
const cfg = buildConfig({
enabled: true,
approvers: ["123"],
agentFilter: ["ops"],
});
expect(
shouldHandleTelegramExecApprovalRequest({
cfg,
request: {
id: "req-1",
request: {
command: "echo hi",
sessionKey: "agent:ops:telegram:direct:123:tail",
},
createdAtMs: 0,
expiresAtMs: 1000,
},
}),
).toBe(true);
});
it("scopes non-telegram turn sources to the stored telegram account", async () => {
const workspace = tempWorkspaceSync({
rootDir: resolvePreferredOpenClawTmpDir(),
prefix: "openclaw-telegram-exec-approvals-",
});
tempWorkspaces.push(workspace);
const tmpDir = workspace.dir;
const storePath = path.join(tmpDir, "sessions.json");
await upsertSessionEntry({
storePath,
sessionKey: "agent:ops:telegram:direct:123",
entry: {
sessionId: "main",
updatedAt: 1,
delivery: normalizeSessionDeliveryState({
context: { channel: "telegram", accountId: "ops" },
origin: { provider: "telegram", accountId: "ops" },
}),
},
});
const cfg = buildMultiAccountTelegramConfig({ sessionStorePath: storePath });
const request = makeChannelApprovalRequest({
id: "req-2",
sessionKey: "agent:ops:telegram:direct:123",
});
expect(
shouldHandleTelegramExecApprovalRequest({
cfg,
accountId: "default",
request,
}),
).toBe(false);
expect(
shouldHandleTelegramExecApprovalRequest({
cfg,
accountId: "ops",
request,
}),
).toBe(true);
});
it("reports each eligible same-channel account as a raw route candidate", () => {
const cfg = buildMultiAccountTelegramConfig({});
const request: TelegramExecApprovalRequest = {
id: "req-same-channel-unbound",
request: {
command: "echo hi",
turnSourceChannel: "telegram",
sessionKey: "agent:ops:missing",
},
createdAtMs: 0,
expiresAtMs: 1000,
};
expect(shouldHandleTelegramExecApprovalRequest({ cfg, accountId: "default", request })).toBe(
true,
);
expect(shouldHandleTelegramExecApprovalRequest({ cfg, accountId: "ops", request })).toBe(true);
});
it("uses request filters when checking unbound telegram account eligibility", () => {
const cfg = buildMultiAccountTelegramConfig({
defaultExecApprovals: {
enabled: true,
approvers: ["123"],
agentFilter: ["ops"],
},
opsExecApprovals: {
enabled: true,
approvers: ["123"],
agentFilter: ["other"],
},
});
const request = makeChannelApprovalRequest({
id: "req-5",
turnSourceChannel: "telegram",
});
expect(
shouldHandleTelegramExecApprovalRequest({
cfg,
accountId: "default",
request,
}),
).toBe(true);
expect(
shouldHandleTelegramExecApprovalRequest({
cfg,
accountId: "ops",
request,
}),
).toBe(false);
});
it("scopes native exec approval handling to configured target accountIds", () => {
const cfg = {
...buildMultiAccountTelegramConfig({}),
approvals: {
exec: {
enabled: true,
mode: "targets",
targets: [{ channel: "telegram", to: "123", accountId: "ops" }],
},
},
} as OpenClawConfig;
const request: TelegramExecApprovalRequest = {
id: "req-target-account",
request: {
command: "echo hi",
sessionKey: "agent:ops:main",
},
createdAtMs: 0,
expiresAtMs: 1000,
};
expect(
shouldHandleTelegramExecApprovalRequest({
cfg,
accountId: "default",
request,
}),
).toBe(false);
expect(
shouldHandleTelegramExecApprovalRequest({
cfg,
accountId: "ops",
request,
}),
).toBe(true);
});
it("preserves unscoped telegram targets when mixed with scoped target accountIds", () => {
const baseCfg = buildMultiAccountTelegramConfig({});
const cfg = {
...baseCfg,
channels: {
telegram: {
...baseCfg.channels?.telegram,
accounts: {
...baseCfg.channels?.telegram?.accounts,
other: telegramAccount("other", { enabled: true, approvers: ["123"] }),
},
},
},
approvals: {
exec: {
enabled: true,
mode: "targets",
targets: [
{ channel: "telegram", to: "123" },
{ channel: "telegram", to: "456", accountId: "ops" },
],
},
},
} as OpenClawConfig;
const request: TelegramExecApprovalRequest = {
id: "req-mixed-target-account",
request: {
command: "echo hi",
sessionKey: "agent:ops:main",
},
createdAtMs: 0,
expiresAtMs: 1000,
};
expect(
shouldHandleTelegramExecApprovalRequest({
cfg,
accountId: "default",
request,
}),
).toBe(true);
expect(
shouldHandleTelegramExecApprovalRequest({
cfg,
accountId: "ops",
request,
}),
).toBe(true);
expect(
shouldHandleTelegramExecApprovalRequest({
cfg,
accountId: "other",
request,
}),
).toBe(false);
});
it("ignores disabled telegram accounts when checking unbound account eligibility", () => {
const cfg = buildMultiAccountTelegramConfig({ opsOverrides: { enabled: false } });
const request = makeChannelApprovalRequest({
id: "req-6",
turnSourceChannel: "telegram",
});
expect(
shouldHandleTelegramExecApprovalRequest({
cfg,
accountId: "default",
request,
}),
).toBe(true);
expect(
shouldHandleTelegramExecApprovalRequest({
cfg,
accountId: "ops",
request,
}),
).toBe(false);
});
it("only injects approval buttons on eligible telegram targets", () => {
const dmCfg = buildConfig({ enabled: true, approvers: ["123"], target: "dm" });
const channelCfg = buildConfig({ enabled: true, approvers: ["123"], target: "channel" });
const bothCfg = buildConfig({ enabled: true, approvers: ["123"], target: "both" });
expect(shouldInjectTelegramExecApprovalButtons({ cfg: dmCfg, to: "123" })).toBe(true);
expect(shouldInjectTelegramExecApprovalButtons({ cfg: dmCfg, to: "-100123" })).toBe(false);
expect(shouldInjectTelegramExecApprovalButtons({ cfg: channelCfg, to: "-100123" })).toBe(true);
expect(shouldInjectTelegramExecApprovalButtons({ cfg: channelCfg, to: "123" })).toBe(false);
expect(shouldInjectTelegramExecApprovalButtons({ cfg: bothCfg, to: "123" })).toBe(true);
expect(shouldInjectTelegramExecApprovalButtons({ cfg: bothCfg, to: "-100123" })).toBe(true);
});
describe("isTelegramExecApprovalTargetRecipient", () => {
function buildTargetConfig(
targets: Array<{ channel: string; to: string; accountId?: string }>,
): OpenClawConfig {
return {
channels: { telegram: { botToken: "tok" } },
approvals: { exec: { enabled: true, mode: "targets", targets } },
} as OpenClawConfig;
}
it("accepts sender who is a DM target", () => {
const cfg = buildTargetConfig([{ channel: "telegram", to: "12345" }]);
expect(isTelegramExecApprovalTargetRecipient({ cfg, senderId: "12345" })).toBe(true);
});
it("rejects sender not in any target", () => {
const cfg = buildTargetConfig([{ channel: "telegram", to: "12345" }]);
expect(isTelegramExecApprovalTargetRecipient({ cfg, senderId: "99999" })).toBe(false);
});
it("rejects group targets", () => {
const cfg = buildTargetConfig([{ channel: "telegram", to: "-100123456" }]);
expect(isTelegramExecApprovalTargetRecipient({ cfg, senderId: "123456" })).toBe(false);
});
it("ignores non-telegram targets", () => {
const cfg = buildTargetConfig([{ channel: "discord", to: "12345" }]);
expect(isTelegramExecApprovalTargetRecipient({ cfg, senderId: "12345" })).toBe(false);
});
it("returns false when no targets configured", () => {
const cfg = buildConfig();
expect(isTelegramExecApprovalTargetRecipient({ cfg, senderId: "12345" })).toBe(false);
});
it("returns false when senderId is empty or null", () => {
const cfg = buildTargetConfig([{ channel: "telegram", to: "12345" }]);
expect(isTelegramExecApprovalTargetRecipient({ cfg, senderId: "" })).toBe(false);
expect(isTelegramExecApprovalTargetRecipient({ cfg, senderId: null })).toBe(false);
expect(isTelegramExecApprovalTargetRecipient({ cfg })).toBe(false);
});
it("matches across multiple targets", () => {
const cfg = buildTargetConfig([
{ channel: "slack", to: "U12345" },
{ channel: "telegram", to: "67890" },
{ channel: "telegram", to: "11111" },
]);
expect(isTelegramExecApprovalTargetRecipient({ cfg, senderId: "67890" })).toBe(true);
expect(isTelegramExecApprovalTargetRecipient({ cfg, senderId: "11111" })).toBe(true);
expect(isTelegramExecApprovalTargetRecipient({ cfg, senderId: "U12345" })).toBe(false);
});
it("scopes by accountId in multi-bot deployments", () => {
const cfg = buildTargetConfig([
{ channel: "telegram", to: "12345", accountId: "account-a" },
{ channel: "telegram", to: "67890", accountId: "account-b" },
]);
expect(
isTelegramExecApprovalTargetRecipient({ cfg, senderId: "12345", accountId: "account-a" }),
).toBe(true);
expect(
isTelegramExecApprovalTargetRecipient({ cfg, senderId: "12345", accountId: "account-b" }),
).toBe(false);
expect(isTelegramExecApprovalTargetRecipient({ cfg, senderId: "12345" })).toBe(true);
});
it("allows unscoped targets regardless of callback accountId", () => {
const cfg = buildTargetConfig([{ channel: "telegram", to: "12345" }]);
expect(
isTelegramExecApprovalTargetRecipient({ cfg, senderId: "12345", accountId: "any-account" }),
).toBe(true);
});
it("requires active target forwarding mode", () => {
const cfg = {
channels: { telegram: { botToken: "tok" } },
approvals: {
exec: {
enabled: true,
mode: "session",
targets: [{ channel: "telegram", to: "12345" }],
},
},
} as OpenClawConfig;
expect(isTelegramExecApprovalTargetRecipient({ cfg, senderId: "12345" })).toBe(false);
});
it("normalizes prefixed Telegram DM targets", () => {
const cfg = buildTargetConfig([{ channel: "telegram", to: "tg:12345" }]);
expect(isTelegramExecApprovalTargetRecipient({ cfg, senderId: "12345" })).toBe(true);
});
it("normalizes accountId matching", () => {
const cfg = buildTargetConfig([{ channel: "telegram", to: "12345", accountId: "Work Bot" }]);
expect(
isTelegramExecApprovalTargetRecipient({ cfg, senderId: "12345", accountId: "work-bot" }),
).toBe(true);
});
});
describe("isTelegramExecApprovalAuthorizedSender", () => {
it("accepts explicit approvers", () => {
const cfg = buildConfig({ enabled: true, approvers: ["123"] });
expect(isTelegramExecApprovalAuthorizedSender({ cfg, senderId: "123" })).toBe(true);
});
it("accepts explicit approvers even when the richer client is disabled", () => {
const cfg = buildConfig({ enabled: false, approvers: ["123"] });
expect(isTelegramExecApprovalAuthorizedSender({ cfg, senderId: "123" })).toBe(true);
});
it("accepts active forwarded DM targets", () => {
const cfg = {
channels: { telegram: { botToken: "tok" } },
approvals: {
exec: {
enabled: true,
mode: "targets",
targets: [{ channel: "telegram", to: "12345" }],
},
},
} as OpenClawConfig;
expect(isTelegramExecApprovalAuthorizedSender({ cfg, senderId: "12345" })).toBe(true);
});
});
});