mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-13 00:20:42 +00:00
416 lines
16 KiB
TypeScript
416 lines
16 KiB
TypeScript
import type { Command } from "commander";
|
|
import { createLazyCliRuntimeLoader } from "../live-transports/shared/live-transport-cli.js";
|
|
import type { MantisDesktopBrowserSmokeOptions } from "./desktop-browser-smoke.runtime.js";
|
|
import type { MantisDiscordSmokeOptions } from "./discord-smoke.runtime.js";
|
|
import type { MantisBeforeAfterOptions } from "./run.runtime.js";
|
|
import type {
|
|
MantisSlackDesktopHydrateMode,
|
|
MantisSlackDesktopSmokeOptions,
|
|
} from "./slack-desktop-smoke.runtime.js";
|
|
import type {
|
|
MantisVisualDriverOptions,
|
|
MantisVisualTaskOptions,
|
|
MantisVisualTaskVisionMode,
|
|
} from "./visual-task.runtime.js";
|
|
|
|
type MantisCliRuntime = typeof import("./cli.runtime.js");
|
|
|
|
const loadMantisCliRuntime = createLazyCliRuntimeLoader<MantisCliRuntime>(
|
|
() => import("./cli.runtime.js"),
|
|
);
|
|
|
|
async function runDiscordSmoke(opts: MantisDiscordSmokeOptions) {
|
|
const runtime = await loadMantisCliRuntime();
|
|
await runtime.runMantisDiscordSmokeCommand(opts);
|
|
}
|
|
|
|
async function runBeforeAfter(opts: MantisBeforeAfterOptions) {
|
|
const runtime = await loadMantisCliRuntime();
|
|
await runtime.runMantisBeforeAfterCommand(opts);
|
|
}
|
|
|
|
async function runDesktopBrowserSmoke(opts: MantisDesktopBrowserSmokeOptions) {
|
|
const runtime = await loadMantisCliRuntime();
|
|
await runtime.runMantisDesktopBrowserSmokeCommand(opts);
|
|
}
|
|
|
|
async function runSlackDesktopSmoke(opts: MantisSlackDesktopSmokeOptions) {
|
|
const runtime = await loadMantisCliRuntime();
|
|
await runtime.runMantisSlackDesktopSmokeCommand(opts);
|
|
}
|
|
|
|
async function runVisualDriver(opts: MantisVisualDriverOptions) {
|
|
const runtime = await loadMantisCliRuntime();
|
|
await runtime.runMantisVisualDriverCommand(opts);
|
|
}
|
|
|
|
async function runVisualTask(opts: MantisVisualTaskOptions) {
|
|
const runtime = await loadMantisCliRuntime();
|
|
await runtime.runMantisVisualTaskCommand(opts);
|
|
}
|
|
|
|
type MantisDiscordSmokeCommanderOptions = {
|
|
channelId?: string;
|
|
guildId?: string;
|
|
message?: string;
|
|
outputDir?: string;
|
|
repoRoot?: string;
|
|
skipPost?: boolean;
|
|
tokenFile?: string;
|
|
tokenFileEnv?: string;
|
|
tokenEnv?: string;
|
|
};
|
|
|
|
type MantisBeforeAfterCommanderOptions = {
|
|
baseline?: string;
|
|
candidate?: string;
|
|
credentialRole?: string;
|
|
credentialSource?: string;
|
|
fast?: boolean;
|
|
outputDir?: string;
|
|
providerMode?: string;
|
|
repoRoot?: string;
|
|
scenario?: string;
|
|
skipBuild?: boolean;
|
|
skipInstall?: boolean;
|
|
transport?: string;
|
|
};
|
|
|
|
type MantisDesktopBrowserSmokeCommanderOptions = {
|
|
browserProfileArchiveEnv?: string;
|
|
browserProfileDir?: string;
|
|
browserUrl?: string;
|
|
class?: string;
|
|
crabboxBin?: string;
|
|
htmlFile?: string;
|
|
idleTimeout?: string;
|
|
keepLease?: boolean;
|
|
leaseId?: string;
|
|
machineClass?: string;
|
|
outputDir?: string;
|
|
provider?: string;
|
|
repoRoot?: string;
|
|
ttl?: string;
|
|
videoDuration?: string;
|
|
};
|
|
|
|
type MantisSlackDesktopSmokeCommanderOptions = {
|
|
altModel?: string;
|
|
class?: string;
|
|
crabboxBin?: string;
|
|
credentialRole?: string;
|
|
credentialSource?: string;
|
|
fast?: boolean;
|
|
gatewaySetup?: boolean;
|
|
hydrateMode?: MantisSlackDesktopHydrateMode;
|
|
idleTimeout?: string;
|
|
keepLease?: boolean;
|
|
leaseId?: string;
|
|
machineClass?: string;
|
|
model?: string;
|
|
outputDir?: string;
|
|
provider?: string;
|
|
providerMode?: string;
|
|
repoRoot?: string;
|
|
scenario?: string[];
|
|
slackChannelId?: string;
|
|
slackUrl?: string;
|
|
ttl?: string;
|
|
};
|
|
|
|
type MantisVisualTaskCommanderOptions = {
|
|
browserUrl?: string;
|
|
class?: string;
|
|
crabboxBin?: string;
|
|
duration?: string;
|
|
expectText?: string;
|
|
idleTimeout?: string;
|
|
keepLease?: boolean;
|
|
leaseId?: string;
|
|
machineClass?: string;
|
|
outputDir?: string;
|
|
provider?: string;
|
|
repoRoot?: string;
|
|
settleMs?: string;
|
|
ttl?: string;
|
|
visionMode?: MantisVisualTaskVisionMode;
|
|
visionModel?: string;
|
|
visionPrompt?: string;
|
|
visionTimeoutMs?: string;
|
|
};
|
|
|
|
type MantisVisualDriverCommanderOptions = {
|
|
browserUrl?: string;
|
|
crabboxBin?: string;
|
|
expectText?: string;
|
|
leaseId?: string;
|
|
outputDir?: string;
|
|
provider?: string;
|
|
repoRoot?: string;
|
|
settleMs?: string;
|
|
visionMode?: MantisVisualTaskVisionMode;
|
|
visionModel?: string;
|
|
visionPrompt?: string;
|
|
visionTimeoutMs?: string;
|
|
};
|
|
|
|
function collectString(value: string, previous: string[] = []) {
|
|
return [...previous, value];
|
|
}
|
|
|
|
function parseOptionalInteger(value: string | undefined, label: string) {
|
|
if (value === undefined) {
|
|
return undefined;
|
|
}
|
|
const parsed = Number.parseInt(value, 10);
|
|
if (!Number.isFinite(parsed) || String(parsed) !== value || parsed < 0) {
|
|
throw new Error(`${label} must be a non-negative integer`);
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
export function registerMantisCli(qa: Command) {
|
|
const mantis = qa
|
|
.command("mantis")
|
|
.description("Run Mantis before/after and live-smoke verification flows");
|
|
|
|
mantis
|
|
.command("run")
|
|
.description("Run a Mantis before/after scenario against baseline and candidate refs")
|
|
.requiredOption("--transport <transport>", "Transport to verify; currently only discord")
|
|
.requiredOption("--scenario <id>", "Mantis scenario id to run")
|
|
.requiredOption("--baseline <ref>", "Ref expected to reproduce the bug")
|
|
.requiredOption("--candidate <ref>", "Ref expected to contain the fix")
|
|
.option("--repo-root <path>", "Repository root to target when running from a neutral cwd")
|
|
.option("--output-dir <path>", "Mantis before/after artifact directory")
|
|
.option("--provider-mode <mode>", "QA provider mode", "live-frontier")
|
|
.option("--credential-source <source>", "QA credential source", "convex")
|
|
.option("--credential-role <role>", "QA credential role", "ci")
|
|
.option("--fast", "Enable fast provider mode where supported", true)
|
|
.option("--skip-install", "Skip pnpm install in baseline/candidate worktrees", false)
|
|
.option("--skip-build", "Skip pnpm build in baseline/candidate worktrees", false)
|
|
.action(async (opts: MantisBeforeAfterCommanderOptions) => {
|
|
await runBeforeAfter({
|
|
baseline: opts.baseline,
|
|
candidate: opts.candidate,
|
|
credentialRole: opts.credentialRole,
|
|
credentialSource: opts.credentialSource,
|
|
fastMode: opts.fast,
|
|
outputDir: opts.outputDir,
|
|
providerMode: opts.providerMode,
|
|
repoRoot: opts.repoRoot,
|
|
scenario: opts.scenario,
|
|
skipBuild: opts.skipBuild,
|
|
skipInstall: opts.skipInstall,
|
|
transport: opts.transport,
|
|
});
|
|
});
|
|
|
|
mantis
|
|
.command("discord-smoke")
|
|
.description("Verify the Mantis Discord bot can see the guild/channel, post, and react")
|
|
.option("--repo-root <path>", "Repository root to target when running from a neutral cwd")
|
|
.option("--output-dir <path>", "Mantis Discord smoke artifact directory")
|
|
.option("--guild-id <id>", "Override OPENCLAW_QA_DISCORD_GUILD_ID")
|
|
.option("--channel-id <id>", "Override OPENCLAW_QA_DISCORD_CHANNEL_ID")
|
|
.option("--token-env <name>", "Env var containing the Mantis Discord bot token")
|
|
.option("--token-file <path>", "File containing the Mantis Discord bot token")
|
|
.option("--token-file-env <name>", "Env var containing the Mantis Discord bot token file path")
|
|
.option("--message <text>", "Smoke message to post")
|
|
.option("--skip-post", "Only check Discord API visibility; do not post or react", false)
|
|
.action(async (opts: MantisDiscordSmokeCommanderOptions) => {
|
|
await runDiscordSmoke({
|
|
channelId: opts.channelId,
|
|
guildId: opts.guildId,
|
|
message: opts.message,
|
|
outputDir: opts.outputDir,
|
|
repoRoot: opts.repoRoot,
|
|
skipPost: opts.skipPost,
|
|
tokenFile: opts.tokenFile,
|
|
tokenFileEnv: opts.tokenFileEnv,
|
|
tokenEnv: opts.tokenEnv,
|
|
});
|
|
});
|
|
|
|
mantis
|
|
.command("desktop-browser-smoke")
|
|
.description(
|
|
"Lease or reuse a Crabbox desktop, open a visible browser, and capture VNC desktop screenshot/video artifacts",
|
|
)
|
|
.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(
|
|
"--browser-profile-archive-env <name>",
|
|
"Env var containing a base64 .tgz Chrome profile archive to restore before launch",
|
|
)
|
|
.option(
|
|
"--browser-profile-dir <remote-path>",
|
|
"Remote Chrome user-data-dir path to reuse for browser login state",
|
|
)
|
|
.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")
|
|
.option("--class <class>", "Alias for --machine-class")
|
|
.option("--lease-id <id>", "Reuse an existing Crabbox lease")
|
|
.option("--idle-timeout <duration>", "Crabbox idle timeout")
|
|
.option("--ttl <duration>", "Crabbox maximum lease lifetime")
|
|
.option("--video-duration <seconds>", "Visible desktop recording duration in seconds")
|
|
.option("--keep-lease", "Keep a lease created by this run after a passing smoke")
|
|
.action(async (opts: MantisDesktopBrowserSmokeCommanderOptions) => {
|
|
await runDesktopBrowserSmoke({
|
|
browserProfileArchiveEnv: opts.browserProfileArchiveEnv,
|
|
browserProfileDir: opts.browserProfileDir,
|
|
browserUrl: opts.browserUrl,
|
|
crabboxBin: opts.crabboxBin,
|
|
htmlFile: opts.htmlFile,
|
|
idleTimeout: opts.idleTimeout,
|
|
keepLease: opts.keepLease,
|
|
leaseId: opts.leaseId,
|
|
machineClass: opts.machineClass ?? opts.class,
|
|
outputDir: opts.outputDir,
|
|
provider: opts.provider,
|
|
repoRoot: opts.repoRoot,
|
|
ttl: opts.ttl,
|
|
videoDurationSeconds: parseOptionalInteger(opts.videoDuration, "--video-duration"),
|
|
});
|
|
});
|
|
|
|
mantis
|
|
.command("slack-desktop-smoke")
|
|
.description(
|
|
"Lease or reuse a Crabbox VNC desktop, run Slack QA inside it, open Slack in the browser, and capture screenshot/video artifacts",
|
|
)
|
|
.option("--repo-root <path>", "Repository root to target when running from a neutral cwd")
|
|
.option("--output-dir <path>", "Mantis Slack desktop artifact directory")
|
|
.option("--crabbox-bin <path>", "Crabbox binary path")
|
|
.option("--provider <provider>", "Crabbox provider")
|
|
.option("--machine-class <class>", "Crabbox machine class")
|
|
.option("--class <class>", "Alias for --machine-class")
|
|
.option("--lease-id <id>", "Reuse an existing Crabbox lease")
|
|
.option("--idle-timeout <duration>", "Crabbox idle timeout")
|
|
.option("--ttl <duration>", "Crabbox maximum lease lifetime")
|
|
.option("--keep-lease", "Keep a lease created by this run after a passing smoke")
|
|
.option("--no-keep-lease", "Stop a lease created by this run after a passing smoke")
|
|
.option("--gateway-setup", "Start a persistent OpenClaw Slack gateway inside the VNC VM")
|
|
.option("--slack-url <url>", "Slack web URL to open in the visible browser")
|
|
.option("--slack-channel-id <id>", "Slack channel id for gateway setup allowlist")
|
|
.option("--provider-mode <mode>", "QA provider mode")
|
|
.option("--hydrate-mode <mode>", "Remote hydrate mode: source or prehydrated")
|
|
.option("--model <ref>", "Primary provider/model ref")
|
|
.option("--alt-model <ref>", "Alternate provider/model ref")
|
|
.option(
|
|
"--scenario <id>",
|
|
"Run only the named Slack QA scenario (repeatable)",
|
|
collectString,
|
|
[],
|
|
)
|
|
.option("--credential-source <source>", "Credential source for Slack QA: env or convex")
|
|
.option("--credential-role <role>", "Credential role for convex auth")
|
|
.option("--fast", "Enable provider fast mode where supported")
|
|
.action(async (opts: MantisSlackDesktopSmokeCommanderOptions) => {
|
|
await runSlackDesktopSmoke({
|
|
alternateModel: opts.altModel,
|
|
crabboxBin: opts.crabboxBin,
|
|
credentialRole: opts.credentialRole,
|
|
credentialSource: opts.credentialSource,
|
|
fastMode: opts.fast,
|
|
gatewaySetup: opts.gatewaySetup,
|
|
hydrateMode: opts.hydrateMode,
|
|
idleTimeout: opts.idleTimeout,
|
|
keepLease: opts.keepLease,
|
|
leaseId: opts.leaseId,
|
|
machineClass: opts.machineClass ?? opts.class,
|
|
outputDir: opts.outputDir,
|
|
primaryModel: opts.model,
|
|
provider: opts.provider,
|
|
providerMode: opts.providerMode,
|
|
repoRoot: opts.repoRoot,
|
|
scenarioIds: opts.scenario,
|
|
slackChannelId: opts.slackChannelId,
|
|
slackUrl: opts.slackUrl,
|
|
ttl: opts.ttl,
|
|
});
|
|
});
|
|
|
|
mantis
|
|
.command("visual-task")
|
|
.description(
|
|
"Lease or reuse a Crabbox desktop, drive visible browser UI, record MP4, screenshot it, and optionally run image-understanding assertions",
|
|
)
|
|
.option("--repo-root <path>", "Repository root to target when running from a neutral cwd")
|
|
.option("--output-dir <path>", "Mantis visual-task artifact directory")
|
|
.option("--crabbox-bin <path>", "Crabbox binary path")
|
|
.option("--provider <provider>", "Crabbox provider")
|
|
.option("--machine-class <class>", "Crabbox machine class")
|
|
.option("--class <class>", "Alias for --machine-class")
|
|
.option("--lease-id <id>", "Reuse an existing Crabbox lease")
|
|
.option("--idle-timeout <duration>", "Crabbox idle timeout")
|
|
.option("--ttl <duration>", "Crabbox maximum lease lifetime")
|
|
.option("--keep-lease", "Keep a lease created by this run after a passing task")
|
|
.option("--browser-url <url>", "URL to open in the visible browser")
|
|
.option("--duration <duration>", "Desktop recording duration")
|
|
.option("--settle-ms <ms>", "Milliseconds to wait after launch before screenshot")
|
|
.option("--vision-mode <mode>", "Vision mode: image-describe or metadata")
|
|
.option("--vision-prompt <text>", "Prompt for image understanding")
|
|
.option("--vision-model <provider/model>", "Image-capable provider/model ref")
|
|
.option("--vision-timeout-ms <ms>", "Image understanding timeout in milliseconds")
|
|
.option("--expect-text <text>", "Case-insensitive text expected in the vision output")
|
|
.action(async (opts: MantisVisualTaskCommanderOptions) => {
|
|
await runVisualTask({
|
|
browserUrl: opts.browserUrl,
|
|
crabboxBin: opts.crabboxBin,
|
|
duration: opts.duration,
|
|
expectText: opts.expectText,
|
|
idleTimeout: opts.idleTimeout,
|
|
keepLease: opts.keepLease,
|
|
leaseId: opts.leaseId,
|
|
machineClass: opts.machineClass ?? opts.class,
|
|
outputDir: opts.outputDir,
|
|
provider: opts.provider,
|
|
repoRoot: opts.repoRoot,
|
|
settleMs: parseOptionalInteger(opts.settleMs, "--settle-ms"),
|
|
ttl: opts.ttl,
|
|
visionMode: opts.visionMode,
|
|
visionModel: opts.visionModel,
|
|
visionPrompt: opts.visionPrompt,
|
|
visionTimeoutMs: parseOptionalInteger(opts.visionTimeoutMs, "--vision-timeout-ms"),
|
|
});
|
|
});
|
|
|
|
mantis
|
|
.command("visual-driver")
|
|
.description(
|
|
"Driver half for Mantis visual-task; launched by Crabbox record --while, then opens browser, screenshots, and runs vision",
|
|
)
|
|
.option("--repo-root <path>", "Repository root to target when running from a neutral cwd")
|
|
.option("--output-dir <path>", "Mantis visual-task artifact directory")
|
|
.option("--crabbox-bin <path>", "Crabbox binary path")
|
|
.option("--provider <provider>", "Crabbox provider")
|
|
.option("--lease-id <id>", "Crabbox lease id")
|
|
.option("--browser-url <url>", "URL to open in the visible browser")
|
|
.option("--settle-ms <ms>", "Milliseconds to wait after launch before screenshot")
|
|
.option("--vision-mode <mode>", "Vision mode: image-describe or metadata")
|
|
.option("--vision-prompt <text>", "Prompt for image understanding")
|
|
.option("--vision-model <provider/model>", "Image-capable provider/model ref")
|
|
.option("--vision-timeout-ms <ms>", "Image understanding timeout in milliseconds")
|
|
.option("--expect-text <text>", "Case-insensitive text expected in the vision output")
|
|
.action(async (opts: MantisVisualDriverCommanderOptions) => {
|
|
await runVisualDriver({
|
|
browserUrl: opts.browserUrl,
|
|
crabboxBin: opts.crabboxBin,
|
|
expectText: opts.expectText,
|
|
leaseId: opts.leaseId,
|
|
outputDir: opts.outputDir,
|
|
provider: opts.provider,
|
|
repoRoot: opts.repoRoot,
|
|
settleMs: parseOptionalInteger(opts.settleMs, "--settle-ms"),
|
|
visionMode: opts.visionMode,
|
|
visionModel: opts.visionModel,
|
|
visionPrompt: opts.visionPrompt,
|
|
visionTimeoutMs: parseOptionalInteger(opts.visionTimeoutMs, "--vision-timeout-ms"),
|
|
});
|
|
});
|
|
}
|