Final sweep: Crucible fixes, Path Tracer polish, forge progress, tests green.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Align dashboard subtitle default and UpsertAgent tests with fleet label behavior; WebSocket coalesce and PathForge hardening; Crucible expanded ops and visual DV fixes; Vitest 610/610 and full test-suite pass; trim PROBLEMS.md to open items only.
This commit is contained in:
@@ -623,6 +623,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
||||
if pathForgeHandler != nil {
|
||||
r.Post("/builder/path-forge", pathForgeHandler.ServeHTTP)
|
||||
}
|
||||
r.Get("/builder/progress/{token}", builderHandler.ServeProgress)
|
||||
r.Delete("/builder/cancel/{token}", func(w http.ResponseWriter, req *http.Request) {
|
||||
token := chi.URLParam(req, "token")
|
||||
if builderHandler.CancelBuild(token) {
|
||||
|
||||
@@ -688,11 +688,18 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Connect to pool in background — do NOT block the auth_response.
|
||||
// The agent can start and the pool proxy will be ready by the time
|
||||
// the first share is submitted.
|
||||
// Once the pool is ready, push the current job so the agent starts
|
||||
// mining immediately instead of waiting for a get_job retry cycle.
|
||||
go func(pc pool.Config, bcs []pool.Config, aid string) {
|
||||
if _, err := h.poolManager.EnsurePoolWithBackups(&pc, bcs); err != nil {
|
||||
proxy, err := h.poolManager.EnsurePoolWithBackups(&pc, bcs)
|
||||
if err != nil {
|
||||
log.Printf("[WS] Pool init for agent %s failed (will retry): %v", aid, err)
|
||||
return
|
||||
}
|
||||
if job := proxy.GetCurrentJob(); job != nil {
|
||||
if wErr := h.writeAgentJSON(aid, Message{Type: "new_job", Payload: mustMarshal(job)}); wErr != nil {
|
||||
log.Printf("[WS] Push initial job to agent %s: %v", aid, wErr)
|
||||
}
|
||||
}
|
||||
}(poolCfg, backupCfgs, agentID)
|
||||
}
|
||||
@@ -794,10 +801,21 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
go h.runPingLoopAgent(ac)
|
||||
}
|
||||
|
||||
conn.WriteJSON(Message{Type: "auth_response", Payload: mustMarshal(map[string]interface{}{
|
||||
"success": true,
|
||||
"agent_id": agentID,
|
||||
})})
|
||||
conn.WriteJSON(Message{Type: "auth_response", Payload: mustMarshal(map[string]interface{}{
|
||||
"success": true,
|
||||
"agent_id": agentID,
|
||||
})})
|
||||
|
||||
// Auto-start mining: ensure the agent isn't stuck in a paused
|
||||
// state from a previous session. The agent's in-memory pause flag
|
||||
// resets on each restart, but sending resume is a cheap no-op and
|
||||
// guarantees hashing begins as soon as a job arrives.
|
||||
if h.agentPoolConfig(agentID).Wallet != "" {
|
||||
_ = h.writeAgentJSON(agentID, Message{
|
||||
Type: "command",
|
||||
Payload: mustMarshal(map[string]interface{}{"action": "resume"}),
|
||||
})
|
||||
}
|
||||
|
||||
// Enrich agent with hostname before broadcasting so the dashboard
|
||||
// immediately shows the correct machine-specific display name.
|
||||
|
||||
@@ -420,3 +420,182 @@ func TestWSHubConnectedAgentCount(t *testing.T) {
|
||||
t.Fatalf("expected 1 connected agent, got %d", hub.connectedAgentCount())
|
||||
}
|
||||
}
|
||||
|
||||
// TestAgentNamePreservedOnReconnect checks that an operator-assigned display
|
||||
// name is not overwritten by the machine hostname when the agent reconnects.
|
||||
func TestAgentNamePreservedOnReconnect(t *testing.T) {
|
||||
database, err := db.New(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
hub := NewWSHub(database)
|
||||
|
||||
// Seed the DB with an agent whose name was customised by the operator.
|
||||
// The hostname field records what the machine reported; the name has been
|
||||
// changed to something different, so it should be preserved on reconnect.
|
||||
if err := database.UpsertAgent(&models.Agent{
|
||||
ID: "renamed-agent",
|
||||
Name: "Living Room PC",
|
||||
Hostname: "DESKTOP-ABC123",
|
||||
Status: "offline",
|
||||
LastSeen: time.Now().Add(-5 * time.Minute),
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Agent reconnects — it reports the same hostname.
|
||||
conn, _ := dialAgentWS(t, hub)
|
||||
resp := authAgentConn(t, conn, map[string]interface{}{
|
||||
"agent_id": "renamed-agent",
|
||||
"hostname": "DESKTOP-ABC123",
|
||||
"version": "1.0",
|
||||
})
|
||||
|
||||
var body map[string]interface{}
|
||||
if err := json.Unmarshal(resp.Payload, &body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body["success"] != true {
|
||||
t.Fatalf("auth should succeed: %+v", body)
|
||||
}
|
||||
|
||||
// Give the auth handler a moment to commit the upsert.
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
|
||||
agent, err := database.GetAgent("renamed-agent")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if agent.Name != "Living Room PC" {
|
||||
t.Errorf("operator name should be preserved; got %q", agent.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAgentNameUpdatesFromHostnameWhenDefault verifies that the name IS updated
|
||||
// when it was never customised (name == hostname, i.e. the default).
|
||||
// TestCommandResultBroadcastToDashboard is the critical end-to-end test that
|
||||
// verifies the full agent→server→dashboard broadcast of command_result.
|
||||
// It was added to cover the gap identified in the Crucible terminal bug investigation.
|
||||
func TestCommandResultBroadcastToDashboard(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)
|
||||
|
||||
// ── Connect dashboard WS ──────────────────────────────────────────────
|
||||
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() })
|
||||
|
||||
// Read all dashboard messages in a goroutine to avoid blocking and to
|
||||
// keep the connection alive (no SetReadDeadline, which would permanently
|
||||
// corrupt the gorilla/websocket connection on timeout).
|
||||
type msgResult struct {
|
||||
body map[string]interface{}
|
||||
err string
|
||||
}
|
||||
cmdResultCh := make(chan msgResult, 1)
|
||||
go func() {
|
||||
_ = dashConn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
for {
|
||||
var msg Message
|
||||
if err := dashConn.ReadJSON(&msg); err != nil {
|
||||
cmdResultCh <- msgResult{err: err.Error()}
|
||||
return
|
||||
}
|
||||
if msg.Type != "command_result" {
|
||||
continue // skip init, presence_snapshot, agent_online, etc.
|
||||
}
|
||||
var body map[string]interface{}
|
||||
if parseErr := json.Unmarshal(msg.Payload, &body); parseErr != nil {
|
||||
cmdResultCh <- msgResult{err: "parse: " + parseErr.Error()}
|
||||
return
|
||||
}
|
||||
cmdResultCh <- msgResult{body: body}
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// ── Connect + authenticate agent WS ──────────────────────────────────
|
||||
agentID := "e2e-agent-001"
|
||||
agentConn := connectTestAgent(t, hub, agentID)
|
||||
|
||||
// ── Agent sends command_result ────────────────────────────────────────
|
||||
cmdPayload, _ := json.Marshal(map[string]interface{}{
|
||||
"action": "exec",
|
||||
"success": true,
|
||||
"message": "hello from agent",
|
||||
})
|
||||
if err := agentConn.WriteJSON(Message{Type: "command_result", Payload: cmdPayload}); err != nil {
|
||||
t.Fatalf("send command_result: %v", err)
|
||||
}
|
||||
|
||||
// ── Dashboard must receive the broadcast ─────────────────────────────
|
||||
select {
|
||||
case r := <-cmdResultCh:
|
||||
if r.err != "" {
|
||||
t.Fatalf("dashboard did not receive command_result: %s", r.err)
|
||||
}
|
||||
if r.body["agent_id"] != agentID {
|
||||
t.Errorf("agent_id: got %v, want %v", r.body["agent_id"], agentID)
|
||||
}
|
||||
if r.body["action"] != "exec" {
|
||||
t.Errorf("action: got %v, want exec", r.body["action"])
|
||||
}
|
||||
if r.body["success"] != true {
|
||||
t.Errorf("success: got %v, want true", r.body["success"])
|
||||
}
|
||||
if r.body["message"] != "hello from agent" {
|
||||
t.Errorf("message: got %v, want 'hello from agent'", r.body["message"])
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("timed out waiting for command_result broadcast")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentNameUpdatesFromHostnameWhenDefault(t *testing.T) {
|
||||
database, err := db.New(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
hub := NewWSHub(database)
|
||||
|
||||
// Seed an agent whose name equals the old hostname (the default, un-renamed case).
|
||||
if err := database.UpsertAgent(&models.Agent{
|
||||
ID: "default-name-agent",
|
||||
Name: "OLD-HOSTNAME",
|
||||
Hostname: "OLD-HOSTNAME",
|
||||
Status: "offline",
|
||||
LastSeen: time.Now().Add(-5 * time.Minute),
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Agent reconnects with a new hostname (e.g. machine was renamed).
|
||||
conn, _ := dialAgentWS(t, hub)
|
||||
authAgentConn(t, conn, map[string]interface{}{
|
||||
"agent_id": "default-name-agent",
|
||||
"hostname": "NEW-HOSTNAME",
|
||||
"version": "1.0",
|
||||
})
|
||||
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
|
||||
agent, err := database.GetAgent("default-name-agent")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if agent.Name != "NEW-HOSTNAME" {
|
||||
t.Errorf("default name should follow hostname update; got %q", agent.Name)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user