Repository: holunda-io/camunda-bpm-taskpool
Issue: taskService.saveTask() without changes causes NoHandlerForCommandException 

### Steps to reproduce

* camunda-bpm-taskpool version:
* Camunda BPM version: 3.8.1
* JDK version:  11
* Operating system: MacOS
* Complete executable reproducer: consider the following test method added to `TaskCollectorITest`:
  ```kotlin
  @Test
  fun `updates attributes via API with update listener when nothing changed`() {

    // deploy
    driver.deployProcess(
      createUserTaskProcess(
        taskListeners = listOf(
          "update" to "#{changeTaskAttributes}"
        )
      )
    )

    // start
    val instance = driver.startProcessInstance()
    driver.assertProcessInstanceWaitsInUserTask(instance)
    verify(commandListGateway).sendToGateway(listOf(createTaskCommand()))

    // trigger the update event on the task
    taskService.saveTask(task())

    val updateCommand = updateTaskCommand()
    verify(commandListGateway).sendToGateway(listOf(updateCommand))

    verifyNoMoreInteractions(commandListGateway)
  }
  ```

This is a slight variation on `updates attributes via API with update listener` where nothing is actually changed in the task. This might seem futile but in our setup, it is used to force a task update after process variables have changed.

The command sent to the gateway is not an `UpdateAttributeTaskCommand` but an `UpdateAttributesHistoricTaskCommand`. From that class's description: `This command has no command handler in the core and is never sent to it.`
It is not intended to be sent to the gateway but should be merged into other commands by the accumulator, which doesn't happen if it's the only command. 

So the problem is that without changes to the task, no `UpdateAttributeTaskCommand` is created and thus the `UpdateAttributesHistoricTaskCommand` stands alone where it never should.

* Steps: In a real-world setup, you can reproduce this by calling `taskService.saveTask()` with an original, unchanged task.

### Expected behaviour

Calling saveTask() with no changes to the task itself should create an `UpdateAttributeTaskCommand` enriched with the current process variables.

### Actual behaviour

An `UpdateAttributesHistoricTaskCommand` is issued which cannot be processed and an exception is thrown:
```
org.axonframework.commandhandling.NoHandlerForCommandException: No handler was subscribed for command [io.holunda.camunda.taskpool.api.task.UpdateAttributesHistoricTaskCommand].
```


Changed files:
integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/DelegateTaskExtensions.kt
integration/camunda-bpm/taskpool-collector/src/test/kotlin/io/holunda/polyflow/taskpool/itest/TestDriver.kt
integration/camunda-bpm/taskpool-collector/src/test/kotlin/io/holunda/polyflow/taskpool/itest/tx/TaskCollectorITest.kt
core/taskpool/taskpool-api/src/main/kotlin/io/holunda/camunda/taskpool/api/task/EngineTaskCommands.kt
integration/common/taskpool-sender/src/main/kotlin/io/holunda/polyflow/taskpool/sender/task/accumulator/SimpleEngineTaskCommandIntentDetector.kt

diff --git a/core/taskpool/taskpool-api/src/main/kotlin/io/holunda/camunda/taskpool/api/task/EngineTaskCommands.kt b/core/taskpool/taskpool-api/src/main/kotlin/io/holunda/camunda/taskpool/api/task/EngineTaskCommands.kt
index b3680cb7c..7e778ca5f 100755
--- a/core/taskpool/taskpool-api/src/main/kotlin/io/holunda/camunda/taskpool/api/task/EngineTaskCommands.kt
+++ b/core/taskpool/taskpool-api/src/main/kotlin/io/holunda/camunda/taskpool/api/task/EngineTaskCommands.kt
@@ -212,7 +212,10 @@ data class UpdateAttributeTaskCommand(
    * Optional task priority.
    */
   val priority: Int?,
-
+  /**
+   * True if no properties of the task have actually changed.
+   */
+  val unchanged: Boolean = false
 ) : TaskIdentityWithPayloadAndCorrelations, EngineTaskCommand
 
 
diff --git a/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/DelegateTaskExtensions.kt b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/DelegateTaskExtensions.kt
index 19ded13b9..26d648f9d 100644
--- a/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/DelegateTaskExtensions.kt
+++ b/integration/camunda-bpm/taskpool-collector/src/main/kotlin/io/holunda/polyflow/taskpool/DelegateTaskExtensions.kt
@@ -100,7 +100,8 @@ fun DelegateTask.toUpdateCommand(applicationName: String) = UpdateAttributeTaskC
   priority = this.priority,
   taskDefinitionKey = this.taskDefinitionKey,
   businessKey = this.execution.businessKey,
-  sourceReference = this.sourceReference(applicationName)
+  sourceReference = this.sourceReference(applicationName),
+  unchanged = this is TaskEntity && this.propertyChanges.isEmpty()
 )
 
 
diff --git a/integration/camunda-bpm/taskpool-collector/src/test/kotlin/io/holunda/polyflow/taskpool/itest/TestDriver.kt b/integration/camunda-bpm/taskpool-collector/src/test/kotlin/io/holunda/polyflow/taskpool/itest/TestDriver.kt
index e1cdd7ac8..e6f6e4870 100644
--- a/integration/camunda-bpm/taskpool-collector/src/test/kotlin/io/holunda/polyflow/taskpool/itest/TestDriver.kt
+++ b/integration/camunda-bpm/taskpool-collector/src/test/kotlin/io/holunda/polyflow/taskpool/itest/TestDriver.kt
@@ -114,7 +114,8 @@ class TestDriver(
     fun updateTaskCommand(
       variables: VariableMap = DEFAULT_VARIABLES,
       instanceBusinessKey: String = BUSINESS_KEY,
-      correlations: VariableMap = newCorrelations()
+      correlations: VariableMap = newCorrelations(),
+      unchanged: Boolean = false
     ) =
       BpmnAwareTests.task(BpmnAwareTests.taskQuery().initializeFormKeys()).let { task ->
         UpdateAttributeTaskCommand(
@@ -137,7 +138,8 @@ class TestDriver(
           enriched = true,
           businessKey = instanceBusinessKey,
           payload = variables,
-          correlations = correlations
+          correlations = correlations,
+          unchanged = unchanged
         )
       }
 
@@ -173,4 +175,4 @@ class TestDriver(
       BpmnAwareTests.assertThat(instance).isNotNull
       BpmnAwareTests.assertThat(instance).isStarted
     }
-}
\\ No newline at end of file
+}
diff --git a/integration/camunda-bpm/taskpool-collector/src/test/kotlin/io/holunda/polyflow/taskpool/itest/tx/TaskCollectorITest.kt b/integration/camunda-bpm/taskpool-collector/src/test/kotlin/io/holunda/polyflow/taskpool/itest/tx/TaskCollectorITest.kt
index f7c78c0a4..92dbec1d0 100644
--- a/integration/camunda-bpm/taskpool-collector/src/test/kotlin/io/holunda/polyflow/taskpool/itest/tx/TaskCollectorITest.kt
+++ b/integration/camunda-bpm/taskpool-collector/src/test/kotlin/io/holunda/polyflow/taskpool/itest/tx/TaskCollectorITest.kt
@@ -387,10 +387,13 @@ internal class TaskCollectorITest {
     runtimeService.setVariable(instance.processInstanceId, "bla", "blubb")
     taskService.saveTask(task())
 
-    val updateCommand = updateTaskCommand(variables = Variables.createVariables().apply {
-      putAll(DEFAULT_VARIABLES)
-      putValue("bla", "blubb")
-    })
+    val updateCommand = updateTaskCommand(
+      variables = Variables.createVariables().apply {
+        putAll(DEFAULT_VARIABLES)
+        putValue("bla", "blubb")
+      },
+      unchanged = true
+    )
     verify(commandListGateway).sendToGateway(listOf(updateCommand))
 
     verifyNoMoreInteractions(commandListGateway)
diff --git a/integration/common/taskpool-sender/src/main/kotlin/io/holunda/polyflow/taskpool/sender/task/accumulator/SimpleEngineTaskCommandIntentDetector.kt b/integration/common/taskpool-sender/src/main/kotlin/io/holunda/polyflow/taskpool/sender/task/accumulator/SimpleEngineTaskCommandIntentDetector.kt
index d83d501ad..6dc3d1e1a 100644
--- a/integration/common/taskpool-sender/src/main/kotlin/io/holunda/polyflow/taskpool/sender/task/accumulator/SimpleEngineTaskCommandIntentDetector.kt
+++ b/integration/common/taskpool-sender/src/main/kotlin/io/holunda/polyflow/taskpool/sender/task/accumulator/SimpleEngineTaskCommandIntentDetector.kt
@@ -6,7 +6,6 @@ import io.holunda.camunda.taskpool.api.task.CamundaTaskEventType.Companion.ATTRI
 import io.holunda.camunda.taskpool.api.task.CamundaTaskEventType.Companion.CANDIDATE_USER_ADD
 import io.holunda.camunda.taskpool.api.task.CamundaTaskEventType.Companion.CANDIDATE_USER_DELETE
 import io.holunda.camunda.taskpool.api.task.CamundaTaskEventType.Companion.COMPLETE
-import io.holunda.camunda.taskpool.api.task.CamundaTaskEventType.Companion.CREATE
 import io.holunda.camunda.taskpool.api.task.CamundaTaskEventType.Companion.DELETE
 
 /**
@@ -63,6 +62,7 @@ class SimpleEngineTaskCommandIntentDetector(
         }
         // updates of the listener are accepted
         is UpdateAttributeTaskCommand -> engineTaskCommands.filter { it.eventName == ATTRIBUTES_LISTENER_UPDATE }
+        is UpdateAssignmentTaskCommand -> engineTaskCommands.filter { it is UpdateAttributeTaskCommand && it.unchanged}
         else -> listOf()
       }
 }