- 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>
145 lines
6.2 KiB
Go
145 lines
6.2 KiB
Go
// memPairingExchanger 的單元測試(DB-on FK 收尾,問題 #2)。
|
||
//
|
||
// 不帶 build tag:屬於預設 `go test ./...` 範圍(無需 Docker)。
|
||
// 驗證 in-memory exchanger 自建 device + 建 session token、且 session token 綁到該 device,
|
||
// 與 pairing_exchange_db_test.go 的 Postgres dbtest 對齊行為。
|
||
package api
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
|
||
"visiona-backend/internal/auth"
|
||
"visiona-backend/internal/device"
|
||
)
|
||
|
||
func TestMemExchange_ProvisionCreatesDeviceAndSession(t *testing.T) {
|
||
ctx := context.Background()
|
||
devRepo := device.NewInMemoryRepository()
|
||
sessions := auth.NewInMemorySessionTokenStore()
|
||
exchanger := NewInMemoryPairingExchanger(devRepo, sessions)
|
||
|
||
res, err := exchanger.Provision(ctx, "owner-1", "parent-hash", auth.SessionTokenTTL, nil)
|
||
require.NoError(t, err)
|
||
require.NotEmpty(t, res.DeviceID)
|
||
require.NotEmpty(t, res.SessionPlaintext)
|
||
require.NotNil(t, res.SessionInfo)
|
||
|
||
// device 自建、owner 對齊
|
||
dev, err := devRepo.Get(ctx, res.DeviceID)
|
||
require.NoError(t, err)
|
||
assert.Equal(t, "owner-1", dev.OwnerUserID)
|
||
assert.Equal(t, defaultPairedDeviceName, dev.Name)
|
||
assert.Equal(t, defaultPairedDeviceType, dev.DeviceType)
|
||
assert.NotNil(t, dev.PairedAt)
|
||
|
||
// session token 綁到自建 device
|
||
tok, err := sessions.Get(ctx, res.SessionPlaintext)
|
||
require.NoError(t, err)
|
||
assert.Equal(t, res.DeviceID, tok.DeviceID, "session token 應綁到自建 device")
|
||
assert.Equal(t, "owner-1", tok.UserID)
|
||
assert.Equal(t, "parent-hash", tok.ParentTokenHash)
|
||
}
|
||
|
||
func TestMemExchange_Provision_DistinctDevices(t *testing.T) {
|
||
ctx := context.Background()
|
||
exchanger := NewInMemoryPairingExchanger(
|
||
device.NewInMemoryRepository(), auth.NewInMemorySessionTokenStore())
|
||
|
||
res1, err := exchanger.Provision(ctx, "owner-1", "", auth.SessionTokenTTL, nil)
|
||
require.NoError(t, err)
|
||
res2, err := exchanger.Provision(ctx, "owner-1", "", auth.SessionTokenTTL, nil)
|
||
require.NoError(t, err)
|
||
|
||
assert.NotEqual(t, res1.DeviceID, res2.DeviceID)
|
||
assert.NotEqual(t, res1.SessionPlaintext, res2.SessionPlaintext)
|
||
}
|
||
|
||
// ==========================================================================
|
||
// WP-0(ADR-018 序號地基):exchange 收 agent 上報序號
|
||
// ==========================================================================
|
||
|
||
// TestMemExchange_Provision_FillsSerialNumber 驗證 agent 上報序號時,自建 device
|
||
// 的 serial_number 有值(+ device_type 若有上報)。
|
||
func TestMemExchange_Provision_FillsSerialNumber(t *testing.T) {
|
||
ctx := context.Background()
|
||
devRepo := device.NewInMemoryRepository()
|
||
exchanger := NewInMemoryPairingExchanger(devRepo, auth.NewInMemorySessionTokenStore())
|
||
|
||
res, err := exchanger.Provision(ctx, "owner-1", "", auth.SessionTokenTTL,
|
||
[]ExchangeDeviceInput{{SerialNumber: "0x1A2B3C4D", DeviceType: "kneron_kl520", Firmware: "KDP"}})
|
||
require.NoError(t, err)
|
||
|
||
dev, err := devRepo.Get(ctx, res.DeviceID)
|
||
require.NoError(t, err)
|
||
assert.Equal(t, "0x1A2B3C4D", dev.SerialNumber, "serial_number 應填入 agent 上報值")
|
||
assert.Equal(t, "kneron_kl520", dev.DeviceType, "device_type 有上報時應覆蓋預設")
|
||
}
|
||
|
||
// TestMemExchange_Provision_SameSerialReusesDevice 驗證同序號重複 exchange
|
||
// 復用既有 device(防唯一約束炸裂;「同序號重配 = 復用」,R4)——不新建、
|
||
// 不報錯、session token 各自獨立。
|
||
func TestMemExchange_Provision_SameSerialReusesDevice(t *testing.T) {
|
||
ctx := context.Background()
|
||
devRepo := device.NewInMemoryRepository()
|
||
exchanger := NewInMemoryPairingExchanger(devRepo, auth.NewInMemorySessionTokenStore())
|
||
input := []ExchangeDeviceInput{{SerialNumber: "0x1A2B3C4D"}}
|
||
|
||
res1, err := exchanger.Provision(ctx, "owner-1", "", auth.SessionTokenTTL, input)
|
||
require.NoError(t, err)
|
||
res2, err := exchanger.Provision(ctx, "owner-1", "", auth.SessionTokenTTL, input)
|
||
require.NoError(t, err, "同序號重配不可失敗(防 23505 的行為對齊)")
|
||
|
||
assert.Equal(t, res1.DeviceID, res2.DeviceID, "同序號重配應復用既有 device")
|
||
assert.NotEqual(t, res1.SessionPlaintext, res2.SessionPlaintext, "session token 應各自獨立")
|
||
|
||
devices, err := devRepo.List(ctx, "owner-1")
|
||
require.NoError(t, err)
|
||
assert.Len(t, devices, 1, "同序號重配不應多建 device")
|
||
}
|
||
|
||
// TestMemExchange_Provision_FakeSerialTreatedAsEmpty 驗證假序號 0x00000000
|
||
// (macOS 無 SDK 的 pyusb fallback)視同無序號:serial 留空、行為與不帶序號一致
|
||
// (每次 exchange 各建一筆 distinct device)。
|
||
func TestMemExchange_Provision_FakeSerialTreatedAsEmpty(t *testing.T) {
|
||
ctx := context.Background()
|
||
devRepo := device.NewInMemoryRepository()
|
||
exchanger := NewInMemoryPairingExchanger(devRepo, auth.NewInMemorySessionTokenStore())
|
||
input := []ExchangeDeviceInput{{SerialNumber: "0x00000000"}}
|
||
|
||
res1, err := exchanger.Provision(ctx, "owner-1", "", auth.SessionTokenTTL, input)
|
||
require.NoError(t, err)
|
||
res2, err := exchanger.Provision(ctx, "owner-1", "", auth.SessionTokenTTL, input)
|
||
require.NoError(t, err)
|
||
|
||
dev1, err := devRepo.Get(ctx, res1.DeviceID)
|
||
require.NoError(t, err)
|
||
assert.Empty(t, dev1.SerialNumber, "假序號應視同無序號(寫 NULL)")
|
||
assert.NotEqual(t, res1.DeviceID, res2.DeviceID, "假序號不進復用分支,各建 distinct device")
|
||
}
|
||
|
||
// TestMemExchange_Provision_PicksFirstUsableSerial 驗證多顆上報時取第一顆
|
||
// 可用序號(空序號 / 假序號跳過)——WP-0 最小落地(R1:多顆完整模型是 WP-B)。
|
||
func TestMemExchange_Provision_PicksFirstUsableSerial(t *testing.T) {
|
||
ctx := context.Background()
|
||
devRepo := device.NewInMemoryRepository()
|
||
exchanger := NewInMemoryPairingExchanger(devRepo, auth.NewInMemorySessionTokenStore())
|
||
|
||
res, err := exchanger.Provision(ctx, "owner-1", "", auth.SessionTokenTTL,
|
||
[]ExchangeDeviceInput{
|
||
{SerialNumber: ""}, // 空序號跳過
|
||
{SerialNumber: "0x00000000"}, // 假序號跳過
|
||
{SerialNumber: " 0x0E5F6071 ", DeviceType: "kneron_kl720"},
|
||
{SerialNumber: "0xFFFF0001"},
|
||
})
|
||
require.NoError(t, err)
|
||
|
||
dev, err := devRepo.Get(ctx, res.DeviceID)
|
||
require.NoError(t, err)
|
||
assert.Equal(t, "0x0E5F6071", dev.SerialNumber, "應取第一顆可用序號(trim 空白)")
|
||
assert.Equal(t, "kneron_kl720", dev.DeviceType)
|
||
}
|