Spaces:
Configuration error
Configuration error
| 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; | |
| }); | |