visionA/local-agent/visiona-agent/tunnel_autostart_test.go
jim800121chen a6c94e21c5 fix(local-agent): 重啟自動從 token.bin 讀 token,免每次重配對
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>
2026-08-02 18:54:09 +08:00

113 lines
3.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"errors"
"testing"
"visiona-agent/internal/tunnel"
)
// errTokenStore 是測試用 TokenStoreLoad() 固定回傳指定的 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 為 nilfallback不自動連
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 用真正的
// EncryptedFileTokenStoretoken.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 建新 storetoken.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應自動連 tunnelgot 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 已清)不應自動連 tunnelgot true")
}
}