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
367 lines
13 KiB
JavaScript
367 lines
13 KiB
JavaScript
/**
|
|
* Deterministic vitest coverage for the assistant-role/IME/assist-key
|
|
* verification parsers (#13581). Fixtures are real-shaped `adb`/`dumpsys`/`cmd`/
|
|
* `logcat` output slices, not mocks of the parser — the parsers are pure
|
|
* string→decision functions, so this suite proves the string handling that
|
|
* regresses silently off-device (a garbled scrape reading as "absent"), covering
|
|
* the present, absent, short-vs-full component form, and empty/garbage cases.
|
|
*
|
|
* Runs in the normal `packages/app` vitest lane (which collects
|
|
* `scripts/**\/*.test.mjs`) so a parser regression fails CI — the earlier
|
|
* `node:test` form was loaded by vitest but collected as ZERO tests, an
|
|
* unenforced green-by-skip.
|
|
*
|
|
* Run: `bun run --cwd packages/app test -- scripts/lib/android-assistant-verify-lib.test.mjs`
|
|
*/
|
|
import assert from "node:assert/strict";
|
|
// #13581 enforcement fix: run under vitest (the lane that collects
|
|
// scripts/**/*.test.mjs) instead of node:test, which vitest loads but collects
|
|
// as ZERO tests — a green-by-skip that left this regression guard unenforced.
|
|
// node:assert still throws on failure, so every assertion below fails the
|
|
// vitest test the same way; only the test-registration source changes.
|
|
import { test } from "vitest";
|
|
|
|
import {
|
|
APP_PACKAGE,
|
|
ASSISTANT_IME_COMPONENT,
|
|
ASSISTANT_VIS_COMPONENT,
|
|
assertDeepLinkLanded,
|
|
classifyImeAsrOutcome,
|
|
DEEP_LINK_SOURCES,
|
|
detectSurfaceInvocation,
|
|
LOG_TAGS,
|
|
normalizeComponent,
|
|
parseAssistantSurfaces,
|
|
parseDefaultInputMethod,
|
|
parseEnabledImes,
|
|
parseRoleHolders,
|
|
parseServiceRegistration,
|
|
parseVoiceInteractionService,
|
|
ROLE_ASSISTANT,
|
|
summarizeLaneVerdict,
|
|
} from "./android-assistant-verify-lib.mjs";
|
|
|
|
test("normalizeComponent canonicalizes short and full forms equally", () => {
|
|
const full = "ai.elizaos.app/ai.elizaos.app.ElizaVoiceInteractionService";
|
|
assert.equal(
|
|
normalizeComponent("ai.elizaos.app/.ElizaVoiceInteractionService"),
|
|
full,
|
|
);
|
|
assert.equal(normalizeComponent(full), full);
|
|
assert.throws(() => normalizeComponent("not a component"));
|
|
assert.throws(() => normalizeComponent(""));
|
|
assert.throws(() => normalizeComponent(undefined));
|
|
});
|
|
|
|
test("parseServiceRegistration finds the VIS in a real dumpsys package slice (short form)", () => {
|
|
const dump = `
|
|
Service Resolver Table:
|
|
Non-Data Actions:
|
|
android.service.voice.VoiceInteractionService:
|
|
4f2a1c ai.elizaos.app/.ElizaVoiceInteractionService filter 88b
|
|
Action: "android.service.voice.VoiceInteractionService"
|
|
`;
|
|
const result = parseServiceRegistration(
|
|
dump,
|
|
ASSISTANT_VIS_COMPONENT,
|
|
"android.service.voice.VoiceInteractionService",
|
|
);
|
|
assert.equal(result.registered, true);
|
|
assert.equal(result.actionSeen, true);
|
|
});
|
|
|
|
test("parseServiceRegistration reports absence when the component is missing", () => {
|
|
const dump = `Service Resolver Table:\n android.view.InputMethod:\n ce01 com.other.keyboard/.SomeIme filter aa\n`;
|
|
const result = parseServiceRegistration(
|
|
dump,
|
|
ASSISTANT_IME_COMPONENT,
|
|
"android.view.InputMethod",
|
|
);
|
|
assert.equal(result.registered, false);
|
|
});
|
|
|
|
test("parseAssistantSurfaces detects every declared surface, and reports the missing one", () => {
|
|
const full = `
|
|
ai.elizaos.app/.ElizaVoiceInteractionService
|
|
ai.elizaos.app/.ElizaVoiceInteractionSessionService
|
|
ai.elizaos.app/.ElizaRecognitionService
|
|
ai.elizaos.app/.ElizaVoiceInputMethodService
|
|
ai.elizaos.app/.ElizaAssistActivity
|
|
`;
|
|
const ok = parseAssistantSurfaces(full);
|
|
assert.equal(ok.allPresent, true);
|
|
assert.deepEqual(ok.missing, []);
|
|
|
|
const missingIme = full.replace(
|
|
"ai.elizaos.app/.ElizaVoiceInputMethodService\n",
|
|
"",
|
|
);
|
|
const partial = parseAssistantSurfaces(missingIme);
|
|
assert.equal(partial.allPresent, false);
|
|
assert.deepEqual(partial.missing, ["inputMethodService"]);
|
|
assert.equal(partial.present.voiceInteractionService, true);
|
|
});
|
|
|
|
test("parseAssistantSurfaces treats a renamed VIS as ABSENT (regression canary)", () => {
|
|
// A renamed component must not prefix-match the expected id — this is exactly
|
|
// the regression the lane exists to catch (rename the VIS → it stops
|
|
// registering under its expected component id).
|
|
const renamed = `
|
|
ai.elizaos.app/.ElizaVoiceInteractionServiceRENAMED
|
|
ai.elizaos.app/.ElizaVoiceInteractionSessionService
|
|
ai.elizaos.app/.ElizaRecognitionService
|
|
ai.elizaos.app/.ElizaVoiceInputMethodService
|
|
ai.elizaos.app/.ElizaAssistActivity
|
|
`;
|
|
const parsed = parseAssistantSurfaces(renamed);
|
|
assert.equal(parsed.allPresent, false);
|
|
assert.deepEqual(parsed.missing, ["voiceInteractionService"]);
|
|
// The session service must still match despite sharing a prefix with the VIS.
|
|
assert.equal(parsed.present.voiceInteractionSessionService, true);
|
|
});
|
|
|
|
test("parseRoleHolders recognizes the held ROLE_ASSISTANT and rejects noise lines", () => {
|
|
const held = parseRoleHolders("ai.elizaos.app\n");
|
|
assert.equal(held.heldByExpected, true);
|
|
assert.deepEqual(held.holders, [APP_PACKAGE]);
|
|
|
|
const none = parseRoleHolders("");
|
|
assert.equal(none.heldByExpected, false);
|
|
|
|
const other = parseRoleHolders("com.google.android.googlequicksearchbox\n");
|
|
assert.equal(other.heldByExpected, false);
|
|
|
|
const errorLine = parseRoleHolders(
|
|
"Exception occurred while executing 'holders'",
|
|
);
|
|
assert.deepEqual(errorLine.holders, []);
|
|
assert.equal(errorLine.heldByExpected, false);
|
|
|
|
assert.equal(ROLE_ASSISTANT, "android.app.role.ASSISTANT");
|
|
});
|
|
|
|
test("parseVoiceInteractionService reads dumpsys ComponentInfo and flattened settings forms", () => {
|
|
const dumpsys =
|
|
" mCurInteractor=ComponentInfo{ai.elizaos.app/ai.elizaos.app.ElizaVoiceInteractionService}";
|
|
const fromDump = parseVoiceInteractionService(dumpsys);
|
|
assert.equal(fromDump.isEliza, true);
|
|
|
|
const settings =
|
|
"ai.elizaos.app/ai.elizaos.app.ElizaVoiceInteractionService\n";
|
|
assert.equal(parseVoiceInteractionService(settings).isEliza, true);
|
|
|
|
const other =
|
|
" mCurInteractor=ComponentInfo{com.google.android.googlequicksearchbox/com.google.VoiceInteractionService}";
|
|
assert.equal(parseVoiceInteractionService(other).isEliza, false);
|
|
|
|
assert.equal(parseVoiceInteractionService("").selected, null);
|
|
});
|
|
|
|
test("parseDefaultInputMethod distinguishes selected Eliza IME from another keyboard and null", () => {
|
|
const eliza = parseDefaultInputMethod(
|
|
"ai.elizaos.app/.ElizaVoiceInputMethodService\n",
|
|
);
|
|
assert.equal(eliza.isEliza, true);
|
|
|
|
const other = parseDefaultInputMethod(
|
|
"com.google.android.inputmethod.latin/.LatinIME",
|
|
);
|
|
assert.equal(other.isEliza, false);
|
|
assert.equal(
|
|
other.selected,
|
|
"com.google.android.inputmethod.latin/.LatinIME",
|
|
);
|
|
|
|
assert.equal(parseDefaultInputMethod("null").selected, null);
|
|
assert.equal(parseDefaultInputMethod("").selected, null);
|
|
});
|
|
|
|
test("parseEnabledImes finds the Eliza IME in an `ime list -s` set", () => {
|
|
const list = [
|
|
"com.google.android.inputmethod.latin/com.android.inputmethod.latin.LatinIME",
|
|
"ai.elizaos.app/.ElizaVoiceInputMethodService",
|
|
].join("\n");
|
|
const parsed = parseEnabledImes(list);
|
|
assert.equal(parsed.elizaEnabled, true);
|
|
assert.equal(parsed.enabled.length, 2);
|
|
|
|
const without = parseEnabledImes(
|
|
"com.google.android.inputmethod.latin/com.android.inputmethod.latin.LatinIME",
|
|
);
|
|
assert.equal(without.elizaEnabled, false);
|
|
});
|
|
|
|
test("detectSurfaceInvocation catches the IME class tag, bracket marker, and deep-link source", () => {
|
|
const byTag = detectSurfaceInvocation(
|
|
"07-04 12:00:01.234 4210 4210 I ElizaVoiceIme: [ElizaVoiceInputMethodService] opening Eliza",
|
|
{
|
|
tag: LOG_TAGS.ime,
|
|
bracket: "ElizaVoiceInputMethodService",
|
|
source: DEEP_LINK_SOURCES.ime,
|
|
},
|
|
);
|
|
assert.equal(byTag.tagHit, true);
|
|
assert.equal(byTag.bracketHit, true);
|
|
assert.equal(byTag.detected, true);
|
|
|
|
const bySource = detectSurfaceInvocation(
|
|
"ActivityTaskManager: START u0 {act=android.intent.action.VIEW dat=elizaos://voice?source=android-ime...}",
|
|
{ source: DEEP_LINK_SOURCES.ime },
|
|
);
|
|
assert.equal(bySource.sourceHit, true);
|
|
assert.equal(bySource.detected, true);
|
|
|
|
const miss = detectSurfaceInvocation("nothing relevant here", {
|
|
tag: LOG_TAGS.vis,
|
|
source: DEEP_LINK_SOURCES.assistantSession,
|
|
});
|
|
assert.equal(miss.detected, false);
|
|
});
|
|
|
|
test("assertDeepLinkLanded requires BOTH resumed MainActivity and the expected source tag", () => {
|
|
const activityDump =
|
|
" ResumedActivity: ActivityRecord{a1b2 u0 ai.elizaos.app/.MainActivity t42}";
|
|
const logcat =
|
|
"ActivityTaskManager: START u0 {dat=elizaos://voice?source=android-assistant-session&voice=1 cmp=ai.elizaos.app/.MainActivity}";
|
|
const landed = assertDeepLinkLanded(
|
|
activityDump,
|
|
logcat,
|
|
DEEP_LINK_SOURCES.assistantSession,
|
|
);
|
|
assert.equal(landed.landed, true);
|
|
assert.equal(landed.mainActivityResumed, true);
|
|
assert.equal(landed.sourceSeen, true);
|
|
|
|
// Foreground MainActivity but no matching source tag → NOT landed (guards
|
|
// against a coincidental unrelated foreground pass).
|
|
const wrongSource = assertDeepLinkLanded(
|
|
activityDump,
|
|
"unrelated log",
|
|
DEEP_LINK_SOURCES.ime,
|
|
);
|
|
assert.equal(wrongSource.mainActivityResumed, true);
|
|
assert.equal(wrongSource.landed, false);
|
|
|
|
// Source present but MainActivity not resumed → NOT landed.
|
|
const notResumed = assertDeepLinkLanded(
|
|
"ResumedActivity: com.other/.Home",
|
|
logcat,
|
|
DEEP_LINK_SOURCES.assistantSession,
|
|
);
|
|
assert.equal(notResumed.landed, false);
|
|
});
|
|
|
|
test("classifyImeAsrOutcome distinguishes committed / engineOff / modelNotReady / error", () => {
|
|
assert.equal(
|
|
classifyImeAsrOutcome(
|
|
"ElizaVoiceIme: [ElizaVoiceInputMethodService] transcript committed (14 chars)",
|
|
),
|
|
"committed",
|
|
);
|
|
assert.equal(
|
|
classifyImeAsrOutcome(
|
|
"ElizaVoiceIme: [ElizaVoiceInputMethodService] ASR loopback unreachable: Connection refused",
|
|
),
|
|
"engineOff",
|
|
);
|
|
assert.equal(
|
|
classifyImeAsrOutcome(
|
|
"ElizaVoiceIme: [ElizaVoiceInputMethodService] ASR responded 503",
|
|
),
|
|
"modelNotReady",
|
|
);
|
|
assert.equal(
|
|
classifyImeAsrOutcome(
|
|
"ElizaVoiceIme: [ElizaVoiceInputMethodService] transcription error",
|
|
),
|
|
"error",
|
|
);
|
|
assert.equal(classifyImeAsrOutcome("nothing"), "unknown");
|
|
});
|
|
|
|
test("summarizeLaneVerdict passes only when every required surface checks out", () => {
|
|
const green = {
|
|
surfacesRegistered: true,
|
|
roleHeld: true,
|
|
imeSelected: true,
|
|
voiceinteractionLanded: true,
|
|
assistKeyLanded: true,
|
|
imeLanded: true,
|
|
asrOutcome: "committed",
|
|
};
|
|
assert.equal(summarizeLaneVerdict(green, true).pass, true);
|
|
|
|
// Engine off is acceptable when the agent is NOT required...
|
|
assert.equal(
|
|
summarizeLaneVerdict({ ...green, asrOutcome: "engineOff" }, false).pass,
|
|
true,
|
|
);
|
|
// ...but a hard failure when the agent IS required (never green-by-skip).
|
|
const requiredButOff = summarizeLaneVerdict(
|
|
{ ...green, asrOutcome: "engineOff" },
|
|
true,
|
|
);
|
|
assert.equal(requiredButOff.pass, false);
|
|
assert.match(requiredButOff.failures.join(" "), /ENGINE_OFF/);
|
|
|
|
const roleMissing = summarizeLaneVerdict(
|
|
{ ...green, roleHeld: false },
|
|
false,
|
|
);
|
|
assert.equal(roleMissing.pass, false);
|
|
assert.match(roleMissing.failures.join(" "), /assistant role/);
|
|
|
|
const voiceinteractionMissing = summarizeLaneVerdict(
|
|
{ ...green, voiceinteractionLanded: false },
|
|
false,
|
|
);
|
|
assert.equal(voiceinteractionMissing.pass, false);
|
|
assert.match(voiceinteractionMissing.failures.join(" "), /voiceinteraction/);
|
|
|
|
const assistKeyMissing = summarizeLaneVerdict(
|
|
{ ...green, assistKeyLanded: false },
|
|
false,
|
|
);
|
|
assert.equal(assistKeyMissing.pass, false);
|
|
assert.match(assistKeyMissing.failures.join(" "), /KEYCODE_ASSIST/);
|
|
|
|
// An unknown ASR outcome is the emulator lane's designed state: the verify
|
|
// lane never raises the IME keyboard or captures audio, so no ASR line is
|
|
// logged and the round-trip cannot be classified. It is acceptable when the
|
|
// agent is NOT required...
|
|
assert.equal(
|
|
summarizeLaneVerdict({ ...green, asrOutcome: "unknown" }, false).pass,
|
|
true,
|
|
);
|
|
// ...but a hard failure when a full engine IS required (never green-by-skip).
|
|
const requiredButUnknown = summarizeLaneVerdict(
|
|
{ ...green, asrOutcome: "unknown" },
|
|
true,
|
|
);
|
|
assert.equal(requiredButUnknown.pass, false);
|
|
assert.match(requiredButUnknown.failures.join(" "), /unknown/);
|
|
|
|
// A transcription error always fails, engine required or not.
|
|
assert.equal(
|
|
summarizeLaneVerdict({ ...green, asrOutcome: "error" }, false).pass,
|
|
false,
|
|
);
|
|
|
|
const notRegistered = summarizeLaneVerdict(
|
|
{ ...green, surfacesRegistered: false },
|
|
false,
|
|
);
|
|
assert.equal(notRegistered.pass, false);
|
|
});
|
|
|
|
test("exported component ids match the native manifest components", () => {
|
|
assert.equal(
|
|
ASSISTANT_VIS_COMPONENT,
|
|
"ai.elizaos.app/.ElizaVoiceInteractionService",
|
|
);
|
|
assert.equal(
|
|
ASSISTANT_IME_COMPONENT,
|
|
"ai.elizaos.app/.ElizaVoiceInputMethodService",
|
|
);
|
|
});
|