
























@@ -0,0 +1,215 @@
1+import { execFile, spawn } from "node:child_process";
2+import fs from "node:fs/promises";
3+import path from "node:path";
4+import { promisify } from "node:util";
5+6+const execFileAsync = promisify(execFile);
7+8+export type RttProviderMode = "mock-openai" | "live-frontier";
9+10+export type RttCliOptions = {
11+providerMode: RttProviderMode;
12+runs: number;
13+harnessRoot: string;
14+output: string;
15+scenarios: string[];
16+timeoutMs: number;
17+};
18+19+export type RttResult = {
20+package: {
21+spec: string;
22+version: string;
23+};
24+run: {
25+id: string;
26+startedAt: string;
27+finishedAt: string;
28+durationMs: number;
29+status: "pass" | "fail";
30+};
31+mode: {
32+providerMode: RttProviderMode;
33+scenarios: string[];
34+};
35+rtt: {
36+canaryMs?: number;
37+mentionReplyMs?: number;
38+};
39+artifacts: {
40+rawSummaryPath: string;
41+rawReportPath: string;
42+rawObservedMessagesPath: string;
43+resultPath: string;
44+};
45+};
46+47+export type TelegramQaSummary = {
48+scenarios?: Array<{
49+id?: string;
50+rttMs?: number;
51+status?: string;
52+}>;
53+};
54+55+const OPENCLAW_PACKAGE_SPEC_RE =
56+/^openclaw@(beta|latest|[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-[1-9][0-9]*|-beta\.[1-9][0-9]*)?)$/u;
57+58+const REQUIRED_TELEGRAM_ENV = [
59+"OPENCLAW_QA_TELEGRAM_GROUP_ID",
60+"OPENCLAW_QA_TELEGRAM_DRIVER_BOT_TOKEN",
61+"OPENCLAW_QA_TELEGRAM_SUT_BOT_TOKEN",
62+] as const;
63+64+export function validateOpenClawPackageSpec(spec: string) {
65+if (!OPENCLAW_PACKAGE_SPEC_RE.test(spec)) {
66+throw new Error(
67+`Package spec must be openclaw@beta, openclaw@latest, or an exact OpenClaw release version; got: ${spec}`,
68+);
69+}
70+return spec;
71+}
72+73+export function safeRunLabel(input: string) {
74+return input.replace(/[^a-zA-Z0-9.-]+/gu, "_").replace(/^_+|_+$/gu, "");
75+}
76+77+export function buildRunId(params: { now: Date; spec: string; index?: number }) {
78+const stamp = params.now.toISOString().replaceAll(":", "").replaceAll(".", "");
79+const suffix = params.index === undefined ? "" : `-${params.index + 1}`;
80+return `${stamp}-${safeRunLabel(params.spec)}${suffix}`;
81+}
82+83+export function extractRtt(summary: TelegramQaSummary) {
84+const scenarios = summary.scenarios ?? [];
85+return {
86+canaryMs: scenarios.find((scenario) => scenario.id === "telegram-canary")?.rttMs,
87+mentionReplyMs: scenarios.find((scenario) => scenario.id === "telegram-mentioned-message-reply")
88+?.rttMs,
89+};
90+}
91+92+export function createHarnessEnv(params: {
93+baseEnv: NodeJS.ProcessEnv;
94+providerMode: RttProviderMode;
95+scenarios: string[];
96+spec: string;
97+version: string;
98+rawOutputDir: string;
99+timeoutMs: number;
100+}) {
101+return {
102+ ...params.baseEnv,
103+OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC: params.spec,
104+OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL: `${params.spec} (${params.version})`,
105+OPENCLAW_NPM_TELEGRAM_PROVIDER_MODE: params.providerMode,
106+OPENCLAW_NPM_TELEGRAM_SCENARIOS: params.scenarios.join(","),
107+OPENCLAW_NPM_TELEGRAM_SKIP_HOTPATH: "1",
108+OPENCLAW_NPM_TELEGRAM_OUTPUT_DIR: params.rawOutputDir,
109+OPENCLAW_NPM_TELEGRAM_FAST: params.baseEnv.OPENCLAW_NPM_TELEGRAM_FAST ?? "1",
110+OPENCLAW_QA_TELEGRAM_CANARY_TIMEOUT_MS: String(params.timeoutMs),
111+OPENCLAW_QA_TELEGRAM_SCENARIO_TIMEOUT_MS: String(params.timeoutMs),
112+};
113+}
114+115+export function assertRequiredEnv(env: NodeJS.ProcessEnv) {
116+const missing = REQUIRED_TELEGRAM_ENV.filter((key) => !env[key]?.trim());
117+if (missing.length > 0) {
118+throw new Error(`Missing Telegram QA env: ${missing.join(", ")}`);
119+}
120+}
121+122+export async function assertHarnessRoot(harnessRoot: string) {
123+const scriptPath = path.join(harnessRoot, "scripts/e2e/npm-telegram-live-docker.sh");
124+try {
125+await fs.access(scriptPath);
126+} catch {
127+throw new Error(`Missing OpenClaw Telegram npm harness: ${scriptPath}`);
128+}
129+}
130+131+export async function assertDockerAvailable() {
132+try {
133+await execFileAsync("docker", ["version", "--format", "{{.Server.Version}}"], {
134+timeout: 10_000,
135+});
136+} catch {
137+throw new Error("Docker is required for RTT runs; install/start Docker and retry.");
138+}
139+}
140+141+export async function resolvePublishedVersion(spec: string) {
142+const { stdout } = await execFileAsync("npm", ["view", spec, "version", "--json"], {
143+timeout: 30_000,
144+});
145+const parsed = JSON.parse(stdout.trim()) as unknown;
146+if (typeof parsed !== "string" || parsed.trim().length === 0) {
147+throw new Error(`npm did not return a version for ${spec}.`);
148+}
149+return parsed.trim();
150+}
151+152+export async function readTelegramSummary(summaryPath: string) {
153+return JSON.parse(await fs.readFile(summaryPath, "utf8")) as TelegramQaSummary;
154+}
155+156+export async function writeJson(pathname: string, value: unknown) {
157+await fs.mkdir(path.dirname(pathname), { recursive: true });
158+await fs.writeFile(pathname, `${JSON.stringify(value, null, 2)}\n`);
159+}
160+161+export async function appendJsonl(pathname: string, value: unknown) {
162+await fs.mkdir(path.dirname(pathname), { recursive: true });
163+await fs.appendFile(pathname, `${JSON.stringify(value)}\n`);
164+}
165+166+export async function runHarness(params: { env: NodeJS.ProcessEnv; harnessRoot: string }) {
167+const scriptPath = path.join(params.harnessRoot, "scripts/e2e/npm-telegram-live-docker.sh");
168+const child = spawn("bash", [scriptPath], {
169+cwd: params.harnessRoot,
170+env: params.env,
171+stdio: "inherit",
172+});
173+const exitCode = await new Promise<number | null>((resolve, reject) => {
174+child.once("error", reject);
175+child.once("exit", resolve);
176+});
177+return exitCode ?? 1;
178+}
179+180+export function buildRttResult(params: {
181+artifacts: RttResult["artifacts"];
182+finishedAt: Date;
183+providerMode: RttProviderMode;
184+rawSummary: TelegramQaSummary;
185+runId: string;
186+scenarios: string[];
187+spec: string;
188+startedAt: Date;
189+version: string;
190+}): RttResult {
191+const failed = (params.rawSummary.scenarios ?? []).some((scenario) => scenario.status === "fail");
192+return {
193+package: {
194+spec: params.spec,
195+version: params.version,
196+},
197+run: {
198+id: params.runId,
199+startedAt: params.startedAt.toISOString(),
200+finishedAt: params.finishedAt.toISOString(),
201+durationMs: params.finishedAt.getTime() - params.startedAt.getTime(),
202+status: failed ? "fail" : "pass",
203+},
204+mode: {
205+providerMode: params.providerMode,
206+scenarios: params.scenarios,
207+},
208+rtt: extractRtt(params.rawSummary),
209+artifacts: params.artifacts,
210+};
211+}
212+213+export const __testing = {
214+REQUIRED_TELEGRAM_ENV,
215+};
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。