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
This commit is contained in:
Dallin Romney 2026-06-18 15:45:33 -07:00 committed by GitHub
parent 37eea55afa
commit 4ca0e52d0e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 110 additions and 225 deletions

View file

@ -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 <telegram-chat-id> \
--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 <telegram-chat-id> \
--flow thinking-final
node scripts/run-vitest.mjs extensions/telegram/src/channel-message-flows.qa.e2e.test.ts
```
## Options
## References
- `--account <accountId>`: Telegram account id when not using the default.
- `--thread-id <id>`: Telegram forum topic/message thread id.
- `--delay-ms <ms>`: Override preview update cadence.
- `--duration-ms <ms>`: Simulated working duration for `working-final`.
- `--final-text <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.

View file

@ -55,6 +55,7 @@ describe("qa scenario catalog", () => {
.toSorted(),
).toStrictEqual(
[
"channel-message-flows",
"control-ui-chat-flow-playwright",
"gateway-smoke",
"package-openclaw-for-docker",

View file

@ -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<void>;
@ -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(),

View file

@ -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<TelegramApi["sendMessage"]>;
type TelegramEditMessageTextParams = Parameters<TelegramApi["editMessageText"]>;
type TelegramDeleteMessageParams = Parameters<TelegramApi["deleteMessage"]>;
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<void>;
};
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 <chat-id> --flow <flow> [options]",
"",
"Flows:",
" thinking-final Reasoning/Thinking preview, then a final answer",
" working-final Editable tool-progress preview, then a final answer",
"",
"Options:",
" --account <accountId> Telegram account id to use",
" --thread-id <id> Telegram forum topic/message thread id",
" --delay-ms <ms> Delay between preview updates (default: flow-specific)",
" --duration-ms <ms> Simulated working duration for working-final (default: 12000)",
" --final-text <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 ?? "<missing>"}.\n\n${usage()}`);
}
if (!target) {
throw new Error(`Missing --target <chat-id>.\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<TelegramFlowResult> {
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<TelegramFlowResult> {
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<void> {
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;
});
}

View file

@ -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.