app.go 只在 env VISIONA_SESSION_TOKEN 有值時才啟動 tunnel、env 空就等 UI Pair, 從沒觸發 token.bin 的載入 → 每次重啟都要重配對。(Manager.startLocked 本來就會 從 config.TokenStore.Load,只是沒被呼叫。) 修法:抽 shouldAutoStartTunnel — env 有 token 用 env(優先序不變);env 空則 tokenStore.Load(),有值就自動 Start(免重配對)、空/err/nil store 等 UI Pair 不擋啟動。 Unpair→Delete token.bin→重啟 Load 回空→不自動連(round-trip test 覆蓋)。 reviewer 通過(0C/0M/1m 雙Load技術債)。build/vet/test 全過 + 5 分支 + Unpair round-trip 測試。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
113 lines
3.7 KiB
Go
113 lines
3.7 KiB
Go
package main
|
||
|
||
import (
|
||
"errors"
|
||
"testing"
|
||
|
||
"visiona-agent/internal/tunnel"
|
||
)
|
||
|
||
// errTokenStore 是測試用 TokenStore,Load() 固定回傳指定的 token / error,
|
||
// 用來覆蓋 shouldAutoStartTunnel 的三種 Load 結果分支。
|
||
type errTokenStore struct {
|
||
token string
|
||
loadErr error
|
||
}
|
||
|
||
func (s *errTokenStore) Save(string) error { return nil }
|
||
func (s *errTokenStore) Load() (string, error) { return s.token, s.loadErr }
|
||
func (s *errTokenStore) Delete() error { return nil }
|
||
|
||
// TestShouldAutoStartTunnel 覆蓋 bug 修復的決策邏輯:
|
||
// - env 有 token(優先序不變)
|
||
// - env 空 + tokenStore 有 token(修復核心:免重配對自動連)
|
||
// - env 空 + tokenStore 空(沒配對過 / Unpair 後:等 UI Pair,不自動連)
|
||
// - env 空 + tokenStore Load 失敗(解密/IO 錯誤:不擋啟動、不自動連)
|
||
// - env 空 + tokenStore 為 nil(fallback:不自動連)
|
||
func TestShouldAutoStartTunnel(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
envToken string
|
||
store tunnel.TokenStore
|
||
want bool
|
||
}{
|
||
{
|
||
name: "env token set → start (env 優先,dev/demo 路徑不斷)",
|
||
envToken: "vAs_from_env",
|
||
store: &errTokenStore{token: "vAs_from_store"}, // 即使 store 也有,env 仍優先
|
||
want: true,
|
||
},
|
||
{
|
||
name: "env empty + store has token → start (修復核心:免重配對)",
|
||
envToken: "",
|
||
store: &errTokenStore{token: "vAs_from_store"},
|
||
want: true,
|
||
},
|
||
{
|
||
name: "env empty + store empty → no start (沒配對過 / Unpair 後等 UI Pair)",
|
||
envToken: "",
|
||
store: &errTokenStore{token: ""},
|
||
want: false,
|
||
},
|
||
{
|
||
name: "env empty + store load error → no start (不擋啟動、等 UI Pair)",
|
||
envToken: "",
|
||
store: &errTokenStore{loadErr: errors.New("decrypt failed")},
|
||
want: false,
|
||
},
|
||
{
|
||
name: "env empty + nil store → no start (fallback)",
|
||
envToken: "",
|
||
store: nil,
|
||
want: false,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
got := shouldAutoStartTunnel(tt.envToken, tt.store, nil)
|
||
if got != tt.want {
|
||
t.Errorf("shouldAutoStartTunnel(%q, %v) = %v, want %v",
|
||
tt.envToken, tt.store, got, tt.want)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestShouldAutoStartTunnel_EncryptedStoreRoundTrip 用真正的
|
||
// EncryptedFileTokenStore(token.bin)驗證「Save 後重啟 → 自動連」的端到端行為,
|
||
// 對齊使用者實際情境(配對存 token.bin,重啟後應免重配對)。
|
||
func TestShouldAutoStartTunnel_EncryptedStoreRoundTrip(t *testing.T) {
|
||
dir := t.TempDir()
|
||
|
||
// 第一次配對:建立 store 並存 token(模擬配對流程的 Save)。
|
||
store1, err := tunnel.NewEncryptedFileTokenStore(dir, nil)
|
||
if err != nil {
|
||
t.Fatalf("NewEncryptedFileTokenStore: %v", err)
|
||
}
|
||
if err := store1.Save("vAs_persisted_token"); err != nil {
|
||
t.Fatalf("Save: %v", err)
|
||
}
|
||
|
||
// 模擬重啟:用同一個 dataDir 建新 store(token.bin 已存在),env 沒設 token。
|
||
store2, err := tunnel.NewEncryptedFileTokenStore(dir, nil)
|
||
if err != nil {
|
||
t.Fatalf("NewEncryptedFileTokenStore (restart): %v", err)
|
||
}
|
||
if got := shouldAutoStartTunnel("", store2, nil); !got {
|
||
t.Error("重啟後(env 空 + token.bin 有 token)應自動連 tunnel,got false")
|
||
}
|
||
|
||
// Unpair 後(Delete token):重啟應回到未配對、等 UI Pair。
|
||
if err := store2.Delete(); err != nil {
|
||
t.Fatalf("Delete: %v", err)
|
||
}
|
||
store3, err := tunnel.NewEncryptedFileTokenStore(dir, nil)
|
||
if err != nil {
|
||
t.Fatalf("NewEncryptedFileTokenStore (after unpair): %v", err)
|
||
}
|
||
if got := shouldAutoStartTunnel("", store3, nil); got {
|
||
t.Error("Unpair 後(token.bin 已清)不應自動連 tunnel,got true")
|
||
}
|
||
}
|