Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions .github/workflows/telegram-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: telegram-e2e

# Runs Telethon-based E2E tests against the Zeph Telegram channel on Test DC.
# Gated by repository secrets — skipped automatically when secrets are absent.
# Runs only on push to main (not on PRs) to avoid flakiness from external services.

on:
push:
branches: [main]

jobs:
telegram-e2e:
name: Telegram E2E (Test DC)
runs-on: ubuntu-latest
# Skip if the required secrets are not configured in the repository
if: >
secrets.ZEPH_TELEGRAM_TEST_TOKEN != '' &&
secrets.TELEGRAM_TEST_API_ID != '' &&
secrets.TELEGRAM_TEST_API_HASH != '' &&
secrets.TELEGRAM_TEST_SESSION != '' &&
secrets.TELEGRAM_TEST_BOT_USERNAME != ''
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip

- name: Install Telethon
run: pip install -r scripts/telegram-e2e/requirements.txt

- name: Restore Telethon session
# TELEGRAM_TEST_SESSION is the base64-encoded content of test_session.session
run: |
mkdir -p .local/testing
echo "${{ secrets.TELEGRAM_TEST_SESSION }}" | base64 -d > .local/testing/test_session.session

- name: Build Zeph (telegram feature)
run: cargo build --features full --bin zeph

- name: Write Zeph config
env:
ZEPH_TELEGRAM_TEST_TOKEN: ${{ secrets.ZEPH_TELEGRAM_TEST_TOKEN }}
run: |
mkdir -p .local/config .local/testing/data .local/testing/debug
cp config/telegram-test.toml .local/config/telegram-test.toml
# Patch allowed_users and use env-backend token for CI (no vault available)
sed -i 's|token = { vault = "ZEPH_TELEGRAM_TEST_TOKEN" }|token = { env = "ZEPH_TELEGRAM_TEST_TOKEN" }|' \
.local/config/telegram-test.toml
sed -i 's|allowed_users = \["your_test_username"\]|allowed_users = ["${{ secrets.TELEGRAM_TEST_ACCOUNT_USERNAME }}"]|' \
.local/config/telegram-test.toml
sed -i 's|backend = "age"|backend = "env"|' \
.local/config/telegram-test.toml

- name: Start Zeph (background)
env:
ZEPH_TELEGRAM_TEST_TOKEN: ${{ secrets.ZEPH_TELEGRAM_TEST_TOKEN }}
run: |
./target/debug/zeph --config .local/config/telegram-test.toml --channel telegram \
2>.local/testing/debug/telegram-session.log &
# Give the bot 8s to connect and start long-polling
sleep 8

- name: Run E2E scenarios
env:
TG_API_ID: ${{ secrets.TELEGRAM_TEST_API_ID }}
TG_API_HASH: ${{ secrets.TELEGRAM_TEST_API_HASH }}
TG_BOT_USERNAME: ${{ secrets.TELEGRAM_TEST_BOT_USERNAME }}
TG_SESSION_PATH: .local/testing/test_session.session
run: python3 scripts/telegram-e2e/telegram_e2e.py

- name: Upload debug log on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: telegram-session-log
path: .local/testing/debug/telegram-session.log
retention-days: 7
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ data/
book/book/
*.log
zeph.log
*.session
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

- test(channels): add injectable test transport to `TelegramChannel` (#2121) — `new_test()` constructor under `#[cfg(test)]` exposes an `mpsc::Sender<IncomingMessage>` so all channel behavioral paths can be tested without a real bot token or live Telegram API; 12 new tests cover `recv()` message delivery, `/reset` and `/skills` command routing, unknown-command passthrough, channel-close returning `None`, text accumulation in `send_chunk()`, `flush_chunks()` state clearing, the `/start` welcome path via wiremock, `flush_chunks()` with `message_id` via wiremock, and `confirm()` timeout/close/yes/no logic at the rx-timeout level; adds `wiremock` and tokio `test-util` to dev-dependencies
- test(tools): add integration tests for `FileExecutor` sandbox access controls (#2117) — 15 tests in `crates/zeph-tools/tests/file_access.rs` covering read/write inside sandbox, sandbox violation on outside paths, symlink escape (single and chained, unix-only), path traversal blocking, multiple allowed paths, empty allowed-paths CWD fallback, tilde regression (#2115), delete/move/copy cross-boundary blocking, `find_path` result filtering to sandbox, `grep` default-path sandbox validation, and nonexistent allowed path resilience
- test(channels): add Telethon-based E2E test suite for Telegram channel using Telegram Test DC (#2122) — `scripts/telegram-e2e/telegram_e2e.py` connects as a MTProto user account, sends 8 scripted scenarios to the Zeph bot, and asserts on replies: `/start` welcome message, math (347×89), `/reset`, `/skills` (MarkdownV2 escaping), empty document (no reply), long output (≥2 split messages), streaming (first chunk latency), and unauthorized-user silence; `setup_tg_test_account.py` creates a persistent Test DC session (phone `+99966XXXXX`, no real SIM required); `config/telegram-test.toml` template for Test DC bot; optional `.github/workflows/telegram-e2e.yml` gated by `ZEPH_TELEGRAM_TEST_TOKEN`/`TELEGRAM_TEST_SESSION` secrets, runs on push to `main` only; `*.session` added to `.gitignore`
- test(cost): add unit test for `max_daily_cents = 0.0` unlimited budget behavior — `CostTracker::check_budget()` must return `Ok(())` regardless of spend when the daily limit is zero (#2110)
- chore(testing): add canonical `config/testing.toml` with `provider = "router"` to enable RAPS/reputation scoring in CI sessions (#2104) — previously `.local/config/testing.toml` used `provider = "openai"` which silently ignored `[llm.router]` and `[llm.router.reputation]`; the new tracked reference config uses `provider = "router"` with `chain = ["openai"]` keeping identical LLM behavior while activating RAPS; copy to `.local/config/testing.toml` before use
- test(memory): add unit tests for `SqliteStore` tier DB methods (#2094) — covers `fetch_tiers`, `count_messages_by_tier`, `find_promotion_candidates`, `manual_promote`, `promote_to_semantic`, and migration 042 schema defaults; 29 new tests across happy path, edge cases (empty input, already-promoted rows, soft-deleted rows, nonexistent IDs), and idempotency invariants
Expand Down
71 changes: 71 additions & 0 deletions config/telegram-test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Zeph configuration for Telegram Test DC E2E testing.
# This template uses placeholder values — copy and fill before use:
#
# cp config/telegram-test.toml .local/config/telegram-test.toml
#
# Then store the bot token in the vault:
# cargo run --features full -- vault set ZEPH_TELEGRAM_TEST_TOKEN '<TOKEN>'
#
# Run Zeph with this config:
# cargo run --features full -- --config .local/config/telegram-test.toml --channel telegram \
# 2>.local/testing/debug/telegram-session.log
#
# Notes:
# - The bot token is obtained from @BotFather on the Telegram Test DC.
# Connect to Test DC in Telegram (test.t.me or via a test-mode client),
# find @BotFather, run /newbot. The token is valid only on Test DC.
# - allowed_users must contain the Telegram username of the Telethon test account
# created with setup_tg_test_account.py.

[agent]
name = "ZephTest"
max_tool_iterations = 10
auto_update_check = false

[llm]
provider = "openai"
base_url = "https://api.openai.com/v1"
model = "gpt-4o-mini"
max_tokens = 4096

[skills]
paths = [".local/testing/skills"]
max_active_skills = 5
prompt_mode = "auto"

[memory]
sqlite_path = ".local/testing/data/zeph-telegram-test.db"
history_limit = 50

[telegram]
# Token from @BotFather on Telegram Test DC (NOT production BotFather)
token = { vault = "ZEPH_TELEGRAM_TEST_TOKEN" }
# Replace with the Telegram username of your test account (created by setup_tg_test_account.py)
allowed_users = ["your_test_username"]

[tools]
enabled = true
summarize_output = true

[tools.shell]
timeout = 30
blocked_commands = []
allowed_commands = []
allowed_paths = []
allow_network = true

[vault]
backend = "age"

[debug]
enabled = true
output_dir = ".local/testing/debug"
format = "raw"

[security]
redact_secrets = true
autonomy_level = "supervised"

[cost]
enabled = true
max_daily_cents = 100
1 change: 1 addition & 0 deletions scripts/telegram-e2e/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
telethon>=1.36
109 changes: 109 additions & 0 deletions scripts/telegram-e2e/setup_tg_test_account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""One-time setup: register a Telegram Test DC user account and save the session.

Run once before using telegram_e2e.py. Produces test_session.session (gitignored).

Usage:
pip install telethon
python3 scripts/telegram-e2e/setup_tg_test_account.py \\
--api-id <API_ID> --api-hash <API_HASH> \\
--phone +99966XXXXX --session .local/testing/test_session.session

Obtaining API credentials:
Visit https://my.telegram.org → Log In → API development tools → Create app.
Use Test DC credentials: set test mode in my.telegram.org before creating the app,
or obtain a separate API_ID/API_HASH for test servers.

Test DC phone numbers:
Any number in +99966XXXXX format works on Test DC (e.g. +9996612345).
The OTP code is always the last 5 digits repeated (e.g. phone +9996612345 → OTP 12345).
No real SIM needed — Test DC is an isolated Telegram server for developers.

Second account (for unauthorized-user scenario):
Run again with a different phone and --session .local/testing/test_session2.session
"""

import argparse
import os
import sys

try:
from telethon.sync import TelegramClient
except ImportError:
print("telethon not installed. Run: pip install telethon", file=sys.stderr)
sys.exit(1)

# Telegram Test DC server address
TEST_DC_HOST = "149.154.167.40"
TEST_DC_PORT = 443


def main() -> None:
parser = argparse.ArgumentParser(
description="Create Telegram Test DC session for E2E testing"
)
parser.add_argument(
"--api-id",
type=int,
default=int(os.environ.get("TG_API_ID", "0")) or None,
required=not os.environ.get("TG_API_ID"),
help="Telegram API ID from my.telegram.org",
)
parser.add_argument(
"--api-hash",
default=os.environ.get("TG_API_HASH"),
required=not os.environ.get("TG_API_HASH"),
help="Telegram API hash from my.telegram.org",
)
parser.add_argument(
"--phone",
required=True,
help="Test DC phone number (+99966XXXXX format)",
)
parser.add_argument(
"--session",
default=os.environ.get("TG_SESSION_PATH", ".local/testing/test_session.session"),
help="Path to save the session file (default: .local/testing/test_session.session)",
)
args = parser.parse_args()

if not args.phone.startswith("+99966"):
print(
"WARNING: Test DC phone numbers use +99966XXXXX format.\n"
"Using a real phone number will connect to production Telegram, not Test DC.",
file=sys.stderr,
)

session_dir = os.path.dirname(args.session)
if session_dir:
os.makedirs(session_dir, exist_ok=True)

# Strip .session suffix — Telethon appends it automatically
session_name = args.session.removesuffix(".session")

print(f"Connecting to Telegram Test DC ({TEST_DC_HOST}:{TEST_DC_PORT})...")
print(f"Session will be saved to: {session_name}.session")

client = TelegramClient(
session_name,
args.api_id,
args.api_hash,
server=(TEST_DC_HOST, TEST_DC_PORT),
)

# start() prompts for OTP interactively
client.start(phone=args.phone)

me = client.get_me()
print(f"\nAuthenticated as: {me.first_name} (@{me.username or 'no username'})")
print(f"Session saved: {session_name}.session")
print("\nNext step: register a bot on Test DC via @BotFather (test server):")
print(" 1. In Telegram, go to @BotFather and /newbot")
print(" 2. Store the token: cargo run --features full -- vault set ZEPH_TELEGRAM_TEST_TOKEN '<TOKEN>'")
print(" 3. Run: python3 scripts/telegram-e2e/telegram_e2e.py --help")

client.disconnect()


if __name__ == "__main__":
main()
Loading
Loading