Close DV-01–04,07,09,13,14: unify operator-deck UX in server/web.
Standardize neon cyan on #00e8f5, wire SacredPageHeader across Builds/Path Tracer/LOTL, dedupe Pages.css, align Path Tracer chrome, drop dead wealth-deck CSS, add prefers-color-scheme shell + HelpTip, sidebar version from package.json build inject, and Vitest CSS regression guards.
This commit is contained in:
@@ -186,6 +186,116 @@ func TestHandleDashboardWSAuthorizedInit(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleDashboardWSInitLimit(t *testing.T) {
|
||||
resetWSAuthUsers(t, testAuthUser, testAuthPass)
|
||||
database, err := db.New(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
hub := NewWSHub(database)
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
if err := database.UpsertAgent(&models.Agent{
|
||||
ID: fmt.Sprintf("agent-%d", i),
|
||||
Name: fmt.Sprintf("Worker %d", i),
|
||||
Status: "offline",
|
||||
LastSeen: time.Now(),
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(hub.HandleDashboardWS))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") +
|
||||
"?token=" + wsDashboardToken(testAuthUser, testAuthPass) + "&init_limit=2&init_offset=1"
|
||||
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("dial: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = conn.Close() })
|
||||
|
||||
var msg Message
|
||||
if err := conn.ReadJSON(&msg); err != nil {
|
||||
t.Fatalf("read init: %v", err)
|
||||
}
|
||||
if msg.Type != "init" {
|
||||
t.Fatalf("expected init, got %q", msg.Type)
|
||||
}
|
||||
var body map[string]interface{}
|
||||
if err := json.Unmarshal(msg.Payload, &body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
agents, _ := body["agents"].([]interface{})
|
||||
if len(agents) != 2 {
|
||||
t.Fatalf("expected 2 agents in init page, got %d", len(agents))
|
||||
}
|
||||
if int(body["total"].(float64)) != 5 {
|
||||
t.Fatalf("total=%v want 5", body["total"])
|
||||
}
|
||||
if int(body["limit"].(float64)) != 2 || int(body["offset"].(float64)) != 1 {
|
||||
t.Fatalf("limit/offset = %v/%v", body["limit"], body["offset"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleDashboardWSTunnelStreamStub(t *testing.T) {
|
||||
resetWSAuthUsers(t, testAuthUser, testAuthPass)
|
||||
database, err := db.New(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
hub := NewWSHub(database)
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(hub.HandleDashboardWS))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") + "?token=" + wsDashboardToken(testAuthUser, testAuthPass)
|
||||
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("dial: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = conn.Close() })
|
||||
|
||||
// Drain init + presence_snapshot.
|
||||
for i := 0; i < 2; i++ {
|
||||
var msg Message
|
||||
if err := conn.ReadJSON(&msg); err != nil {
|
||||
t.Fatalf("read bootstrap %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := conn.WriteJSON(Message{Type: "tunnel_stream", Payload: json.RawMessage(`{"port":8989}`)}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for {
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatal("timed out waiting for tunnel_stream response")
|
||||
}
|
||||
var resp Message
|
||||
if err := conn.ReadJSON(&resp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp.Type != "tunnel_stream" {
|
||||
continue
|
||||
}
|
||||
var body map[string]interface{}
|
||||
if err := json.Unmarshal(resp.Payload, &body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body["implemented"] != false {
|
||||
t.Fatalf("expected implemented=false, got %+v", body)
|
||||
}
|
||||
if _, ok := body["error"].(string); !ok {
|
||||
t.Fatalf("expected error string, got %+v", body)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func resetWSAuthUsersMulti(t *testing.T, creds map[string]string) {
|
||||
t.Helper()
|
||||
users := make(map[string]string, len(creds))
|
||||
@@ -795,6 +905,105 @@ func TestMiningStatusRelayCoalescedToStatsBatch(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatsBatchRelaysFailedMethods(t *testing.T) {
|
||||
resetWSAuthUsers(t, testAuthUser, testAuthPass)
|
||||
database, err := db.New(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
hub := NewWSHub(database)
|
||||
|
||||
dashSrv := httptest.NewServer(http.HandlerFunc(hub.HandleDashboardWS))
|
||||
t.Cleanup(dashSrv.Close)
|
||||
dashURL := "ws" + strings.TrimPrefix(dashSrv.URL, "http") + "?token=" + wsDashboardToken(testAuthUser, testAuthPass)
|
||||
dashConn, _, err := websocket.DefaultDialer.Dial(dashURL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("dial dashboard: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = dashConn.Close() })
|
||||
|
||||
type batchResult struct {
|
||||
updates []map[string]interface{}
|
||||
err string
|
||||
}
|
||||
batchCh := make(chan batchResult, 1)
|
||||
go func() {
|
||||
_ = dashConn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
for {
|
||||
var msg Message
|
||||
if err := dashConn.ReadJSON(&msg); err != nil {
|
||||
batchCh <- batchResult{err: err.Error()}
|
||||
return
|
||||
}
|
||||
if msg.Type != "stats_batch" {
|
||||
continue
|
||||
}
|
||||
var body struct {
|
||||
Updates []json.RawMessage `json:"updates"`
|
||||
}
|
||||
if parseErr := json.Unmarshal(msg.Payload, &body); parseErr != nil {
|
||||
batchCh <- batchResult{err: parseErr.Error()}
|
||||
return
|
||||
}
|
||||
updates := make([]map[string]interface{}, 0, len(body.Updates))
|
||||
for _, raw := range body.Updates {
|
||||
var u map[string]interface{}
|
||||
if json.Unmarshal(raw, &u) != nil {
|
||||
continue
|
||||
}
|
||||
updates = append(updates, u)
|
||||
}
|
||||
if len(updates) < 1 {
|
||||
continue
|
||||
}
|
||||
batchCh <- batchResult{updates: updates}
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
agentID := "failed-methods-agent"
|
||||
conn := connectTestAgent(t, hub, agentID)
|
||||
statsPayload, _ := json.Marshal(map[string]interface{}{
|
||||
"hashrate_15s": 0.0,
|
||||
"active_method": "inprocess",
|
||||
"chain_exhausted": false,
|
||||
"failed_methods": []map[string]interface{}{
|
||||
{"method": "container", "reason": "image not found", "at": "2026-06-07T12:00:00Z"},
|
||||
{"method": "docker_load", "reason": "no tar", "at": "2026-06-07T12:00:01Z"},
|
||||
},
|
||||
})
|
||||
if err := conn.WriteJSON(Message{Type: "stats", Payload: statsPayload}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stopStatsBatchTimer(hub)
|
||||
hub.flushStatsBatch()
|
||||
|
||||
select {
|
||||
case r := <-batchCh:
|
||||
if r.err != "" {
|
||||
t.Fatalf("dashboard did not receive stats_batch: %s", r.err)
|
||||
}
|
||||
if len(r.updates) != 1 {
|
||||
t.Fatalf("expected 1 update, got %d: %+v", len(r.updates), r.updates)
|
||||
}
|
||||
u := r.updates[0]
|
||||
if u["agent_id"] != agentID {
|
||||
t.Errorf("agent_id = %v", u["agent_id"])
|
||||
}
|
||||
failed, ok := u["failed_methods"].([]interface{})
|
||||
if !ok || len(failed) != 2 {
|
||||
t.Fatalf("failed_methods = %T %v", u["failed_methods"], u["failed_methods"])
|
||||
}
|
||||
first, _ := failed[0].(map[string]interface{})
|
||||
if first["method"] != "container" || first["reason"] != "image not found" {
|
||||
t.Errorf("first failure = %v", first)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("timed out waiting for stats_batch with failed_methods")
|
||||
}
|
||||
}
|
||||
|
||||
// stopStatsBatchTimer cancels the 250ms flush timer so tests can read statsBatch
|
||||
// without racing flushStatsBatch clearing the pending map.
|
||||
func stopStatsBatchTimer(hub *WSHub) {
|
||||
|
||||
Reference in New Issue
Block a user