From cd15ce35a0eef145841867dd78dd24e6f8305c74 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Mon, 18 May 2026 10:04:37 +0530 Subject: [PATCH] fix(qa): keep telegram user creds mantis-only --- docs/concepts/qa-e2e-automation.md | 34 +++------- docs/help/testing.md | 65 +------------------ extensions/qa-lab/runtime-api.ts | 5 -- .../telegram-user-credential.runtime.test.ts | 58 ----------------- .../telegram-user-credential.runtime.ts | 36 ---------- package.json | 1 - scripts/e2e/telegram-user-crabbox-proof.ts | 8 +-- scripts/e2e/telegram-user-credential.ts | 12 ++-- ...is-telegram-desktop-proof-workflow.test.ts | 36 +++++++++- 9 files changed, 57 insertions(+), 198 deletions(-) delete mode 100644 extensions/qa-lab/src/live-transports/telegram/telegram-user-credential.runtime.test.ts delete mode 100644 extensions/qa-lab/src/live-transports/telegram/telegram-user-credential.runtime.ts diff --git a/docs/concepts/qa-e2e-automation.md b/docs/concepts/qa-e2e-automation.md index 06df5bbf8920..f3354c17325c 100644 --- a/docs/concepts/qa-e2e-automation.md +++ b/docs/concepts/qa-e2e-automation.md @@ -594,34 +594,20 @@ Telegram, Discord, Slack, and WhatsApp lanes can lease credentials from a shared Payload shapes the broker validates on `admin/add`: - Telegram (`kind: "telegram"`): `{ groupId: string, driverToken: string, sutToken: string }` - `groupId` must be a numeric chat-id string. -- Telegram real user (`kind: "telegram-user"`): `{ groupId: string, sutToken: string, testerUserId: string, testerUsername: string, telegramApiId: string, telegramApiHash: string, tdlibDatabaseEncryptionKey: string, tdlibArchiveBase64: string, tdlibArchiveSha256: string, desktopTdataArchiveBase64: string, desktopTdataArchiveSha256: string }` - one exclusive burner-account lease used by both the TDLib CLI driver and Telegram Desktop visual witness. +- Telegram real user (`kind: "telegram-user"`): `{ groupId: string, sutToken: string, testerUserId: string, testerUsername: string, telegramApiId: string, telegramApiHash: string, tdlibDatabaseEncryptionKey: string, tdlibArchiveBase64: string, tdlibArchiveSha256: string, desktopTdataArchiveBase64: string, desktopTdataArchiveSha256: string }` - Mantis Telegram Desktop proof only. Generic QA Lab lanes must not acquire this kind. - Discord (`kind: "discord"`): `{ guildId: string, channelId: string, driverBotToken: string, sutBotToken: string, sutApplicationId: string }`. - WhatsApp (`kind: "whatsapp"`): `{ driverPhoneE164: string, sutPhoneE164: string, driverAuthArchiveBase64: string, sutAuthArchiveBase64: string, groupJid?: string }` - phone numbers must be distinct E.164 strings. -For visual real-user Telegram proof, prefer a held Crabbox session: +The Mantis Telegram Desktop proof workflow holds one exclusive Convex +`telegram-user` lease for both the TDLib CLI driver and Telegram Desktop +witness, then releases it after publishing proof. -```bash -pnpm qa:telegram-user:crabbox -- start --tdlib-url http://artifacts.openclaw.ai/tdlib-v1.8.0-linux-x64.tgz --output-dir .artifacts/qa-e2e/telegram-user-crabbox/pr-review -pnpm qa:telegram-user:crabbox -- send --session .artifacts/qa-e2e/telegram-user-crabbox/pr-review/session.json --text /status -pnpm qa:telegram-user:crabbox -- finish --session .artifacts/qa-e2e/telegram-user-crabbox/pr-review/session.json -``` - -`start` holds one exclusive Convex `telegram-user` lease for both the TDLib CLI -driver and Telegram Desktop witness, starts desktop recording, and leaves the -Crabbox alive for arbitrary agent-driven repro steps. Agents can use `send`, -`run`, `screenshot`, and `status` until they are satisfied, then `finish` -collects the screenshot, video, motion-trimmed video/GIF, TDLib probe outputs, -and logs before releasing the credential. `publish --session --pr -` comments only the motion GIF by default; `--full-artifacts` is the -explicit opt-in for logs and JSON output. The default `probe` command remains a -one-command shorthand for quick `/status` smoke checks. - -Use `--mock-response-file ` when a PR needs a deterministic visual diff: -the same mock model reply can be run on `main` and on the PR head while the -Telegram formatter or delivery layer changes. Capture defaults are tuned for PR -comments: standard Crabbox class, 24fps desktop recording, 24fps motion GIF, and -1920px preview width. Before/after comments should publish a clean bundle that -contains only the intended GIFs. +When a PR needs a deterministic visual diff, Mantis can use the same mock model +reply on `main` and on the PR head while the Telegram formatter or delivery +layer changes. Capture defaults are tuned for PR comments: standard Crabbox +class, 24fps desktop recording, 24fps motion GIF, and 1920px preview width. +Before/after comments should publish a clean bundle that contains only the +intended GIFs. Slack lanes can also use the pool. Slack payload shape checks currently live in the Slack QA runner rather than the broker; use `{ channelId: string, driverBotToken: string, sutBotToken: string, sutAppToken: string }`, with a Slack channel id like `Cxxxxxxxxxx`. See [Setting up the Slack workspace](#setting-up-the-slack-workspace) for app and scope provisioning. diff --git a/docs/help/testing.md b/docs/help/testing.md index fb2654e9c718..c2c8c0a68c3a 100644 --- a/docs/help/testing.md +++ b/docs/help/testing.md @@ -450,70 +450,7 @@ Payload shape for Telegram real-user kind: - `{ groupId: string, sutToken: string, testerUserId: string, testerUsername: string, telegramApiId: string, telegramApiHash: string, tdlibDatabaseEncryptionKey: string, tdlibArchiveBase64: string, tdlibArchiveSha256: string, desktopTdataArchiveBase64: string, desktopTdataArchiveSha256: string }` - `groupId`, `testerUserId`, and `telegramApiId` must be numeric strings. - `tdlibArchiveSha256` and `desktopTdataArchiveSha256` must be SHA-256 hex strings. -- `kind: "telegram-user"` represents one Telegram burner account. Treat the lease as account-wide: the TDLib CLI driver and Telegram Desktop visual witness restore from the same payload, and only one job should hold the lease at a time. - -Telegram real-user lease restore: - -```bash -tmp=$(mktemp -d /tmp/openclaw-telegram-user.XXXXXX) -node --import tsx scripts/e2e/telegram-user-credential.ts lease-restore \ - --user-driver-dir "$tmp/user-driver" \ - --desktop-workdir "$tmp/desktop" \ - --lease-file "$tmp/lease.json" -TELEGRAM_USER_DRIVER_STATE_DIR="$tmp/user-driver" \ - uv run ~/.codex/skills/custom/telegram-e2e-bot-to-bot/scripts/user-driver.py status --json -node --import tsx scripts/e2e/telegram-user-credential.ts release --lease-file "$tmp/lease.json" -``` - -Use the restored Desktop profile with `Telegram -workdir "$tmp/desktop"` when a visual recording is needed. In local operator environments, `scripts/e2e/telegram-user-credential.ts` reads `~/.codex/skills/custom/telegram-e2e-bot-to-bot/convex.local.env` by default if process env vars are absent. - -Agent-driven Crabbox session: - -```bash -pnpm qa:telegram-user:crabbox -- start \ - --tdlib-url http://artifacts.openclaw.ai/tdlib-v1.8.0-linux-x64.tgz \ - --output-dir .artifacts/qa-e2e/telegram-user-crabbox/pr-review -pnpm qa:telegram-user:crabbox -- send \ - --session .artifacts/qa-e2e/telegram-user-crabbox/pr-review/session.json \ - --text /status -pnpm qa:telegram-user:crabbox -- finish \ - --session .artifacts/qa-e2e/telegram-user-crabbox/pr-review/session.json -``` - -`start` leases the `telegram-user` credential, restores the same account into -TDLib and Telegram Desktop on a Crabbox Linux desktop, starts a local mock SUT -gateway from the current checkout, opens the visible Telegram chat, starts -desktop recording, and writes a private `session.json`. While the session is -alive, an agent can keep testing until satisfied: - -- `send --session --text ` sends through the real TDLib user and waits for the SUT reply. -- `run --session -- ` runs an arbitrary command on the Crabbox and saves its output, for example `bash -lc 'source /tmp/openclaw-telegram-user-crabbox/env.sh && python3 /tmp/openclaw-telegram-user-crabbox/user-driver.py transcript --limit 20 --json'`. -- `screenshot --session ` captures the current visible desktop. -- `status --session ` prints the lease and WebVNC command. -- `finish --session ` stops the recorder, captures screenshot/video/motion-trim artifacts, releases the Convex credential, stops local SUT processes, and stops the Crabbox lease unless `--keep-box` is passed. -- `publish --session --pr ` publishes a GIF-only PR comment by default. Pass `--full-artifacts` only when logs or JSON artifacts are intentionally needed. - -For deterministic visual repros, pass `--mock-response-file ` to `start` -or to the one-command `probe` shorthand. The runner defaults to a standard -Crabbox class, 24fps recording, 24fps motion GIF previews, and 1920px GIF -width. Override with `--class`, `--record-fps`, `--preview-fps`, and -`--preview-width` only when the proof needs different capture settings. - -One-command Crabbox proof: - -```bash -pnpm qa:telegram-user:crabbox -- --text /status -``` - -The default `probe` command is shorthand for one start/send/finish cycle. Use -it for a quick `/status` smoke. Use the session commands for PR review, -bug-reproduction work, or any case where the agent needs minutes of arbitrary -experimentation before deciding the proof is complete. Use `--id ` to -reuse a warm desktop lease, `--keep-box` to keep VNC open after finish, -`--desktop-chat-title ` to pick the visible chat, and `--tdlib-url ` -when using a prebaked Linux `libtdjson.so` archive instead of building TDLib on -a fresh box. The runner verifies `--tdlib-url` with `--tdlib-sha256 ` or, -by default, a sibling `.sha256` file. +- `kind: "telegram-user"` is reserved for the Mantis Telegram Desktop proof workflow. Generic QA Lab lanes must not acquire it. Broker-validated multi-channel payloads: diff --git a/extensions/qa-lab/runtime-api.ts b/extensions/qa-lab/runtime-api.ts index 4dd06377e565..fd5e39eb35e1 100644 --- a/extensions/qa-lab/runtime-api.ts +++ b/extensions/qa-lab/runtime-api.ts @@ -39,8 +39,3 @@ export { setQaChannelRuntime, } from "./src/runtime-api.js"; export { startQaLiveLaneGateway } from "./src/live-transports/shared/live-gateway.runtime.js"; -export { - TELEGRAM_USER_QA_CREDENTIAL_KIND, - parseTelegramUserQaCredentialPayload, - type TelegramUserQaCredentialPayload, -} from "./src/live-transports/telegram/telegram-user-credential.runtime.js"; diff --git a/extensions/qa-lab/src/live-transports/telegram/telegram-user-credential.runtime.test.ts b/extensions/qa-lab/src/live-transports/telegram/telegram-user-credential.runtime.test.ts deleted file mode 100644 index 6c0f5f73bf8e..000000000000 --- a/extensions/qa-lab/src/live-transports/telegram/telegram-user-credential.runtime.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - TELEGRAM_USER_QA_CREDENTIAL_KIND, - parseTelegramUserQaCredentialPayload, -} from "./telegram-user-credential.runtime.js"; - -describe("Telegram user QA credential payload", () => { - it("parses the account-wide CLI and Desktop credential shape", () => { - const sha256 = "a".repeat(64); - - expect( - parseTelegramUserQaCredentialPayload({ - groupId: " -100123 ", - sutToken: " sut-token ", - testerUserId: " 8709353529 ", - testerUsername: " OpenClawTestUser ", - telegramApiId: " 123456 ", - telegramApiHash: " api-hash ", - tdlibDatabaseEncryptionKey: " db-key ", - tdlibArchiveBase64: " tdlib-archive ", - tdlibArchiveSha256: sha256.toUpperCase(), - desktopTdataArchiveBase64: " desktop-archive ", - desktopTdataArchiveSha256: sha256, - }), - ).toEqual({ - groupId: "-100123", - sutToken: "sut-token", - testerUserId: "8709353529", - testerUsername: "OpenClawTestUser", - telegramApiId: "123456", - telegramApiHash: "api-hash", - tdlibDatabaseEncryptionKey: "db-key", - tdlibArchiveBase64: "tdlib-archive", - tdlibArchiveSha256: sha256, - desktopTdataArchiveBase64: "desktop-archive", - desktopTdataArchiveSha256: sha256, - }); - expect(TELEGRAM_USER_QA_CREDENTIAL_KIND).toBe("telegram-user"); - }); - - it("rejects malformed payloads", () => { - expect(() => - parseTelegramUserQaCredentialPayload({ - groupId: "-100123", - sutToken: "sut-token", - testerUserId: "not-a-user-id", - testerUsername: "OpenClawTestUser", - telegramApiId: "123456", - telegramApiHash: "api-hash", - tdlibDatabaseEncryptionKey: "db-key", - tdlibArchiveBase64: "tdlib-archive", - tdlibArchiveSha256: "a".repeat(64), - desktopTdataArchiveBase64: "desktop-archive", - desktopTdataArchiveSha256: "b".repeat(64), - }), - ).toThrow(/numeric string/u); - }); -}); diff --git a/extensions/qa-lab/src/live-transports/telegram/telegram-user-credential.runtime.ts b/extensions/qa-lab/src/live-transports/telegram/telegram-user-credential.runtime.ts deleted file mode 100644 index 6a365c2a52bf..000000000000 --- a/extensions/qa-lab/src/live-transports/telegram/telegram-user-credential.runtime.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { z } from "zod"; - -export const TELEGRAM_USER_QA_CREDENTIAL_KIND = "telegram-user"; - -const sha256HexSchema = z - .string() - .trim() - .toLowerCase() - .regex(/^[a-f0-9]{64}$/u, "must be a SHA-256 hex string"); - -const numericStringSchema = z.string().trim().regex(/^\d+$/u, "must be a numeric string"); - -const telegramUserQaCredentialPayloadSchema = z.object({ - groupId: z - .string() - .trim() - .regex(/^-?\d+$/u, "must be a numeric Telegram chat id"), - sutToken: z.string().trim().min(1), - testerUserId: numericStringSchema, - testerUsername: z.string().trim().min(1), - telegramApiId: numericStringSchema, - telegramApiHash: z.string().trim().min(1), - tdlibDatabaseEncryptionKey: z.string().trim().min(1), - tdlibArchiveBase64: z.string().trim().min(1), - tdlibArchiveSha256: sha256HexSchema, - desktopTdataArchiveBase64: z.string().trim().min(1), - desktopTdataArchiveSha256: sha256HexSchema, -}); - -export type TelegramUserQaCredentialPayload = z.infer; - -export function parseTelegramUserQaCredentialPayload( - payload: unknown, -): TelegramUserQaCredentialPayload { - return telegramUserQaCredentialPayloadSchema.parse(payload); -} diff --git a/package.json b/package.json index fee1f0605cd8..08b8d8961101 100644 --- a/package.json +++ b/package.json @@ -1559,7 +1559,6 @@ "qa:lab:up:fast": "node --import tsx scripts/qa-lab-up.ts --use-prebuilt-image --bind-ui-dist --skip-ui-build", "qa:lab:watch": "vite build --watch --config extensions/qa-lab/web/vite.config.ts", "qa:otel:smoke": "node --import tsx scripts/qa-otel-smoke.ts", - "qa:telegram-user:crabbox": "node --import tsx scripts/e2e/telegram-user-crabbox-proof.ts", "release-metadata:check": "node scripts/check-release-metadata-only.mjs", "release:beta": "node scripts/release-candidate-checklist.mjs", "release:beta-smoke": "node --import tsx scripts/release-beta-smoke.ts", diff --git a/scripts/e2e/telegram-user-crabbox-proof.ts b/scripts/e2e/telegram-user-crabbox-proof.ts index 1b12665af1cb..70c409520b4e 100644 --- a/scripts/e2e/telegram-user-crabbox-proof.ts +++ b/scripts/e2e/telegram-user-crabbox-proof.ts @@ -1697,10 +1697,10 @@ async function startSession(root: string, opts: Options, outputDir: string) { }, webvnc: `${opts.crabboxBin} webvnc --provider ${opts.provider} --target ${opts.target} --id ${leaseId} --open`, commands: { - send: `pnpm qa:telegram-user:crabbox -- send --session ${path.relative(root, pathname)} --text '/status'`, - view: `pnpm qa:telegram-user:crabbox -- view --session ${path.relative(root, pathname)} --message-id `, - run: `pnpm qa:telegram-user:crabbox -- run --session ${path.relative(root, pathname)} -- bash -lc 'source ${REMOTE_ROOT}/env.sh && python3 ${REMOTE_ROOT}/user-driver.py transcript --limit 20 --json'`, - finish: `pnpm qa:telegram-user:crabbox -- finish --session ${path.relative(root, pathname)} --preview-crop telegram-window`, + send: `openclaw-telegram-user-crabbox-proof send --session ${path.relative(root, pathname)} --text '/status'`, + view: `openclaw-telegram-user-crabbox-proof view --session ${path.relative(root, pathname)} --message-id `, + run: `openclaw-telegram-user-crabbox-proof run --session ${path.relative(root, pathname)} -- bash -lc 'source ${REMOTE_ROOT}/env.sh && python3 ${REMOTE_ROOT}/user-driver.py transcript --limit 20 --json'`, + finish: `openclaw-telegram-user-crabbox-proof finish --session ${path.relative(root, pathname)} --preview-crop telegram-window`, }, }; } catch (error) { diff --git a/scripts/e2e/telegram-user-credential.ts b/scripts/e2e/telegram-user-credential.ts index 03f4fcd4cfb3..ab9d08a80e8b 100644 --- a/scripts/e2e/telegram-user-credential.ts +++ b/scripts/e2e/telegram-user-credential.ts @@ -3,10 +3,7 @@ import { spawn } from "node:child_process"; import { createHash } from "node:crypto"; import { chmod, copyFile, mkdir, readFile, rm, unlink, writeFile } from "node:fs/promises"; -import { - TELEGRAM_USER_QA_CREDENTIAL_KIND, - parseTelegramUserQaCredentialPayload, -} from "../../extensions/qa-lab/runtime-api.js"; +import { normalizeCredentialPayloadForKind } from "../qa/convex-credential-broker/convex/payload-validation.js"; type JsonObject = Record; @@ -15,6 +12,7 @@ const DEFAULT_BOT_CREDENTIALS_FILE = "~/.codex/skills/custom/telegram-e2e-bot-to-bot/credentials.local.json"; const DEFAULT_CONVEX_ENV_FILE = "~/.codex/skills/custom/telegram-e2e-bot-to-bot/convex.local.env"; const CHUNKED_PAYLOAD_MARKER = "__openclawQaCredentialPayloadChunksV1"; +const TELEGRAM_USER_QA_CREDENTIAL_KIND = "telegram-user"; function usage(): never { throw new Error( @@ -156,6 +154,10 @@ function optionalPositiveInteger(value: string | undefined, fallback: number) { return parsed; } +function parseTelegramUserQaCredentialPayload(payload: Record): JsonObject { + return normalizeCredentialPayloadForKind(TELEGRAM_USER_QA_CREDENTIAL_KIND, payload); +} + async function fileSha256(path: string) { return createHash("sha256") .update(await readFile(path)) @@ -342,7 +344,7 @@ async function hydratePayloadFromLease(params: { if (serialized.length !== marker.byteLength) { throw new Error("Chunked payload length mismatch."); } - return parseTelegramUserQaCredentialPayload(JSON.parse(serialized)) as JsonObject; + return parseTelegramUserQaCredentialPayload(JSON.parse(serialized)); } async function createTelegramUserPayload(opts: Map) { diff --git a/test/scripts/mantis-telegram-desktop-proof-workflow.test.ts b/test/scripts/mantis-telegram-desktop-proof-workflow.test.ts index 90f024b1d608..25386a61660e 100644 --- a/test/scripts/mantis-telegram-desktop-proof-workflow.test.ts +++ b/test/scripts/mantis-telegram-desktop-proof-workflow.test.ts @@ -1,13 +1,16 @@ -import { existsSync, readFileSync } from "node:fs"; +import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; import { describe, expect, it } from "vitest"; import { parse } from "yaml"; const PROOF_SCRIPT = "scripts/e2e/telegram-user-crabbox-proof.ts"; +const CREDENTIAL_SCRIPT = "scripts/e2e/telegram-user-credential.ts"; const USER_DRIVER = "scripts/e2e/telegram-user-driver.py"; +const QA_LAB_RUNTIME_API = "extensions/qa-lab/runtime-api.ts"; const PACKAGE_JSON = "package.json"; const WORKFLOW = ".github/workflows/mantis-telegram-desktop-proof.yml"; const LIVE_WORKFLOW = ".github/workflows/mantis-telegram-live.yml"; const PROMPT = ".github/codex/prompts/mantis-telegram-desktop-proof.md"; +const DOCS = ["docs/help/testing.md", "docs/concepts/qa-e2e-automation.md"]; type WorkflowStep = { env?: Record; @@ -76,6 +79,13 @@ function jobStep(workflowFile: string, jobName: string, stepName: string): Workf return step; } +function filesUnder(root: string): string[] { + return readdirSync(root).flatMap((name) => { + const file = `${root}/${name}`; + return statSync(file).isDirectory() ? filesUnder(file) : [file]; + }); +} + describe("Mantis Telegram Desktop proof workflow", () => { it("runs with the repository pnpm major", () => { const workflow = parse(readFileSync(WORKFLOW, "utf8")) as Workflow; @@ -171,6 +181,30 @@ describe("Mantis Telegram Desktop proof workflow", () => { expect(readFileSync(USER_DRIVER, "utf8")).toContain("/usr/local/lib/libtdjson.so"); }); + it("keeps Telegram Desktop proof credentials out of the generic qa-lab API", () => { + const packageJson = JSON.parse(readFileSync(PACKAGE_JSON, "utf8")) as { + scripts?: Record; + }; + const workflowFiles = filesUnder(".github/workflows").filter((file) => file.endsWith(".yml")); + const telegramUserWorkflows = workflowFiles.filter((file) => + readFileSync(file, "utf8").includes("telegram-user"), + ); + + expect(readFileSync(QA_LAB_RUNTIME_API, "utf8")).not.toContain("telegram-user"); + expect(packageJson.scripts).not.toHaveProperty("qa:telegram-user:crabbox"); + expect(telegramUserWorkflows).toEqual([WORKFLOW]); + for (const doc of DOCS) { + expect(readFileSync(doc, "utf8")).not.toContain("pnpm qa:telegram-user:crabbox"); + } + expect(readFileSync(PROOF_SCRIPT, "utf8")).not.toContain("pnpm qa:telegram-user:crabbox"); + expect(readFileSync(CREDENTIAL_SCRIPT, "utf8")).toContain( + 'const TELEGRAM_USER_QA_CREDENTIAL_KIND = "telegram-user";', + ); + expect(readFileSync(CREDENTIAL_SCRIPT, "utf8")).toContain( + "../qa/convex-credential-broker/convex/payload-validation.js", + ); + }); + it("authorizes Telegram Desktop from the leased TDLib user session", () => { const proofScript = readFileSync(PROOF_SCRIPT, "utf8"); const userDriver = readFileSync(USER_DRIVER, "utf8");