From 4ca0e52d0edf76fd47c7f23c2f12b6766b623683 Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Thu, 18 Jun 2026 15:45:33 -0700 Subject: [PATCH] test: fold channel message flows into qa e2e (#93174) * test: fold channel flows into qa e2e * test: keep channel flow skill pointed at qa * test: move channel flow proof under telegram --- .agents/skills/channel-message-flows/SKILL.md | 42 ++-- .../qa-lab/src/scenario-catalog.test.ts | 1 + .../src/channel-message-flows.qa.e2e.test.ts | 68 ++---- .../test-support}/channel-message-flows.ts | 197 +++++------------- .../channels/channel-message-flows.yaml | 27 +++ 5 files changed, 110 insertions(+), 225 deletions(-) rename test/scripts/channel-message-flows.test.ts => extensions/telegram/src/channel-message-flows.qa.e2e.test.ts (85%) rename {scripts/dev => extensions/telegram/src/test-support}/channel-message-flows.ts (65%) create mode 100644 qa/scenarios/channels/channel-message-flows.yaml diff --git a/.agents/skills/channel-message-flows/SKILL.md b/.agents/skills/channel-message-flows/SKILL.md index 2e4a900ea27..714945b7b89 100644 --- a/.agents/skills/channel-message-flows/SKILL.md +++ b/.agents/skills/channel-message-flows/SKILL.md @@ -1,44 +1,34 @@ --- name: channel-message-flows -description: "Use when previewing local channel message flow fixtures." +description: "Use when running QA Lab channel message flow evidence." --- # Channel Message Flows -Use this from the OpenClaw repo root to send canned channel preview flows while iterating on message UX. These are real sends/edits/deletes against the configured channel target. +Use this from the OpenClaw repo root to run the QA Lab evidence for Telegram +draft/final delivery sequencing. This skill no longer launches a standalone +script; the behavior is owned by the QA scenario and its Vitest-backed e2e test. -## Telegram +## QA Scenario -Native Telegram `sendMessageDraft` tool progress, then a final answer: +Run the scenario through QA Lab: ```bash -node --import tsx scripts/dev/channel-message-flows.ts \ - --channel telegram \ - --target \ - --flow working-final \ - --duration-ms 20000 +pnpm openclaw qa suite --scenario channel-message-flows ``` -Thinking preview, then a final answer: +Run the focused e2e test directly in a Codex worktree: ```bash -node --import tsx scripts/dev/channel-message-flows.ts \ - --channel telegram \ - --target \ - --flow thinking-final +node scripts/run-vitest.mjs extensions/telegram/src/channel-message-flows.qa.e2e.test.ts ``` -## Options +## References -- `--account `: Telegram account id when not using the default. -- `--thread-id `: Telegram forum topic/message thread id. -- `--delay-ms `: Override preview update cadence. -- `--duration-ms `: Simulated working duration for `working-final`. -- `--final-text `: Override the durable final message. +- `qa/scenarios/channels/channel-message-flows.yaml` +- `extensions/telegram/src/channel-message-flows.qa.e2e.test.ts` +- `extensions/telegram/src/test-support/channel-message-flows.ts` -## Notes - -- `--target` is the numeric Telegram chat id. -- `working-final` exercises native Telegram `sendMessageDraft` with static `Working` status and sample tool progress. -- `thinking-final` exercises formatted `Thinking` reasoning preview clearing before the final answer. -- Only `--channel telegram` is implemented for now. +The scenario covers `channels.streaming` as primary evidence and records +secondary coverage for thread preservation, delivery ordering, and reasoning +preview visibility. diff --git a/extensions/qa-lab/src/scenario-catalog.test.ts b/extensions/qa-lab/src/scenario-catalog.test.ts index 774068fe32b..935980e1f9a 100644 --- a/extensions/qa-lab/src/scenario-catalog.test.ts +++ b/extensions/qa-lab/src/scenario-catalog.test.ts @@ -55,6 +55,7 @@ describe("qa scenario catalog", () => { .toSorted(), ).toStrictEqual( [ + "channel-message-flows", "control-ui-chat-flow-playwright", "gateway-smoke", "package-openclaw-for-docker", diff --git a/test/scripts/channel-message-flows.test.ts b/extensions/telegram/src/channel-message-flows.qa.e2e.test.ts similarity index 85% rename from test/scripts/channel-message-flows.test.ts rename to extensions/telegram/src/channel-message-flows.qa.e2e.test.ts index eb2b26f51eb..3d3c56ae654 100644 --- a/test/scripts/channel-message-flows.test.ts +++ b/extensions/telegram/src/channel-message-flows.qa.e2e.test.ts @@ -1,14 +1,13 @@ -// Channel Message Flows tests cover channel message flows script behavior. +// Channel Message Flows tests cover QA Lab channel delivery evidence. +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { describe, expect, it, vi } from "vitest"; import { - parseChannelMessageFlowArgs, resolveTelegramFlowThreadSpec, runTelegramThinkingFinalFlow, runTelegramWorkingFinalFlow, -} from "../../scripts/dev/channel-message-flows.ts"; -import type { OpenClawConfig } from "../../src/config/types.openclaw.js"; +} from "./test-support/channel-message-flows.js"; -describe("channel message flows dev runner", () => { +describe("channel message flows QA e2e", () => { function createTestDraftStream(params?: { update?: (text: string) => void; flush?: () => Promise; @@ -16,6 +15,7 @@ describe("channel message flows dev runner", () => { }) { return { update: vi.fn(params?.update ?? (() => {})), + updatePreview: vi.fn(), flush: vi.fn(params?.flush ?? (async () => {})), clear: vi.fn(params?.clear ?? (async () => {})), stop: vi.fn(async () => {}), @@ -24,55 +24,6 @@ describe("channel message flows dev runner", () => { }; } - it("parses the Telegram thinking-final flow from channel/target flags", () => { - const parsed = parseChannelMessageFlowArgs([ - "--channel", - "telegram", - "--target", - "123", - "--flow", - "thinking-final", - "--account", - "sut", - "--thread-id", - "42", - "--delay-ms", - "0", - ]); - - expect(parsed).toEqual({ - accountId: "sut", - channel: "telegram", - delayMs: 0, - flow: "thinking-final", - target: "123", - threadId: 42, - }); - }); - - it("parses the Telegram working-final flow from channel/chat flags", () => { - const parsed = parseChannelMessageFlowArgs([ - "--channel", - "telegram", - "--chat", - "123", - "--flow", - "working-final", - "--duration-ms", - "12000", - "--delay-ms", - "0", - ]); - - expect(parsed).toEqual({ - channel: "telegram", - delayMs: 0, - durationMs: 12000, - flow: "working-final", - target: "123", - }); - }); - it("streams thinking updates, clears the preview, then sends the final answer", async () => { const events: string[] = []; const stream = { @@ -85,6 +36,7 @@ describe("channel message flows dev runner", () => { clear: vi.fn(async () => { events.push("clear"); }), + updatePreview: vi.fn(), stop: vi.fn(async () => {}), messageId: vi.fn(() => 17), forceNewMessage: vi.fn(), @@ -96,9 +48,11 @@ describe("channel message flows dev runner", () => { const result = await runTelegramThinkingFinalFlow( { + accountId: "sut", cfg: {} as OpenClawConfig, delayMs: 0, target: "123", + threadId: 42, thinkingUpdates: ["Checking the request.", "Reading the Telegram code.", "Ready."], }, { @@ -114,11 +68,11 @@ describe("channel message flows dev runner", () => { expect(events.at(-2)).toBe("clear"); expect(events.at(-1)).toBe("final"); expect(sendFinal).toHaveBeenCalledWith({ - accountId: undefined, + accountId: "sut", cfg: {}, target: "123", text: "Final answer: the Telegram thinking preview cleared and this durable reply landed.", - threadId: undefined, + threadId: 42, }); expect(result).toEqual({ finalMessageId: "99", previewUpdates: 3 }); }); @@ -130,6 +84,7 @@ describe("channel message flows dev runner", () => { throw new Error("flush failed"); }), clear: vi.fn(async () => {}), + updatePreview: vi.fn(), stop: vi.fn(async () => {}), messageId: vi.fn(() => 17), forceNewMessage: vi.fn(), @@ -161,6 +116,7 @@ describe("channel message flows dev runner", () => { update: vi.fn(() => {}), flush: vi.fn(async () => {}), clear: vi.fn(async () => {}), + updatePreview: vi.fn(), stop: vi.fn(async () => {}), messageId: vi.fn(() => 17), forceNewMessage: vi.fn(), diff --git a/scripts/dev/channel-message-flows.ts b/extensions/telegram/src/test-support/channel-message-flows.ts similarity index 65% rename from scripts/dev/channel-message-flows.ts rename to extensions/telegram/src/test-support/channel-message-flows.ts index 968387cf2a6..a61417bf3be 100644 --- a/scripts/dev/channel-message-flows.ts +++ b/extensions/telegram/src/test-support/channel-message-flows.ts @@ -1,42 +1,27 @@ -#!/usr/bin/env -S node --import tsx -// Channel Message Flows script supports OpenClaw repository automation. +// Channel Message Flows runtime supports QA Lab channel delivery evidence. import { setTimeout as sleep } from "node:timers/promises"; -import { fileURLToPath } from "node:url"; import type { Bot } from "grammy"; import type { Message } from "grammy/types"; -import { - deleteMessageTelegram, - editMessageTelegram, - sendMessageTelegram, -} from "../../extensions/telegram/runtime-api.js"; -import type { TelegramThreadSpec } from "../../extensions/telegram/src/bot/helpers.js"; -import { - createTelegramDraftStream, - type TelegramDraftStream, -} from "../../extensions/telegram/src/draft-stream.js"; +import { formatReasoningMessage } from "openclaw/plugin-sdk/agent-runtime"; +import { formatChannelProgressDraftText } from "openclaw/plugin-sdk/channel-outbound"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; +import type { TelegramThreadSpec } from "../bot/helpers.js"; +import { createTelegramDraftStream, type TelegramDraftStream } from "../draft-stream.js"; import { buildTelegramRichMarkdown, + type TelegramEditRichMessageTextParams, type TelegramInputRichMessage, -} from "../../extensions/telegram/src/rich-message.js"; -import { formatReasoningMessage } from "../../src/agents/embedded-agent-utils.js"; -import { getRuntimeConfig } from "../../src/config/config.js"; -import type { OpenClawConfig } from "../../src/config/types.openclaw.js"; -import { formatChannelProgressDraftText } from "../../src/plugin-sdk/channel-outbound.js"; + type TelegramSendRichMessageParams, +} from "../rich-message.js"; +import { deleteMessageTelegram, editMessageTelegram, sendMessageTelegram } from "../send.js"; + +type TelegramApi = Bot["api"]; +type TelegramSendMessageParams = Parameters; +type TelegramEditMessageTextParams = Parameters; +type TelegramDeleteMessageParams = Parameters; -type SupportedChannel = "telegram"; type SupportedFlow = "thinking-final" | "working-final"; -export type ChannelMessageFlowArgs = { - accountId?: string; - channel: SupportedChannel; - delayMs?: number; - durationMs?: number; - finalText?: string; - flow: SupportedFlow; - target: string; - threadId?: number; -}; - type TelegramSendFinalParams = { accountId?: string; cfg: OpenClawConfig; @@ -50,19 +35,7 @@ type TelegramFlowResult = { previewUpdates: number; }; -function toError(value: unknown): Error { - return value instanceof Error ? value : new Error(String(value)); -} - -function requireFinalMessageId(final: { messageId?: string }, flow: SupportedFlow): string { - const messageId = final.messageId?.trim(); - if (!messageId) { - throw new Error(`${flow} final send did not return a durable Telegram message id`); - } - return messageId; -} - -type TelegramThinkingFinalDeps = { +type TelegramFlowDeps = { createDraftStream?: (params: { accountId?: string; cfg: OpenClawConfig; @@ -73,13 +46,18 @@ type TelegramThinkingFinalDeps = { sleep?: (ms: number) => Promise; }; -export type TelegramThinkingFinalFlowOptions = ChannelMessageFlowArgs & { +export type TelegramThinkingFinalFlowOptions = { + accountId?: string; cfg: OpenClawConfig; + delayMs?: number; + finalText?: string; + target: string; + threadId?: number; thinkingUpdates?: readonly string[]; }; -export type TelegramWorkingFinalFlowOptions = ChannelMessageFlowArgs & { - cfg: OpenClawConfig; +export type TelegramWorkingFinalFlowOptions = TelegramThinkingFinalFlowOptions & { + durationMs?: number; }; const DEFAULT_THINKING_FINAL_UPDATES = [ @@ -123,71 +101,16 @@ const DEFAULT_WORKING_PROGRESS_TIMELINE = [ }, ] as const; -function usage(): string { - return [ - "Usage:", - " node --import tsx scripts/dev/channel-message-flows.ts --channel telegram --target --flow [options]", - "", - "Flows:", - " thinking-final Reasoning/Thinking preview, then a final answer", - " working-final Editable tool-progress preview, then a final answer", - "", - "Options:", - " --account Telegram account id to use", - " --thread-id Telegram forum topic/message thread id", - " --delay-ms Delay between preview updates (default: flow-specific)", - " --duration-ms Simulated working duration for working-final (default: 12000)", - " --final-text Override the final durable message", - ].join("\n"); +function toError(value: unknown): Error { + return value instanceof Error ? value : new Error(String(value)); } -function readFlagValue(args: readonly string[], flag: string): string | undefined { - const index = args.indexOf(flag); - if (index === -1) { - return undefined; +function requireFinalMessageId(final: { messageId?: string }, flow: SupportedFlow): string { + const messageId = final.messageId?.trim(); + if (!messageId) { + throw new Error(`${flow} final send did not return a durable Telegram message id`); } - return args[index + 1]; -} - -function parseIntegerFlag(raw: string | undefined, label: string): number | undefined { - if (raw == null) { - return undefined; - } - if (!/^\d+$/u.test(raw)) { - throw new Error(`${label} must be a non-negative integer.\n\n${usage()}`); - } - return Number(raw); -} - -export function parseChannelMessageFlowArgs(args: readonly string[]): ChannelMessageFlowArgs { - if (args.includes("--help") || args.includes("-h")) { - throw new Error(usage()); - } - - const channel = readFlagValue(args, "--channel"); - const flow = readFlagValue(args, "--flow"); - const target = readFlagValue(args, "--target") ?? readFlagValue(args, "--chat"); - - if (channel !== "telegram") { - throw new Error(`Only --channel telegram is supported for now.\n\n${usage()}`); - } - if (flow !== "thinking-final" && flow !== "working-final") { - throw new Error(`Unsupported --flow ${flow ?? ""}.\n\n${usage()}`); - } - if (!target) { - throw new Error(`Missing --target .\n\n${usage()}`); - } - - return { - accountId: readFlagValue(args, "--account") ?? readFlagValue(args, "--account-id"), - channel, - delayMs: parseIntegerFlag(readFlagValue(args, "--delay-ms"), "--delay-ms"), - durationMs: parseIntegerFlag(readFlagValue(args, "--duration-ms"), "--duration-ms"), - finalText: readFlagValue(args, "--final-text"), - flow, - target, - threadId: parseIntegerFlag(readFlagValue(args, "--thread-id"), "--thread-id"), - }; + return messageId; } function resolveWorkingProgressLines(elapsedMs: number): string[] { @@ -207,15 +130,15 @@ function richMessageText(richMessage: TelegramInputRichMessage): { text: string; textMode: "markdown" | "html"; } { - return "html" in richMessage + return richMessage.html !== undefined ? { text: richMessage.html, textMode: "html" } : { text: richMessage.markdown, textMode: "markdown" }; } function createTelegramFlowApi(params: { accountId?: string; cfg: OpenClawConfig }): Bot["api"] { - return { + const api = { raw: { - sendRichMessage: async (sendParams) => { + sendRichMessage: async (sendParams: TelegramSendRichMessageParams) => { const richText = richMessageText(sendParams.rich_message); const result = await sendMessageTelegram(String(sendParams.chat_id), richText.text, { accountId: params.accountId, @@ -225,7 +148,7 @@ function createTelegramFlowApi(params: { accountId?: string; cfg: OpenClawConfig }); return { message_id: Number(result.messageId) } as Message; }, - editMessageText: async (editParams) => { + editMessageText: async (editParams: TelegramEditRichMessageTextParams) => { if (typeof editParams.message_id !== "number") { throw new Error("Telegram flow rich edit requires message_id."); } @@ -243,7 +166,11 @@ function createTelegramFlowApi(params: { accountId?: string; cfg: OpenClawConfig return true; }, }, - sendMessage: async (chatId, text, sendParams) => { + sendMessage: async ( + chatId: TelegramSendMessageParams[0], + text: TelegramSendMessageParams[1], + sendParams: TelegramSendMessageParams[2], + ) => { const result = await sendMessageTelegram(String(chatId), text, { accountId: params.accountId, cfg: params.cfg, @@ -252,7 +179,12 @@ function createTelegramFlowApi(params: { accountId?: string; cfg: OpenClawConfig }); return { message_id: Number(result.messageId) }; }, - editMessageText: async (chatId, messageId, text, editParams) => { + editMessageText: async ( + chatId: TelegramEditMessageTextParams[0], + messageId: TelegramEditMessageTextParams[1], + text: TelegramEditMessageTextParams[2], + editParams: TelegramEditMessageTextParams[3], + ) => { await editMessageTelegram(String(chatId), messageId, text, { accountId: params.accountId, cfg: params.cfg, @@ -260,14 +192,18 @@ function createTelegramFlowApi(params: { accountId?: string; cfg: OpenClawConfig }); return true; }, - deleteMessage: async (chatId, messageId) => { + deleteMessage: async ( + chatId: TelegramDeleteMessageParams[0], + messageId: TelegramDeleteMessageParams[1], + ) => { await deleteMessageTelegram(String(chatId), messageId, { accountId: params.accountId, cfg: params.cfg, }); return true; }, - } as Bot["api"]; + }; + return api as unknown as Bot["api"]; } export function resolveTelegramFlowThreadSpec(threadId?: number): TelegramThreadSpec | undefined { @@ -300,7 +236,7 @@ async function sendTelegramFinal(params: TelegramSendFinalParams): Promise<{ mes export async function runTelegramThinkingFinalFlow( options: TelegramThinkingFinalFlowOptions, - deps: TelegramThinkingFinalDeps = {}, + deps: TelegramFlowDeps = {}, ): Promise { const delayMs = options.delayMs ?? 900; const thinkingUpdates = options.thinkingUpdates ?? DEFAULT_THINKING_FINAL_UPDATES; @@ -358,7 +294,7 @@ export async function runTelegramThinkingFinalFlow( export async function runTelegramWorkingFinalFlow( options: TelegramWorkingFinalFlowOptions, - deps: TelegramThinkingFinalDeps = {}, + deps: TelegramFlowDeps = {}, ): Promise { const delayMs = options.delayMs ?? 2_000; const durationMs = options.durationMs ?? 12_000; @@ -421,28 +357,3 @@ export async function runTelegramWorkingFinalFlow( previewUpdates, }; } - -export async function main(args = process.argv.slice(2)): Promise { - if (args.includes("--help") || args.includes("-h")) { - process.stdout.write(`${usage()}\n`); - return; - } - - const parsed = parseChannelMessageFlowArgs(args); - const cfg = getRuntimeConfig(); - const result = - parsed.flow === "working-final" - ? await runTelegramWorkingFinalFlow({ ...parsed, cfg }) - : await runTelegramThinkingFinalFlow({ ...parsed, cfg }); - - process.stdout.write( - `Sent ${parsed.channel}/${parsed.flow} to ${parsed.target} (${result.previewUpdates} preview updates, final message ${result.finalMessageId ?? "unknown"}).\n`, - ); -} - -if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { - main().catch((err: unknown) => { - process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`); - process.exitCode = 1; - }); -} diff --git a/qa/scenarios/channels/channel-message-flows.yaml b/qa/scenarios/channels/channel-message-flows.yaml new file mode 100644 index 00000000000..8e8a2721d04 --- /dev/null +++ b/qa/scenarios/channels/channel-message-flows.yaml @@ -0,0 +1,27 @@ +title: Channel message flow evidence + +scenario: + id: channel-message-flows + surface: channels + coverage: + primary: + - channels.streaming + secondary: + - channels.threads + - runtime.delivery + - runtime.reasoning-visibility + objective: Exercise Telegram draft/final delivery sequencing through QA Lab evidence. + successCriteria: + - Thinking updates flush, clear, and send the final answer in order. + - Working previews are cleared before final delivery. + - Thread IDs are preserved in Telegram flow thread specs. + docsRefs: + - docs/channels/telegram.md + - docs/concepts/qa-e2e-automation.md + codeRefs: + - extensions/telegram/src/channel-message-flows.qa.e2e.test.ts + - extensions/telegram/src/test-support/channel-message-flows.ts + execution: + kind: vitest + path: extensions/telegram/src/channel-message-flows.qa.e2e.test.ts + summary: Vitest coverage for channel message flow sequencing.