- local-agent/server:detector 保留 kn_number(parseScanDevices 可測化、 合成 ID 語意不動)+ DeviceInfo.SerialNumber + Manager serialToLocalID 反查表 + GetDevice 雙查(sessions 先、serial 後,純加法) - visiona-agent:pairing exchange 帶本地裝置清單(DeviceLister 失敗不中斷 配對、timeout 2s、omitempty 舊版相容) - visionA-backend:exchange 收 devices —— R1 取第一顆可用序號、R2 假序號 0x00000000 寫 NULL、R4 同 owner 同序號復用既有 device_id(防 23505)、 pg+mem 兩實作對齊 - 五個 proxy 操作(flash/inference/camera/connect/disconnect)收斂於 GetDevice 單一入口,serial 路由一處涵蓋 - docs:api-spec.md §2 增補 POST /api/pairing/exchange(schema + R1/R2/R4) - 測試:行為級四環節鏈 + dbtest 130 實跑 6/6 + DBOn 回歸 4/4; 三 module build/vet/test 全綠 - review:通過 0C/0M/6Mi/5Sug(.autoflow/05-implementation/review/ wp0-serial-routing-review.md);Minor #1 Rescan stale session 掛 WP-C 前置 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
214 lines
6.7 KiB
Go
214 lines
6.7 KiB
Go
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")
|
||
}
|
||
}
|