Improvements to FakeRequestManager:
- Refactor delays to support a per-response delay via MainThreadRunner.
- Add support for failed responses.
- Enqueue triggerRefresh as a HEAD_INVALIDATE task. This correctly puts the
  TaskQueue into a delaying state.

Also fix concurrent modification exceptions in FakeMainThreadRunner.

PiperOrigin-RevId: 242946107
Change-Id: I9cd7a1dd544d4fb38d2e67960fbe18d0c50c78a3
diff --git a/src/main/java/com/google/android/libraries/feed/common/concurrent/TaskQueue.java b/src/main/java/com/google/android/libraries/feed/common/concurrent/TaskQueue.java
index 89d78ac..4e0bc18 100644
--- a/src/main/java/com/google/android/libraries/feed/common/concurrent/TaskQueue.java
+++ b/src/main/java/com/google/android/libraries/feed/common/concurrent/TaskQueue.java
@@ -1,4 +1,4 @@
-// Copyright 2018 The Feed Authors.
+// Copyright 2019 The Feed Authors.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -64,7 +64,7 @@
    * thread. Starvation checks are started when we initially delay the queue and only runs while the
    * queue is delayed.
    */
-  @VisibleForTesting static final long STARVATION_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(15);
+  @VisibleForTesting public static final long STARVATION_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(15);
 
   @VisibleForTesting static final long STARVATION_CHECK_MS = TimeUnit.SECONDS.toMillis(6);
 
diff --git a/src/main/java/com/google/android/libraries/feed/common/concurrent/testing/FakeMainThreadRunner.java b/src/main/java/com/google/android/libraries/feed/common/concurrent/testing/FakeMainThreadRunner.java
index bf62e43..c28f5f7 100644
--- a/src/main/java/com/google/android/libraries/feed/common/concurrent/testing/FakeMainThreadRunner.java
+++ b/src/main/java/com/google/android/libraries/feed/common/concurrent/testing/FakeMainThreadRunner.java
@@ -1,4 +1,4 @@
-// Copyright 2018 The Feed Authors.
+// Copyright 2019 The Feed Authors.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -24,6 +24,7 @@
 import java.util.Comparator;
 import java.util.List;
 import java.util.PriorityQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * A {@link MainThreadRunner} which listens to a {@link FakeClock} to determine when to execute
@@ -31,6 +32,7 @@
  */
 public final class FakeMainThreadRunner extends MainThreadRunner {
 
+  private final AtomicBoolean currentlyExecutingTasks = new AtomicBoolean();
   private final FakeClock fakeClock;
   private final FakeThreadUtils fakeThreadUtils;
   private final List<Runnable> tasksToRun = new ArrayList<>();
@@ -109,13 +111,21 @@
 
   /** Runs all eligible tasks. */
   public void runAllTasks() {
-    boolean policy = fakeThreadUtils.enforceMainThread(true);
-    for (Runnable task : tasksToRun) {
-      task.run();
-      completedTaskCount++;
+    if (currentlyExecutingTasks.getAndSet(true)) {
+      return;
     }
-    tasksToRun.clear();
-    fakeThreadUtils.enforceMainThread(policy);
+
+    boolean policy = fakeThreadUtils.enforceMainThread(true);
+    try {
+      while (!tasksToRun.isEmpty()) {
+        Runnable task = tasksToRun.remove(0);
+        task.run();
+        completedTaskCount++;
+      }
+    } finally {
+      fakeThreadUtils.enforceMainThread(policy);
+      currentlyExecutingTasks.set(false);
+    }
   }
 
   /** Returns {@literal true} if there are tasks to run or tasks have run. */
diff --git a/src/main/java/com/google/android/libraries/feed/common/testing/BUILD b/src/main/java/com/google/android/libraries/feed/common/testing/BUILD
index 42f4cbe..9613519 100644
--- a/src/main/java/com/google/android/libraries/feed/common/testing/BUILD
+++ b/src/main/java/com/google/android/libraries/feed/common/testing/BUILD
@@ -19,6 +19,7 @@
         "//src/main/java/com/google/android/libraries/feed/common/functional",
         "//src/main/java/com/google/android/libraries/feed/common/protoextensions",
         "//src/main/java/com/google/android/libraries/feed/common/time",
+        "//src/main/java/com/google/android/libraries/feed/common/time/testing",
         "//src/main/java/com/google/android/libraries/feed/feedapplifecyclelistener",
         "//src/main/java/com/google/android/libraries/feed/feedmodelprovider",
         "//src/main/java/com/google/android/libraries/feed/feedprotocoladapter",
diff --git a/src/main/java/com/google/android/libraries/feed/common/testing/InfraIntegrationScope.java b/src/main/java/com/google/android/libraries/feed/common/testing/InfraIntegrationScope.java
index fef4604..244dc99 100644
--- a/src/main/java/com/google/android/libraries/feed/common/testing/InfraIntegrationScope.java
+++ b/src/main/java/com/google/android/libraries/feed/common/testing/InfraIntegrationScope.java
@@ -20,14 +20,12 @@
 import com.google.android.libraries.feed.api.protocoladapter.ProtocolAdapter;
 import com.google.android.libraries.feed.api.scope.ClearAllListener;
 import com.google.android.libraries.feed.api.sessionmanager.SessionManager;
-import com.google.android.libraries.feed.common.concurrent.MainThreadRunner;
 import com.google.android.libraries.feed.common.concurrent.TaskQueue;
 import com.google.android.libraries.feed.common.concurrent.testing.FakeMainThreadRunner;
 import com.google.android.libraries.feed.common.concurrent.testing.FakeThreadUtils;
 import com.google.android.libraries.feed.common.protoextensions.FeedExtensionRegistry;
-import com.google.android.libraries.feed.common.time.Clock;
-import com.google.android.libraries.feed.common.time.SystemClockImpl;
 import com.google.android.libraries.feed.common.time.TimingUtils;
+import com.google.android.libraries.feed.common.time.testing.FakeClock;
 import com.google.android.libraries.feed.feedapplifecyclelistener.FeedAppLifecycleListener;
 import com.google.android.libraries.feed.feedmodelprovider.FeedModelProviderFactory;
 import com.google.android.libraries.feed.feedprotocoladapter.FeedProtocolAdapter;
@@ -67,10 +65,11 @@
    */
   public static final long TIMEOUT_TEST_TIMEOUT = TimeUnit.SECONDS.toMillis(20);
 
-  private final Clock clock;
   private final Configuration configuration;
   private final ContentStorageDirect contentStorage;
   private final ExecutorService executorService;
+  private final FakeClock fakeClock;
+  private final FakeMainThreadRunner fakeMainThreadRunner;
   private final FakeRequestManager fakeRequestManager;
   private final FeedAppLifecycleListener appLifecycleListener;
   private final FeedModelProviderFactory modelProviderFactory;
@@ -81,27 +80,25 @@
   private final SchedulerApi schedulerApi;
   private final TaskQueue taskQueue;
   private final ThreadUtils threadUtils;
-  private final long requestDelayMs;
 
   private InfraIntegrationScope(
       ThreadUtils threadUtils,
       ExecutorService executorService,
       SchedulerApi schedulerApi,
-      Clock clock,
+      FakeClock fakeClock,
       Configuration configuration,
-      long requestDelayMs,
       ContentStorageDirect contentStorage,
-      JournalStorageDirect journalStorage) {
-    this.clock = clock;
+      JournalStorageDirect journalStorage,
+      FakeMainThreadRunner fakeMainThreadRunner) {
+    this.fakeClock = fakeClock;
     this.configuration = configuration;
     this.contentStorage = contentStorage;
     this.executorService = executorService;
     this.journalStorage = journalStorage;
-    this.requestDelayMs = requestDelayMs;
+    this.fakeMainThreadRunner = fakeMainThreadRunner;
     this.schedulerApi = schedulerApi;
     this.threadUtils = threadUtils;
     TimingUtils timingUtils = new TimingUtils();
-    MainThreadRunner mainThreadRunner = new MainThreadRunner();
     appLifecycleListener = new FeedAppLifecycleListener(threadUtils);
     FakeBasicLoggingApi fakeBasicLoggingApi = new FakeBasicLoggingApi();
 
@@ -110,9 +107,9 @@
         new TaskQueue(
             fakeBasicLoggingApi,
             executorService,
-            FakeMainThreadRunner.runTasksImmediately(),
-            clock,
-            false);
+            fakeMainThreadRunner,
+            fakeClock,
+            /* checkStarvation= */ true);
     store =
         new FeedStore(
             timingUtils,
@@ -121,15 +118,16 @@
             journalStorage,
             threadUtils,
             taskQueue,
-            clock,
+            fakeClock,
             fakeBasicLoggingApi,
-            mainThreadRunner);
+            fakeMainThreadRunner);
     feedProtocolAdapter = new FeedProtocolAdapter(timingUtils);
     fakeRequestManager =
         new FakeRequestManager(
             new FakeThreadUtils(/* enforceThreadChecks= */ false),
+            fakeMainThreadRunner,
             feedProtocolAdapter,
-            requestDelayMs);
+            taskQueue);
     FakeActionUploadRequestManager fakeActionUploadRequestManager =
         new FakeActionUploadRequestManager(new FakeThreadUtils());
     feedSessionManager =
@@ -143,7 +141,7 @@
                 fakeActionUploadRequestManager,
                 schedulerApi,
                 configuration,
-                clock,
+                fakeClock,
                 appLifecycleListener)
             .create();
     new ClearAllListener(taskQueue, feedSessionManager, store, threadUtils, appLifecycleListener);
@@ -154,7 +152,7 @@
             threadUtils,
             timingUtils,
             taskQueue,
-            mainThreadRunner,
+            fakeMainThreadRunner,
             configuration);
   }
 
@@ -170,6 +168,10 @@
     return modelProviderFactory;
   }
 
+  public FakeClock getFakeClock() {
+    return fakeClock;
+  }
+
   public FeedStore getStore() {
     return store;
   }
@@ -192,11 +194,11 @@
         threadUtils,
         executorService,
         schedulerApi,
-        clock,
+        fakeClock,
         configuration,
-        requestDelayMs,
         contentStorage,
-        journalStorage);
+        journalStorage,
+        fakeMainThreadRunner);
   }
 
   private static class ExtensionProvider implements ProtoExtensionProvider {
@@ -216,13 +218,13 @@
 
   /** Builder for creating the {@link InfraIntegrationScope} */
   public static class Builder {
+    private final FakeClock fakeClock = new FakeClock();
+    private final FakeMainThreadRunner fakeMainThreadRunner =
+        FakeMainThreadRunner.create(fakeClock);
     private final ThreadUtils threadUtils;
 
-    private Clock clock = new SystemClockImpl();
     private Configuration configuration = Configuration.getDefaultInstance();
-    private ExecutorService executorService = MoreExecutors.newDirectExecutorService();
     private SchedulerApi schedulerApi;
-    private long requestDelayMs = 0;
 
     public Builder(ThreadUtils threadUtils) {
       this.threadUtils = threadUtils;
@@ -234,36 +236,21 @@
       return this;
     }
 
-    public Builder setClock(Clock clock) {
-      this.clock = clock;
-      return this;
-    }
-
-    public Builder setExecutorService(ExecutorService executorService) {
-      this.executorService = executorService;
-      return this;
-    }
-
     public Builder setSchedulerApi(SchedulerApi schedulerApi) {
       this.schedulerApi = schedulerApi;
       return this;
     }
 
-    public Builder setRequestDelayMs(long requestDelayMs) {
-      this.requestDelayMs = requestDelayMs;
-      return this;
-    }
-
     public InfraIntegrationScope build() {
       return new InfraIntegrationScope(
           threadUtils,
-          executorService,
+          MoreExecutors.newDirectExecutorService(),
           schedulerApi,
-          clock,
+          fakeClock,
           configuration,
-          requestDelayMs,
           new InMemoryContentStorage(),
-          new InMemoryJournalStorage());
+          new InMemoryJournalStorage(),
+          fakeMainThreadRunner);
     }
   }
 }
diff --git a/src/main/java/com/google/android/libraries/feed/testing/requestmanager/BUILD b/src/main/java/com/google/android/libraries/feed/testing/requestmanager/BUILD
index 4c7a626..7c33eaf 100644
--- a/src/main/java/com/google/android/libraries/feed/testing/requestmanager/BUILD
+++ b/src/main/java/com/google/android/libraries/feed/testing/requestmanager/BUILD
@@ -10,6 +10,7 @@
         "//src/main/java/com/google/android/libraries/feed/api/protocoladapter",
         "//src/main/java/com/google/android/libraries/feed/api/requestmanager",
         "//src/main/java/com/google/android/libraries/feed/common",
+        "//src/main/java/com/google/android/libraries/feed/common/concurrent",
         "//src/main/java/com/google/android/libraries/feed/common/concurrent/testing",
         "//src/main/java/com/google/android/libraries/feed/common/functional",
         "//src/main/java/com/google/android/libraries/feed/host/logging",
diff --git a/src/main/java/com/google/android/libraries/feed/testing/requestmanager/FakeRequestManager.java b/src/main/java/com/google/android/libraries/feed/testing/requestmanager/FakeRequestManager.java
index 0dd648f..7c28ebe 100644
--- a/src/main/java/com/google/android/libraries/feed/testing/requestmanager/FakeRequestManager.java
+++ b/src/main/java/com/google/android/libraries/feed/testing/requestmanager/FakeRequestManager.java
@@ -1,4 +1,4 @@
-// Copyright 2018 The Feed Authors.
+// Copyright 2019 The Feed Authors.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -17,6 +17,9 @@
 import com.google.android.libraries.feed.api.protocoladapter.ProtocolAdapter;
 import com.google.android.libraries.feed.api.requestmanager.RequestManager;
 import com.google.android.libraries.feed.common.Result;
+import com.google.android.libraries.feed.common.concurrent.MainThreadRunner;
+import com.google.android.libraries.feed.common.concurrent.TaskQueue;
+import com.google.android.libraries.feed.common.concurrent.TaskQueue.TaskType;
 import com.google.android.libraries.feed.common.concurrent.testing.FakeThreadUtils;
 import com.google.android.libraries.feed.common.functional.Consumer;
 import com.google.android.libraries.feed.common.functional.Supplier;
@@ -36,9 +39,10 @@
  */
 public class FakeRequestManager implements RequestManager {
   private final FakeThreadUtils fakeThreadUtils;
+  private final MainThreadRunner mainThreadRunner;
   private final ProtocolAdapter protocolAdapter;
-  private final long requestDelayMs;
-  private final Queue<Response> responses = new ArrayDeque<>();
+  private final Queue<ResponseWithDelay> responses = new ArrayDeque<>();
+  private final TaskQueue taskQueue;
   /*@Nullable*/ private StreamToken latestStreamToken = null;
   @RequestReason private int latestRequestReason = RequestReason.UNKNOWN;
 
@@ -47,16 +51,36 @@
       defaultTriggerRefreshConsumerSupplier;
 
   public FakeRequestManager(
-      FakeThreadUtils fakeThreadUtils, ProtocolAdapter protocolAdapter, long requestDelayMs) {
+      FakeThreadUtils fakeThreadUtils,
+      MainThreadRunner mainThreadRunner,
+      ProtocolAdapter protocolAdapter,
+      TaskQueue taskQueue) {
     this.fakeThreadUtils = fakeThreadUtils;
+    this.mainThreadRunner = mainThreadRunner;
     this.protocolAdapter = protocolAdapter;
-    this.requestDelayMs = requestDelayMs;
+    this.taskQueue = taskQueue;
   }
 
   // TODO: queue responses for action uploads
-  /** Adds a Response to the Queue. */
+  /** Adds a Response to the queue. */
   public FakeRequestManager queueResponse(Response response) {
-    responses.add(response);
+    return queueResponse(response, /* delayMs= */ 0);
+  }
+
+  /** Adds a Response to the queue with a delay. */
+  public FakeRequestManager queueResponse(Response response, long delayMs) {
+    responses.add(new ResponseWithDelay(response, delayMs));
+    return this;
+  }
+
+  /** Adds an error to the queue. */
+  public FakeRequestManager queueError() {
+    return queueError(/* delayMs= */ 0);
+  }
+
+  /** Adds an error to the queue with a delay. */
+  public FakeRequestManager queueError(long delayMs) {
+    responses.add(new ResponseWithDelay(delayMs));
     return this;
   }
 
@@ -67,13 +91,7 @@
       Consumer<Result<List<StreamDataOperation>>> consumer) {
     fakeThreadUtils.checkNotMainThread();
     latestStreamToken = streamToken;
-    Response response = responses.remove();
-    Result<List<StreamDataOperation>> result = protocolAdapter.createModel(response);
-
-    // Call the consumer on the main thread.
-    boolean policy = fakeThreadUtils.enforceMainThread(true);
-    consumer.accept(result);
-    fakeThreadUtils.enforceMainThread(policy);
+    handleResponseWithDelay(responses.remove(), consumer);
   }
 
   @Override
@@ -88,21 +106,37 @@
       ConsistencyToken token,
       Consumer<Result<List<StreamDataOperation>>> consumer) {
     latestRequestReason = reason;
-    Response response = responses.remove();
-    Result<List<StreamDataOperation>> result = protocolAdapter.createModel(response);
+    ResponseWithDelay responseWithDelay = responses.remove();
+    taskQueue.execute(
+        "FakeRequestManager#triggerRefresh",
+        TaskType.HEAD_INVALIDATE,
+        () -> {
+          handleResponseWithDelay(responseWithDelay, consumer);
+        });
+  }
 
-    if (requestDelayMs > 0) {
-      try {
-        // slightly delay the results.
-        Thread.sleep(requestDelayMs);
-      } catch (InterruptedException e) {
-        throw new IllegalStateException(e);
-      }
+  private void handleResponseWithDelay(
+      ResponseWithDelay responseWithDelay, Consumer<Result<List<StreamDataOperation>>> consumer) {
+    if (responseWithDelay.delayMs > 0) {
+      mainThreadRunner.executeWithDelay(
+          "FakeRequestManager#consumer",
+          () -> {
+            invokeConsumer(responseWithDelay, consumer);
+          },
+          responseWithDelay.delayMs);
+    } else {
+      invokeConsumer(responseWithDelay, consumer);
     }
+  }
 
-    // Call the consumer on the main thread.
+  private void invokeConsumer(
+      ResponseWithDelay responseWithDelay, Consumer<Result<List<StreamDataOperation>>> consumer) {
     boolean policy = fakeThreadUtils.enforceMainThread(true);
-    consumer.accept(result);
+    if (responseWithDelay.isError) {
+      consumer.accept(Result.failure());
+    } else {
+      consumer.accept(protocolAdapter.createModel(responseWithDelay.response));
+    }
     fakeThreadUtils.enforceMainThread(policy);
   }
 
@@ -136,4 +170,22 @@
   public int getLatestRequestReason() {
     return latestRequestReason;
   }
+
+  private static final class ResponseWithDelay {
+    private final Response response;
+    private final boolean isError;
+    private final long delayMs;
+
+    private ResponseWithDelay(Response response, long delayMs) {
+      this.response = response;
+      this.delayMs = delayMs;
+      isError = false;
+    }
+
+    private ResponseWithDelay(long delayMs) {
+      this.response = Response.getDefaultInstance();
+      this.delayMs = delayMs;
+      isError = true;
+    }
+  }
 }
diff --git a/src/test/java/com/google/android/libraries/feed/feedsessionmanager/FeedSessionManagerTest.java b/src/test/java/com/google/android/libraries/feed/feedsessionmanager/FeedSessionManagerTest.java
index 8c0c799..7df3bdd 100644
--- a/src/test/java/com/google/android/libraries/feed/feedsessionmanager/FeedSessionManagerTest.java
+++ b/src/test/java/com/google/android/libraries/feed/feedsessionmanager/FeedSessionManagerTest.java
@@ -1,4 +1,4 @@
-// Copyright 2018 The Feed Authors.
+// Copyright 2019 The Feed Authors.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -131,7 +131,8 @@
     fakeStore = new FakeStore(fakeThreadUtils, fakeTaskQueue, fakeClock);
     fakeProtocolAdapter = new FakeProtocolAdapter();
     fakeRequestManager =
-        new FakeRequestManager(fakeThreadUtils, fakeProtocolAdapter, /* requestDelayMs= */ 0);
+        new FakeRequestManager(
+            fakeThreadUtils, fakeMainThreadRunner, fakeProtocolAdapter, fakeTaskQueue);
     fakeRequestManager.queueResponse(Response.getDefaultInstance());
     when(schedulerApi.shouldSessionRequestData(any(SessionManagerState.class)))
         .thenReturn(RequestBehavior.NO_REQUEST_WITH_CONTENT);
diff --git a/src/test/java/com/google/android/libraries/feed/infraintegration/BUILD b/src/test/java/com/google/android/libraries/feed/infraintegration/BUILD
index c473a7d..92dc65e 100644
--- a/src/test/java/com/google/android/libraries/feed/infraintegration/BUILD
+++ b/src/test/java/com/google/android/libraries/feed/infraintegration/BUILD
@@ -40,6 +40,7 @@
         "//src/main/java/com/google/android/libraries/feed/common/testing",
         "//src/main/java/com/google/android/libraries/feed/host/logging",
         "//src/main/java/com/google/android/libraries/feed/testing/requestmanager",
+        "//src/main/proto/com/google/android/libraries/feed/api/proto:client_feed_java_proto_lite",
         "//src/main/proto/search/now/wire/feed:feed_java_proto_lite",
         "@com_google_protobuf_javalite//:protobuf_java_lite",
         "@maven//:com_google_truth_truth",
@@ -63,6 +64,7 @@
         "//src/main/java/com/google/android/libraries/feed/common/testing",
         "//src/main/java/com/google/android/libraries/feed/host/logging",
         "//src/main/java/com/google/android/libraries/feed/testing/requestmanager",
+        "//src/main/proto/com/google/android/libraries/feed/api/proto:client_feed_java_proto_lite",
         "//src/main/proto/search/now/wire/feed:feed_java_proto_lite",
         "@com_google_protobuf_javalite//:protobuf_java_lite",
         "@maven//:com_google_truth_truth",
@@ -105,7 +107,10 @@
         "//src/main/java/com/google/android/libraries/feed/api/common",
         "//src/main/java/com/google/android/libraries/feed/api/modelprovider",
         "//src/main/java/com/google/android/libraries/feed/api/sessionmanager",
+        "//src/main/java/com/google/android/libraries/feed/common/concurrent",
+        "//src/main/java/com/google/android/libraries/feed/common/concurrent/testing",
         "//src/main/java/com/google/android/libraries/feed/common/testing",
+        "//src/main/java/com/google/android/libraries/feed/common/time/testing",
         "//src/main/java/com/google/android/libraries/feed/host/logging",
         "//src/main/java/com/google/android/libraries/feed/testing/requestmanager",
         "//src/main/proto/com/google/android/libraries/feed/api/proto:client_feed_java_proto_lite",
@@ -386,6 +391,7 @@
         "//src/main/java/com/google/android/libraries/feed/common/testing",
         "//src/main/java/com/google/android/libraries/feed/host/logging",
         "//src/main/java/com/google/android/libraries/feed/testing/requestmanager",
+        "//src/main/proto/com/google/android/libraries/feed/api/proto:client_feed_java_proto_lite",
         "//src/main/proto/search/now/wire/feed:feed_java_proto_lite",
         "@com_google_protobuf_javalite//:protobuf_java_lite",
         "@maven//:com_google_truth_truth",
@@ -408,8 +414,11 @@
         "//src/main/java/com/google/android/libraries/feed/api/protocoladapter",
         "//src/main/java/com/google/android/libraries/feed/api/sessionmanager",
         "//src/main/java/com/google/android/libraries/feed/common",
+        "//src/main/java/com/google/android/libraries/feed/common/concurrent",
+        "//src/main/java/com/google/android/libraries/feed/common/concurrent/testing",
         "//src/main/java/com/google/android/libraries/feed/common/functional",
         "//src/main/java/com/google/android/libraries/feed/common/testing",
+        "//src/main/java/com/google/android/libraries/feed/common/time/testing",
         "//src/main/java/com/google/android/libraries/feed/host/config",
         "//src/main/java/com/google/android/libraries/feed/host/logging",
         "//src/main/java/com/google/android/libraries/feed/testing/requestmanager",
@@ -430,13 +439,15 @@
     aapt_version = "aapt2",
     manifest_values = DEFAULT_ANDROID_LOCAL_TEST_MANIFEST,
     deps = [
-        "//src/main/java/com/google/android/libraries/feed/api/common",
         "//src/main/java/com/google/android/libraries/feed/api/modelprovider",
         "//src/main/java/com/google/android/libraries/feed/api/sessionmanager",
+        "//src/main/java/com/google/android/libraries/feed/common/concurrent/testing",
         "//src/main/java/com/google/android/libraries/feed/common/testing",
+        "//src/main/java/com/google/android/libraries/feed/common/time/testing",
         "//src/main/java/com/google/android/libraries/feed/feedsessionmanager",
         "//src/main/java/com/google/android/libraries/feed/host/config",
         "//src/main/java/com/google/android/libraries/feed/host/scheduler",
+        "//src/main/java/com/google/android/libraries/feed/testing/host/scheduler",
         "//src/main/java/com/google/android/libraries/feed/testing/requestmanager",
         "//src/main/proto/com/google/android/libraries/feed/api/proto:client_feed_java_proto_lite",
         "//src/main/proto/search/now/wire/feed:feed_java_proto_lite",
@@ -455,12 +466,14 @@
     aapt_version = "aapt2",
     manifest_values = DEFAULT_ANDROID_LOCAL_TEST_MANIFEST,
     deps = [
-        "//src/main/java/com/google/android/libraries/feed/api/common",
         "//src/main/java/com/google/android/libraries/feed/api/modelprovider",
         "//src/main/java/com/google/android/libraries/feed/api/protocoladapter",
+        "//src/main/java/com/google/android/libraries/feed/common/concurrent/testing",
         "//src/main/java/com/google/android/libraries/feed/common/testing",
+        "//src/main/java/com/google/android/libraries/feed/common/time/testing",
         "//src/main/java/com/google/android/libraries/feed/host/config",
         "//src/main/java/com/google/android/libraries/feed/host/scheduler",
+        "//src/main/java/com/google/android/libraries/feed/testing/host/scheduler",
         "//src/main/java/com/google/android/libraries/feed/testing/requestmanager",
         "//src/main/proto/com/google/android/libraries/feed/api/proto:client_feed_java_proto_lite",
         "//src/main/proto/search/now/wire/feed:feed_java_proto_lite",
@@ -489,6 +502,7 @@
         "//src/main/java/com/google/android/libraries/feed/host/scheduler",
         "//src/main/java/com/google/android/libraries/feed/testing/modelprovider",
         "//src/main/java/com/google/android/libraries/feed/testing/requestmanager",
+        "//src/main/proto/com/google/android/libraries/feed/api/proto:client_feed_java_proto_lite",
         "//src/main/proto/search/now/wire/feed:feed_java_proto_lite",
         "@com_google_protobuf_javalite//:protobuf_java_lite",
         "@maven//:org_mockito_mockito_core",
diff --git a/src/test/java/com/google/android/libraries/feed/infraintegration/ContentRemoveTest.java b/src/test/java/com/google/android/libraries/feed/infraintegration/ContentRemoveTest.java
index efc2323..db9e3ff 100644
--- a/src/test/java/com/google/android/libraries/feed/infraintegration/ContentRemoveTest.java
+++ b/src/test/java/com/google/android/libraries/feed/infraintegration/ContentRemoveTest.java
@@ -1,4 +1,4 @@
-// Copyright 2018 The Feed Authors.
+// Copyright 2019 The Feed Authors.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -34,6 +34,8 @@
 import com.google.android.libraries.feed.common.testing.ResponseBuilder;
 import com.google.android.libraries.feed.host.logging.RequestReason;
 import com.google.android.libraries.feed.testing.requestmanager.FakeRequestManager;
+import com.google.search.now.feed.client.StreamDataProto.StreamToken;
+import com.google.search.now.wire.feed.ConsistencyTokenProto.ConsistencyToken;
 import com.google.search.now.wire.feed.ContentIdProto.ContentId;
 import java.util.List;
 import org.junit.Before;
@@ -94,8 +96,10 @@
 
     requestManager.queueResponse(
         ResponseBuilder.builder().removeFeature(cards[1], ROOT_CONTENT_ID).build());
-    requestManager.triggerRefresh(
-        RequestReason.OPEN_WITHOUT_CONTENT,
+    // TODO: sessions reject removes without a CLEAR_ALL or paging with a different token.
+    requestManager.loadMore(
+        StreamToken.getDefaultInstance(),
+        ConsistencyToken.getDefaultInstance(),
         sessionManager.getUpdateConsumer(MutationContext.EMPTY_CONTEXT));
 
     ArgumentCaptor<FeatureChange> capture = ArgumentCaptor.forClass(FeatureChange.class);
diff --git a/src/test/java/com/google/android/libraries/feed/infraintegration/ContentUpdateTest.java b/src/test/java/com/google/android/libraries/feed/infraintegration/ContentUpdateTest.java
index d93d0fb..846f82d 100644
--- a/src/test/java/com/google/android/libraries/feed/infraintegration/ContentUpdateTest.java
+++ b/src/test/java/com/google/android/libraries/feed/infraintegration/ContentUpdateTest.java
@@ -1,4 +1,4 @@
-// Copyright 2018 The Feed Authors.
+// Copyright 2019 The Feed Authors.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -35,6 +35,8 @@
 import com.google.android.libraries.feed.common.testing.ResponseBuilder;
 import com.google.android.libraries.feed.host.logging.RequestReason;
 import com.google.android.libraries.feed.testing.requestmanager.FakeRequestManager;
+import com.google.search.now.feed.client.StreamDataProto.StreamToken;
+import com.google.search.now.wire.feed.ConsistencyTokenProto.ConsistencyToken;
 import com.google.search.now.wire.feed.ContentIdProto.ContentId;
 import java.util.ArrayList;
 import java.util.List;
@@ -109,8 +111,10 @@
 
     // Create an update response for the two content items
     requestManager.queueResponse(ResponseBuilder.builder().addCardsToRoot(cards).build());
-    requestManager.triggerRefresh(
-        RequestReason.OPEN_WITHOUT_CONTENT,
+    // TODO: sessions reject updates without a CLEAR_ALL or paging with a different token.
+    requestManager.loadMore(
+        StreamToken.getDefaultInstance(),
+        ConsistencyToken.getDefaultInstance(),
         sessionManager.getUpdateConsumer(MutationContext.EMPTY_CONTEXT));
 
     int id = 0;
diff --git a/src/test/java/com/google/android/libraries/feed/infraintegration/EmptyStreamTest.java b/src/test/java/com/google/android/libraries/feed/infraintegration/EmptyStreamTest.java
index 1c0b175..faaa2d5 100644
--- a/src/test/java/com/google/android/libraries/feed/infraintegration/EmptyStreamTest.java
+++ b/src/test/java/com/google/android/libraries/feed/infraintegration/EmptyStreamTest.java
@@ -1,4 +1,4 @@
-// Copyright 2018 The Feed Authors.
+// Copyright 2019 The Feed Authors.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -21,29 +21,30 @@
 import static org.mockito.MockitoAnnotations.initMocks;
 
 import com.google.android.libraries.feed.api.common.MutationContext;
-import com.google.android.libraries.feed.api.common.ThreadUtils;
 import com.google.android.libraries.feed.api.modelprovider.ModelProvider;
 import com.google.android.libraries.feed.api.modelprovider.ModelProvider.State;
 import com.google.android.libraries.feed.api.modelprovider.ModelProviderFactory;
 import com.google.android.libraries.feed.api.modelprovider.ModelProviderObserver;
 import com.google.android.libraries.feed.api.sessionmanager.SessionManager;
+import com.google.android.libraries.feed.common.concurrent.TaskQueue;
+import com.google.android.libraries.feed.common.concurrent.testing.FakeThreadUtils;
 import com.google.android.libraries.feed.common.testing.InfraIntegrationScope;
 import com.google.android.libraries.feed.common.testing.ResponseBuilder;
 import com.google.android.libraries.feed.common.testing.ResponseBuilder.WireProtocolInfo;
+import com.google.android.libraries.feed.common.time.testing.FakeClock;
 import com.google.android.libraries.feed.host.logging.RequestReason;
 import com.google.android.libraries.feed.testing.requestmanager.FakeRequestManager;
 import com.google.search.now.feed.client.StreamDataProto.UiContext;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.Mock;
 import org.robolectric.RobolectricTestRunner;
 
 /** Test which verifies the ModelProvider state for the empty stream cases. */
 @RunWith(RobolectricTestRunner.class)
 public class EmptyStreamTest {
-  @Mock private ThreadUtils threadUtils;
 
+  private FakeClock fakeClock;
   private FakeRequestManager requestManager;
   private SessionManager sessionManager;
   private ModelProviderFactory modelProviderFactory;
@@ -51,7 +52,10 @@
   @Before
   public void setUp() {
     initMocks(this);
-    InfraIntegrationScope scope = new InfraIntegrationScope.Builder(threadUtils).build();
+    InfraIntegrationScope scope =
+        new InfraIntegrationScope.Builder(new FakeThreadUtils(/* enforceThreadChecks= */ false))
+            .build();
+    fakeClock = scope.getFakeClock();
     requestManager = scope.getRequestManager();
     sessionManager = scope.getSessionManager();
     modelProviderFactory = scope.getModelProviderFactory();
@@ -101,6 +105,8 @@
     requestManager.triggerRefresh(
         RequestReason.OPEN_WITHOUT_CONTENT,
         sessionManager.getUpdateConsumer(MutationContext.EMPTY_CONTEXT));
+    // TODO: the empty response is missing CLEAR_ALL, so the TaskQueue is delaying.
+    fakeClock.advance(TaskQueue.STARVATION_TIMEOUT_MS);
     ModelProvider modelProvider = modelProviderFactory.createNew(null);
 
     assertThat(modelProvider).isNotNull();
diff --git a/src/test/java/com/google/android/libraries/feed/infraintegration/GcTest.java b/src/test/java/com/google/android/libraries/feed/infraintegration/GcTest.java
index ca137be..f442d9b 100644
--- a/src/test/java/com/google/android/libraries/feed/infraintegration/GcTest.java
+++ b/src/test/java/com/google/android/libraries/feed/infraintegration/GcTest.java
@@ -47,13 +47,12 @@
       };
   private static final long LIFETIME_MS = Duration.ofHours(1).toMillis();
 
-  private final FakeClock fakeClock = new FakeClock();
   private final InfraIntegrationScope scope =
       new InfraIntegrationScope.Builder(new FakeThreadUtils(/* enforceThreadChecks= */ false))
-          .setClock(fakeClock)
           .setConfiguration(
               new Configuration.Builder().put(ConfigKey.SESSION_LIFETIME_MS, LIFETIME_MS).build())
           .build();
+  private final FakeClock fakeClock = scope.getFakeClock();
 
   @Test
   public void testGc_contentInLiveSessionRetained() {
diff --git a/src/test/java/com/google/android/libraries/feed/infraintegration/RootOnlyTest.java b/src/test/java/com/google/android/libraries/feed/infraintegration/RootOnlyTest.java
index e1a7258..73a6b7e 100644
--- a/src/test/java/com/google/android/libraries/feed/infraintegration/RootOnlyTest.java
+++ b/src/test/java/com/google/android/libraries/feed/infraintegration/RootOnlyTest.java
@@ -1,4 +1,4 @@
-// Copyright 2018 The Feed Authors.
+// Copyright 2019 The Feed Authors.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -34,7 +34,9 @@
 import com.google.android.libraries.feed.common.testing.ResponseBuilder.WireProtocolInfo;
 import com.google.android.libraries.feed.host.logging.RequestReason;
 import com.google.android.libraries.feed.testing.requestmanager.FakeRequestManager;
+import com.google.search.now.feed.client.StreamDataProto.StreamToken;
 import com.google.search.now.feed.client.StreamDataProto.UiContext;
+import com.google.search.now.wire.feed.ConsistencyTokenProto.ConsistencyToken;
 import com.google.search.now.wire.feed.ContentIdProto.ContentId;
 import org.junit.Before;
 import org.junit.Test;
@@ -102,8 +104,10 @@
 
     ResponseBuilder responseBuilder = new ResponseBuilder().addRootFeature();
     requestManager.queueResponse(responseBuilder.build());
-    requestManager.triggerRefresh(
-        RequestReason.OPEN_WITHOUT_CONTENT,
+    // TODO: sessions reject updates without a CLEAR_ALL or paging with a different token.
+    requestManager.loadMore(
+        StreamToken.getDefaultInstance(),
+        ConsistencyToken.getDefaultInstance(),
         sessionManager.getUpdateConsumer(MutationContext.EMPTY_CONTEXT));
     verify(changeObserver, never()).onSessionFinished(any(UiContext.class));
 
@@ -142,8 +146,10 @@
             .build();
     responseBuilder = new ResponseBuilder().addRootFeature(anotherRoot);
     requestManager.queueResponse(responseBuilder.build());
-    requestManager.triggerRefresh(
-        RequestReason.OPEN_WITHOUT_CONTENT,
+    // TODO: sessions reject updates without a CLEAR_ALL or paging with a different token.
+    requestManager.loadMore(
+        StreamToken.getDefaultInstance(),
+        ConsistencyToken.getDefaultInstance(),
         sessionManager.getUpdateConsumer(MutationContext.EMPTY_CONTEXT));
     verify(changeObserver).onSessionFinished(any(UiContext.class));
   }
diff --git a/src/test/java/com/google/android/libraries/feed/infraintegration/SemanticPropertiesTest.java b/src/test/java/com/google/android/libraries/feed/infraintegration/SemanticPropertiesTest.java
index 0dda9b7..91ae372 100644
--- a/src/test/java/com/google/android/libraries/feed/infraintegration/SemanticPropertiesTest.java
+++ b/src/test/java/com/google/android/libraries/feed/infraintegration/SemanticPropertiesTest.java
@@ -1,4 +1,4 @@
-// Copyright 2018 The Feed Authors.
+// Copyright 2019 The Feed Authors.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -63,7 +63,10 @@
     ByteString semanticData = ByteString.copyFromUtf8("helloWorld");
 
     Response response =
-        new ResponseBuilder().addCardWithSemanticData(contentId, semanticData).build();
+        new ResponseBuilder()
+            .addClearOperation()
+            .addCardWithSemanticData(contentId, semanticData)
+            .build();
     requestManager.queueResponse(response);
     requestManager.triggerRefresh(
         RequestReason.OPEN_WITHOUT_CONTENT,
diff --git a/src/test/java/com/google/android/libraries/feed/infraintegration/StructureUpdateTest.java b/src/test/java/com/google/android/libraries/feed/infraintegration/StructureUpdateTest.java
index 9069ae5..8a4c56c 100644
--- a/src/test/java/com/google/android/libraries/feed/infraintegration/StructureUpdateTest.java
+++ b/src/test/java/com/google/android/libraries/feed/infraintegration/StructureUpdateTest.java
@@ -1,4 +1,4 @@
-// Copyright 2018 The Feed Authors.
+// Copyright 2019 The Feed Authors.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -35,6 +35,8 @@
 import com.google.android.libraries.feed.common.testing.ResponseBuilder;
 import com.google.android.libraries.feed.host.logging.RequestReason;
 import com.google.android.libraries.feed.testing.requestmanager.FakeRequestManager;
+import com.google.search.now.feed.client.StreamDataProto.StreamToken;
+import com.google.search.now.wire.feed.ConsistencyTokenProto.ConsistencyToken;
 import com.google.search.now.wire.feed.ContentIdProto.ContentId;
 import java.util.List;
 import org.junit.Before;
@@ -93,8 +95,10 @@
 
     // Append new children to root
     requestManager.queueResponse(ResponseBuilder.builder().addCardsToRoot(appendedCards).build());
-    requestManager.triggerRefresh(
-        RequestReason.OPEN_WITHOUT_CONTENT,
+    // TODO: sessions reject updates without a CLEAR_ALL or paging with a different token.
+    requestManager.loadMore(
+        StreamToken.getDefaultInstance(),
+        ConsistencyToken.getDefaultInstance(),
         sessionManager.getUpdateConsumer(MutationContext.EMPTY_CONTEXT));
 
     // assert the new state of the stream
@@ -143,8 +147,10 @@
 
     // Now append additional children to the stream (and cursor)
     requestManager.queueResponse(ResponseBuilder.builder().addCardsToRoot(appendedCards).build());
-    requestManager.triggerRefresh(
-        RequestReason.OPEN_WITHOUT_CONTENT,
+    // TODO: sessions reject updates without a CLEAR_ALL or paging with a different token.
+    requestManager.loadMore(
+        StreamToken.getDefaultInstance(),
+        ConsistencyToken.getDefaultInstance(),
         sessionManager.getUpdateConsumer(MutationContext.EMPTY_CONTEXT));
     modelValidator.assertCursorSize(cursor, 3);
   }
diff --git a/src/test/java/com/google/android/libraries/feed/infraintegration/SyntheticTokensTest.java b/src/test/java/com/google/android/libraries/feed/infraintegration/SyntheticTokensTest.java
index 565c216..f91ef6f 100644
--- a/src/test/java/com/google/android/libraries/feed/infraintegration/SyntheticTokensTest.java
+++ b/src/test/java/com/google/android/libraries/feed/infraintegration/SyntheticTokensTest.java
@@ -1,4 +1,4 @@
-// Copyright 2018 The Feed Authors.
+// Copyright 2019 The Feed Authors.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -21,7 +21,6 @@
 import static org.mockito.MockitoAnnotations.initMocks;
 
 import com.google.android.libraries.feed.api.common.MutationContext;
-import com.google.android.libraries.feed.api.common.ThreadUtils;
 import com.google.android.libraries.feed.api.common.testing.ContentIdGenerators;
 import com.google.android.libraries.feed.api.modelprovider.ModelChild;
 import com.google.android.libraries.feed.api.modelprovider.ModelCursor;
@@ -35,11 +34,14 @@
 import com.google.android.libraries.feed.api.protocoladapter.ProtocolAdapter;
 import com.google.android.libraries.feed.api.sessionmanager.SessionManager;
 import com.google.android.libraries.feed.common.Result;
+import com.google.android.libraries.feed.common.concurrent.TaskQueue;
+import com.google.android.libraries.feed.common.concurrent.testing.FakeThreadUtils;
 import com.google.android.libraries.feed.common.functional.Consumer;
 import com.google.android.libraries.feed.common.testing.InfraIntegrationScope;
 import com.google.android.libraries.feed.common.testing.ModelProviderValidator;
 import com.google.android.libraries.feed.common.testing.PagingState;
 import com.google.android.libraries.feed.common.testing.ResponseBuilder;
+import com.google.android.libraries.feed.common.time.testing.FakeClock;
 import com.google.android.libraries.feed.host.config.Configuration;
 import com.google.android.libraries.feed.host.config.Configuration.ConfigKey;
 import com.google.android.libraries.feed.host.logging.RequestReason;
@@ -52,7 +54,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
 import org.robolectric.RobolectricTestRunner;
 
 /** Test Synthetic tokens. */
@@ -62,8 +63,7 @@
   private static final int PAGE_SIZE = 4;
   private static final int MIN_PAGE_SIZE = 2;
 
-  @Mock private ThreadUtils threadUtils;
-
+  private FakeClock fakeClock;
   private FakeRequestManager requestManager;
   private SessionManager sessionManager;
   private ModelProviderFactory modelProviderFactory;
@@ -81,7 +81,10 @@
             .put(ConfigKey.NON_CACHED_MIN_PAGE_SIZE, MIN_PAGE_SIZE)
             .build();
     InfraIntegrationScope scope =
-        new InfraIntegrationScope.Builder(threadUtils).setConfiguration(configuration).build();
+        new InfraIntegrationScope.Builder(new FakeThreadUtils(/* enforceThreadChecks= */ false))
+            .setConfiguration(configuration)
+            .build();
+    fakeClock = scope.getFakeClock();
     requestManager = scope.getRequestManager();
     sessionManager = scope.getSessionManager();
     modelProviderFactory = scope.getModelProviderFactory();
@@ -517,5 +520,6 @@
     requestManager.triggerRefresh(
         RequestReason.OPEN_WITHOUT_CONTENT,
         sessionManager.getUpdateConsumer(MutationContext.EMPTY_CONTEXT));
+    fakeClock.advance(TaskQueue.STARVATION_TIMEOUT_MS);
   }
 }
diff --git a/src/test/java/com/google/android/libraries/feed/infraintegration/TimeoutSessionBaseTest.java b/src/test/java/com/google/android/libraries/feed/infraintegration/TimeoutSessionBaseTest.java
index c5657fd..2d5ddf5 100644
--- a/src/test/java/com/google/android/libraries/feed/infraintegration/TimeoutSessionBaseTest.java
+++ b/src/test/java/com/google/android/libraries/feed/infraintegration/TimeoutSessionBaseTest.java
@@ -1,4 +1,4 @@
-// Copyright 2018 The Feed Authors.
+// Copyright 2019 The Feed Authors.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -15,36 +15,31 @@
 package com.google.android.libraries.feed.infraintegration;
 
 import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.when;
 import static org.mockito.MockitoAnnotations.initMocks;
 
-import com.google.android.libraries.feed.api.common.ThreadUtils;
 import com.google.android.libraries.feed.api.modelprovider.ModelError;
 import com.google.android.libraries.feed.api.modelprovider.ModelProvider;
 import com.google.android.libraries.feed.api.modelprovider.ModelProviderFactory;
 import com.google.android.libraries.feed.api.modelprovider.ModelProviderObserver;
 import com.google.android.libraries.feed.api.sessionmanager.SessionManager;
+import com.google.android.libraries.feed.common.concurrent.testing.FakeThreadUtils;
 import com.google.android.libraries.feed.common.testing.InfraIntegrationScope;
 import com.google.android.libraries.feed.common.testing.ModelProviderValidator;
 import com.google.android.libraries.feed.common.testing.ResponseBuilder;
+import com.google.android.libraries.feed.common.time.testing.FakeClock;
 import com.google.android.libraries.feed.feedsessionmanager.FeedSessionManager;
 import com.google.android.libraries.feed.host.config.Configuration;
-import com.google.android.libraries.feed.host.scheduler.SchedulerApi;
 import com.google.android.libraries.feed.host.scheduler.SchedulerApi.RequestBehavior;
-import com.google.android.libraries.feed.host.scheduler.SchedulerApi.SessionManagerState;
+import com.google.android.libraries.feed.testing.host.scheduler.FakeSchedulerApi;
 import com.google.android.libraries.feed.testing.requestmanager.FakeRequestManager;
 import com.google.search.now.feed.client.StreamDataProto.UiContext;
 import com.google.search.now.wire.feed.ContentIdProto.ContentId;
-import java.util.concurrent.Executors;
 import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicBoolean;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.Mock;
 import org.robolectric.RobolectricTestRunner;
-import org.robolectric.shadows.ShadowLooper;
 
 /**
  * This is a TimeoutSession test which verifies the REQUEST_WITH_WAIT.
@@ -58,9 +53,11 @@
   // This flag will should be flipped to debug the test.  It will disable TimeoutExceptions.
   private static final boolean DEBUG = false;
 
-  @Mock private ThreadUtils threadUtils;
-  @Mock private SchedulerApi schedulerApi;
+  private final FakeThreadUtils fakeThreadUtils =
+      new FakeThreadUtils(/* enforceThreadChecks= */ false);
+  private final FakeSchedulerApi fakeSchedulerApi = new FakeSchedulerApi(fakeThreadUtils);
 
+  private FakeClock fakeClock;
   private FakeRequestManager requestManager;
   private SessionManager sessionManager;
   private ModelProviderFactory modelProviderFactory;
@@ -72,11 +69,11 @@
     initMocks(this);
     Configuration configuration = InfraIntegrationScope.getTimeoutSchedulerConfig();
     InfraIntegrationScope scope =
-        new InfraIntegrationScope.Builder(threadUtils)
+        new InfraIntegrationScope.Builder(fakeThreadUtils)
             .setConfiguration(configuration)
-            .setExecutorService(Executors.newSingleThreadExecutor())
-            .setSchedulerApi(schedulerApi)
+            .setSchedulerApi(fakeSchedulerApi)
             .build();
+    fakeClock = scope.getFakeClock();
     requestManager = scope.getRequestManager();
     sessionManager = scope.getSessionManager();
     modelProviderFactory = scope.getModelProviderFactory();
@@ -103,45 +100,48 @@
         };
 
     // Load up the initial request
-    requestManager.queueResponse(ResponseBuilder.forClearAllWithCards(requestOne).build());
+    requestManager.queueResponse(
+        ResponseBuilder.forClearAllWithCards(requestOne).build(), /* delayMs= */ 100);
 
     // Wait for the request to complete (REQUEST_WITH_WAIT).  This will trigger the request and wait
     // for it to complete to populate the new session.
-    when(schedulerApi.shouldSessionRequestData(any(SessionManagerState.class)))
-        .thenReturn(RequestBehavior.REQUEST_WITH_WAIT);
+    fakeSchedulerApi.setRequestBehavior(RequestBehavior.REQUEST_WITH_WAIT);
     ModelProvider modelProvider = modelProviderFactory.createNew(null);
 
     // This will wait for the session to be created and validate the root cursor
     AtomicBoolean finished = new AtomicBoolean(false);
     assertSessionCreation(modelProvider, finished, requestOne);
+    long startTimeMs = fakeClock.currentTimeMillis();
     while (!finished.get()) {
       // Loop through the tasks and wait for the assertSessionCreation to set finished to true
-      ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
-      if (timeoutDeadline > 0 && System.currentTimeMillis() > timeoutDeadline) {
+      fakeClock.tick();
+      if (timeoutDeadline > 0 && fakeClock.currentTimeMillis() > timeoutDeadline) {
         throw new TimeoutException();
       }
     }
+    assertThat(fakeClock.currentTimeMillis() - startTimeMs).isAtLeast(100L);
 
     // Create a new ModelProvider from HEAD (NO_REQUEST_WITH_CONTENT)
-    when(schedulerApi.shouldSessionRequestData(any(SessionManagerState.class)))
-        .thenReturn(RequestBehavior.NO_REQUEST_WITH_CONTENT);
+    fakeSchedulerApi.setRequestBehavior(RequestBehavior.NO_REQUEST_WITH_CONTENT);
     // This will wait for the session to be created and validate the root cursor
     modelProvider = modelProviderFactory.createNew(null);
     assertSessionCreation(modelProvider, finished, requestOne);
+    startTimeMs = fakeClock.currentTimeMillis();
     while (!finished.get()) {
       // Loop through the tasks and wait for the assertSessionCreation to set finished to true
-      ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
-      if (timeoutDeadline > 0 && System.currentTimeMillis() > timeoutDeadline) {
+      fakeClock.tick();
+      if (timeoutDeadline > 0 && fakeClock.currentTimeMillis() > timeoutDeadline) {
         throw new TimeoutException();
       }
     }
+    assertThat(fakeClock.currentTimeMillis() - startTimeMs).isEqualTo(0);
   }
 
   private void assertSessionCreation(
       ModelProvider modelProvider, AtomicBoolean finished, ContentId... cards) {
     finished.set(false);
     timeoutDeadline =
-        DEBUG ? InfraIntegrationScope.TIMEOUT_TEST_TIMEOUT + System.currentTimeMillis() : 0;
+        DEBUG ? InfraIntegrationScope.TIMEOUT_TEST_TIMEOUT + fakeClock.currentTimeMillis() : 0;
     modelProvider.registerObserver(
         new ModelProviderObserver() {
           @Override
diff --git a/src/test/java/com/google/android/libraries/feed/infraintegration/TimeoutSessionWithContentTest.java b/src/test/java/com/google/android/libraries/feed/infraintegration/TimeoutSessionWithContentTest.java
index b1f0e4e..8e2ec0a 100644
--- a/src/test/java/com/google/android/libraries/feed/infraintegration/TimeoutSessionWithContentTest.java
+++ b/src/test/java/com/google/android/libraries/feed/infraintegration/TimeoutSessionWithContentTest.java
@@ -1,4 +1,4 @@
-// Copyright 2018 The Feed Authors.
+// Copyright 2019 The Feed Authors.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -16,11 +16,8 @@
 
 import static com.google.android.libraries.feed.common.testing.ResponseBuilder.ROOT_CONTENT_ID;
 import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.when;
 import static org.mockito.MockitoAnnotations.initMocks;
 
-import com.google.android.libraries.feed.api.common.ThreadUtils;
 import com.google.android.libraries.feed.api.modelprovider.FeatureChange.ChildChanges;
 import com.google.android.libraries.feed.api.modelprovider.ModelError;
 import com.google.android.libraries.feed.api.modelprovider.ModelFeature;
@@ -28,25 +25,23 @@
 import com.google.android.libraries.feed.api.modelprovider.ModelProviderFactory;
 import com.google.android.libraries.feed.api.modelprovider.ModelProviderObserver;
 import com.google.android.libraries.feed.api.protocoladapter.ProtocolAdapter;
+import com.google.android.libraries.feed.common.concurrent.testing.FakeThreadUtils;
 import com.google.android.libraries.feed.common.testing.InfraIntegrationScope;
 import com.google.android.libraries.feed.common.testing.ModelProviderValidator;
 import com.google.android.libraries.feed.common.testing.ResponseBuilder;
+import com.google.android.libraries.feed.common.time.testing.FakeClock;
 import com.google.android.libraries.feed.host.config.Configuration;
-import com.google.android.libraries.feed.host.scheduler.SchedulerApi;
 import com.google.android.libraries.feed.host.scheduler.SchedulerApi.RequestBehavior;
-import com.google.android.libraries.feed.host.scheduler.SchedulerApi.SessionManagerState;
+import com.google.android.libraries.feed.testing.host.scheduler.FakeSchedulerApi;
 import com.google.android.libraries.feed.testing.requestmanager.FakeRequestManager;
 import com.google.search.now.feed.client.StreamDataProto.UiContext;
 import com.google.search.now.wire.feed.ContentIdProto.ContentId;
-import java.util.concurrent.Executors;
 import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicBoolean;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.Mock;
 import org.robolectric.RobolectricTestRunner;
-import org.robolectric.shadows.ShadowLooper;
 
 /**
  * This is a TimeoutSession test which verifies REQUEST_WITH_CONTENT
@@ -72,9 +67,11 @@
         ResponseBuilder.createFeatureContentId(5)
       };
 
-  @Mock private ThreadUtils threadUtils;
-  @Mock private SchedulerApi schedulerApi;
+  private final FakeThreadUtils fakeThreadUtils =
+      new FakeThreadUtils(/* enforceThreadChecks= */ false);
+  private final FakeSchedulerApi fakeSchedulerApi = new FakeSchedulerApi(fakeThreadUtils);
 
+  private FakeClock fakeClock;
   private FakeRequestManager requestManager;
   private ModelProviderFactory modelProviderFactory;
   private ProtocolAdapter protocolAdapter;
@@ -86,12 +83,11 @@
     initMocks(this);
     Configuration configuration = InfraIntegrationScope.getTimeoutSchedulerConfig();
     InfraIntegrationScope scope =
-        new InfraIntegrationScope.Builder(threadUtils)
+        new InfraIntegrationScope.Builder(fakeThreadUtils)
             .setConfiguration(configuration)
-            .setExecutorService(Executors.newSingleThreadExecutor())
-            .setSchedulerApi(schedulerApi)
-            .setRequestDelayMs(100)
+            .setSchedulerApi(fakeSchedulerApi)
             .build();
+    fakeClock = scope.getFakeClock();
     requestManager = scope.getRequestManager();
     modelProviderFactory = scope.getModelProviderFactory();
     protocolAdapter = scope.getProtocolAdapter();
@@ -113,46 +109,50 @@
   public void testRequestWithWait() throws TimeoutException {
 
     // Load up the initial request
-    requestManager.queueResponse(ResponseBuilder.forClearAllWithCards(REQUEST_ONE).build());
+    requestManager.queueResponse(
+        ResponseBuilder.forClearAllWithCards(REQUEST_ONE).build(), /* delayMs= */ 100);
 
     // Wait for the request to complete (REQUEST_WITH_CONTENT).  This will trigger the request and
     // wait for it to complete to populate the new session.
-    when(schedulerApi.shouldSessionRequestData(any(SessionManagerState.class)))
-        .thenReturn(RequestBehavior.REQUEST_WITH_WAIT);
+    fakeSchedulerApi.setRequestBehavior(RequestBehavior.REQUEST_WITH_WAIT);
     ModelProvider modelProvider = modelProviderFactory.createNew(null);
 
     // This will wait for the session to be created and validate the root cursor
     AtomicBoolean finished = new AtomicBoolean(false);
     assertSessionCreation(modelProvider, finished);
+    long startTimeMs = fakeClock.currentTimeMillis();
     while (!finished.get()) {
       // Loop through the tasks and wait for the assertSessionCreation to set finished to true
-      ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
-      if (timeoutDeadline > 0 && System.currentTimeMillis() > timeoutDeadline) {
+      fakeClock.tick();
+      if (timeoutDeadline > 0 && fakeClock.currentTimeMillis() > timeoutDeadline) {
         throw new TimeoutException();
       }
     }
+    assertThat(fakeClock.currentTimeMillis() - startTimeMs).isAtLeast(100L);
 
     // Create a new ModelProvider from HEAD (REQUEST_WITH_CONTENT)
-    requestManager.queueResponse(ResponseBuilder.forClearAllWithCards(REQUEST_TWO).build());
-    when(schedulerApi.shouldSessionRequestData(any(SessionManagerState.class)))
-        .thenReturn(RequestBehavior.REQUEST_WITH_CONTENT);
+    requestManager.queueResponse(
+        ResponseBuilder.forClearAllWithCards(REQUEST_TWO).build(), /* delayMs= */ 100);
+    fakeSchedulerApi.setRequestBehavior(RequestBehavior.REQUEST_WITH_CONTENT);
     // This will wait for the session to be created and validate the root cursor
     modelProvider = modelProviderFactory.createNew(null);
     assertSessionCreationWithRequest(modelProvider, finished);
+    startTimeMs = fakeClock.currentTimeMillis();
     while (!finished.get()) {
       // Loop through the tasks and wait for the assertSessionCreation to set finished to true
-      ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
-      if (timeoutDeadline > 0 && System.currentTimeMillis() > timeoutDeadline) {
+      fakeClock.tick();
+      if (timeoutDeadline > 0 && fakeClock.currentTimeMillis() > timeoutDeadline) {
         throw new TimeoutException();
       }
     }
+    assertThat(fakeClock.currentTimeMillis() - startTimeMs).isAtLeast(100L);
   }
 
   // Verifies the initial session.
   private void assertSessionCreation(ModelProvider modelProvider, AtomicBoolean finished) {
     finished.set(false);
     timeoutDeadline =
-        DEBUG ? InfraIntegrationScope.TIMEOUT_TEST_TIMEOUT + System.currentTimeMillis() : 0;
+        DEBUG ? InfraIntegrationScope.TIMEOUT_TEST_TIMEOUT + fakeClock.currentTimeMillis() : 0;
     modelProvider.registerObserver(
         new ModelProviderObserver() {
           @Override
@@ -180,7 +180,7 @@
       ModelProvider modelProvider, AtomicBoolean finished) {
     finished.set(false);
     timeoutDeadline =
-        DEBUG ? InfraIntegrationScope.TIMEOUT_TEST_TIMEOUT + System.currentTimeMillis() : 0;
+        DEBUG ? InfraIntegrationScope.TIMEOUT_TEST_TIMEOUT + fakeClock.currentTimeMillis() : 0;
     modelProvider.registerObserver(
         new ModelProviderObserver() {
           @Override
diff --git a/src/test/java/com/google/android/libraries/feed/infraintegration/ViewDepthProviderTests.java b/src/test/java/com/google/android/libraries/feed/infraintegration/ViewDepthProviderTests.java
index a2cfe55..4bd5db1 100644
--- a/src/test/java/com/google/android/libraries/feed/infraintegration/ViewDepthProviderTests.java
+++ b/src/test/java/com/google/android/libraries/feed/infraintegration/ViewDepthProviderTests.java
@@ -35,6 +35,8 @@
 import com.google.android.libraries.feed.host.scheduler.SchedulerApi.SessionManagerState;
 import com.google.android.libraries.feed.testing.modelprovider.FakeViewDepthProvider;
 import com.google.android.libraries.feed.testing.requestmanager.FakeRequestManager;
+import com.google.search.now.feed.client.StreamDataProto.StreamToken;
+import com.google.search.now.wire.feed.ConsistencyTokenProto.ConsistencyToken;
 import com.google.search.now.wire.feed.ContentIdProto.ContentId;
 import org.junit.Before;
 import org.junit.Test;
@@ -166,8 +168,10 @@
     // Now page in the same content, this should all be updates
     requestManager.queueResponse(
         ResponseBuilder.builder().addCardsToRoot(REQUEST_TWO_WITH_DUPLICATES_PAGE).build());
-    requestManager.triggerRefresh(
-        RequestReason.HOST_REQUESTED,
+    // TODO: sessions reject updates without a CLEAR_ALL or paging with a different token.
+    requestManager.loadMore(
+        StreamToken.getDefaultInstance(),
+        ConsistencyToken.getDefaultInstance(),
         sessionManager.getUpdateConsumer(MutationContext.EMPTY_CONTEXT));
 
     modelValidator.assertCursorContents(