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") } }