openclaw/src/cli/program.test.ts
Arne Moor 69608fd305 feat: add telegram provider with CLI integration
Add Telegram as a third messaging provider alongside web and twilio.

Core Features:
- Interactive login flow with phone/SMS/2FA authentication
- Send text and media messages (images, videos, audio, documents)
- Monitor incoming messages with auto-reply support
- Session management at ~/.clawdis/telegram/session/
- Full CLI integration (login, logout, status, send, relay commands)

Implementation Details:
- Uses telegram npm package for MTProto API access
- Supports both URL and local file media sending
- Cross-platform path handling (Windows/Unix)
- Optional Twilio env vars (supports Telegram-only usage)
- Minimal provider abstraction pattern
- Comprehensive test coverage (440 tests passing)

Changes:
- Add Telegram module (client, login, monitor, inbound, outbound, session)
- Add provider factory and base interfaces
- Wire Telegram functions into CLI deps
- Update env validation to make Twilio fields optional
- Add telegram to all CLI commands (login, logout, status, send, relay)
- Add null checks in Twilio code for optional env fields
- Fix send command to properly load session and connect
- Add local file support with cross-platform path handling
- Update login message to show correct ~/.clawdis path
- Add comprehensive tests and documentation

Basic Usage:
  warelay login --provider telegram
  warelay send --provider telegram --to "@user" --message "Hi"
  warelay send --provider telegram --to "@user" --media "/path/to/file.jpg"
  warelay relay --provider telegram

All tests pass (63 files, 440 tests). Zero TypeScript errors.
2025-12-05 18:59:38 +01:00

130 lines
3.9 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
const sendCommand = vi.fn();
const statusCommand = vi.fn();
const webhookCommand = vi.fn().mockResolvedValue(undefined);
const ensureTwilioEnv = vi.fn();
const loginWeb = vi.fn();
const monitorWebProvider = vi.fn();
const pickProvider = vi.fn();
const monitorTwilio = vi.fn();
const logTwilioFrom = vi.fn();
const logWebSelfId = vi.fn();
const waitForever = vi.fn();
const spawnRelayTmux = vi.fn().mockResolvedValue("warelay-relay");
const runtime = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(() => {
throw new Error("exit");
}),
};
vi.mock("../commands/send.js", () => ({ sendCommand }));
vi.mock("../commands/status.js", () => ({ statusCommand }));
vi.mock("../commands/webhook.js", () => ({ webhookCommand }));
vi.mock("../env.js", () => ({ ensureTwilioEnv }));
vi.mock("../runtime.js", () => ({ defaultRuntime: runtime }));
vi.mock("../provider-web.js", () => ({
loginWeb,
monitorWebProvider,
pickProvider,
}));
vi.mock("./deps.js", () => ({
createDefaultDeps: () => ({ waitForever }),
logTwilioFrom,
logWebSelfId,
monitorTwilio,
}));
vi.mock("./relay_tmux.js", () => ({ spawnRelayTmux }));
const { buildProgram } = await import("./program.js");
describe("cli program", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("runs send with required options", async () => {
const program = buildProgram();
await program.parseAsync(["send", "--to", "+1", "--message", "hi"], {
from: "user",
});
expect(sendCommand).toHaveBeenCalled();
});
it("rejects invalid relay provider", async () => {
const program = buildProgram();
await expect(
program.parseAsync(["relay", "--provider", "bogus"], { from: "user" }),
).rejects.toThrow("exit");
expect(runtime.error).toHaveBeenCalledWith(
"--provider must be auto, web, twilio, or telegram",
);
});
it("falls back to twilio when web relay fails", async () => {
pickProvider.mockResolvedValue("web");
monitorWebProvider.mockRejectedValue(new Error("no web"));
const program = buildProgram();
await expect(
program.parseAsync(
["relay", "--provider", "auto", "--interval", "2", "--lookback", "1"],
{ from: "user" },
),
).rejects.toThrow("exit");
expect(logWebSelfId).toHaveBeenCalled();
expect(ensureTwilioEnv).not.toHaveBeenCalled();
expect(monitorTwilio).not.toHaveBeenCalled();
});
it("runs relay tmux attach command", async () => {
const originalIsTTY = process.stdout.isTTY;
(process.stdout as typeof process.stdout & { isTTY?: boolean }).isTTY =
true;
const program = buildProgram();
await program.parseAsync(["relay:tmux:attach"], { from: "user" });
expect(spawnRelayTmux).toHaveBeenCalledWith(
"pnpm warelay relay --verbose",
true,
false,
);
(process.stdout as typeof process.stdout & { isTTY?: boolean }).isTTY =
originalIsTTY;
});
it("runs relay heartbeat command", async () => {
pickProvider.mockResolvedValue("web");
monitorWebProvider.mockResolvedValue(undefined);
const originalExit = runtime.exit;
runtime.exit = vi.fn();
const program = buildProgram();
await program.parseAsync(["relay:heartbeat"], { from: "user" });
expect(logWebSelfId).toHaveBeenCalled();
expect(monitorWebProvider).toHaveBeenCalledWith(
false,
undefined,
true,
undefined,
runtime,
undefined,
{ replyHeartbeatNow: true },
);
expect(runtime.exit).not.toHaveBeenCalled();
runtime.exit = originalExit;
});
it("runs relay heartbeat tmux helper", async () => {
const program = buildProgram();
await program.parseAsync(["relay:heartbeat:tmux"], { from: "user" });
const shouldAttach = Boolean(process.stdout.isTTY);
expect(spawnRelayTmux).toHaveBeenCalledWith(
"pnpm warelay relay --verbose --heartbeat-now",
shouldAttach,
);
});
});