import MaestroOptions from '../models/maestro_options'; import Credentials from '../models/credentials'; import BaseProvider, { ProviderResult } from './base_provider'; import type { JsonRunResult } from '../utils/json_output'; export interface MaestroRunAssets { logs?: Record; video?: string | false; screenshots?: string[]; } export type MaestroFlowStatus = 'WAITING' | 'READY' | 'DONE' | 'FAILED' | 'CANCELLED'; export interface MaestroFlowInfo { id: number; name: string; report?: string; requested_at?: string; completed_at?: string; status: MaestroFlowStatus; success?: number; test_case_id?: number; shard_index?: number; error_messages?: string[]; assets?: MaestroRunAssets; } export interface MaestroRunEnvironment { device?: string; name?: string; version?: string; } export interface MaestroRunInfo { id: number; status: 'WAITING' | 'READY' | 'DONE' | 'FAILED' | 'CANCELLED'; capabilities: { deviceName: string; platformName: string; version?: string; }; environment?: MaestroRunEnvironment; success: number; report?: string; options?: Record; assets?: MaestroRunAssets; flows?: MaestroFlowInfo[]; error_messages?: string[]; } export interface MaestroRunDetails extends MaestroRunInfo { completed: boolean; assets_synced: boolean; } export interface MaestroStatusResponse { runs: MaestroRunInfo[]; success: boolean; completed: boolean; } export type MaestroResult = ProviderResult; /** One entry of GET /app-automate/maestro (project list). */ export interface MaestroProjectSummary { id: number; name: string; created_at: string; updated_at: string; completed: boolean; app?: { app_url?: string; icon_url?: string | null; app_version?: string | null; bundle_id?: string | null; }; flows?: { id: number; name: string; }[]; runs: number[]; } export interface MaestroProjectListResponse { data: MaestroProjectSummary[]; meta: { offset: number; count: number; total: number; }; } export interface ListProjectsParams { count?: number; offset?: number; } export interface MaestroSocketMessage { id: number; payload: string; } export interface MissingFileReference { flowFile: string; referencedFile: string; resolvedPath: string; } export interface DuplicateFlowReference { referencedFlow: string; referencedBy: string[]; } export default class Maestro extends BaseProvider { protected readonly URL = "https://api.testingbot.com/v1/app-automate/maestro"; protected readonly jsonProvider: "maestro"; private detectedPlatform; private socket; private updateServer; private updateKey; private socketFallbackWarned; private otherAppUrls; private uploadedFlowCount; private flowAnimationFrame; private flowAnimationTimer; private latestFlows; private latestDisplayedLineCount; private flowsTableDisplayed; private flowTableHasFailuresColumn; private flowAttempts; constructor(credentials: Credentials, options: MaestroOptions); private static readonly SUPPORTED_APP_EXTENSIONS; private resolvedAppPath; private appTempDirs; /** The app path to upload: the materialized download/extraction, else the option. */ private get appPath(); /** * Rejects an app path that is missing, has an unsupported extension, or is * unreadable. With --app-url only the URL syntax is checked here; the file * is validated after download. */ private validateAppFile; /** * Turns --app-url and .tar.gz inputs into a local app the upload pipeline * understands: downloads the URL, then extracts a .tar.gz to its .app * bundle. No-op for plain local files. Temp directories are removed by * cleanupAppTemp() once the upload is done. */ private materializeApp; private cleanupAppTemp; private validate; /** * Detect platform from app file content using magic bytes */ private detectPlatform; run(): Promise; /** * `testingbot upload`: uploads (or dedupes) the app and returns the project * id that can be passed to `--app-binary-id`. No flows, no run. */ uploadOnly(): Promise<{ success: true; appId: number; } | { success: false; error: string; }>; /** * Creates a fresh project that shares the stored app of `sourceId` * (--app-binary-id). The server reports the app's platform, which replaces * file-based detection when --platform was not given. */ private reuseApp; private uploadApp; private static isOtherAppUrl; private uploadOtherApps; /** * Zip a .app bundle directory into a temporary zip file */ private zipAppBundle; private checkAppChecksum; /** * Collect and resolve all flow files, their dependencies, and determine the * base directory for the zip structure. This is shared by both uploadFlows * and the dry-run path. * * Returns null if the input is a single .zip file (direct upload, no processing). */ collectFlows(): Promise<{ allFlowFiles: string[]; topLevelFlowFiles: string[]; baseDir: string | undefined; } | null>; private uploadFlows; /** * Search ancestor directories of the given flow files for a Maestro config file * (config.yaml or config.yml). This identifies the project root so the zip * preserves the directory structure needed for relative paths like ../../screens/. */ /** * Drops flows matched by --exclude-flows from `files` in place. Entries may * be files, directories (everything beneath is excluded) or glob patterns. * Returns how many files were removed. Runs before dependency discovery, so * an excluded flow that another flow still runFlow's is bundled as a * subflow but never executes on its own. */ private applyFlowExclusions; private findMaestroProjectRoot; private discoverFlows; private discoverDependencies; /** * Check if a file path is a Maestro config file (config.yaml or config.yml) */ private isConfigFile; private readFlowTags; /** * Load includeTags / excludeTags declared in the Maestro project's * config.yaml (or config.yml). Returns undefined fields when no config * exists or the values are not arrays. */ private loadConfigTags; private filterFlowsByTags; /** * Check if a string looks like a file path (relative path with extension) */ private looksLikePath; /** * Try to add a file path as a dependency if it exists */ private tryAddDependency; /** * Recursively extract file paths from any value in the YAML structure */ private extractPathsFromValue; /** * Find all file references in flow files that don't exist on disk. * This validates that all referenced files (runScript, runFlow, addMedia, etc.) * will be included in the zip. */ findMissingReferences(flowFiles: string[], allIncludedFiles: string[]): Promise; /** * Find flows that were passed as top-level flows (each executed standalone) * but are ALSO referenced via `runFlow` by another top-level flow. Maestro * has no notion of a "subflow-only" file, so such a flow runs twice: once * standalone and once per runFlow invocation. Returns the offending flows so * the user can be warned. */ findDuplicateFlowReferences(topLevelFlowFiles: string[]): Promise; /** * Extract the set of resolved absolute file paths referenced via `runFlow` * (including nested runFlow inside inline commands) in a single flow file. */ private extractRunFlowReferences; /** * Recursively collect resolved `runFlow` file references into `refs`. */ private collectRunFlowRefs; /** * Log warnings for flows that will execute more than once (top-level flows * also invoked via runFlow by another top-level flow). */ private logDuplicateFlowReferences; /** * Recursively find missing file references in a YAML value */ private findMissingInValue; /** * Log warnings for missing file references */ private logMissingReferences; private logIncludedFiles; private createFlowsZip; /** * Compute the common parent directory of all files */ private computeCommonDirectory; /** * Before submitting a device matrix, spell out what is about to run: every * flow executes once per device, so the cost is devices × flows. */ private logDeviceMatrixSummary; private runTests; private getStatus; private waitForCompletion; /** * Poll completion condition used when --retry > 0. True once no flow is * still running and no failed flow is still eligible for — or awaiting the * result of — a retry. Replaces the run-level `completed` flag, which can * flip prematurely while a freshly-queued retry has not yet surfaced. */ private retriesComplete; /** * Issues a retry for every failed flow/shard whose latest attempt has just * settled and still has retries left — immediately, without waiting for the * rest of the run. Each attempt id is retried at most once; a retry whose * request permanently fails is abandoned so the poll loop can still finish. */ private issueEligibleRetries; /** * Triggers a retry of a single flow/shard and returns the new attempt's * MaestroRunTest id. */ private retryFlow; /** * Stops a run, overriding the base provider's `/stop` with Maestro's `/cancel`. * * A 409 means the run reached a terminal state first, which is the normal * outcome when Ctrl-C lands as the last flow finishes. That is a success for * our purposes, so it is not reported as a failure to stop. */ protected stopRun(runId: number): Promise; /** Stable key identifying the logical flow/shard a flow attempt belongs to. */ private flowGroupKey; /** Latest attempt per logical flow/shard (highest id wins). */ private groupLatest; /** * Maps each flow's id to its 1-based attempt number within its logical * group (ordered by id). The first attempt is 1; anything higher is a retry. */ private computeFlowAttempts; /** * The flow name as shown in the table, prefixed with a retry marker when the * row is a retry attempt. Reads the attempt map populated at render time. */ private flowRowName; /** * Colorizes the retry marker without affecting column width: the padding is * computed on the plain string first, then the glyph is wrapped in color. */ private colorizeRetryIcon; private isFlowFailed; /** Failed flows/shards, considering only the latest attempt per group. */ private failedLatestAttempts; /** A run passes if every logical flow's latest attempt passed. */ private runPassed; private computeOverallSuccess; protected dashboardUrl(runId?: number): string | undefined; /** * Adds per-flow rows to the JSON run. Every attempt is listed so retries * are visible; `latest` marks the attempt whose verdict counts, and the run's * `passed` follows the same last-attempt-wins rule as the console summary. */ protected runToJson(run: MaestroRunInfo): JsonRunResult; /** * Polls the run status, rendering the live flow table, until `isComplete` * returns true. Returns the raw status without printing the final summary or * fetching reports/artifacts — that is finalize()'s job. * * `onPoll`, when provided, runs after each poll is rendered and before the * completion check — used by --retry to queue retries for flows that have * just failed without waiting for the rest of the run to finish. */ private pollOnce; /** * Renders the final table/summary, reports the last-attempt-wins result, and * fetches reports/artifacts. Called once after the (optional) retry loop. */ private finalize; /** * `testingbot status`: reports the current state of an existing project. * With `wait`, blocks until every run has finished and prints the same live * table and summary as a foreground `maestro` run. */ status(appId: number, options?: { wait?: boolean; }): Promise; /** * `testingbot artifacts`: downloads reports and/or artifact bundles for a * finished project, reusing the same code path as `--report` and * `--download-artifacts` on a foreground run. */ artifacts(appId: number): Promise; /** `testingbot list`: newest-first page of the account's Maestro projects. */ listProjects(params?: ListProjectsParams): Promise; /** Logs the error the way run() does and returns an `error` result. */ private errorResult; /** One-shot, non-animated snapshot of a project for `testingbot status`. */ private printStatusSummary; private displayRunStatus; /** * Get the display name for a run, preferring environment.name over capabilities.deviceName * This shows the actual device used when a wildcard (*) was specified */ private getRunDisplayName; private getStatusInfo; private getFlowStatusDisplay; private hasAnyFlowFailed; private calculateFlowDuration; private getTerminalHeight; private getMaxDisplayableFlows; private getTerminalWidth; /** * Returns the maximum length of `flow.name` that keeps the rendered row * within the current terminal width, so the row does not visually wrap. * Wrapped rows break the `\x1b[NA` cursor-up math used by in-place updates, * which is what causes the table to repeat instead of refresh in place * (e.g. with --shard-split where the API returns long comma-joined names). * * Row layout is: " {duration:10} {status:10} {name}[ {error}]" — overhead * is 23 plain-width chars before `name`. `extra` reserves room for trailing * content like a fail-reason suffix. */ private getMaxNameLength; private truncateForRow; private getRemainingSummary; private displayFlowsWithLimit; /** * Builds the (dimmed) header and separator lines for the flows table. When * `hasFailures` is set, the "Fail reason" column is appended. */ private buildFlowsTableHeader; private displayFlowsTableHeader; /** * Builds the single-line representation of a flow row: duration, status, the * (padded) flow name, and — when the flow has failed — its first error * message inline. Shared by the static render (displayFlowRow) and the live * in-place updates (updateFlowsInPlace) so the fail reason shows * consistently, including while a retry is still running. */ private buildFlowRowLine; private displayFlowRow; private displayFlowsTable; /** * Starts the flow-table animation loop. Re-renders the cached flow rows at * `FLOW_ANIMATION_MS` so WAITING/RUNNING spinner frames advance between * (much slower) polls. Calling while already running is a no-op. */ private startFlowAnimation; private stopFlowAnimation; protected stopAnimations(): void; private updateFlowsInPlace; private fetchReports; private getRunDetails; private waitForArtifactsSync; private downloadFile; private generateArtifactZipName; private sanitizeFlowDirName; private buildFlowDirNames; private downloadAssetBundle; private downloadArtifacts; private createZipFromDirectory; private connectToUpdateServer; private disconnectFromUpdateServer; private handleMaestroData; private handleMaestroError; } //# sourceMappingURL=maestro.d.ts.map