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

366 lines
12 KiB
JavaScript

#!/usr/bin/env node
/**
* One-command physical-iPhone e2e lane: build + deploy the current tree to the
* plugged-in iPhone, assert it boots on-device, capture the filmstrip, pull the
* boot-trace, and collapse everything into one run-scoped triage bundle whose
* absolute path is printed as the last line (issue #14337).
*
* Chains three already-proven scripts through the argv builders in
* `lib/ios-device-e2e-lib.mjs`:
* 1. ios-device-deploy.mjs --skip-appexes (build → sign → devicectl install;
* appex surfaces excluded — logged loudly by the deploy script). The deploy
* also writes the renderer-freshness assert + the deploy ledger row.
* 2. ios-device-capture.mjs --platform device (BootCapture — the ON-DEVICE
* assertion: did the freshly deployed app reach home or the error card?
* `mobile-local-chat-smoke` is simulator-only, so BootCapture is the
* device assertion of record). Its filmstrip attachments land in smoke/, so
* the one on-device boot yields both the verdict and the watchable artifact.
* 3. ios-device-logs.mjs --no-console --pull-boot-trace (full-Bun engine
* observability; attached --console SIGTRAPs the no-JIT engine, #11515).
*
* Run assembly — per-step timing, artifact collection, PNG→JPG / MOV→MP4 inline
* conversion, junit, and the machine-readable `summary.json` — goes through the
* shared `lib/device-e2e-bundle.mjs` framework, the same one `android-e2e.mjs`
* and `ios-e2e.mjs` use, so all three device lanes emit one summary shape
* (PR #14509 reconciliation). Every step's exit is loud: a non-zero child fails
* the lane non-zero and the partial bundle is still finalized. The bundle's
* absolute path is the final stdout line so an agent can post the smoke/
* filmstrip + logs/ boot-trace inline to the PR.
*
* Device id: --device flag or ELIZA_IOS_DEVICE_ID.
*
* Usage:
* node scripts/ios-device-e2e.mjs [--device <id>] [--skip-appexes]
* [--no-skip-appexes] [--skip-logs] [--require-chat]
* [--bundle-id <id>] [--output <dir>]
*/
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { readDevicectlDeviceList } from "./ios-device-devicectl.mjs";
import {
findDeviceRecord,
parseCliArgs,
resolveDeviceId,
} from "./ios-device-lib.mjs";
import {
captureFailureForensics,
collectBundleArtifacts,
createDeviceE2eBundle,
finalizeDeviceE2eBundle,
formatFailureForensicsBlock,
runBundledCommand,
setBundleBuild,
setBundleDevice,
} from "./lib/device-e2e-bundle.mjs";
import {
buildDeviceDeployCommand,
buildDeviceFailureBootTraceCommand,
buildDeviceFailureScreenshotCommand,
buildDeviceLogsCommand,
buildDeviceSmokeCommand,
planIosDeviceE2eSteps,
} from "./lib/ios-device-e2e-lib.mjs";
import {
freshRendererManifestPath,
readRendererManifest,
} from "./lib/ios-renderer-stamp.mjs";
const scriptsDir = path.dirname(fileURLToPath(import.meta.url));
const appRoot = path.resolve(scriptsDir, "..");
const repoRoot = path.resolve(appRoot, "..", "..");
const log = (message) => console.log(`[ios-device-e2e] ${message}`);
const fail = (message) => {
console.error(`[ios-device-e2e] ERROR: ${message}`);
process.exit(1);
};
let activeDeviceContext = null;
const DEFAULT_FAILURE_COMMAND_TIMEOUT_MS = 30_000;
function isNonEmptyFile(filePath) {
try {
return fs.statSync(filePath).isFile() && fs.statSync(filePath).size > 0;
} catch {
// error-policy:J3 Failure artifacts are optional diagnostics; absence is
// recorded in the command log rather than treated as a captured file.
return false;
}
}
function directFiles(dir) {
return fs
.readdirSync(dir, { withFileTypes: true })
.filter((entry) => entry.isFile())
.map((entry) => path.join(dir, entry.name));
}
function writeFailureCommandLog({ failureDir, label, command, result }) {
const logPath = path.join(failureDir, `${label}.log`);
const timedOut = result.error?.code === "ETIMEDOUT";
const status =
result.status === null
? `signal ${result.signal}`
: `exit ${result.status}`;
fs.writeFileSync(
logPath,
[
`$ ${command.cmd} ${command.args.join(" ")}`,
`status: ${status}`,
`timeoutMs: ${command.timeoutMs ?? DEFAULT_FAILURE_COMMAND_TIMEOUT_MS}`,
`timedOut: ${timedOut ? "yes" : "no"}`,
"",
"stdout:",
result.stdout || "",
"",
"stderr:",
result.stderr || "",
"",
"error:",
result.error?.message || "",
].join("\n"),
);
return logPath;
}
function captureFailureCommand({ failureDir, label, command, expectedFiles }) {
const before = new Set(directFiles(failureDir));
const expected = new Set(expectedFiles);
const result = spawnSync(command.cmd, command.args, {
encoding: "utf8",
maxBuffer: 32 * 1024 * 1024,
timeout: command.timeoutMs ?? DEFAULT_FAILURE_COMMAND_TIMEOUT_MS,
});
const captured = expectedFiles.filter(isNonEmptyFile);
const after = directFiles(failureDir).filter((file) => !before.has(file));
const diagnosticLog = writeFailureCommandLog({
failureDir,
label,
command,
result,
});
const commandProducedUnexpectedFiles = after.filter(
(file) =>
file !== diagnosticLog && !expected.has(file) && isNonEmptyFile(file),
);
return [...captured, ...commandProducedUnexpectedFiles, diagnosticLog];
}
// Failure forensics for a chained-child step: the child script already wrote its
// own artifacts into smoke/ or logs/ when it reached those phases, and this adds
// point-of-failure evidence from the physical phone when the host tools allow it.
function captureDeviceFailure(bundle, step, error) {
return captureFailureForensics(
bundle,
step,
({ failureDir }) => {
const causePath = path.join(failureDir, "failure-cause.txt");
fs.writeFileSync(causePath, `${error?.message ?? error}\n`);
const artifacts = [causePath];
if (!activeDeviceContext) return artifacts;
const screenshotPath = path.join(failureDir, "screen.png");
artifacts.push(
...captureFailureCommand({
failureDir,
label: "screenshot-capture",
command: buildDeviceFailureScreenshotCommand({
deviceUdid: activeDeviceContext.udid,
outputFile: screenshotPath,
}),
expectedFiles: [screenshotPath],
}),
);
artifacts.push(
...captureFailureCommand({
failureDir,
label: "boot-trace-capture",
command: buildDeviceFailureBootTraceCommand({
scriptsDir,
deviceId: activeDeviceContext.deviceId,
outputFile: path.join(failureDir, "boot-trace-run"),
bundleId: activeDeviceContext.bundleId,
}),
expectedFiles: [],
}),
);
return artifacts;
},
error,
);
}
async function main() {
const args = parseCliArgs(process.argv.slice(2), {
booleans: [
"skip-appexes",
"no-skip-appexes",
"skip-logs",
"require-chat",
"help",
],
});
if (args.help) {
console.log(
"Usage: node scripts/ios-device-e2e.mjs [--device <id>] [--no-skip-appexes] [--skip-logs] [--require-chat] [--bundle-id <id>] [--output <dir>]",
);
return;
}
if (process.platform !== "darwin") fail("iOS device e2e requires macOS.");
const deviceId = resolveDeviceId({ flagValue: args.device ?? null });
if (!deviceId) {
fail(
"no device given. Pass --device <devicectl-id|udid|name> or set ELIZA_IOS_DEVICE_ID.\n" +
"List devices with: xcrun devicectl list devices",
);
}
const device = findDeviceRecord(readDevicectlDeviceList(), deviceId);
if (!device) {
fail(
`device "${deviceId}" not found via devicectl. List: xcrun devicectl list devices ` +
"(pair the phone: Finder → device → Trust, and enable Developer Mode).",
);
}
log(
`device: ${device.name} (identifier ${device.identifier}, udid ${device.udid})`,
);
// --skip-appexes is the default unattended posture (PR #13174); --no-skip-appexes
// opts back in when per-appex profiles exist.
const skipAppexes = !args["no-skip-appexes"];
const bundleId = args["bundle-id"] || null;
activeDeviceContext = {
bundleId,
deviceId,
identifier: device.identifier,
udid: device.udid,
};
const bundle = createDeviceE2eBundle({
appDir: appRoot,
lane: "ios-device",
outputDir: args.output,
});
setBundleDevice(bundle, {
udid: device.udid,
identifier: device.identifier,
name: device.name,
kind: "ios-device",
});
// skippedAppexes is run configuration that scopes what the build proves; carry
// it on the build metadata (buildId/commit are filled in after the deploy).
setBundleBuild(bundle, { skippedAppexes: skipAppexes });
// BootCapture attachments land in smoke/ (per-#14337 bundle layout); the shared
// finalize collects logs/ + raw/ + reports/, so smoke/ is collected explicitly.
const smokeDir = path.join(bundle.root, "smoke");
fs.mkdirSync(smokeDir, { recursive: true });
const steps = planIosDeviceE2eSteps({
skipLogs: Boolean(args["skip-logs"]),
});
log(`plan: ${steps.map((s) => s.id).join(" → ")}`);
log(`bundle: ${bundle.root}`);
const commandFor = (step) => {
switch (step.id) {
case "deploy":
return buildDeviceDeployCommand({
scriptsDir,
deviceId,
skipAppexes,
bundleId,
});
case "smoke":
return buildDeviceSmokeCommand({
scriptsDir,
deviceId,
outputDir: smokeDir,
requireChat: Boolean(args["require-chat"]),
bundleId,
});
case "logs":
return buildDeviceLogsCommand({
scriptsDir,
deviceId,
// ios-device-logs derives its own trace filename from
// path.dirname(--output), so point --output at a file inside logsDir.
outputFile: path.join(bundle.logsDir, "boot-trace-run"),
bundleId,
});
default:
throw new Error(`unknown step: ${step.id}`);
}
};
let finalResult = "failed";
let finalError = null;
try {
for (const step of steps) {
const { cmd, args: cmdArgs } = commandFor(step);
runBundledCommand(bundle, step.label, cmd, cmdArgs, {
cwd: appRoot,
onFailure: (bundleStep, error) =>
captureDeviceFailure(bundle, bundleStep, error),
});
if (step.id === "deploy") {
// The deploy step installs the freshly built bundle; surface its
// buildId/commit into the summary by reading the fresh dist stamp (the
// exact bundle the deploy installed). The deploy already asserted
// renderer freshness, so this is informational metadata only.
try {
const fresh = readRendererManifest(
freshRendererManifestPath({ repoRoot }),
"freshly built",
);
setBundleBuild(bundle, {
buildId: fresh.buildId,
commit: fresh.commit,
});
} catch (error) {
// error-policy:J7 diagnostics-must-not-kill-the-loop — the summary's
// build metadata is informational; the deploy already asserted
// freshness. Warn and leave it null rather than failing a good run.
bundle.warnings.push(
`summary build metadata unavailable: ${error?.message ?? error}`,
);
log(`summary build metadata unavailable: ${error?.message ?? error}`);
}
}
}
finalResult = "passed";
} catch (error) {
// runBundledCommand already recorded the step as failed and ran the failure
// forensics; this stops the chain at the first failing step.
finalError = error;
}
collectBundleArtifacts(bundle, [smokeDir]);
const bundleRoot = finalizeDeviceE2eBundle(bundle, finalResult);
if (finalError) {
log(
`FAILED: ${finalError.message ?? finalError}. Bundle (partial) written.`,
);
const block = formatFailureForensicsBlock(bundle, finalError);
if (block) process.stderr.write(`\n${block}`);
// Last line: the absolute bundle path — the machine-readable handoff even on
// failure (contract with #14337).
console.log(bundleRoot);
process.exit(1);
}
log(
`ALL iOS DEVICE E2E PASSED ✅ (${skipAppexes ? "appexes SKIPPED — widget/keyboard/device-activity surfaces UNTESTED" : "appexes included"})`,
);
// Last line: the absolute bundle path (contract with #14337 — agents post the
// smoke/ filmstrip + logs/ boot-trace inline to the PR).
console.log(bundleRoot);
}
main().catch((error) => fail(error?.stack ?? String(error)));