jim800121chen b9ee184586 feat(device): WP-0 序號串通 + serial 路由(ADR-018 走向 A' 第一階段)
- 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>
2026-07-10 09:31:51 +08:00

106 lines
3.1 KiB
Go

package kneron
import (
"testing"
)
// TestParseScanDevices_WithKnNumber verifies the serial number (kn_number)
// reported by the Python bridge is preserved into DeviceInfo.SerialNumber
// while the synthetic local ID stays untouched (ADR-018 serial routing, WP-0).
func TestParseScanDevices_WithKnNumber(t *testing.T) {
raw := []interface{}{
map[string]interface{}{
"port": "1-2",
"firmware": "KDP",
"kn_number": "0x1A2B3C4D",
"product_id": "0x0100",
},
map[string]interface{}{
"port": "1-3",
"firmware": "KDP2",
"kn_number": "0x0E5F6071",
"product_id": "0x0720",
},
}
devices := parseScanDevices(raw)
if len(devices) != 2 {
t.Fatalf("parseScanDevices() = %d devices, want 2", len(devices))
}
if devices[0].ID != "kl520-0" {
t.Errorf("devices[0].ID = %q, want kl520-0 (synthetic ID must not change)", devices[0].ID)
}
if devices[0].SerialNumber != "0x1A2B3C4D" {
t.Errorf("devices[0].SerialNumber = %q, want 0x1A2B3C4D", devices[0].SerialNumber)
}
if devices[1].ID != "kl720-0" {
t.Errorf("devices[1].ID = %q, want kl720-0", devices[1].ID)
}
if devices[1].SerialNumber != "0x0E5F6071" {
t.Errorf("devices[1].SerialNumber = %q, want 0x0E5F6071", devices[1].SerialNumber)
}
}
// TestParseScanDevices_MissingKnNumber verifies a payload without kn_number
// still yields the full device list (serial empty, no panic, no device drop).
func TestParseScanDevices_MissingKnNumber(t *testing.T) {
raw := []interface{}{
map[string]interface{}{
"port": "1-2",
"firmware": "KDP",
"product_id": "0x0100",
},
}
devices := parseScanDevices(raw)
if len(devices) != 1 {
t.Fatalf("parseScanDevices() = %d devices, want 1 (device must not be dropped)", len(devices))
}
if devices[0].SerialNumber != "" {
t.Errorf("SerialNumber = %q, want empty when kn_number absent", devices[0].SerialNumber)
}
if devices[0].ID != "kl520-0" {
t.Errorf("ID = %q, want kl520-0", devices[0].ID)
}
}
// TestParseScanDevices_NonStringKnNumber verifies a non-string kn_number
// (e.g. raw number instead of formatted hex string) does not panic and the
// device is still registered with an empty serial.
func TestParseScanDevices_NonStringKnNumber(t *testing.T) {
raw := []interface{}{
map[string]interface{}{
"port": "1-2",
"kn_number": float64(123456), // JSON number decodes to float64
"product_id": "0x0200",
},
}
devices := parseScanDevices(raw)
if len(devices) != 1 {
t.Fatalf("parseScanDevices() = %d devices, want 1", len(devices))
}
if devices[0].SerialNumber != "" {
t.Errorf("SerialNumber = %q, want empty for non-string kn_number", devices[0].SerialNumber)
}
}
// TestParseScanDevices_WhitespaceTrimmed verifies kn_number whitespace is trimmed.
func TestParseScanDevices_WhitespaceTrimmed(t *testing.T) {
raw := []interface{}{
map[string]interface{}{
"kn_number": " 0x1A2B3C4D ",
"product_id": "0x0100",
},
}
devices := parseScanDevices(raw)
if len(devices) != 1 {
t.Fatalf("parseScanDevices() = %d devices, want 1", len(devices))
}
if devices[0].SerialNumber != "0x1A2B3C4D" {
t.Errorf("SerialNumber = %q, want trimmed 0x1A2B3C4D", devices[0].SerialNumber)
}
}