package device import ( "testing" "visiona-agent/server/internal/driver" ) type testDriver struct { info driver.DeviceInfo connected bool } func (d *testDriver) Info() driver.DeviceInfo { return d.info } func (d *testDriver) Connect() error { d.connected = true d.info.Status = driver.StatusConnected return nil } func (d *testDriver) Disconnect() error { d.connected = false d.info.Status = driver.StatusDisconnected return nil } func (d *testDriver) IsConnected() bool { return d.connected } func (d *testDriver) Flash(_ string, _ chan<- driver.FlashProgress) error { return nil } func (d *testDriver) StartInference() error { return nil } func (d *testDriver) StopInference() error { return nil } func (d *testDriver) ReadInference() (*driver.InferenceResult, error) { return nil, nil } func (d *testDriver) RunInference(_ []byte) (*driver.InferenceResult, error) { return nil, nil } func (d *testDriver) GetModelInfo() (*driver.ModelInfo, error) { return nil, nil } func TestManager_ListDevices(t *testing.T) { registry := NewRegistry() mgr := NewManager(registry, "") mgr.sessions["test-1"] = NewSession(&testDriver{ info: driver.DeviceInfo{ID: "test-1", Name: "Test Device", Type: "KL720", Status: driver.StatusDetected}, }) devices := mgr.ListDevices() if len(devices) != 1 { t.Errorf("ListDevices() = %d, want 1", len(devices)) } } func TestManager_ListDevices_Empty(t *testing.T) { registry := NewRegistry() mgr := NewManager(registry, "") // 無硬體時應回傳空 list(R5-5a:沒插硬體就空白,不給 Mock 資料) devices := mgr.ListDevices() if len(devices) != 0 { t.Errorf("ListDevices() with no hardware = %d, want 0", len(devices)) } } func TestManager_GetDevice(t *testing.T) { registry := NewRegistry() mgr := NewManager(registry, "") mgr.sessions["test-1"] = NewSession(&testDriver{ info: driver.DeviceInfo{ID: "test-1"}, }) t.Run("existing device", func(t *testing.T) { s, err := mgr.GetDevice("test-1") if err != nil { t.Errorf("GetDevice() error = %v", err) } if s == nil { t.Error("GetDevice() returned nil session") } }) t.Run("non-existing device", func(t *testing.T) { _, err := mgr.GetDevice("test-999") if err == nil { t.Error("GetDevice() expected error for non-existing device") } }) } func TestManager_Connect(t *testing.T) { registry := NewRegistry() mgr := NewManager(registry, "") td := &testDriver{info: driver.DeviceInfo{ID: "test-1", Status: driver.StatusDetected}} mgr.sessions["test-1"] = NewSession(td) // Drain event bus in background go func() { for range mgr.Events() { } }() err := mgr.Connect("test-1") if err != nil { t.Errorf("Connect() error = %v", err) } if !td.connected { t.Error("Connect() did not connect device") } } // ========================================================================== // Serial routing (ADR-018 WP-0): serialToLocalID + GetDevice dual lookup // ========================================================================== // seedDevice registers a test driver session and rebuilds the serial index, // mimicking what Start()/Rescan() do after registration. func seedDevice(mgr *Manager, info driver.DeviceInfo) *testDriver { td := &testDriver{info: info} mgr.mu.Lock() mgr.sessions[info.ID] = NewSession(td) mgr.rebuildSerialIndexLocked() mgr.mu.Unlock() return td } func TestManager_GetDevice_BySerial(t *testing.T) { mgr := NewManager(NewRegistry(), "") seedDevice(mgr, driver.DeviceInfo{ID: "kl520-0", SerialNumber: "0x1A2B3C4D"}) t.Run("serial hits the physical device", func(t *testing.T) { s, err := mgr.GetDevice("0x1A2B3C4D") if err != nil { t.Fatalf("GetDevice(serial) error = %v", err) } if got := s.Driver.Info().ID; got != "kl520-0" { t.Errorf("GetDevice(serial) resolved ID = %q, want kl520-0", got) } }) t.Run("local synthetic id still works (backward compat)", func(t *testing.T) { s, err := mgr.GetDevice("kl520-0") if err != nil { t.Fatalf("GetDevice(localID) error = %v", err) } if s == nil { t.Fatal("GetDevice(localID) returned nil session") } }) t.Run("unknown id (e.g. cloud UUID) still misses", func(t *testing.T) { if _, err := mgr.GetDevice("d76718a9-cf15-4795-914e-df5ae46ee536"); err == nil { t.Error("GetDevice(UUID) expected error, got nil") } }) } func TestManager_GetDevice_MultipleSerials(t *testing.T) { mgr := NewManager(NewRegistry(), "") mgr.mu.Lock() mgr.sessions["kl520-0"] = NewSession(&testDriver{info: driver.DeviceInfo{ID: "kl520-0", SerialNumber: "0xAAAA0001"}}) mgr.sessions["kl720-0"] = NewSession(&testDriver{info: driver.DeviceInfo{ID: "kl720-0", SerialNumber: "0xBBBB0002"}}) mgr.rebuildSerialIndexLocked() mgr.mu.Unlock() s1, err := mgr.GetDevice("0xAAAA0001") if err != nil { t.Fatalf("GetDevice(0xAAAA0001) error = %v", err) } if got := s1.Driver.Info().ID; got != "kl520-0" { t.Errorf("serial 0xAAAA0001 resolved to %q, want kl520-0", got) } s2, err := mgr.GetDevice("0xBBBB0002") if err != nil { t.Fatalf("GetDevice(0xBBBB0002) error = %v", err) } if got := s2.Driver.Info().ID; got != "kl720-0" { t.Errorf("serial 0xBBBB0002 resolved to %q, want kl720-0", got) } } func TestManager_SerialIndex_SkipsEmptyAndFakeSerial(t *testing.T) { mgr := NewManager(NewRegistry(), "") mgr.mu.Lock() // Empty serial (bridge could not read kn_number). mgr.sessions["kl520-0"] = NewSession(&testDriver{info: driver.DeviceInfo{ID: "kl520-0"}}) // Fake serial from pyusb fallback (no SDK) — must not be routable. mgr.sessions["kl720-0"] = NewSession(&testDriver{info: driver.DeviceInfo{ID: "kl720-0", SerialNumber: fakeSerialNumber}}) mgr.rebuildSerialIndexLocked() mgr.mu.Unlock() if _, err := mgr.GetDevice(""); err == nil { t.Error("GetDevice(\"\") expected error (empty serial must not be indexed)") } if _, err := mgr.GetDevice(fakeSerialNumber); err == nil { t.Errorf("GetDevice(%s) expected error (fake serial must not be routable)", fakeSerialNumber) } // Devices themselves remain reachable by local id. if _, err := mgr.GetDevice("kl520-0"); err != nil { t.Errorf("GetDevice(kl520-0) error = %v", err) } if _, err := mgr.GetDevice("kl720-0"); err != nil { t.Errorf("GetDevice(kl720-0) error = %v", err) } } func TestManager_SerialIndex_RemovedDeviceUnroutable(t *testing.T) { mgr := NewManager(NewRegistry(), "") seedDevice(mgr, driver.DeviceInfo{ID: "kl520-0", SerialNumber: "0x1A2B3C4D"}) // Simulate Rescan removing the device: delete session + rebuild index. mgr.mu.Lock() delete(mgr.sessions, "kl520-0") mgr.rebuildSerialIndexLocked() mgr.mu.Unlock() if _, err := mgr.GetDevice("0x1A2B3C4D"); err == nil { t.Error("GetDevice(serial) expected error after device removal") } }