Files
openclaw/scripts/e2e/npm-telegram-live-runner.ts
Peter Steinberger c23d66e3b5 refactor: consolidate coercion ownership (#122692)
* refactor: consolidate coercion ownership

* test: align shard check with weighted planning

* chore: refresh plugin SDK API baseline
2026-08-12 09:25:28 -07:00

271 lines
9.5 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 { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import type { QaProviderMode } from "../../extensions/qa-lab/src/run-config.ts";
import type { QaSuiteRoundTripProbe } from "../../extensions/qa-lab/src/suite-round-trip.ts";
import { normalizeCsvOrLooseStringList } from "../../packages/normalization-core/src/string-normalization.ts";
import { isStrictAffirmativeValue } from "../lib/arg-utils.mts";
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";
const EXTENDED_STABLE_2026_6_35 = "2026.6.35";
function projectExtendedStable2026_6_35QaConfig(cfg: OpenClawConfig): OpenClawConfig {
const { entries, ...agents } = cfg.agents ?? {};
const { mediaModels, ...defaults } = agents.defaults ?? {};
return {
...cfg,
// The frozen candidate validates the pre-entries config shape. Keep this
// projection at the package harness boundary so current runtime stays canonical.
memory: { backend: "builtin" },
plugins: {
...cfg.plugins,
bundledDiscovery: "compat",
},
agents: {
...agents,
defaults: {
...defaults,
...(mediaModels?.image ? { imageGenerationModel: mediaModels.image } : {}),
},
list: Object.entries(entries ?? {}).map(([id, agent]) => Object.assign({ id }, agent)),
},
} as OpenClawConfig;
}
function resolvePackageConfigMutation(env: NodeJS.ProcessEnv = process.env) {
return env.OPENCLAW_NPM_TELEGRAM_PACKAGE_VERSION === EXTENDED_STABLE_2026_6_35
? projectExtendedStable2026_6_35QaConfig
: undefined;
}
function resolveRttOptions(env: NodeJS.ProcessEnv, selectedScenarioIds: readonly string[] = []) {
const explicitCheckIds = normalizeCsvOrLooseStringList(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 (isStrictAffirmativeValue(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 mutateConfig = resolvePackageConfigMutation();
const repoRoot = path.resolve(process.env.OPENCLAW_NPM_TELEGRAM_REPO_ROOT ?? process.cwd());
const outputDir = resolvePackageTelegramOutputDir(process.env, repoRoot);
const scenarioIds = normalizeCsvOrLooseStringList(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: isStrictAffirmativeValue(process.env.OPENCLAW_NPM_TELEGRAM_FAST),
scenarioIds,
resolvedScenarioIds: prioritizeRoundTripProbeScenario(resolvedScenarioIds, rttOptions),
roundTripProbe: createRoundTripProbe(rttOptions),
...(mutateConfig ? { mutateConfig } : {}),
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 = {
resolvePackageTelegramOutputDir,
resolveCredentialRole,
resolveCredentialSource,
createRoundTripProbe,
prioritizeRoundTripProbeScenario,
resolveRttOptions,
resolveTrustedOpenClawCommand,
shouldFailPackageTelegramRun,
};