Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions workspace-server/cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,15 @@ func main() {
registry.StartHibernationMonitor(c, wh.HibernateWorkspace)
})

// RFC #2829 PR-3: stuck-task sweeper for the durable delegations
// ledger. Marks deadline-exceeded rows as failed and heartbeat-stale
// in-flight rows as stuck. Both transitions go through the ledger's
// terminal forward-only protection so concurrent UpdateStatus calls
// are not clobbered. Defaults: 5min interval, 10min stale threshold;
// override via DELEGATION_SWEEPER_INTERVAL_S / DELEGATION_STUCK_THRESHOLD_S.
delegSweeper := handlers.NewDelegationSweeper(nil, nil)
go supervised.RunWithRecover(ctx, "delegation-sweeper", delegSweeper.Start)

// Channel Manager — social channel integrations (Telegram, Slack, etc.)
channelMgr := channels.NewManager(wh, broadcaster)
go supervised.RunWithRecover(ctx, "channel-manager", channelMgr.Start)
Expand Down
78 changes: 78 additions & 0 deletions workspace-server/internal/router/admin_delegations_route_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package router

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/DATA-DOG/go-sqlmock"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/db"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/handlers"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/middleware"
"github.com/gin-gonic/gin"
)

// admin_delegations_route_test.go — pin the RFC #2829 PR-4 wiring.
//
// Both the List and Stats endpoints must:
// 1. Be registered at the documented path
// 2. Be gated by AdminAuth (caller without a valid admin token → 401)
//
// Without this gate test, a future router refactor could silently drop
// AdminAuth on these endpoints — the operator dashboard would still work
// for the operator, but unauthenticated callers could pull the in-flight
// delegation list including caller_id, callee_id, and task previews.

func buildAdminDelegationsEngine(t *testing.T) *gin.Engine {
t.Helper()
gin.SetMode(gin.TestMode)
r := gin.New()
adH := handlers.NewAdminDelegationsHandler(db.DB)
r.GET("/admin/delegations", middleware.AdminAuth(db.DB), adH.List)
r.GET("/admin/delegations/stats", middleware.AdminAuth(db.DB), adH.Stats)
return r
}

// Both tests use the existing AdminAuth pattern: set ADMIN_TOKEN to disable
// the dev-mode fail-open branch, and have HasAnyLiveTokenGlobal return ≥1
// so AdminAuth enforces auth (rather than fail-open on fresh install).
// Without these two switches AdminAuth would return 200 + invoke the
// handler — defeating the gate test.

func TestAdminDelegationsRoute_List_RequiresAdminAuth(t *testing.T) {
t.Setenv("ADMIN_TOKEN", "test-admin-secret-not-presented-by-caller")
mock := setupRouterTestDB(t)
mock.ExpectQuery("SELECT COUNT.*FROM workspace_auth_tokens").
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))

r := buildAdminDelegationsEngine(t)
w := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/admin/delegations", nil)
r.ServeHTTP(w, req)

if w.Code != http.StatusUnauthorized {
t.Errorf("expected 401 for unauthenticated request, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("sqlmock unmet: %v", err)
}
}

func TestAdminDelegationsRoute_Stats_RequiresAdminAuth(t *testing.T) {
t.Setenv("ADMIN_TOKEN", "test-admin-secret-not-presented-by-caller")
mock := setupRouterTestDB(t)
mock.ExpectQuery("SELECT COUNT.*FROM workspace_auth_tokens").
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))

r := buildAdminDelegationsEngine(t)
w := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/admin/delegations/stats", nil)
r.ServeHTTP(w, req)

if w.Code != http.StatusUnauthorized {
t.Errorf("expected 401 for unauthenticated request, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("sqlmock unmet: %v", err)
}
}
9 changes: 9 additions & 0 deletions workspace-server/internal/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,15 @@ func Setup(hub *ws.Hub, broadcaster *events.Broadcaster, prov *provisioner.Provi
r.POST("/admin/a2a-queue/drop-stale", middleware.AdminAuth(db.DB), qH.DropStale)
}

// Admin — RFC #2829 PR-4 dashboard endpoints over the durable
// `delegations` ledger (PR-1 schema). Operators triage in-flight,
// stuck, or failed delegations without direct DB access.
{
adH := handlers.NewAdminDelegationsHandler(db.DB)
r.GET("/admin/delegations", middleware.AdminAuth(db.DB), adH.List)
r.GET("/admin/delegations/stats", middleware.AdminAuth(db.DB), adH.Stats)
}

// Admin — workspace template image refresh. Pulls latest images from GHCR
// and recreates running ws-* containers so they adopt the new image.
// Final step of the runtime CD chain — see docs/workspace-runtime-package.md.
Expand Down
Loading