feat(qa): publish Mantis desktop screenshots

This commit is contained in:
Peter Steinberger
2026-05-04 01:48:11 +01:00
parent ecab09870a
commit 4856cbb017
6 changed files with 164 additions and 6 deletions

View File

@@ -225,6 +225,8 @@ describe("qa cli registration", () => {
".artifacts/qa-e2e/mantis/desktop-browser",
"--browser-url",
"https://openclaw.ai/docs",
"--html-file",
"qa-artifacts/timeline.html",
"--crabbox-bin",
"/tmp/crabbox",
"--provider",
@@ -243,6 +245,7 @@ describe("qa cli registration", () => {
expect(runMantisDesktopBrowserSmokeCommand).toHaveBeenCalledWith({
browserUrl: "https://openclaw.ai/docs",
crabboxBin: "/tmp/crabbox",
htmlFile: "qa-artifacts/timeline.html",
idleTimeout: "30m",
keepLease: true,
leaseId: "cbx_123abc",
@@ -268,6 +271,7 @@ describe("qa cli registration", () => {
expect(runMantisDesktopBrowserSmokeCommand).toHaveBeenCalledWith({
browserUrl: undefined,
crabboxBin: undefined,
htmlFile: undefined,
idleTimeout: undefined,
keepLease: undefined,
leaseId: undefined,

View File

@@ -56,6 +56,7 @@ type MantisDesktopBrowserSmokeCommanderOptions = {
browserUrl?: string;
class?: string;
crabboxBin?: string;
htmlFile?: string;
idleTimeout?: string;
keepLease?: boolean;
leaseId?: string;
@@ -137,6 +138,7 @@ export function registerMantisCli(qa: Command) {
.option("--repo-root <path>", "Repository root to target when running from a neutral cwd")
.option("--output-dir <path>", "Mantis desktop browser artifact directory")
.option("--browser-url <url>", "URL to open in the visible browser")
.option("--html-file <path>", "Repo-local HTML file to render in the visible browser")
.option("--crabbox-bin <path>", "Crabbox binary path")
.option("--provider <provider>", "Crabbox provider")
.option("--machine-class <class>", "Crabbox machine class")
@@ -149,6 +151,7 @@ export function registerMantisCli(qa: Command) {
await runDesktopBrowserSmoke({
browserUrl: opts.browserUrl,
crabboxBin: opts.crabboxBin,
htmlFile: opts.htmlFile,
idleTimeout: opts.idleTimeout,
keepLease: opts.keepLease,
leaseId: opts.leaseId,

View File

@@ -16,6 +16,8 @@ describe("mantis desktop browser smoke runtime", () => {
});
it("leases a desktop box, runs a visible browser, copies artifacts, and stops on pass", async () => {
await fs.mkdir(path.join(repoRoot, "qa-artifacts"), { recursive: true });
await fs.writeFile(path.join(repoRoot, "qa-artifacts", "timeline.html"), "<h1>Mantis</h1>");
const commands: { args: readonly string[]; command: string }[] = [];
const runner = vi.fn(async (command: string, args: readonly string[]) => {
commands.push({ command, args });
@@ -53,6 +55,7 @@ describe("mantis desktop browser smoke runtime", () => {
browserUrl: "https://openclaw.ai/docs",
commandRunner: runner,
crabboxBin: "/tmp/crabbox",
htmlFile: "qa-artifacts/timeline.html",
now: () => new Date("2026-05-04T12:00:00.000Z"),
outputDir: ".artifacts/qa-e2e/mantis/desktop-browser-test",
repoRoot,
@@ -81,15 +84,19 @@ describe("mantis desktop browser smoke runtime", () => {
expect(remoteScript).toContain("${BROWSER:-}");
expect(remoteScript).toContain("${CHROME_BIN:-}");
expect(remoteScript).toContain("chromium-browser");
expect(remoteScript).toContain("base64 -d");
expect(remoteScript).toContain('url="file://$out/input.html"');
expect(remoteScript).toContain('"browserBinary": "$browser_bin"');
await expect(fs.readFile(result.screenshotPath ?? "", "utf8")).resolves.toBe("png");
const summary = JSON.parse(await fs.readFile(result.summaryPath, "utf8")) as {
browserUrl: string;
crabbox: { id: string; vncCommand: string };
htmlFile?: string;
status: string;
};
expect(summary.browserUrl).toMatch(/^file:\/\//u);
expect(summary).toMatchObject({
browserUrl: "https://openclaw.ai/docs",
htmlFile: path.join(repoRoot, "qa-artifacts", "timeline.html"),
crabbox: {
id: "cbx_abc123",
vncCommand: "/tmp/crabbox vnc --provider hetzner --id cbx_abc123 --open",
@@ -98,6 +105,21 @@ describe("mantis desktop browser smoke runtime", () => {
});
});
it("rejects html files outside the repository", async () => {
const runner = vi.fn(async () => ({ stdout: "", stderr: "" }));
await expect(
runMantisDesktopBrowserSmoke({
commandRunner: runner,
crabboxBin: "/tmp/crabbox",
htmlFile: "../outside.html",
outputDir: ".artifacts/qa-e2e/mantis/desktop-browser-outside",
repoRoot,
}),
).rejects.toThrow("Mantis desktop HTML file must be inside the repository");
expect(runner).not.toHaveBeenCalled();
});
it("keeps an existing lease and writes failure reports when the remote run fails", async () => {
const commands: { args: readonly string[]; command: string }[] = [];
const runner = vi.fn(async (command: string, args: readonly string[]) => {

View File

@@ -1,6 +1,7 @@
import { spawn, type SpawnOptions } from "node:child_process";
import fs from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { ensureRepoBoundDirectory, resolveRepoRelativeOutputDir } from "../cli-paths.js";
@@ -9,6 +10,7 @@ export type MantisDesktopBrowserSmokeOptions = {
commandRunner?: CommandRunner;
crabboxBin?: string;
env?: NodeJS.ProcessEnv;
htmlFile?: string;
idleTimeout?: string;
keepLease?: boolean;
leaseId?: string;
@@ -58,6 +60,7 @@ type MantisDesktopBrowserSmokeSummary = {
summaryPath: string;
};
browserUrl: string;
htmlFile?: string;
crabbox: {
bin: string;
createdLease: boolean;
@@ -174,16 +177,43 @@ function shellQuote(value: string) {
return `'${value.replaceAll("'", "'\\''")}'`;
}
function renderRemoteScript(params: { browserUrl: string; remoteOutputDir: string }) {
function resolveRepoBoundFile(repoRoot: string, filePath: string, label: string) {
const resolved = path.resolve(repoRoot, filePath);
const relative = path.relative(repoRoot, resolved);
if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative)) {
throw new Error(`${label} must be inside the repository: ${filePath}`);
}
return resolved;
}
function renderRemoteScript(params: {
browserUrl: string;
htmlBase64?: string;
remoteOutputDir: string;
}) {
const shellUrl = shellQuote(params.browserUrl);
const shellUrlJson = shellQuote(JSON.stringify(params.browserUrl));
const htmlBase64 = shellQuote(params.htmlBase64 ?? "");
const shellOutputDir = shellQuote(params.remoteOutputDir);
const inputModeJson = shellQuote(JSON.stringify(params.htmlBase64 ? "html-file" : "url"));
const openedUrlJson = shellQuote(
JSON.stringify(
params.htmlBase64 ? `file://${params.remoteOutputDir}/input.html` : params.browserUrl,
),
);
return `set -euo pipefail
out=${shellOutputDir}
url=${shellUrl}
url_json=${shellUrlJson}
html_b64=${htmlBase64}
input_mode_json=${inputModeJson}
opened_url_json=${openedUrlJson}
rm -rf "$out"
mkdir -p "$out"
if [ -n "$html_b64" ]; then
printf '%s' "$html_b64" | base64 -d >"$out/input.html"
url="file://$out/input.html"
fi
export DISPLAY="\${DISPLAY:-:99}"
if ! command -v scrot >/dev/null 2>&1; then
sudo apt-get update -y >"$out/apt.log" 2>&1
@@ -228,6 +258,8 @@ cat >"$out/remote-metadata.json" <<MANTIS_REMOTE_METADATA
"browserBinary": "$browser_bin",
"display": "$DISPLAY",
"chromePid": $chrome_pid,
"inputMode": $input_mode_json,
"openedUrl": $opened_url_json,
"capturedAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
MANTIS_REMOTE_METADATA
@@ -241,6 +273,7 @@ function renderReport(summary: MantisDesktopBrowserSmokeSummary) {
"",
`Status: ${summary.status}`,
`Browser URL: ${summary.browserUrl}`,
summary.htmlFile ? `HTML file: ${summary.htmlFile}` : undefined,
`Output: ${summary.outputDir}`,
`Started: ${summary.startedAt}`,
`Finished: ${summary.finishedAt}`,
@@ -412,7 +445,16 @@ export async function runMantisDesktopBrowserSmoke(
trimToValue(env[CRABBOX_IDLE_TIMEOUT_ENV]) ??
DEFAULT_IDLE_TIMEOUT;
const ttl = trimToValue(opts.ttl) ?? trimToValue(env[CRABBOX_TTL_ENV]) ?? DEFAULT_TTL;
const browserUrl = trimToValue(opts.browserUrl) ?? DEFAULT_BROWSER_URL;
const htmlFileOption = trimToValue(opts.htmlFile);
const htmlFile = htmlFileOption
? resolveRepoBoundFile(repoRoot, htmlFileOption, "Mantis desktop HTML file")
: undefined;
const htmlBase64 = htmlFile
? Buffer.from(await fs.readFile(htmlFile)).toString("base64")
: undefined;
const browserUrl = htmlFile
? pathToFileURL(htmlFile).toString()
: (trimToValue(opts.browserUrl) ?? DEFAULT_BROWSER_URL);
const runner = opts.commandRunner ?? defaultCommandRunner;
const explicitLeaseId = trimToValue(opts.leaseId) ?? trimToValue(env[CRABBOX_LEASE_ID_ENV]);
const keepLease = opts.keepLease ?? isTruthyOptIn(env[CRABBOX_KEEP_ENV]);
@@ -455,7 +497,7 @@ export async function runMantisDesktopBrowserSmoke(
"--no-sync",
"--shell",
"--",
renderRemoteScript({ browserUrl, remoteOutputDir }),
renderRemoteScript({ browserUrl, htmlBase64, remoteOutputDir }),
],
cwd: repoRoot,
runner,
@@ -479,6 +521,7 @@ export async function runMantisDesktopBrowserSmoke(
summaryPath,
},
browserUrl,
htmlFile,
crabbox: {
bin: crabboxBin,
createdLease,
@@ -508,6 +551,7 @@ export async function runMantisDesktopBrowserSmoke(
summaryPath,
},
browserUrl,
htmlFile,
crabbox: {
bin: crabboxBin,
createdLease,