Add fleet registry removal with confirm modal and agent_removed WS.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

Operators can remove machines from Crucible and dashboard rosters with honest messaging that deletion is registry-only; bulk select, oath ledger entries, and Go/Vitest coverage included.
This commit is contained in:
AetherForge
2026-06-07 18:23:41 -07:00
parent e1b1a2aa65
commit 07fdb39b63
21 changed files with 533 additions and 53 deletions

View File

@@ -78,6 +78,8 @@ func fleetChiRoute(method, pattern string, handler http.HandlerFunc) http.Handle
r.Post(pattern, handler)
case http.MethodPut:
r.Put(pattern, handler)
case http.MethodDelete:
r.Delete(pattern, handler)
default:
panic("unsupported method " + method)
}
@@ -989,3 +991,134 @@ func TestFleetPostAgentCommandEmptyAgentID(t *testing.T) {
t.Fatalf("expected 400 for empty id, got %d", rec.Code)
}
}
func readDashboardWSMessage(t *testing.T, conn *websocket.Conn, wantType string) Message {
t.Helper()
deadline := time.Now().Add(5 * time.Second)
for time.Now().Before(deadline) {
var msg Message
if err := conn.ReadJSON(&msg); err != nil {
t.Fatalf("read %s: %v", wantType, err)
}
if msg.Type == wantType {
return msg
}
}
t.Fatalf("timed out waiting for %s", wantType)
return Message{}
}
func TestFleetDeleteAgentSuccess(t *testing.T) {
fh, database, ws, _ := newTestFleetHandler(t)
agent := &models.Agent{ID: "del-agent-1", Name: "rig", Status: "offline", LastSeen: time.Now()}
if err := database.UpsertAgent(agent); err != nil {
t.Fatal(err)
}
dashConn := connectTestDashboard(t, ws)
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodDelete, "/agents/del-agent-1", nil)
fleetChiRoute(http.MethodDelete, "/agents/{id}", fh.DeleteAgent).ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status %d body %s", rec.Code, rec.Body.String())
}
var body map[string]interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatal(err)
}
if body["success"] != true {
t.Fatalf("unexpected body: %v", body)
}
if _, err := database.GetAgent("del-agent-1"); err == nil {
t.Fatal("expected agent removed from database")
}
removed := readDashboardWSMessage(t, dashConn, "agent_removed")
var payload map[string]string
if err := json.Unmarshal(removed.Payload, &payload); err != nil {
t.Fatal(err)
}
if payload["agent_id"] != "del-agent-1" {
t.Fatalf("agent_id = %q", payload["agent_id"])
}
}
func TestFleetDeleteAgentEmptyID(t *testing.T) {
fh, _, _, _ := newTestFleetHandler(t)
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodDelete, "/", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("id", "")
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
fh.DeleteAgent(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", rec.Code)
}
}
func TestFleetDeleteAgentDisconnectsOnline(t *testing.T) {
fh, database, ws, _ := newTestFleetHandler(t)
agentID := "del-online"
connectTestAgent(t, ws, agentID)
if !ws.isAgentConnected(agentID) {
t.Fatal("agent should be connected")
}
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodDelete, "/agents/"+agentID, nil)
fleetChiRoute(http.MethodDelete, "/agents/{id}", fh.DeleteAgent).ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status %d body %s", rec.Code, rec.Body.String())
}
if ws.isAgentConnected(agentID) {
t.Fatal("agent should be disconnected after delete")
}
if _, err := database.GetAgent(agentID); err == nil {
t.Fatal("expected agent removed from database")
}
}
func TestFleetBulkDeleteAgents(t *testing.T) {
fh, database, ws, _ := newTestFleetHandler(t)
for _, id := range []string{"bulk-del-a", "bulk-del-b", "bulk-del-missing"} {
if id == "bulk-del-missing" {
continue
}
agent := &models.Agent{ID: id, Name: id, Status: "offline", LastSeen: time.Now()}
if err := database.UpsertAgent(agent); err != nil {
t.Fatal(err)
}
}
connectTestAgent(t, ws, "bulk-del-a")
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/agents/bulk-delete",
strings.NewReader(`{"ids":["bulk-del-a","bulk-del-b","bulk-del-missing"]}`))
fh.BulkDeleteAgents(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status %d body %s", rec.Code, rec.Body.String())
}
var body map[string]interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatal(err)
}
if body["deleted"].(float64) != 2 {
t.Fatalf("deleted count = %v", body["deleted"])
}
if _, err := database.GetAgent("bulk-del-a"); err == nil {
t.Fatal("bulk-del-a should be gone")
}
if _, err := database.GetAgent("bulk-del-b"); err == nil {
t.Fatal("bulk-del-b should be gone")
}
}
func TestFleetBulkDeleteAgentsRequiresIDs(t *testing.T) {
fh, _, _, _ := newTestFleetHandler(t)
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/agents/bulk-delete", strings.NewReader(`{"ids":[]}`))
fh.BulkDeleteAgents(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", rec.Code)
}
}