Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

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);
});