Watch
1
0
Fork
You've already forked RedFlag
0
RedFlag/agent/internal/system/device_test.go
Fimeg ff2f30f47a v0.2.9.3: device classification + ARM support — Pixel 3 lands
DEVICE-002: ARM machine-ID fallback — device-tree model + /etc/machine-id
combo, then /proc/cpuinfo Serial (all-zero rejected), before the weak
hostname fallback. Hardware-bound IDs on DMI-less devices.

DEVICE-001: agent detects device_type (server/desktop/phone/tablet) from
/sys signals — system battery (scope=Device peripherals excluded, UPS
excluded), DRM connector state, framebuffer min-dimension for phone/tablet
split. Reports device_type/device_model/os_distro in registration and
system-info paths.

SERVER-001: migration 061 — device_type, device_type_manual (operator
override, never agent-written), device_model, os_distro on agents.
effective_device_type computed into every serialized agent.

SERVER-002: PUT /admin/agents/:id/device-type — set/clear override,
enum-validated, journaled.

WEB-001: device-type icons + fleet filter, device model in list, detail
header badge with reclassify dropdown, os_distro surfaced.

INSTALL-003: arm64 install path unblocked — helper (required manifest
component) now cross-built aarch64-unknown-linux-musl via rust-lld in the
server image, signed at boot (helperArches += arm64), listed in the release
manifest. Install template already handled uname -m and pacman.

Plus in-flight: desktop tray wiring, enrollment page polish, CI workflow
updates, RAF session-broker/pacman-scanner docs, native installer scaffold.
2026-07-06 18:21:23 -04:00

174 lines
5 KiB
Go

package system
// device_test.go — DEVICE-001: form-factor detection against mocked /sys trees.
import (
"os"
"path/filepath"
"testing"
)
// fakeSysTree builds a mock power_supply dir, drm dir, and fb virtual_size file.
type fakeSysTree struct {
powerDir string
drmDir string
fbSize string
}
func newFakeSysTree(t *testing.T) *fakeSysTree {
t.Helper()
root := t.TempDir()
tree := &fakeSysTree{
powerDir: filepath.Join(root, "power_supply"),
drmDir: filepath.Join(root, "drm"),
fbSize: filepath.Join(root, "fb0_virtual_size"),
}
if err := os.MkdirAll(tree.powerDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(tree.drmDir, 0o755); err != nil {
t.Fatal(err)
}
return tree
}
func (f *fakeSysTree) addPowerSupply(t *testing.T, name, typ, scope string) {
t.Helper()
dir := filepath.Join(f.powerDir, name)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "type"), []byte(typ+"\n"), 0o644); err != nil {
t.Fatal(err)
}
if scope != "" {
if err := os.WriteFile(filepath.Join(dir, "scope"), []byte(scope+"\n"), 0o644); err != nil {
t.Fatal(err)
}
}
}
func (f *fakeSysTree) addConnector(t *testing.T, name, status string) {
t.Helper()
dir := filepath.Join(f.drmDir, name)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "status"), []byte(status+"\n"), 0o644); err != nil {
t.Fatal(err)
}
}
func (f *fakeSysTree) setFramebuffer(t *testing.T, size string) {
t.Helper()
if err := os.WriteFile(f.fbSize, []byte(size+"\n"), 0o644); err != nil {
t.Fatal(err)
}
}
func (f *fakeSysTree) detect() string {
return detectDeviceTypeFrom(f.powerDir, f.drmDir, f.fbSize)
}
func TestDetectServer(t *testing.T) {
tree := newFakeSysTree(t)
tree.addPowerSupply(t, "AC", "Mains", "")
tree.addConnector(t, "card0-VGA-1", "disconnected")
if got := tree.detect(); got != "server" {
t.Errorf("[ERROR] [agent] [system] no battery, no display: got %q, want server", got)
}
}
func TestDetectDesktop(t *testing.T) {
tree := newFakeSysTree(t)
tree.addConnector(t, "card0-HDMI-A-1", "connected")
if got := tree.detect(); got != "desktop" {
t.Errorf("[ERROR] [agent] [system] display, no battery: got %q, want desktop", got)
}
}
func TestDetectPhonePixel3(t *testing.T) {
// Pixel 3 blueline: system battery, panel connected, 1080x2160 portrait fb
tree := newFakeSysTree(t)
tree.addPowerSupply(t, "battery", "Battery", "System")
tree.addConnector(t, "card0-DSI-1", "connected")
tree.setFramebuffer(t, "1080,2160")
if got := tree.detect(); got != "phone" {
t.Errorf("[ERROR] [agent] [system] Pixel 3 signals: got %q, want phone", got)
}
}
func TestDetectTablet(t *testing.T) {
tree := newFakeSysTree(t)
tree.addPowerSupply(t, "battery", "Battery", "")
tree.addConnector(t, "card0-DSI-1", "connected")
tree.setFramebuffer(t, "2560,1600")
if got := tree.detect(); got != "tablet" {
t.Errorf("[ERROR] [agent] [system] large battery-backed display: got %q, want tablet", got)
}
}
func TestDetectBatteryNoDisplayIsPhone(t *testing.T) {
tree := newFakeSysTree(t)
tree.addPowerSupply(t, "bms", "Battery", "")
if got := tree.detect(); got != "phone" {
t.Errorf("[ERROR] [agent] [system] battery, no display: got %q, want phone", got)
}
}
func TestPeripheralBatteryDoesNotMakeDesktopMobile(t *testing.T) {
// Bluetooth mouse battery has scope=Device; desktop must stay desktop
tree := newFakeSysTree(t)
tree.addPowerSupply(t, "hid-aa:bb-battery", "Battery", "Device")
tree.addConnector(t, "card0-DP-1", "connected")
if got := tree.detect(); got != "desktop" {
t.Errorf("[ERROR] [agent] [system] peripheral battery misclassified: got %q, want desktop", got)
}
}
func TestUPSDoesNotMakeServerMobile(t *testing.T) {
tree := newFakeSysTree(t)
tree.addPowerSupply(t, "ups", "UPS", "")
if got := tree.detect(); got != "server" {
t.Errorf("[ERROR] [agent] [system] UPS misclassified: got %q, want server", got)
}
}
func TestReadDeviceModelPrefersDeviceTree(t *testing.T) {
dir := t.TempDir()
dt := filepath.Join(dir, "model")
dmi := filepath.Join(dir, "product_name")
if err := os.WriteFile(dt, []byte("Google Pixel 3\x00"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(dmi, []byte("Precision 5820\n"), 0o644); err != nil {
t.Fatal(err)
}
if got := readDeviceModelFrom(dt, dmi); got != "Google Pixel 3" {
t.Errorf("[ERROR] [agent] [system] got %q, want device-tree model", got)
}
if got := readDeviceModelFrom(filepath.Join(dir, "missing"), dmi); got != "Precision 5820" {
t.Errorf("[ERROR] [agent] [system] got %q, want DMI fallback", got)
}
}
func TestOSDistroParsesID(t *testing.T) {
dir := t.TempDir()
osRelease := filepath.Join(dir, "os-release")
content := "NAME=\"Arch Linux ARM\"\nPRETTY_NAME=\"Arch Linux ARM\"\nID=archarm\nID_LIKE=arch\n"
if err := os.WriteFile(osRelease, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
if got := osDistroFrom(osRelease); got != "archarm" {
t.Errorf("[ERROR] [agent] [system] got %q, want archarm", got)
}
}