package ws // hub_sentinel_test.go — M8-4b:驗證 Hub 在第一個 client 連上時寫 sentinel file // // 測試對應 .autoflow/04-architecture/v2/startup-pipeline.md §3 階段 6。 import ( "os" "path/filepath" "testing" "time" ) // 模擬一個 dummy client(不需要真的 WebSocket 連線,Hub.Run 只關心 *Client pointer) func dummyClient() *Client { return &Client{Send: make(chan []byte, 1)} } func TestHub_StartupSentinel_WrittenOnFirstRegister(t *testing.T) { dir, err := os.MkdirTemp("", "ws-hub-sentinel-*") if err != nil { t.Fatalf("mkdtemp: %v", err) } defer os.RemoveAll(dir) hub := NewHub() hub.SetStartupSentinel(dir) go hub.Run() // 第一個 client 加入任意 room hub.RegisterSync(&Subscription{Client: dummyClient(), Room: "test-room"}) // sentinel 應該在 dir 下出現 path := filepath.Join(dir, ".first-ws-connected") if _, err := os.Stat(path); err != nil { t.Fatalf("sentinel file 應該被寫入:%v", err) } } func TestHub_StartupSentinel_WrittenOnlyOnce(t *testing.T) { dir, err := os.MkdirTemp("", "ws-hub-sentinel-once-*") if err != nil { t.Fatalf("mkdtemp: %v", err) } defer os.RemoveAll(dir) hub := NewHub() hub.SetStartupSentinel(dir) go hub.Run() // 第一個 client hub.RegisterSync(&Subscription{Client: dummyClient(), Room: "test-room"}) path := filepath.Join(dir, ".first-ws-connected") info1, err := os.Stat(path) if err != nil { t.Fatalf("first sentinel: %v", err) } // 等一點時間確保 mtime 會不同(如果有寫第二次) time.Sleep(50 * time.Millisecond) // 第二、三個 client 加入 hub.RegisterSync(&Subscription{Client: dummyClient(), Room: "test-room"}) hub.RegisterSync(&Subscription{Client: dummyClient(), Room: "another-room"}) info2, err := os.Stat(path) if err != nil { t.Fatalf("second stat: %v", err) } if !info1.ModTime().Equal(info2.ModTime()) { t.Fatalf("sentinel file 不應該被重寫;mtime1=%v mtime2=%v", info1.ModTime(), info2.ModTime()) } } func TestHub_StartupSentinel_DisabledWhenDataDirEmpty(t *testing.T) { hub := NewHub() // 不呼叫 SetStartupSentinel → sentinelDataDir 為空 go hub.Run() // 加 client 不該 panic、也不該寫任何檔案 hub.RegisterSync(&Subscription{Client: dummyClient(), Room: "test-room"}) // 沒有路徑可以驗證,這個測試主要驗證「不 panic」即可 }