#!/usr/bin/env node
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 renderScenarioList(summary) {
const scenarios = Array.isArray(summary.scenarios) ? summary.scenarios : [];
if (scenarios.length === 0) {
return "
No scenarios recorded.";
}
return scenarios
.map((scenario) => {
const statusClass = scenario.status === "pass" ? "pass" : "fail";
const rtt = typeof scenario.rttMs === "number" ? `, ${Math.round(scenario.rttMs)}ms RTT` : "";
return `${escapeHtml(scenario.status ?? "unknown")} ${escapeHtml(scenario.title ?? scenario.id)} ${escapeHtml(scenario.id ?? "")}${rtt}${escapeHtml(scenario.details ?? "")}
`;
})
.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");
}
export function renderTelegramEvidenceHtml({ observedMessages, summary }) {
const counts = summary.counts ?? {};
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,
scenarioLabel,
summary,
}) {
const counts = summary.counts ?? {};
const pass = counts.failed === 0 && Number(counts.total ?? 0) > 0;
const scenarioNames = Array.isArray(summary.scenarios)
? summary.scenarios.map((scenario) => scenario.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 summary",
path: "telegram-qa-summary.json",
targetPath: "summary.json",
},
{
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 summaryPath = path.join(outputDir, "telegram-qa-summary.json");
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 summary: ${summaryPath}`);
}
if (!existsSync(observedPath)) {
throw new Error(`Missing Telegram observed messages: ${observedPath}`);
}
if (!existsSync(reportPath)) {
writeFileSync(reportPath, "# Mantis Telegram Live QA\n\nTelegram QA report was unavailable.\n");
}
const summary = readJson(summaryPath);
const observedMessages = 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,
scenarioLabel: args.scenario_label,
summary,
});
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);
}
}