mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
c23d66e3b5
* refactor: consolidate coercion ownership * test: align shard check with weighted planning * chore: refresh plugin SDK API baseline
271 lines
9.5 KiB
TypeScript
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,
|
|
};
|