Unify action container environments

Add npm openwhisk package to NodeJS images, and deprecation warning for uses of the 'whisk' context object.

Make OpenWhisk related environment variables available in all container runtimes.
diff --git a/core/javaAction/proxy/src/main/java/openwhisk/java/action/JarLoader.java b/core/javaAction/proxy/src/main/java/openwhisk/java/action/JarLoader.java
index b221416..3b30244 100644
--- a/core/javaAction/proxy/src/main/java/openwhisk/java/action/JarLoader.java
+++ b/core/javaAction/proxy/src/main/java/openwhisk/java/action/JarLoader.java
@@ -17,6 +17,7 @@
 
 import java.io.File;
 import java.io.InputStream;
+import java.lang.reflect.Field;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
 import java.net.MalformedURLException;
@@ -26,6 +27,8 @@
 import java.nio.file.Path;
 import java.nio.file.StandardCopyOption;
 import java.util.Base64;
+import java.util.Collections;
+import java.util.Map;
 
 import com.google.gson.JsonObject;
 
@@ -63,7 +66,23 @@
         this.mainMethod = m;
     }
 
-    public JsonObject invokeMain(JsonObject arg) throws Exception {
+    public JsonObject invokeMain(JsonObject arg, Map<String, String> env) throws Exception {
+        augmentEnv(env);
         return (JsonObject) mainMethod.invoke(null, arg);
     }
+
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    private static void augmentEnv(Map<String, String> newEnv) {
+        try {
+            for (Class cl : Collections.class.getDeclaredClasses()) {
+                if ("java.util.Collections$UnmodifiableMap".equals(cl.getName())) {
+                    Field field = cl.getDeclaredField("m");
+                    field.setAccessible(true);
+                    Object obj = field.get(System.getenv());
+                    Map<String, String> map = (Map<String, String>) obj;
+                    map.putAll(newEnv);
+                }
+            }
+        } catch (Exception e) {}
+    }
 }
diff --git a/core/javaAction/proxy/src/main/java/openwhisk/java/action/Proxy.java b/core/javaAction/proxy/src/main/java/openwhisk/java/action/Proxy.java
index 0028305..4927b76 100644
--- a/core/javaAction/proxy/src/main/java/openwhisk/java/action/Proxy.java
+++ b/core/javaAction/proxy/src/main/java/openwhisk/java/action/Proxy.java
@@ -25,6 +25,7 @@
 import java.net.InetSocketAddress;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Path;
+import java.util.HashMap;
 
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
@@ -120,11 +121,19 @@
                 JsonElement ie = parser.parse(new BufferedReader(new InputStreamReader(is, "UTF-8")));
                 JsonObject inputObject = ie.getAsJsonObject().getAsJsonObject("value");
 
+                HashMap<String, String> env = new HashMap<String, String>();
+                for (String p : new String[] { "api_key", "namespace", "action_name", "activation_id", "deadline" }) {
+                    try {
+                        String val = ie.getAsJsonObject().getAsJsonPrimitive(p).getAsString();
+                        env.put(String.format("__OW_%s", p.toUpperCase()), val);
+                    } catch (Exception e) {}
+                }
+
                 Thread.currentThread().setContextClassLoader(loader);
                 System.setSecurityManager(new WhiskSecurityManager());
 
                 // User code starts running here.
-                JsonObject output = loader.invokeMain(inputObject);
+                JsonObject output = loader.invokeMain(inputObject, env);
                 // User code finished running here.
 
                 if(output == null) {
diff --git a/core/nodejsAction/Dockerfile b/core/nodejsAction/Dockerfile
index 6af9760..1abd89e 100644
--- a/core/nodejsAction/Dockerfile
+++ b/core/nodejsAction/Dockerfile
@@ -34,6 +34,7 @@
 nano@5.10.0 \
 node-uuid@1.4.2 \
 oauth2-server@2.4.0 \
+openwhisk@2.6.0 \
 process@0.11.0 \
 request@2.79.0 \
 rimraf@2.5.1 \
diff --git a/tests/src/actionContainers/ActionProxyContainerTests.scala b/tests/src/actionContainers/ActionProxyContainerTests.scala
index 110b77b..8584b85 100644
--- a/tests/src/actionContainers/ActionProxyContainerTests.scala
+++ b/tests/src/actionContainers/ActionProxyContainerTests.scala
@@ -26,12 +26,8 @@
 import ActionContainer.withContainer
 import common.TestUtils
 import common.WskActorSystem
-import spray.json.JsArray
-import spray.json.JsNull
-import spray.json.JsNumber
-import spray.json.JsObject
-import spray.json.JsString
-import spray.json.JsBoolean
+import spray.json.DefaultJsonProtocol._
+import spray.json._
 
 @RunWith(classOf[JUnitRunner])
 class ActionProxyContainerTests extends BasicActionRunnerTests with WskActorSystem {
@@ -81,20 +77,31 @@
     val stdEnvSamples = {
         val bash = """
                 |#!/bin/bash
-                |echo "{ \"auth\": \"$AUTH_KEY\", \"edge\": \"$EDGE_HOST\" }"
+                |echo "{ \
+                |\"api_host\": \"$__OW_API_HOST\", \"api_key\": \"$__OW_API_KEY\", \
+                |\"namespace\": \"$__OW_NAMESPACE\", \"action_name\": \"$__OW_ACTION_NAME\", \
+                |\"activation_id\": \"$__OW_ACTIVATION_ID\", \"deadline\": \"$__OW_DEADLINE\" }"
             """.stripMargin.trim
 
         val python = """
                 |#!/usr/bin/env python
                 |import os
-                |print '{ "auth": "%s", "edge": "%s" }' % (os.environ['AUTH_KEY'], os.environ['EDGE_HOST'])
+                |
+                |print '{ "api_host": "%s", "api_key": "%s", "namespace": "%s", "action_name" : "%s", "activation_id": "%s", "deadline": "%s" }' % (
+                |  os.environ['__OW_API_HOST'], os.environ['__OW_API_KEY'],
+                |  os.environ['__OW_NAMESPACE'], os.environ['__OW_ACTION_NAME'],
+                |  os.environ['__OW_ACTIVATION_ID'], os.environ['__OW_DEADLINE'])
             """.stripMargin.trim
 
         val perl = """
                 |#!/usr/bin/env perl
-                |$a = $ENV{'AUTH_KEY'};
-                |$e = $ENV{'EDGE_HOST'};
-                |print "{ \"auth\": \"$a\", \"edge\": \"$e\" }";
+                |$a = $ENV{'__OW_API_HOST'};
+                |$b = $ENV{'__OW_API_KEY'};
+                |$c = $ENV{'__OW_NAMESPACE'};
+                |$d = $ENV{'__OW_ACTION_NAME'};
+                |$e = $ENV{'__OW_ACTIVATION_ID'};
+                |$f = $ENV{'__OW_DEADLINE'};
+                |print "{ \"api_host\": \"$a\", \"api_key\": \"$b\", \"namespace\": \"$c\", \"action_name\": \"$d\", \"activation_id\": \"$e\", \"deadline\": \"$f\" }";
             """.stripMargin.trim
 
         // excluding perl as it not installed in alpine based image
@@ -244,28 +251,37 @@
     }
 
     /** Runs tests for code samples which are expected to return the expected standard environment {auth, edge}. */
-    def testEnv(stdEnvSamples: Seq[(String, String)], enforceEmptyOutputStream: Boolean = true) = {
+    def testEnv(stdEnvSamples: Seq[(String, String)], enforceEmptyOutputStream: Boolean = true, enforceEmptyErrorStream: Boolean = true) = {
         for (s <- stdEnvSamples) {
             it should s"run a ${s._1} script and confirm expected environment variables" in {
-                val auth = JsString("abc")
-                val edge = "xyz"
-                val env = Map("EDGE_HOST" -> edge)
+                val props = Seq(
+                    "api_host" -> "xyz",
+                    "api_key" -> "abc",
+                    "namespace" -> "zzz",
+                    "action_name" -> "xxx",
+                    "activation_id" -> "iii",
+                    "deadline" -> "123")
+                val env = props.map { case (k, v) => s"__OW_${k.toUpperCase()}" -> v }
 
-                val (out, err) = withActionContainer(env) { c =>
+                val (out, err) = withActionContainer(env.take(1).toMap) { c =>
                     val (initCode, _) = c.init(initPayload(s._2))
                     initCode should be(200)
 
-                    val (runCode, out) = c.run(runPayload(JsObject(), Some(JsObject("authKey" -> auth))))
+                    val (runCode, out) = c.run(runPayload(JsObject(), Some(props.toMap.toJson.asJsObject)))
                     runCode should be(200)
                     out shouldBe defined
-                    out.get.fields("auth") shouldBe auth
-                    out.get.fields("edge") shouldBe JsString(edge)
+                    props.map {
+                        case (k, v) => withClue(k) {
+                            out.get.fields(k) shouldBe JsString(v)
+                        }
+
+                    }
                 }
 
                 checkStreams(out, err, {
                     case (o, e) =>
                         if (enforceEmptyOutputStream) o shouldBe empty
-                        e shouldBe empty
+                        if (enforceEmptyErrorStream) e shouldBe empty
                 })
             }
         }
diff --git a/tests/src/actionContainers/JavaActionContainerTests.scala b/tests/src/actionContainers/JavaActionContainerTests.scala
index 77bfff7..876f0b4 100644
--- a/tests/src/actionContainers/JavaActionContainerTests.scala
+++ b/tests/src/actionContainers/JavaActionContainerTests.scala
@@ -20,6 +20,7 @@
 import org.scalatest.FlatSpec
 import org.scalatest.Matchers
 import org.scalatest.junit.JUnitRunner
+import spray.json.DefaultJsonProtocol._
 import spray.json._
 
 import ActionContainer.withContainer
@@ -28,19 +29,62 @@
 import common.WskActorSystem
 
 @RunWith(classOf[JUnitRunner])
-class JavaActionContainerTests extends FlatSpec with Matchers with WskActorSystem {
+class JavaActionContainerTests extends FlatSpec with Matchers with WskActorSystem with ActionProxyContainerTestUtils {
 
     // Helpers specific to javaaction
-    def withJavaContainer(code: ActionContainer => Unit) = withContainer("javaaction")(code)
+    def withJavaContainer(code: ActionContainer => Unit, env: Map[String, String] = Map.empty) = withContainer("javaaction", env)(code)
     def initPayload(mainClass: String, jar64: String) = JsObject(
         "value" -> JsObject(
             "name" -> JsString("dummyAction"),
             "main" -> JsString(mainClass),
             "jar" -> JsString(jar64)))
-    def runPayload(args: JsValue) = JsObject("value" -> args)
 
     behavior of "Java action"
 
+    it should s"run a java snippet and confirm expected environment variables" in {
+        val props = Seq("api_host" -> "xyz",
+            "api_key" -> "abc",
+            "namespace" -> "zzz",
+            "action_name" -> "xxx",
+            "activation_id" -> "iii",
+            "deadline" -> "123")
+        val env = props.map { case (k, v) => s"__OW_${k.toUpperCase}" -> v }
+        val (out, err) = withJavaContainer({ c =>
+            val jar = JarBuilder.mkBase64Jar(
+                Seq("example", "HelloWhisk.java") -> """
+                    | package example;
+                    |
+                    | import com.google.gson.JsonObject;
+                    |
+                    | public class HelloWhisk {
+                    |     public static JsonObject main(JsonObject args) {
+                    |         JsonObject response = new JsonObject();
+                    |         response.addProperty("api_host", System.getenv("__OW_API_HOST"));
+                    |         response.addProperty("api_key", System.getenv("__OW_API_KEY"));
+                    |         response.addProperty("namespace", System.getenv("__OW_NAMESPACE"));
+                    |         response.addProperty("action_name", System.getenv("__OW_ACTION_NAME"));
+                    |         response.addProperty("activation_id", System.getenv("__OW_ACTIVATION_ID"));
+                    |         response.addProperty("deadline", System.getenv("__OW_DEADLINE"));
+                    |         return response;
+                    |     }
+                    | }
+                """.stripMargin.trim)
+
+            val (initCode, _) = c.init(initPayload("example.HelloWhisk", jar))
+            initCode should be(200)
+
+            val (runCode, out) = c.run(runPayload(JsObject(), Some(props.toMap.toJson.asJsObject)))
+            runCode should be(200)
+            props.map {
+                case (k, v) => out.get.fields(k) shouldBe JsString(v)
+
+            }
+        }, env.take(1).toMap)
+
+        out.trim shouldBe empty
+        err.trim shouldBe empty
+    }
+
     it should "support valid flows" in {
         val (out, err) = withJavaContainer { c =>
             val jar = JarBuilder.mkBase64Jar(
diff --git a/tests/src/actionContainers/NodeJsActionContainerTests.scala b/tests/src/actionContainers/NodeJsActionContainerTests.scala
index 395449d..f33e3e2 100644
--- a/tests/src/actionContainers/NodeJsActionContainerTests.scala
+++ b/tests/src/actionContainers/NodeJsActionContainerTests.scala
@@ -31,6 +31,8 @@
 
     lazy val nodejsContainerImageName = "nodejsaction"
 
+    val hasDeprecationWarnings = true
+
     override def withActionContainer(env: Map[String, String] = Map.empty)(code: ActionContainer => Unit) = {
         withContainer(nodejsContainerImageName, env)(code)
     }
@@ -68,6 +70,21 @@
           """.stripMargin)
     })
 
+    testEnv(Seq {
+        ("node", """
+         |function main(args) {
+         |    return {
+         |       "api_host": process.env['__OW_API_HOST'],
+         |       "api_key": process.env['__OW_API_KEY'],
+         |       "namespace": process.env['__OW_NAMESPACE'],
+         |       "action_name": process.env['__OW_ACTION_NAME'],
+         |       "activation_id": process.env['__OW_ACTIVATION_ID'],
+         |       "deadline": process.env['__OW_DEADLINE']
+         |    }
+         |}
+         """.stripMargin.trim)
+    })
+
     it should "fail to initialize with bad code" in {
         val (out, err) = withNodeJsContainer { c =>
             val code = """
@@ -158,7 +175,7 @@
         checkStreams(out, err, {
             case (o, e) =>
                 o shouldBe empty
-                e shouldBe empty
+                if (!hasDeprecationWarnings) e shouldBe empty
         })
     }
 
@@ -180,7 +197,58 @@
         checkStreams(out, err, {
             case (o, e) =>
                 o shouldBe empty
-                e shouldBe empty
+                if (!hasDeprecationWarnings) e shouldBe empty
+        })
+    }
+
+    it should "warn when using deprecated whisk object methods" in {
+        val (out, err) = withNodeJsContainer { c =>
+            val code = """
+                | function main(args) {
+                |     whisk.getAuthKey(whisk.setAuthKey('xxx'));
+                |     try { whisk.invoke(); } catch (e) {}
+                |     try { whisk.trigger();  } catch (e) {}
+                |     setTimeout(function () { whisk.done(); }, 1000);
+                |     return whisk.async();
+                | }
+            """.stripMargin
+
+            c.init(initPayload(code))._1 should be(200)
+
+            val (runCode, runRes) = c.run(runPayload(JsObject()))
+            runCode should be(200)
+        }
+
+        checkStreams(out, err, {
+            case (o, e) =>
+                o shouldBe empty
+                e should not be empty
+                val lines = e.split("\n")
+                lines.filter { l => l.startsWith("[WARN] \"whisk.") && l.contains("deprecated") }.length shouldBe 8
+        })
+    }
+
+    it should "warn when using deprecated whisk.error" in {
+        val (out, err) = withNodeJsContainer { c =>
+            val code = """
+                | function main(args) {
+                |     whisk.error("{warnme: true}");
+                | }
+            """.stripMargin
+
+            c.init(initPayload(code))._1 should be(200)
+
+            val (runCode, runRes) = c.run(runPayload(JsObject()))
+            runCode should be(200)
+        }
+
+        checkStreams(out, err, {
+            case (o, e) =>
+                o shouldBe empty
+                e should not be empty
+                val lines = e.split("\n")
+                lines.length shouldBe 1
+                lines.forall { l => l.startsWith("[WARN] \"whisk.") && l.contains("deprecated") }
         })
     }
 
@@ -205,7 +273,7 @@
         checkStreams(out, err, {
             case (o, e) =>
                 o should include("more than once")
-                e shouldBe empty
+                if (!hasDeprecationWarnings) e shouldBe empty
         })
     }
 
@@ -243,7 +311,7 @@
         checkStreams(out, err, {
             case (o, e) =>
                 o shouldBe empty
-                e shouldBe empty
+                if (!hasDeprecationWarnings) e shouldBe empty
         }, 3)
     }
 
@@ -268,7 +336,7 @@
         checkStreams(out, err, {
             case (o, e) =>
                 o shouldBe empty
-                e shouldBe empty
+                if (!hasDeprecationWarnings) e shouldBe empty
         })
     }
 
@@ -302,7 +370,7 @@
         checkStreams(out, err, {
             case (o, e) =>
                 o shouldBe empty
-                e shouldBe empty
+                if (!hasDeprecationWarnings) e shouldBe empty
         }, 2)
     }
 
diff --git a/tests/src/actionContainers/PythonActionContainerTests.scala b/tests/src/actionContainers/PythonActionContainerTests.scala
index fe17476..41968bf 100644
--- a/tests/src/actionContainers/PythonActionContainerTests.scala
+++ b/tests/src/actionContainers/PythonActionContainerTests.scala
@@ -53,7 +53,14 @@
         ("python", """
          |import os
          |def main(dict):
-         |    return { "auth": os.environ['AUTH_KEY'], "edge": os.environ['EDGE_HOST'] }
+         |    return {
+         |       "api_host": os.environ['__OW_API_HOST'],
+         |       "api_key": os.environ['__OW_API_KEY'],
+         |       "namespace": os.environ['__OW_NAMESPACE'],
+         |       "action_name": os.environ['__OW_ACTION_NAME'],
+         |       "activation_id": os.environ['__OW_ACTIVATION_ID'],
+         |       "deadline": os.environ['__OW_DEADLINE']
+         |    }
          """.stripMargin.trim)
     })
 
diff --git a/tests/src/actionContainers/Swift3ActionContainerTests.scala b/tests/src/actionContainers/Swift3ActionContainerTests.scala
index f1f5bbd..a294bea 100644
--- a/tests/src/actionContainers/Swift3ActionContainerTests.scala
+++ b/tests/src/actionContainers/Swift3ActionContainerTests.scala
@@ -27,20 +27,8 @@
 
     override val enforceEmptyOutputStream = false
     override lazy val swiftContainerImageName = "swift3action"
-    override lazy val envCode =  """
-         |func main(args: [String: Any]) -> [String: Any] {
-         |     let env = ProcessInfo.processInfo.environment
-         |     var auth = "???"
-         |     var edge = "???"
-         |     if let authKey : String = env["AUTH_KEY"] {
-         |         auth = "\(authKey)"
-         |     }
-         |     if let edgeHost : String = env["EDGE_HOST"] {
-         |         edge = "\(edgeHost)"
-         |     }
-         |     return ["auth": auth, "edge": edge]
-         |}
-         """.stripMargin
+    override lazy val envCode = makeEnvCode("ProcessInfo.processInfo")
+
     override lazy val errorCode = """
                 | // You need an indirection, or swiftc detects the div/0
                 | // at compile-time. Smart.
diff --git a/tests/src/actionContainers/SwiftActionContainerTests.scala b/tests/src/actionContainers/SwiftActionContainerTests.scala
index 44d965e..cfe3ce2 100644
--- a/tests/src/actionContainers/SwiftActionContainerTests.scala
+++ b/tests/src/actionContainers/SwiftActionContainerTests.scala
@@ -32,21 +32,38 @@
     // prints status messages and there doesn't seem to be a way to quiet them
     val enforceEmptyOutputStream = true
     lazy val swiftContainerImageName = "swiftaction"
+    lazy val envCode = makeEnvCode("NSProcessInfo.processInfo()")
 
-    lazy val envCode = """
+    def makeEnvCode(processInfo: String) = ("""
          |func main(args: [String: Any]) -> [String: Any] {
-         |     let env = NSProcessInfo.processInfo().environment
-         |     var auth = "???"
-         |     var edge = "???"
-         |     if let authKey : String = env["AUTH_KEY"] {
-         |         auth = "\(authKey)"
+         |     let env = """+processInfo+""".environment
+         |     var a = "???"
+         |     var b = "???"
+         |     var c = "???"
+         |     var d = "???"
+         |     var e = "???"
+         |     var f = "???"
+         |     if let v : String = env["__OW_API_HOST"] {
+         |         a = "\(v)"
          |     }
-         |     if let edgeHost : String = env["EDGE_HOST"] {
-         |         edge = "\(edgeHost)"
+         |     if let v : String = env["__OW_API_KEY"] {
+         |         b = "\(v)"
          |     }
-         |     return ["auth": auth, "edge": edge]
+         |     if let v : String = env["__OW_NAMESPACE"] {
+         |         c = "\(v)"
+         |     }
+         |     if let v : String = env["__OW_ACTION_NAME"] {
+         |         d = "\(v)"
+         |     }
+         |     if let v : String = env["__OW_ACTIVATION_ID"] {
+         |         e = "\(v)"
+         |     }
+         |     if let v : String = env["__OW_DEADLINE"] {
+         |         f = "\(v)"
+         |     }
+         |     return ["api_host": a, "api_key": b, "namespace": c, "action_name": d, "activation_id": e, "deadline": f]
          |}
-         """.stripMargin
+         """).stripMargin
 
     lazy val errorCode = """
                 | // You need an indirection, or swiftc detects the div/0
diff --git a/tests/src/system/basic/CLIPythonTests.scala b/tests/src/system/basic/CLIPythonTests.scala
index 2f00ca6..891745b 100644
--- a/tests/src/system/basic/CLIPythonTests.scala
+++ b/tests/src/system/basic/CLIPythonTests.scala
@@ -72,7 +72,7 @@
 
     it should "invoke an invalid action and get error back" in withAssetCleaner(wskprops) {
         (wp, assetHelper) =>
-            val name = "basicInvoke"
+            val name = "bad code"
             assetHelper.withCleaner(wsk.action, name) {
                 (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("malformed.py")))
             }
diff --git a/tests/src/system/basic/WskActionTests.scala b/tests/src/system/basic/WskActionTests.scala
index 1cc8e8e..524eb20 100644
--- a/tests/src/system/basic/WskActionTests.scala
+++ b/tests/src/system/basic/WskActionTests.scala
@@ -45,6 +45,22 @@
 
     behavior of "Whisk actions"
 
+    it should "invoke an action returning a promise" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "hello promise"
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("helloPromise.js")))
+            }
+
+            val run = wsk.action.invoke(name)
+            withActivation(wsk.activation, run) {
+                activation =>
+                    activation.response.status shouldBe "success"
+                    activation.response.result shouldBe Some(JsObject("done" -> true.toJson))
+                    activation.logs.get.mkString(" ") shouldBe empty
+            }
+    }
+
     it should "invoke an action with a space in the name" in withAssetCleaner(wskprops) {
         (wp, assetHelper) =>
             val name = "hello Async"
@@ -169,7 +185,7 @@
             wsk.parseJsonString(rr.stdout).getFieldPath("exec", "code") shouldBe Some(JsString(""))
     }
 
-    it should "blocking invoke nested blocking actions" in withAssetCleaner(wskprops) {
+    it should "blocking invoke of nested blocking actions" in withAssetCleaner(wskprops) {
         (wp, assetHelper) =>
             val name = "nestedBlockingAction"
             val child = "wc"
diff --git a/tests/src/system/basic/WskBasicNodeTests.scala b/tests/src/system/basic/WskBasicNodeTests.scala
index 4e5fdca..d78c7d6 100644
--- a/tests/src/system/basic/WskBasicNodeTests.scala
+++ b/tests/src/system/basic/WskBasicNodeTests.scala
@@ -113,6 +113,7 @@
             }
     }
 
+    // TODO: remove this tests and its assets when "whisk.js" is removed entirely as it is no longer necessary
     it should "Ensure that whisk.invoke() returns a promise" in withAssetCleaner(wskprops) {
         val expectedDuration = 3.seconds
 
@@ -165,6 +166,7 @@
             }
     }
 
+    // TODO: remove this tests and its assets when "whisk.js" is removed entirely as it is no longer necessary
     it should "Ensure that whisk.invoke() still uses a callback when provided one" in withAssetCleaner(wskprops) {
         val expectedDuration = 3.seconds
 
@@ -216,6 +218,7 @@
             }
     }
 
+    // TODO: remove this tests and its assets when "whisk.js" is removed entirely as it is no longer necessary
     it should "Ensure that whisk.trigger() still uses a callback when provided one" in withAssetCleaner(wskprops) {
         (wp, assetHelper) =>
             // this action supplies a 'next' callback to whisk.trigger()
@@ -251,6 +254,7 @@
             }
     }
 
+    // TODO: remove this tests and its assets when "whisk.js" is removed entirely as it is no longer necessary
     it should "Ensure that whisk.trigger() returns a promise" in withAssetCleaner(wskprops) {
         (wp, assetHelper) =>
             // this action supplies a 'next' callback to whisk.trigger()
diff --git a/tests/src/system/basic/WskBasicTests.scala b/tests/src/system/basic/WskBasicTests.scala
index 36c661d..1acfa40 100644
--- a/tests/src/system/basic/WskBasicTests.scala
+++ b/tests/src/system/basic/WskBasicTests.scala
@@ -16,19 +16,15 @@
 
 package system.basic
 
-import java.time.Instant
 import java.io.File
+import java.time.Instant
 
 import org.junit.runner.RunWith
 import org.scalatest.junit.JUnitRunner
 
 import common.TestHelpers
 import common.TestUtils
-import common.TestUtils.CONFLICT
-import common.TestUtils.SUCCESS_EXIT
-import common.TestUtils.UNAUTHORIZED
-import common.TestUtils.FORBIDDEN
-import common.TestUtils.ERROR_EXIT
+import common.TestUtils._
 import common.Wsk
 import common.WskProps
 import common.WskTestHelpers
@@ -361,15 +357,17 @@
             }
     }
 
-    it should "create and invoke a blocking action resulting in an error response object" in withAssetCleaner(wskprops) {
+    it should "create and invoke a blocking action resulting in an failed promise" in withAssetCleaner(wskprops) {
         (wp, assetHelper) =>
             val name = "errorResponseObject"
             assetHelper.withCleaner(wsk.action, name) {
                 (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("asyncError.js")))
             }
 
-            wsk.action.invoke(name, blocking = true, expectedExitCode = 246)
-                .stderr should include regex (""""error": "name '!' contains illegal characters \(code \d+\)"""")
+            val stderr = wsk.action.invoke(name, blocking = true, expectedExitCode = 246).stderr
+            CliActivation.serdes.read(stderr.parseJson).response.result shouldBe Some {
+                JsObject("error" -> JsObject("msg" -> "failed activation on purpose".toJson))
+            }
     }
 
     it should "invoke a blocking action and get only the result" in withAssetCleaner(wskprops) {
diff --git a/tests/src/whisk/core/cli/test/WskBasicUsageTests.scala b/tests/src/whisk/core/cli/test/WskBasicUsageTests.scala
index e832c11..0bf4198 100644
--- a/tests/src/whisk/core/cli/test/WskBasicUsageTests.scala
+++ b/tests/src/whisk/core/cli/test/WskBasicUsageTests.scala
@@ -45,6 +45,8 @@
 import whisk.utils.retry
 import JsonArgsForTests._
 import whisk.http.Messages
+import common.WskAdmin
+import java.time.Clock
 
 /**
  * Tests for basic CLI usage. Some of these tests require a deployed backend.
@@ -452,6 +454,63 @@
             }
     }
 
+    it should "invoke an action using npm openwhisk" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "hello npm openwhisk"
+            assetHelper.withCleaner(wsk.action, name, confirmDelete = false) {
+                (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("helloOpenwhiskPackage.js")))
+            }
+
+            val run = wsk.action.invoke(name, Map("ignore_certs" -> true.toJson, "name" -> name.toJson))
+            withActivation(wsk.activation, run) {
+                activation =>
+                    activation.response.status shouldBe "success"
+                    activation.response.result shouldBe Some(JsObject("delete" -> true.toJson))
+                    activation.logs.get.mkString(" ") should include("action list has this many actions")
+            }
+
+            wsk.action.delete(name, expectedExitCode = TestUtils.NOT_FOUND)
+    }
+
+    it should "invoke an action receiving context properties" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val user = WskAdmin.getUser(wskprops.authKey)
+            val name = "context"
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("helloContext.js")))
+            }
+
+            val start = Instant.now(Clock.systemUTC()).toEpochMilli
+            val run = wsk.action.invoke(name)
+            withActivation(wsk.activation, run) {
+                activation =>
+                    activation.response.status shouldBe "success"
+                    val fields = activation.response.result.get.convertTo[Map[String, String]]
+                    fields("api_host") shouldBe WhiskProperties.getEdgeHost + ":" + WhiskProperties.getEdgeHostApiPort
+                    fields("api_key") shouldBe wskprops.authKey
+                    fields("namespace") shouldBe user
+                    fields("action_name") shouldBe s"/$user/$name"
+                    fields("activation_id") shouldBe activation.activationId
+                    fields("deadline").toLong should be >= start
+            }
+    }
+
+    it should s"invoke an action that returns a result by the deadline" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "deadline"
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("helloDeadline.js")), timeout = Some(3 seconds))
+            }
+
+            val start = Instant.now(Clock.systemUTC()).toEpochMilli
+            val run = wsk.action.invoke(name)
+            withActivation(wsk.activation, run) {
+                activation =>
+                    activation.response.status shouldBe "success"
+                    activation.response.result shouldBe Some(JsObject("timedout" -> true.toJson))
+            }
+    }
+
     behavior of "Wsk packages"
 
     it should "create, and get a package to verify parameter and annotation parsing" in withAssetCleaner(wskprops) {