browser-speak / tools /run-hosted-smoke.mjs
Mike0021's picture
Add worker network telemetry to browser evidence
d2ae80e verified
Raw
History Blame Contribute Delete
9.75 kB
#!/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;
});