mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
33ea7ffa54
* fix(qa): isolate package Telegram harness Keep private QA source, dependencies, taxonomy, and SDK dist in the trusted harness while the installed candidate owns its CLI, Gateway runtime, and persisted mock auth. Preserve the documented package RTT canary after taxonomy selection. Co-authored-by: Dallin Romney <dallinromney@gmail.com> Co-authored-by: Vincent Koc <vincentkoc@ieee.org> * fix(qa): export private QA harness SDK entries Canonicalize the QA-only plugin SDK entries shared by the private build and package Telegram harness manifest so qa-runtime and qa-lab resolve from trusted dist. * fix(qa): expose private runtime to package harness * fix(qa): surface Telegram observer conflicts * fix(qa): accept separate preview and final messages * test(qa): exercise Telegram poll delay contract --------- Co-authored-by: Peter Steinberger <steipete@gmail.com> Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
248 lines
8.4 KiB
TypeScript
248 lines
8.4 KiB
TypeScript
// Telegram package Docker harness.
|
|
// Runs QA live transport code against the package candidate installed in Docker.
|
|
|
|
import { randomUUID } from "node:crypto";
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import type { QaProviderMode } from "../../extensions/qa-lab/src/run-config.ts";
|
|
import type { QaSuiteRoundTripProbe } from "../../extensions/qa-lab/src/suite-round-trip.ts";
|
|
|
|
function parseBoolean(value: string | undefined) {
|
|
const normalized = value?.trim().toLowerCase();
|
|
return normalized === "1" || normalized === "true" || normalized === "yes";
|
|
}
|
|
|
|
function splitCsv(value: string | undefined) {
|
|
return (value ?? "")
|
|
.split(",")
|
|
.map((entry) => entry.trim())
|
|
.filter((entry) => entry.length > 0);
|
|
}
|
|
|
|
function parsePositiveIntegerEnv(env: NodeJS.ProcessEnv, name: string) {
|
|
const raw = env[name]?.trim();
|
|
if (!raw) {
|
|
return undefined;
|
|
}
|
|
if (!/^\d+$/u.test(raw)) {
|
|
throw new Error(`invalid ${name}: ${raw}`);
|
|
}
|
|
const value = Number(raw);
|
|
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
throw new Error(`invalid ${name}: ${raw}`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function resolveCredentialSource(env: NodeJS.ProcessEnv) {
|
|
return env.OPENCLAW_NPM_TELEGRAM_CREDENTIAL_SOURCE ?? env.OPENCLAW_QA_CREDENTIAL_SOURCE;
|
|
}
|
|
|
|
function resolveCredentialRole(env: NodeJS.ProcessEnv) {
|
|
return env.OPENCLAW_NPM_TELEGRAM_CREDENTIAL_ROLE ?? env.OPENCLAW_QA_CREDENTIAL_ROLE;
|
|
}
|
|
|
|
function createRunId() {
|
|
return `${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
|
|
}
|
|
|
|
function resolvePackageTelegramOutputDir(env: NodeJS.ProcessEnv, repoRoot: string) {
|
|
return (
|
|
env.OPENCLAW_NPM_TELEGRAM_OUTPUT_DIR?.trim() ||
|
|
path.join(repoRoot, ".artifacts", "qa-e2e", `npm-telegram-live-${createRunId()}`)
|
|
);
|
|
}
|
|
|
|
const DEFAULT_RTT_CHECK_ID = "channel-canary";
|
|
|
|
function resolveRttOptions(env: NodeJS.ProcessEnv, selectedScenarioIds: readonly string[] = []) {
|
|
const explicitCheckIds = splitCsv(env.OPENCLAW_NPM_TELEGRAM_RTT_CHECKS);
|
|
const checkIds = explicitCheckIds.length > 0 ? explicitCheckIds : [DEFAULT_RTT_CHECK_ID];
|
|
const unknownCheckIds = checkIds.filter((checkId) => checkId !== DEFAULT_RTT_CHECK_ID);
|
|
if (unknownCheckIds.length > 0) {
|
|
throw new Error(`unknown Telegram QA RTT check: ${unknownCheckIds[0]}`);
|
|
}
|
|
if (
|
|
explicitCheckIds.length === 0 &&
|
|
selectedScenarioIds.length > 0 &&
|
|
!selectedScenarioIds.includes(DEFAULT_RTT_CHECK_ID)
|
|
) {
|
|
return undefined;
|
|
}
|
|
const count = parsePositiveIntegerEnv(env, "OPENCLAW_NPM_TELEGRAM_RTT_SAMPLES") ?? 20;
|
|
return {
|
|
scenarioId: DEFAULT_RTT_CHECK_ID,
|
|
count,
|
|
timeoutMs: parsePositiveIntegerEnv(env, "OPENCLAW_NPM_TELEGRAM_RTT_TIMEOUT_MS") ?? 30_000,
|
|
maxFailures: parsePositiveIntegerEnv(env, "OPENCLAW_NPM_TELEGRAM_RTT_MAX_FAILURES") ?? count,
|
|
};
|
|
}
|
|
|
|
function createRoundTripProbe(
|
|
options: ReturnType<typeof resolveRttOptions>,
|
|
): QaSuiteRoundTripProbe | undefined {
|
|
if (!options) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
...options,
|
|
markerPrefix: "QA-TELEGRAM-RTT",
|
|
input: {
|
|
conversation: { id: "telegram-rtt-room", kind: "group" },
|
|
senderId: "qa-rtt-driver",
|
|
senderName: "QA RTT Driver",
|
|
},
|
|
textPrefix: "@openclaw Telegram RTT check. Reply exactly: ",
|
|
chainReplies: true,
|
|
};
|
|
}
|
|
|
|
function prioritizeRoundTripProbeScenario(
|
|
scenarioIds: readonly string[],
|
|
options: ReturnType<typeof resolveRttOptions>,
|
|
) {
|
|
if (!options) {
|
|
return [...scenarioIds];
|
|
}
|
|
return [
|
|
options.scenarioId,
|
|
...scenarioIds.filter((scenarioId) => scenarioId !== options.scenarioId),
|
|
];
|
|
}
|
|
|
|
async function shouldFailPackageTelegramRun(
|
|
result: { summaryPath: string },
|
|
env: NodeJS.ProcessEnv = process.env,
|
|
) {
|
|
if (parseBoolean(env.OPENCLAW_NPM_TELEGRAM_ALLOW_FAILURES)) {
|
|
return false;
|
|
}
|
|
const { readQaSuiteFailedOrSkippedScenarioCountFromFile } =
|
|
await import("../../extensions/qa-lab/src/suite-summary.ts");
|
|
return (await readQaSuiteFailedOrSkippedScenarioCountFromFile(result.summaryPath)) > 0;
|
|
}
|
|
|
|
async function resolveTrustedOpenClawCommand(
|
|
rawCommand: string,
|
|
env: NodeJS.ProcessEnv = process.env,
|
|
) {
|
|
if (!path.isAbsolute(rawCommand)) {
|
|
throw new Error("OPENCLAW_NPM_TELEGRAM_SUT_COMMAND must be an absolute path.");
|
|
}
|
|
const commandName = path.basename(rawCommand);
|
|
if (commandName !== "openclaw" && commandName !== "openclaw.cmd") {
|
|
throw new Error(
|
|
`OPENCLAW_NPM_TELEGRAM_SUT_COMMAND must point to openclaw; got: ${commandName}`,
|
|
);
|
|
}
|
|
const npmPrefix = env.NPM_CONFIG_PREFIX?.trim();
|
|
if (!npmPrefix) {
|
|
throw new Error("Missing NPM_CONFIG_PREFIX for installed openclaw command validation.");
|
|
}
|
|
const [realCommand, realPrefix] = await Promise.all([
|
|
fs.realpath(rawCommand),
|
|
fs.realpath(npmPrefix),
|
|
]);
|
|
if (realCommand !== realPrefix && !realCommand.startsWith(`${realPrefix}${path.sep}`)) {
|
|
throw new Error("OPENCLAW_NPM_TELEGRAM_SUT_COMMAND must resolve inside NPM_CONFIG_PREFIX.");
|
|
}
|
|
return {
|
|
executablePath: rawCommand,
|
|
usePackagedPlugins: true,
|
|
} as const;
|
|
}
|
|
|
|
async function main() {
|
|
const [
|
|
{ runQaTelegramSuite },
|
|
{ resolveTelegramQaScenarioIds },
|
|
{ DEFAULT_QA_LIVE_PROVIDER_MODE },
|
|
] = await Promise.all([
|
|
import("../../extensions/qa-lab/src/live-transports/telegram/cli.runtime.ts"),
|
|
import("../../extensions/qa-lab/src/live-transports/telegram/scenario-selection.ts"),
|
|
import("../../extensions/qa-lab/src/providers/index.ts"),
|
|
]);
|
|
const rawSutOpenClawCommand = process.env.OPENCLAW_NPM_TELEGRAM_SUT_COMMAND?.trim();
|
|
if (!rawSutOpenClawCommand) {
|
|
throw new Error("Missing OPENCLAW_NPM_TELEGRAM_SUT_COMMAND.");
|
|
}
|
|
const sutOpenClawCommand = await resolveTrustedOpenClawCommand(rawSutOpenClawCommand);
|
|
|
|
const repoRoot = path.resolve(process.env.OPENCLAW_NPM_TELEGRAM_REPO_ROOT ?? process.cwd());
|
|
const outputDir = resolvePackageTelegramOutputDir(process.env, repoRoot);
|
|
const scenarioIds = splitCsv(process.env.OPENCLAW_NPM_TELEGRAM_SCENARIOS);
|
|
const providerMode =
|
|
(process.env.OPENCLAW_NPM_TELEGRAM_PROVIDER_MODE as QaProviderMode | undefined) ??
|
|
DEFAULT_QA_LIVE_PROVIDER_MODE;
|
|
const primaryModel = process.env.OPENCLAW_NPM_TELEGRAM_MODEL;
|
|
const resolvedScenarioIds = resolveTelegramQaScenarioIds({
|
|
providerMode,
|
|
primaryModel,
|
|
scenarioIds,
|
|
});
|
|
const rttOptions = resolveRttOptions(process.env, scenarioIds);
|
|
const result = await runQaTelegramSuite({
|
|
allowFailures: true,
|
|
failFast: true,
|
|
repoRoot,
|
|
outputDir,
|
|
sutOpenClawCommand,
|
|
providerMode,
|
|
primaryModel,
|
|
alternateModel: process.env.OPENCLAW_NPM_TELEGRAM_ALT_MODEL,
|
|
fastMode: parseBoolean(process.env.OPENCLAW_NPM_TELEGRAM_FAST),
|
|
scenarioIds,
|
|
resolvedScenarioIds: prioritizeRoundTripProbeScenario(resolvedScenarioIds, rttOptions),
|
|
roundTripProbe: createRoundTripProbe(rttOptions),
|
|
sutAccountId: process.env.OPENCLAW_NPM_TELEGRAM_SUT_ACCOUNT,
|
|
credentialSource: resolveCredentialSource(process.env),
|
|
credentialRole: resolveCredentialRole(process.env),
|
|
});
|
|
if (!result) {
|
|
throw new Error("Package Telegram QA did not produce suite artifacts.");
|
|
}
|
|
|
|
process.stdout.write(`Package Telegram QA report: ${result.reportPath}\n`);
|
|
process.stdout.write(`Package Telegram QA summary: ${result.summaryPath}\n`);
|
|
if (await shouldFailPackageTelegramRun(result)) {
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
|
|
async function formatRunnerErrorMessage(error: unknown) {
|
|
try {
|
|
// Widen the specifier so the source-only test-root program does not try to
|
|
// resolve dist (TS2307); the docker-e2e boundary guard requires importing
|
|
// built dist here, so the cast stays structural instead of a src reference.
|
|
const distErrorsPath = "../../dist/infra/errors.js" as string;
|
|
const { formatErrorMessage } = (await import(distErrorsPath)) as {
|
|
formatErrorMessage: (err: unknown) => string;
|
|
};
|
|
return formatErrorMessage(error);
|
|
} catch {
|
|
return error instanceof Error ? error.message : String(error);
|
|
}
|
|
}
|
|
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
main().catch(async (error: unknown) => {
|
|
process.stderr.write(
|
|
`package telegram live e2e failed: ${await formatRunnerErrorMessage(error)}\n`,
|
|
);
|
|
process.exitCode = 1;
|
|
});
|
|
}
|
|
|
|
export const testing = {
|
|
parsePositiveIntegerEnv,
|
|
resolvePackageTelegramOutputDir,
|
|
resolveCredentialRole,
|
|
resolveCredentialSource,
|
|
createRoundTripProbe,
|
|
prioritizeRoundTripProbeScenario,
|
|
resolveRttOptions,
|
|
resolveTrustedOpenClawCommand,
|
|
shouldFailPackageTelegramRun,
|
|
};
|