test(e2e): add black-box telegram rtt driver

This commit is contained in:
Ayaan Zaidi
2026-05-01 13:54:47 +05:30
parent 111432a7a6
commit 494eb01ac8
2 changed files with 350 additions and 0 deletions

View File

@@ -0,0 +1,112 @@
#!/usr/bin/env node
import fs from "node:fs";
const [configPath, mockPort, groupId, driverToken, sutToken, packageVersion] =
process.argv.slice(2);
if (!configPath || !mockPort || !groupId || !driverToken || !sutToken || !packageVersion) {
throw new Error(
"usage: npm-telegram-rtt-config.mjs <config> <mock-port> <group-id> <driver-token> <sut-token> <package-version>",
);
}
const driverId = driverToken.split(":", 1)[0];
const config = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
function supportsVisibleReplies(version) {
const match = /(\d{4})\.(\d+)\.(\d+)/u.exec(version);
if (!match) {
return false;
}
const [, year, month, day] = match.map(Number);
return year > 2026 || (year === 2026 && (month > 4 || (month === 4 && day >= 27)));
}
config.gateway = {
mode: "local",
port: 18789,
bind: "loopback",
auth: { mode: "none" },
};
config.models = config.models ?? {};
config.models.providers = config.models.providers ?? {};
config.models.providers.openai = {
api: "openai-responses",
apiKey: {
source: "env",
provider: "default",
id: "OPENAI_API_KEY",
},
baseUrl: `http://127.0.0.1:${mockPort}/v1`,
models: [
{
id: "gpt-5.5",
name: "gpt-5.5",
api: "openai-responses",
contextWindow: 128000,
},
],
};
config.agents = config.agents ?? {};
config.agents.defaults = config.agents.defaults ?? {};
config.agents.defaults.model = { primary: "openai/gpt-5.5" };
config.agents.defaults.models = {
"openai/gpt-5.5": {
params: {
transport: "sse",
openaiWsWarmup: false,
},
},
};
config.agents.list = [
{
id: "main",
default: true,
name: "Main",
workspace: "~/workspace",
model: { primary: "openai/gpt-5.5" },
},
];
config.plugins = config.plugins ?? {};
config.plugins.enabled = true;
config.plugins.allow = ["telegram", "openai"];
config.plugins.entries = {
telegram: { enabled: true },
openai: { enabled: true },
};
config.channels = config.channels ?? {};
config.channels.telegram = {
enabled: true,
botToken: {
source: "env",
provider: "default",
id: "TELEGRAM_BOT_TOKEN",
},
dmPolicy: "allowlist",
allowFrom: [driverId],
defaultTo: driverId,
groupPolicy: "allowlist",
groupAllowFrom: [driverId],
groups: {
[groupId]: {
requireMention: false,
allowFrom: [driverId],
},
},
};
if (supportsVisibleReplies(packageVersion)) {
config.messages = {
...config.messages,
groupChat: {
...config.messages?.groupChat,
visibleReplies: "automatic",
},
};
}
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);

View File

@@ -0,0 +1,238 @@
#!/usr/bin/env node
import fs from "node:fs/promises";
import path from "node:path";
const groupId = process.env.OPENCLAW_QA_TELEGRAM_GROUP_ID;
const driverToken = process.env.OPENCLAW_QA_TELEGRAM_DRIVER_BOT_TOKEN;
const sutToken = process.env.OPENCLAW_QA_TELEGRAM_SUT_BOT_TOKEN;
const outputDir = process.env.OPENCLAW_NPM_TELEGRAM_OUTPUT_DIR ?? ".artifacts/rtt/raw";
const timeoutMs = Number(process.env.OPENCLAW_QA_TELEGRAM_SCENARIO_TIMEOUT_MS ?? "180000");
const canaryTimeoutMs = Number(
process.env.OPENCLAW_QA_TELEGRAM_CANARY_TIMEOUT_MS ?? String(timeoutMs),
);
const scenarioIds = (
process.env.OPENCLAW_NPM_TELEGRAM_SCENARIOS ?? "telegram-mentioned-message-reply"
)
.split(",")
.map((value) => value.trim())
.filter(Boolean);
if (!groupId || !driverToken || !sutToken) {
throw new Error(
"missing Telegram env: OPENCLAW_QA_TELEGRAM_GROUP_ID, OPENCLAW_QA_TELEGRAM_DRIVER_BOT_TOKEN, OPENCLAW_QA_TELEGRAM_SUT_BOT_TOKEN",
);
}
class TelegramBot {
constructor(token) {
this.baseUrl = `https://api.telegram.org/bot${token}`;
}
async call(method, body) {
const response = await fetch(`${this.baseUrl}/${method}`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
const payload = await response.json();
if (!response.ok || payload.ok !== true) {
throw new Error(`${method} failed: ${JSON.stringify(payload)}`);
}
return payload.result;
}
getMe() {
return this.call("getMe", {});
}
sendMessage(params) {
return this.call("sendMessage", params);
}
getUpdates(params) {
return this.call("getUpdates", params);
}
}
const driver = new TelegramBot(driverToken);
const sut = new TelegramBot(sutToken);
const observedMessages = [];
let driverUpdateOffset = 0;
function messageText(message) {
return message.text ?? message.caption ?? "";
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function flushUpdates(bot) {
let updates = await bot.getUpdates({ timeout: 0, allowed_updates: ["message"] });
let nextOffset;
while (updates.length > 0) {
const lastUpdateId = updates.at(-1).update_id;
nextOffset = lastUpdateId + 1;
updates = await bot.getUpdates({
offset: nextOffset,
timeout: 0,
allowed_updates: ["message"],
});
}
return nextOffset;
}
async function waitForSutReply(params) {
const deadline = Date.now() + params.timeoutMs;
while (Date.now() < deadline) {
const updates = await driver.getUpdates({
offset: driverUpdateOffset,
timeout: 5,
allowed_updates: ["message"],
});
for (const update of updates) {
driverUpdateOffset = Math.max(driverUpdateOffset, update.update_id + 1);
const message = update.message;
if (!message || String(message.chat?.id) !== String(groupId)) {
continue;
}
observedMessages.push({
updateId: update.update_id,
messageId: message.message_id,
fromId: message.from?.id,
fromUsername: message.from?.username,
replyToMessageId: message.reply_to_message?.message_id,
text: messageText(message),
scenarioId: params.scenarioId,
scenarioTitle: params.scenarioTitle,
});
if (message.from?.id !== params.sutId) {
continue;
}
if (message.date < params.startedUnixSeconds) {
continue;
}
const text = messageText(message);
const replyMatches = message.reply_to_message?.message_id === params.requestMessageId;
const markerMatches = params.matchText ? text.includes(params.matchText) : false;
const anySutReplyMatches = params.allowAnySutReply;
if (replyMatches || markerMatches || anySutReplyMatches) {
return message;
}
}
}
throw new Error(`timed out after ${params.timeoutMs}ms waiting for Telegram message`);
}
async function runScenario(params) {
const startedAt = new Date();
const startedUnixSeconds = Math.floor(startedAt.getTime() / 1000);
const request = await driver.sendMessage({
chat_id: groupId,
text: params.input,
disable_notification: true,
});
try {
const reply = await waitForSutReply({
allowAnySutReply: params.allowAnySutReply,
matchText: params.matchText,
requestMessageId: request.message_id,
scenarioId: params.id,
scenarioTitle: params.title,
startedUnixSeconds,
sutId: params.sutId,
timeoutMs: params.timeoutMs,
});
const rttMs = Date.now() - startedAt.getTime();
return {
id: params.id,
title: params.title,
status: "pass",
details: `observed SUT message ${reply.message_id}`,
rttMs,
};
} catch (error) {
return {
id: params.id,
title: params.title,
status: "fail",
details: error instanceof Error ? error.message : String(error),
};
}
}
function reportMarkdown(summary) {
const lines = ["# Telegram RTT", ""];
for (const scenario of summary.scenarios) {
lines.push(`## ${scenario.title}`, "");
lines.push(`- Status: ${scenario.status}`);
lines.push(`- Details: ${scenario.details}`);
if (scenario.rttMs !== undefined) {
lines.push(`- RTT: ${scenario.rttMs}ms`);
}
lines.push("");
}
return lines.join("\n");
}
async function main() {
await fs.mkdir(outputDir, { recursive: true });
const [driverMe, sutMe] = await Promise.all([driver.getMe(), sut.getMe()]);
driverUpdateOffset = (await flushUpdates(driver)) ?? driverUpdateOffset;
const scenarios = [];
scenarios.push(
await runScenario({
allowAnySutReply: true,
id: "telegram-canary",
input: `/status@${sutMe.username}`,
sutId: sutMe.id,
timeoutMs: canaryTimeoutMs,
title: "Telegram canary",
}),
);
if (scenarioIds.includes("telegram-mentioned-message-reply")) {
const marker = `OPENCLAW_RTT_${Date.now().toString(36)}`;
scenarios.push(
await runScenario({
allowAnySutReply: true,
id: "telegram-mentioned-message-reply",
input: `/status@${sutMe.username} RTT marker ${marker}`,
matchText: "OPENCLAW_RTT_OK",
sutId: sutMe.id,
timeoutMs,
title: "Telegram status command reply",
}),
);
}
const failed = scenarios.filter((scenario) => scenario.status === "fail").length;
const summary = {
provider: "telegram",
driver: { id: driverMe.id, username: driverMe.username },
sut: { id: sutMe.id, username: sutMe.username },
startedAt: new Date().toISOString(),
status: failed > 0 ? "fail" : "pass",
totals: { total: scenarios.length, failed, passed: scenarios.length - failed },
scenarios,
};
await fs.writeFile(
path.join(outputDir, "telegram-qa-summary.json"),
`${JSON.stringify(summary, null, 2)}\n`,
);
await fs.writeFile(path.join(outputDir, "telegram-qa-report.md"), reportMarkdown(summary));
await fs.writeFile(
path.join(outputDir, "telegram-qa-observed-messages.json"),
`${JSON.stringify(observedMessages, null, 2)}\n`,
);
if (failed > 0) {
process.exitCode = 1;
}
}
await main();