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:
@@ -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