DB 接入塊 0-5 上主幹後的收尾工作,讓 DB-on 模式可真人使用 + 補齊功能與測試。 OIDC / pairing FK 修復(接 DB 上線必要): - 新建 internal/user package(User + Store + InMemory + Postgres);OIDC callback 驗證 id_token 成功後 fail-closed upsert users(sub 直接當 users.id,MC sub 為 UUID) - pairing exchange 雲端自建 device(不動 local-tool)+ 同 tx 綁 session token; 自建 device 空 serial 寫 NULL(避免撞 partial unique) - device.SaveTx / session.CreateTx 新增 tx-aware 版本 B4 model metadata: - 轉檔 result 的 analysis_info(input_shape/classes/framework)串進 model: converter_client → flow → adapter → model.Model → PG → ModelResponse DTO - input_shape 優先用陣列、後備四維組 NCHW、缺一不亂組;全 optional 防禦性 - 前端詳細頁顯示(另 repo);轉檔端串接交接檔 b4-converter-handoff.md nginx healthz(部署層): - 新增 /healthz/deep 轉發 backend(ping PG+Redis、down 回 503)給 LB - 修掉 default_server return 444 短路 bug(docker healthcheck 長期 unhealthy 真因) storage error 統一映射(不洩漏 storage 後端細節)。 測試:補 internal/api(storage/errors handler)、cmd/api-server(seed/adapter)、 internal/db(redis)、relay/session 弱處,含 testcontainers integration。 DB 接入相關 package 真環境覆蓋達 88-94%。全程 Reviewer 審查 + 130 真 PG/Redis dbtest 綠。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
165 lines
5.4 KiB
Go
165 lines
5.4 KiB
Go
package relay
|
||
|
||
import (
|
||
"context"
|
||
"net"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/hashicorp/yamux"
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
|
||
"visiona-backend/internal/session"
|
||
)
|
||
|
||
// owner: testing agent(internal/relay local_handle 補測)
|
||
//
|
||
// 測試策略:LocalHandle 包住 *yamux.Session。我們不需要真實 WebSocket / 真實 tunnel —
|
||
// yamux 可直接跑在 net.Pipe() 之上(in-memory、純 Go、不需 docker),
|
||
// 這已足以驗證 LocalHandle 的「session 開 stream / 關閉 / IsClosed」邏輯。
|
||
// 真正端對端的 yamux-over-WebSocket 由 relay 的 integration_test.go 涵蓋。
|
||
|
||
// newYamuxPair 用 net.Pipe 建一對 yamux session(server 給 LocalHandle,client 端配合)。
|
||
// 回傳 server session(LocalHandle 用)+ client session(驅動 Accept,避免 Open 卡住)。
|
||
func newYamuxPair(t *testing.T) (server, client *yamux.Session) {
|
||
t.Helper()
|
||
c1, c2 := net.Pipe()
|
||
|
||
srv, err := yamux.Server(c1, yamux.DefaultConfig())
|
||
require.NoError(t, err, "yamux.Server")
|
||
cli, err := yamux.Client(c2, yamux.DefaultConfig())
|
||
require.NoError(t, err, "yamux.Client")
|
||
|
||
t.Cleanup(func() {
|
||
_ = srv.Close()
|
||
_ = cli.Close()
|
||
_ = c1.Close()
|
||
_ = c2.Close()
|
||
})
|
||
return srv, cli
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Summary / RecordHeartbeat — 純邏輯(mutex-protected,不碰 yamux)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
func TestLocalHandle_Summary_ReflectsConstructorArgs(t *testing.T) {
|
||
srv, _ := newYamuxPair(t)
|
||
h := NewLocalHandle(srv, "vAc_tok", "203.0.113.7:54321")
|
||
|
||
sum := h.Summary()
|
||
require.NotNil(t, sum)
|
||
assert.Equal(t, "vAc_tok", sum.Token)
|
||
assert.Equal(t, "203.0.113.7:54321", sum.RemoteAddr)
|
||
assert.False(t, sum.ConnectedAt.IsZero(), "ConnectedAt 應已設定")
|
||
assert.False(t, sum.LastHeartbeat.IsZero(), "LastHeartbeat 應已設定")
|
||
// ConnectedAt 與 LastHeartbeat 初始相同
|
||
assert.Equal(t, sum.ConnectedAt, sum.LastHeartbeat)
|
||
}
|
||
|
||
func TestLocalHandle_Summary_ReturnsCopy(t *testing.T) {
|
||
srv, _ := newYamuxPair(t)
|
||
h := NewLocalHandle(srv, "vAc_tok", "addr")
|
||
|
||
// caller 改 snapshot 不應影響內部狀態(防中間態觀察 / 並發寫)。
|
||
s1 := h.Summary()
|
||
s1.Token = "MUTATED"
|
||
s1.LastHeartbeat = time.Unix(0, 0)
|
||
|
||
s2 := h.Summary()
|
||
assert.Equal(t, "vAc_tok", s2.Token, "Summary() 必須回副本,caller 修改不得污染內部")
|
||
assert.NotEqual(t, time.Unix(0, 0), s2.LastHeartbeat)
|
||
}
|
||
|
||
func TestLocalHandle_RecordHeartbeat_UpdatesLastHeartbeat(t *testing.T) {
|
||
srv, _ := newYamuxPair(t)
|
||
h := NewLocalHandle(srv, "vAc_tok", "addr")
|
||
|
||
before := h.Summary().LastHeartbeat
|
||
newTime := before.Add(30 * time.Second)
|
||
h.RecordHeartbeat(newTime)
|
||
|
||
after := h.Summary().LastHeartbeat
|
||
assert.True(t, after.Equal(newTime), "RecordHeartbeat 應更新 LastHeartbeat:want %v got %v", newTime, after)
|
||
// ConnectedAt 不應被動到
|
||
assert.Equal(t, before, h.Summary().ConnectedAt)
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// OpenStream / IsClosed / Close — 跑在 net.Pipe 上的真 yamux(in-memory)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
func TestLocalHandle_OpenStream_Succeeds(t *testing.T) {
|
||
srv, cli := newYamuxPair(t)
|
||
h := NewLocalHandle(srv, "vAc_tok", "addr")
|
||
|
||
// client 端需要 Accept 才能讓 server 端 Open 完成 handshake。
|
||
acceptDone := make(chan net.Conn, 1)
|
||
go func() {
|
||
stream, err := cli.Accept()
|
||
if err == nil {
|
||
acceptDone <- stream
|
||
} else {
|
||
acceptDone <- nil
|
||
}
|
||
}()
|
||
|
||
conn, err := h.OpenStream(context.Background())
|
||
require.NoError(t, err, "session 健在時 OpenStream 應成功")
|
||
require.NotNil(t, conn)
|
||
t.Cleanup(func() { _ = conn.Close() })
|
||
|
||
select {
|
||
case s := <-acceptDone:
|
||
require.NotNil(t, s, "client 端應 Accept 到對應 stream")
|
||
_ = s.Close()
|
||
case <-time.After(2 * time.Second):
|
||
t.Fatal("client 端未在 2s 內 Accept 到 stream")
|
||
}
|
||
}
|
||
|
||
func TestLocalHandle_IsClosed_FalseWhenLive(t *testing.T) {
|
||
srv, _ := newYamuxPair(t)
|
||
h := NewLocalHandle(srv, "vAc_tok", "addr")
|
||
assert.False(t, h.IsClosed(), "活的 session IsClosed 應為 false")
|
||
}
|
||
|
||
func TestLocalHandle_Close_MarksClosed(t *testing.T) {
|
||
srv, _ := newYamuxPair(t)
|
||
h := NewLocalHandle(srv, "vAc_tok", "addr")
|
||
|
||
require.NoError(t, h.Close())
|
||
assert.True(t, h.IsClosed(), "Close 後 IsClosed 應為 true")
|
||
}
|
||
|
||
// TestLocalHandle_OpenStream_AfterClose 驗證 close 後 OpenStream 回 ErrSessionClosed。
|
||
func TestLocalHandle_OpenStream_AfterClose(t *testing.T) {
|
||
srv, _ := newYamuxPair(t)
|
||
h := NewLocalHandle(srv, "vAc_tok", "addr")
|
||
|
||
require.NoError(t, h.Close())
|
||
|
||
_, err := h.OpenStream(context.Background())
|
||
assert.ErrorIs(t, err, session.ErrSessionClosed, "已關閉的 session OpenStream 應回 ErrSessionClosed")
|
||
}
|
||
|
||
// TestLocalHandle_OpenStream_CtxCancelled 驗證 ctx 已取消時 OpenStream 早退回 ctx.Err()。
|
||
func TestLocalHandle_OpenStream_CtxCancelled(t *testing.T) {
|
||
srv, _ := newYamuxPair(t)
|
||
h := NewLocalHandle(srv, "vAc_tok", "addr")
|
||
|
||
ctx, cancel := context.WithCancel(context.Background())
|
||
cancel() // 立即取消
|
||
|
||
_, err := h.OpenStream(ctx)
|
||
require.Error(t, err)
|
||
assert.ErrorIs(t, err, context.Canceled, "ctx 取消時 OpenStream 應回 ctx.Err()")
|
||
}
|
||
|
||
// TestLocalHandle_ImplementsHandle 編譯期 + 執行期確認介面一致。
|
||
func TestLocalHandle_ImplementsHandle(t *testing.T) {
|
||
srv, _ := newYamuxPair(t)
|
||
var _ session.Handle = NewLocalHandle(srv, "t", "a")
|
||
}
|