Add ADB management and live car telemetry

This commit is contained in:
2026-07-31 22:23:12 +02:00
parent 34b31c4caf
commit 0e4ce931ec
12 changed files with 911 additions and 23 deletions
+70 -1
View File
@@ -1,5 +1,14 @@
import { describe, expect, it, vi } from "vitest";
import { collectAdbStatus, type AdbConfig, type AdbExecutor } from "../src/adb.js";
import { existsSync } from "node:fs";
import {
collectAdbStatus,
collectCarTelemetry,
installAdbApk,
launchAdbTarget,
listInstalledAdbPackages,
type AdbConfig,
type AdbExecutor
} from "../src/adb.js";
const enabledConfig: AdbConfig = {
enabled: true,
@@ -129,3 +138,63 @@ describe("read-only ADB collector", () => {
});
});
});
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("parses the verified MG4 live properties and marks SAIC-only data unavailable", async () => {
const execute: AdbExecutor = async () => ({
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: 17 },
gear: { available: true, value: "4" },
wheelAngleDegrees: { available: true, value: -12.5 },
batteryPercent: { available: false },
rangeKm: { available: false }
});
});
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");
});
});
+50
View File
@@ -250,6 +250,56 @@ describe("authentication boundary", () => {
expect(invalidConfiguration.json()).toMatchObject({ error: { code: "INVALID_HOTSPOT_CONFIGURATION" } });
});
it("protects ADB operations and persists only validated shortcuts", async () => {
const app = await createApp();
expect((await app.inject({ method: "GET", url: "/api/adb/packages" })).statusCode).toBe(401);
expect((await app.inject({ method: "GET", url: "/api/adb/shortcuts" })).statusCode).toBe(401);
expect((await app.inject({ method: "GET", url: "/api/car/telemetry" })).statusCode).toBe(401);
const token = await csrf(app);
await setup(app, token);
const login = await app.inject({
method: "POST",
url: "/api/auth/login",
headers: mutationHeaders(token),
payload: { username: "owner", password: "Correct-horse1" }
});
const session = login.cookies.find((item) => item.name === SESSION_COOKIE)?.value ?? "";
const cookie = `${CSRF_COOKIE}=${token}; ${SESSION_COOKIE}=${session}`;
const headers = { ...mutationHeaders(token), cookie };
const invalid = await app.inject({
method: "POST",
url: "/api/adb/shortcuts",
headers,
payload: { name: "Unsafe", type: "component", value: "com.example/.Main;reboot" }
});
expect(invalid.statusCode).toBe(400);
const created = await app.inject({
method: "POST",
url: "/api/adb/shortcuts",
headers,
payload: { name: "Settings", type: "component", value: "com.android.settings/.Settings" }
});
expect(created.statusCode).toBe(201);
const shortcutId = created.json<{ id: number }>().id;
const shortcuts = await app.inject({ method: "GET", url: "/api/adb/shortcuts", headers: { cookie } });
expect(shortcuts.json()).toMatchObject({ shortcuts: [{ id: shortcutId, name: "Settings" }] });
const badApk = await app.inject({
method: "POST",
url: "/api/adb/install",
headers: { ...headers, "content-type": "application/vnd.android.package-archive" },
payload: Buffer.from("plain text")
});
expect(badApk.statusCode).toBe(400);
expect(badApk.json()).toMatchObject({ error: { code: "INVALID_APK" } });
const removed = await app.inject({ method: "DELETE", url: `/api/adb/shortcuts/${shortcutId}`, headers });
expect(removed.statusCode).toBe(204);
});
it("exposes only allowlisted jobs and authenticated run history", async () => {
const app = await createApp();
const token = await csrf(app);