#!/usr/bin/env node import { spawn } from "node:child_process"; import { readFile, writeFile, mkdir } from "node:fs/promises"; import { tmpdir } from "node:os"; import { dirname, resolve } from "node:path"; import { sourceFingerprint } from "./source-fingerprint.mjs"; const spaceId = process.env.BROWSER_SPEAK_SPACE_ID ?? "Mike0021/browser-speak"; const hostedUrl = normalizeUrl( process.env.BROWSER_SPEAK_HOSTED_URL ?? "https://mike0021-browser-speak.static.hf.space/", ); const resultPath = resolve(process.env.BROWSER_SPEAK_HOSTED_SMOKE_JSON ?? `${tmpdir()}/browser-speak-hosted-smoke.json`); const uiJson = resolve(process.env.BROWSER_SPEAK_HOSTED_UI_JSON ?? `${tmpdir()}/browser-speak-hosted-ui-smoke.json`); const uiScreenshotDir = resolve( process.env.BROWSER_SPEAK_HOSTED_UI_SCREENSHOT_DIR ?? `${tmpdir()}/browser-speak-hosted-ui-smoke`, ); const clientSideJson = resolve( process.env.BROWSER_SPEAK_HOSTED_CLIENT_SIDE_JSON ?? `${tmpdir()}/browser-speak-hosted-client-side-smoke.json`, ); const evidenceExportJson = resolve( process.env.BROWSER_SPEAK_HOSTED_EVIDENCE_EXPORT_JSON ?? `${tmpdir()}/browser-speak-hosted-evidence-export-smoke.json`, ); const clientSideProfileDir = resolve( process.env.BROWSER_SPEAK_HOSTED_CLIENT_SIDE_PROFILE_DIR ?? `${tmpdir()}/browser-speak-hosted-client-side-profile`, ); const skipUi = process.env.BROWSER_SPEAK_HOSTED_SKIP_UI === "true"; const skipEvidenceExport = process.env.BROWSER_SPEAK_HOSTED_SKIP_EVIDENCE_EXPORT === "true"; const skipClientSide = process.env.BROWSER_SPEAK_HOSTED_SKIP_CLIENT_SIDE === "true"; const skipHfInfo = process.env.BROWSER_SPEAK_HOSTED_SKIP_HF_INFO === "true"; async function main() { await mkdir(dirname(resultPath), { recursive: true }); const steps = []; const hfInfo = skipHfInfo ? null : await runJsonCommand("hf", ["spaces", "info", spaceId], { label: "HF Space info", }).catch((error) => ({ error: error.message })); const hubApiInfo = skipHfInfo ? null : await fetchHubApiInfo().catch((error) => ({ error: error.message })); const hostHead = await fetchHostHead().catch((error) => ({ ok: false, error: error.message })); if (!skipUi) { steps.push( await runStep("hosted UI smoke", process.execPath, ["tools/run-ui-smoke.mjs"], { BROWSER_SPEAK_URL: hostedUrl, BROWSER_SPEAK_UI_JSON: uiJson, BROWSER_SPEAK_UI_SCREENSHOT_DIR: uiScreenshotDir, }), ); } if (!skipEvidenceExport) { steps.push( await runStep("hosted evidence export smoke", process.execPath, ["tools/run-evidence-export-smoke.mjs"], { BROWSER_SPEAK_URL: hostedUrl, BROWSER_SPEAK_EVIDENCE_EXPORT_JSON: evidenceExportJson, }), ); } if (!skipClientSide) { steps.push( await runStep("hosted client-side/no-server smoke", process.execPath, ["tools/run-client-side-smoke.mjs"], { BROWSER_SPEAK_URL: hostedUrl, BROWSER_SPEAK_CLIENT_SIDE_JSON: clientSideJson, BROWSER_SPEAK_CLIENT_SIDE_PROFILE_DIR: clientSideProfileDir, }), ); } const ui = skipUi ? null : await readJsonIfExists(uiJson); const evidenceExport = skipEvidenceExport ? null : await readJsonIfExists(evidenceExportJson); const clientSide = skipClientSide ? null : await readJsonIfExists(clientSideJson); const spaceShas = [hfInfo?.sha, hubApiInfo?.sha].filter(Boolean); const commitMatch = spaceShas.length > 0 && hostHead?.hfSpaceCommit ? spaceShas.every((sha) => sha === hostHead.hfSpaceCommit) : hfInfo?.error && hubApiInfo?.error ? false : null; const cardData = hubApiInfo?.cardData ?? hfInfo?.card_data ?? null; const spaceConfigParsed = hubApiInfo?.error || skipHfInfo ? null : cardData?.sdk === "static" && Boolean(cardData?.app_file); const noServerSmoke = clientSide == null ? null : Boolean( clientSide.passed && Array.isArray(clientSide.suspects) && clientSide.suspects.length === 0 && Array.isArray(clientSide.benchmarkErrors) && clientSide.benchmarkErrors.length === 0, ); const passed = hostHead?.ok !== false && commitMatch !== false && steps.every((step) => step.status === "pass") && (ui == null || ui.passed !== false) && (clientSide == null || noServerSmoke === true); const payload = { generatedAt: new Date().toISOString(), sourceFingerprint: await sourceFingerprint(), passed, spaceId, hostedUrl, hfInfo, hubApiInfo, hostHead, commitMatch, spaceConfigParsed, noServerSmoke, artifacts: { uiJson: skipUi ? null : uiJson, uiScreenshotDir: skipUi ? null : uiScreenshotDir, evidenceExportJson: skipEvidenceExport ? null : evidenceExportJson, clientSideJson: skipClientSide ? null : clientSideJson, }, steps, uiSummary: ui ? summarizeUiSmoke(ui) : null, evidenceExportSummary: evidenceExport ? summarizeEvidenceExportSmoke(evidenceExport) : null, clientSideSummary: clientSide ? summarizeClientSideSmoke(clientSide) : null, }; await writeFile(resultPath, `${JSON.stringify(payload, null, 2)}\n`); console.log(`Wrote hosted smoke JSON: ${resultPath}`); if (hfInfo?.sha) console.log(`HF Space SHA: ${hfInfo.sha}`); if (hubApiInfo?.sha) console.log(`Hub API SHA: ${hubApiInfo.sha}`); if (spaceConfigParsed != null) console.log(`Hub card config: ${spaceConfigParsed ? "parsed" : "missing"}`); if (hostHead?.hfSpaceCommit) console.log(`Static host commit: ${hostHead.hfSpaceCommit}`); if (commitMatch != null) console.log(`Commit match: ${commitMatch ? "yes" : "no"}`); if (noServerSmoke != null) console.log(`No-server smoke: ${noServerSmoke ? "pass" : "fail"}`); if (!passed) process.exitCode = 1; } async function fetchHubApiInfo() { const response = await fetch(`https://huggingface.co/api/spaces/${spaceId}`, { cache: "no-store" }); if (!response.ok) throw new Error(`Hub API returned HTTP ${response.status}`); const payload = await response.json(); return { id: payload.id ?? spaceId, sha: payload.sha ?? "", sdk: payload.sdk ?? "", cardData: payload.cardData ?? null, runtime: payload.runtime ?? null, host: payload.host ?? "", lastModified: payload.lastModified ?? null, }; } async function fetchHostHead() { const response = await fetch(hostedUrl, { method: "HEAD", cache: "no-store" }); return { ok: response.ok, status: response.status, statusText: response.statusText, url: response.url, hfSpaceCommit: response.headers.get("x-repo-commit") ?? "", etag: response.headers.get("etag") ?? "", contentType: response.headers.get("content-type") ?? "", cacheControl: response.headers.get("cache-control") ?? "", }; } async function runStep(label, command, args, env = {}) { console.log(`Running ${label}.`); const startedAt = Date.now(); const result = await runCommand(command, args, { env: { ...process.env, ...env, }, inherit: true, }); return { label, command: [command, ...args], status: result.exitCode === 0 ? "pass" : "fail", exitCode: result.exitCode, durationMs: Date.now() - startedAt, }; } async function runJsonCommand(command, args, { label }) { const result = await runCommand(command, args, { inherit: false }); if (result.exitCode !== 0) { throw new Error(`${label} failed with exit ${result.exitCode}: ${result.stderr || result.stdout}`); } try { return JSON.parse(result.stdout); } catch (error) { throw new Error(`${label} did not return JSON: ${error.message}`); } } function runCommand(command, args, { env = process.env, inherit = false } = {}) { return new Promise((resolvePromise, reject) => { const child = spawn(command, args, { env, stdio: inherit ? "inherit" : ["ignore", "pipe", "pipe"], }); let stdout = ""; let stderr = ""; if (!inherit) { child.stdout.on("data", (chunk) => { stdout += chunk; }); child.stderr.on("data", (chunk) => { stderr += chunk; }); } child.on("error", reject); child.on("exit", (exitCode) => resolvePromise({ exitCode, stdout, stderr })); }); } async function readJsonIfExists(path) { try { return JSON.parse(await readFile(path, "utf8")); } catch { return null; } } function summarizeUiSmoke(ui) { return { passed: ui.passed, url: ui.url, viewports: ui.viewports?.map((viewport) => ({ name: viewport.name, passed: viewport.passed, overflowCount: viewport.overflow?.length ?? 0, })), }; } function summarizeClientSideSmoke(clientSide) { return { passed: clientSide.passed, url: clientSide.url, totalRequestCount: clientSide.summary?.totalRequests ?? clientSide.requests?.length ?? 0, benchmarkRequestCount: clientSide.summary?.benchmarkRequests ?? null, serverInferenceSuspectCount: clientSide.summary?.serverInferenceSuspects ?? clientSide.suspects?.length ?? 0, suspectCount: clientSide.suspects?.length ?? 0, benchmarkErrorCount: clientSide.benchmarkErrors?.length ?? 0, resultKinds: clientSide.benchmarkResults?.map((result) => result.kind) ?? [], browserExport: clientSide.browserExport ?? null, hostMetadata: clientSide.hostMetadata ?? clientSide.browserExport?.hostMetadata ?? null, }; } function summarizeEvidenceExportSmoke(evidenceExport) { return { passed: evidenceExport.passed, url: evidenceExport.url, summary: evidenceExport.summary ?? null, }; } function normalizeUrl(value) { return value.endsWith("/") ? value : `${value}/`; } main().catch((error) => { console.error(error.stack ?? error.message); process.exitCode = 1; });