Je pense que vous pouvez éviter le problème en remplaçant le texte vide par une valeur temporaire, en mettant à jour tout le reste du texte, puis en remplaçant la valeur temporaire par une valeur nulle.
Je ne comprends pas XPath, il existe probablement une bien meilleure façon de procéder, mais cela semble fonctionner :
SELECT
--#3: Replace the temporary value with null, this keeps the start and end tag
UpdateXML(
--#2: Replace everything but the temporary value
UpdateXML(
--#1: Replace empty text with a temporary value
UpdateXML(xmlData, '/TEST/VALUE[not(text())]', '<VALUE>TEMPORARY VALUE</VALUE>')
,'/TEST/VALUE[text()!="TEMPORARY VALUE"]/text()', 'hello')
,'/TEST/VALUE[text()="TEMPORARY VALUE"]/text()', null) examle
FROM (SELECT XMLType('<TEST><VALUE>hi</VALUE><VALUE>hola</VALUE><VALUE></VALUE></TEST>') as xmlData FROM DUAL);