Files
elizaos--eliza/packages/app/scripts/android-assistant-verify.mjs
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

376 lines
13 KiB
JavaScript

#!/usr/bin/env node
/**
* Automated, repeatable adb-driven verification lane for Eliza's Android
* assistant surfaces (issue #13581): the ROLE_ASSISTANT VoiceInteractionService,
* the voice-input IME, and the assist-key / assist-gesture routing. It is the
* regression lane the surfaces shipped without — every `adb install -r` clears
* the assistant role and the enabled/selected IME, so a dev-loop reinstall
* silently un-configures the surface and, until now, nothing noticed.
*
* What it does, against whatever build is installed on the attached device:
* 1. Assert the manifest-declared surfaces survived install-time parsing —
* the VIS/session/recognition services, the IME service, the assist
* activity — via `dumpsys package` (a rejected VoiceInteractionServiceInfo
* never registers, so this catches a broken manifest as a hard failure).
* 2. Re-apply the assistant role (`cmd role add-role-holder …ASSISTANT`) and
* the IME (`ime enable` + `ime set`) that a reinstall clears, then assert
* the secure settings (`voice_interaction_service`, `default_input_method`).
* 3. Fire the assistant (`cmd voiceinteraction show`), the assist key
* (`input keyevent KEYCODE_ASSIST`), and the IME open-app path, and assert
* via logcat + `dumpsys activity` that the Eliza deep-link
* (`elizaos://voice?source=android-assistant-session` / `…=android-ime`)
* lands in MainActivity.
* 4. Classify the IME ASR round-trip: a committed transcript when a full
* engine is up, or the designed ENGINE_OFF state when it is not — asserted,
* never skipped silently.
*
* Honest device gating (never green-by-skip): with NO device attached the lane
* prints an N/A verdict and exits 0 — UNLESS `ELIZA_ANDROID_REQUIRE_AGENT=1`
* (or `--require-device`), which makes a missing device a hard failure (exit 1).
* The ASR engine is gated SEPARATELY (`--require-engine` /
* `ELIZA_ANDROID_REQUIRE_ENGINE=1`): only then does an ENGINE_OFF ASR outcome
* fail — the engine-less emulator leaves it unset and asserts the designed
* ENGINE_OFF state instead. All decision logic lives in the pure, unit-tested
* `android-assistant-verify-lib.mjs`; this file only runs adb and feeds its
* stdout to those parsers.
*
* Flags: --serial <s> --require-device --require-engine --json --no-apply
* Env: ANDROID_SERIAL, ELIZA_ANDROID_REQUIRE_AGENT, ELIZA_ANDROID_REQUIRE_ENGINE
*/
import {
APP_PACKAGE,
ASSISTANT_IME_COMPONENT,
ASSISTANT_VIS_COMPONENT,
assertDeepLinkLanded,
classifyImeAsrOutcome,
DEEP_LINK_SOURCES,
detectSurfaceInvocation,
LOG_TAGS,
parseAssistantSurfaces,
parseDefaultInputMethod,
parseEnabledImes,
parseRoleHolders,
parseVoiceInteractionService,
ROLE_ASSISTANT,
summarizeLaneVerdict,
} from "./lib/android-assistant-verify-lib.mjs";
import {
adbTry,
ensureEmulatorPermissive,
isInstalled,
listDevices,
resolveAdb,
resolveSerial,
} from "./lib/android-device.mjs";
const has = (flag) => process.argv.includes(flag);
const val = (flag, fb) => {
const i = process.argv.indexOf(flag);
return i >= 0 ? process.argv[i + 1] : fb;
};
const log = (m) => console.log(`[android-assistant-verify] ${m}`);
// Two INDEPENDENT gates — do not conflate them.
// REQUIRE_DEVICE governs device presence: a required-but-missing device is a
// hard failure (the #13581 "never green-by-skip" ask). Also set by
// ELIZA_ANDROID_REQUIRE_AGENT, the repo's standing "the device/agent must be
// real" flag used across the android lanes.
// REQUIRE_ENGINE governs the on-device ASR engine: when set, an ENGINE_OFF IME
// ASR outcome fails. It is SEPARATE because the emulator carries no engine —
// on a full-engine build (or a real device with a staged model) set it to
// require a committed transcript; on the engine-less emulator leave it unset
// so the lane asserts the designed ENGINE_OFF state instead.
const REQUIRE_DEVICE =
has("--require-device") ||
process.env.ELIZA_ANDROID_REQUIRE_AGENT === "1" ||
process.env.ELIZA_ANDROID_REQUIRE_AGENT === "true";
const REQUIRE_ENGINE =
has("--require-engine") ||
process.env.ELIZA_ANDROID_REQUIRE_ENGINE === "1" ||
process.env.ELIZA_ANDROID_REQUIRE_ENGINE === "true";
const APPLY = !has("--no-apply");
const JSON_OUT = has("--json");
const IME_COMPONENT = ASSISTANT_IME_COMPONENT;
const VIS_COMPONENT = ASSISTANT_VIS_COMPONENT;
/** Run an adb shell command on the device, returning trimmed stdout (or "" on failure). */
function sh(adb, serial, args) {
return adbTry(adb, ["-s", serial, "shell", ...args]).trim();
}
/** Clear the logcat ring so a subsequent scrape only sees this run's lines. */
function clearLogcat(adb, serial) {
adbTry(adb, ["-s", serial, "logcat", "-c"], { stdio: "ignore" });
}
/** Dump and return the current logcat buffer (bounded to the assistant tags). */
function dumpLogcat(adb, serial) {
return adbTry(adb, ["-s", serial, "logcat", "-d", "-v", "brief"]);
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
/**
* Re-apply the assistant role + IME a reinstall clears. Idempotent: re-running
* on an already-configured device is a no-op. Emulators are rooted by
* ensureEmulatorPermissive so `cmd role add-role-holder` succeeds; on a retail
* device without root the grant may be refused — the subsequent secure-settings
* assertion is what turns that into a visible failure rather than a false pass.
*/
function applyRoleAndIme(adb, serial) {
log("re-applying assistant role + IME (cleared by adb install -r)…");
// The role grant needs the role framework; --user 0 targets the primary user.
sh(adb, serial, [
"cmd",
"role",
"add-role-holder",
ROLE_ASSISTANT,
APP_PACKAGE,
]);
sh(adb, serial, [
"settings",
"put",
"secure",
"voice_interaction_service",
VIS_COMPONENT,
]);
sh(adb, serial, ["settings", "put", "secure", "assistant", VIS_COMPONENT]);
sh(adb, serial, ["ime", "enable", IME_COMPONENT]);
sh(adb, serial, ["ime", "set", IME_COMPONENT]);
}
async function verifyOnDevice(adb, serial) {
const checks = {};
// (1) Surfaces registered — dumpsys package for the app, then fall back to a
// full package dump if the per-package form is unavailable on this image.
let pkgDump = adbTry(adb, [
"-s",
serial,
"shell",
"dumpsys",
"package",
APP_PACKAGE,
]);
if (!pkgDump.includes(APP_PACKAGE)) {
pkgDump = adbTry(adb, ["-s", serial, "shell", "dumpsys", "package"]);
}
const surfaces = parseAssistantSurfaces(pkgDump);
checks.surfaces = surfaces;
log(
surfaces.allPresent
? "surfaces: VIS + session + recognition + IME + assist activity all registered"
: `surfaces MISSING: ${surfaces.missing.join(", ")}`,
);
if (APPLY) applyRoleAndIme(adb, serial);
// (2) Secure settings + role holders reflect Eliza.
const roleOut = sh(adb, serial, ["cmd", "role", "holders", ROLE_ASSISTANT]);
const role = parseRoleHolders(roleOut);
const visSetting = parseVoiceInteractionService(
sh(adb, serial, ["settings", "get", "secure", "voice_interaction_service"]),
);
const imeSetting = parseDefaultInputMethod(
sh(adb, serial, ["settings", "get", "secure", "default_input_method"]),
);
const imeEnabled = parseEnabledImes(sh(adb, serial, ["ime", "list", "-s"]));
checks.role = role;
checks.visSetting = visSetting;
checks.imeSetting = imeSetting;
checks.imeEnabled = imeEnabled;
log(`role held by Eliza: ${role.heldByExpected}`);
log(`voice_interaction_service is Eliza: ${visSetting.isEliza}`);
log(
`default_input_method is Eliza IME: ${imeSetting.isEliza} (enabled: ${imeEnabled.elizaEnabled})`,
);
// (3a) Assist-gesture invocation via cmd voiceinteraction show.
clearLogcat(adb, serial);
sh(adb, serial, ["cmd", "voiceinteraction", "show"]);
await sleep(2_500);
const assistLog = dumpLogcat(adb, serial);
const assistDump = adbTry(adb, [
"-s",
serial,
"shell",
"dumpsys",
"activity",
"activities",
]);
const visInvoked = detectSurfaceInvocation(assistLog, {
tag: LOG_TAGS.vis,
bracket: "ElizaVoiceInteractionSession",
source: DEEP_LINK_SOURCES.assistantSession,
});
const assistLanded = assertDeepLinkLanded(
assistDump,
assistLog,
DEEP_LINK_SOURCES.assistantSession,
);
checks.visInvoked = visInvoked;
checks.voiceinteractionLanded = assistLanded;
// (3b) Hardware assist key: input keyevent KEYCODE_ASSIST → should reach the
// VIS session (role held) or the ACTION_ASSIST fallback activity.
clearLogcat(adb, serial);
sh(adb, serial, ["input", "keyevent", "KEYCODE_ASSIST"]);
await sleep(2_500);
const keyLog = dumpLogcat(adb, serial);
const keyDump = adbTry(adb, [
"-s",
serial,
"shell",
"dumpsys",
"activity",
"activities",
]);
const keySessionLanded = assertDeepLinkLanded(
keyDump,
keyLog,
DEEP_LINK_SOURCES.assistantSession,
);
const keyAssistLanded = assertDeepLinkLanded(
keyDump,
keyLog,
DEEP_LINK_SOURCES.assist,
);
const keyLanded = keySessionLanded.landed || keyAssistLanded.landed;
checks.assistKeyLanded = keyLanded;
checks.assistKeySessionLanded = keySessionLanded;
checks.assistKeyActivityLanded = keyAssistLanded;
log(`assist key (KEYCODE_ASSIST) reached Eliza: ${keyLanded}`);
// (3c) IME invocation → open-app deep link. Fire the IME's open-Eliza intent
// directly (elizaos://voice?source=android-ime) so the entry point runs even
// headless where no editor has focus to raise the keyboard.
clearLogcat(adb, serial);
adbTry(adb, [
"-s",
serial,
"shell",
"am",
"start",
"-a",
"android.intent.action.VIEW",
"-d",
`elizaos://voice?source=${DEEP_LINK_SOURCES.ime}&action=voice&voice=1`,
`${APP_PACKAGE}/.MainActivity`,
]);
await sleep(2_500);
const imeLog = dumpLogcat(adb, serial);
const imeDump = adbTry(adb, [
"-s",
serial,
"shell",
"dumpsys",
"activity",
"activities",
]);
const imeLanded = assertDeepLinkLanded(
imeDump,
imeLog,
DEEP_LINK_SOURCES.ime,
);
checks.imeLanded = imeLanded;
log(`IME deep-link reached MainActivity: ${imeLanded.landed}`);
// (4) IME ASR round-trip classification (committed vs. designed ENGINE_OFF).
const asrOutcome = classifyImeAsrOutcome(`${imeLog}\n${assistLog}`);
checks.asrOutcome = asrOutcome;
log(`IME ASR outcome: ${asrOutcome}`);
const verdict = summarizeLaneVerdict(
{
surfacesRegistered: surfaces.allPresent,
roleHeld: role.heldByExpected,
imeSelected: imeSetting.isEliza && imeEnabled.elizaEnabled,
voiceinteractionLanded: assistLanded.landed,
assistKeyLanded: keyLanded,
imeLanded: imeLanded.landed,
asrOutcome,
},
REQUIRE_ENGINE,
);
checks.verdict = verdict;
return checks;
}
async function main() {
let adb;
try {
adb = resolveAdb();
} catch (error) {
return finish({
status: "na",
reason: `adb unavailable: ${error.message}`,
});
}
const devices = listDevices(adb);
if (devices.length === 0) {
return finish({
status: "na",
reason:
"no Android device/emulator attached — assistant/IME/assist-key checks need a device.",
});
}
const serial = resolveSerial(
adb,
val("--serial", process.env.ANDROID_SERIAL),
);
process.env.ANDROID_SERIAL = serial;
log(`device serial=${serial}`);
await ensureEmulatorPermissive(adb, serial, { log });
if (!isInstalled(adb, serial)) {
return finish({
status: REQUIRE_DEVICE ? "fail" : "na",
reason: `${APP_PACKAGE} not installed on ${serial}. Install the app APK first (bun run --cwd packages/app install:android:adb).`,
});
}
const checks = await verifyOnDevice(adb, serial);
return finish({
status: checks.verdict.pass ? "pass" : "fail",
serial,
requireDevice: REQUIRE_DEVICE,
checks,
});
}
function finish(result) {
if (JSON_OUT) {
console.log(JSON.stringify(result, null, 2));
}
if (result.status === "na") {
if (REQUIRE_DEVICE) {
console.error(
`[android-assistant-verify] REQUIRED device missing: ${result.reason}`,
);
process.exit(1);
}
log(`N/A (skipped honestly): ${result.reason}`);
process.exit(0);
}
if (result.status === "fail") {
console.error(
`[android-assistant-verify] FAILED: ${
result.reason ??
result.checks?.verdict?.failures?.join("; ") ??
"verification failed"
}`,
);
process.exit(1);
}
log("PASSED ✅ Android assistant-role / IME / assist-key verification");
process.exit(0);
}
main().catch((error) => {
console.error(`[android-assistant-verify] ERROR: ${error?.stack ?? error}`);
process.exit(1);
});