Repository: getmoto/moto

Issue:
[SWF] DecisionTask previousStartedEventId always 0
**Repro**: Unfortunately we can't share our internals, but this should be reproducible with any workflow definition -- given a workflow with steps A->B->C notice that you should continuously spawn the "A" step at each decision task (the proceedings from A->B, B->C all work correctly). 

**What**: DecisionTasks are always populated with a previousStartedEventId of 0, and based on some quick investigation it doesn't look like that value is updated. 

**Expected**: Based on the AWS documentation here (https://docs.aws.amazon.com/amazonswf/latest/apireference/API_PollForDecisionTask.html) we rely on the previousStartedEventId to tell us whether we have already processed events on subsequent polls. With it always being zero our local mocks end up handling the initial workflow started event every time decision tasks are polled and we end up getting into this recursive state. 

**What Happens**: The workflows proceed and decision tasks are still created, but because they never have a new previousStartedEventId set we lose the PollForDecisionTask feature discussed above.

**Version**: 1.3.7. We run it in the standalone server mode. 




Discussion:
Yes, it looks like we haven't implemented this. Setting this issue as a feature request.

Thanks!

Resolving patch:
diff --git a/moto/swf/models/decision_task.py b/moto/swf/models/decision_task.py
--- a/moto/swf/models/decision_task.py
+++ b/moto/swf/models/decision_task.py
@@ -15,7 +15,7 @@ def __init__(self, workflow_execution, scheduled_event_id):
         self.workflow_type = workflow_execution.workflow_type
         self.task_token = str(uuid.uuid4())
         self.scheduled_event_id = scheduled_event_id
-        self.previous_started_event_id = 0
+        self.previous_started_event_id = None
         self.started_event_id = None
         self.started_timestamp = None
         self.start_to_close_timeout = (
@@ -40,18 +40,20 @@ def to_full_dict(self, reverse_order=False):
         hsh = {
             "events": [evt.to_dict() for evt in events],
             "taskToken": self.task_token,
-            "previousStartedEventId": self.previous_started_event_id,
             "workflowExecution": self.workflow_execution.to_short_dict(),
             "workflowType": self.workflow_type.to_short_dict(),
         }
+        if self.previous_started_event_id is not None:
+            hsh["previousStartedEventId"] = self.previous_started_event_id
         if self.started_event_id:
             hsh["startedEventId"] = self.started_event_id
         return hsh
 
-    def start(self, started_event_id):
+    def start(self, started_event_id, previous_started_event_id=None):
         self.state = "STARTED"
         self.started_timestamp = unix_time()
         self.started_event_id = started_event_id
+        self.previous_started_event_id = previous_started_event_id
 
     def complete(self):
         self._check_workflow_execution_open()
diff --git a/moto/swf/models/workflow_execution.py b/moto/swf/models/workflow_execution.py
--- a/moto/swf/models/workflow_execution.py
+++ b/moto/swf/models/workflow_execution.py
@@ -82,6 +82,7 @@ def __init__(self, domain, workflow_type, workflow_id, **kwargs):
         self._events = []
         # child workflows
         self.child_workflow_executions = []
+        self._previous_started_event_id = None
 
     def __repr__(self):
         return "WorkflowExecution(run_id: {0})".format(self.run_id)
@@ -295,7 +296,8 @@ def start_decision_task(self, task_token, identity=None):
             scheduled_event_id=dt.scheduled_event_id,
             identity=identity,
         )
-        dt.start(evt.event_id)
+        dt.start(evt.event_id, self._previous_started_event_id)
+        self._previous_started_event_id = evt.event_id
 
     def complete_decision_task(
         self, task_token, decisions=None, execution_context=None


Test patch:
diff --git a/tests/test_swf/models/test_decision_task.py b/tests/test_swf/models/test_decision_task.py
--- a/tests/test_swf/models/test_decision_task.py
+++ b/tests/test_swf/models/test_decision_task.py
@@ -24,15 +24,16 @@ def test_decision_task_full_dict_representation():
 
     fd = dt.to_full_dict()
     fd["events"].should.be.a("list")
-    fd["previousStartedEventId"].should.equal(0)
+    fd.should_not.contain("previousStartedEventId")
     fd.should_not.contain("startedEventId")
     fd.should.contain("taskToken")
     fd["workflowExecution"].should.equal(wfe.to_short_dict())
     fd["workflowType"].should.equal(wft.to_short_dict())
 
-    dt.start(1234)
+    dt.start(1234, 1230)
     fd = dt.to_full_dict()
     fd["startedEventId"].should.equal(1234)
+    fd["previousStartedEventId"].should.equal(1230)
 
 
 def test_decision_task_first_timeout():
diff --git a/tests/test_swf/responses/test_decision_tasks.py b/tests/test_swf/responses/test_decision_tasks.py
--- a/tests/test_swf/responses/test_decision_tasks.py
+++ b/tests/test_swf/responses/test_decision_tasks.py
@@ -30,6 +30,30 @@ def test_poll_for_decision_task_when_one():
     )
 
 
+@mock_swf_deprecated
+def test_poll_for_decision_task_previous_started_event_id():
+    conn = setup_workflow()
+
+    resp = conn.poll_for_decision_task("test-domain", "queue")
+    assert resp["workflowExecution"]["runId"] == conn.run_id
+    assert "previousStartedEventId" not in resp
+
+    # Require a failing decision, in this case a non-existant activity type
+    attrs = {
+        "activityId": "spam",
+        "activityType": {"name": "test-activity", "version": "v1.42"},
+        "taskList": "eggs",
+    }
+    decision = {
+        "decisionType": "ScheduleActivityTask",
+        "scheduleActivityTaskDecisionAttributes": attrs,
+    }
+    conn.respond_decision_task_completed(resp["taskToken"], decisions=[decision])
+    resp = conn.poll_for_decision_task("test-domain", "queue")
+    assert resp["workflowExecution"]["runId"] == conn.run_id
+    assert resp["previousStartedEventId"] == 3
+
+
 @mock_swf_deprecated
 def test_poll_for_decision_task_when_none():
     conn = setup_workflow()