import { describe, expect, it, vi } from "vitest"; import { existsSync } from "node:fs"; import { collectAdbStatus, collectCarTelemetry, installAdbApk, launchAdbTarget, listInstalledAdbPackages, type AdbConfig, type AdbExecutor } from "../src/adb.js"; const enabledConfig: AdbConfig = { enabled: true, executable: "/usr/bin/adb", serial: "MG4-HEAD-UNIT", timeoutMs: 2_500 }; describe("read-only ADB collector", () => { it("does not execute anything while integration is disabled", async () => { const execute = vi.fn(); await expect(collectAdbStatus({ ...enabledConfig, enabled: false }, execute)).resolves.toEqual({ available: false, state: "disabled", reason: "ADB integration is not configured" }); expect(execute).not.toHaveBeenCalled(); }); it("uses only fixed argument arrays for one serial and parses identity and display values", async () => { const values = new Map([ ["get-state", "device\n"], ["shell getprop ro.product.manufacturer", "SAIC\n"], ["shell getprop ro.product.model", "AUTUS\n"], ["shell getprop ro.build.version.release", "9\n"], ["shell getprop ro.build.version.sdk", "28\n"], ["shell getprop ro.build.display.id", "PPR1.181005.003\n"], ["shell getprop ro.product.cpu.abi", "arm64-v8a\n"], ["shell wm size", "Physical size: 1920x720\n"], ["shell wm density", "Physical density: 160\n"] ]); const calls: string[][] = []; const execute: AdbExecutor = async (executable, arguments_, options) => { expect(executable).toBe("/usr/bin/adb"); expect(options).toEqual({ timeout: 2_500, maxBuffer: 16 * 1024 }); calls.push([...arguments_]); return { stdout: values.get(arguments_.slice(2).join(" ")) ?? "", stderr: "" }; }; await expect(collectAdbStatus(enabledConfig, execute)).resolves.toMatchObject({ available: true, value: { state: "connected", identity: { manufacturer: { available: true, value: "SAIC" }, androidVersion: { available: true, value: "9" }, apiLevel: { available: true, value: 28 }, abi: { available: true, value: "arm64-v8a" } }, display: { size: { available: true, value: { widthPixels: 1920, heightPixels: 720 } }, densityDpi: { available: true, value: 160 } } } }); expect(calls).toHaveLength(9); for (const arguments_ of calls) { expect(arguments_.slice(0, 2)).toEqual(["-s", "MG4-HEAD-UNIT"]); expect(arguments_).not.toContain("sh"); expect(arguments_).not.toContain("-c"); } }); it("reports authorization and offline failures without leaking command output", async () => { const unauthorized: AdbExecutor = async () => { throw { stderr: "error: device unauthorized. private-debug-details" }; }; await expect(collectAdbStatus(enabledConfig, unauthorized)).resolves.toEqual({ available: false, state: "unauthorized", reason: "Authorize this Pi on the infotainment screen" }); const offline: AdbExecutor = async () => ({ stdout: "offline\n", stderr: "" }); await expect(collectAdbStatus(enabledConfig, offline)).resolves.toMatchObject({ available: false, state: "offline" }); }); it("automatically discovers exactly one device and refuses ambiguous connections", async () => { const calls: string[][] = []; const execute: AdbExecutor = async (_executable, arguments_) => { calls.push([...arguments_]); if (arguments_[0] === "devices") { return { stdout: "List of devices attached\nAUTO-MG4\tdevice product:saic model:autus\n", stderr: "" }; } if (arguments_.at(-1) === "get-state") return { stdout: "device\n", stderr: "" }; return { stdout: "value\n", stderr: "" }; }; const status = await collectAdbStatus({ ...enabledConfig, serial: null }, execute); expect(status).toMatchObject({ available: true, value: { state: "connected" } }); expect(calls[0]).toEqual(["devices"]); for (const arguments_ of calls.slice(1)) expect(arguments_.slice(0, 2)).toEqual(["-s", "AUTO-MG4"]); const multiple: AdbExecutor = async () => ({ stdout: "List of devices attached\nMG4\tdevice\nPHONE\tdevice\n", stderr: "" }); await expect(collectAdbStatus({ ...enabledConfig, serial: null }, multiple)).resolves.toMatchObject({ available: false, state: "ambiguous" }); }); it("reports an automatically discovered device awaiting authorization", async () => { const execute: AdbExecutor = async () => ({ stdout: "List of devices attached\nAUTO-MG4\tunauthorized\n", stderr: "" }); await expect(collectAdbStatus({ ...enabledConfig, serial: null }, execute)).resolves.toMatchObject({ available: false, state: "unauthorized" }); }); it("keeps individual fields unavailable when a connected device only partially responds", async () => { const execute: AdbExecutor = async (_executable, arguments_) => { if (arguments_.at(-1) === "get-state") return { stdout: "device\n", stderr: "" }; if (arguments_.at(-1) === "ro.product.model") throw new Error("read failed"); return { stdout: "unexpected\n", stderr: "" }; }; const status = await collectAdbStatus(enabledConfig, execute); expect(status).toMatchObject({ available: true, value: { identity: { model: { available: false, reason: "ADB could not read model" } } } }); }); }); describe("ADB dashboard operations", () => { it("lists installed packages and identifies system paths", async () => { const execute: AdbExecutor = async (_executable, arguments_) => { expect(arguments_).toEqual(["-s", "MG4-HEAD-UNIT", "shell", "pm", "list", "packages", "-f"]); return { stdout: "package:/data/app/com.example/base.apk=com.example\npackage:/system/app/Settings/Settings.apk=com.android.settings\n", stderr: "" }; }; await expect(listInstalledAdbPackages(enabledConfig, execute)).resolves.toEqual([ { packageName: "com.android.settings", apkPath: "/system/app/Settings/Settings.apk", system: true }, { packageName: "com.example", apkPath: "/data/app/com.example/base.apk", system: false } ]); }); it("launches validated packages and components without a shell", async () => { const calls: string[][] = []; const execute: AdbExecutor = async (_executable, arguments_) => { calls.push([...arguments_]); return { stdout: "", stderr: "" }; }; await launchAdbTarget(enabledConfig, { type: "component", value: "com.android.settings/.Settings" }, execute); await launchAdbTarget(enabledConfig, { type: "package", value: "com.example.app" }, execute); expect(calls).toEqual([ ["-s", "MG4-HEAD-UNIT", "shell", "am", "start", "-n", "com.android.settings/.Settings"], ["-s", "MG4-HEAD-UNIT", "shell", "monkey", "-p", "com.example.app", "-c", "android.intent.category.LAUNCHER", "1"] ]); await expect(launchAdbTarget(enabledConfig, { type: "component", value: "com.example/.Main;reboot" }, execute)).rejects.toThrow("INVALID_ADB_COMPONENT"); }); it("reads vehicle data through a pushed ADB probe without an installed companion app", async () => { const calls: string[][] = []; const execute: AdbExecutor = async (_executable, arguments_) => { calls.push([...arguments_]); if (arguments_.includes("app_process")) { return { stdout: '{"status":"ok","soc":73.5,"rangeKm":286,"speedKph":18,"batteryVolts":397.4,"totalConsumptionKwh":6.2,"chargingStatus":0}\n', stderr: "" }; } return { stdout: "[arcsoft.avm.mCurCarSpeed]: [17]\n[arcsoft.avm.mCurCarGear]: [4]\n[arcsoft.avm.mCurCarWheelAngle]: [-12.5]\n", stderr: "" }; }; await expect(collectCarTelemetry(enabledConfig, execute)).resolves.toMatchObject({ available: true, speedKph: { available: true, value: 18 }, gear: { available: true, value: "4" }, wheelAngleDegrees: { available: true, value: -12.5 }, batteryPercent: { available: true, value: 73.5 }, rangeKm: { available: true, value: 286 }, batteryVoltage: { available: true, value: 397.4 }, totalConsumptionKwh: { available: true, value: 6.2 }, chargingStatus: { available: true, value: 0 } }); expect(calls.some((call) => call[2] === "push" && call.at(-1) === "/data/local/tmp/pi-car-telemetry-probe.dex")).toBe(true); expect(calls.some((call) => call.includes("app_process"))).toBe(true); expect(calls.flat()).not.toContain("content://cloud.molberg.mgutility.vehicle"); await collectCarTelemetry(enabledConfig, execute); expect(calls.filter((call) => call[2] === "push")).toHaveLength(1); }); it("keeps direct properties available when the factory vehicle service cannot be sampled", async () => { const execute: AdbExecutor = async (_executable, arguments_) => { if (arguments_.includes("app_process")) throw new Error("service unavailable"); return { stdout: "[arcsoft.avm.mCurCarSpeed]: [17]\n[arcsoft.avm.mCurCarGear]: [4]\n[arcsoft.avm.mCurCarWheelAngle]: [-12.5]\n", stderr: "" }; }; await expect(collectCarTelemetry(enabledConfig, execute)).resolves.toMatchObject({ speedKph: { available: true, value: 17 }, batteryPercent: { available: false, reason: expect.stringContaining("factory vehicle service over ADB") } }); }); it("accepts only ZIP-formatted APK payloads and removes the temporary file", async () => { let temporaryPath = ""; const execute: AdbExecutor = async (_executable, arguments_, options) => { temporaryPath = arguments_.at(-1) ?? ""; expect(arguments_.slice(0, 4)).toEqual(["-s", "MG4-HEAD-UNIT", "install", "-r"]); expect(existsSync(temporaryPath)).toBe(true); expect(options.timeout).toBe(120_000); return { stdout: "Success\n", stderr: "" }; }; await installAdbApk(enabledConfig, Buffer.from([0x50, 0x4b, 0x03, 0x04, 0x00]), execute); expect(existsSync(temporaryPath)).toBe(false); await expect(installAdbApk(enabledConfig, Buffer.from("not an apk"), execute)).rejects.toThrow("INVALID_APK"); }); });