426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
390 lines
11 KiB
JavaScript
390 lines
11 KiB
JavaScript
#!/usr/bin/env node
|
|
// iOS end-to-end orchestrator (macOS only — uses `xcrun simctl`). Mirrors
|
|
// android-e2e.mjs for the iOS Simulator. The iOS WebView (WKWebView) is not
|
|
// CDP-drivable like Android, so there is no Playwright route-coverage sweep;
|
|
// instead this proves the device-level real paths and fails LOUDLY:
|
|
// 1. A simulator is booted (boots one if needed).
|
|
// 2. The app is built + installed.
|
|
// 3. Local route: on-device agent + smallest model + real chat round-trip
|
|
// (mobile-local-chat-smoke ios full-bun path).
|
|
// 4. Deep-link / auth-callback registration + drive (mobile-auth-simulator).
|
|
// 5. (optional) Cloud route: real provisioning probe.
|
|
//
|
|
// Flags: --device <name|udid> --app-path <App.app> --skip-build
|
|
// --skip-local-chat --skip-auth --cloud --no-wait --output <dir>
|
|
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import {
|
|
assertNonVacuousPlan,
|
|
buildAuthSmokeCommand,
|
|
buildCloudProvisioningCommand,
|
|
buildIosSimBuildCommand,
|
|
buildLocalChatSmokeCommand,
|
|
extractAppId,
|
|
isAppInstalled,
|
|
parseIosE2eArgs,
|
|
planIosE2eSteps,
|
|
resolveTargetDevice,
|
|
selectBootedUdid,
|
|
} from "./ios-e2e-lib.mjs";
|
|
import {
|
|
captureFailureForensics,
|
|
createDeviceE2eBundle,
|
|
finalizeDeviceE2eBundle,
|
|
finishBundleStep,
|
|
formatFailureForensicsBlock,
|
|
recordBundleArtifact,
|
|
runBundledCommand,
|
|
setBundleBuild,
|
|
setBundleDevice,
|
|
startBundleStep,
|
|
} from "./lib/device-e2e-bundle.mjs";
|
|
import { acquireDeviceLease } from "./lib/device-lease.mjs";
|
|
import {
|
|
assertCandidateIosAppRendererFresh,
|
|
assertInstalledIosAppRendererFresh,
|
|
} from "./lib/ios-renderer-stamp.mjs";
|
|
import { clearIosSmokeDefaults } from "./lib/ios-sim-defaults-hygiene.mjs";
|
|
import { findLatestBuiltIosSimulatorApp } from "./lib/ios-simulator-app-product.mjs";
|
|
|
|
const appDir = path.resolve(fileURLToPath(import.meta.url), "..", "..");
|
|
const repoRoot = path.resolve(appDir, "..", "..");
|
|
const flags = parseIosE2eArgs(process.argv);
|
|
const log = (m) => console.log(`[ios-e2e] ${m}`);
|
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
|
function readAppId() {
|
|
const configPath = path.join(appDir, "app.config.ts");
|
|
return extractAppId(fs.readFileSync(configPath, "utf8"));
|
|
}
|
|
|
|
function run(bundle, name, cmd, args, env = {}) {
|
|
runBundledCommand(bundle, name, cmd, args, {
|
|
cwd: appDir,
|
|
env,
|
|
onFailure: (step, error) => captureIosFailure(bundle, step, error),
|
|
});
|
|
}
|
|
|
|
let activeIosContext = { udid: null };
|
|
|
|
function captureIosFailure(bundle, step, error) {
|
|
const { udid } = activeIosContext;
|
|
return captureFailureForensics(
|
|
bundle,
|
|
step,
|
|
({ failureDir }) => {
|
|
const files = [];
|
|
const causePath = path.join(failureDir, "failure-cause.txt");
|
|
fs.writeFileSync(causePath, `${error?.message ?? error}\n`);
|
|
files.push(causePath);
|
|
if (udid) {
|
|
const screenshotPath = path.join(failureDir, "screen.png");
|
|
simctl(["io", udid, "screenshot", "--type=png", screenshotPath]);
|
|
files.push(screenshotPath);
|
|
const logPath = path.join(failureDir, "ios-sim.log");
|
|
const result = spawnSync(
|
|
"xcrun",
|
|
[
|
|
"simctl",
|
|
"spawn",
|
|
udid,
|
|
"log",
|
|
"show",
|
|
"--style",
|
|
"compact",
|
|
"--last",
|
|
"2m",
|
|
],
|
|
{ encoding: "utf8", maxBuffer: 32 * 1024 * 1024 },
|
|
);
|
|
fs.writeFileSync(
|
|
logPath,
|
|
result.status === 0
|
|
? result.stdout
|
|
: result.stderr || `simctl log show exited with ${result.status}\n`,
|
|
);
|
|
files.push(logPath);
|
|
}
|
|
return files;
|
|
},
|
|
error,
|
|
);
|
|
}
|
|
|
|
function failIosStep(bundle, step, error) {
|
|
captureIosFailure(bundle, step, error);
|
|
finishBundleStep(bundle, step, "failed", error);
|
|
}
|
|
|
|
function simctl(args) {
|
|
return execFileSync("xcrun", ["simctl", ...args], { encoding: "utf8" });
|
|
}
|
|
|
|
function trySimctl(args) {
|
|
try {
|
|
return simctl(args).trim();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function captureSimulatorScreenshot(bundle, udid) {
|
|
const outPath = path.join(bundle.rawDir, "ios-final.png");
|
|
simctl(["io", udid, "screenshot", "--type=png", outPath]);
|
|
return outPath;
|
|
}
|
|
|
|
async function recordSimulatorVideo(bundle, udid, durationSeconds = 3) {
|
|
const outPath = path.join(bundle.rawDir, "ios-final.mov");
|
|
const recorder = spawn(
|
|
"xcrun",
|
|
[
|
|
"simctl",
|
|
"io",
|
|
udid,
|
|
"recordVideo",
|
|
"--codec",
|
|
"h264",
|
|
"--force",
|
|
outPath,
|
|
],
|
|
{ stdio: "ignore" },
|
|
);
|
|
await delay(Math.max(1, durationSeconds) * 1000);
|
|
recorder.kill("SIGINT");
|
|
await Promise.race([
|
|
new Promise((resolve) => recorder.once("close", resolve)),
|
|
delay(5_000),
|
|
]);
|
|
if (!fs.existsSync(outPath)) return null;
|
|
return outPath;
|
|
}
|
|
|
|
function captureSimulatorLog(bundle, udid) {
|
|
const outPath = path.join(bundle.logsDir, "ios-sim.log");
|
|
const result = spawnSync(
|
|
"xcrun",
|
|
[
|
|
"simctl",
|
|
"spawn",
|
|
udid,
|
|
"log",
|
|
"show",
|
|
"--style",
|
|
"compact",
|
|
"--last",
|
|
"5m",
|
|
],
|
|
{ encoding: "utf8", maxBuffer: 32 * 1024 * 1024 },
|
|
);
|
|
fs.writeFileSync(
|
|
outPath,
|
|
result.status === 0
|
|
? result.stdout
|
|
: result.stderr || `simctl log show exited with ${result.status}\n`,
|
|
);
|
|
return outPath;
|
|
}
|
|
|
|
function bootedUdid() {
|
|
const raw = trySimctl(["list", "devices", "booted", "--json"]);
|
|
if (!raw) return null;
|
|
return selectBootedUdid(JSON.parse(raw));
|
|
}
|
|
|
|
function ensureSimulatorBooted(deviceName) {
|
|
if (process.platform !== "darwin") {
|
|
throw new Error("iOS e2e requires macOS (xcrun simctl).");
|
|
}
|
|
const existing = bootedUdid();
|
|
if (existing) {
|
|
log(`reusing booted simulator ${existing}`);
|
|
return existing;
|
|
}
|
|
const target = resolveTargetDevice(deviceName);
|
|
log(`booting simulator ${target}`);
|
|
try {
|
|
simctl(["boot", target]);
|
|
} catch (error) {
|
|
throw new Error(
|
|
`Could not boot simulator "${target}": ${error.message}. List devices with \`xcrun simctl list devices\`.`,
|
|
);
|
|
}
|
|
execFileSync("open", ["-a", "Simulator"], { stdio: "ignore" });
|
|
const udid = bootedUdid();
|
|
if (!udid) throw new Error(`Simulator ${target} did not reach Booted state.`);
|
|
return udid;
|
|
}
|
|
|
|
function installBuiltSimulatorApp(udid, appId) {
|
|
const appPath = flags.appPath ?? findLatestBuiltIosSimulatorApp();
|
|
if (!appPath) {
|
|
throw new Error(
|
|
"Could not find a Debug-iphonesimulator App.app after build. Pass --app-path or inspect Xcode DerivedData.",
|
|
);
|
|
}
|
|
|
|
assertCandidateIosAppRendererFresh({
|
|
appPath,
|
|
bundleId: appId,
|
|
repoRoot,
|
|
log,
|
|
});
|
|
trySimctl(["terminate", udid, appId]);
|
|
trySimctl(["uninstall", udid, appId]);
|
|
log(`installing built simulator app ${appPath}`);
|
|
simctl(["install", udid, appPath]);
|
|
const installed = trySimctl(["get_app_container", udid, appId, "app"]);
|
|
if (!isAppInstalled(installed)) {
|
|
throw new Error(`${appId} was not installed after simctl install.`);
|
|
}
|
|
return assertInstalledIosAppRendererFresh({
|
|
udid,
|
|
bundleId: appId,
|
|
repoRoot,
|
|
log,
|
|
});
|
|
}
|
|
|
|
function runStep(bundle, step, { udid, appId }) {
|
|
switch (step.id) {
|
|
case "build": {
|
|
log("building the iOS Simulator app…");
|
|
const build = buildIosSimBuildCommand();
|
|
run(bundle, step.label, build.cmd, build.args);
|
|
const installStep = startBundleStep(bundle, "install iOS Simulator app");
|
|
try {
|
|
const stamp = installBuiltSimulatorApp(udid, appId);
|
|
setBundleBuild(bundle, {
|
|
buildId: stamp?.buildId ?? null,
|
|
commit: stamp?.commit ?? null,
|
|
});
|
|
finishBundleStep(bundle, installStep, "passed");
|
|
} catch (error) {
|
|
failIosStep(bundle, installStep, error);
|
|
throw error;
|
|
}
|
|
return;
|
|
}
|
|
case "auth": {
|
|
log(`${step.label}…`);
|
|
const auth = buildAuthSmokeCommand(udid);
|
|
run(bundle, step.label, auth.cmd, auth.args);
|
|
return;
|
|
}
|
|
case "local-chat": {
|
|
log(`${step.label}…`);
|
|
const chat = buildLocalChatSmokeCommand();
|
|
run(bundle, step.label, chat.cmd, chat.args, {
|
|
ELIZA_DEVICE_E2E_ARTIFACT_DIR: path.join(bundle.root, "test-results"),
|
|
ELIZA_IOS_ARTIFACT_DIR: path.join(bundle.root, "test-results", "ios"),
|
|
});
|
|
return;
|
|
}
|
|
case "cloud": {
|
|
log(`${step.label}…`);
|
|
const cloud = buildCloudProvisioningCommand();
|
|
run(bundle, step.label, cloud.cmd, cloud.args);
|
|
return;
|
|
}
|
|
default:
|
|
throw new Error(`unknown orchestrator step: ${step.id}`);
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const bundle = createDeviceE2eBundle({
|
|
appDir,
|
|
lane: "ios-sim",
|
|
outputDir: flags.output,
|
|
});
|
|
let finalResult = "failed";
|
|
let finalError = null;
|
|
let lease = null;
|
|
let udid = null;
|
|
let appId = null;
|
|
|
|
try {
|
|
const steps = planIosE2eSteps(flags);
|
|
// Refuse a run that would print success without exercising any device path.
|
|
assertNonVacuousPlan(steps);
|
|
log(`plan: ${steps.map((s) => s.id).join(" → ")}`);
|
|
|
|
appId = readAppId();
|
|
const bootStep = startBundleStep(bundle, "boot iOS Simulator");
|
|
try {
|
|
udid = ensureSimulatorBooted(flags.device);
|
|
activeIosContext = { udid };
|
|
finishBundleStep(bundle, bootStep, "passed");
|
|
} catch (error) {
|
|
failIosStep(bundle, bootStep, error);
|
|
throw error;
|
|
}
|
|
log(`simulator udid=${udid}`);
|
|
setBundleDevice(bundle, { udid, kind: "ios-simulator" });
|
|
lease = await acquireDeviceLease(`ios:${udid}`, {
|
|
waitMs: flags.noWait ? 0 : undefined,
|
|
log,
|
|
});
|
|
|
|
clearIosSmokeDefaults({ udid, bundleId: appId, log });
|
|
for (const step of steps) {
|
|
runStep(bundle, step, { udid, appId });
|
|
}
|
|
finalResult = "passed";
|
|
log("ALL iOS E2E PASSED ✅");
|
|
} catch (error) {
|
|
finalError = error;
|
|
throw error;
|
|
} finally {
|
|
if (udid && appId) {
|
|
try {
|
|
recordBundleArtifact(
|
|
bundle,
|
|
captureSimulatorScreenshot(bundle, udid),
|
|
"screenshot",
|
|
);
|
|
} catch (error) {
|
|
// error-policy:J7 Bundle capture is diagnostic; preserve the runner result.
|
|
bundle.warnings.push(
|
|
`final iOS screenshot failed: ${error?.message ?? error}`,
|
|
);
|
|
}
|
|
try {
|
|
const video = await recordSimulatorVideo(bundle, udid);
|
|
if (video) recordBundleArtifact(bundle, video, "video");
|
|
} catch (error) {
|
|
// error-policy:J7 Bundle capture is diagnostic; preserve the runner result.
|
|
bundle.warnings.push(
|
|
`final iOS video failed: ${error?.message ?? error}`,
|
|
);
|
|
}
|
|
try {
|
|
recordBundleArtifact(bundle, captureSimulatorLog(bundle, udid), "log");
|
|
} catch (error) {
|
|
// error-policy:J7 Bundle capture is diagnostic; preserve the runner result.
|
|
bundle.warnings.push(
|
|
`final iOS log capture failed: ${error?.message ?? error}`,
|
|
);
|
|
}
|
|
}
|
|
if (udid && appId) {
|
|
clearIosSmokeDefaults({ udid, bundleId: appId, log });
|
|
}
|
|
lease?.release();
|
|
const bundleRoot = finalizeDeviceE2eBundle(bundle, finalResult);
|
|
if (finalError) {
|
|
const block = formatFailureForensicsBlock(bundle, finalError);
|
|
if (block) process.stderr.write(`\n${block}`);
|
|
}
|
|
log(`bundle: ${bundleRoot}`);
|
|
}
|
|
}
|
|
main().catch((error) => {
|
|
console.error(`[ios-e2e] FAILED: ${error?.message ?? error}`);
|
|
process.exit(1);
|
|
});
|