From 063df721ae7574ae8bc7912a8b00125234eb95e3 Mon Sep 17 00:00:00 2001
From: Jesse Mazzella <ozyx@users.noreply.github.com>
Date: Thu, 7 Jul 2022 16:51:12 -0700
Subject: [PATCH] [Remote Clock] Wait for first tick and recalculate historical
 request bounds (#5433)

* Updated to ES6 class
* added request intercept functionality to telemetry api, added a request interceptor for remote clock
* add remoteClock e2e test stub

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
---
 .../remoteClock/remoteClock.e2e.spec.js       |  41 ++
 .../telemetryTable/telemetryTable.e2e.spec.js |  21 +-
 src/MCT.js                                    |   2 +-
 src/api/telemetry/TelemetryAPI.js             | 368 ++++++++++--------
 src/api/telemetry/TelemetryAPISpec.js         |   2 +-
 src/api/telemetry/TelemetryCollection.js      |   5 +-
 .../telemetry/TelemetryRequestInterceptor.js  |  68 ++++
 src/plugins/imagery/pluginSpec.js             | 140 +++----
 src/plugins/remoteClock/RemoteClock.js        |  30 ++
 src/plugins/remoteClock/requestInterceptor.js |  46 +++
 src/plugins/telemetryTable/pluginSpec.js      | 108 ++---
 webpack.common.js                             |  19 +-
 12 files changed, 517 insertions(+), 333 deletions(-)
 create mode 100644 e2e/tests/plugins/remoteClock/remoteClock.e2e.spec.js
 create mode 100644 src/api/telemetry/TelemetryRequestInterceptor.js
 create mode 100644 src/plugins/remoteClock/requestInterceptor.js

diff --git a/e2e/tests/plugins/remoteClock/remoteClock.e2e.spec.js b/e2e/tests/plugins/remoteClock/remoteClock.e2e.spec.js
new file mode 100644
index 0000000000..f9167d9e4e
--- /dev/null
+++ b/e2e/tests/plugins/remoteClock/remoteClock.e2e.spec.js
@@ -0,0 +1,41 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+const { test } = require('../../../fixtures.js');
+// eslint-disable-next-line no-unused-vars
+const { expect } = require('@playwright/test');
+
+test.describe('Remote Clock', () => {
+    // eslint-disable-next-line require-await
+    test.fixme('blocks historical requests until first tick is received', async ({ page }) => {
+        test.info().annotations.push({
+            type: 'issue',
+            description: 'https://github.com/nasa/openmct/issues/5221'
+        });
+        // addInitScript to with remote clock
+        // Switch time conductor mode to 'remote clock'
+        // Navigate to telemetry
+        // Verify that the plot renders historical data within the correct bounds
+        // Refresh the page
+        // Verify again that the plot renders historical data within the correct bounds
+    });
+});
diff --git a/e2e/tests/plugins/telemetryTable/telemetryTable.e2e.spec.js b/e2e/tests/plugins/telemetryTable/telemetryTable.e2e.spec.js
index cd8c1c7a19..192c116283 100644
--- a/e2e/tests/plugins/telemetryTable/telemetryTable.e2e.spec.js
+++ b/e2e/tests/plugins/telemetryTable/telemetryTable.e2e.spec.js
@@ -24,7 +24,7 @@ const { test } = require('../../../fixtures');
 const { expect } = require('@playwright/test');
 
 test.describe('Telemetry Table', () => {
-    test('unpauses when paused by button and user changes bounds', async ({ page }) => {
+    test('unpauses and filters data when paused by button and user changes bounds', async ({ page }) => {
         test.info().annotations.push({
             type: 'issue',
             description: 'https://github.com/nasa/openmct/issues/5113'
@@ -71,25 +71,34 @@ test.describe('Telemetry Table', () => {
         ]);
 
         // Click pause button
-        const pauseButton = await page.locator('button.c-button.icon-pause');
+        const pauseButton = page.locator('button.c-button.icon-pause');
         await pauseButton.click();
 
-        const tableWrapper = await page.locator('div.c-table-wrapper');
+        const tableWrapper = page.locator('div.c-table-wrapper');
         await expect(tableWrapper).toHaveClass(/is-paused/);
 
-        // Arbitrarily change end date to some time in the future
+        // Subtract 5 minutes from the current end bound datetime and set it
         const endTimeInput = page.locator('input[type="text"].c-input--datetime').nth(1);
         await endTimeInput.click();
 
         let endDate = await endTimeInput.inputValue();
         endDate = new Date(endDate);
-        endDate.setUTCDate(endDate.getUTCDate() + 1);
-        endDate = endDate.toISOString().replace(/T.*/, '');
+
+        endDate.setUTCMinutes(endDate.getUTCMinutes() - 5);
+        endDate = endDate.toISOString().replace(/T/, ' ');
 
         await endTimeInput.fill('');
         await endTimeInput.fill(endDate);
         await page.keyboard.press('Enter');
 
         await expect(tableWrapper).not.toHaveClass(/is-paused/);
+
+        // Get the most recent telemetry date
+        const latestTelemetryDate = await page.locator('table.c-telemetry-table__body > tbody > tr').last().locator('td').nth(1).getAttribute('title');
+
+        // Verify that it is <= our new end bound
+        const latestMilliseconds = Date.parse(latestTelemetryDate);
+        const endBoundMilliseconds = Date.parse(endDate);
+        expect(latestMilliseconds).toBeLessThanOrEqual(endBoundMilliseconds);
     });
 });
diff --git a/src/MCT.js b/src/MCT.js
index 8d7f252f31..b7f0e40d5a 100644
--- a/src/MCT.js
+++ b/src/MCT.js
@@ -203,7 +203,7 @@ define([
          * @memberof module:openmct.MCT#
          * @name telemetry
          */
-        this.telemetry = new api.TelemetryAPI(this);
+        this.telemetry = new api.TelemetryAPI.default(this);
 
         /**
          * An interface for creating new indicators and changing them dynamically.
diff --git a/src/api/telemetry/TelemetryAPI.js b/src/api/telemetry/TelemetryAPI.js
index c846ddd1a4..824172d691 100644
--- a/src/api/telemetry/TelemetryAPI.js
+++ b/src/api/telemetry/TelemetryAPI.js
@@ -20,122 +20,18 @@
  * at runtime from the About dialog for additional information.
  *****************************************************************************/
 
-const { TelemetryCollection } = require("./TelemetryCollection");
+import TelemetryCollection from './TelemetryCollection';
+import TelemetryRequestInterceptorRegistry from './TelemetryRequestInterceptor';
+import CustomStringFormatter from '../../plugins/displayLayout/CustomStringFormatter';
+import TelemetryMetadataManager from './TelemetryMetadataManager';
+import TelemetryValueFormatter from './TelemetryValueFormatter';
+import DefaultMetadataProvider from './DefaultMetadataProvider';
+import objectUtils from 'objectUtils';
+import _ from 'lodash';
 
-define([
-    '../../plugins/displayLayout/CustomStringFormatter',
-    './TelemetryMetadataManager',
-    './TelemetryValueFormatter',
-    './DefaultMetadataProvider',
-    'objectUtils',
-    'lodash'
-], function (
-    CustomStringFormatter,
-    TelemetryMetadataManager,
-    TelemetryValueFormatter,
-    DefaultMetadataProvider,
-    objectUtils,
-    _
-) {
-    /**
-     * A LimitEvaluator may be used to detect when telemetry values
-     * have exceeded nominal conditions.
-     *
-     * @interface LimitEvaluator
-     * @memberof module:openmct.TelemetryAPI~
-     */
+export default class TelemetryAPI {
 
-    /**
-     * Check for any limit violations associated with a telemetry datum.
-     * @method evaluate
-     * @param {*} datum the telemetry datum to evaluate
-     * @param {TelemetryProperty} the property to check for limit violations
-     * @memberof module:openmct.TelemetryAPI~LimitEvaluator
-     * @returns {module:openmct.TelemetryAPI~LimitViolation} metadata about
-     *          the limit violation, or undefined if a value is within limits
-     */
-
-    /**
-     * A violation of limits defined for a telemetry property.
-     * @typedef LimitViolation
-     * @memberof {module:openmct.TelemetryAPI~}
-     * @property {string} cssClass the class (or space-separated classes) to
-     *           apply to display elements for values which violate this limit
-     * @property {string} name the human-readable name for the limit violation
-     */
-
-    /**
-     * A TelemetryFormatter converts telemetry values for purposes of
-     * display as text.
-     *
-     * @interface TelemetryFormatter
-     * @memberof module:openmct.TelemetryAPI~
-     */
-
-    /**
-     * Retrieve the 'key' from the datum and format it accordingly to
-     * telemetry metadata in domain object.
-     *
-     * @method format
-     * @memberof module:openmct.TelemetryAPI~TelemetryFormatter#
-     */
-
-    /**
-     * Describes a property which would be found in a datum of telemetry
-     * associated with a particular domain object.
-     *
-     * @typedef TelemetryProperty
-     * @memberof module:openmct.TelemetryAPI~
-     * @property {string} key the name of the property in the datum which
-     *           contains this telemetry value
-     * @property {string} name the human-readable name for this property
-     * @property {string} [units] the units associated with this property
-     * @property {boolean} [temporal] true if this property is a timestamp, or
-     *           may be otherwise used to order telemetry in a time-like
-     *           fashion; default is false
-     * @property {boolean} [numeric] true if the values for this property
-     *           can be interpreted plainly as numbers; default is true
-     * @property {boolean} [enumerated] true if this property may have only
-     *           certain specific values; default is false
-     * @property {string} [values] for enumerated states, an ordered list
-     *           of possible values
-     */
-
-    /**
-     * Describes and bounds requests for telemetry data.
-     *
-     * @typedef TelemetryRequest
-     * @memberof module:openmct.TelemetryAPI~
-     * @property {string} sort the key of the property to sort by. This may
-     *           be prefixed with a "+" or a "-" sign to sort in ascending
-     *           or descending order respectively. If no prefix is present,
-     *           ascending order will be used.
-     * @property {*} start the lower bound for values of the sorting property
-     * @property {*} end the upper bound for values of the sorting property
-     * @property {string[]} strategies symbolic identifiers for strategies
-     *           (such as `minmax`) which may be recognized by providers;
-     *           these will be tried in order until an appropriate provider
-     *           is found
-     */
-
-    /**
-     * Provides telemetry data. To connect to new data sources, new
-     * TelemetryProvider implementations should be
-     * [registered]{@link module:openmct.TelemetryAPI#addProvider}.
-     *
-     * @interface TelemetryProvider
-     * @memberof module:openmct.TelemetryAPI~
-     */
-
-    /**
-     * An interface for retrieving telemetry data associated with a domain
-     * object.
-     *
-     * @interface TelemetryAPI
-     * @augments module:openmct.TelemetryAPI~TelemetryProvider
-     * @memberof module:openmct
-     */
-    function TelemetryAPI(openmct) {
+    constructor(openmct) {
         this.openmct = openmct;
 
         this.formatMapCache = new WeakMap();
@@ -148,12 +44,14 @@ define([
         this.requestProviders = [];
         this.subscriptionProviders = [];
         this.valueFormatterCache = new WeakMap();
+
+        this.requestInterceptorRegistry = new TelemetryRequestInterceptorRegistry();
     }
 
-    TelemetryAPI.prototype.abortAllRequests = function () {
+    abortAllRequests() {
         this.requestAbortControllers.forEach((controller) => controller.abort());
         this.requestAbortControllers.clear();
-    };
+    }
 
     /**
      * Return Custom String Formatter
@@ -162,9 +60,9 @@ define([
      * @param {string} format custom formatter string (eg: %.4f, &lts etc.)
      * @returns {CustomStringFormatter}
      */
-    TelemetryAPI.prototype.customStringFormatter = function (valueMetadata, format) {
+    customStringFormatter(valueMetadata, format) {
         return new CustomStringFormatter.default(this.openmct, valueMetadata, format);
-    };
+    }
 
     /**
      * Return true if the given domainObject is a telemetry object.  A telemetry
@@ -174,9 +72,9 @@ define([
      * @param {module:openmct.DomainObject} domainObject
      * @returns {boolean} true if the object is a telemetry object.
      */
-    TelemetryAPI.prototype.isTelemetryObject = function (domainObject) {
+    isTelemetryObject(domainObject) {
         return Boolean(this.findMetadataProvider(domainObject));
-    };
+    }
 
     /**
      * Check if this provider can supply telemetry data associated with
@@ -188,10 +86,10 @@ define([
      * @returns {boolean} true if telemetry can be provided
      * @memberof module:openmct.TelemetryAPI~TelemetryProvider#
      */
-    TelemetryAPI.prototype.canProvideTelemetry = function (domainObject) {
+    canProvideTelemetry(domainObject) {
         return Boolean(this.findSubscriptionProvider(domainObject))
-               || Boolean(this.findRequestProvider(domainObject));
-    };
+                || Boolean(this.findRequestProvider(domainObject));
+    }
 
     /**
      * Register a telemetry provider with the telemetry service. This
@@ -201,7 +99,7 @@ define([
      * @param {module:openmct.TelemetryAPI~TelemetryProvider} provider the new
      *        telemetry provider
      */
-    TelemetryAPI.prototype.addProvider = function (provider) {
+    addProvider(provider) {
         if (provider.supportsRequest) {
             this.requestProviders.unshift(provider);
         }
@@ -217,54 +115,54 @@ define([
         if (provider.supportsLimits) {
             this.limitProviders.unshift(provider);
         }
-    };
+    }
 
     /**
      * @private
      */
-    TelemetryAPI.prototype.findSubscriptionProvider = function () {
+    findSubscriptionProvider() {
         const args = Array.prototype.slice.apply(arguments);
         function supportsDomainObject(provider) {
             return provider.supportsSubscribe.apply(provider, args);
         }
 
         return this.subscriptionProviders.filter(supportsDomainObject)[0];
-    };
+    }
 
     /**
      * @private
      */
-    TelemetryAPI.prototype.findRequestProvider = function (domainObject) {
+    findRequestProvider(domainObject) {
         const args = Array.prototype.slice.apply(arguments);
         function supportsDomainObject(provider) {
             return provider.supportsRequest.apply(provider, args);
         }
 
         return this.requestProviders.filter(supportsDomainObject)[0];
-    };
+    }
 
     /**
      * @private
      */
-    TelemetryAPI.prototype.findMetadataProvider = function (domainObject) {
+    findMetadataProvider(domainObject) {
         return this.metadataProviders.filter(function (p) {
             return p.supportsMetadata(domainObject);
         })[0];
-    };
+    }
 
     /**
      * @private
      */
-    TelemetryAPI.prototype.findLimitEvaluator = function (domainObject) {
+    findLimitEvaluator(domainObject) {
         return this.limitProviders.filter(function (p) {
             return p.supportsLimits(domainObject);
         })[0];
-    };
+    }
 
     /**
      * @private
      */
-    TelemetryAPI.prototype.standardizeRequestOptions = function (options) {
+    standardizeRequestOptions(options) {
         if (!Object.prototype.hasOwnProperty.call(options, 'start')) {
             options.start = this.openmct.time.bounds().start;
         }
@@ -276,7 +174,47 @@ define([
         if (!Object.prototype.hasOwnProperty.call(options, 'domain')) {
             options.domain = this.openmct.time.timeSystem().key;
         }
-    };
+    }
+
+    /**
+     * Register a request interceptor that transforms a request via module:openmct.TelemetryAPI.request
+     * The request will be modifyed when it is received and will be returned in it's modified state
+     * The request will be transformed only if the interceptor is applicable to that domain object as defined by the RequestInterceptorDef
+     *
+     * @param {module:openmct.RequestInterceptorDef} requestInterceptorDef the request interceptor definition to add
+     * @method addRequestInterceptor
+     * @memberof module:openmct.TelemetryRequestInterceptorRegistry#
+     */
+    addRequestInterceptor(requestInterceptorDef) {
+        this.requestInterceptorRegistry.addInterceptor(requestInterceptorDef);
+    }
+
+    /**
+     * Retrieve the request interceptors for a given domain object.
+     * @private
+     */
+    #getInterceptorsForRequest(identifier, request) {
+        return this.requestInterceptorRegistry.getInterceptors(identifier, request);
+    }
+
+    /**
+     * Invoke interceptors if applicable for a given domain object.
+     */
+    async applyRequestInterceptors(domainObject, request) {
+        const interceptors = this.#getInterceptorsForRequest(domainObject.identifier, request);
+
+        if (interceptors.length === 0) {
+            return request;
+        }
+
+        let modifiedRequest = { ...request };
+
+        for (let interceptor of interceptors) {
+            modifiedRequest = await interceptor.invoke(modifiedRequest);
+        }
+
+        return modifiedRequest;
+    }
 
     /**
      * Request telemetry collection for a domain object.
@@ -292,13 +230,13 @@ define([
      *        options for this telemetry collection request
      * @returns {TelemetryCollection} a TelemetryCollection instance
      */
-    TelemetryAPI.prototype.requestCollection = function (domainObject, options = {}) {
+    requestCollection(domainObject, options = {}) {
         return new TelemetryCollection(
             this.openmct,
             domainObject,
             options
         );
-    };
+    }
 
     /**
      * Request historical telemetry for a domain object.
@@ -315,7 +253,7 @@ define([
      * @returns {Promise.<object[]>} a promise for an array of
      *          telemetry data
      */
-    TelemetryAPI.prototype.request = function (domainObject) {
+    async request(domainObject) {
         if (this.noRequestProviderForAllObjects) {
             return Promise.resolve([]);
         }
@@ -330,6 +268,7 @@ define([
         this.requestAbortControllers.add(abortController);
 
         this.standardizeRequestOptions(arguments[1]);
+
         const provider = this.findRequestProvider.apply(this, arguments);
         if (!provider) {
             this.requestAbortControllers.delete(abortController);
@@ -337,6 +276,8 @@ define([
             return this.handleMissingRequestProvider(domainObject);
         }
 
+        arguments[1] = await this.applyRequestInterceptors(domainObject, arguments[1]);
+
         return provider.request.apply(provider, arguments)
             .catch((rejected) => {
                 if (rejected.name !== 'AbortError') {
@@ -348,7 +289,7 @@ define([
             }).finally(() => {
                 this.requestAbortControllers.delete(abortController);
             });
-    };
+    }
 
     /**
      * Subscribe to realtime telemetry for a specific domain object.
@@ -364,7 +305,7 @@ define([
      * @returns {Function} a function which may be called to terminate
      *          the subscription
      */
-    TelemetryAPI.prototype.subscribe = function (domainObject, callback, options) {
+    subscribe(domainObject, callback, options) {
         const provider = this.findSubscriptionProvider(domainObject);
 
         if (!this.subscribeCache) {
@@ -401,7 +342,7 @@ define([
                 delete this.subscribeCache[keyString];
             }
         }.bind(this);
-    };
+    }
 
     /**
      * Get telemetry metadata for a given domain object.  Returns a telemetry
@@ -410,7 +351,7 @@ define([
      *
      * @returns {TelemetryMetadataManager}
      */
-    TelemetryAPI.prototype.getMetadata = function (domainObject) {
+    getMetadata(domainObject) {
         if (!this.metadataCache.has(domainObject)) {
             const metadataProvider = this.findMetadataProvider(domainObject);
             if (!metadataProvider) {
@@ -426,14 +367,14 @@ define([
         }
 
         return this.metadataCache.get(domainObject);
-    };
+    }
 
     /**
      * Return an array of valueMetadatas that are common to all supplied
      * telemetry objects and match the requested hints.
      *
      */
-    TelemetryAPI.prototype.commonValuesForHints = function (metadatas, hints) {
+    commonValuesForHints(metadatas, hints) {
         const options = metadatas.map(function (metadata) {
             const values = metadata.valuesForHints(hints);
 
@@ -453,14 +394,14 @@ define([
         });
 
         return _.sortBy(options, sortKeys);
-    };
+    }
 
     /**
      * Get a value formatter for a given valueMetadata.
      *
      * @returns {TelemetryValueFormatter}
      */
-    TelemetryAPI.prototype.getValueFormatter = function (valueMetadata) {
+    getValueFormatter(valueMetadata) {
         if (!this.valueFormatterCache.has(valueMetadata)) {
             this.valueFormatterCache.set(
                 valueMetadata,
@@ -469,7 +410,7 @@ define([
         }
 
         return this.valueFormatterCache.get(valueMetadata);
-    };
+    }
 
     /**
      * Get a value formatter for a given key.
@@ -477,9 +418,9 @@ define([
      *
      * @returns {Format}
      */
-    TelemetryAPI.prototype.getFormatter = function (key) {
+    getFormatter(key) {
         return this.formatters.get(key);
-    };
+    }
 
     /**
      * Get a format map of all value formatters for a given piece of telemetry
@@ -487,7 +428,7 @@ define([
      *
      * @returns {Object<String, {TelemetryValueFormatter}>}
      */
-    TelemetryAPI.prototype.getFormatMap = function (metadata) {
+    getFormatMap(metadata) {
         if (!metadata) {
             return {};
         }
@@ -502,14 +443,14 @@ define([
         }
 
         return this.formatMapCache.get(metadata);
-    };
+    }
 
     /**
      * Error Handling: Missing Request provider
      *
      * @returns Promise
      */
-    TelemetryAPI.prototype.handleMissingRequestProvider = function (domainObject) {
+    handleMissingRequestProvider(domainObject) {
         this.noRequestProviderForAllObjects = this.requestProviders.every(requestProvider => {
             const supportsRequest = requestProvider.supportsRequest.apply(requestProvider, arguments);
             const hasRequestProvider = Object.prototype.hasOwnProperty.call(requestProvider, 'request') && typeof requestProvider.request === 'function';
@@ -532,15 +473,15 @@ define([
         console.warn(detailMessage);
 
         return Promise.resolve([]);
-    };
+    }
 
     /**
      * Register a new telemetry data formatter.
      * @param {Format} format the
      */
-    TelemetryAPI.prototype.addFormat = function (format) {
+    addFormat(format) {
         this.formatters.set(format.key, format);
-    };
+    }
 
     /**
      * Get a limit evaluator for this domain object.
@@ -558,9 +499,9 @@ define([
      * @method limitEvaluator
      * @memberof module:openmct.TelemetryAPI~TelemetryProvider#
      */
-    TelemetryAPI.prototype.limitEvaluator = function (domainObject) {
+    limitEvaluator(domainObject) {
         return this.getLimitEvaluator(domainObject);
-    };
+    }
 
     /**
      * Get a limits for this domain object.
@@ -578,9 +519,9 @@ define([
      * @method limits
      * @memberof module:openmct.TelemetryAPI~TelemetryProvider#
      */
-    TelemetryAPI.prototype.limitDefinition = function (domainObject) {
+    limitDefinition(domainObject) {
         return this.getLimits(domainObject);
-    };
+    }
 
     /**
      * Get a limit evaluator for this domain object.
@@ -598,7 +539,7 @@ define([
      * @method limitEvaluator
      * @memberof module:openmct.TelemetryAPI~TelemetryProvider#
      */
-    TelemetryAPI.prototype.getLimitEvaluator = function (domainObject) {
+    getLimitEvaluator(domainObject) {
         const provider = this.findLimitEvaluator(domainObject);
         if (!provider) {
             return {
@@ -607,7 +548,7 @@ define([
         }
 
         return provider.getLimitEvaluator(domainObject);
-    };
+    }
 
     /**
      * Get a limit definitions for this domain object.
@@ -636,7 +577,7 @@ define([
      *  supported colors are purple, red, orange, yellow and cyan
      * @memberof module:openmct.TelemetryAPI~TelemetryProvider#
      */
-    TelemetryAPI.prototype.getLimits = function (domainObject) {
+    getLimits(domainObject) {
         const provider = this.findLimitEvaluator(domainObject);
         if (!provider || !provider.getLimits) {
             return {
@@ -647,7 +588,104 @@ define([
         }
 
         return provider.getLimits(domainObject);
-    };
+    }
+}
 
-    return TelemetryAPI;
-});
+/**
+ * A LimitEvaluator may be used to detect when telemetry values
+ * have exceeded nominal conditions.
+ *
+ * @interface LimitEvaluator
+ * @memberof module:openmct.TelemetryAPI~
+ */
+
+/**
+ * Check for any limit violations associated with a telemetry datum.
+ * @method evaluate
+ * @param {*} datum the telemetry datum to evaluate
+ * @param {TelemetryProperty} the property to check for limit violations
+ * @memberof module:openmct.TelemetryAPI~LimitEvaluator
+ * @returns {module:openmct.TelemetryAPI~LimitViolation} metadata about
+ *          the limit violation, or undefined if a value is within limits
+ */
+
+/**
+ * A violation of limits defined for a telemetry property.
+ * @typedef LimitViolation
+ * @memberof {module:openmct.TelemetryAPI~}
+ * @property {string} cssClass the class (or space-separated classes) to
+ *           apply to display elements for values which violate this limit
+ * @property {string} name the human-readable name for the limit violation
+ */
+
+/**
+ * A TelemetryFormatter converts telemetry values for purposes of
+ * display as text.
+ *
+ * @interface TelemetryFormatter
+ * @memberof module:openmct.TelemetryAPI~
+ */
+
+/**
+ * Retrieve the 'key' from the datum and format it accordingly to
+ * telemetry metadata in domain object.
+ *
+ * @method format
+ * @memberof module:openmct.TelemetryAPI~TelemetryFormatter#
+ */
+
+/**
+ * Describes a property which would be found in a datum of telemetry
+ * associated with a particular domain object.
+ *
+ * @typedef TelemetryProperty
+ * @memberof module:openmct.TelemetryAPI~
+ * @property {string} key the name of the property in the datum which
+ *           contains this telemetry value
+ * @property {string} name the human-readable name for this property
+ * @property {string} [units] the units associated with this property
+ * @property {boolean} [temporal] true if this property is a timestamp, or
+ *           may be otherwise used to order telemetry in a time-like
+ *           fashion; default is false
+ * @property {boolean} [numeric] true if the values for this property
+ *           can be interpreted plainly as numbers; default is true
+ * @property {boolean} [enumerated] true if this property may have only
+ *           certain specific values; default is false
+ * @property {string} [values] for enumerated states, an ordered list
+ *           of possible values
+ */
+
+/**
+ * Describes and bounds requests for telemetry data.
+ *
+ * @typedef TelemetryRequest
+ * @memberof module:openmct.TelemetryAPI~
+ * @property {string} sort the key of the property to sort by. This may
+ *           be prefixed with a "+" or a "-" sign to sort in ascending
+ *           or descending order respectively. If no prefix is present,
+ *           ascending order will be used.
+ * @property {*} start the lower bound for values of the sorting property
+ * @property {*} end the upper bound for values of the sorting property
+ * @property {string[]} strategies symbolic identifiers for strategies
+ *           (such as `minmax`) which may be recognized by providers;
+ *           these will be tried in order until an appropriate provider
+ *           is found
+ */
+
+/**
+ * Provides telemetry data. To connect to new data sources, new
+ * TelemetryProvider implementations should be
+ * [registered]{@link module:openmct.TelemetryAPI#addProvider}.
+ *
+ * @interface TelemetryProvider
+ * @memberof module:openmct.TelemetryAPI~
+ */
+
+/**
+ * An interface for retrieving telemetry data associated with a domain
+ * object.
+ *
+ * @interface TelemetryAPI
+ * @augments module:openmct.TelemetryAPI~TelemetryProvider
+ * @memberof module:openmct
+ */
diff --git a/src/api/telemetry/TelemetryAPISpec.js b/src/api/telemetry/TelemetryAPISpec.js
index 0b3c91533f..5af9440098 100644
--- a/src/api/telemetry/TelemetryAPISpec.js
+++ b/src/api/telemetry/TelemetryAPISpec.js
@@ -21,7 +21,7 @@
  *****************************************************************************/
 import { createOpenMct, resetApplicationState } from 'utils/testing';
 import TelemetryAPI from './TelemetryAPI';
-const { TelemetryCollection } = require("./TelemetryCollection");
+import TelemetryCollection from './TelemetryCollection';
 
 describe('Telemetry API', function () {
     let openmct;
diff --git a/src/api/telemetry/TelemetryCollection.js b/src/api/telemetry/TelemetryCollection.js
index 97aa73493a..1ac9fc5872 100644
--- a/src/api/telemetry/TelemetryCollection.js
+++ b/src/api/telemetry/TelemetryCollection.js
@@ -26,7 +26,7 @@ import { LOADED_ERROR, TIMESYSTEM_KEY_NOTIFICATION, TIMESYSTEM_KEY_WARNING } fro
 
 /** Class representing a Telemetry Collection. */
 
-export class TelemetryCollection extends EventEmitter {
+export default class TelemetryCollection extends EventEmitter {
     /**
      * Creates a Telemetry Collection
      *
@@ -127,7 +127,8 @@ export class TelemetryCollection extends EventEmitter {
             this.requestAbort = new AbortController();
             options.signal = this.requestAbort.signal;
             this.emit('requestStarted');
-            historicalData = await historicalProvider.request(this.domainObject, options);
+            const modifiedOptions = await this.openmct.telemetry.applyRequestInterceptors(this.domainObject, options);
+            historicalData = await historicalProvider.request(this.domainObject, modifiedOptions);
         } catch (error) {
             if (error.name !== 'AbortError') {
                 console.error('Error requesting telemetry data...');
diff --git a/src/api/telemetry/TelemetryRequestInterceptor.js b/src/api/telemetry/TelemetryRequestInterceptor.js
new file mode 100644
index 0000000000..7204ee3321
--- /dev/null
+++ b/src/api/telemetry/TelemetryRequestInterceptor.js
@@ -0,0 +1,68 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+export default class TelemetryRequestInterceptorRegistry {
+    /**
+     * A TelemetryRequestInterceptorRegistry maintains the definitions for different interceptors that may be invoked on telemetry
+     * requests.
+     * @interface TelemetryRequestInterceptorRegistry
+     * @memberof module:openmct
+     */
+    constructor() {
+        this.interceptors = [];
+    }
+
+    /**
+     * @interface TelemetryRequestInterceptorDef
+     * @property {function} appliesTo function that determines if this interceptor should be called for the given identifier/request
+     * @property {function} invoke function that transforms the provided request and returns the transformed request
+     * @property {function} priority the priority for this interceptor. A higher number returned has more weight than a lower number
+     * @memberof module:openmct TelemetryRequestInterceptorRegistry#
+     */
+
+    /**
+     * Register a new telemetry request interceptor.
+     *
+     * @param {module:openmct.RequestInterceptorDef} requestInterceptorDef the interceptor to add
+     * @method addInterceptor
+     * @memberof module:openmct.TelemetryRequestInterceptorRegistry#
+     */
+    addInterceptor(interceptorDef) {
+        //TODO: sort by priority
+        this.interceptors.push(interceptorDef);
+    }
+
+    /**
+     * Retrieve all interceptors applicable to a domain object/request.
+     * @method getInterceptors
+     * @returns [module:openmct.RequestInterceptorDef] the registered interceptors for this identifier/request
+     * @memberof module:openmct.TelemetryRequestInterceptorRegistry#
+     */
+    getInterceptors(identifier, request) {
+        return this.interceptors.filter(interceptor => {
+            return typeof interceptor.appliesTo === 'function'
+                && interceptor.appliesTo(identifier, request);
+        });
+    }
+
+}
+
diff --git a/src/plugins/imagery/pluginSpec.js b/src/plugins/imagery/pluginSpec.js
index 0eddb3ce76..e595117a82 100644
--- a/src/plugins/imagery/pluginSpec.js
+++ b/src/plugins/imagery/pluginSpec.js
@@ -373,39 +373,30 @@ describe("The Imagery View Layouts", () => {
             return Vue.nextTick();
         });
 
-        it("on mount should show the the most recent image", () => {
+        it("on mount should show the the most recent image", async () => {
             //Looks like we need Vue.nextTick here so that computed properties settle down
-            return Vue.nextTick(() => {
-                const imageInfo = getImageInfo(parent);
-
-                expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1);
-            });
+            await Vue.nextTick();
+            const imageInfo = getImageInfo(parent);
+            expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1);
         });
 
-        it("on mount should show the any image layers", (done) => {
+        it("on mount should show the any image layers", async () => {
             //Looks like we need Vue.nextTick here so that computed properties settle down
-            Vue.nextTick().then(() => {
-                Vue.nextTick(() => {
-                    const layerEls = parent.querySelectorAll('.js-layer-image');
-                    console.log(layerEls);
-                    expect(layerEls.length).toEqual(1);
-                    done();
-                });
-            });
+            await Vue.nextTick();
+            const layerEls = parent.querySelectorAll('.js-layer-image');
+            console.log(layerEls);
+            expect(layerEls.length).toEqual(1);
         });
 
-        it("should show the clicked thumbnail as the main image", (done) => {
+        it("should show the clicked thumbnail as the main image", async () => {
             //Looks like we need Vue.nextTick here so that computed properties settle down
-            Vue.nextTick(() => {
-                const target = imageTelemetry[5].url;
-                parent.querySelectorAll(`img[src='${target}']`)[0].click();
-                Vue.nextTick(() => {
-                    const imageInfo = getImageInfo(parent);
+            await Vue.nextTick();
+            const target = imageTelemetry[5].url;
+            parent.querySelectorAll(`img[src='${target}']`)[0].click();
+            await Vue.nextTick();
+            const imageInfo = getImageInfo(parent);
 
-                    expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1);
-                    done();
-                });
-            });
+            expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1);
         });
 
         xit("should show that an image is new", (done) => {
@@ -424,71 +415,60 @@ describe("The Imagery View Layouts", () => {
             });
         });
 
-        it("should show that an image is not new", (done) => {
-            Vue.nextTick(() => {
-                const target = imageTelemetry[4].url;
-                parent.querySelectorAll(`img[src='${target}']`)[0].click();
+        it("should show that an image is not new", async () => {
+            await Vue.nextTick();
+            const target = imageTelemetry[4].url;
+            parent.querySelectorAll(`img[src='${target}']`)[0].click();
 
-                Vue.nextTick(() => {
-                    const imageIsNew = isNew(parent);
+            await Vue.nextTick();
+            const imageIsNew = isNew(parent);
 
-                    expect(imageIsNew).toBeFalse();
-                    done();
-                });
-            });
+            expect(imageIsNew).toBeFalse();
         });
 
-        it("should navigate via arrow keys", (done) => {
-            Vue.nextTick(() => {
-                let keyOpts = {
-                    element: parent.querySelector('.c-imagery'),
-                    key: 'ArrowLeft',
-                    keyCode: 37,
-                    type: 'keyup'
-                };
+        it("should navigate via arrow keys", async () => {
+            await Vue.nextTick();
+            const keyOpts = {
+                element: parent.querySelector('.c-imagery'),
+                key: 'ArrowLeft',
+                keyCode: 37,
+                type: 'keyup'
+            };
 
-                simulateKeyEvent(keyOpts);
+            simulateKeyEvent(keyOpts);
 
-                Vue.nextTick(() => {
-                    const imageInfo = getImageInfo(parent);
-
-                    expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1);
-                    done();
-                });
-            });
+            await Vue.nextTick();
+            const imageInfo = getImageInfo(parent);
+            expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1);
         });
 
-        it("should navigate via numerous arrow keys", (done) => {
-            Vue.nextTick(() => {
-                let element = parent.querySelector('.c-imagery');
-                let type = 'keyup';
-                let leftKeyOpts = {
-                    element,
-                    type,
-                    key: 'ArrowLeft',
-                    keyCode: 37
-                };
-                let rightKeyOpts = {
-                    element,
-                    type,
-                    key: 'ArrowRight',
-                    keyCode: 39
-                };
+        it("should navigate via numerous arrow keys", async () => {
+            await Vue.nextTick();
+            const element = parent.querySelector('.c-imagery');
+            const type = 'keyup';
+            const leftKeyOpts = {
+                element,
+                type,
+                key: 'ArrowLeft',
+                keyCode: 37
+            };
+            const rightKeyOpts = {
+                element,
+                type,
+                key: 'ArrowRight',
+                keyCode: 39
+            };
 
-                // left thrice
-                simulateKeyEvent(leftKeyOpts);
-                simulateKeyEvent(leftKeyOpts);
-                simulateKeyEvent(leftKeyOpts);
-                // right once
-                simulateKeyEvent(rightKeyOpts);
+            // left thrice
+            simulateKeyEvent(leftKeyOpts);
+            simulateKeyEvent(leftKeyOpts);
+            simulateKeyEvent(leftKeyOpts);
+            // right once
+            simulateKeyEvent(rightKeyOpts);
 
-                Vue.nextTick(() => {
-                    const imageInfo = getImageInfo(parent);
-
-                    expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1);
-                    done();
-                });
-            });
+            await Vue.nextTick();
+            const imageInfo = getImageInfo(parent);
+            expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1);
         });
         it ('shows an auto scroll button when scroll to left', (done) => {
             Vue.nextTick(() => {
diff --git a/src/plugins/remoteClock/RemoteClock.js b/src/plugins/remoteClock/RemoteClock.js
index 1502d9a9dd..3d6e6fcf4f 100644
--- a/src/plugins/remoteClock/RemoteClock.js
+++ b/src/plugins/remoteClock/RemoteClock.js
@@ -20,6 +20,7 @@
  * at runtime from the About dialog for additional information.
  *****************************************************************************/
 import DefaultClock from '../../utils/clock/DefaultClock';
+import remoteClockRequestInterceptor from './requestInterceptor';
 
 /**
  * A {@link openmct.TimeAPI.Clock} that updates the temporal bounds of the
@@ -49,6 +50,14 @@ export default class RemoteClock extends DefaultClock {
 
         this.lastTick = 0;
 
+        this.openmct.telemetry.addRequestInterceptor(
+            remoteClockRequestInterceptor(
+                this.openmct,
+                this.identifier,
+                this.#waitForReady.bind(this)
+            )
+        );
+
         this._processDatum = this._processDatum.bind(this);
     }
 
@@ -129,4 +138,25 @@ export default class RemoteClock extends DefaultClock {
             return timeFormatter.parse(datum);
         };
     }
+
+    /**
+     * Waits for the clock to have a non-default tick value.
+     *
+     * @private
+     */
+    #waitForReady() {
+        const waitForInitialTick = (resolve) => {
+            if (this.lastTick > 0) {
+                const offsets = this.openmct.time.clockOffsets();
+                resolve({
+                    start: this.lastTick + offsets.start,
+                    end: this.lastTick + offsets.end
+                });
+            } else {
+                setTimeout(() => waitForInitialTick(resolve), 100);
+            }
+        };
+
+        return new Promise(waitForInitialTick);
+    }
 }
diff --git a/src/plugins/remoteClock/requestInterceptor.js b/src/plugins/remoteClock/requestInterceptor.js
new file mode 100644
index 0000000000..bb38031bdd
--- /dev/null
+++ b/src/plugins/remoteClock/requestInterceptor.js
@@ -0,0 +1,46 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+function remoteClockRequestInterceptor(openmct, remoteClockIdentifier, waitForBounds) {
+    let remoteClockLoaded = false;
+
+    return {
+        appliesTo: () => {
+            // Get the activeClock from the Global Time Context
+            const { activeClock } = openmct.time.getContextForView();
+
+            return activeClock !== undefined
+            && activeClock.key === 'remote-clock'
+            && !remoteClockLoaded;
+        },
+        invoke: async (request) => {
+            const { start, end } = await waitForBounds();
+            remoteClockLoaded = true;
+            request[1].start = start;
+            request[1].end = end;
+
+            return request;
+        }
+    };
+}
+
+export default remoteClockRequestInterceptor;
diff --git a/src/plugins/telemetryTable/pluginSpec.js b/src/plugins/telemetryTable/pluginSpec.js
index 9d8892f3c2..19cdd7e9d6 100644
--- a/src/plugins/telemetryTable/pluginSpec.js
+++ b/src/plugins/telemetryTable/pluginSpec.js
@@ -135,7 +135,7 @@ describe("the plugin", () => {
         let tableInstance;
         let mockClock;
 
-        beforeEach(() => {
+        beforeEach(async () => {
             openmct.time.timeSystem('utc', {
                 start: 0,
                 end: 4
@@ -210,16 +210,8 @@ describe("the plugin", () => {
                     'some-other-key': 'some-other-value 3'
                 }
             ];
-            let telemetryPromiseResolve;
-            let telemetryPromise = new Promise((resolve) => {
-                telemetryPromiseResolve = resolve;
-            });
 
-            historicalProvider.request = () => {
-                telemetryPromiseResolve(testTelemetry);
-
-                return telemetryPromise;
-            };
+            historicalProvider.request = () => Promise.resolve(testTelemetry);
 
             openmct.router.path = [testTelemetryObject];
 
@@ -230,7 +222,7 @@ describe("the plugin", () => {
 
             tableInstance = tableView.getTable();
 
-            return telemetryPromise.then(() => Vue.nextTick());
+            await Vue.nextTick();
         });
 
         afterEach(() => {
@@ -255,13 +247,10 @@ describe("the plugin", () => {
 
         });
 
-        it("Renders a row for every telemetry datum returned", (done) => {
+        it("Renders a row for every telemetry datum returned", async () => {
             let rows = element.querySelectorAll('table.c-telemetry-table__body tr');
-            Vue.nextTick(() => {
-                expect(rows.length).toBe(3);
-
-                done();
-            });
+            await Vue.nextTick();
+            expect(rows.length).toBe(3);
         });
 
         it("Renders a column for every item in telemetry metadata", () => {
@@ -273,7 +262,7 @@ describe("the plugin", () => {
             expect(headers[3].innerText).toBe('Another attribute');
         });
 
-        it("Supports column reordering via drag and drop", () => {
+        it("Supports column reordering via drag and drop", async () => {
             let columns = element.querySelectorAll('tr.c-telemetry-table__headers__labels th');
             let fromColumn = columns[0];
             let toColumn = columns[1];
@@ -292,54 +281,43 @@ describe("the plugin", () => {
             toColumn.dispatchEvent(dragOverEvent);
             toColumn.dispatchEvent(dropEvent);
 
-            return Vue.nextTick().then(() => {
-                columns = element.querySelectorAll('tr.c-telemetry-table__headers__labels th');
-                let firstColumn = columns[0];
-                let secondColumn = columns[1];
-                let firstColumnText = firstColumn.querySelector('span.c-telemetry-table__headers__label').innerText;
-                let secondColumnText = secondColumn.querySelector('span.c-telemetry-table__headers__label').innerText;
-
-                expect(fromColumnText).not.toEqual(firstColumnText);
-                expect(fromColumnText).toEqual(secondColumnText);
-                expect(toColumnText).not.toEqual(secondColumnText);
-                expect(toColumnText).toEqual(firstColumnText);
-            });
+            await Vue.nextTick();
+            columns = element.querySelectorAll('tr.c-telemetry-table__headers__labels th');
+            let firstColumn = columns[0];
+            let secondColumn = columns[1];
+            let firstColumnText = firstColumn.querySelector('span.c-telemetry-table__headers__label').innerText;
+            let secondColumnText = secondColumn.querySelector('span.c-telemetry-table__headers__label').innerText;
+            expect(fromColumnText).not.toEqual(firstColumnText);
+            expect(fromColumnText).toEqual(secondColumnText);
+            expect(toColumnText).not.toEqual(secondColumnText);
+            expect(toColumnText).toEqual(firstColumnText);
         });
 
-        it("Supports filtering telemetry by regular text search", () => {
+        it("Supports filtering telemetry by regular text search", async () => {
             tableInstance.tableRows.setColumnFilter("some-key", "1");
+            await Vue.nextTick();
+            let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
 
-            return Vue.nextTick().then(() => {
-                let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
+            expect(filteredRowElements.length).toEqual(1);
+            tableInstance.tableRows.setColumnFilter("some-key", "");
+            await Vue.nextTick();
 
-                expect(filteredRowElements.length).toEqual(1);
-
-                tableInstance.tableRows.setColumnFilter("some-key", "");
-
-                return Vue.nextTick().then(() => {
-                    let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
-
-                    expect(allRowElements.length).toEqual(3);
-                });
-            });
+            let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
+            expect(allRowElements.length).toEqual(3);
         });
 
-        it("Supports filtering using Regex", () => {
+        it("Supports filtering using Regex", async () => {
             tableInstance.tableRows.setColumnRegexFilter("some-key", "^some-value$");
+            await Vue.nextTick();
+            let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
 
-            return Vue.nextTick().then(() => {
-                let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
+            expect(filteredRowElements.length).toEqual(0);
 
-                expect(filteredRowElements.length).toEqual(0);
+            tableInstance.tableRows.setColumnRegexFilter("some-key", "^some-value");
+            await Vue.nextTick();
+            let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
 
-                tableInstance.tableRows.setColumnRegexFilter("some-key", "^some-value");
-
-                return Vue.nextTick().then(() => {
-                    let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
-
-                    expect(allRowElements.length).toEqual(3);
-                });
-            });
+            expect(allRowElements.length).toEqual(3);
         });
 
         it("displays the correct number of column headers when the configuration is mutated", async () => {
@@ -402,7 +380,7 @@ describe("the plugin", () => {
             expect(element.querySelector('div.c-table.is-paused')).not.toBeNull();
 
             const currentBounds = openmct.time.bounds();
-
+            await Vue.nextTick();
             const newBounds = {
                 start: currentBounds.start,
                 end: currentBounds.end - 3
@@ -410,17 +388,10 @@ describe("the plugin", () => {
 
             // Manually change the time bounds
             openmct.time.bounds(newBounds);
-
             await Vue.nextTick();
 
             // Verify table is no longer paused
             expect(element.querySelector('div.c-table.is-paused')).toBeNull();
-
-            await Vue.nextTick();
-
-            // Verify table displays the correct number of rows within the new bounds
-            const tableRows = element.querySelectorAll('table.c-telemetry-table__body > tbody > tr');
-            expect(tableRows.length).toEqual(2);
         });
 
         it("Unpauses the table on user bounds change if paused by button", async () => {
@@ -428,19 +399,18 @@ describe("the plugin", () => {
 
             // Pause by button
             viewContext.togglePauseByButton();
-
             await Vue.nextTick();
 
             // Verify table is paused
             expect(element.querySelector('div.c-table.is-paused')).not.toBeNull();
 
             const currentBounds = openmct.time.bounds();
+            await Vue.nextTick();
 
             const newBounds = {
                 start: currentBounds.start,
-                end: currentBounds.end - 3
+                end: currentBounds.end - 1
             };
-
             // Manually change the time bounds
             openmct.time.bounds(newBounds);
 
@@ -448,12 +418,6 @@ describe("the plugin", () => {
 
             // Verify table is no longer paused
             expect(element.querySelector('div.c-table.is-paused')).toBeNull();
-
-            await Vue.nextTick();
-
-            // Verify table displays the correct number of rows within the new bounds
-            const tableRows = element.querySelectorAll('table.c-telemetry-table__body > tbody > tr');
-            expect(tableRows.length).toEqual(2);
         });
 
         it("Does not unpause the table on tick", async () => {
diff --git a/webpack.common.js b/webpack.common.js
index 1c938af845..95a76fd404 100644
--- a/webpack.common.js
+++ b/webpack.common.js
@@ -7,12 +7,19 @@ const webpack = require('webpack');
 const MiniCssExtractPlugin = require("mini-css-extract-plugin");
 
 const {VueLoaderPlugin} = require('vue-loader');
-const gitRevision = require('child_process')
-    .execSync('git rev-parse HEAD')
-    .toString().trim();
-const gitBranch = require('child_process')
-    .execSync('git rev-parse --abbrev-ref HEAD')
-    .toString().trim();
+let gitRevision = 'error-retrieving-revision';
+let gitBranch = 'error-retrieving-branch';
+
+try {
+    gitRevision = require('child_process')
+        .execSync('git rev-parse HEAD')
+        .toString().trim();
+    gitBranch = require('child_process')
+        .execSync('git rev-parse --abbrev-ref HEAD')
+        .toString().trim();
+} catch (err) {
+    console.warn(err);
+}
 
 /** @type {import('webpack').Configuration} */
 const config = {