Files
elizaos--eliza/packages/app-core/scripts/verify-android-sms-gateway-e2e.mjs
T
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

395 lines
11 KiB
JavaScript

#!/usr/bin/env node
/**
* Strict physical Android SMS gateway verifier.
*
* This command installs/prepares the SMS gateway app, clears logcat, then
* waits for the real runtime milestones produced by an inbound SMS:
* receiver -> gateway work queued -> cloud accepted -> SMS reply sent.
*/
import { spawn, spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(scriptDir, "..", "..", "..");
const installScript = path.join(scriptDir, "install-android-sms-gateway.mjs");
const adbPath =
"/opt/homebrew/share/android-commandlinetools/platform-tools/adb";
const defaultEvidencePath = path.join(
repoRoot,
".eliza-local",
"android-sms-gateway-e2e-latest.json",
);
const milestones = [
{
key: "receiver",
label: "SMS receiver observed inbound delivery",
pattern: /ElizaSmsReceiver/,
},
{
key: "queued",
label: "gateway work queued",
pattern: /ElizaSmsGateway.*Queued SMS gateway work/,
},
{
key: "cloud",
label: "cloud gateway accepted inbound SMS",
pattern: /ElizaSmsGateway.*Cloud gateway accepted SMS/,
},
{
key: "sending",
label: "reply SMS send attempted",
pattern: /ElizaSmsGateway.*Sending SMS gateway reply/,
},
{
key: "persisted",
label: "reply SMS persisted",
pattern: /ElizaSmsGateway.*SMS gateway reply sent and persisted/,
},
];
function usage() {
return [
"Usage: node packages/app-core/scripts/verify-android-sms-gateway-e2e.mjs [options]",
"",
"Options:",
" --serial <serial> adb serial. Defaults to the only connected device.",
" --wait-device <seconds> Wait for an adb device before installing. Defaults to 300.",
" --timeout <seconds> Wait for SMS milestones. Defaults to 180.",
" --skip-install Do not install or grant role before watching logs.",
" --from <number> Optional sender number to display in instructions.",
" --evidence <path> Write structured proof JSON. Defaults to .eliza-local/android-sms-gateway-e2e-latest.json.",
" --no-evidence Do not write a proof JSON file.",
].join("\n");
}
function parseArgs(argv) {
const args = {
serial: null,
waitDeviceSeconds: 300,
timeoutSeconds: 180,
install: true,
from: null,
evidencePath: defaultEvidencePath,
};
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
const next = () => {
const value = argv[++i];
if (!value) throw new Error(`${arg} requires a value`);
return value;
};
if (arg === "--serial") args.serial = next();
else if (arg === "--wait-device")
args.waitDeviceSeconds = Number.parseInt(next(), 10);
else if (arg === "--timeout")
args.timeoutSeconds = Number.parseInt(next(), 10);
else if (arg === "--skip-install") args.install = false;
else if (arg === "--from") args.from = next();
else if (arg === "--evidence") args.evidencePath = path.resolve(next());
else if (arg === "--no-evidence") args.evidencePath = null;
else if (arg === "--help" || arg === "-h") {
console.log(usage());
process.exit(0);
} else {
throw new Error(`Unknown argument: ${arg}\n${usage()}`);
}
}
for (const [key, value] of Object.entries(args)) {
if (
key === "serial" ||
key === "install" ||
key === "from" ||
key === "evidencePath"
) {
continue;
}
if (!Number.isInteger(value) || value < 0) {
throw new Error(`${key} must be a non-negative integer`);
}
}
return args;
}
function writeEvidence({
evidencePath,
ok,
serial,
from,
timeoutSeconds,
seen,
missing,
buffer,
}) {
if (!evidencePath) return;
fs.mkdirSync(path.dirname(evidencePath), { recursive: true });
const evidence = {
ok,
gatewayPhoneNumber: "+14159611510",
serial,
from: from ?? null,
timeoutSeconds,
checkedAt: new Date().toISOString(),
milestones: milestones.map((milestone) => ({
key: milestone.key,
label: milestone.label,
seen: seen.has(milestone.key),
line: seen.get(milestone.key) ?? null,
})),
missing,
logTail: buffer.split(/\r?\n/).filter(Boolean).slice(-80),
};
fs.writeFileSync(evidencePath, `${JSON.stringify(evidence, null, 2)}\n`);
console.log(`[sms-gateway-e2e] evidence=${evidencePath}`);
}
function parseAdbService(line) {
const parts = String(line).trim().split(/\s+/);
return {
name: parts[0] ?? "",
type: parts[1] ?? "",
endpoint: parts[2] ?? "",
raw: line,
};
}
function writeNoDeviceEvidence({
evidencePath,
waitDeviceSeconds,
from,
services,
}) {
if (!evidencePath) return;
fs.mkdirSync(path.dirname(evidencePath), { recursive: true });
const parsedServices = services.map(parseAdbService);
const evidence = {
ok: false,
gatewayPhoneNumber: "+14159611510",
serial: null,
from: from ?? null,
waitDeviceSeconds,
checkedAt: new Date().toISOString(),
blocker: "no_adb_device",
adbServices: parsedServices,
pairingEndpointAdvertised: parsedServices.some((service) =>
service.type.includes("_adb-tls-pairing"),
),
connectEndpointAdvertised: parsedServices.some((service) =>
service.type.includes("_adb-tls-connect"),
),
milestones: milestones.map((milestone) => ({
key: milestone.key,
label: milestone.label,
seen: false,
line: null,
})),
missing: milestones.map((milestone) => milestone.key),
nextSteps: [
"Open Android Developer Options > Wireless debugging > Pair device with pairing code.",
"Run: bun run --cwd packages/app-core sms-gateway:watch:pair",
"Then run: bun run --cwd packages/app-core sms-gateway:verify",
],
};
fs.writeFileSync(evidencePath, `${JSON.stringify(evidence, null, 2)}\n`);
console.log(`[sms-gateway-e2e] evidence=${evidencePath}`);
}
function run(command, args, { allowFailure = false } = {}) {
const result = spawnSync(command, args, {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
});
if (result.status !== 0 && !allowFailure) {
throw new Error(
`${command} ${args.join(" ")} failed:\n${result.stderr || result.stdout}`,
);
}
return result;
}
function listDevices() {
const result = run(adbPath, ["devices", "-l"]);
return result.stdout
.split(/\r?\n/)
.slice(1)
.map((line) => line.trim())
.filter(Boolean)
.filter((line) => /\bdevice\b/.test(line))
.map((line) => line.split(/\s+/)[0]);
}
function adbMdnsServices() {
const result = run(adbPath, ["mdns", "services"], { allowFailure: true });
return result.stdout
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line && !/^List of discovered mdns services/i.test(line));
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function resolveSerial(args) {
if (args.serial) return args.serial;
const deadline = Date.now() + args.waitDeviceSeconds * 1000;
let lastServices = [];
while (Date.now() <= deadline) {
const devices = listDevices();
if (devices.length === 1) return devices[0];
if (devices.length > 1) {
throw new Error(
`Multiple adb devices are connected; pass --serial. Devices: ${devices.join(", ")}`,
);
}
lastServices = adbMdnsServices();
await sleep(1000);
}
const services = lastServices.length
? ` Advertised wireless ADB services: ${lastServices.join("; ")}.`
: " No wireless ADB pairing/connect services are currently advertised.";
writeNoDeviceEvidence({
evidencePath: args.evidencePath,
waitDeviceSeconds: args.waitDeviceSeconds,
from: args.from,
services: lastServices,
});
throw new Error(
`Timed out waiting ${args.waitDeviceSeconds}s for an adb device.${services} ` +
"Open Android Developer Options > Wireless debugging > Pair device with pairing code, then run: " +
"node packages/app-core/scripts/install-android-sms-gateway.mjs --pair auto --wait-pair 300 --connect auto --wait-device 60 --grant-role --clear-logcat --watch-logs 60",
);
}
function installAndPrepare(serial) {
const result = spawnSync(
"node",
[
installScript,
"--serial",
serial,
"--grant-role",
"--clear-logcat",
"--logcat-lines",
"25",
],
{ stdio: "inherit" },
);
if (result.status !== 0) {
throw new Error(`install/prepare failed with exit code ${result.status}`);
}
}
function adbArgs(serial, args) {
return ["-s", serial, ...args];
}
async function watchMilestones({ serial, timeoutSeconds, from }) {
const seen = new Map();
console.log(
`[sms-gateway-e2e] Send a real SMS${from ? ` from ${from}` : ""} to +14159611510 now.`,
);
console.log(
`[sms-gateway-e2e] Waiting ${timeoutSeconds}s for: ${milestones.map((m) => m.key).join(", ")}`,
);
const child = spawn(
adbPath,
adbArgs(serial, [
"logcat",
"-v",
"time",
"ElizaSmsGateway:D",
"ElizaSmsReceiver:D",
"AndroidRuntime:E",
"*:S",
]),
{ stdio: ["ignore", "pipe", "pipe"] },
);
const deadline = Date.now() + timeoutSeconds * 1000;
let buffer = "";
child.stdout.on("data", (chunk) => {
const text = chunk.toString();
buffer += text;
for (const line of text.split(/\r?\n/)) {
for (const milestone of milestones) {
if (!seen.has(milestone.key) && milestone.pattern.test(line)) {
seen.set(milestone.key, line);
console.log(
`[sms-gateway-e2e] PASS ${milestone.label}: ${line.trim()}`,
);
}
}
}
});
child.stderr.on("data", (chunk) => {
buffer += chunk.toString();
});
while (Date.now() <= deadline) {
if (milestones.every((milestone) => seen.has(milestone.key))) {
child.kill("SIGTERM");
return { ok: true, seen, buffer };
}
await sleep(500);
}
child.kill("SIGTERM");
return { ok: false, seen, buffer };
}
async function main() {
const args = parseArgs(process.argv.slice(2));
const serial = await resolveSerial(args);
console.log(`[sms-gateway-e2e] Using adb device ${serial}`);
if (args.install) installAndPrepare(serial);
else run(adbPath, adbArgs(serial, ["logcat", "-c"]), { allowFailure: true });
const result = await watchMilestones({
serial,
timeoutSeconds: args.timeoutSeconds,
from: args.from,
});
if (!result.ok) {
const missing = milestones
.filter((milestone) => !result.seen.has(milestone.key))
.map((milestone) => milestone.key);
writeEvidence({
evidencePath: args.evidencePath,
ok: false,
serial,
from: args.from,
timeoutSeconds: args.timeoutSeconds,
seen: result.seen,
missing,
buffer: result.buffer,
});
throw new Error(`Missing SMS gateway milestones: ${missing.join(", ")}`);
}
writeEvidence({
evidencePath: args.evidencePath,
ok: true,
serial,
from: args.from,
timeoutSeconds: args.timeoutSeconds,
seen: result.seen,
missing: [],
buffer: result.buffer,
});
console.log(
"[sms-gateway-e2e] Physical Android SMS gateway verification passed.",
);
}
main().catch((error) => {
console.error(
`[sms-gateway-e2e] ${error instanceof Error ? error.message : String(error)}`,
);
process.exit(1);
});