fix(restart): block Restart on removed workspaces to prevent resurrection (#306) #2394
@@ -229,6 +229,13 @@ func (h *WorkspaceHandler) Restart(c *gin.Context) {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "lookup failed"})
|
||||
return
|
||||
}
|
||||
// Block restart if workspace is removed — same 404 as not-found so we don't
|
||||
// leak that the row ever existed, and to prevent resurrecting a removed
|
||||
// workspace to 'provisioning' before the async runRestartCycle guard fires.
|
||||
if status == string(models.StatusRemoved) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "workspace not found"})
|
||||
return
|
||||
}
|
||||
// Block restart if any ancestor is paused — must resume parent first
|
||||
if paused, parentName := isParentPaused(ctx, id); paused {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "parent workspace \"" + parentName + "\" is paused — resume it first"})
|
||||
|
||||
@@ -70,6 +70,33 @@ func TestRestartHandler_DBConnectionError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestartHandler_RemovedWorkspaceReturns404(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
broadcaster := newTestBroadcaster()
|
||||
handler := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir())
|
||||
|
||||
mock.ExpectQuery("SELECT status, name, tier, COALESCE").
|
||||
WithArgs("ws-removed").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"status", "name", "tier", "runtime"}).
|
||||
AddRow("removed", "Removed Agent", 1, "claude-code"))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "ws-removed"}}
|
||||
c.Request = httptest.NewRequest("POST", "/workspaces/ws-removed/restart", nil)
|
||||
|
||||
handler.Restart(c)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected 404 for removed workspace, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("unmet sqlmock expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestartHandler_AncestorPausedBlocksRestart(t *testing.T) {
|
||||
mock := setupTestDB(t)
|
||||
setupTestRedis(t)
|
||||
|
||||
Reference in New Issue
Block a user