#!/usr/bin/env node
// Builds an HTML/manifest evidence bundle from Telegram QA scenario summaries.
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
function parseArgs(argv) {
const args = {};
for (let index = 0; index < argv.length; index += 1) {
const key = argv[index];
if (!key.startsWith("--")) {
throw new Error(`Unexpected argument: ${key}`);
}
const name = key.slice(2).replaceAll("-", "_");
const value = argv[index + 1];
if (!value || value.startsWith("--")) {
throw new Error(`Missing value for ${key}`);
}
args[name] = value;
index += 1;
}
return args;
}
function readJson(filePath) {
return JSON.parse(readFileSync(filePath, "utf8"));
}
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
function formatMessageText(message) {
const text = typeof message.text === "string" ? message.text : "";
const caption = typeof message.caption === "string" ? message.caption : "";
const content = text || caption || "";
if (content.trim()) {
return content;
}
const mediaKinds = Array.isArray(message.mediaKinds) ? message.mediaKinds : [];
return mediaKinds.length > 0 ? `[${mediaKinds.join(", ")}]` : "[no text]";
}
function evidenceEntries(summary) {
return Array.isArray(summary.entries) ? summary.entries : [];
}
function evidenceCounts(summary) {
const entries = evidenceEntries(summary);
const failed = entries.filter((entry) => entry?.result?.status !== "pass").length;
return {
total: entries.length,
passed: entries.length - failed,
failed,
};
}
function evidenceCredentialSource(summary) {
const entry = evidenceEntries(summary)[0];
return (
entry?.execution?.provider?.auth ??
entry?.execution?.provider?.fixture ??
entry?.execution?.packageSource?.kind ??
"unknown"
);
}
// Historical Telegram summary artifacts can still appear in old Mantis uploads.
// Current QA producers write qa-evidence.json for gate inputs.
function legacyTelegramSummaryToEvidenceSummary(summary) {
const scenarios = Array.isArray(summary.scenarios) ? summary.scenarios : [];
return {
kind: "openclaw.qa.evidence-summary",
schemaVersion: 2,
generatedAt: new Date().toISOString(),
entries: scenarios.map((scenario) => ({
test: {
kind: "legacy-telegram-qa-scenario",
id: scenario?.id ?? "legacy-telegram-scenario",
title: scenario?.title ?? scenario?.id ?? "Legacy Telegram scenario",
},
mapping: {
profile: "release",
coverage: [],
},
execution: {
runner: "legacy-telegram-qa",
environment: {
ref: null,
os: "unknown",
nodeVersion: "unknown",
},
provider: {
id: "unknown",
live: true,
model: { name: null, ref: null },
auth: summary.credentials?.source ?? "unknown",
},
channel: {
id: "telegram",
live: true,
driver: "native",
},
packageSource: { kind: "unknown" },
artifacts: [
{
kind: "summary",
path: "telegram-qa-summary.json",
source: "legacy-telegram-qa",
},
],
},
result: {
status: scenario?.status === "skip" ? "skipped" : (scenario?.status ?? "fail"),
...(scenario?.status === "pass"
? {}
: { failure: { reason: scenario?.details ?? "legacy scenario did not pass" } }),
...(typeof scenario?.rttMs === "number" ? { timing: { rttMs: scenario.rttMs } } : {}),
},
})),
};
}
function renderScenarioList(summary) {
const entries = evidenceEntries(summary);
if (entries.length === 0) {
return "
No evidence entries recorded.";
}
return entries
.map((entry) => {
const status = entry?.result?.status ?? "unknown";
const statusClass = status === "pass" ? "pass" : "fail";
const timing = entry?.result?.timing;
const rttMs = timing?.p50Ms ?? timing?.rttMs;
const rtt = typeof rttMs === "number" ? `, ${Math.round(rttMs)}ms RTT` : "";
return `${escapeHtml(status)} ${escapeHtml(entry?.test?.title ?? entry?.test?.id)} ${escapeHtml(entry?.test?.id ?? "")}${rtt}${escapeHtml(entry?.result?.failure?.reason ?? "")}
`;
})
.join("\n");
}
function renderObservedMessages(observedMessages) {
if (!Array.isArray(observedMessages) || observedMessages.length === 0) {
return 'No observed Telegram messages were recorded.
';
}
return observedMessages
.map((message, index) => {
const sender = message.senderIsBot ? "bot" : "user";
const scenario = message.scenarioTitle ?? message.scenarioId ?? "";
const text = formatMessageText(message);
const buttons = Array.isArray(message.inlineButtons)
? message.inlineButtons
: typeof message.inlineButtonCount === "number" && message.inlineButtonCount > 0
? [`${message.inlineButtonCount} inline button(s)`]
: [];
return [
``,
` #${index + 1}${escapeHtml(sender)}${scenario ? `${escapeHtml(scenario)}` : ""}
`,
` ${escapeHtml(text)}`,
buttons.length > 0
? ` ${buttons.map((button) => `${escapeHtml(button)}`).join("")}
`
: "",
"",
]
.filter(Boolean)
.join("\n");
})
.join("\n");
}
/**
* Renders a self-contained Telegram evidence HTML report.
*/
export function renderTelegramEvidenceHtml({ observedMessages, summary }) {
const counts = evidenceCounts(summary);
const pass = counts.failed === 0 && Number(counts.total ?? 0) > 0;
return `
Mantis Telegram Live Evidence
Scenarios
${renderScenarioList(summary)}
Observed Telegram Messages
${renderObservedMessages(observedMessages)}
`;
}
export function buildTelegramEvidenceManifest({
candidateRef,
candidateSha,
hasObservedMessages = true,
scenarioLabel,
summary,
summaryArtifactPath = "qa-evidence.json",
}) {
const counts = evidenceCounts(summary);
const pass = counts.failed === 0 && Number(counts.total ?? 0) > 0;
const scenarioNames = evidenceEntries(summary)
.map((entry) => entry?.test?.id)
.filter(Boolean);
const scenario = scenarioLabel || scenarioNames.join(",") || "telegram-live";
const status = pass ? "pass" : "fail";
const artifacts = [
{
kind: "desktopScreenshot",
lane: "candidate",
label: "Telegram live transcript",
path: "telegram-live-desktop.png",
targetPath: "telegram-live-desktop.png",
alt: "Rendered Telegram live transcript in a Crabbox desktop browser",
width: 720,
inline: true,
required: false,
},
{
kind: "motionPreview",
lane: "candidate",
label: "Telegram motion preview",
path: "telegram-live-preview.gif",
targetPath: "telegram-live-preview.gif",
alt: "Animated Telegram live transcript capture",
width: 720,
inline: true,
required: false,
},
{
kind: "motionClip",
lane: "candidate",
label: "Telegram change MP4",
path: "telegram-live-change.mp4",
targetPath: "telegram-live-change.mp4",
required: false,
},
{
kind: "fullVideo",
lane: "candidate",
label: "Telegram desktop MP4",
path: "telegram-live.mp4",
targetPath: "telegram-live.mp4",
required: false,
},
{
kind: "metadata",
lane: "run",
label: "Telegram QA evidence summary",
path: summaryArtifactPath,
targetPath: "summary.json",
},
...(hasObservedMessages
? [
{
kind: "metadata",
lane: "run",
label: "Telegram observed messages",
path: "telegram-qa-observed-messages.json",
targetPath: "observed-messages.json",
},
]
: []),
{
kind: "metadata",
lane: "run",
label: "Telegram transcript HTML",
path: "telegram-live-transcript.html",
targetPath: "telegram-live-transcript.html",
},
{
kind: "metadata",
lane: "run",
label: "Telegram preview metadata",
path: "telegram-live-preview.json",
targetPath: "telegram-live-preview.json",
required: false,
},
{
kind: "metadata",
lane: "run",
label: "Telegram QA error",
path: "error.txt",
targetPath: "error.txt",
required: false,
},
{
kind: "report",
lane: "run",
label: "Telegram QA report",
path: "telegram-qa-report.md",
targetPath: "report.md",
},
];
return {
schemaVersion: 1,
id: "telegram-live",
title: "Mantis Telegram Live QA",
summary:
"Mantis ran the Telegram live QA lane with Convex-leased credentials, rendered a redacted transcript in a Crabbox desktop browser, and captured screenshot/video evidence for PR review.",
scenario,
comparison: {
candidate: {
...(candidateSha ? { sha: candidateSha } : {}),
...(candidateRef ? { ref: candidateRef } : {}),
expected: "Telegram live QA scenarios pass",
status,
fixed: pass,
},
pass,
},
artifacts,
};
}
export function writeTelegramEvidence(rawArgs = process.argv.slice(2)) {
const args = parseArgs(rawArgs);
if (!args.output_dir) {
throw new Error("Missing --output-dir.");
}
const outputDir = path.resolve(args.output_dir);
mkdirSync(outputDir, { recursive: true });
const evidenceSummaryPath = path.join(outputDir, "qa-evidence.json");
const legacySummaryPath = path.join(outputDir, "telegram-qa-summary.json");
const usesCurrentEvidenceSummary = existsSync(evidenceSummaryPath);
const summaryPath = usesCurrentEvidenceSummary ? evidenceSummaryPath : legacySummaryPath;
const observedPath = path.join(outputDir, "telegram-qa-observed-messages.json");
const reportPath = path.join(outputDir, "telegram-qa-report.md");
if (!existsSync(summaryPath)) {
throw new Error(`Missing Telegram QA evidence summary: ${evidenceSummaryPath}`);
}
const summary = usesCurrentEvidenceSummary
? readJson(evidenceSummaryPath)
: legacyTelegramSummaryToEvidenceSummary(readJson(legacySummaryPath));
const counts = evidenceCounts(summary);
const pass = counts.failed === 0 && Number(counts.total ?? 0) > 0;
if (!existsSync(reportPath)) {
if (pass) {
throw new Error(`Missing Telegram QA report for passing summary: ${reportPath}`);
}
writeFileSync(reportPath, "# Mantis Telegram Live QA\n\nTelegram QA report was unavailable.\n");
}
const hasLegacyObservedMessages = !usesCurrentEvidenceSummary && existsSync(observedPath);
const observedMessages = hasLegacyObservedMessages ? readJson(observedPath) : [];
const transcriptHtml = renderTelegramEvidenceHtml({ observedMessages, summary });
writeFileSync(path.join(outputDir, "telegram-live-transcript.html"), transcriptHtml, "utf8");
const manifest = buildTelegramEvidenceManifest({
candidateRef: args.candidate_ref,
candidateSha: args.candidate_sha,
hasObservedMessages: hasLegacyObservedMessages,
scenarioLabel: args.scenario_label,
summary,
summaryArtifactPath: path.basename(summaryPath),
});
writeFileSync(
path.join(outputDir, "mantis-evidence.json"),
`${JSON.stringify(manifest, null, 2)}\n`,
"utf8",
);
return {
manifest,
manifestPath: path.join(outputDir, "mantis-evidence.json"),
transcriptPath: path.join(outputDir, "telegram-live-transcript.html"),
};
}
const executedPath = process.argv[1] ? path.resolve(process.argv[1]) : "";
if (executedPath === fileURLToPath(import.meta.url)) {
try {
writeTelegramEvidence();
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}
}