Repository: nasa-ammos/aerie
Issue: Scheduling Goal Linked to Anchored Activity Failing

### Checked for duplicates

No - I haven't checked

### Is this a regression?

No - This is a new bug

### Version

1.6.1

### Describe the bug

When running a scheduling goal that is a coexistence goal to link an activity to an anchored activity (already in the plan pre-scheduling) results in a scheduling failure error "The scheduler should not be deleting activity instances". 

While debugging, when the activity anchoring was removed but the activity timing was kept the same the scheduling goal worked successfully - indicating this error arose from trying to schedule off of an anchored activity type.

The plan prior to running the scheduling goal look like:
![image](https://github.com/NASA-AMMOS/aerie/assets/40210944/215ae4b5-e698-4716-83b7-9e32cbd00fcb)
With three occurrences of SYS_TCMOTM_Define_MnvrKOZ anchored to occur prior to SYS_TCMOTM_Upd_BurnConditions.

**Scheduling Goal:**
```
// Link TCMOTM_Upd_DoDV_Single_Slew_Conds 1.5 hours before TCMOTM_Upd_BurnConditions for RWA maneuver (OR 30mins after SYS_TCMOTM_Define_MnvrKOZ start)
// 2 hours for RWA-2 slew maneuver; 1.0 hour for single RCS-2 slew maneuver; 0.75 hours for RWA-4 slew maneuver
var linkUpd_DoDV_Single_Slew: Goal = Goal.CoexistenceGoal({
    forEach: Spans.ForEachActivity(ActivityTypes.SYS_TCMOTM_Define_MnvrKOZ, mnvr => mnvr.parameters.TCMOTM_SlewType.equal("RWA_2").and(mnvr.window()).spans()).windows(),
    activityTemplate:ActivityTemplates.TCMOTM_Upd_DoDV_Sigle_Slew_Conds({ 
        TCMOTM_DoDV_Single_Slew_Dur: {quantity: 90, units: "MINUTES"} // 1.5 hrs
    }),
    startsAt: TimingConstraint.singleton(WindowProperty.START).plus(Temporal.Duration.from({ minutes: 30 }))
}).and(Goal.CoexistenceGoal({
    forEach: Spans.ForEachActivity(ActivityTypes.SYS_TCMOTM_Define_MnvrKOZ, gen2 => gen2.parameters.TCMOTM_SlewType.equal("RWA_4").and(gen2.window()).spans()).windows(),
    activityTemplate:ActivityTemplates.TCMOTM_Upd_DoDV_Sigle_Slew_Conds({ 
        TCMOTM_DoDV_Single_Slew_Dur: {quantity: 45, units: "MINUTES"} // 0.75 hrs
    }),
    startsAt: TimingConstraint.singleton(WindowProperty.START).plus(Temporal.Duration.from({ minutes: 30 }))
})).and(Goal.CoexistenceGoal({
    forEach: Spans.ForEachActivity(ActivityTypes.SYS_TCMOTM_Define_MnvrKOZ, gen3 => gen3.parameters.TCMOTM_SlewType.equal("RCS_2").and(gen3.window()).spans()).windows(),
    activityTemplate:ActivityTemplates.TCMOTM_Upd_DoDV_Sigle_Slew_Conds({ 
        TCMOTM_DoDV_Single_Slew_Dur: {quantity: 60, units: "MINUTES"} // 1 hr
    }),
    startsAt: TimingConstraint.singleton(WindowProperty.START).plus(Temporal.Duration.from({ minutes: 30 }))
}))

export default (): Goal => {
  return linkUpd_DoDV_Single_Slew
}
```

### Reproduction

To reproduce this error:

1. Place two activity types into a plan manually - one anchored off of the other
2. Develop a simple scheduling goal that links an activity to the _anchored_ activity type in the plan
3. Observe the scheduling results/errors that come up as a result of the scheduling run.


### Logs

```shell
gov.nasa.jpl.aerie.scheduler.server.exceptions.ResultsProtocolFailure: The scheduler should not be deleting activity instances
	at gov.nasa.jpl.aerie.scheduler.worker.services.SynchronousSchedulerAgent.storeFinalPlan(SynchronousSchedulerAgent.java:560)
	at gov.nasa.jpl.aerie.scheduler.worker.services.SynchronousSchedulerAgent.schedule(SynchronousSchedulerAgent.java:222)
	at gov.nasa.jpl.aerie.scheduler.worker.SchedulerWorkerAppDriver.main(SchedulerWorkerAppDriver.java:91)
Caused by: gov.nasa.jpl.aerie.scheduler.server.services.PlanServiceException: The scheduler should not be deleting activity instances
	at gov.nasa.jpl.aerie.scheduler.server.services.GraphQLMerlinService.updatePlanActivityDirectives(GraphQLMerlinService.java:414)
	at gov.nasa.jpl.aerie.scheduler.worker.services.SynchronousSchedulerAgent.storeFinalPlan(SynchronousSchedulerAgent.java:549)
	... 2 more
```


### System Info

```shell
Google Chrome browser, macOS Ventura 13.3.1
```


### Severity

Minor

Changed files:
scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java
scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java
scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/PrioritySolverTest.java
scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java

diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java
index 951535d5e..a507c7492 100644
--- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java
+++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java
@@ -28,7 +28,6 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
-import java.util.stream.Collectors;
 
 public class ResumableSimulationDriver<Model> implements AutoCloseable {
   private static final Logger logger = LoggerFactory.getLogger(ResumableSimulationDriver.class);
@@ -223,9 +222,11 @@ public class ResumableSimulationDriver<Model> implements AutoCloseable {
     // Get all activities as close as possible to absolute time, then schedule all activities.
     // Using HashMap explicitly because it allows `null` as a key.
     // `null` key means that an activity is not waiting on another activity to finish to know its start time
-    final HashMap<ActivityDirectiveId, List<Pair<ActivityDirectiveId, Duration>>> resolved = new StartOffsetReducer(
+    HashMap<ActivityDirectiveId, List<Pair<ActivityDirectiveId, Duration>>> resolved = new StartOffsetReducer(
         planDuration,
         schedule).compute();
+    // Filter out activities that are before the plan start
+    resolved = StartOffsetReducer.filterOutNegativeStartOffset(resolved);
 
     scheduleActivities(
         schedule,
diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java
index c39ae42f1..944e6a1a1 100644
--- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java
+++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java
@@ -133,10 +133,6 @@ public class PrioritySolver implements Solver {
 
   public record InsertActivityResult(boolean success, List<SchedulingActivityDirective> activitiesInserted){}
 
-  private InsertActivityResult checkAndInsertAct(SchedulingActivityDirective act){
-    return checkAndInsertActs(List.of(act));
-  }
-
   /**
    * Tries to insert a collection of activity instances in plan. Simulates each of the activity and checks whether the expected
    * duration is equal to the simulated duration.
@@ -220,10 +216,7 @@ public class PrioritySolver implements Solver {
     //turn off simulation checking for initial plan contents (must accept user input regardless)
     final var prevCheckFlag = this.checkSimBeforeInsertingActivities;
     this.checkSimBeforeInsertingActivities = false;
-    problem.getInitialPlan().getActivitiesByTime().stream()
-      .filter( act -> (act.startOffset()==null)
-               || problem.getPlanningHorizon().contains( act.startOffset() ) )
-      .forEach(this::checkAndInsertAct);
+    checkAndInsertActs(problem.getInitialPlan().getActivitiesByTime());
     this.checkSimBeforeInsertingActivities = prevCheckFlag;
 
     evaluation = new Evaluation();
diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/PrioritySolverTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/PrioritySolverTest.java
index 06c9db498..1d76ac160 100644
--- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/PrioritySolverTest.java
+++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/PrioritySolverTest.java
@@ -5,6 +5,7 @@ import com.google.common.truth.Correspondence;
 import gov.nasa.jpl.aerie.constraints.time.Interval;
 import gov.nasa.jpl.aerie.constraints.time.Windows;
 import gov.nasa.jpl.aerie.constraints.tree.WindowsWrapperExpression;
+import gov.nasa.jpl.aerie.merlin.driver.MissionModel;
 import gov.nasa.jpl.aerie.merlin.protocol.types.Duration;
 import gov.nasa.jpl.aerie.scheduler.constraints.activities.ActivityCreationTemplate;
 import gov.nasa.jpl.aerie.scheduler.constraints.activities.ActivityExpression;
@@ -31,7 +32,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 
 public class PrioritySolverTest {
   private static PrioritySolver makeEmptyProblemSolver() {
-    return new PrioritySolver(new Problem(null, h, null, null));
+    MissionModel<?> bananaMissionModel = SimulationUtility.getBananaMissionModel();
+    return new PrioritySolver(
+            new Problem(
+                    bananaMissionModel,
+                    h,
+                    new SimulationFacade(h, bananaMissionModel),
+                    SimulationUtility.getBananaSchedulerModel()));
   }
 
   private static PrioritySolver makeProblemSolver(Problem problem) {
diff --git a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java
index ccc775b0f..3c2afe168 100644
--- a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java
+++ b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java
@@ -1858,13 +1858,17 @@ public class SchedulingIntegrationTests {
     assertEquals(2, results.updatedPlan().size());
   }
 
+  /**
+   * Test that the scheduler can correctly place activities off of activities anchored to start at
+   * the end of another activity.
+   */
   @Test
-  void testRelativeActivityPlanSimpleCoexistenceGoalEnd() {
+  void testRelativeActivityPlanZeroStartOffsetEnd() {
     /*
-    Start with a plan with B -> A
+    Start with a plan with B anchored to end of A
     Goal: for each B, place a C
     And make sure that C ends up in the right place
-     */
+    */
     final var activityDuration = Duration.of(1, Duration.HOUR);
     final var results = runScheduler(
         BANANANATION,
@@ -1938,10 +1942,14 @@ public class SchedulingIntegrationTests {
     assertEquals(Duration.of(125, Duration.MINUTES), peelBanana.startOffset());
   }
 
+  /**
+   * Test that the scheduler can correctly place activities off of activities anchored to start
+   * at the same time of another activity.
+   */
   @Test
-  void testRelativeActivityPlanSimpleCoexistenceGoalStart() {
+  void testRelativeActivityPlanZeroStartOffsetStart() {
     /*
-    Start with a plan with B -> A
+    Start with a plan with B anchored to start of A
     Goal: for each B, place a C
     And make sure that C ends up in the right place
      */
@@ -2018,6 +2026,293 @@ public class SchedulingIntegrationTests {
     assertEquals(Duration.of(65, Duration.MINUTES), peelBanana.startOffset());
   }
 
+  /**
+   * Test that the scheduler can correctly place activities off of activities anchored to start before another activity.
+   */
+  @Test
+  void testRelativeActivityPlanNegativeStartOffsetStart() {
+    /*
+    Start with a plan with B anchored to A
+    Goal: for each B, place a C
+    And make sure that C ends up in the right place
+     */
+    final var activityDuration = Duration.of(1, Duration.HOUR);
+    final var tenMinutes = Duration.of(10, MINUTES);
+    final var results = runScheduler(
+        BANANANATION,
+        Map.of(
+            new ActivityDirectiveId(1L),
+            new ActivityDirective(
+                tenMinutes,
+                "DurationParameterActivity",
+                Map.of(
+                    "duration", SerializedValue.of(activityDuration.in(Duration.MICROSECONDS))),
+                null,
+                true),
+            new ActivityDirectiveId(2L),
+            new ActivityDirective(
+                Duration.of(-5, MINUTES),
+                "GrowBanana",
+                Map.of(
+                    "quantity", SerializedValue.of(1),
+                    "growingDuration", SerializedValue.of(activityDuration.in(Duration.MICROSECONDS))),
+                new ActivityDirectiveId(1L),
+                true)),
+        List.of(new SchedulingGoal(new GoalId(0L), """
+          export default () => Goal.CoexistenceGoal({
+            forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana),
+            activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromStem"}),
+            startsAt: TimingConstraint.singleton(WindowProperty.START).plus(Temporal.Duration.from({ minutes : 5}))
+          })
+          """, true)),
+        PLANNING_HORIZON);
+
+    assertEquals(1, results.scheduleResults.goalResults().size());
+    final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L));
+
+    assertTrue(goalResult.satisfied());
+    assertEquals(1, goalResult.createdActivities().size());
+    for (final var activity : goalResult.createdActivities()) {
+      assertNotNull(activity);
+    }
+    for (final var activity : goalResult.satisfyingActivities()) {
+      assertNotNull(activity);
+    }
+
+    final var planByActivityType = partitionByActivityType(results.updatedPlan());
+    final var peelBananas = planByActivityType.get("PeelBanana");
+    final var growBananas = planByActivityType.get("GrowBanana");
+    final var durationParamActivities = planByActivityType.get("DurationParameterActivity");
+
+    assertEquals(1, peelBananas.size());
+    assertEquals(1, growBananas.size());
+    assertEquals(1, durationParamActivities.size());
+    final var peelBanana = peelBananas.iterator().next();
+    final var growBanana = growBananas.iterator().next();
+    final var durationParamActivity = durationParamActivities.iterator().next();
+
+    assertEquals(tenMinutes, durationParamActivity.startOffset());
+    assertEquals(Duration.of(-5, MINUTES), growBanana.startOffset());
+    assertTrue(growBanana.anchoredToStart());
+
+    assertEquals(SerializedValue.of(1), growBanana.serializedActivity().getArguments().get("quantity"));
+    assertEquals(tenMinutes, peelBanana.startOffset());
+    assertEquals(SerializedValue.of("fromStem"), peelBanana.serializedActivity().getArguments().get("peelDirection"));
+  }
+
+  /**
+   * Test that the scheduler can correctly place activities off of activities anchored to start after the start
+   * of another activity.
+   */
+  @Test
+  void testRelativeActivityPlanPositiveStartOffsetStart() {
+    /*
+    Start with a plan with B anchored to A
+    Goal: for each B, place a C
+    And make sure that C ends up in the right place
+     */
+    final var activityDuration = Duration.of(1, Duration.HOUR);
+    final var tenMinutes = Duration.of(10, MINUTES);
+    final var results = runScheduler(
+        BANANANATION,
+        Map.of(
+            new ActivityDirectiveId(1L),
+            new ActivityDirective(
+                Duration.ZERO,
+                "DurationParameterActivity",
+                Map.of(
+                    "duration", SerializedValue.of(activityDuration.in(Duration.MICROSECONDS))),
+                null,
+                true),
+            new ActivityDirectiveId(2L),
+            new ActivityDirective(
+                tenMinutes,
+                "GrowBanana",
+                Map.of(
+                    "quantity", SerializedValue.of(1),
+                    "growingDuration", SerializedValue.of(activityDuration.in(Duration.MICROSECONDS))),
+                new ActivityDirectiveId(1L),
+                true)),
+        List.of(new SchedulingGoal(new GoalId(0L), """
+          export default () => Goal.CoexistenceGoal({
+            forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana),
+            activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromStem"}),
+            startsAt: TimingConstraint.singleton(WindowProperty.START).plus(Temporal.Duration.from({ minutes : 5}))
+          })
+          """, true)),
+        PLANNING_HORIZON);
+
+    assertEquals(1, results.scheduleResults.goalResults().size());
+    final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L));
+
+    assertTrue(goalResult.satisfied());
+    assertEquals(1, goalResult.createdActivities().size());
+    for (final var activity : goalResult.createdActivities()) {
+      assertNotNull(activity);
+    }
+    for (final var activity : goalResult.satisfyingActivities()) {
+      assertNotNull(activity);
+    }
+
+    final var planByActivityType = partitionByActivityType(results.updatedPlan());
+    final var peelBananas = planByActivityType.get("PeelBanana");
+    final var growBananas = planByActivityType.get("GrowBanana");
+    final var durationParamActivities = planByActivityType.get("DurationParameterActivity");
+
+    assertEquals(1, peelBananas.size());
+    assertEquals(1, growBananas.size());
+    assertEquals(1, durationParamActivities.size());
+    final var peelBanana = peelBananas.iterator().next();
+    final var growBanana = growBananas.iterator().next();
+    final var durationParamActivity = durationParamActivities.iterator().next();
+
+    assertEquals(Duration.ZERO, durationParamActivity.startOffset());
+
+    assertEquals(tenMinutes, growBanana.startOffset());
+    assertEquals(SerializedValue.of(1), growBanana.serializedActivity().getArguments().get("quantity"));
+
+    assertEquals(Duration.of(15, Duration.MINUTES), peelBanana.startOffset());
+    assertEquals(SerializedValue.of("fromStem"), peelBanana.serializedActivity().getArguments().get("peelDirection"));
+  }
+
+    /**
+   * Test that the scheduler can correctly place activities off of activities anchored to start after the start
+   * of another activity.
+   */
+  @Test
+  void testRelativeActivityPlanPositiveEndOffsetEnd() {
+    /*
+    Start with a plan with B anchored to A
+    Goal: for each B, place a C
+    And make sure that C ends up in the right place
+     */
+    final var activityDuration = Duration.of(1, Duration.HOUR);
+    final var tenMinutes = Duration.of(10, MINUTES);
+    final var results = runScheduler(
+        BANANANATION,
+        Map.of(
+            new ActivityDirectiveId(1L),
+            new ActivityDirective(
+                Duration.ZERO,
+                "DurationParameterActivity",
+                Map.of(
+                    "duration", SerializedValue.of(activityDuration.in(Duration.MICROSECONDS))),
+                null,
+                true),
+            new ActivityDirectiveId(2L),
+            new ActivityDirective(
+                tenMinutes,
+                "GrowBanana",
+                Map.of(
+                    "quantity", SerializedValue.of(1),
+                    "growingDuration", SerializedValue.of(activityDuration.in(Duration.MICROSECONDS))),
+                new ActivityDirectiveId(1L),
+                false)),
+        List.of(new SchedulingGoal(new GoalId(0L), """
+          export default () => Goal.CoexistenceGoal({
+            forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana),
+            activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromStem"}),
+            startsAt: TimingConstraint.singleton(WindowProperty.START).plus(Temporal.Duration.from({ minutes : 5}))
+          })
+          """, true)),
+        PLANNING_HORIZON);
+
+    assertEquals(1, results.scheduleResults.goalResults().size());
+    final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L));
+
+    assertTrue(goalResult.satisfied());
+    assertEquals(1, goalResult.createdActivities().size());
+    for (final var activity : goalResult.createdActivities()) {
+      assertNotNull(activity);
+    }
+    for (final var activity : goalResult.satisfyingActivities()) {
+      assertNotNull(activity);
+    }
+
+    final var planByActivityType = partitionByActivityType(results.updatedPlan());
+    final var peelBananas = planByActivityType.get("PeelBanana");
+    final var growBananas = planByActivityType.get("GrowBanana");
+    final var durationParamActivities = planByActivityType.get("DurationParameterActivity");
+
+    assertEquals(1, peelBananas.size());
+    assertEquals(1, growBananas.size());
+    assertEquals(1, durationParamActivities.size());
+    final var peelBanana = peelBananas.iterator().next();
+    final var growBanana = growBananas.iterator().next();
+    final var durationParamActivity = durationParamActivities.iterator().next();
+
+    assertEquals(Duration.ZERO, durationParamActivity.startOffset());
+
+    assertEquals(Duration.of(10, MINUTES), growBanana.startOffset());
+    assertFalse(growBanana.anchoredToStart());
+    assertEquals(SerializedValue.of(1), growBanana.serializedActivity().getArguments().get("quantity"));
+
+    assertEquals(Duration.of(75, Duration.MINUTES), peelBanana.startOffset());
+    assertEquals(SerializedValue.of("fromStem"), peelBanana.serializedActivity().getArguments().get("peelDirection"));
+  }
+
+  /**
+   * Test that the scheduler does not schedule activities based on activities which are outside plan bounds
+   */
+  @Test
+  void testDontScheduleFromOutsidePlanBounds(){
+    final var activityDuration = Duration.of(1, Duration.HOUR);
+    final var results = runScheduler(
+        BANANANATION,
+        Map.of(
+            // Activity Before Plan Start
+            new ActivityDirectiveId(1L),
+            new ActivityDirective(
+                Duration.of(-10, MINUTES),
+                "GrowBanana",
+                Map.of(
+                    "quantity", SerializedValue.of(1),
+                    "growingDuration", SerializedValue.of(activityDuration.in(Duration.MICROSECONDS))),
+                null,
+                true),
+            // Activity Anchored to run after Plan End
+            new ActivityDirectiveId(2L),
+            new ActivityDirective(
+                Duration.of(10, MINUTES),
+                "GrowBanana",
+                Map.of(
+                    "quantity", SerializedValue.of(2),
+                    "growingDuration", SerializedValue.of(activityDuration.in(Duration.MICROSECONDS))),
+                null,
+                false)),
+        List.of(new SchedulingGoal(new GoalId(0L), """
+          export default () => Goal.CoexistenceGoal({
+            forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana),
+            activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromStem"}),
+            startsAt: TimingConstraint.singleton(WindowProperty.START).plus(Temporal.Duration.from({ minutes : 5}))
+          })
+          """, true)),
+        PLANNING_HORIZON);
+
+    assertEquals(1, results.scheduleResults.goalResults().size());
+    final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L));
+
+    // The goal is satisfied, but placed no activities,
+    // because all activities it could've matched on are outside the plan bounds
+    assertTrue(goalResult.satisfied());
+    assertEquals(0, goalResult.createdActivities().size());
+
+    final var planByActivityType = partitionByActivityType(results.updatedPlan());
+    final var growBananas = planByActivityType.get("GrowBanana");
+    final var gbIterator = growBananas.iterator();
+
+    assertNull(planByActivityType.get("PeelBanana"));
+    assertEquals(2, growBananas.size());
+
+    final var growBanana1 = gbIterator.next();
+    assertEquals(Duration.of(-10, MINUTES), growBanana1.startOffset());
+    assertEquals(SerializedValue.of(1), growBanana1.serializedActivity().getArguments().get("quantity"));
+
+    final var growBanana2 = gbIterator.next();
+    assertEquals(Duration.of(10, MINUTES), growBanana2.startOffset());
+    assertEquals(SerializedValue.of(2), growBanana2.serializedActivity().getArguments().get("quantity"));
+  }
+
   /**
    * Test the option to turn off simulation in between goals.
    *