mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-22 18:35:21 -06:00
fc5265d685
* 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
577 lines
18 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|