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.
174 lines
5 KiB
Go
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)
|
|
}
|
|
}
|