WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s

This commit is contained in:
yawan
2026-09-22 16:19:06 +08:00
commit f7fc992f57
3929 changed files with 1006607 additions and 0 deletions
+152
View File
@@ -0,0 +1,152 @@
package wecom
import (
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
ws "github.com/gorilla/websocket"
)
// newTestConn dials a throwaway WebSocket server and returns the client side.
// The server drains frames until the client goes away.
func newTestConn(t *testing.T) *ws.Conn {
t.Helper()
upgrader := ws.Upgrader{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
serverConn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer func() { _ = serverConn.Close() }()
for {
if _, _, err := serverConn.ReadMessage(); err != nil {
return
}
}
}))
t.Cleanup(server.Close)
conn, _, err := ws.DefaultDialer.Dial("ws"+strings.TrimPrefix(server.URL, "http"), nil)
if err != nil {
t.Fatalf("dial test websocket: %v", err)
}
t.Cleanup(func() { _ = conn.Close() })
return conn
}
// connUsable reports whether conn still accepts writes.
func connUsable(t *testing.T, conn *ws.Conn) bool {
t.Helper()
_ = conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
return conn.WriteMessage(ws.TextMessage, []byte("probe")) == nil
}
func TestCloseConnClosesAndClearsActiveConnection(t *testing.T) {
conn := newTestConn(t)
c := &LongConnClient{conn: conn}
c.closeConn()
if c.conn != nil {
t.Error("closeConn left c.conn set")
}
if connUsable(t, conn) {
t.Error("closeConn did not close the connection")
}
}
func TestStopClosesActiveConnection(t *testing.T) {
conn := newTestConn(t)
c := &LongConnClient{conn: conn}
c.Stop()
if !c.closed.Load() {
t.Error("Stop did not mark the client closed")
}
if c.conn != nil {
t.Error("Stop left c.conn set")
}
if connUsable(t, conn) {
t.Error("Stop did not close the connection")
}
}
func TestCloseConnIfClosesActiveConnection(t *testing.T) {
conn := newTestConn(t)
c := &LongConnClient{conn: conn}
c.closeConnIf(conn)
if c.conn != nil {
t.Error("closeConnIf left c.conn set")
}
if connUsable(t, conn) {
t.Error("closeConnIf did not close the active connection")
}
}
// A heartbeat loop or detached callback handler from a previous connection
// generation must not tear down the connection opened by a later reconnect.
func TestCloseConnIfIgnoresSupersededConnection(t *testing.T) {
stale := newTestConn(t)
active := newTestConn(t)
c := &LongConnClient{conn: active}
c.closeConnIf(stale)
if c.conn != active {
t.Fatal("closeConnIf cleared the active connection on behalf of a stale caller")
}
if !connUsable(t, active) {
t.Fatal("closeConnIf closed the active connection on behalf of a stale caller")
}
}
func TestCloseConnIfIgnoresNilConnection(t *testing.T) {
conn := newTestConn(t)
c := &LongConnClient{conn: conn}
c.closeConnIf(nil)
if c.conn != conn {
t.Error("closeConnIf(nil) cleared the active connection")
}
if !connUsable(t, conn) {
t.Error("closeConnIf(nil) closed the active connection")
}
}
// Exercises the close and write paths together so the race detector can catch
// any regression that reintroduces unsynchronized access to c.conn.
func TestCloseConnRacesWithWriteJSON(t *testing.T) {
for i := 0; i < 20; i++ {
conn := newTestConn(t)
c := &LongConnClient{conn: conn}
var wg sync.WaitGroup
wg.Add(3)
go func() {
defer wg.Done()
_ = c.writeJSON(wsFrame{Cmd: cmdPing})
}()
go func() {
defer wg.Done()
c.closeConnIf(conn)
}()
go func() {
defer wg.Done()
c.closeConn()
}()
wg.Wait()
if c.conn != nil {
t.Fatal("connection pointer still set after concurrent closes")
}
}
}