You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openwhisk.apache.org by cs...@apache.org on 2017/06/26 15:59:41 UTC

[incubator-openwhisk-cli] 01/36: Clean up test project (#1960)

This is an automated email from the ASF dual-hosted git repository.

csantanapr pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-openwhisk-cli.git

commit 9f09096661f16557a3aebf82bffb475b5ae984b4
Author: Markus Thömmes <ma...@me.com>
AuthorDate: Tue Mar 7 15:35:29 2017 +0100

    Clean up test project (#1960)
    
    
    
    - Remove unused dependencies
    - Define dependencies as compile time to be able to include them from other projects
    - Standardize layout
    
    * Bumping restassured version
    
    * Update gitignore to include .cache-tests
---
 .../apigw/healthtests/ApiGwEndToEndTests.scala     |  120 ++
 .../src/test/scala/system/basic/CLIJavaTests.scala |  105 ++
 .../test/scala/system/basic/CLIPythonTests.scala   |  108 ++
 .../src/test/scala/system/basic/ConsoleTests.scala |   98 ++
 .../src/test/scala/system/basic/PackageTests.scala |  146 +++
 .../system/basic/Swift3WhiskObjectTests.scala      |   99 ++
 .../test/scala/system/basic/WskActionTests.scala   |  293 +++++
 .../test/scala/system/basic/WskBasicTests.scala    |  739 ++++++++++++
 .../src/test/scala/system/basic/WskRuleTests.scala |  360 ++++++
 .../src/test/scala/system/basic/WskSdkTests.scala  |  110 ++
 .../test/scala/system/basic/WskSequenceTests.scala |  528 +++++++++
 .../scala/whisk/core/admin/WskAdminTests.scala     |   85 ++
 .../actions/test/ApiGwRoutemgmtActionTests.scala   |  332 ++++++
 .../scala/whisk/core/cli/test/ApiGwTests.scala     |  511 ++++++++
 .../core/cli/test/WskActionSequenceTests.scala     |   85 ++
 .../whisk/core/cli/test/WskBasicUsageTests.scala   | 1245 ++++++++++++++++++++
 .../whisk/core/cli/test/WskEntitlementTests.scala  |  376 ++++++
 .../whisk/core/cli/test/WskWebActionsTests.scala   |  117 ++
 18 files changed, 5457 insertions(+)

diff --git a/tests/src/test/scala/apigw/healthtests/ApiGwEndToEndTests.scala b/tests/src/test/scala/apigw/healthtests/ApiGwEndToEndTests.scala
new file mode 100644
index 0000000..1833d43
--- /dev/null
+++ b/tests/src/test/scala/apigw/healthtests/ApiGwEndToEndTests.scala
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * 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.
+ */
+
+package apigw.healthtests
+
+import java.io.BufferedWriter
+import java.io.File
+import java.io.FileWriter
+
+import scala.concurrent.duration.DurationInt
+
+import org.junit.runner.RunWith
+import org.scalatest.FlatSpec
+import org.scalatest.Matchers
+import org.scalatest.junit.JUnitRunner
+
+import com.jayway.restassured.RestAssured
+
+import common.TestHelpers
+import common.TestUtils
+import common.TestUtils._
+import common.Wsk
+import common.WskAdmin
+import common.WskProps
+import common.WskTestHelpers
+import spray.json._
+import spray.json.DefaultJsonProtocol._
+import system.rest.RestUtil
+
+/**
+ * Basic tests of the download link for Go CLI binaries
+ */
+@RunWith(classOf[JUnitRunner])
+class ApiGwEndToEndTests extends FlatSpec with Matchers with RestUtil with TestHelpers with WskTestHelpers {
+
+    implicit val wskprops = WskProps()
+    val wsk = new Wsk
+    val (cliuser, clinamespace) = WskAdmin.getUser(wskprops.authKey)
+
+    it should s"create an API and successfully invoke that API" in {
+        val testName = "APIGW_HEALTHTEST1"
+        val testbasepath = "/" + testName + "_bp"
+        val testrelpath = "/path"
+        val testurlop = "get"
+        val testapiname = testName + " API Name"
+        val actionName = "echo"
+        val urlqueryparam = "name"
+        val urlqueryvalue = "test"
+
+        try {
+            println("cli user: " + cliuser + "; cli namespace: " + clinamespace)
+
+            // Create the action for the API
+            val file = TestUtils.getTestActionFilename(s"echo.js")
+            wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = SUCCESS_EXIT)
+
+            // Create the API
+            var rr = wsk.api.create(basepath = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname))
+            rr.stdout should include("ok: created API")
+            val apiurl = rr.stdout.split("\n")(1)
+            println(s"apiurl: '${apiurl}'")
+
+            // Validate the API was successfully created
+            // List result will look like:
+            // ok: APIs
+            // Action                            Verb             API Name  URL
+            // /_//whisk.system/utils/echo          get  APIGW_HEALTHTEST1 API Name  http://172.17.0.1:9001/api/ab9082cd-ea8e-465a-8a65-b491725cc4ef/APIGW_HEALTHTEST1_bp/path
+            rr = wsk.api.list(basepathOrApiName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop))
+            rr.stdout should include("ok: APIs")
+            rr.stdout should include regex (s"${actionName}\\s+${testurlop}\\s+${testapiname}\\s+")
+            rr.stdout should include(testbasepath + testrelpath)
+
+            // Recreate the API using a JSON swagger file
+            rr = wsk.api.get(basepathOrApiName = Some(testbasepath))
+            val swaggerfile = File.createTempFile("api", ".json")
+            swaggerfile.deleteOnExit()
+            val bw = new BufferedWriter(new FileWriter(swaggerfile))
+            bw.write(rr.stdout)
+            bw.close()
+
+            // Delete API to that it can be recreated again using the generated swagger file
+            val deleteApiResult = wsk.api.delete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)
+
+            // Create the API again, but use the swagger file this time
+            rr = wsk.api.create(swagger = Some(swaggerfile.getAbsolutePath()))
+            rr.stdout should include("ok: created API")
+            val swaggerapiurl = rr.stdout.split("\n")(1)
+            println(s"apiurl: '${swaggerapiurl}'")
+
+            // Call the API URL and validate the results
+            val response = whisk.utils.retry({
+                val response = RestAssured.given().config(sslconfig).get(s"$swaggerapiurl?$urlqueryparam=$urlqueryvalue")
+                response.statusCode should be(200)
+                response
+            }, 5, Some(1.second))
+            val responseString = response.body.asString
+            println("URL invocation response: " + responseString)
+            responseString.parseJson.asJsObject.fields(urlqueryparam).convertTo[String] should be(urlqueryvalue)
+
+        } finally {
+            println("Deleting action: " + actionName)
+            val finallydeleteActionResult = wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT)
+            println("Deleting API: " + testbasepath)
+            val finallydeleteApiResult = wsk.api.delete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)
+        }
+    }
+}
diff --git a/tests/src/test/scala/system/basic/CLIJavaTests.scala b/tests/src/test/scala/system/basic/CLIJavaTests.scala
new file mode 100644
index 0000000..1f7ccef
--- /dev/null
+++ b/tests/src/test/scala/system/basic/CLIJavaTests.scala
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * 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.
+ */
+
+package system.basic
+
+import scala.concurrent.duration.DurationInt
+
+import common.TestHelpers
+import common.TestUtils
+import common.TestUtils.ANY_ERROR_EXIT
+import common.WskTestHelpers
+import common.WskProps
+import common.Wsk
+
+import org.junit.runner.RunWith
+import org.scalatest.Matchers
+import org.scalatest.junit.JUnitRunner
+
+import spray.json.JsString
+
+@RunWith(classOf[JUnitRunner])
+class CLIJavaTests
+    extends TestHelpers
+    with WskTestHelpers
+    with Matchers {
+
+    implicit val wskprops = WskProps()
+    val wsk = new Wsk
+    val expectedDuration = 120.seconds
+    val activationPollDuration = 60.seconds
+
+    behavior of "Java Actions"
+
+    /**
+     * Test the Java "hello world" demo sequence
+     */
+    it should "Invoke a java action" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "helloJava"
+            val file = Some(TestUtils.getTestActionFilename("helloJava.jar"))
+
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, file, main = Some("hello.HelloJava"))
+            }
+
+            val start = System.currentTimeMillis()
+            withActivation(wsk.activation, wsk.action.invoke(name), totalWait = activationPollDuration) {
+                _.response.result.get.toString should include("Hello stranger!")
+            }
+
+            withActivation(wsk.activation, wsk.action.invoke(name, Map("name" -> JsString("Sir"))), totalWait = activationPollDuration) {
+                _.response.result.get.toString should include("Hello Sir!")
+            }
+
+            withClue("Test duration exceeds expectation (ms)") {
+                val duration = System.currentTimeMillis() - start
+                duration should be <= expectedDuration.toMillis
+            }
+    }
+
+    /*
+     * Example from the docs.
+     */
+    it should "Invoke a Java action where main is in the default package" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "helloJavaDefaultPkg"
+            val file = Some(TestUtils.getTestActionFilename("helloJavaDefaultPackage.jar"))
+
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, file, main = Some("Hello"))
+            }
+
+            withActivation(wsk.activation, wsk.action.invoke(name, Map()), totalWait = activationPollDuration) {
+                _.response.result.get.toString should include("Hello stranger!")
+            }
+    }
+
+    it should "Ensure that Java actions cannot be created without a specified main method" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "helloJavaWithNoMainSpecified"
+            val file = Some(TestUtils.getTestActionFilename("helloJava.jar"))
+
+            val createResult = assetHelper.withCleaner(wsk.action, name, confirmDelete = false) {
+                (action, _) =>
+                    action.create(name, file, expectedExitCode = ANY_ERROR_EXIT)
+            }
+
+            val output = s"${createResult.stdout}\n${createResult.stderr}"
+
+            output should include("main")
+    }
+}
diff --git a/tests/src/test/scala/system/basic/CLIPythonTests.scala b/tests/src/test/scala/system/basic/CLIPythonTests.scala
new file mode 100644
index 0000000..ff7ea82
--- /dev/null
+++ b/tests/src/test/scala/system/basic/CLIPythonTests.scala
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * 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.
+ */
+
+package system.basic;
+
+import org.junit.runner.RunWith
+import org.scalatest.Matchers
+import org.scalatest.junit.JUnitRunner
+import common.TestUtils
+import common.Wsk
+import common.WskProps
+import spray.json._
+import spray.json.DefaultJsonProtocol.StringJsonFormat
+import common.TestHelpers
+import common.WskTestHelpers
+import common.WskProps
+import common.WhiskProperties
+
+@RunWith(classOf[JUnitRunner])
+class CLIPythonTests
+    extends TestHelpers
+    with WskTestHelpers
+    with Matchers {
+
+    implicit val wskprops = WskProps()
+    val wsk = new Wsk
+
+    behavior of "Native Python Action"
+
+    it should "invoke an action and get the result" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "basicInvoke"
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("hello.py")))
+            }
+
+            withActivation(wsk.activation, wsk.action.invoke(name, Map("name" -> "Prince".toJson))) {
+                _.response.result.get.toString should include("Prince")
+            }
+    }
+
+    it should "invoke an action with a non-default entry point" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "nonDefaultEntryPoint"
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("niam.py")), main = Some("niam"))
+            }
+
+            withActivation(wsk.activation, wsk.action.invoke(name, Map())) {
+                _.response.result.get.fields.get("greetings") should be(Some(JsString("Hello from a non-standard entrypoint.")))
+            }
+    }
+
+    it should "invoke an action from a zip file with a non-default entry point" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "pythonZipWithNonDefaultEntryPoint"
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("python.zip")), main = Some("niam"), kind = Some("python"))
+            }
+
+            withActivation(wsk.activation, wsk.action.invoke(name, Map("name" -> "Prince".toJson))) {
+                _.response.result.get shouldBe JsObject("greeting" -> JsString("Hello Prince!"))
+            }
+    }
+
+    it should "invoke an action and confirm expected environment is defined" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "stdenv"
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("stdenv.py")))
+            }
+
+            withActivation(wsk.activation, wsk.action.invoke(name)) {
+                activation =>
+                    val result = activation.response.result.get
+                    result.fields.get("error") shouldBe empty
+                    result.fields.get("auth") shouldBe Some(JsString(WhiskProperties.readAuthKey(WhiskProperties.getAuthFileForTesting)))
+                    result.fields.get("edge").toString.trim should not be empty
+            }
+    }
+
+    it should "invoke an invalid action and get error back" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "bad code"
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("malformed.py")))
+            }
+
+            withActivation(wsk.activation, wsk.action.invoke(name)) {
+                activation =>
+                    activation.response.result.get.fields.get("error") shouldBe Some(JsString("The action failed to generate or locate a binary. See logs for details."))
+                    activation.logs.get.mkString("\n") should { not include ("pythonaction.py") and not include ("flask") }
+            }
+    }
+}
diff --git a/tests/src/test/scala/system/basic/ConsoleTests.scala b/tests/src/test/scala/system/basic/ConsoleTests.scala
new file mode 100644
index 0000000..3561e34
--- /dev/null
+++ b/tests/src/test/scala/system/basic/ConsoleTests.scala
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * 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.
+ */
+
+package system.basic;
+
+import java.time.Clock
+import java.time.Instant
+
+import scala.concurrent.duration.Duration
+import scala.concurrent.duration.DurationInt
+import scala.concurrent.duration.MILLISECONDS
+import scala.language.postfixOps
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+import common.TestHelpers
+import common.TestUtils
+import common.Wsk
+import common.WskProps
+import common.WskTestHelpers
+import spray.json.DefaultJsonProtocol.IntJsonFormat
+import spray.json.DefaultJsonProtocol.StringJsonFormat
+import spray.json.pimpAny
+
+/**
+ * Tests of the text console
+ */
+@RunWith(classOf[JUnitRunner])
+class ConsoleTests
+    extends TestHelpers
+    with WskTestHelpers {
+
+    implicit val wskprops = WskProps()
+    val wsk = new Wsk
+    val guestNamespace = wskprops.namespace
+
+    behavior of "Wsk Activation Console"
+
+    it should "show an activation log message for hello world" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val packageName = "samples"
+            val actionName = "helloWorld"
+            val fullActionName = s"/$guestNamespace/$packageName/$actionName"
+            assetHelper.withCleaner(wsk.pkg, packageName) {
+                (pkg, _) => pkg.create(packageName, shared = Some(true))
+            }
+
+            assetHelper.withCleaner(wsk.action, fullActionName) {
+                (action, _) => action.create(fullActionName, Some(TestUtils.getTestActionFilename("hello.js")))
+            }
+
+            val duration = Some(30 seconds)
+            val payload = new String("from the console!".getBytes, "UTF-8")
+            val run = wsk.action.invoke(fullActionName, Map("payload" -> payload.toJson))
+            withActivation(wsk.activation, run, totalWait = duration.get) {
+                activation =>
+                    val console = wsk.activation.console(10 seconds, since = duration)
+                    println(console.stdout)
+                    console.stdout should include(payload)
+        }
+    }
+
+    it should "show repeated activations" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "countdown"
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("countdown.js")))
+            }
+
+            val start = Instant.now(Clock.systemUTC())
+            val run = wsk.action.invoke(name, Map("n" -> 3.toJson))
+            withActivation(wsk.activation, run) {
+                activation =>
+                    val activations = wsk.activation.pollFor(N = 4, Some(name), since = Some(start), retries = 80).length
+                    withClue(s"expected activations:") {
+                        activations should be(4)
+                    }
+                    val duration = Duration(Instant.now(Clock.systemUTC()).toEpochMilli - start.toEpochMilli, MILLISECONDS)
+                    val console = wsk.activation.console(10 seconds, since = Some(duration))
+                    console.stdout should include("Happy New Year")
+            }
+    }
+
+}
diff --git a/tests/src/test/scala/system/basic/PackageTests.scala b/tests/src/test/scala/system/basic/PackageTests.scala
new file mode 100644
index 0000000..da4026f
--- /dev/null
+++ b/tests/src/test/scala/system/basic/PackageTests.scala
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * 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.
+ */
+
+package system.basic
+
+import java.util.Date
+import scala.language.postfixOps
+import scala.collection.mutable.HashMap
+import scala.concurrent.duration.DurationInt
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+import common.TestUtils
+import common.Wsk
+import common.WskProps
+import spray.json._
+import spray.json.DefaultJsonProtocol.StringJsonFormat
+import common.TestHelpers
+import common.WskTestHelpers
+import common.TestHelpers
+import common.WskProps
+
+@RunWith(classOf[JUnitRunner])
+class PackageTests
+    extends TestHelpers
+    with WskTestHelpers {
+
+    implicit val wskprops = WskProps()
+    val wsk = new Wsk
+    val LOG_DELAY = 80 seconds
+
+    behavior of "Wsk Package"
+
+    it should "confirm wsk exists" in {
+        Wsk.exists
+    }
+
+    it should "allow creation and deletion of a package" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "simplepackage"
+            assetHelper.withCleaner(wsk.pkg, name) {
+                (pkg, _) => pkg.create(name, Map())
+            }
+    }
+
+    val params1 = Map("p1" -> "v1".toJson, "p2" -> "".toJson)
+    val params2 = Map("p1" -> "v1".toJson, "p2" -> "v2".toJson, "p3" -> "v3".toJson)
+
+    it should "allow creation of a package with parameters" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "simplepackagewithparams"
+            assetHelper.withCleaner(wsk.pkg, name) { (pkg, _) =>
+                pkg.create(name, params1)
+            }
+    }
+
+    it should "allow updating a package" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "simplepackagetoupdate"
+            assetHelper.withCleaner(wsk.pkg, name) { (pkg, _) =>
+                pkg.create(name, params1)
+                pkg.create(name, params2, update = true)
+            }
+    }
+
+    it should "allow binding of a package" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "simplepackagetobind"
+            val bindName = "simplebind"
+            assetHelper.withCleaner(wsk.pkg, name) { (pkg, _) =>
+                pkg.create(name, params1)
+            }
+            assetHelper.withCleaner(wsk.pkg, bindName) { (pkg, _) =>
+                pkg.bind(name, bindName, params2)
+            }
+    }
+
+    it should "perform package binds so parameters are inherited" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val packageName = "package1"
+            val bindName = "package2"
+            val actionName = "print"
+            val packageActionName = packageName + "/" + actionName
+            val bindActionName = bindName + "/" + actionName
+            val packageParams = Map("key1a" -> "value1a".toJson, "key1b" -> "value1b".toJson)
+            val bindParams = Map("key2a" -> "value2a".toJson, "key1b" -> "value2b".toJson)
+            val actionParams = Map("key0" -> "value0".toJson)
+            val file = TestUtils.getTestActionFilename("printParams.js")
+            assetHelper.withCleaner(wsk.pkg, packageName) { (pkg, _) =>
+                pkg.create(packageName, packageParams)
+            }
+            assetHelper.withCleaner(wsk.action, packageActionName) { (action, _) =>
+                action.create(packageActionName, Some(file), parameters = actionParams)
+            }
+            assetHelper.withCleaner(wsk.pkg, bindName) { (pkg, _) =>
+                pkg.bind(packageName, bindName, bindParams)
+            }
+
+            // Check that the description of packages and actions includes all the inherited parameters.
+            val packageDescription = wsk.pkg.get(packageName).stdout
+            val bindDescription = wsk.pkg.get(bindName).stdout
+            val packageActionDescription = wsk.action.get(packageActionName).stdout
+            val bindActionDescription = wsk.action.get(bindActionName).stdout
+            checkForParameters(packageDescription, packageParams)
+            checkForParameters(bindDescription, packageParams, bindParams)
+            checkForParameters(packageActionDescription, packageParams, actionParams)
+            checkForParameters(bindActionDescription, packageParams, bindParams, actionParams)
+
+            // Check that inherited parameters are passed to the action.
+            val now = new Date().toString()
+            val run = wsk.action.invoke(bindActionName, Map("payload" -> now.toJson))
+            withActivation(wsk.activation, run, totalWait = LOG_DELAY) {
+                _.logs.get.mkString(" ") should include regex (
+                    String.format(".*key0: value0.*key1a: value1a.*key1b: value2b.*key2a: value2a.*payload: %s", now))
+            }
+    }
+
+    /**
+     * Check that a description of an item includes the specified parameters.
+     * Parameters keys in later parameter maps override earlier ones.
+     */
+    def checkForParameters(itemDescription: String, paramSets: Map[String, JsValue]*) {
+        // Merge and the parameters handling overrides.
+        val merged = HashMap.empty[String, JsValue]
+        paramSets.foreach { merged ++= _ }
+        val flatDescription = itemDescription.replace("\n", "").replace("\r", "")
+        merged.foreach {
+            case (key: String, value: JsValue) =>
+                val toFind = s""""key": "${key}",.*"value": ${value.toString}"""
+                flatDescription should include regex toFind
+        }
+    }
+
+}
diff --git a/tests/src/test/scala/system/basic/Swift3WhiskObjectTests.scala b/tests/src/test/scala/system/basic/Swift3WhiskObjectTests.scala
new file mode 100644
index 0000000..c04c516
--- /dev/null
+++ b/tests/src/test/scala/system/basic/Swift3WhiskObjectTests.scala
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * 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.
+ */
+
+package system.basic
+
+import scala.concurrent.duration.DurationInt
+import scala.language.postfixOps
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+import common.TestHelpers
+import common.TestUtils
+import common.Wsk
+import common.WskProps
+import common.WskTestHelpers
+import spray.json.DefaultJsonProtocol.StringJsonFormat
+import spray.json.pimpAny
+
+@RunWith(classOf[JUnitRunner])
+class Swift3WhiskObjectTests
+    extends TestHelpers
+    with WskTestHelpers {
+
+    implicit val wskprops = WskProps()
+    val wsk = new Wsk
+
+    behavior of "Swift 3 Whisk backend API"
+
+    it should "allow Swift actions to invoke other actions" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            // use CLI to create action from dat/actions/invokeAction.swift
+            val file = TestUtils.getTestActionFilename("invoke.swift")
+            val actionName = "invokeAction"
+            assetHelper.withCleaner(wsk.action, actionName) {
+                (action, _) => action.create(name = actionName, artifact = Some(file), kind = Some("swift:3"))
+            }
+
+            // invoke the action
+            val run = wsk.action.invoke(actionName)
+            withActivation(wsk.activation, run, initialWait = 5 seconds, totalWait = 60 seconds) {
+                activation =>
+                    // should be successful
+                    activation.response.success shouldBe true
+
+                    // should have a field named "activationId" which is the date action's activationId
+                    activation.response.result.get.fields("activationId").toString.length should be >= 32
+
+                    // check for "date" field that comes from invoking the date action
+                    //activation.response.result.get.fieldPathExists("response", "result", "date") should be(true)
+            }
+    }
+
+    it should "allow Swift actions to trigger events" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            // create a trigger
+            val triggerName = s"TestTrigger ${System.currentTimeMillis()}"
+            assetHelper.withCleaner(wsk.trigger, triggerName) {
+                (trigger, _) => trigger.create(triggerName)
+            }
+
+            // create an action that fires the trigger
+            val file = TestUtils.getTestActionFilename("trigger.swift")
+            val actionName = "ActionThatTriggers"
+            assetHelper.withCleaner(wsk.action, actionName) {
+                (action, _) => action.create(name = actionName, artifact = Some(file), kind = Some("swift:3"))
+            }
+
+            // invoke the action
+            val run = wsk.action.invoke(actionName, Map("triggerName" -> triggerName.toJson))
+            withActivation(wsk.activation, run, initialWait = 5 seconds, totalWait = 60 seconds) {
+                activation =>
+                    // should be successful
+                    activation.response.success shouldBe true
+
+                    // should have a field named "activationId" which is the date action's activationId
+                    activation.response.result.get.fields("activationId").toString.length should be >= 32
+
+                    // should result in an activation for triggerName
+                    val triggerActivations = wsk.activation.pollFor(1, Some(triggerName), retries = 20)
+                    withClue(s"trigger activations for $triggerName:") {
+                        triggerActivations.length should be(1)
+                    }
+            }
+    }
+}
diff --git a/tests/src/test/scala/system/basic/WskActionTests.scala b/tests/src/test/scala/system/basic/WskActionTests.scala
new file mode 100644
index 0000000..dcb6dcd
--- /dev/null
+++ b/tests/src/test/scala/system/basic/WskActionTests.scala
@@ -0,0 +1,293 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * 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.
+ */
+
+package system.basic
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+import common.JsHelpers
+import common.TestHelpers
+import common.TestUtils
+import common.Wsk
+import common.WskProps
+import common.WskTestHelpers
+import spray.json._
+import spray.json.DefaultJsonProtocol._
+import spray.json.JsObject
+import spray.json.pimpAny
+
+@RunWith(classOf[JUnitRunner])
+class WskActionTests
+    extends TestHelpers
+    with WskTestHelpers
+    with JsHelpers {
+
+    implicit val wskprops = WskProps()
+    val wsk = new Wsk
+
+    val testString = "this is a test"
+    val testResult = JsObject("count" -> testString.split(" ").length.toJson)
+    val guestNamespace = wskprops.namespace
+
+    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"
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("helloAsync.js")))
+            }
+
+            val run = wsk.action.invoke(name, Map("payload" -> testString.toJson))
+            withActivation(wsk.activation, run) {
+                activation =>
+                    activation.response.status shouldBe "success"
+                    activation.response.result shouldBe Some(testResult)
+                    activation.logs.get.mkString(" ") should include(testString)
+            }
+    }
+
+    it should "pass parameters bound on creation-time to the action" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "printParams"
+            val params = Map(
+                "param1" -> "test1",
+                "param2" -> "test2")
+
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) =>
+                    action.create(
+                        name,
+                        Some(TestUtils.getTestActionFilename("printParams.js")),
+                        parameters = params.mapValues(_.toJson))
+            }
+
+            val invokeParams = Map("payload" -> testString)
+            val run = wsk.action.invoke(name, invokeParams.mapValues(_.toJson))
+            withActivation(wsk.activation, run) {
+                activation =>
+                    val logs = activation.logs.get.mkString(" ")
+
+                    (params ++ invokeParams).foreach {
+                        case (key, value) =>
+                            logs should include(s"params.$key: $value")
+                    }
+            }
+    }
+
+    it should "copy an action and invoke it successfully" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "copied"
+            val packageName = "samples"
+            val actionName = "wordcount"
+            val fullQualifiedName = s"/$guestNamespace/$packageName/$actionName"
+
+            assetHelper.withCleaner(wsk.pkg, packageName) {
+                (pkg, _) => pkg.create(packageName, shared = Some(true))
+            }
+
+            assetHelper.withCleaner(wsk.action, fullQualifiedName) {
+                val file = Some(TestUtils.getTestActionFilename("wc.js"))
+                (action, _) => action.create(fullQualifiedName, file)
+            }
+
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, Some(fullQualifiedName), Some("copy"))
+            }
+
+            val run = wsk.action.invoke(name, Map("payload" -> testString.toJson))
+            withActivation(wsk.activation, run) {
+                activation =>
+                    activation.response.status shouldBe "success"
+                    activation.response.result shouldBe Some(testResult)
+                    activation.logs.get.mkString(" ") should include(testString)
+            }
+    }
+
+    it should "copy an action and ensure exec, parameters, and annotations copied" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val origActionName = "orignAction"
+            val copiedActionName = "copiedAction"
+            val params = Map("a" -> "A".toJson)
+            val annots = Map("b" -> "B".toJson)
+
+            assetHelper.withCleaner(wsk.action, origActionName) {
+                val file = Some(TestUtils.getTestActionFilename("wc.js"))
+                (action, _) => action.create(origActionName, file, parameters = params, annotations = annots)
+            }
+
+            assetHelper.withCleaner(wsk.action, copiedActionName) {
+                (action, _) => action.create(copiedActionName, Some(origActionName), Some("copy"))
+            }
+
+            val copiedAction = getJSONFromCLIResponse(wsk.action.get(copiedActionName).stdout)
+            val origAction = getJSONFromCLIResponse(wsk.action.get(copiedActionName).stdout)
+
+            copiedAction.fields("annotations") shouldBe origAction.fields("annotations")
+            copiedAction.fields("parameters") shouldBe origAction.fields("parameters")
+            copiedAction.fields("exec") shouldBe origAction.fields("exec")
+            copiedAction.fields("version") shouldBe JsString("0.0.1")
+    }
+
+    it should "recreate and invoke a new action with different code" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "recreatedAction"
+            assetHelper.withCleaner(wsk.action, name, false) {
+                (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("wc.js")))
+            }
+
+            val run1 = wsk.action.invoke(name, Map("payload" -> testString.toJson))
+            withActivation(wsk.activation, run1) {
+                activation =>
+                    activation.response.status shouldBe "success"
+                    activation.logs.get.mkString(" ") should include(s"The message '$testString' has")
+            }
+
+            wsk.action.delete(name)
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("hello.js")))
+            }
+
+            val run2 = wsk.action.invoke(name, Map("payload" -> testString.toJson))
+            withActivation(wsk.activation, run2) {
+                activation =>
+                    activation.response.status shouldBe "success"
+                    activation.logs.get.mkString(" ") should include(s"hello $testString")
+            }
+    }
+
+    it should "fail to invoke an action with an empty file" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "empty"
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("empty.js")))
+            }
+            val run = wsk.action.invoke(name)
+            withActivation(wsk.activation, run) {
+                activation =>
+                    activation.response.status shouldBe "action developer error"
+                    activation.response.result shouldBe Some(JsObject("error" -> "Missing main/no code to execute.".toJson))
+            }
+    }
+
+    it should "create an action with an empty file" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "empty"
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("empty.js")))
+            }
+            val rr = wsk.action.get(name)
+            wsk.parseJsonString(rr.stdout).getFieldPath("exec", "code") shouldBe Some(JsString(""))
+    }
+
+    it should "blocking invoke of nested blocking actions" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "nestedBlockingAction"
+            val child = "wc"
+
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("wcbin.js")))
+            }
+            assetHelper.withCleaner(wsk.action, child) {
+                (action, _) => action.create(child, Some(TestUtils.getTestActionFilename("wc.js")))
+            }
+
+            val run = wsk.action.invoke(name, Map("payload" -> testString.toJson), blocking = true)
+            val activation = wsk.parseJsonString(run.stdout).convertTo[CliActivation]
+
+            withClue(s"check failed for activation: $activation") {
+                val wordCount = testString.split(" ").length
+                activation.response.result.get shouldBe JsObject("binaryCount" -> s"${wordCount.toBinaryString} (base 2)".toJson)
+            }
+    }
+
+    it should "blocking invoke an asynchronous action" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "helloAsync"
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("helloAsync.js")))
+            }
+
+            val run = wsk.action.invoke(name, Map("payload" -> testString.toJson), blocking = true)
+            val activation = wsk.parseJsonString(run.stdout).convertTo[CliActivation]
+
+            withClue(s"check failed for activation: $activation") {
+                activation.response.status shouldBe "success"
+                activation.response.result shouldBe Some(testResult)
+                activation.logs shouldBe Some(List())
+            }
+    }
+
+    it should "reject an invoke with the wrong parameters set" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val fullQualifiedName = s"/$guestNamespace/samples/helloWorld"
+            val payload = "bob"
+            val rr = wsk.cli(Seq("action", "invoke", fullQualifiedName, payload) ++ wskprops.overrides,
+                expectedExitCode = TestUtils.ERROR_EXIT)
+            rr.stderr should include("Run 'wsk --help' for usage.")
+            rr.stderr should include(s"error: Invalid argument(s): $payload")
+    }
+
+    it should "not be able to use 'ping' in an action" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "ping"
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("ping.js")))
+            }
+
+            val run = wsk.action.invoke(name, Map("payload" -> "google.com".toJson))
+            withActivation(wsk.activation, run) {
+                activation =>
+                    activation.response.result shouldBe Some(JsObject(
+                        "stderr" -> "ping: icmp open socket: Operation not permitted\n".toJson,
+                        "stdout" -> "".toJson))
+            }
+    }
+
+    ignore should "support UTF-8 as input and output format" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "utf8Test"
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("hello.js")))
+            }
+
+            val utf8 = "«ταБЬℓσö»: 1<2 & 4+1>³, now 20%€§$ off!"
+            val run = wsk.action.invoke(name, Map("payload" -> utf8.toJson))
+            withActivation(wsk.activation, run) {
+                activation =>
+                    activation.response.status shouldBe "success"
+                    activation.logs.get.mkString(" ") should include(s"hello $utf8")
+            }
+    }
+
+}
diff --git a/tests/src/test/scala/system/basic/WskBasicTests.scala b/tests/src/test/scala/system/basic/WskBasicTests.scala
new file mode 100644
index 0000000..f9e2fb9
--- /dev/null
+++ b/tests/src/test/scala/system/basic/WskBasicTests.scala
@@ -0,0 +1,739 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * 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.
+ */
+
+package system.basic
+
+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._
+import common.Wsk
+import common.WskProps
+import common.WskTestHelpers
+import spray.json._
+import spray.json.DefaultJsonProtocol._
+import spray.json.pimpAny
+import scala.concurrent.duration.DurationInt
+import scala.language.postfixOps
+
+@RunWith(classOf[JUnitRunner])
+class WskBasicTests
+    extends TestHelpers
+    with WskTestHelpers {
+
+    implicit val wskprops = WskProps()
+    val wsk = new Wsk
+    val defaultAction = Some(TestUtils.getTestActionFilename("hello.js"))
+
+    behavior of "Wsk CLI"
+
+    it should "confirm wsk exists" in {
+        Wsk.exists
+    }
+
+    it should "show api build details" in {
+        val tmpProps = File.createTempFile("wskprops", ".tmp")
+        try {
+            val env = Map("WSK_CONFIG_FILE" -> tmpProps.getAbsolutePath())
+            wsk.cli(Seq("property", "set", "-i") ++ wskprops.overrides, env = env)
+            val rr = wsk.cli(Seq("property", "get", "--apibuild", "--apibuildno", "-i"), env = env)
+            rr.stderr should not include ("https:///api/v1: http: no Host in request URL")
+            rr.stdout should not include regex("Cannot determine API build")
+            rr.stdout should include regex ("""(?i)whisk API build\s+201.*""")
+            rr.stdout should include regex ("""(?i)whisk API build number\s+.*""")
+        } finally {
+            tmpProps.delete()
+        }
+    }
+
+    it should "reject creating duplicate entity" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "testDuplicateCreate"
+            assetHelper.withCleaner(wsk.trigger, name) {
+                (trigger, _) => trigger.create(name)
+            }
+            assetHelper.withCleaner(wsk.action, name, confirmDelete = false) {
+                (action, _) => action.create(name, defaultAction, expectedExitCode = CONFLICT)
+            }
+    }
+
+    it should "reject deleting entity in wrong collection" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "testCrossDelete"
+            assetHelper.withCleaner(wsk.trigger, name) {
+                (trigger, _) => trigger.create(name)
+            }
+            wsk.action.delete(name, expectedExitCode = CONFLICT)
+    }
+
+    it should "reject unauthenticated access" in {
+        implicit val wskprops = WskProps("xxx") // shadow properties
+        val errormsg = "The supplied authentication is invalid"
+        wsk.namespace.list(expectedExitCode = UNAUTHORIZED).
+            stderr should include(errormsg)
+        wsk.namespace.get(expectedExitCode = UNAUTHORIZED).
+            stderr should include(errormsg)
+    }
+
+    behavior of "Wsk Package CLI"
+
+    it should "create, update, get and list a package" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "testPackage"
+            val params = Map("a" -> "A".toJson)
+            assetHelper.withCleaner(wsk.pkg, name) {
+                (pkg, _) =>
+                    pkg.create(name, parameters = params, shared = Some(true))
+                    pkg.create(name, update = true)
+            }
+            val stdout = wsk.pkg.get(name).stdout
+            stdout should include regex (""""key": "a"""")
+            stdout should include regex (""""value": "A"""")
+            stdout should include regex (""""publish": true""")
+            stdout should include regex (""""version": "0.0.2"""")
+            wsk.pkg.list().stdout should include(name)
+    }
+
+    it should "create, and get a package summary" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val packageName = "packageName"
+            val actionName = "actionName"
+            val packageAnnots = Map(
+                "description" -> JsString("Package description"),
+                "parameters" -> JsArray(
+                    JsObject(
+                        "name" -> JsString("paramName1"),
+                        "description" -> JsString("Parameter description 1")),
+                    JsObject(
+                        "name" -> JsString("paramName2"),
+                        "description" -> JsString("Parameter description 2"))))
+            val actionAnnots = Map(
+                "description" -> JsString("Action description"),
+                "parameters" -> JsArray(
+                    JsObject(
+                        "name" -> JsString("paramName1"),
+                        "description" -> JsString("Parameter description 1")),
+                    JsObject(
+                        "name" -> JsString("paramName2"),
+                        "description" -> JsString("Parameter description 2"))))
+
+            assetHelper.withCleaner(wsk.pkg, packageName) {
+                (pkg, _) =>
+                    pkg.create(packageName, annotations = packageAnnots)
+            }
+
+            wsk.action.create(packageName + "/" + actionName, defaultAction, annotations = actionAnnots)
+            val stdout = wsk.pkg.get(packageName, summary = true).stdout
+            val ns_regex_list = wsk.namespace.list().stdout.trim.replace('\n', '|')
+            wsk.action.delete(packageName + "/" + actionName)
+
+            stdout should include regex (s"(?i)package /${ns_regex_list}/${packageName}: Package description\\s*\\(parameters: paramName1, paramName2\\)")
+            stdout should include regex (s"(?i)action /${ns_regex_list}/${packageName}/${actionName}: Action description\\s*\\(parameters: paramName1, paramName2\\)")
+    }
+
+    it should "create a package with a name that contains spaces" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "package with spaces"
+
+            val res = assetHelper.withCleaner(wsk.pkg, name) {
+                (pkg, _) =>
+                    pkg.create(name)
+            }
+
+            res.stdout should include(s"ok: created package $name")
+    }
+
+    it should "create a package, and get its individual fields" in withAssetCleaner(wskprops) {
+        val name = "packageFields"
+        val paramInput = Map("payload" -> "test".toJson)
+        val successMsg = s"ok: got package $name, displaying field"
+
+        (wp, assetHelper) =>
+            assetHelper.withCleaner(wsk.pkg, name) {
+                (action, _) => action.create(name, parameters = paramInput)
+            }
+
+            val expectedParam = JsObject(
+                "payload" -> JsString("test"))
+
+            val ns_regex_list = wsk.namespace.list().stdout.trim.replace('\n', '|')
+
+            wsk.pkg.get(name, fieldFilter = Some("namespace")).stdout should include regex (s"""(?i)$successMsg namespace\n$ns_regex_list""")
+            wsk.pkg.get(name, fieldFilter = Some("name")).stdout should include(s"""$successMsg name\n"$name"""")
+            wsk.pkg.get(name, fieldFilter = Some("version")).stdout should include(s"""$successMsg version\n"0.0.1"""")
+            wsk.pkg.get(name, fieldFilter = Some("publish")).stdout should include(s"""$successMsg publish\nfalse""")
+            wsk.pkg.get(name, fieldFilter = Some("binding")).stdout should include regex (s"""\\{\\}""")
+            wsk.pkg.get(name, fieldFilter = Some("invalid"), expectedExitCode = ERROR_EXIT).stderr should include("error: Invalid field filter 'invalid'.")
+    }
+
+    behavior of "Wsk Action CLI"
+
+    it should "create the same action twice with different cases" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            assetHelper.withCleaner(wsk.action, "TWICE") { (action, name) => action.create(name, defaultAction) }
+            assetHelper.withCleaner(wsk.action, "twice") { (action, name) => action.create(name, defaultAction) }
+    }
+
+    it should "create an action, then update its kind" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "createAndUpdate"
+            val file = Some(TestUtils.getTestActionFilename("hello.js"))
+
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, file, kind = Some("nodejs"))
+            }
+
+            // create action as nodejs (v0.12)
+            wsk.action.get(name).stdout should include regex (""""kind": "nodejs"""")
+
+            // update to nodejs:6
+            wsk.action.create(name, file, kind = Some("nodejs:6"), update = true)
+            wsk.action.get(name).stdout should include regex (""""kind": "nodejs:6"""")
+    }
+
+    it should "create, update, get and list an action" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "createAndUpdate"
+            val file = Some(TestUtils.getTestActionFilename("hello.js"))
+            val params = Map("a" -> "A".toJson)
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) =>
+                    action.create(name, file, parameters = params)
+                    action.create(name, None, parameters = Map("b" -> "B".toJson), update = true)
+            }
+            val stdout = wsk.action.get(name).stdout
+            stdout should not include regex(""""key": "a"""")
+            stdout should not include regex(""""value": "A"""")
+            stdout should include regex (""""key": "b""")
+            stdout should include regex (""""value": "B"""")
+            stdout should include regex (""""publish": false""")
+            stdout should include regex (""""version": "0.0.2"""")
+            wsk.action.list().stdout should include(name)
+    }
+
+    it should "reject delete of action that does not exist" in {
+        wsk.action.sanitize("deleteFantasy").
+            stderr should include regex ("""The requested resource does not exist. \(code \d+\)""")
+    }
+
+    it should "create, and invoke an action that utilizes a docker container" in withAssetCleaner(wskprops) {
+        val name = "dockerContainer"
+        (wp, assetHelper) =>
+            assetHelper.withCleaner(wsk.action, name) {
+                // this docker image will be need to be pulled from dockerhub and hence has to be published there first
+                (action, _) => action.create(name, Some("openwhisk/example"), kind = Some("docker"))
+            }
+
+            val args = Map("payload" -> "test".toJson)
+            val run = wsk.action.invoke(name, args)
+            withActivation(wsk.activation, run) {
+                activation =>
+                    activation.response.result shouldBe Some(JsObject(
+                        "args" -> args.toJson,
+                        "msg" -> "Hello from arbitrary C program!".toJson))
+            }
+    }
+
+    it should "create, and invoke an action that utilizes dockerskeleton with native zip" in withAssetCleaner(wskprops) {
+        val name = "dockerContainerWithZip"
+        (wp, assetHelper) =>
+            assetHelper.withCleaner(wsk.action, name) {
+                // this docker image will be need to be pulled from dockerhub and hence has to be published there first
+                (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("blackbox.zip")), kind = Some("docker"))
+            }
+
+            val run = wsk.action.invoke(name, Map())
+            withActivation(wsk.activation, run) {
+                activation =>
+                    activation.response.result shouldBe Some(JsObject(
+                        "msg" -> "hello zip".toJson))
+                    activation.logs shouldBe defined
+                    val logs = activation.logs.get.toString
+                    logs should include("This is an example zip used with the docker skeleton action.")
+                    logs should not include ("XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX")
+            }
+    }
+
+    it should "create, and invoke an action using a parameter file" in withAssetCleaner(wskprops) {
+        val name = "paramFileAction"
+        val file = Some(TestUtils.getTestActionFilename("argCheck.js"))
+        val argInput = Some(TestUtils.getTestActionFilename("validInput2.json"))
+
+        (wp, assetHelper) =>
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, file)
+            }
+
+            val expectedOutput = JsObject(
+                "payload" -> JsString("test"))
+            val run = wsk.action.invoke(name, parameterFile = argInput)
+            withActivation(wsk.activation, run) {
+                activation =>
+                    activation.response.result shouldBe Some(expectedOutput)
+            }
+    }
+
+    it should "create an action, and get its individual fields" in withAssetCleaner(wskprops) {
+        val name = "actionFields"
+        val paramInput = Map("payload" -> "test".toJson)
+        val successMsg = s"ok: got action $name, displaying field"
+
+        (wp, assetHelper) =>
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, defaultAction, parameters = paramInput)
+            }
+
+            val expectedParam = JsObject(
+                "payload" -> JsString("test"))
+
+            val ns_regex_list = wsk.namespace.list().stdout.trim.replace('\n', '|')
+
+            wsk.action.get(name, fieldFilter = Some("name")).stdout should include(s"""$successMsg name\n"$name"""")
+            wsk.action.get(name, fieldFilter = Some("version")).stdout should include(s"""$successMsg version\n"0.0.1"""")
+            wsk.action.get(name, fieldFilter = Some("exec")).stdout should include regex (s"""$successMsg exec\n\\{\\s+"kind":\\s+"nodejs:6",\\s+"code":\\s+"\\/\\*\\*[\\\\r]*\\\\n \\* Hello, world.[\\\\r]*\\\\n \\*\\/[\\\\r]*\\\\nfunction main\\(params\\) \\{[\\\\r]*\\\\n    console.log\\('hello', params.payload\\+'!'\\);[\\\\r]*\\\\n\\}[\\\\r]*\\\\n"\n\\}""")
+            wsk.action.get(name, fieldFilter = Some("parameters")).stdout should include regex (s"""$successMsg parameters\n\\[\\s+\\{\\s+"key":\\s+"payload",\\s+"value":\\s+"test"\\s+\\}\\s+\\]""")
+            wsk.action.get(name, fieldFilter = Some("annotations")).stdout should include regex (s"""$successMsg annotations\n\\[\\s+\\{\\s+"key":\\s+"exec",\\s+"value":\\s+"nodejs:6"\\s+\\}\\s+\\]""")
+            wsk.action.get(name, fieldFilter = Some("limits")).stdout should include regex (s"""$successMsg limits\n\\{\\s+"timeout":\\s+60000,\\s+"memory":\\s+256,\\s+"logs":\\s+10\\s+\\}""")
+            wsk.action.get(name, fieldFilter = Some("namespace")).stdout should include regex (s"""(?i)$successMsg namespace\n$ns_regex_list""")
+            wsk.action.get(name, fieldFilter = Some("invalid"), expectedExitCode = ERROR_EXIT).stderr should include("error: Invalid field filter 'invalid'.")
+            wsk.action.get(name, fieldFilter = Some("publish")).stdout should include(s"""$successMsg publish\nfalse""")
+    }
+
+    /**
+     * Tests creating an action from a malformed js file. This should fail in
+     * some way - preferably when trying to create the action. If not, then
+     * surely when it runs there should be some indication in the logs. Don't
+     * think this is true currently.
+     */
+    it should "create and invoke action with malformed js resulting in activation error" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "MALFORMED"
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("malformed.js")))
+            }
+
+            val run = wsk.action.invoke(name, Map("payload" -> "whatever".toJson))
+            withActivation(wsk.activation, run) {
+                activation =>
+                    activation.response.status shouldBe "action developer error"
+                    // representing nodejs giving an error when given malformed.js
+                    activation.response.result.get.toString should include("ReferenceError")
+            }
+    }
+
+    it should "create and invoke a blocking action resulting in an application error response" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "applicationError"
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("applicationError.js")))
+            }
+
+            wsk.action.invoke(name, blocking = true, expectedExitCode = 246)
+                .stderr should include regex (""""error": "This error thrown on purpose by the action."""")
+    }
+
+    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")))
+            }
+
+            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) {
+        (wp, assetHelper) =>
+            val name = "basicInvoke"
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("wc.js")))
+            }
+            wsk.action.invoke(name, Map("payload" -> "one two three".toJson), blocking = true, result = true)
+                .stdout should include regex (""""count": 3""")
+    }
+
+    it should "create, and get an action summary" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "actionName"
+            val annots = Map(
+                "description" -> JsString("Action description"),
+                "parameters" -> JsArray(
+                    JsObject(
+                        "name" -> JsString("paramName1"),
+                        "description" -> JsString("Parameter description 1")),
+                    JsObject(
+                        "name" -> JsString("paramName2"),
+                        "description" -> JsString("Parameter description 2"))))
+
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) =>
+                    action.create(name, defaultAction, annotations = annots)
+            }
+
+            val stdout = wsk.action.get(name, summary = true).stdout
+            val ns_regex_list = wsk.namespace.list().stdout.trim.replace('\n', '|')
+
+            stdout should include regex (s"(?i)action /${ns_regex_list}/${name}: Action description\\s*\\(parameters: paramName1, paramName2\\)")
+    }
+
+    it should "create an action with a name that contains spaces" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "action with spaces"
+
+            val res = assetHelper.withCleaner(wsk.action, name) {
+                (action, _) =>
+                    action.create(name, defaultAction)
+            }
+
+            res.stdout should include(s"ok: created action $name")
+    }
+
+    it should "create an action, and invoke an action that returns an empty JSON object" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "emptyJSONAction"
+
+            val res = assetHelper.withCleaner(wsk.action, name) {
+                (action, _) =>
+                    action.create(name, Some(TestUtils.getTestActionFilename("emptyJSONResult.js")))
+                    action.invoke(name, blocking = true, result = true)
+            }
+
+            res.stdout shouldBe ("{}\n")
+    }
+
+    it should "create, and invoke an action that times out to ensure the result is empty" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "sleepAction"
+            val params = Map("payload" -> "100000".toJson)
+            val allowedActionDuration = 120 seconds
+            val res = assetHelper.withCleaner(wsk.action, name) {
+                (action, _) =>
+                    action.create(name, Some(TestUtils.getTestActionFilename("timeout.js")),
+                        timeout = Some(allowedActionDuration))
+                    action.invoke(name, parameters = params, blocking = true, result = true)
+            }
+
+            res.stdout should include regex (s"""\\{\\s+"activationId":\\s+"[a-z0-9]{32}"\\s+\\}""")
+    }
+
+    it should "create, and get docker action get ensure exec code is omitted" in withAssetCleaner(wskprops) {
+        val name = "dockerContainer"
+        (wp, assetHelper) =>
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, Some("fakeContainer"), kind = Some("docker"))
+            }
+
+            wsk.action.get(name).stdout should not include (""""code"""")
+    }
+
+    behavior of "Wsk Trigger CLI"
+
+    it should "create, update, get, fire and list trigger" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "listTriggers"
+            val params = Map("a" -> "A".toJson)
+            assetHelper.withCleaner(wsk.trigger, name) {
+                (trigger, _) =>
+                    trigger.create(name, parameters = params)
+                    trigger.create(name, update = true)
+            }
+            val stdout = wsk.trigger.get(name).stdout
+            stdout should include regex (""""key": "a"""")
+            stdout should include regex (""""value": "A"""")
+            stdout should include regex (""""publish": false""")
+            stdout should include regex (""""version": "0.0.2"""")
+
+            val dynamicParams = Map("t" -> "T".toJson)
+            val run = wsk.trigger.fire(name, dynamicParams)
+            withActivation(wsk.activation, run) {
+                activation =>
+                    activation.response.result shouldBe Some(dynamicParams.toJson)
+                    activation.duration shouldBe 0L // shouldn't exist but CLI generates it
+                    activation.end shouldBe Instant.EPOCH // shouldn't exist but CLI generates it
+            }
+
+            val runWithNoParams = wsk.trigger.fire(name, Map())
+            withActivation(wsk.activation, runWithNoParams) {
+                activation =>
+                    activation.response.result shouldBe Some(JsObject())
+                    activation.duration shouldBe 0L // shouldn't exist but CLI generates it
+                    activation.end shouldBe Instant.EPOCH // shouldn't exist but CLI generates it
+            }
+
+            wsk.trigger.list().stdout should include(name)
+    }
+
+    it should "create, and get a trigger summary" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "triggerName"
+            val annots = Map(
+                "description" -> JsString("Trigger description"),
+                "parameters" -> JsArray(
+                    JsObject(
+                        "name" -> JsString("paramName1"),
+                        "description" -> JsString("Parameter description 1")),
+                    JsObject(
+                        "name" -> JsString("paramName2"),
+                        "description" -> JsString("Parameter description 2"))))
+
+            assetHelper.withCleaner(wsk.trigger, name) {
+                (trigger, _) =>
+                    trigger.create(name, annotations = annots)
+            }
+
+            val stdout = wsk.trigger.get(name, summary = true).stdout
+            val ns_regex_list = wsk.namespace.list().stdout.trim.replace('\n', '|')
+
+            stdout should include regex (s"trigger /${ns_regex_list}/${name}: Trigger description\\s*\\(parameters: paramName1, paramName2\\)")
+    }
+
+    it should "create a trigger with a name that contains spaces" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "trigger with spaces"
+
+            val res = assetHelper.withCleaner(wsk.trigger, name) {
+                (trigger, _) =>
+                    trigger.create(name)
+            }
+
+            res.stdout should include regex (s"ok: created trigger $name")
+    }
+
+    it should "create, and fire a trigger using a parameter file" in withAssetCleaner(wskprops) {
+        val name = "paramFileTrigger"
+        val file = Some(TestUtils.getTestActionFilename("argCheck.js"))
+        val argInput = Some(TestUtils.getTestActionFilename("validInput2.json"))
+
+        (wp, assetHelper) =>
+            assetHelper.withCleaner(wsk.trigger, name) {
+                (trigger, _) =>
+                    trigger.create(name)
+            }
+
+            val expectedOutput = JsObject(
+                "payload" -> JsString("test"))
+            val run = wsk.trigger.fire(name, parameterFile = argInput)
+            withActivation(wsk.activation, run) {
+                activation =>
+                    activation.response.result shouldBe Some(expectedOutput)
+            }
+    }
+
+    it should "create a trigger, and get its individual fields" in withAssetCleaner(wskprops) {
+        val name = "triggerFields"
+        val paramInput = Map("payload" -> "test".toJson)
+        val successMsg = s"ok: got trigger $name, displaying field"
+
+        (wp, assetHelper) =>
+            assetHelper.withCleaner(wsk.trigger, name) {
+                (trigger, _) =>
+                    trigger.create(name, parameters = paramInput)
+            }
+
+            val expectedParam = JsObject(
+                "payload" -> JsString("test"))
+
+            val ns_regex_list = wsk.namespace.list().stdout.trim.replace('\n', '|')
+
+            wsk.trigger.get(name, fieldFilter = Some("namespace")).stdout should include regex (s"""(?i)$successMsg namespace\n$ns_regex_list""")
+            wsk.trigger.get(name, fieldFilter = Some("name")).stdout should include(s"""$successMsg name\n"$name"""")
+            wsk.trigger.get(name, fieldFilter = Some("version")).stdout should include(s"""$successMsg version\n"0.0.1"""")
+            wsk.trigger.get(name, fieldFilter = Some("publish")).stdout should include(s"""$successMsg publish\nfalse""")
+            wsk.trigger.get(name, fieldFilter = Some("annotations")).stdout should include(s"""$successMsg annotations\n[]""")
+            wsk.trigger.get(name, fieldFilter = Some("parameters")).stdout should include regex (s"""$successMsg parameters\n\\[\\s+\\{\\s+"key":\\s+"payload",\\s+"value":\\s+"test"\\s+\\}\\s+\\]""")
+            wsk.trigger.get(name, fieldFilter = Some("limits")).stdout should include(s"""$successMsg limits\n{}""")
+            wsk.trigger.get(name, fieldFilter = Some("invalid"), expectedExitCode = ERROR_EXIT).stderr should include("error: Invalid field filter 'invalid'.")
+    }
+
+    it should "create, and fire a trigger to ensure result is empty" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "emptyResultTrigger"
+            assetHelper.withCleaner(wsk.trigger, name) {
+                (trigger, _) =>
+                    trigger.create(name)
+            }
+
+            val run = wsk.trigger.fire(name)
+            withActivation(wsk.activation, run) {
+                activation =>
+                    activation.response.result shouldBe Some(JsObject())
+            }
+    }
+
+    behavior of "Wsk Rule CLI"
+
+    it should "create rule, get rule, update rule and list rule" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val ruleName = "listRules"
+            val triggerName = "listRulesTrigger"
+            val actionName = "listRulesAction";
+            assetHelper.withCleaner(wsk.trigger, triggerName) {
+                (trigger, name) => trigger.create(name)
+            }
+            assetHelper.withCleaner(wsk.action, actionName) {
+                (action, name) => action.create(name, defaultAction)
+            }
+            assetHelper.withCleaner(wsk.rule, ruleName) {
+                (rule, name) =>
+                    rule.create(name, trigger = triggerName, action = actionName)
+            }
+
+            // finally, we perform the update, and expect success this time
+            wsk.rule.create(ruleName, trigger = triggerName, action = actionName, update = true)
+
+            val stdout = wsk.rule.get(ruleName).stdout
+            stdout should include(ruleName)
+            stdout should include(triggerName)
+            stdout should include(actionName)
+            stdout should include regex (""""version": "0.0.2"""")
+            wsk.rule.list().stdout should include(ruleName)
+    }
+
+    it should "create rule, get rule, ensure rule is enabled by default" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val ruleName = "enabledRule"
+            val triggerName = "enabledRuleTrigger"
+            val actionName = "enabledRuleAction";
+            assetHelper.withCleaner(wsk.trigger, triggerName) {
+                (trigger, name) => trigger.create(name)
+            }
+            assetHelper.withCleaner(wsk.action, actionName) {
+                (action, name) => action.create(name, defaultAction)
+            }
+            assetHelper.withCleaner(wsk.rule, ruleName) {
+                (rule, name) =>
+                    rule.create(name, trigger = triggerName, action = actionName)
+            }
+
+            val stdout = wsk.rule.get(ruleName).stdout
+            stdout should include regex (""""status":\s*"active"""")
+    }
+
+    it should "display a rule summary when --summary flag is used with 'wsk rule get'" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val ruleName = "mySummaryRule"
+            val triggerName = "summaryRuleTrigger"
+            val actionName = "summaryRuleAction";
+            assetHelper.withCleaner(wsk.trigger, triggerName) {
+                (trigger, name) => trigger.create(name)
+            }
+            assetHelper.withCleaner(wsk.action, actionName) {
+                (action, name) => action.create(name, defaultAction)
+            }
+            assetHelper.withCleaner(wsk.rule, ruleName, confirmDelete = false) {
+                (rule, name) => rule.create(name, trigger = triggerName, action = actionName)
+            }
+            // Summary namespace should match one of the allowable namespaces (typically 'guest')
+            val ns_regex_list = wsk.namespace.list().stdout.trim.replace('\n', '|')
+            val stdout = wsk.rule.get(ruleName, summary = true).stdout
+
+            stdout should include regex (s"(?i)rule /${ns_regex_list}/${ruleName}\\s*\\(status: active\\)")
+    }
+
+    it should "create a rule, and get its individual fields" in withAssetCleaner(wskprops) {
+        val ruleName = "ruleFields"
+        val triggerName = "ruleTriggerFields"
+        val actionName = "ruleActionFields"; val paramInput = Map("payload" -> "test".toJson)
+        val successMsg = s"ok: got rule $ruleName, displaying field"
+
+        (wp, assetHelper) =>
+
+            assetHelper.withCleaner(wsk.trigger, triggerName) {
+                (trigger, name) => trigger.create(name)
+            }
+            assetHelper.withCleaner(wsk.action, actionName) {
+                (action, name) => action.create(name, defaultAction)
+            }
+            assetHelper.withCleaner(wsk.rule, ruleName) {
+                (rule, name) =>
+                    rule.create(name, trigger = triggerName, action = actionName)
+            }
+
+            val ns_regex_list = wsk.namespace.list().stdout.trim.replace('\n', '|')
+
+            wsk.rule.get(ruleName, fieldFilter = Some("namespace")).stdout should include regex (s"""(?i)$successMsg namespace\n$ns_regex_list""")
+            wsk.rule.get(ruleName, fieldFilter = Some("name")).stdout should include(s"""$successMsg name\n"$ruleName"""")
+            wsk.rule.get(ruleName, fieldFilter = Some("version")).stdout should include(s"""$successMsg version\n"0.0.1"\n""")
+            wsk.rule.get(ruleName, fieldFilter = Some("status")).stdout should include(s"""$successMsg status\n"active"""")
+            val trigger = wsk.rule.get(ruleName, fieldFilter = Some("trigger")).stdout
+            trigger should include regex (s"""$successMsg trigger\n""")
+            trigger should include(triggerName)
+            trigger should not include (actionName)
+            val action = wsk.rule.get(ruleName, fieldFilter = Some("action")).stdout
+            action should include regex (s"""$successMsg action\n""")
+            action should include(actionName)
+            action should not include (triggerName)
+    }
+
+    behavior of "Wsk Namespace CLI"
+
+    it should "return a list of exactly one namespace" in {
+        wsk.namespace.list().
+            stdout.lines should have size 2 // headline + namespace
+    }
+
+    it should "list entities in default namespace" in {
+        // use a fresh wsk props instance that is guaranteed to use
+        // the default namespace
+        wsk.namespace.get(expectedExitCode = SUCCESS_EXIT)(WskProps()).
+            stdout should include("default")
+    }
+
+    it should "not list entities with an invalid namespace" in {
+        val namespace = "fakeNamespace"
+        val stderr = wsk.namespace.get(Some(s"/${namespace}"), expectedExitCode = FORBIDDEN).stderr
+
+        stderr should include(s"Unable to obtain the list of entities for namespace '${namespace}'")
+    }
+
+    behavior of "Wsk Activation CLI"
+
+    it should "create a trigger, and fire a trigger to get its individual fields from an activation" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "activationFields"
+
+            assetHelper.withCleaner(wsk.trigger, name) {
+                (trigger, _) =>
+                    trigger.create(name)
+            }
+
+            val ns_regex_list = wsk.namespace.list().stdout.trim.replace('\n', '|')
+
+            val run = wsk.trigger.fire(name)
+            withActivation(wsk.activation, run) {
+                activation =>
+                    val successMsg = s"ok: got activation ${activation.activationId}, displaying field"
+                    wsk.activation.get(activation.activationId, fieldFilter = Some("namespace")).stdout should include regex (s"""(?i)$successMsg namespace\n$ns_regex_list""")
+                    wsk.activation.get(activation.activationId, fieldFilter = Some("name")).stdout should include(s"""$successMsg name\n"$name"""")
+                    wsk.activation.get(activation.activationId, fieldFilter = Some("version")).stdout should include(s"""$successMsg version\n"0.0.1"""")
+                    wsk.activation.get(activation.activationId, fieldFilter = Some("publish")).stdout should include(s"""$successMsg publish\nfalse""")
+                    wsk.activation.get(activation.activationId, fieldFilter = Some("subject")).stdout should include regex (s"""(?i)$successMsg subject\n""")
+                    wsk.activation.get(activation.activationId, fieldFilter = Some("activationid")).stdout should include(s"""$successMsg activationid\n"${activation.activationId}""")
+                    wsk.activation.get(activation.activationId, fieldFilter = Some("start")).stdout should include regex (s"""$successMsg start\n\\d""")
+                    wsk.activation.get(activation.activationId, fieldFilter = Some("end")).stdout should include regex (s"""$successMsg end\n\\d""")
+                    wsk.activation.get(activation.activationId, fieldFilter = Some("duration")).stdout should include regex (s"""$successMsg duration\n\\d""")
+                    wsk.activation.get(activation.activationId, fieldFilter = Some("annotations")).stdout should include(s"""$successMsg annotations\n[]""")
+            }
+    }
+}
diff --git a/tests/src/test/scala/system/basic/WskRuleTests.scala b/tests/src/test/scala/system/basic/WskRuleTests.scala
new file mode 100644
index 0000000..aef370d
--- /dev/null
+++ b/tests/src/test/scala/system/basic/WskRuleTests.scala
@@ -0,0 +1,360 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * 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.
+ */
+
+package system.basic
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+import common.TestHelpers
+import common.TestUtils
+import common.Wsk
+import common.WskProps
+import common.WskTestHelpers
+import spray.json._
+import spray.json.DefaultJsonProtocol._
+import java.time.Instant
+
+@RunWith(classOf[JUnitRunner])
+class WskRuleTests
+    extends TestHelpers
+    with WskTestHelpers {
+
+    implicit val wskprops = WskProps()
+    val wsk = new Wsk
+    val defaultAction = TestUtils.getTestActionFilename("wc.js")
+    val secondAction = TestUtils.getTestActionFilename("hello.js")
+    val testString = "this is a test"
+    val testResult = JsObject("count" -> testString.split(" ").length.toJson)
+
+    /**
+     * Sets up trigger -> rule -> action triplets. Deduplicates triggers and rules
+     * and links it all up.
+     *
+     * @param rules Tuple3s containing
+     *   (rule, trigger, (action name for created action, action name for the rule binding, actionFile))
+     *   where the action name for the created action is allowed to differ from that used by the rule binding
+     *   for cases that reference actions in a package binding.
+     */
+    def ruleSetup(rules: Seq[(String, String, (String, String, String))], assetHelper: AssetCleaner) = {
+        val triggers = rules.map(_._2).distinct
+        val actions = rules.map(_._3).distinct
+
+        triggers.foreach { trigger =>
+            assetHelper.withCleaner(wsk.trigger, trigger) {
+                (trigger, name) => trigger.create(name)
+            }
+        }
+
+        actions.foreach {
+            case (actionName, _, file) =>
+                assetHelper.withCleaner(wsk.action, actionName) {
+                    (action, name) => action.create(name, Some(file))
+                }
+        }
+
+        rules.foreach {
+            case (ruleName, triggerName, action) =>
+                assetHelper.withCleaner(wsk.rule, ruleName) {
+                    (rule, name) => rule.create(name, triggerName, action._2)
+                }
+        }
+    }
+
+    behavior of "Whisk rules"
+
+    it should "invoke the action attached on trigger fire, creating an activation for each entity including the cause" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val ruleName = "r1to1"
+            val triggerName = "t1to1"
+            val actionName = "a1 to 1" // spaces in name intended for greater test coverage
+
+            ruleSetup(Seq(
+                (ruleName, triggerName, (actionName, actionName, defaultAction))),
+                assetHelper)
+
+            val run = wsk.trigger.fire(triggerName, Map("payload" -> testString.toJson))
+
+            withActivation(wsk.activation, run) {
+                triggerActivation =>
+                    triggerActivation.cause shouldBe None
+
+                    withActivationsFromEntity(wsk.activation, ruleName, since = Some(triggerActivation.start)) {
+                        _.head.cause shouldBe Some(triggerActivation.activationId)
+                    }
+
+                    withActivationsFromEntity(wsk.activation, actionName, since = Some(triggerActivation.start)) { activationList =>
+                        activationList.head.response.result shouldBe Some(testResult)
+                        activationList.head.cause shouldBe None
+                    }
+            }
+    }
+
+    it should "invoke the action from a package attached on trigger fire, creating an activation for each entity including the cause" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val ruleName = "pr1to1"
+            val triggerName = "pt1to1"
+            val pkgName = "rule pkg" // spaces in name intended to test uri path encoding
+            val actionName = "a1 to 1"
+            val pkgActionName = s"$pkgName/$actionName"
+
+            assetHelper.withCleaner(wsk.pkg, pkgName) {
+                (pkg, name) => pkg.create(name)
+            }
+
+            ruleSetup(Seq(
+                (ruleName, triggerName, (pkgActionName, pkgActionName, defaultAction))),
+                assetHelper)
+
+            val now = Instant.now
+            val run = wsk.trigger.fire(triggerName, Map("payload" -> testString.toJson))
+
+            withActivation(wsk.activation, run) {
+                triggerActivation =>
+                    triggerActivation.cause shouldBe None
+
+                    withActivationsFromEntity(wsk.activation, ruleName, since = Some(triggerActivation.start)) {
+                        _.head.cause shouldBe Some(triggerActivation.activationId)
+                    }
+
+                    withActivationsFromEntity(wsk.activation, actionName, since = Some(triggerActivation.start)) {
+                        _.head.response.result shouldBe Some(testResult)
+                    }
+            }
+    }
+
+    it should "invoke the action from a package binding attached on trigger fire, creating an activation for each entity including the cause" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val ruleName = "pr1to1"
+            val triggerName = "pt1to1"
+            val pkgName = "rule pkg" // spaces in name intended to test uri path encoding
+            val pkgBindingName = "rule pkg binding"
+            val actionName = "a1 to 1"
+            val pkgActionName = s"$pkgName/$actionName"
+
+            assetHelper.withCleaner(wsk.pkg, pkgName) {
+                (pkg, name) => pkg.create(name)
+            }
+
+            assetHelper.withCleaner(wsk.pkg, pkgBindingName) {
+                (pkg, name) => pkg.bind(pkgName, pkgBindingName)
+            }
+
+            ruleSetup(Seq(
+                (ruleName, triggerName, (pkgActionName, s"$pkgBindingName/$actionName", defaultAction))),
+                assetHelper)
+
+            val run = wsk.trigger.fire(triggerName, Map("payload" -> testString.toJson))
+
+            withActivation(wsk.activation, run) {
+                triggerActivation =>
+                    triggerActivation.cause shouldBe None
+
+                    withActivationsFromEntity(wsk.activation, ruleName, since = Some(triggerActivation.start)) {
+                        _.head.cause shouldBe Some(triggerActivation.activationId)
+                    }
+
+                    withActivationsFromEntity(wsk.activation, actionName, since = Some(triggerActivation.start)) {
+                        _.head.response.result shouldBe Some(testResult)
+                    }
+            }
+    }
+
+    it should "not activate an action if the rule is deleted when the trigger is fired" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val ruleName = "ruleDelete"
+            val triggerName = "ruleDeleteTrigger"
+            val actionName = "ruleDeleteAction"
+
+            assetHelper.withCleaner(wsk.trigger, triggerName) {
+                (trigger, name) => trigger.create(name)
+            }
+            assetHelper.withCleaner(wsk.action, actionName) {
+                (action, name) => action.create(name, Some(defaultAction))
+            }
+            assetHelper.withCleaner(wsk.rule, ruleName, false) {
+                (rule, name) => rule.create(name, triggerName, actionName)
+            }
+
+            val first = wsk.trigger.fire(triggerName, Map("payload" -> "bogus".toJson))
+            wsk.rule.delete(ruleName)
+            wsk.trigger.fire(triggerName, Map("payload" -> "bogus2".toJson))
+
+            withActivation(wsk.activation, first) {
+                activation =>
+                    // tries to find 2 activations for the action, should only find 1
+                    val activations = wsk.activation.pollFor(2, Some(actionName), since = Some(activation.start), retries = 30)
+
+                    activations.length shouldBe 1
+            }
+    }
+
+    it should "enable and disable a rule and check action is activated only when rule is enabled" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val ruleName = "ruleDisable"
+            val triggerName = "ruleDisableTrigger"
+            val actionName = "ruleDisableAction"
+
+            ruleSetup(Seq(
+                (ruleName, triggerName, (actionName, actionName, defaultAction))),
+                assetHelper)
+
+            val first = wsk.trigger.fire(triggerName, Map("payload" -> testString.toJson))
+            wsk.rule.disableRule(ruleName)
+            wsk.trigger.fire(triggerName, Map("payload" -> s"$testString with added words".toJson))
+            wsk.rule.enableRule(ruleName)
+            wsk.trigger.fire(triggerName, Map("payload" -> testString.toJson))
+
+            withActivation(wsk.activation, first) {
+                triggerActivation =>
+                    withActivationsFromEntity(wsk.activation, actionName, N = 2, since = Some(triggerActivation.start)) {
+                        activations =>
+                            val results = activations.map(_.response.result)
+                            results should contain theSameElementsAs Seq(Some(testResult), Some(testResult))
+                    }
+            }
+    }
+
+    it should "be able to recreate a rule with the same name and match it successfully" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val ruleName = "ruleRecreate"
+            val triggerName1 = "ruleRecreateTrigger1"
+            val triggerName2 = "ruleRecreateTrigger2"
+            val actionName = "ruleRecreateAction"
+
+            assetHelper.withCleaner(wsk.trigger, triggerName1) {
+                (trigger, name) => trigger.create(name)
+            }
+            assetHelper.withCleaner(wsk.action, actionName) {
+                (action, name) => action.create(name, Some(defaultAction))
+            }
+            assetHelper.withCleaner(wsk.rule, ruleName, false) {
+                (rule, name) => rule.create(name, triggerName1, actionName)
+            }
+
+            wsk.rule.delete(ruleName)
+
+            assetHelper.withCleaner(wsk.trigger, triggerName2) {
+                (trigger, name) => trigger.create(name)
+            }
+            assetHelper.withCleaner(wsk.rule, ruleName) {
+                (rule, name) => rule.create(name, triggerName2, actionName)
+            }
+
+            val first = wsk.trigger.fire(triggerName2, Map("payload" -> testString.toJson))
+            withActivation(wsk.activation, first) {
+                triggerActivation =>
+                    withActivationsFromEntity(wsk.activation, actionName, since = Some(triggerActivation.start)) {
+                        _.head.response.result shouldBe Some(testResult)
+                    }
+            }
+    }
+
+    it should "connect two triggers via rules to one action and activate it accordingly" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val triggerName1 = "t2to1a"
+            val triggerName2 = "t2to1b"
+            val actionName = "a2to1"
+
+            ruleSetup(Seq(
+                ("r2to1a", triggerName1, (actionName, actionName, defaultAction)),
+                ("r2to1b", triggerName2, (actionName, actionName, defaultAction))),
+                assetHelper)
+
+            val testPayloads = Seq("got three words", "got four words, period")
+
+            val run = wsk.trigger.fire(triggerName1, Map("payload" -> testPayloads(0).toJson))
+            wsk.trigger.fire(triggerName2, Map("payload" -> testPayloads(1).toJson))
+
+            withActivation(wsk.activation, run) {
+                triggerActivation =>
+                    withActivationsFromEntity(wsk.activation, actionName, N = 2, since = Some(triggerActivation.start)) {
+                        activations =>
+                            val results = activations.map(_.response.result)
+                            val expectedResults = testPayloads.map { payload =>
+                                Some(JsObject("count" -> payload.split(" ").length.toJson))
+                            }
+
+                            results should contain theSameElementsAs expectedResults
+                    }
+            }
+    }
+
+    it should "connect one trigger to two different actions, invoking them both eventually" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val triggerName = "t1to2"
+            val actionName1 = "a1to2a"
+            val actionName2 = "a1to2b"
+
+            ruleSetup(Seq(
+                ("r1to2a", triggerName, (actionName1, actionName1, defaultAction)),
+                ("r1to2b", triggerName, (actionName2, actionName2, secondAction))),
+                assetHelper)
+
+            val run = wsk.trigger.fire(triggerName, Map("payload" -> testString.toJson))
+
+            withActivation(wsk.activation, run) {
+                triggerActivation =>
+                    withActivationsFromEntity(wsk.activation, actionName1, since = Some(triggerActivation.start)) {
+                        _.head.response.result shouldBe Some(testResult)
+                    }
+                    withActivationsFromEntity(wsk.activation, actionName2, since = Some(triggerActivation.start)) {
+                        _.head.logs.get.mkString(" ") should include(s"hello $testString")
+                    }
+            }
+    }
+
+    it should "connect two triggers to two different actions, invoking them both eventually" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val triggerName1 = "t1to1a"
+            val triggerName2 = "t1to1b"
+            val actionName1 = "a1to1a"
+            val actionName2 = "a1to1b"
+
+            ruleSetup(Seq(
+                ("r2to2a", triggerName1, (actionName1, actionName1, defaultAction)),
+                ("r2to2b", triggerName1, (actionName2, actionName2, secondAction)),
+                ("r2to2c", triggerName2, (actionName1, actionName1, defaultAction)),
+                ("r2to2d", triggerName2, (actionName2, actionName2, secondAction))),
+                assetHelper)
+
+            val testPayloads = Seq("got three words", "got four words, period")
+            val run = wsk.trigger.fire(triggerName1, Map("payload" -> testPayloads(0).toJson))
+            wsk.trigger.fire(triggerName2, Map("payload" -> testPayloads(1).toJson))
+
+            withActivation(wsk.activation, run) {
+                triggerActivation =>
+                    withActivationsFromEntity(wsk.activation, actionName1, N = 2, since = Some(triggerActivation.start)) {
+                        activations =>
+                            val results = activations.map(_.response.result)
+                            val expectedResults = testPayloads.map { payload =>
+                                Some(JsObject("count" -> payload.split(" ").length.toJson))
+                            }
+
+                            results should contain theSameElementsAs expectedResults
+                    }
+                    withActivationsFromEntity(wsk.activation, actionName2, N = 2, since = Some(triggerActivation.start)) {
+                        activations =>
+                            // drops the leftmost 39 characters (timestamp + streamname)
+                            val logs = activations.map(_.logs.get.map(_.drop(39))).flatten
+                            val expectedLogs = testPayloads.map { payload => s"hello $payload!" }
+
+                            logs should contain theSameElementsAs expectedLogs
+                    }
+            }
+    }
+
+}
diff --git a/tests/src/test/scala/system/basic/WskSdkTests.scala b/tests/src/test/scala/system/basic/WskSdkTests.scala
new file mode 100644
index 0000000..0c79349
--- /dev/null
+++ b/tests/src/test/scala/system/basic/WskSdkTests.scala
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * 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.
+ */
+
+package system.basic
+
+import java.io.File
+
+import scala.collection.JavaConversions.asScalaBuffer
+
+import org.apache.commons.io.FileUtils
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+import common.TestHelpers
+import common.TestUtils.SUCCESS_EXIT
+import common.WhiskProperties
+import common.Wsk
+import common.WskProps
+import common.WskTestHelpers
+
+@RunWith(classOf[JUnitRunner])
+class WskSdkTests
+    extends TestHelpers
+    with WskTestHelpers {
+
+    implicit val wskprops = WskProps()
+    val wsk = new Wsk
+
+    behavior of "Wsk SDK"
+
+    it should "download docker action sdk" in {
+        val dir = File.createTempFile("wskinstall", ".tmp")
+        dir.delete()
+        dir.mkdir() should be(true)
+        try {
+            wsk.cli(wskprops.overrides ++ Seq("sdk", "install", "docker"), workingDir = dir).
+                stdout should include("The docker skeleton is now installed at the current directory.")
+
+            val sdk = new File(dir, "dockerSkeleton")
+            sdk.exists() should be(true)
+            sdk.isDirectory() should be(true)
+
+            val dockerfile = new File(sdk, "Dockerfile")
+            dockerfile.exists() should be(true)
+            dockerfile.isFile() should be(true)
+            val lines = FileUtils.readLines(dockerfile)
+            // confirm that the image is correct
+            lines.get(1) shouldBe "FROM openwhisk/dockerskeleton"
+
+            val buildAndPushFile = new File(sdk, "buildAndPush.sh")
+            buildAndPushFile.canExecute() should be(true)
+
+            // confirm there is no other divergence from the base dockerfile
+            val originalDockerfile = WhiskProperties.getFileRelativeToWhiskHome("sdk/docker/Dockerfile")
+            val originalLines = FileUtils.readLines(originalDockerfile)
+            lines.get(0) shouldBe originalLines.get(0)
+            lines.drop(2).mkString("\n") shouldBe originalLines.drop(2).mkString("\n")
+        } finally {
+            FileUtils.deleteDirectory(dir)
+        }
+    }
+
+    it should "download iOS sdk" in {
+        val dir = File.createTempFile("wskinstall", ".tmp")
+        dir.delete()
+        dir.mkdir() should be(true)
+
+        wsk.cli(wskprops.overrides ++ Seq("sdk", "install", "iOS"), workingDir = dir).
+            stdout should include("Downloaded OpenWhisk iOS starter app. Unzip OpenWhiskIOSStarterApp.zip and open the project in Xcode.")
+
+        val sdk = new File(dir, "OpenWhiskIOSStarterApp.zip")
+        sdk.exists() should be(true)
+        sdk.isFile() should be(true)
+        FileUtils.sizeOf(sdk) should be > 30000L
+        FileUtils.deleteDirectory(dir)
+    }
+
+    it should "install the bash auto-completion bash script" in {
+        // Use a temp dir for testing to not disturb user's local folder
+        val dir = File.createTempFile("wskinstall", ".tmp")
+        dir.delete()
+        dir.mkdir() should be(true)
+
+        val scriptfilename = "wsk_cli_bash_completion.sh"
+        var scriptfile = new File(dir.getPath(), scriptfilename)
+        try {
+            val stdout = wsk.cli(Seq("sdk", "install", "bashauto"), workingDir = dir, expectedExitCode = SUCCESS_EXIT).stdout
+            stdout should include("is installed in the current directory")
+            val fileContent = FileUtils.readFileToString(scriptfile)
+            fileContent should include("bash completion for wsk")
+        } finally {
+            scriptfile.delete()
+            FileUtils.deleteDirectory(dir)
+        }
+    }
+
+}
diff --git a/tests/src/test/scala/system/basic/WskSequenceTests.scala b/tests/src/test/scala/system/basic/WskSequenceTests.scala
new file mode 100644
index 0000000..dd41108
--- /dev/null
+++ b/tests/src/test/scala/system/basic/WskSequenceTests.scala
@@ -0,0 +1,528 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * 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.
+ */
+
+package system.basic
+
+import java.time.Instant
+import java.util.Date
+
+import scala.concurrent.duration.DurationInt
+import scala.language.postfixOps
+import scala.util.matching.Regex
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+import common.StreamLogging
+import common.TestHelpers
+import common.TestUtils
+import common.TestUtils._
+import common.Wsk
+import common.WskProps
+import common.WskTestHelpers
+import spray.json._
+import spray.json.DefaultJsonProtocol._
+import spray.testkit.ScalatestRouteTest
+import whisk.core.WhiskConfig
+import whisk.http.Messages.sequenceIsTooLong
+
+/**
+ * Tests sequence execution
+ */
+
+@RunWith(classOf[JUnitRunner])
+class WskSequenceTests
+    extends TestHelpers
+    with ScalatestRouteTest
+    with WskTestHelpers
+    with StreamLogging {
+
+    implicit val wskprops = WskProps()
+    val wsk = new Wsk
+    val allowedActionDuration = 120 seconds
+    val shortDuration = 10 seconds
+
+    val whiskConfig = new WhiskConfig(Map(WhiskConfig.actionSequenceDefaultLimit -> null))
+    assert(whiskConfig.isValid)
+
+    behavior of "Wsk Sequence"
+
+    it should "invoke a blocking sequence action and invoke the updated sequence with normal payload and payload with error field" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "sequence"
+            val actions = Seq("split", "sort", "head", "cat")
+            for (actionName <- actions) {
+                val file = TestUtils.getTestActionFilename(s"$actionName.js")
+                assetHelper.withCleaner(wsk.action, actionName) { (action, _) =>
+                    action.create(name = actionName, artifact = Some(file), timeout = Some(allowedActionDuration))
+                }
+            }
+
+            println(s"Sequence $actions")
+            assetHelper.withCleaner(wsk.action, name) {
+                val sequence = actions.mkString(",")
+                (action, _) => action.create(name, Some(sequence), kind = Some("sequence"), timeout = Some(allowedActionDuration))
+            }
+
+            val now = "it is now " + new Date()
+            val args = Array("what time is it?", now)
+            val run = wsk.action.invoke(name, Map("payload" -> args.mkString("\n").toJson))
+            withActivation(wsk.activation, run, totalWait = 4 * allowedActionDuration) {
+                activation =>
+                    checkSequenceLogsAndAnnotations(activation, 4) // 4 activations in this sequence
+                    activation.cause shouldBe None // topmost sequence
+                    val result = activation.response.result.get
+                    result.fields.get("payload") shouldBe defined
+                    result.fields.get("length") should not be defined
+                    result.fields.get("lines") shouldBe Some(JsArray(Vector(now.toJson)))
+            }
+
+            // update action sequence and run it with normal payload
+            val newSequence = Seq("split", "sort").mkString(",")
+            println(s"Update sequence to $newSequence")
+            wsk.action.create(name, Some(newSequence), kind = Some("sequence"), timeout = Some(allowedActionDuration), update = true)
+            val secondrun = wsk.action.invoke(name, Map("payload" -> args.mkString("\n").toJson))
+            withActivation(wsk.activation, secondrun, totalWait = 2 * allowedActionDuration) {
+                activation =>
+                    checkSequenceLogsAndAnnotations(activation, 2) // 2 activations in this sequence
+                    val result = activation.response.result.get
+                    result.fields.get("length") shouldBe Some(2.toJson)
+                    result.fields.get("lines") shouldBe Some(args.sortWith(_.compareTo(_) < 0).toArray.toJson)
+            }
+
+            println("Run sequence with error in payload")
+            // run sequence with error in the payload; nothing should run
+            val payload = Map("error" -> JsString("irrelevant error string"))
+            val thirdrun = wsk.action.invoke(name, payload)
+            withActivation(wsk.activation, thirdrun, totalWait = allowedActionDuration) {
+                activation =>
+                    activation.logs shouldBe defined
+                    // no activations should have run
+                    activation.logs.get.size shouldBe (0)
+                    activation.response.success shouldBe (false)
+                    // the status should be error
+                    activation.response.status shouldBe ("application error")
+                    val result = activation.response.result.get
+                    // the result of the activation should be the payload
+                    result shouldBe (JsObject(payload))
+
+            }
+    }
+
+    /**
+     * s -> echo, x, echo
+     * x -> echo
+     *
+     * update x -> <limit-1> echo -- should work
+     * run s -> should stop after <limit> echo
+     */
+    it should "create a sequence, run it, update one of the atomic actions to a sequence and stop executing the outer sequence when limit reached" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val xName = "xSequence"
+            val sName = "sSequence"
+            val echo = "echo"
+
+            // create echo action
+            val file = TestUtils.getTestActionFilename(s"$echo.js")
+            assetHelper.withCleaner(wsk.action, echo) { (action, actionName) =>
+                action.create(name = actionName, artifact = Some(file), timeout = Some(allowedActionDuration))
+            }
+            // create x
+            assetHelper.withCleaner(wsk.action, xName) {
+                (action, seqName) => action.create(seqName, Some(echo), kind = Some("sequence"))
+            }
+            // create s
+            assetHelper.withCleaner(wsk.action, sName) {
+                (action, seqName) => action.create(seqName, Some(s"$echo,$xName,$echo"), kind = Some("sequence"))
+            }
+
+            // invoke s
+            val now = "it is now " + new Date()
+            val args = Array("what time is it?", now)
+            val argsJson = args.mkString("\n").toJson
+            val run = wsk.action.invoke(sName, Map("payload" -> argsJson))
+            println(s"RUN: ${run.stdout}")
+            withActivation(wsk.activation, run, totalWait = 2 * allowedActionDuration) {
+                activation =>
+                    checkSequenceLogsAndAnnotations(activation, 3) // 3 activations in this sequence
+                    val result = activation.response.result.get
+                    result.fields.get("payload") shouldBe Some(argsJson)
+            }
+            // update x with limit echo
+            val limit = whiskConfig.actionSequenceLimit.toInt
+            val manyEcho = for (i <- 1 to limit) yield echo
+
+            wsk.action.create(xName, Some(manyEcho.mkString(",")), kind = Some("sequence"), update = true)
+
+            val updateRun = wsk.action.invoke(sName, Map("payload" -> argsJson))
+            withActivation(wsk.activation, updateRun, totalWait = 2 * allowedActionDuration) {
+                activation =>
+                    activation.response.status shouldBe ("application error")
+                    checkSequenceLogsAndAnnotations(activation, 2)
+                    val result = activation.response.result.get
+                    result.fields.get("error") shouldBe Some(JsString(sequenceIsTooLong))
+                    // check that inner sequence had only (limit - 1) activations
+                    val innerSeq = activation.logs.get(1) // the id of the inner sequence activation
+                    val getInnerSeq = wsk.activation.get(innerSeq)
+                    withActivation(wsk.activation, getInnerSeq, totalWait = allowedActionDuration) {
+                        innerSeqActivation =>
+                            innerSeqActivation.logs.get.size shouldBe (limit - 1)
+                            innerSeqActivation.cause shouldBe defined
+                            innerSeqActivation.cause.get shouldBe (activation.activationId)
+                    }
+            }
+    }
+
+    it should "invoke a blocking sequence action with an enclosing sequence action" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val inner_name = "inner_sequence"
+            val outer_name = "outer_sequence"
+            val inner_actions = Seq("sort", "head")
+            val actions = Seq("split") ++ inner_actions ++ Seq("cat")
+            // create atomic actions
+            for (actionName <- actions) {
+                val file = TestUtils.getTestActionFilename(s"$actionName.js")
+                assetHelper.withCleaner(wsk.action, actionName) { (action, _) =>
+                    action.create(name = actionName, artifact = Some(file), timeout = Some(allowedActionDuration))
+                }
+            }
+
+            // create inner sequence
+            assetHelper.withCleaner(wsk.action, inner_name) {
+                val inner_sequence = inner_actions.mkString(",")
+                (action, _) => action.create(inner_name, Some(inner_sequence), kind = Some("sequence"))
+            }
+
+            // create outer sequence
+            assetHelper.withCleaner(wsk.action, outer_name) {
+                val outer_sequence = Seq("split", "inner_sequence", "cat").mkString(",")
+                (action, _) => action.create(outer_name, Some(outer_sequence), kind = Some("sequence"))
+            }
+
+            val now = "it is now " + new Date()
+            val args = Array("what time is it?", now)
+            val run = wsk.action.invoke(outer_name, Map("payload" -> args.mkString("\n").toJson))
+            withActivation(wsk.activation, run, totalWait = 4 * allowedActionDuration) {
+                activation =>
+                    checkSequenceLogsAndAnnotations(activation, 3) // 3 activations in this sequence
+                    activation.cause shouldBe None // topmost sequence
+                    val result = activation.response.result.get
+                    result.fields.get("payload") shouldBe defined
+                    result.fields.get("length") should not be defined
+                    result.fields.get("lines") shouldBe Some(JsArray(Vector(now.toJson)))
+            }
+    }
+
+    it should "create and run a sequence in a package with parameters" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val sName = "sSequence"
+
+            // create a package
+            val pkgName = "echopackage"
+            val pkgStr = "LonelyPackage"
+            assetHelper.withCleaner(wsk.pkg, pkgName) {
+                (pkg, name) => pkg.create(name, Map("payload" -> JsString(pkgStr)))
+            }
+            val helloName = "hello"
+            val helloWithPkg = s"$pkgName/$helloName"
+
+            // create hello action in package
+            val file = TestUtils.getTestActionFilename(s"$helloName.js")
+            val actionStr = "AtomicAction"
+            assetHelper.withCleaner(wsk.action, helloWithPkg) { (action, actionName) =>
+                action.create(name = actionName, artifact = Some(file), timeout = Some(allowedActionDuration), parameters = Map("payload" -> JsString(actionStr)))
+            }
+            // create s
+            assetHelper.withCleaner(wsk.action, sName) {
+                (action, seqName) => action.create(seqName, Some(helloWithPkg), kind = Some("sequence"))
+            }
+            val run = wsk.action.invoke(sName)
+            // action params trump package params
+            checkLogsAtomicAction(0, run, new Regex(actionStr))
+            // run with some parameters
+            val sequenceStr = "AlmightySequence"
+            val sequenceParamRun = wsk.action.invoke(sName, parameters = Map("payload" -> JsString(sequenceStr)))
+            // sequence param should be passed to the first atomic action and trump the action params
+            checkLogsAtomicAction(0, sequenceParamRun, new Regex(sequenceStr))
+            // update action and remove the params by sending an unused param that overrides previous params
+            wsk.action.create(name = helloWithPkg, artifact = Some(file), timeout = Some(allowedActionDuration), parameters = Map("param" -> JsString("irrelevant")), update = true)
+            val sequenceParamSecondRun = wsk.action.invoke(sName, parameters = Map("payload" -> JsString(sequenceStr)))
+            // sequence param should be passed to the first atomic action and trump the package params
+            checkLogsAtomicAction(0, sequenceParamSecondRun, new Regex(sequenceStr))
+            val pkgParamRun = wsk.action.invoke(sName)
+            // no sequence params, no atomic action params used, the pkg params should show up
+            checkLogsAtomicAction(0, pkgParamRun, new Regex(pkgStr))
+    }
+
+    it should "run a sequence with an action in a package binding with parameters" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val packageName = "package1"
+            val bindName = "package2"
+            val actionName = "print"
+            val packageActionName = packageName + "/" + actionName
+            val bindActionName = bindName + "/" + actionName
+            val packageParams = Map("key1a" -> "value1a".toJson, "key1b" -> "value1b".toJson)
+            val bindParams = Map("key2a" -> "value2a".toJson, "key1b" -> "value2b".toJson)
+            val actionParams = Map("key0" -> "value0".toJson)
+            val file = TestUtils.getTestActionFilename("printParams.js")
+            assetHelper.withCleaner(wsk.pkg, packageName) { (pkg, _) =>
+                pkg.create(packageName, packageParams)
+            }
+            assetHelper.withCleaner(wsk.action, packageActionName) { (action, _) =>
+                action.create(packageActionName, Some(file), parameters = actionParams)
+            }
+            assetHelper.withCleaner(wsk.pkg, bindName) { (pkg, _) =>
+                pkg.bind(packageName, bindName, bindParams)
+            }
+            // sequence
+            val sName = "sequenceWithBindingParams"
+            assetHelper.withCleaner(wsk.action, sName) {
+                (action, seqName) => action.create(seqName, Some(bindActionName), kind = Some("sequence"))
+            }
+            // Check that inherited parameters are passed to the action.
+            val now = new Date().toString()
+            val run = wsk.action.invoke(sName, Map("payload" -> now.toJson))
+            // action params trump package params
+            checkLogsAtomicAction(0, run, new Regex(String.format(".*key0: value0.*key1a: value1a.*key1b: value2b.*key2a: value2a.*payload: %s", now)))
+    }
+    /**
+     * s -> apperror, echo
+     * only apperror should run
+     */
+    it should "stop execution of a sequence (with no payload) on error" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val sName = "sSequence"
+            val apperror = "applicationError"
+            val echo = "echo"
+
+            // create actions
+            val actions = Seq(apperror, echo)
+            for (actionName <- actions) {
+                val file = TestUtils.getTestActionFilename(s"$actionName.js")
+                assetHelper.withCleaner(wsk.action, actionName) { (action, actionName) =>
+                    action.create(name = actionName, artifact = Some(file), timeout = Some(allowedActionDuration))
+                }
+            }
+            // create sequence s
+            assetHelper.withCleaner(wsk.action, sName) {
+                (action, seqName) => action.create(seqName, artifact = Some(actions.mkString(",")), kind = Some("sequence"))
+            }
+            // run sequence s with no payload
+            val run = wsk.action.invoke(sName)
+            withActivation(wsk.activation, run, totalWait = 2 * allowedActionDuration) {
+                activation =>
+                    checkSequenceLogsAndAnnotations(activation, 1) // only the first action should have run
+                    activation.response.success shouldBe (false)
+                    // the status should be error
+                    activation.response.status shouldBe ("application error")
+                    val result = activation.response.result.get
+                    // the result of the activation should be the application error
+                    result shouldBe (JsObject("error" -> JsString("This error thrown on purpose by the action.")))
+            }
+    }
+
+    /**
+     * s -> echo, initforever
+     * should run both, but error
+     */
+    it should "propagate execution error (timeout) from atomic action to sequence" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val sName = "sSequence"
+            val initforever = "initforever"
+            val echo = "echo"
+
+            // create actions
+            val actions = Seq(echo, initforever)
+            // timeouts for the action; make the one for initforever short
+            val timeout = Map(echo -> allowedActionDuration, initforever -> shortDuration)
+            for (actionName <- actions) {
+                val file = TestUtils.getTestActionFilename(s"$actionName.js")
+                assetHelper.withCleaner(wsk.action, actionName) { (action, actionName) =>
+                    action.create(name = actionName, artifact = Some(file), timeout = Some(timeout(actionName)))
+                }
+            }
+            // create sequence s
+            assetHelper.withCleaner(wsk.action, sName) {
+                (action, seqName) => action.create(seqName, artifact = Some(actions.mkString(",")), kind = Some("sequence"))
+            }
+            // run sequence s with no payload
+            val run = wsk.action.invoke(sName)
+            withActivation(wsk.activation, run, totalWait = 2 * allowedActionDuration) {
+                activation =>
+                    checkSequenceLogsAndAnnotations(activation, 2) // 2 actions
+                    activation.response.success shouldBe (false)
+                    // the status should be error
+                    //activation.response.status shouldBe("application error")
+                    val result = activation.response.result.get
+                    // the result of the activation should be timeout
+                    result shouldBe (JsObject("error" -> JsString("The action exceeded its time limits of 10000 milliseconds during initialization.")))
+            }
+    }
+
+    /**
+     * s -> echo, sleep
+     * sleep sleeps for 90s, timeout set at 120s
+     * should run both, the blocking call should be transformed into a non-blocking call, but finish executing
+     */
+    it should "execute a sequence in blocking fashion and finish execution even if longer than blocking response timeout" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val sName = "sSequence"
+            val sleep = "timeout"
+            val echo = "echo"
+
+            // create actions
+            val actions = Seq(echo, sleep)
+            for (actionName <- actions) {
+                val file = TestUtils.getTestActionFilename(s"$actionName.js")
+                assetHelper.withCleaner(wsk.action, actionName) { (action, actionName) =>
+                    action.create(name = actionName, artifact = Some(file), timeout = Some(allowedActionDuration))
+                }
+            }
+            // create sequence s
+            assetHelper.withCleaner(wsk.action, sName) {
+                (action, seqName) => action.create(seqName, artifact = Some(actions.mkString(",")), kind = Some("sequence"))
+            }
+            // run sequence s with sleep equal to payload
+            val payload = 65000
+            val run = wsk.action.invoke(sName, parameters = Map("payload" -> JsNumber(payload)), blocking = true)
+            withActivation(wsk.activation, run, initialWait = 5 seconds, totalWait = 3 * allowedActionDuration) {
+                activation =>
+                    checkSequenceLogsAndAnnotations(activation, 2) // 2 actions
+                    activation.response.success shouldBe (true)
+                    // the status should be error
+                    //activation.response.status shouldBe("application error")
+                    val result = activation.response.result.get
+                    // the result of the activation should be timeout
+                    result shouldBe (JsObject("msg" -> JsString(s"[OK] message terminated successfully after $payload milliseconds.")))
+            }
+    }
+
+    /**
+     * sequence s -> echo
+     * t trigger with payload
+     * rule r: t -> s
+     */
+    it should "execute a sequence that is part of a rule and pass the trigger parameters to the sequence" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val seqName = "seqRule"
+            val actionName = "echo"
+            val triggerName = "trigSeq"
+            val ruleName = "ruleSeq"
+
+            val itIsNow = "it is now " + new Date()
+            // set up all entities
+            // trigger
+            val triggerPayload: Map[String, JsValue] = Map("payload" -> JsString(itIsNow))
+            assetHelper.withCleaner(wsk.trigger, triggerName) {
+                (trigger, name) => trigger.create(name, parameters = triggerPayload)
+            }
+            // action
+            val file = TestUtils.getTestActionFilename(s"$actionName.js")
+            assetHelper.withCleaner(wsk.action, actionName) { (action, actionName) =>
+                action.create(name = actionName, artifact = Some(file), timeout = Some(allowedActionDuration))
+            }
+            // sequence
+            assetHelper.withCleaner(wsk.action, seqName) {
+                (action, seqName) => action.create(seqName, artifact = Some(actionName), kind = Some("sequence"))
+            }
+            // rule
+            assetHelper.withCleaner(wsk.rule, ruleName) {
+                (rule, name) => rule.create(name, triggerName, seqName)
+            }
+            // fire trigger
+            val run = wsk.trigger.fire(triggerName)
+            // check that the sequence was invoked and that the echo action produced the expected result
+            checkEchoSeqRuleResult(run, seqName, JsObject(triggerPayload))
+            // fire trigger with new payload
+            val now = "this is now: " + Instant.now
+            val newPayload = Map("payload" -> JsString(now))
+            val newRun = wsk.trigger.fire(triggerName, newPayload)
+            checkEchoSeqRuleResult(newRun, seqName, JsObject(newPayload))
+    }
+
+    /**
+     * checks the result of an echo sequence connected to a trigger through a rule
+     * @param triggerFireRun the run result of firing the trigger
+     * @param seqName the sequence name
+     * @param triggerPayload the payload used for the trigger (that should be reflected in the sequence result)
+     */
+    private def checkEchoSeqRuleResult(triggerFireRun: RunResult, seqName: String, triggerPayload: JsObject) = {
+        withActivation(wsk.activation, triggerFireRun) {
+            triggerActivation =>
+                withActivationsFromEntity(wsk.activation, seqName, since = Some(triggerActivation.start)) { activationList =>
+                    activationList.head.response.result shouldBe Some(triggerPayload)
+                    activationList.head.cause shouldBe None
+                }
+        }
+    }
+
+    /**
+     * checks logs for the activation of a sequence (length/size and ids)
+     * checks that the cause field for composing atomic actions is set properly
+     * checks duration
+     * checks memory
+     */
+    private def checkSequenceLogsAndAnnotations(activation: CliActivation, size: Int) = {
+        activation.logs shouldBe defined
+        // check that the logs are what they are supposed to be (activation ids)
+        // check that the cause field is properly set for these activations
+        activation.logs.get.size shouldBe (size) // the number of activations in this sequence
+        var totalTime: Long = 0
+        var maxMemory: Long = 0
+        for (id <- activation.logs.get) {
+            withActivation(wsk.activation, id, initialWait = 1 second, pollPeriod = 60 seconds, totalWait = allowedActionDuration) {
+                componentActivation =>
+                    componentActivation.cause shouldBe defined
+                    componentActivation.cause.get shouldBe (activation.activationId)
+                    // check causedBy
+                    val causedBy = componentActivation.getAnnotationValue("causedBy")
+                    causedBy shouldBe defined
+                    causedBy.get shouldBe (JsString("sequence"))
+                    totalTime += componentActivation.duration
+                    // extract memory
+                    val mem = extractMemoryAnnotation(componentActivation)
+                    maxMemory = maxMemory max mem
+            }
+        }
+        // extract duration
+        activation.duration shouldBe (totalTime)
+        // extract memory
+        activation.annotations shouldBe defined
+        val memory = extractMemoryAnnotation(activation)
+        memory shouldBe (maxMemory)
+    }
+
+    /** checks that the logs of the idx-th atomic action from a sequence contains logsStr */
+    private def checkLogsAtomicAction(atomicActionIdx: Int, run: RunResult, regex: Regex) {
+        withActivation(wsk.activation, run, totalWait = 2 * allowedActionDuration) { activation =>
+            checkSequenceLogsAndAnnotations(activation, 1)
+            val componentId = activation.logs.get(atomicActionIdx)
+            val getComponentActivation = wsk.activation.get(componentId)
+            withActivation(wsk.activation, getComponentActivation, totalWait = allowedActionDuration) { componentActivation =>
+                println(componentActivation)
+                componentActivation.logs shouldBe defined
+                val logs = componentActivation.logs.get.mkString(" ")
+                regex.findFirstIn(logs) shouldBe defined
+            }
+        }
+    }
+
+    private def extractMemoryAnnotation(activation: CliActivation): Long = {
+        val limits = activation.getAnnotationValue("limits")
+        limits shouldBe defined
+        limits.get.asJsObject.getFields("memory")(0).convertTo[Long]
+    }
+}
diff --git a/tests/src/test/scala/whisk/core/admin/WskAdminTests.scala b/tests/src/test/scala/whisk/core/admin/WskAdminTests.scala
new file mode 100644
index 0000000..4dfdc79
--- /dev/null
+++ b/tests/src/test/scala/whisk/core/admin/WskAdminTests.scala
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * 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.
+ */
+
+package whisk.core.admin
+
+import scala.concurrent.duration.DurationInt
+
+import org.junit.runner.RunWith
+import org.scalatest.Matchers
+import org.scalatest.junit.JUnitRunner
+
+import common.RunWskAdminCmd
+import common.TestHelpers
+import common.WskAdmin
+import whisk.core.entity.AuthKey
+import whisk.core.entity.Subject
+import whisk.core.entity.WhiskAuth
+
+@RunWith(classOf[JUnitRunner])
+class WskAdminTests
+    extends TestHelpers
+    with Matchers {
+
+    behavior of "Wsk Admin CLI"
+
+    it should "confirm wskadmin exists" in {
+        WskAdmin.exists
+    }
+
+    it should "CRD a subject" in {
+        val wskadmin = new RunWskAdminCmd {}
+        val auth = WhiskAuth(Subject(), AuthKey())
+        val subject = auth.subject.asString
+        try {
+            println(s"CRD subject: $subject")
+            val create = wskadmin.cli(Seq("user", "create", subject))
+            val get = wskadmin.cli(Seq("user", "get", subject))
+            create.stdout should be(get.stdout)
+
+            val authkey = get.stdout.trim
+            authkey should include(":")
+            authkey.split(":")(0).length should be(36)
+            authkey.split(":")(1).length should be >= 64
+
+            wskadmin.cli(Seq("user", "whois", authkey)).stdout.trim should be(Seq(s"subject: $subject", s"namespace: $subject").mkString("\n"))
+
+            whisk.utils.retry({
+                // reverse lookup by namespace
+                wskadmin.cli(Seq("user", "list", "-k", subject)).stdout.trim should be(authkey)
+            }, 10, Some(1.second))
+
+            wskadmin.cli(Seq("user", "delete", subject)).stdout should include("Subject deleted")
+
+            // recreate with explicit
+            val newspace = s"${subject}.myspace"
+            wskadmin.cli(Seq("user", "create", subject, "-ns", newspace, "-u", auth.authkey.compact))
+
+            whisk.utils.retry({
+                // reverse lookup by namespace
+                wskadmin.cli(Seq("user", "list", "-k", newspace)).stdout.trim should be(auth.authkey.compact)
+            }, 10, Some(1.second))
+
+            wskadmin.cli(Seq("user", "get", subject, "-ns", newspace)).stdout.trim should be(auth.authkey.compact)
+
+            // delete namespace
+            wskadmin.cli(Seq("user", "delete", subject, "-ns", newspace)).stdout should include("Namespace deleted")
+        } finally {
+            wskadmin.cli(Seq("user", "delete", subject)).stdout should include("Subject deleted")
+        }
+    }
+
+}
diff --git a/tests/src/test/scala/whisk/core/apigw/actions/test/ApiGwRoutemgmtActionTests.scala b/tests/src/test/scala/whisk/core/apigw/actions/test/ApiGwRoutemgmtActionTests.scala
new file mode 100644
index 0000000..d289d7a
--- /dev/null
+++ b/tests/src/test/scala/whisk/core/apigw/actions/test/ApiGwRoutemgmtActionTests.scala
@@ -0,0 +1,332 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * 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.
+ */
+
+package whisk.core.apigw.actions.test
+
+import org.junit.runner.RunWith
+import org.scalatest.BeforeAndAfterAll
+import org.scalatest.junit.JUnitRunner
+
+import common.JsHelpers
+import common.StreamLogging
+import common.TestHelpers
+import common.TestUtils.ANY_ERROR_EXIT
+import common.TestUtils.DONTCARE_EXIT
+import common.TestUtils.RunResult
+import common.TestUtils.SUCCESS_EXIT
+import common.Wsk
+import common.WskActorSystem
+import common.WskAdmin
+import common.WskProps
+import common.WskTestHelpers
+import spray.json._
+import spray.json.DefaultJsonProtocol._
+
+case class ApiAction(
+    name: String,
+    namespace: String,
+    backendMethod: String = "POST",
+    backendUrl: String,
+    authkey: String) {
+    def toJson(): JsObject = {
+        return JsObject(
+            "name" -> name.toJson,
+            "namespace" -> namespace.toJson,
+            "backendMethod" -> backendMethod.toJson,
+            "backendUrl" -> backendUrl.toJson,
+            "authkey" -> authkey.toJson)
+    }
+}
+
+/**
+ * Tests for basic CLI usage. Some of these tests require a deployed backend.
+ */
+@RunWith(classOf[JUnitRunner])
+class ApiGwRoutemgmtActionTests
+    extends TestHelpers
+    with BeforeAndAfterAll
+    with WskActorSystem
+    with WskTestHelpers
+    with JsHelpers
+    with StreamLogging {
+
+    val systemId = "whisk.system"
+    implicit val wskprops = WskProps(authKey = WskAdmin.listKeys(systemId)(0)._1, namespace = systemId)
+    val wsk = new Wsk
+
+    def getApis(
+        bpOrName: Option[String],
+        relpath: Option[String] = None,
+        operation: Option[String] = None,
+        docid: Option[String] = None): Vector[JsValue] = {
+        val parms = Map[String, JsValue]() ++
+            Map("__ow_meta_namespace" -> wskprops.namespace.toJson) ++
+            { bpOrName map { b => Map("basepath" -> b.toJson) } getOrElse Map[String, JsValue]() } ++
+            { relpath map { r => Map("relpath" -> r.toJson) } getOrElse Map[String, JsValue]() } ++
+            { operation map { o => Map("operation" -> o.toJson) } getOrElse Map[String, JsValue]() } ++
+            { docid map { d => Map("docid" -> d.toJson) } getOrElse Map[String, JsValue]() }
+
+        val rr = wsk.action.invoke(
+            name = "routemgmt/getApi",
+            parameters = parms,
+            blocking = true,
+            result = true,
+            expectedExitCode = SUCCESS_EXIT)(wskprops)
+        var apiJsArray: JsArray =
+            try {
+                var apisobj = rr.stdout.parseJson.asJsObject.fields("apis")
+                apisobj.convertTo[JsArray]
+            } catch {
+                case e: Exception =>
+                    JsArray.empty
+            }
+        return apiJsArray.elements
+    }
+
+    def createApi(
+        namespace: Option[String] = Some("_"),
+        basepath: Option[String] = Some("/"),
+        relpath: Option[String],
+        operation: Option[String],
+        apiname: Option[String],
+        action: Option[ApiAction],
+        swagger: Option[String] = None,
+        expectedExitCode: Int = SUCCESS_EXIT): RunResult = {
+        val parms = Map[String, JsValue]() ++
+            { namespace map { n => Map("namespace" -> n.toJson) } getOrElse Map[String, JsValue]() } ++
+            { basepath map { b => Map("gatewayBasePath" -> b.toJson) } getOrElse Map[String, JsValue]() } ++
+            { relpath map { r => Map("gatewayPath" -> r.toJson) } getOrElse Map[String, JsValue]() } ++
+            { operation map { o => Map("gatewayMethod" -> o.toJson) } getOrElse Map[String, JsValue]() } ++
+            { apiname map { an => Map("apiName" -> an.toJson) } getOrElse Map[String, JsValue]() } ++
+            { action map { a => Map("action" -> a.toJson) } getOrElse Map[String, JsValue]() } ++
+            { swagger map { s => Map("swagger" -> s.toJson) } getOrElse Map[String, JsValue]() }
+        val parm = Map[String, JsValue]("apidoc" -> JsObject(parms)) ++
+            { namespace map { n => Map("__ow_meta_namespace" -> n.toJson) } getOrElse Map[String, JsValue]() }
+
+        val rr = wsk.action.invoke(
+            name = "routemgmt/createApi",
+            parameters = parm,
+            blocking = true,
+            result = true,
+            expectedExitCode = expectedExitCode)(wskprops)
+        return rr
+    }
+
+    def deleteApi(
+        namespace: Option[String] = Some("_"),
+        basepath: Option[String] = Some("/"),
+        relpath: Option[String] = None,
+        operation: Option[String] = None,
+        apiname: Option[String] = None,
+        expectedExitCode: Int = SUCCESS_EXIT): RunResult = {
+        val parms = Map[String, JsValue]() ++
+            { namespace map { n => Map("__ow_meta_namespace" -> n.toJson) } getOrElse Map[String, JsValue]() } ++
+            { basepath map { b => Map("basepath" -> b.toJson) } getOrElse Map[String, JsValue]() } ++
+            { relpath map { r => Map("relpath" -> r.toJson) } getOrElse Map[String, JsValue]() } ++
+            { operation map { o => Map("operation" -> o.toJson) } getOrElse Map[String, JsValue]() } ++
+            { apiname map { an => Map("apiname" -> an.toJson) } getOrElse Map[String, JsValue]() }
+
+        val rr = wsk.action.invoke(
+            name = "routemgmt/deleteApi",
+            parameters = parms,
+            blocking = true,
+            result = true,
+            expectedExitCode = expectedExitCode)(wskprops)
+        return rr
+    }
+
+    def apiMatch(
+        apiarr: Vector[JsValue],
+        basepath: String = "/",
+        relpath: String = "",
+        operation: String = "",
+        apiname: String = "",
+        action: ApiAction = null): Boolean = {
+        var matches: Boolean = false
+        for (api <- apiarr) {
+            val basepathExists = JsObjectHelper(api.asJsObject).fieldPathExists("value", "apidoc", "basePath")
+            if (basepathExists) {
+                System.out.println("basePath exists")
+                val basepathMatches = (JsObjectHelper(api.asJsObject).getFieldPath("value", "apidoc", "basePath").get.convertTo[String] == basepath)
+                if (basepathMatches) {
+                    System.out.println("basePath matches: " + basepath)
+                    val apinameExists = JsObjectHelper(api.asJsObject).fieldPathExists("value", "apidoc", "info", "title")
+                    if (apinameExists) {
+                        System.out.println("api name exists")
+                        val apinameMatches = (JsObjectHelper(api.asJsObject).getFieldPath("value", "apidoc", "info", "title").get.convertTo[String] == apiname)
+                        if (apinameMatches) {
+                            System.out.println("api name matches: " + apiname)
+                            val endpointMatches = JsObjectHelper(api.asJsObject).fieldPathExists("value", "apidoc", "paths", relpath, operation)
+                            if (endpointMatches) {
+                                System.out.println("endpoint exists/matches : " + relpath + "  " + operation)
+                                val actionConfig = JsObjectHelper(api.asJsObject).getFieldPath("value", "apidoc", "paths", relpath, operation, "x-ibm-op-ext").get.asJsObject
+                                val actionMatches = actionMatch(actionConfig, action)
+                                if (actionMatches) {
+                                    System.out.println("endpoint action matches")
+                                    matches = true;
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        return matches
+    }
+
+    def actionMatch(
+        jsAction: JsObject,
+        action: ApiAction): Boolean = {
+        val matches = jsAction.fields("backendMethod").convertTo[String] == action.backendMethod &&
+            jsAction.fields("backendUrl").convertTo[String] == action.backendUrl &&
+            jsAction.fields("actionNamespace").convertTo[String] == action.namespace &&
+            jsAction.fields("actionName").convertTo[String] == action.name
+        return matches
+    }
+
+    behavior of "API Gateway routemgmt action parameter validation"
+
+    it should "verify successful creation of a new API" in {
+        val testName = "APIGWTEST1"
+        val testbasepath = "/" + testName + "_bp"
+        val testrelpath = "/path"
+        val testurlop = "get"
+        val testapiname = testName + " API Name"
+        val actionName = testName + "_action"
+        val actionNamespace = wskprops.namespace
+        val actionUrl = "http://some.whisk.host/api/v1/namespaces/" + actionNamespace + "/actions/" + actionName
+        val actionAuthKey = testName + "_authkey"
+        val testaction = ApiAction(name = actionName, namespace = actionNamespace, backendUrl = actionUrl, authkey = actionAuthKey)
+
+        try {
+            val createResult = createApi(namespace = Some(wskprops.namespace), basepath = Some(testbasepath), relpath = Some(testrelpath),
+                operation = Some(testurlop), apiname = Some(testapiname), action = Some(testaction))
+            JsObjectHelper(createResult.stdout.parseJson.asJsObject).fieldPathExists("apidoc") should be(true)
+            val apiVector = getApis(bpOrName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop))
+            apiVector.size should be > 0
+            apiMatch(apiVector, testbasepath, testrelpath, testurlop, testapiname, testaction) should be(true)
+        } finally {
+            val deleteResult = deleteApi(namespace = Some(wskprops.namespace), basepath = Some(testbasepath), expectedExitCode = DONTCARE_EXIT)
+        }
+    }
+
+    it should "verify successful API deletion using basepath" in {
+        val testName = "APIGWTEST2"
+        val testbasepath = "/" + testName + "_bp"
+        val testrelpath = "/path"
+        val testurlop = "get"
+        val testapiname = testName + " API Name"
+        val actionName = testName + "_action"
+        val actionNamespace = wskprops.namespace
+        val actionUrl = "http://some.whisk.host/api/v1/namespaces/" + actionNamespace + "/actions/" + actionName
+        val actionAuthKey = testName + "_authkey"
+        val testaction = ApiAction(name = actionName, namespace = actionNamespace, backendUrl = actionUrl, authkey = actionAuthKey)
+
+        try {
+            val createResult = createApi(namespace = Some(wskprops.namespace), basepath = Some(testbasepath), relpath = Some(testrelpath),
+                operation = Some(testurlop), apiname = Some(testapiname), action = Some(testaction))
+            JsObjectHelper(createResult.stdout.parseJson.asJsObject).fieldPathExists("apidoc") should be(true)
+            var apiVector = getApis(bpOrName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop))
+            apiVector.size should be > 0
+            apiMatch(apiVector, testbasepath, testrelpath, testurlop, testapiname, testaction) should be(true)
+            val deleteResult = deleteApi(namespace = Some(wskprops.namespace), basepath = Some(testbasepath))
+            apiVector = getApis(bpOrName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop))
+            apiMatch(apiVector, testbasepath, testrelpath, testurlop, testapiname, testaction) should be(false)
+        } finally {
+            val deleteResult = deleteApi(namespace = Some(wskprops.namespace), basepath = Some(testbasepath), expectedExitCode = DONTCARE_EXIT)
+        }
+    }
+
+    it should "verify successful addition of new relative path to existing API" in {
+        val testName = "APIGWTEST3"
+        val testbasepath = "/" + testName + "_bp"
+        val testrelpath = "/path"
+        val testnewrelpath = "/path_new"
+        val testurlop = "get"
+        val testnewurlop = "delete"
+        val testapiname = testName + " API Name"
+        val actionName = testName + "_action"
+        val actionNamespace = wskprops.namespace
+        val actionUrl = "http://some.whisk.host/api/v1/namespaces/" + actionNamespace + "/actions/" + actionName
+        val actionAuthKey = testName + "_authkey"
+        val testaction = ApiAction(name = actionName, namespace = actionNamespace, backendUrl = actionUrl, authkey = actionAuthKey)
+
+        try {
+            var createResult = createApi(namespace = Some(wskprops.namespace), basepath = Some(testbasepath), relpath = Some(testrelpath),
+                operation = Some(testurlop), apiname = Some(testapiname), action = Some(testaction))
+            createResult = createApi(namespace = Some(wskprops.namespace), basepath = Some(testbasepath), relpath = Some(testnewrelpath),
+                operation = Some(testnewurlop), apiname = Some(testapiname), action = Some(testaction))
+            JsObjectHelper(createResult.stdout.parseJson.asJsObject).fieldPathExists("apidoc") should be(true)
+            var apiVector = getApis(bpOrName = Some(testbasepath))
+            apiVector.size should be > 0
+            apiMatch(apiVector, testbasepath, testrelpath, testurlop, testapiname, testaction) should be(true)
+            apiMatch(apiVector, testbasepath, testnewrelpath, testnewurlop, testapiname, testaction) should be(true)
+        } finally {
+            val deleteResult = deleteApi(namespace = Some(wskprops.namespace), basepath = Some(testbasepath), expectedExitCode = DONTCARE_EXIT)
+        }
+    }
+
+    it should "reject routemgmt actions that are invoked with not enough parameters" in {
+        val invalidArgs = Seq(
+            //getApi
+            ("/whisk.system/routemgmt/getApi", ANY_ERROR_EXIT, "namespace is required", Seq()),
+
+            //deleteApi
+            ("/whisk.system/routemgmt/deleteApi", ANY_ERROR_EXIT, "namespace is required", Seq("-p", "basepath", "/ApiGwRoutemgmtActionTests_bp")),
+            ("/whisk.system/routemgmt/deleteApi", ANY_ERROR_EXIT, "basepath is required", Seq("-p", "__ow_meta_namespace", "_")),
+            ("/whisk.system/routemgmt/deleteApi", ANY_ERROR_EXIT, "When specifying an operation, the relpath is required",
+                Seq("-p", "__ow_meta_namespace", "_", "-p", "basepath", "/ApiGwRoutemgmtActionTests_bp", "-p", "operation", "get")),
+
+            //createApi
+            ("/whisk.system/routemgmt/createApi", ANY_ERROR_EXIT, "apidoc is required", Seq("-p", "__ow_meta_namespace", "_")),
+            ("/whisk.system/routemgmt/createApi", ANY_ERROR_EXIT, "apidoc is missing the namespace field",
+                Seq("-p", "__ow_meta_namespace", "_", "-p", "apidoc", "{}")),
+            ("/whisk.system/routemgmt/createApi", ANY_ERROR_EXIT, "apidoc is missing the gatewayBasePath field",
+                Seq("-p", "__ow_meta_namespace", "_", "-p", "apidoc", """{"namespace":"_"}""")),
+            ("/whisk.system/routemgmt/createApi", ANY_ERROR_EXIT, "apidoc is missing the gatewayPath field",
+                Seq("-p", "__ow_meta_namespace", "_", "-p", "apidoc", """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp"}""")),
+            ("/whisk.system/routemgmt/createApi", ANY_ERROR_EXIT, "apidoc is missing the gatewayMethod field",
+                Seq("-p", "__ow_meta_namespace", "_", "-p", "apidoc", """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp","gatewayPath":"ApiGwRoutemgmtActionTests_rp"}""")),
+            ("/whisk.system/routemgmt/createApi", ANY_ERROR_EXIT, "apidoc is missing the action field",
+                Seq("-p", "__ow_meta_namespace", "_", "-p", "apidoc", """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp","gatewayPath":"ApiGwRoutemgmtActionTests_rp","gatewayMethod":"get"}""")),
+            ("/whisk.system/routemgmt/createApi", ANY_ERROR_EXIT, "action is missing the backendMethod field",
+                Seq("-p", "__ow_meta_namespace", "_", "-p", "apidoc", """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp","gatewayPath":"ApiGwRoutemgmtActionTests_rp","gatewayMethod":"get","action":{}}""")),
+            ("/whisk.system/routemgmt/createApi", ANY_ERROR_EXIT, "action is missing the backendUrl field",
+                Seq("-p", "__ow_meta_namespace", "_", "-p", "apidoc", """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp","gatewayPath":"ApiGwRoutemgmtActionTests_rp","gatewayMethod":"get","action":{"backendMethod":"post"}}""")),
+            ("/whisk.system/routemgmt/createApi", ANY_ERROR_EXIT, "action is missing the namespace field",
+                Seq("-p", "__ow_meta_namespace", "_", "-p", "apidoc", """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp","gatewayPath":"ApiGwRoutemgmtActionTests_rp","gatewayMethod":"get","action":{"backendMethod":"post","backendUrl":"URL"}}""")),
+            ("/whisk.system/routemgmt/createApi", ANY_ERROR_EXIT, "action is missing the name field",
+                Seq("-p", "__ow_meta_namespace", "_", "-p", "apidoc", """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp","gatewayPath":"ApiGwRoutemgmtActionTests_rp","gatewayMethod":"get","action":{"backendMethod":"post","backendUrl":"URL","namespace":"_"}}""")),
+            ("/whisk.system/routemgmt/createApi", ANY_ERROR_EXIT, "action is missing the authkey field",
+                Seq("-p", "__ow_meta_namespace", "_", "-p", "apidoc", """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp","gatewayPath":"ApiGwRoutemgmtActionTests_rp","gatewayMethod":"get","action":{"backendMethod":"post","backendUrl":"URL","namespace":"_","name":"N"}}""")),
+            ("/whisk.system/routemgmt/createApi", ANY_ERROR_EXIT, "swagger and gatewayBasePath are mutually exclusive and cannot be specified together",
+                Seq("-p", "__ow_meta_namespace", "_", "-p", "apidoc", """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp","gatewayPath":"ApiGwRoutemgmtActionTests_rp","gatewayMethod":"get","action":{"backendMethod":"post","backendUrl":"URL","namespace":"_","name":"N","authkey":"XXXX"},"swagger":{}}""")),
+            ("/whisk.system/routemgmt/createApi", ANY_ERROR_EXIT, "apidoc field cannot be parsed. Ensure it is valid JSON",
+                Seq("-p", "__ow_meta_namespace", "_", "-p", "apidoc", "{1:[}}}")))
+
+        invalidArgs foreach {
+            case (action: String, exitcode: Int, errmsg: String, params: Seq[String]) =>
+                val cmd: Seq[String] = Seq("action",
+                    "invoke",
+                    action,
+                    "-i", "-b", "-r",
+                    "--apihost", wskprops.apihost,
+                    "--auth", wskprops.authKey) ++ params
+                val rr = wsk.cli(cmd, expectedExitCode = exitcode)
+                rr.stderr should include regex (errmsg)
+        }
+    }
+}
diff --git a/tests/src/test/scala/whisk/core/cli/test/ApiGwTests.scala b/tests/src/test/scala/whisk/core/cli/test/ApiGwTests.scala
new file mode 100644
index 0000000..eae638e
--- /dev/null
+++ b/tests/src/test/scala/whisk/core/cli/test/ApiGwTests.scala
@@ -0,0 +1,511 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * 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.
+ */
+
+package whisk.core.cli.test
+
+import java.time.Instant
+import scala.concurrent.duration._
+import org.junit.runner.RunWith
+import org.scalatest.BeforeAndAfterAll
+import org.scalatest.junit.JUnitRunner
+import common.TestHelpers
+import common.TestUtils._
+import common.TestUtils
+import common.WhiskProperties
+import common.Wsk
+import common.WskAdmin
+import common.WskProps
+import common.WskTestHelpers
+
+/**
+ * Tests for testing the CLI "api" subcommand.  Most of these tests require a deployed backend.
+ */
+@RunWith(classOf[JUnitRunner])
+class ApiGwTests
+    extends TestHelpers
+    with WskTestHelpers
+    with BeforeAndAfterAll {
+
+    implicit val wskprops = WskProps()
+    val wsk = new Wsk
+    val (cliuser, clinamespace) = WskAdmin.getUser(wskprops.authKey)
+
+    // This test suite makes enough CLI invocations in 60 seconds to trigger the OpenWhisk
+    // throttling restriction.  To avoid CLI failures due to being throttled, track the
+    // CLI invocation calls and when at the throttle limit, pause the next CLI invocation
+    // with exactly enough time to relax the throttling.
+    val throttleWindow = 1.minute
+    var cliCallCount = 5  // Set to >0 to allow for other action invocations in prior tests
+    var clearedThrottleTime = Instant.now
+    val maxActionsPerMin = WhiskProperties.getMaxActionInvokesPerMinute()
+
+    /**
+      * Expected to be called before or after each CLI invocation
+      * If number of CLI invocations in this suite have reached the throttle limit
+      * then pause the test for enough time so that the throttle restriction is gone
+      */
+    def checkThrottle() = {
+      // If the # CLI calls at the throttle limit, then wait enough time to avoid the CLI being blocked
+      cliCallCount += 1
+      if ( cliCallCount > maxActionsPerMin ) {
+        println(s"Action invokes ${cliCallCount} exceeds per minute thottle limit of ${maxActionsPerMin}")
+        val waitedAlready = Duration.fromNanos(java.time.Duration.between(clearedThrottleTime, Instant.now).toNanos)
+        settleThrottle(waitedAlready)
+        cliCallCount = 0
+        clearedThrottleTime = Instant.now
+      }
+    }
+
+    /**
+     * Settles throttles of 1 minute. Waits up to 1 minute depending on the time already waited.
+     *
+     * @param waitedAlready the time already gone after the last invoke or fire
+     */
+    def settleThrottle(waitedAlready: FiniteDuration) = {
+      val timeToWait = (throttleWindow - waitedAlready).max(Duration.Zero)
+      println(s"Waiting for ${timeToWait.toSeconds} seconds, already waited for ${waitedAlready.toSeconds} seconds")
+      Thread.sleep(timeToWait.toMillis)
+    }
+
+    /*
+     * Forcibly clear the throttle so that downstream tests are not affected by
+     * this test suite
+     */
+    override def afterAll() = {
+      // If this test suite is exiting with over 30 action invocations since the last throttle clearing, clear the throttle
+      if (cliCallCount > 30) {
+        val waitedAlready = Duration.fromNanos(java.time.Duration.between(clearedThrottleTime, Instant.now).toNanos)
+        settleThrottle(waitedAlready)
+      }
+    }
+
+    def apiCreate(
+      basepath: Option[String] = None,
+      relpath: Option[String] = None,
+      operation: Option[String] = None,
+      action: Option[String] = None,
+      apiname: Option[String] = None,
+      swagger: Option[String] = None,
+      expectedExitCode: Int = SUCCESS_EXIT): RunResult = {
+        checkThrottle()
+        wsk.api.create(basepath, relpath, operation, action, apiname, swagger, expectedExitCode)
+    }
+
+    def apiList(
+      basepathOrApiName: Option[String] = None,
+      relpath: Option[String] = None,
+      operation: Option[String] = None,
+      limit: Option[Int] = None,
+      since: Option[Instant] = None,
+      full: Option[Boolean] = None,
+      expectedExitCode: Int = SUCCESS_EXIT): RunResult = {
+        checkThrottle()
+        wsk.api.list(basepathOrApiName, relpath, operation, limit, since, full, expectedExitCode)
+    }
+
+    def apiGet(
+      basepathOrApiName: Option[String] = None,
+      full: Option[Boolean] = None,
+      expectedExitCode: Int = SUCCESS_EXIT): RunResult = {
+        checkThrottle()
+        wsk.api.get(basepathOrApiName, full, expectedExitCode)
+    }
+
+    def apiDelete(
+      basepathOrApiName: String,
+      relpath: Option[String] = None,
+      operation: Option[String] = None,
+      expectedExitCode: Int = SUCCESS_EXIT): RunResult = {
+        checkThrottle()
+        wsk.api.delete(basepathOrApiName, relpath, operation, expectedExitCode)
+    }
+
+    behavior of "Wsk api"
+
+    it should "reject an api commands with an invalid path parameter" in {
+        val badpath = "badpath"
+
+        var rr = apiCreate(basepath = Some("/basepath"), relpath = Some(badpath), operation = Some("GET"), action = Some("action"), expectedExitCode = ANY_ERROR_EXIT)
+        rr.stderr should include (s"'${badpath}' must begin with '/'")
+
+        rr = apiDelete(basepathOrApiName = "/basepath", relpath = Some(badpath), operation = Some("GET"), expectedExitCode = ANY_ERROR_EXIT)
+        rr.stderr should include (s"'${badpath}' must begin with '/'")
+
+        rr = apiList(basepathOrApiName = Some("/basepath"), relpath = Some(badpath), operation = Some("GET"), expectedExitCode = ANY_ERROR_EXIT)
+        rr.stderr should include (s"'${badpath}' must begin with '/'")
+    }
+
+    it should "verify full list output" in {
+      val testName = "CLI_APIGWTEST_RO1"
+      val testbasepath = "/" + testName + "_bp"
+      val testrelpath = "/path"
+      val testnewrelpath = "/path_new"
+      val testurlop = "get"
+      val testapiname = testName + " API Name"
+      val actionName = testName + "_action"
+      try {
+        println("cli user: " + cliuser + "; cli namespace: " + clinamespace)
+
+        var rr = apiCreate(basepath = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname))
+        println("api create: " + rr.stdout)
+        rr.stdout should include("ok: created API")
+        rr = apiList(basepathOrApiName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop), full = Some(true))
+        println("api list: " + rr.stdout)
+        rr.stdout should include("ok: APIs")
+        rr.stdout should include regex (s"Action:\\s+/${clinamespace}/${actionName}\n")
+        rr.stdout should include regex (s"Verb:\\s+${testurlop}\n")
+        rr.stdout should include regex (s"Base path:\\s+${testbasepath}\n")
+        rr.stdout should include regex (s"Path:\\s+${testrelpath}\n")
+        rr.stdout should include regex (s"API Name:\\s+${testapiname}\n")
+        rr.stdout should include regex (s"URL:\\s+")
+        rr.stdout should include(testbasepath + testrelpath)
+      }
+      finally {
+        val deleteresult = apiDelete(basepathOrApiName = testbasepath)
+      }
+    }
+
+    it should "verify successful creation and deletion of a new API" in {
+        val testName = "CLI_APIGWTEST1"
+        val testbasepath = "/"+testName+"_bp"
+        val testrelpath = "/path"
+        val testnewrelpath = "/path_new"
+        val testurlop = "get"
+        val testapiname = testName+" API Name"
+        val actionName = testName+"_action"
+        try {
+            println("cli user: "+cliuser+"; cli namespace: "+clinamespace)
+
+            var rr = apiCreate(basepath = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname))
+            rr.stdout should include("ok: created API")
+            rr = apiList(basepathOrApiName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop))
+            rr.stdout should include("ok: APIs")
+            rr.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+")
+            rr.stdout should include(testbasepath + testrelpath)
+            val deleteresult = apiDelete(basepathOrApiName = testbasepath)
+            deleteresult.stdout should include("ok: deleted API")
+        }
+        finally {
+            val deleteresult = apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)
+        }
+    }
+
+    it should "verify get API name " in {
+        val testName = "CLI_APIGWTEST3"
+        val testbasepath = "/"+testName+"_bp"
+        val testrelpath = "/path"
+        val testnewrelpath = "/path_new"
+        val testurlop = "get"
+        val testapiname = testName+" API Name"
+        val actionName = testName+"_action"
+        try {
+            var rr = apiCreate(basepath = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname))
+            rr.stdout should include("ok: created API")
+            rr = apiGet(basepathOrApiName = Some(testapiname))
+            rr.stdout should include(testbasepath)
+            rr.stdout should include(s"${actionName}")
+        }
+        finally {
+            val deleteresult = apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)
+        }
+    }
+
+    it should "verify delete API name " in {
+      val testName = "CLI_APIGWTEST4"
+      val testbasepath = "/"+testName+"_bp"
+      val testrelpath = "/path"
+      val testnewrelpath = "/path_new"
+      val testurlop = "get"
+      val testapiname = testName+" API Name"
+      val actionName = testName+"_action"
+      try {
+        var rr = apiCreate(basepath = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname))
+        rr.stdout should include("ok: created API")
+        rr = apiDelete(basepathOrApiName = testapiname)
+        rr.stdout should include("ok: deleted API")
+      }
+      finally {
+        val deleteresult = apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)
+      }
+    }
+
+    it should "verify delete API basepath " in {
+      val testName = "CLI_APIGWTEST5"
+      val testbasepath = "/"+testName+"_bp"
+      val testrelpath = "/path"
+      val testnewrelpath = "/path_new"
+      val testurlop = "get"
+      val testapiname = testName+" API Name"
+      val actionName = testName+"_action"
+      try {
+        var rr = apiCreate(basepath = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname))
+        rr.stdout should include("ok: created API")
+        rr = apiDelete(basepathOrApiName = testbasepath)
+        rr.stdout should include("ok: deleted API")
+      }
+      finally {
+        val deleteresult = apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)
+      }
+    }
+
+    it should "verify adding endpoints to existing api" in {
+      val testName = "CLI_APIGWTEST6"
+      val testbasepath = "/"+testName+"_bp"
+      val testrelpath = "/path2"
+      val testnewrelpath = "/path_new"
+      val testurlop = "get"
+      val testapiname = testName+" API Name"
+      val actionName = testName+"_action"
+      val newEndpoint = "/newEndpoint"
+      try {
+        var rr = apiCreate(basepath = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname))
+        rr.stdout should include("ok: created API")
+        rr = apiCreate(basepath = Some(testbasepath), relpath = Some(newEndpoint), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname))
+        rr.stdout should include("ok: created API")
+        rr = apiList(basepathOrApiName = Some(testbasepath))
+        rr.stdout should include("ok: APIs")
+        rr.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+")
+        rr.stdout should include(testbasepath + testrelpath)
+        rr.stdout should include(testbasepath + newEndpoint)
+      }
+      finally {
+        val deleteresult = apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)
+      }
+    }
+
+    it should "verify successful creation with swagger doc as input" in {
+      // NOTE: These values must match the swagger file contents
+      val testName = "CLI_APIGWTEST7"
+      val testbasepath = "/"+testName+"_bp"
+      val testrelpath = "/path"
+      val testurlop = "get"
+      val testapiname = testName+" API Name"
+      val actionName = testName+"_action"
+      val swaggerPath = TestUtils.getTestApiGwFilename("testswaggerdoc1")
+      try {
+        var rr = apiCreate(swagger = Some(swaggerPath))
+        rr.stdout should include("ok: created API")
+        rr = apiList(basepathOrApiName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop))
+        println("list stdout: "+rr.stdout)
+        println("list stderr: "+rr.stderr)
+        rr.stdout should include("ok: APIs")
+        // Actual CLI namespace will vary from local dev to automated test environments, so don't check
+        rr.stdout should include regex (s"/[@\\w._\\-]+/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+")
+        rr.stdout should include(testbasepath + testrelpath)
+      }
+      finally {
+        val deleteresult = apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)
+      }
+    }
+
+    it should "verify adding endpoints to two existing apis" in {
+      val testName = "CLI_APIGWTEST8"
+      val testbasepath = "/"+testName+"_bp"
+      val testbasepath2 = "/"+testName+"_bp2"
+      val testrelpath = "/path2"
+      val testnewrelpath = "/path_new"
+      val testurlop = "get"
+      val testapiname = testName+" API Name"
+      val actionName = testName+"_action"
+      val newEndpoint = "/newEndpoint"
+      try {
+        var rr = apiCreate(basepath = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname))
+        rr.stdout should include("ok: created API")
+        rr = apiCreate(basepath = Some(testbasepath2), relpath = Some(testrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname))
+        rr.stdout should include("ok: created API")
+
+        // Update both APIs - each with a new endpoint
+        rr = apiCreate(basepath = Some(testbasepath), relpath = Some(newEndpoint), operation = Some(testurlop), action = Some(actionName))
+        rr.stdout should include("ok: created API")
+        rr = apiCreate(basepath = Some(testbasepath2), relpath = Some(newEndpoint), operation = Some(testurlop), action = Some(actionName))
+        rr.stdout should include("ok: created API")
+
+        rr = apiList(basepathOrApiName = Some(testbasepath))
+        rr.stdout should include("ok: APIs")
+        rr.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+")
+        rr.stdout should include(testbasepath + testrelpath)
+        rr.stdout should include(testbasepath + newEndpoint)
+
+        rr = apiList(basepathOrApiName = Some(testbasepath2))
+        rr.stdout should include("ok: APIs")
+        rr.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+")
+        rr.stdout should include(testbasepath2 + testrelpath)
+        rr.stdout should include(testbasepath2 + newEndpoint)
+      }
+      finally {
+        var deleteresult = apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)
+        deleteresult = apiDelete(basepathOrApiName = testbasepath2, expectedExitCode = DONTCARE_EXIT)
+      }
+    }
+
+    it should "verify successful creation of a new API using an action name using all allowed characters" in {
+      // Be aware: full action name is close to being truncated by the 'list' command
+      // e.g. /lime@us.ibm.com/CLI_APIGWTEST9a-c@t ion  is currently at the 40 char 'list' display max
+      val testName = "CLI_APIGWTEST9"
+      val testbasepath = "/" + testName + "_bp"
+      val testrelpath = "/path"
+      val testnewrelpath = "/path_new"
+      val testurlop = "get"
+      val testapiname = testName+" API Name"
+      val actionName = testName+"a-c@t ion"
+      try {
+        println("cli user: "+cliuser+"; cli namespace: "+clinamespace)
+
+        var rr = apiCreate(basepath = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname))
+        rr.stdout should include("ok: created API")
+        rr = apiList(basepathOrApiName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop))
+        rr.stdout should include("ok: APIs")
+        rr.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+")
+        rr.stdout should include(testbasepath + testrelpath)
+        val deleteresult = apiDelete(basepathOrApiName = testbasepath)
+        deleteresult.stdout should include("ok: deleted API")
+      }
+      finally {
+        val deleteresult = apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)
+      }
+    }
+
+    it should "verify failed creation with invalid swagger doc as input" in {
+      val testName = "CLI_APIGWTEST10"
+      val testbasepath = "/" + testName + "_bp"
+      val testrelpath = "/path"
+      val testnewrelpath = "/path_new"
+      val testurlop = "get"
+      val testapiname = testName + " API Name"
+      val actionName = testName + "_action"
+      val swaggerPath = TestUtils.getTestApiGwFilename(s"testswaggerdocinvalid")
+      try {
+        var rr = apiCreate(swagger = Some(swaggerPath), expectedExitCode = ANY_ERROR_EXIT)
+        println("api create stdout: " + rr.stdout)
+        println("api create stderr: " + rr.stderr)
+        rr.stderr should include(s"Swagger file is invalid")
+      } finally {
+        val deleteresult = apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)
+      }
+    }
+
+    it should "verify delete basepath/path " in {
+      val testName = "CLI_APIGWTEST11"
+      val testbasepath = "/" + testName + "_bp"
+      val testrelpath = "/path"
+      val testnewrelpath = "/path_new"
+      val testurlop = "get"
+      val testapiname = testName + " API Name"
+      val actionName = testName + "_action"
+      try {
+        var rr = apiCreate(basepath = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname))
+        rr.stdout should include("ok: created API")
+        var rr2 = apiCreate(basepath = Some(testbasepath), relpath = Some(testnewrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname))
+        rr2.stdout should include("ok: created API")
+        rr = apiDelete(basepathOrApiName = testbasepath, relpath = Some(testrelpath))
+        rr.stdout should include("ok: deleted " + testrelpath +" from "+ testbasepath)
+        rr2 = apiList(basepathOrApiName = Some(testbasepath), relpath = Some(testnewrelpath))
+        rr2.stdout should include("ok: APIs")
+        rr2.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+")
+        rr2.stdout should include(testbasepath + testnewrelpath)
+      } finally {
+        val deleteresult = apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)
+      }
+    }
+
+    it should "verify delete single operation from existing API basepath/path/operation(s) " in {
+      val testName = "CLI_APIGWTEST12"
+      val testbasepath = "/" + testName + "_bp"
+      val testrelpath = "/path2"
+      val testnewrelpath = "/path_new"
+      val testurlop = "get"
+      val testurlop2 = "post"
+      val testapiname = testName + " API Name"
+      val actionName = testName + "_action"
+      try {
+        var rr = apiCreate(basepath = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname))
+        rr.stdout should include("ok: created API")
+        rr = apiCreate(basepath = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop2), action = Some(actionName), apiname = Some(testapiname))
+        rr.stdout should include("ok: created API")
+        rr = apiList(basepathOrApiName = Some(testbasepath))
+        rr.stdout should include("ok: APIs")
+        rr.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+")
+        rr.stdout should include(testbasepath + testrelpath)
+        rr = apiDelete(basepathOrApiName = testbasepath,relpath = Some(testrelpath), operation = Some(testurlop2))
+        rr.stdout should include("ok: deleted " + testrelpath + " " + "POST" +" from "+ testbasepath)
+        rr = apiList(basepathOrApiName = Some(testbasepath))
+        rr.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+")
+      } finally {
+        val deleteresult = apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)
+      }
+    }
+
+    it should "verify successful creation with complex swagger doc as input" in {
+      val testName = "CLI_APIGWTEST13"
+      val testbasepath = "/test1/v1"
+      val testrelpath = "/whisk.system/utils/echo"
+      val testrelpath2 = "/whisk.system/utils/split"
+      val testurlop = "get"
+      val testapiname = "/test1/v1"
+      val actionName = "test1a"
+      val swaggerPath = TestUtils.getTestApiGwFilename(s"testswaggerdoc2")
+      try {
+        var rr = apiCreate(swagger = Some(swaggerPath))
+        println("api create stdout: " + rr.stdout)
+        println("api create stderror: " + rr.stderr)
+        rr.stdout should include("ok: created API")
+        rr = apiList(basepathOrApiName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop))
+        rr.stdout should include("ok: APIs")
+        // Actual CLI namespace will vary from local dev to automated test environments, so don't check
+        rr.stdout should include regex (s"/[@\\w._\\-]+/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+")
+        rr.stdout should include(testbasepath + testrelpath)
+        rr.stdout should include(testbasepath + testrelpath2)
+      } finally {
+        val deleteresult = apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)
+      }
+    }
+
+    it should "verify successful creation and deletion with multiple base paths" in {
+      val testName = "CLI_APIGWTEST14"
+      val testbasepath = "/" + testName + "_bp"
+      val testbasepath2 = "/" + testName + "_bp2"
+      val testrelpath = "/path"
+      val testnewrelpath = "/path_new"
+      val testurlop = "get"
+      val testapiname = testName + " API Name"
+      val actionName = testName + "_action"
+      try {
+        var rr = apiCreate(basepath = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname))
+        rr.stdout should include("ok: created API")
+        rr = apiList(basepathOrApiName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop))
+        rr.stdout should include("ok: APIs")
+        rr.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+")
+        rr.stdout should include(testbasepath + testrelpath)
+        rr = apiCreate(basepath = Some(testbasepath2), relpath = Some(testrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname))
+        rr.stdout should include("ok: created API")
+        rr = apiList(basepathOrApiName = Some(testbasepath2), relpath = Some(testrelpath), operation = Some(testurlop))
+        rr.stdout should include("ok: APIs")
+        rr.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+")
+        rr.stdout should include(testbasepath2 + testrelpath)
+        rr = apiDelete(basepathOrApiName = testbasepath2)
+        rr.stdout should include("ok: deleted API")
+        rr = apiList(basepathOrApiName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop))
+        rr.stdout should include("ok: APIs")
+        rr.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+")
+        rr.stdout should include(testbasepath + testrelpath)
+        rr = apiDelete(basepathOrApiName = testbasepath)
+        rr.stdout should include("ok: deleted API")
+      } finally {
+        var deleteresult = apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)
+        deleteresult = apiDelete(basepathOrApiName = testbasepath2, expectedExitCode = DONTCARE_EXIT)
+      }
+    }
+}
diff --git a/tests/src/test/scala/whisk/core/cli/test/WskActionSequenceTests.scala b/tests/src/test/scala/whisk/core/cli/test/WskActionSequenceTests.scala
new file mode 100644
index 0000000..99297f9
--- /dev/null
+++ b/tests/src/test/scala/whisk/core/cli/test/WskActionSequenceTests.scala
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * 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.
+ */
+
+package whisk.core.cli.test
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+import common.TestHelpers
+import common.TestUtils
+import common.Wsk
+import common.WskAdmin
+import common.WskProps
+import common.WskTestHelpers
+import spray.json._
+
+/**
+ * Tests creation and retrieval of a sequence action
+ */
+@RunWith(classOf[JUnitRunner])
+class WskActionSequenceTests
+    extends TestHelpers
+    with WskTestHelpers {
+
+    implicit val wskprops = WskProps()
+    val wsk = new Wsk
+    val defaultNamespace = wskprops.namespace
+    val (user, namespace) = WskAdmin.getUser(wskprops.authKey)
+
+    behavior of "Wsk Action Sequence"
+
+    it should "create, and get an action sequence" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "actionSeq"
+            val packageName = "samples"
+            val helloName = "hello"
+            val catName = "cat"
+            val fullHelloActionName = s"/$defaultNamespace/$packageName/$helloName"
+            val fullCatActionName = s"/$defaultNamespace/$packageName/$catName"
+
+            assetHelper.withCleaner(wsk.pkg, packageName) {
+                (pkg, _) => pkg.create(packageName, shared = Some(true))(wp)
+            }
+
+            assetHelper.withCleaner(wsk.action, fullHelloActionName) {
+                val file = Some(TestUtils.getTestActionFilename("hello.js"))
+                (action, _) => action.create(fullHelloActionName, file)(wp)
+            }
+
+            assetHelper.withCleaner(wsk.action, fullCatActionName) {
+                val file = Some(TestUtils.getTestActionFilename("cat.js"))
+                (action, _) => action.create(fullCatActionName, file)(wp)
+            }
+
+            val artifacts = s"$fullHelloActionName,$fullCatActionName"
+            val kindValue = JsString("sequence")
+            val compValue = JsArray(
+                JsString(resolveDefaultNamespace(fullHelloActionName)),
+                JsString(resolveDefaultNamespace(fullCatActionName)))
+
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, Some(artifacts), kind = Some("sequence"))
+            }
+
+            val stdout = wsk.action.get(name).stdout
+            assert(stdout.startsWith(s"ok: got action $name\n"))
+            wsk.parseJsonString(stdout).fields("exec").asJsObject.fields("components") shouldBe compValue
+            wsk.parseJsonString(stdout).fields("exec").asJsObject.fields("kind") shouldBe kindValue
+    }
+
+    private def resolveDefaultNamespace(actionName: String) = actionName.replace("/_/", s"/$namespace/")
+}
diff --git a/tests/src/test/scala/whisk/core/cli/test/WskBasicUsageTests.scala b/tests/src/test/scala/whisk/core/cli/test/WskBasicUsageTests.scala
new file mode 100644
index 0000000..e421df8
--- /dev/null
+++ b/tests/src/test/scala/whisk/core/cli/test/WskBasicUsageTests.scala
@@ -0,0 +1,1245 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * 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.
+ */
+
+package whisk.core.cli.test
+
+import java.io.File
+import java.io.BufferedWriter
+import java.io.FileWriter
+import java.time.Instant
+
+import scala.language.postfixOps
+import scala.concurrent.duration.Duration
+import scala.concurrent.duration.DurationInt
+import scala.util.Random
+
+import org.apache.commons.io.FileUtils
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+import common.TestHelpers
+import common.TestUtils
+import common.TestUtils._
+import common.WhiskProperties
+import common.Wsk
+import common.WskProps
+import common.WskTestHelpers
+import spray.json.DefaultJsonProtocol._
+import spray.json._
+import whisk.core.entity._
+import whisk.core.entity.LogLimit._
+import whisk.core.entity.MemoryLimit._
+import whisk.core.entity.TimeLimit._
+import whisk.core.entity.size.SizeInt
+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.
+ */
+@RunWith(classOf[JUnitRunner])
+class WskBasicUsageTests
+    extends TestHelpers
+    with WskTestHelpers {
+
+    implicit val wskprops = WskProps()
+    val wsk = new Wsk
+    val defaultAction = Some(TestUtils.getTestActionFilename("hello.js"))
+
+    behavior of "Wsk CLI usage"
+
+    it should "confirm wsk exists" in {
+        Wsk.exists
+    }
+
+    it should "show help and usage info" in {
+        val stdout = wsk.cli(Seq("-h")).stdout
+        stdout should include regex ("""(?i)Usage:""")
+        stdout should include regex ("""(?i)Flags""")
+        stdout should include regex ("""(?i)Available commands""")
+        stdout should include regex ("""(?i)--help""")
+    }
+
+    it should "show help and usage info using the default language" in {
+        val env = Map("LANG" -> "de_DE")
+        // Call will fail with exit code 2 if language not supported
+        wsk.cli(Seq("-h"), env = env)
+    }
+
+    it should "show cli build version" in {
+        val stdout = wsk.cli(Seq("property", "get", "--cliversion")).stdout
+        stdout should include regex ("""(?i)whisk CLI version\s+201.*""")
+    }
+
+    it should "show api version" in {
+        val stdout = wsk.cli(Seq("property", "get", "--apiversion")).stdout
+        stdout should include regex ("""(?i)whisk API version\s+v1""")
+    }
+
+    it should "set apihost, auth, and namespace" in {
+        val tmpwskprops = File.createTempFile("wskprops", ".tmp")
+        try {
+            val namespace = wsk.namespace.list().stdout.trim.split("\n").last
+            val env = Map("WSK_CONFIG_FILE" -> tmpwskprops.getAbsolutePath())
+            val stdout = wsk.cli(Seq("property", "set", "-i", "--apihost", wskprops.apihost, "--auth", wskprops.authKey,
+                "--namespace", namespace), env = env).stdout
+            stdout should include(s"ok: whisk auth set to ${wskprops.authKey}")
+            stdout should include(s"ok: whisk API host set to ${wskprops.apihost}")
+            stdout should include(s"ok: whisk namespace set to ${namespace}")
+        } finally {
+            tmpwskprops.delete()
+        }
+    }
+
+    it should "ensure default namespace is used when a blank namespace is set" in {
+        val tmpwskprops = File.createTempFile("wskprops", ".tmp")
+        try {
+            val writer = new BufferedWriter(new FileWriter(tmpwskprops))
+            writer.write(s"NAMESPACE=")
+            writer.close()
+            val env = Map("WSK_CONFIG_FILE" -> tmpwskprops.getAbsolutePath())
+            val stdout = wsk.cli(Seq("property", "get", "-i", "--namespace"), env = env).stdout
+            stdout should include regex ("whisk namespace\\s+_")
+        } finally {
+            tmpwskprops.delete()
+        }
+    }
+
+    it should "show api build version using property file" in {
+        val tmpwskprops = File.createTempFile("wskprops", ".tmp")
+        try {
+            val env = Map("WSK_CONFIG_FILE" -> tmpwskprops.getAbsolutePath())
+            wsk.cli(Seq("property", "set", "-i") ++ wskprops.overrides, env = env)
+            val stdout = wsk.cli(Seq("property", "get", "--apibuild", "-i"), env = env).stdout
+            stdout should include regex ("""(?i)whisk API build\s+201.*""")
+        } finally {
+            tmpwskprops.delete()
+        }
+    }
+
+    it should "fail to show api build when setting apihost to bogus value" in {
+        val tmpwskprops = File.createTempFile("wskprops", ".tmp")
+        try {
+            val env = Map("WSK_CONFIG_FILE" -> tmpwskprops.getAbsolutePath())
+            wsk.cli(Seq("property", "set", "-i", "--apihost", "xxxx.yyyy"), env = env)
+            val rr = wsk.cli(Seq("property", "get", "--apibuild", "-i"), env = env, expectedExitCode = ANY_ERROR_EXIT)
+            rr.stdout should include regex ("""whisk API build\s*Unknown""")
+            rr.stderr should include regex ("Unable to obtain API build information")
+        } finally {
+            tmpwskprops.delete()
+        }
+    }
+
+    it should "show api build using http apihost" in {
+        val tmpwskprops = File.createTempFile("wskprops", ".tmp")
+        try {
+            val env = Map("WSK_CONFIG_FILE" -> tmpwskprops.getAbsolutePath())
+            val apihost = s"http://${WhiskProperties.getControllerHost}:${WhiskProperties.getControllerPort}"
+            wsk.cli(Seq("property", "set", "--apihost", apihost), env = env)
+            val rr = wsk.cli(Seq("property", "get", "--apibuild", "-i"), env = env)
+            rr.stdout should not include regex("""whisk API build\s*Unknown""")
+            rr.stderr should not include regex("Unable to obtain API build information")
+            rr.stdout should include regex ("""(?i)whisk API build\s+201.*""")
+        } finally {
+            tmpwskprops.delete()
+        }
+    }
+
+    it should "validate default property values" in {
+        val tmpwskprops = File.createTempFile("wskprops", ".tmp")
+        val env = Map("WSK_CONFIG_FILE" -> tmpwskprops.getAbsolutePath())
+        val stdout = wsk.cli(Seq("property", "unset", "--auth", "--apihost", "--apiversion", "--namespace"), env = env).stdout
+        try {
+            stdout should include regex ("ok: whisk auth unset")
+            stdout should include regex ("ok: whisk API host unset")
+            stdout should include regex ("ok: whisk API version unset")
+            stdout should include regex ("ok: whisk namespace unset")
+
+            wsk.cli(Seq("property", "get", "--auth"), env = env).
+                stdout should include regex ("""(?i)whisk auth\s*$""") // default = empty string
+            wsk.cli(Seq("property", "get", "--apihost"), env = env).
+                stdout should include regex ("""(?i)whisk API host\s*$""") // default = empty string
+            wsk.cli(Seq("property", "get", "--namespace"), env = env).
+                stdout should include regex ("""(?i)whisk namespace\s*_$""") // default = _
+        } finally {
+            tmpwskprops.delete()
+        }
+    }
+
+    it should "set auth in property file" in {
+        val tmpwskprops = File.createTempFile("wskprops", ".tmp")
+        val env = Map("WSK_CONFIG_FILE" -> tmpwskprops.getAbsolutePath())
+        wsk.cli(Seq("property", "set", "--auth", "testKey"), env = env)
+        try {
+            val fileContent = FileUtils.readFileToString(tmpwskprops)
+            fileContent should include("AUTH=testKey")
+        } finally {
+            tmpwskprops.delete()
+        }
+    }
+
+    it should "set multiple property values with single command" in {
+        val tmpwskprops = File.createTempFile("wskprops", ".tmp")
+        val env = Map("WSK_CONFIG_FILE" -> tmpwskprops.getAbsolutePath())
+        val stdout = wsk.cli(Seq("property", "set", "--auth", "testKey", "--apihost", "openwhisk.ng.bluemix.net", "--apiversion", "v1"), env = env).stdout
+        try {
+            stdout should include regex ("ok: whisk auth set")
+            stdout should include regex ("ok: whisk API host set")
+            stdout should include regex ("ok: whisk API version set")
+            val fileContent = FileUtils.readFileToString(tmpwskprops)
+            fileContent should include("AUTH=testKey")
+            fileContent should include("APIHOST=openwhisk.ng.bluemix.net")
+            fileContent should include("APIVERSION=v1")
+        } finally {
+            tmpwskprops.delete()
+        }
+    }
+
+    it should "reject bad command" in {
+        val result = wsk.cli(Seq("bogus"), expectedExitCode = ERROR_EXIT)
+        result.stderr should include regex ("""(?i)Run 'wsk --help' for usage""")
+    }
+
+    it should "reject authenticated command when no auth key is given" in {
+        // override wsk props file in case it exists
+        val tmpwskprops = File.createTempFile("wskprops", ".tmp")
+        val env = Map("WSK_CONFIG_FILE" -> tmpwskprops.getAbsolutePath())
+        val stderr = wsk.cli(Seq("list") ++ wskprops.overrides, env = env, expectedExitCode = MISUSE_EXIT).stderr
+        try {
+            stderr should include regex (s"usage[:.]") // Python CLI: "usage:", Go CLI: "usage."
+            stderr should include("--auth is required")
+        } finally {
+            tmpwskprops.delete()
+        }
+    }
+
+    it should "reject a command when the API host is not set" in {
+        val tmpwskprops = File.createTempFile("wskprops", ".tmp")
+        try {
+            val env = Map("WSK_CONFIG_FILE" -> tmpwskprops.getAbsolutePath())
+            val stderr = wsk.cli(Seq("property", "get", "-i"), env = env, expectedExitCode = ERROR_EXIT).stderr
+            stderr should include("The API host is not valid: An API host must be provided.")
+        } finally {
+            tmpwskprops.delete()
+        }
+    }
+
+    behavior of "Wsk actions"
+
+    it should "reject creating entities with invalid names" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val names = Seq(
+                ("", NOT_ALLOWED),
+                (" ", BAD_REQUEST),
+                ("hi+there", BAD_REQUEST),
+                ("$hola", BAD_REQUEST),
+                ("dora?", BAD_REQUEST),
+                ("|dora|dora?", BAD_REQUEST))
+
+            names foreach {
+                case (name, ec) =>
+                    assetHelper.withCleaner(wsk.action, name, confirmDelete = false) {
+                        (action, _) => action.create(name, defaultAction, expectedExitCode = ec)
+                    }
+            }
+    }
+
+    it should "reject create with missing file" in {
+        wsk.action.create("missingFile", Some("notfound"),
+            expectedExitCode = MISUSE_EXIT).
+            stderr should include("not a valid file")
+    }
+
+    it should "reject action update when specified file is missing" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            // Create dummy action to update
+            val name = "updateMissingFile"
+            val file = Some(TestUtils.getTestActionFilename("hello.js"))
+            assetHelper.withCleaner(wsk.action, name) { (action, name) => action.create(name, file) }
+            // Update it with a missing file
+            wsk.action.create("updateMissingFile", Some("notfound"), update = true, expectedExitCode = MISUSE_EXIT)
+    }
+
+    it should "create, and get an action to verify parameter and annotation parsing" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "actionAnnotations"
+            val file = Some(TestUtils.getTestActionFilename("hello.js"))
+
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) =>
+                    action.create(name, file, annotations = getValidJSONTestArgInput,
+                        parameters = getValidJSONTestArgInput)
+            }
+
+            val stdout = wsk.action.get(name).stdout
+            assert(stdout.startsWith(s"ok: got action $name\n"))
+
+            val receivedParams = wsk.parseJsonString(stdout).fields("parameters").convertTo[JsArray].elements
+            val receivedAnnots = wsk.parseJsonString(stdout).fields("annotations").convertTo[JsArray].elements
+            val escapedJSONArr = getValidJSONTestArgOutput.convertTo[JsArray].elements
+
+            for (expectedItem <- escapedJSONArr) {
+                receivedParams should contain(expectedItem)
+                receivedAnnots should contain(expectedItem)
+            }
+    }
+
+    it should "create, and get an action to verify file parameter and annotation parsing" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "actionAnnotAndParamParsing"
+            val file = Some(TestUtils.getTestActionFilename("hello.js"))
+            val argInput = Some(TestUtils.getTestActionFilename("validInput1.json"))
+
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) =>
+                    action.create(name, file, annotationFile = argInput, parameterFile = argInput)
+            }
+
+            val stdout = wsk.action.get(name).stdout
+            assert(stdout.startsWith(s"ok: got action $name\n"))
+
+            val receivedParams = wsk.parseJsonString(stdout).fields("parameters").convertTo[JsArray].elements
+            val receivedAnnots = wsk.parseJsonString(stdout).fields("annotations").convertTo[JsArray].elements
+            val escapedJSONArr = getJSONFileOutput.convertTo[JsArray].elements
+
+            for (expectedItem <- escapedJSONArr) {
+                receivedParams should contain(expectedItem)
+                receivedAnnots should contain(expectedItem)
+            }
+    }
+
+    it should "create an action with the proper parameter and annotation escapes" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "actionEscapes"
+            val file = Some(TestUtils.getTestActionFilename("hello.js"))
+
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) =>
+                    action.create(name, file, parameters = getEscapedJSONTestArgInput,
+                        annotations = getEscapedJSONTestArgInput)
+            }
+
+            val stdout = wsk.action.get(name).stdout
+            assert(stdout.startsWith(s"ok: got action $name\n"))
+
+            val receivedParams = wsk.parseJsonString(stdout).fields("parameters").convertTo[JsArray].elements
+            val receivedAnnots = wsk.parseJsonString(stdout).fields("annotations").convertTo[JsArray].elements
+            val escapedJSONArr = getEscapedJSONTestArgOutput.convertTo[JsArray].elements
+
+            for (expectedItem <- escapedJSONArr) {
+                receivedParams should contain(expectedItem)
+                receivedAnnots should contain(expectedItem)
+            }
+    }
+
+    it should "invoke an action that exits during initialization and get appropriate error" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "abort init"
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("initexit.js")))
+            }
+
+            withActivation(wsk.activation, wsk.action.invoke(name)) {
+                activation =>
+                    val response = activation.response
+                    response.result.get.fields("error") shouldBe Messages.abnormalInitialization.toJson
+                    response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.ContainerError)
+            }
+    }
+
+    it should "invoke an action that hangs during initialization and get appropriate error" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "hang init"
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) =>
+                    action.create(
+                        name,
+                        Some(TestUtils.getTestActionFilename("initforever.js")),
+                        timeout = Some(3 seconds))
+            }
+
+            withActivation(wsk.activation, wsk.action.invoke(name)) {
+                activation =>
+                    val response = activation.response
+                    response.result.get.fields("error") shouldBe Messages.timedoutActivation(3 seconds, true).toJson
+                    response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.ApplicationError)
+            }
+    }
+
+    it should "invoke an action that exits during run and get appropriate error" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "abort run"
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("runexit.js")))
+            }
+
+            withActivation(wsk.activation, wsk.action.invoke(name)) {
+                activation =>
+                    val response = activation.response
+                    response.result.get.fields("error") shouldBe Messages.abnormalRun.toJson
+                    response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.ContainerError)
+            }
+    }
+
+    it should "ensure keys are not omitted from activation record" in withAssetCleaner(wskprops) {
+        val name = "activationRecordTest"
+
+        (wp, assetHelper) =>
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("argCheck.js")))
+            }
+
+            val run = wsk.action.invoke(name)
+            withActivation(wsk.activation, run) {
+                activation =>
+                    activation.start should be > Instant.EPOCH
+                    activation.end should be > Instant.EPOCH
+                    activation.response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.Success)
+                    activation.response.success shouldBe true
+                    activation.response.result shouldBe Some(JsObject())
+                    activation.logs shouldBe Some(List())
+                    activation.annotations shouldBe defined
+            }
+    }
+
+    it should "write the action-path and the limits to the annotations" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "annotations"
+            val memoryLimit = 512 MB
+            val logLimit = 1 MB
+            val timeLimit = 60 seconds
+
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("helloAsync.js")), memory = Some(memoryLimit), timeout = Some(timeLimit), logsize = Some(logLimit))
+            }
+
+            val run = wsk.action.invoke(name, Map("payload" -> "this is a test".toJson))
+            withActivation(wsk.activation, run) {
+                activation =>
+                    activation.response.status shouldBe "success"
+                    val annotations = activation.annotations.get
+
+                    val limitsObj = JsObject(
+                        "key" -> JsString("limits"),
+                        "value" -> ActionLimits(TimeLimit(timeLimit), MemoryLimit(memoryLimit), LogLimit(logLimit)).toJson)
+
+                    val path = annotations.find { _.fields("key").convertTo[String] == "path" }.get
+
+                    path.fields("value").convertTo[String] should fullyMatch regex (s""".*/$name""")
+                    annotations should contain(limitsObj)
+            }
+    }
+
+    it should "create, and invoke an action that utilizes an invalid docker container with appropriate error" in withAssetCleaner(wskprops) {
+        val name = "invalid dockerContainer"
+        val containerName = s"bogus${Random.alphanumeric.take(16).mkString.toLowerCase}"
+
+        (wp, assetHelper) =>
+            assetHelper.withCleaner(wsk.action, name) {
+                // docker name is a randomly generate string
+                (action, _) => action.create(name, Some(containerName), kind = Some("docker"))
+            }
+
+            val run = wsk.action.invoke(name)
+            withActivation(wsk.activation, run) {
+                activation =>
+                    activation.response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.ApplicationError)
+                    activation.response.result.get.fields("error") shouldBe s"Failed to pull container image '$containerName'.".toJson
+                    activation.annotations shouldBe defined
+                    val limits = activation.annotations.get.filter(_.fields("key").convertTo[String] == "limits")
+                    withClue(limits) {
+                        limits.length should be > 0
+                        limits(0).fields("value") should not be JsNull
+                    }
+            }
+    }
+
+    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, namespace) = 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.getApiHost
+                    fields("api_key") shouldBe wskprops.authKey
+                    fields("namespace") shouldBe namespace
+                    fields("action_name") shouldBe s"/$namespace/$name"
+                    fields("activation_id") shouldBe activation.activationId
+                    fields("deadline").toLong should be >= start
+            }
+    }
+
+    it should "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 run = wsk.action.invoke(name)
+            withActivation(wsk.activation, run) {
+                activation =>
+                    activation.response.status shouldBe "success"
+                    activation.response.result shouldBe Some(JsObject("timedout" -> true.toJson))
+            }
+    }
+
+    it should "invoke an action twice, where the first times out but the second does not and should succeed" in withAssetCleaner(wskprops) {
+        // this test issues two activations: the first is forced to time out and not return a result by its deadline (ie it does not resolve
+        // its promise). The invoker should reclaim its container so that a second activation of the same action (which must happen within a
+        // short period of time (seconds, not minutes) is allocated a fresh container and hence runs as expected (vs. hitting in the container
+        // cache and reusing a bad container).
+        (wp, assetHelper) =>
+            val name = "timeout"
+            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 hungRun = wsk.action.invoke(name, Map("forceHang" -> true.toJson))
+            withActivation(wsk.activation, hungRun) {
+                activation =>
+                    // the first action must fail with a timeout error
+                    activation.response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.ApplicationError)
+                    activation.response.result shouldBe Some(JsObject("error" -> Messages.timedoutActivation(3 seconds, false).toJson))
+            }
+
+            // run the action again, this time without forcing it to timeout
+            // it should succeed because it ran in a fresh container
+            val goodRun = wsk.action.invoke(name, Map("forceHang" -> false.toJson))
+            withActivation(wsk.activation, goodRun) {
+                activation =>
+                    // the first action must fail with a timeout error
+                    activation.response.status shouldBe "success"
+                    activation.response.result shouldBe Some(JsObject("timedout" -> true.toJson))
+            }
+    }
+
+    behavior of "Wsk packages"
+
+    it should "create, and delete a package" in {
+        val name = "createDeletePackage"
+        wsk.pkg.create(name).stdout should include(s"ok: created package $name")
+        wsk.pkg.delete(name).stdout should include(s"ok: deleted package $name")
+    }
+
+    it should "create, and get a package to verify parameter and annotation parsing" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "packageAnnotAndParamParsing"
+
+            assetHelper.withCleaner(wsk.pkg, name) {
+                (pkg, _) =>
+                    pkg.create(name, annotations = getValidJSONTestArgInput, parameters = getValidJSONTestArgInput)
+            }
+
+            val stdout = wsk.pkg.get(name).stdout
+            assert(stdout.startsWith(s"ok: got package $name\n"))
+
+            val receivedParams = wsk.parseJsonString(stdout).fields("parameters").convertTo[JsArray].elements
+            val receivedAnnots = wsk.parseJsonString(stdout).fields("annotations").convertTo[JsArray].elements
+            val escapedJSONArr = getValidJSONTestArgOutput.convertTo[JsArray].elements
+
+            for (expectedItem <- escapedJSONArr) {
+                receivedParams should contain(expectedItem)
+                receivedAnnots should contain(expectedItem)
+            }
+    }
+
+    it should "create, and get a package to verify file parameter and annotation parsing" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "packageAnnotAndParamFileParsing"
+            val file = Some(TestUtils.getTestActionFilename("hello.js"))
+            val argInput = Some(TestUtils.getTestActionFilename("validInput1.json"))
+
+            assetHelper.withCleaner(wsk.pkg, name) {
+                (pkg, _) =>
+                    pkg.create(name, annotationFile = argInput, parameterFile = argInput)
+            }
+
+            val stdout = wsk.pkg.get(name).stdout
+            assert(stdout.startsWith(s"ok: got package $name\n"))
+
+            val receivedParams = wsk.parseJsonString(stdout).fields("parameters").convertTo[JsArray].elements
+            val receivedAnnots = wsk.parseJsonString(stdout).fields("annotations").convertTo[JsArray].elements
+            val escapedJSONArr = getJSONFileOutput.convertTo[JsArray].elements
+
+            for (expectedItem <- escapedJSONArr) {
+                receivedParams should contain(expectedItem)
+                receivedAnnots should contain(expectedItem)
+            }
+    }
+
+    it should "create a package with the proper parameter and annotation escapes" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "packageEscapses"
+
+            assetHelper.withCleaner(wsk.pkg, name) {
+                (pkg, _) =>
+                    pkg.create(name, parameters = getEscapedJSONTestArgInput,
+                        annotations = getEscapedJSONTestArgInput)
+            }
+
+            val stdout = wsk.pkg.get(name).stdout
+            assert(stdout.startsWith(s"ok: got package $name\n"))
+
+            val receivedParams = wsk.parseJsonString(stdout).fields("parameters").convertTo[JsArray].elements
+            val receivedAnnots = wsk.parseJsonString(stdout).fields("annotations").convertTo[JsArray].elements
+            val escapedJSONArr = getEscapedJSONTestArgOutput.convertTo[JsArray].elements
+
+            for (expectedItem <- escapedJSONArr) {
+                receivedParams should contain(expectedItem)
+                receivedAnnots should contain(expectedItem)
+            }
+    }
+
+    it should "report conformance error accessing action as package" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "aAsP"
+            val file = Some(TestUtils.getTestActionFilename("hello.js"))
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) => action.create(name, file)
+            }
+
+            wsk.pkg.get(name, expectedExitCode = CONFLICT).
+                stderr should include(Messages.conformanceMessage)
+
+            wsk.pkg.bind(name, "bogus", expectedExitCode = CONFLICT).
+                stderr should include(Messages.requestedBindingIsNotValid)
+
+            wsk.pkg.bind("bogus", "alsobogus", expectedExitCode = BAD_REQUEST).
+                stderr should include(Messages.bindingDoesNotExist)
+
+    }
+
+    behavior of "Wsk triggers"
+
+    it should "create, and get a trigger to verify parameter and annotation parsing" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "triggerAnnotAndParamParsing"
+
+            assetHelper.withCleaner(wsk.trigger, name) {
+                (trigger, _) =>
+                    trigger.create(name, annotations = getValidJSONTestArgInput, parameters = getValidJSONTestArgInput)
+            }
+
+            val stdout = wsk.trigger.get(name).stdout
+            assert(stdout.startsWith(s"ok: got trigger $name\n"))
+
+            val receivedParams = wsk.parseJsonString(stdout).fields("parameters").convertTo[JsArray].elements
+            val receivedAnnots = wsk.parseJsonString(stdout).fields("annotations").convertTo[JsArray].elements
+            val escapedJSONArr = getValidJSONTestArgOutput.convertTo[JsArray].elements
+
+            for (expectedItem <- escapedJSONArr) {
+                receivedParams should contain(expectedItem)
+                receivedAnnots should contain(expectedItem)
+            }
+    }
+
+    it should "create, and get a trigger to verify file parameter and annotation parsing" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "triggerAnnotAndParamFileParsing"
+            val file = Some(TestUtils.getTestActionFilename("hello.js"))
+            val argInput = Some(TestUtils.getTestActionFilename("validInput1.json"))
+
+            assetHelper.withCleaner(wsk.trigger, name) {
+                (trigger, _) =>
+                    trigger.create(name, annotationFile = argInput, parameterFile = argInput)
+            }
+
+            val stdout = wsk.trigger.get(name).stdout
+            assert(stdout.startsWith(s"ok: got trigger $name\n"))
+
+            val receivedParams = wsk.parseJsonString(stdout).fields("parameters").convertTo[JsArray].elements
+            val receivedAnnots = wsk.parseJsonString(stdout).fields("annotations").convertTo[JsArray].elements
+            val escapedJSONArr = getJSONFileOutput.convertTo[JsArray].elements
+
+            for (expectedItem <- escapedJSONArr) {
+                receivedParams should contain(expectedItem)
+                receivedAnnots should contain(expectedItem)
+            }
+    }
+
+    it should "display a trigger summary when --summary flag is used with 'wsk trigger get'" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val triggerName = "mySummaryTrigger"
+            assetHelper.withCleaner(wsk.trigger, triggerName, confirmDelete = false) {
+                (trigger, name) => trigger.create(name)
+            }
+
+            // Summary namespace should match one of the allowable namespaces (typically 'guest')
+            val ns_regex_list = wsk.namespace.list().stdout.trim.replace('\n', '|')
+            val stdout = wsk.trigger.get(triggerName, summary = true).stdout
+            stdout should include regex (s"(?i)trigger\\s+/${ns_regex_list}/${triggerName}")
+    }
+
+    it should "create a trigger with the proper parameter and annotation escapes" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "triggerEscapes"
+
+            assetHelper.withCleaner(wsk.trigger, name) {
+                (trigger, _) =>
+                    trigger.create(name, parameters = getEscapedJSONTestArgInput,
+                        annotations = getEscapedJSONTestArgInput)
+            }
+
+            val stdout = wsk.trigger.get(name).stdout
+            assert(stdout.startsWith(s"ok: got trigger $name\n"))
+
+            val receivedParams = wsk.parseJsonString(stdout).fields("parameters").convertTo[JsArray].elements
+            val receivedAnnots = wsk.parseJsonString(stdout).fields("annotations").convertTo[JsArray].elements
+            val escapedJSONArr = getEscapedJSONTestArgOutput.convertTo[JsArray].elements
+
+            for (expectedItem <- escapedJSONArr) {
+                receivedParams should contain(expectedItem)
+                receivedAnnots should contain(expectedItem)
+            }
+    }
+
+    it should "not create a trigger when feed fails to initialize" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            assetHelper.withCleaner(wsk.trigger, "badfeed", confirmDelete = false) {
+                (trigger, name) =>
+                    trigger.create(name, feed = Some(s"bogus"), expectedExitCode = ANY_ERROR_EXIT).
+                        exitCode should equal(NOT_FOUND)
+                    trigger.get(name, expectedExitCode = NOT_FOUND)
+
+                    trigger.create(name, feed = Some(s"bogus/feed"), expectedExitCode = ANY_ERROR_EXIT).
+                        exitCode should equal(NOT_FOUND)
+                    trigger.get(name, expectedExitCode = NOT_FOUND)
+            }
+    }
+
+    behavior of "Wsk api"
+
+    it should "reject an api commands with an invalid path parameter" in {
+        val badpath = "badpath"
+
+        var rr = wsk.cli(Seq("api-experimental", "create", "/basepath", badpath, "GET", "action", "--auth", wskprops.authKey) ++ wskprops.overrides, expectedExitCode = ANY_ERROR_EXIT)
+        rr.stderr should include(s"'${badpath}' must begin with '/'")
+
+        rr = wsk.cli(Seq("api-experimental", "delete", "/basepath", badpath, "GET", "--auth", wskprops.authKey) ++ wskprops.overrides, expectedExitCode = ANY_ERROR_EXIT)
+        rr.stderr should include(s"'${badpath}' must begin with '/'")
+
+        rr = wsk.cli(Seq("api-experimental", "list", "/basepath", badpath, "GET", "--auth", wskprops.authKey) ++ wskprops.overrides, expectedExitCode = ANY_ERROR_EXIT)
+        rr.stderr should include(s"'${badpath}' must begin with '/'")
+    }
+
+    it should "reject an api commands with an invalid verb parameter" in {
+        val badverb = "badverb"
+
+        var rr = wsk.cli(Seq("api-experimental", "create", "/basepath", "/path", badverb, "action", "--auth", wskprops.authKey) ++ wskprops.overrides, expectedExitCode = ANY_ERROR_EXIT)
+        rr.stderr should include(s"'${badverb}' is not a valid API verb.  Valid values are:")
+
+        rr = wsk.cli(Seq("api-experimental", "delete", "/basepath", "/path", badverb, "--auth", wskprops.authKey) ++ wskprops.overrides, expectedExitCode = ANY_ERROR_EXIT)
+        rr.stderr should include(s"'${badverb}' is not a valid API verb.  Valid values are:")
+
+        rr = wsk.cli(Seq("api-experimental", "list", "/basepath", "/path", badverb, "--auth", wskprops.authKey) ++ wskprops.overrides, expectedExitCode = ANY_ERROR_EXIT)
+        rr.stderr should include(s"'${badverb}' is not a valid API verb.  Valid values are:")
+    }
+
+    it should "reject an api create command with an API name argument and an API name option" in {
+        val apiName = "An API Name"
+        val rr = wsk.cli(Seq("api-experimental", "create", apiName, "/path", "GET", "action", "-n", apiName, "--auth", wskprops.authKey) ++ wskprops.overrides, expectedExitCode = ANY_ERROR_EXIT)
+        rr.stderr should include(s"An API name can only be specified once.")
+    }
+
+    it should "reject an api create command that specifies a nonexistent configuration file" in {
+        val configfile = "/nonexistent/file"
+        val rr = wsk.cli(Seq("api-experimental", "create", "-c", configfile, "--auth", wskprops.authKey) ++ wskprops.overrides, expectedExitCode = ANY_ERROR_EXIT)
+        rr.stderr should include(s"Error reading swagger file '${configfile}':")
+    }
+
+    it should "reject an api create command specifying a non-JSON configuration file" in {
+        val file = File.createTempFile("api.json", ".txt")
+        file.deleteOnExit()
+        val filename = file.getAbsolutePath()
+
+        val bw = new BufferedWriter(new FileWriter(file))
+        bw.write("a=A")
+        bw.close()
+
+        val rr = wsk.cli(Seq("api-experimental", "create", "-c", filename, "--auth", wskprops.authKey) ++ wskprops.overrides, expectedExitCode = ANY_ERROR_EXIT)
+        rr.stderr should include(s"Error parsing swagger file '${filename}':")
+    }
+
+    it should "reject an api create command specifying a non-swagger JSON configuration file" in {
+        val file = File.createTempFile("api.json", ".txt")
+        file.deleteOnExit()
+        val filename = file.getAbsolutePath()
+
+        val bw = new BufferedWriter(new FileWriter(file))
+        bw.write("""|{
+                    |   "swagger": "2.0",
+                    |   "info": {
+                    |      "title": "My API",
+                    |      "version": "1.0.0"
+                    |   },
+                    |   "BADbasePath": "/bp",
+                    |   "paths": {
+                    |     "/rp": {
+                    |       "get":{}
+                    |     }
+                    |   }
+                    |}""".stripMargin)
+        bw.close()
+
+        val rr = wsk.cli(Seq("api-experimental", "create", "-c", filename, "--auth", wskprops.authKey) ++ wskprops.overrides, expectedExitCode = ANY_ERROR_EXIT)
+        rr.stderr should include(s"Swagger file is invalid (missing basePath, info, paths, or swagger fields")
+    }
+
+    behavior of "Wsk entity list formatting"
+
+    it should "create, and list a package with a long name" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "x" * 70
+            assetHelper.withCleaner(wsk.pkg, name) {
+                (pkg, _) =>
+                    pkg.create(name)
+            }
+            retry({
+                wsk.pkg.list().stdout should include(s"$name private")
+            }, 5, Some(1 second))
+    }
+
+    it should "create, and list an action with a long name" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "x" * 70
+            val file = Some(TestUtils.getTestActionFilename("hello.js"))
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) =>
+                    action.create(name, file)
+            }
+            retry({
+                wsk.action.list().stdout should include(s"$name private nodejs")
+            }, 5, Some(1 second))
+    }
+
+    it should "create, and list a trigger with a long name" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "x" * 70
+            assetHelper.withCleaner(wsk.trigger, name) {
+                (trigger, _) =>
+                    trigger.create(name)
+            }
+            retry({
+                wsk.trigger.list().stdout should include(s"$name private")
+            }, 5, Some(1 second))
+    }
+
+    it should "create, and list a rule with a long name" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val ruleName = "x" * 70
+            val triggerName = "listRulesTrigger"
+            val actionName = "listRulesAction";
+            assetHelper.withCleaner(wsk.trigger, triggerName) {
+                (trigger, name) => trigger.create(name)
+            }
+            assetHelper.withCleaner(wsk.action, actionName) {
+                (action, name) => action.create(name, defaultAction)
+            }
+            assetHelper.withCleaner(wsk.rule, ruleName) {
+                (rule, name) =>
+                    rule.create(name, trigger = triggerName, action = actionName)
+            }
+            retry({
+                wsk.rule.list().stdout should include(s"$ruleName private")
+            }, 5, Some(1 second))
+    }
+
+    behavior of "Wsk params and annotations"
+
+    it should "reject commands that are executed with invalid JSON for annotations and parameters" in {
+        val invalidJSONInputs = getInvalidJSONInput
+        val invalidJSONFiles = Seq(
+            TestUtils.getTestActionFilename("malformed.js"),
+            TestUtils.getTestActionFilename("invalidInput1.json"),
+            TestUtils.getTestActionFilename("invalidInput2.json"),
+            TestUtils.getTestActionFilename("invalidInput3.json"),
+            TestUtils.getTestActionFilename("invalidInput4.json"))
+        val paramCmds = Seq(
+            Seq("action", "create", "actionName", TestUtils.getTestActionFilename("hello.js")),
+            Seq("action", "update", "actionName", TestUtils.getTestActionFilename("hello.js")),
+            Seq("action", "invoke", "actionName"),
+            Seq("package", "create", "packageName"),
+            Seq("package", "update", "packageName"),
+            Seq("package", "bind", "packageName", "boundPackageName"),
+            Seq("trigger", "create", "triggerName"),
+            Seq("trigger", "update", "triggerName"),
+            Seq("trigger", "fire", "triggerName"))
+        val annotCmds = Seq(
+            Seq("action", "create", "actionName", TestUtils.getTestActionFilename("hello.js")),
+            Seq("action", "update", "actionName", TestUtils.getTestActionFilename("hello.js")),
+            Seq("package", "create", "packageName"),
+            Seq("package", "update", "packageName"),
+            Seq("package", "bind", "packageName", "boundPackageName"),
+            Seq("trigger", "create", "triggerName"),
+            Seq("trigger", "update", "triggerName"))
+
+        for (cmd <- paramCmds) {
+            for (invalid <- invalidJSONInputs) {
+                wsk.cli(cmd ++ Seq("-p", "key", invalid) ++ wskprops.overrides, expectedExitCode = ERROR_EXIT)
+                    .stderr should include("Invalid parameter argument")
+            }
+
+            for (invalid <- invalidJSONFiles) {
+                wsk.cli(cmd ++ Seq("-P", invalid) ++ wskprops.overrides, expectedExitCode = ERROR_EXIT)
+                    .stderr should include("Invalid parameter argument")
+
+            }
+        }
+
+        for (cmd <- annotCmds) {
+            for (invalid <- invalidJSONInputs) {
+                wsk.cli(cmd ++ Seq("-a", "key", invalid) ++ wskprops.overrides, expectedExitCode = ERROR_EXIT)
+                    .stderr should include("Invalid annotation argument")
+            }
+
+            for (invalid <- invalidJSONFiles) {
+                wsk.cli(cmd ++ Seq("-A", invalid) ++ wskprops.overrides, expectedExitCode = ERROR_EXIT)
+                    .stderr should include("Invalid annotation argument")
+            }
+        }
+    }
+
+    it should "reject commands that are executed with a missing or invalid parameter or annotation file" in {
+        val emptyFile = TestUtils.getTestActionFilename("emtpy.js")
+        val missingFile = "notafile"
+        val emptyFileMsg = s"File '$emptyFile' is not a valid file or it does not exist"
+        val missingFileMsg = s"File '$missingFile' is not a valid file or it does not exist"
+        val invalidArgs = Seq(
+            (Seq("action", "create", "actionName", TestUtils.getTestActionFilename("hello.js"), "-P", emptyFile),
+                emptyFileMsg),
+            (Seq("action", "update", "actionName", TestUtils.getTestActionFilename("hello.js"), "-P", emptyFile),
+                emptyFileMsg),
+            (Seq("action", "invoke", "actionName", "-P", emptyFile), emptyFileMsg),
+            (Seq("action", "create", "actionName", "-P", emptyFile), emptyFileMsg),
+            (Seq("action", "update", "actionName", "-P", emptyFile), emptyFileMsg),
+            (Seq("action", "invoke", "actionName", "-P", emptyFile), emptyFileMsg),
+            (Seq("package", "create", "packageName", "-P", emptyFile), emptyFileMsg),
+            (Seq("package", "update", "packageName", "-P", emptyFile), emptyFileMsg),
+            (Seq("package", "bind", "packageName", "boundPackageName", "-P", emptyFile), emptyFileMsg),
+            (Seq("package", "create", "packageName", "-P", emptyFile), emptyFileMsg),
+            (Seq("package", "update", "packageName", "-P", emptyFile), emptyFileMsg),
+            (Seq("package", "bind", "packageName", "boundPackageName", "-P", emptyFile), emptyFileMsg),
+            (Seq("trigger", "create", "triggerName", "-P", emptyFile), emptyFileMsg),
+            (Seq("trigger", "update", "triggerName", "-P", emptyFile), emptyFileMsg),
+            (Seq("trigger", "fire", "triggerName", "-P", emptyFile), emptyFileMsg),
+            (Seq("trigger", "create", "triggerName", "-P", emptyFile), emptyFileMsg),
+            (Seq("trigger", "update", "triggerName", "-P", emptyFile), emptyFileMsg),
+            (Seq("trigger", "fire", "triggerName", "-P", emptyFile), emptyFileMsg),
+            (Seq("action", "create", "actionName", TestUtils.getTestActionFilename("hello.js"), "-A", missingFile),
+                missingFileMsg),
+            (Seq("action", "update", "actionName", TestUtils.getTestActionFilename("hello.js"), "-A", missingFile),
+                missingFileMsg),
+            (Seq("action", "invoke", "actionName", "-A", missingFile), missingFileMsg),
+            (Seq("action", "create", "actionName", "-A", missingFile), missingFileMsg),
+            (Seq("action", "update", "actionName", "-A", missingFile), missingFileMsg),
+            (Seq("action", "invoke", "actionName", "-A", missingFile), missingFileMsg),
+            (Seq("package", "create", "packageName", "-A", missingFile), missingFileMsg),
+            (Seq("package", "update", "packageName", "-A", missingFile), missingFileMsg),
+            (Seq("package", "bind", "packageName", "boundPackageName", "-A", missingFile), missingFileMsg),
+            (Seq("package", "create", "packageName", "-A", missingFile), missingFileMsg),
+            (Seq("package", "update", "packageName", "-A", missingFile), missingFileMsg),
+            (Seq("package", "bind", "packageName", "boundPackageName", "-A", missingFile), missingFileMsg),
+            (Seq("trigger", "create", "triggerName", "-A", missingFile), missingFileMsg),
+            (Seq("trigger", "update", "triggerName", "-A", missingFile), missingFileMsg),
+            (Seq("trigger", "fire", "triggerName", "-A", missingFile), missingFileMsg),
+            (Seq("trigger", "create", "triggerName", "-A", missingFile), missingFileMsg),
+            (Seq("trigger", "update", "triggerName", "-A", missingFile), missingFileMsg),
+            (Seq("trigger", "fire", "triggerName", "-A", missingFile), missingFileMsg))
+
+        invalidArgs foreach {
+            case (cmd, err) =>
+                val stderr = wsk.cli(cmd, expectedExitCode = MISUSE_EXIT).stderr
+                stderr should include(err)
+                stderr should include("Run 'wsk --help' for usage.")
+        }
+    }
+
+    it should "reject commands that are executed with not enough param or annot arguments" in {
+        val invalidParamMsg = "Arguments for '-p' must be a key/value pair"
+        val invalidAnnotMsg = "Arguments for '-a' must be a key/value pair"
+        val invalidParamFileMsg = "An argument must be provided for '-P'"
+        val invalidAnnotFileMsg = "An argument must be provided for '-A'"
+        val invalidArgs = Seq(
+            (Seq("action", "create", "actionName", "-p"), invalidParamMsg),
+            (Seq("action", "create", "actionName", "-p", "key"), invalidParamMsg),
+            (Seq("action", "create", "actionName", "-P"), invalidParamFileMsg),
+            (Seq("action", "update", "actionName", "-p"), invalidParamMsg),
+            (Seq("action", "update", "actionName", "-p", "key"), invalidParamMsg),
+            (Seq("action", "update", "actionName", "-P"), invalidParamFileMsg),
+            (Seq("action", "invoke", "actionName", "-p"), invalidParamMsg),
+            (Seq("action", "invoke", "actionName", "-p", "key"), invalidParamMsg),
+            (Seq("action", "invoke", "actionName", "-P"), invalidParamFileMsg),
+            (Seq("action", "create", "actionName", "-a"), invalidAnnotMsg),
+            (Seq("action", "create", "actionName", "-a", "key"), invalidAnnotMsg),
+            (Seq("action", "create", "actionName", "-A"), invalidAnnotFileMsg),
+            (Seq("action", "update", "actionName", "-a"), invalidAnnotMsg),
+            (Seq("action", "update", "actionName", "-a", "key"), invalidAnnotMsg),
+            (Seq("action", "update", "actionName", "-A"), invalidAnnotFileMsg),
+            (Seq("action", "invoke", "actionName", "-a"), invalidAnnotMsg),
+            (Seq("action", "invoke", "actionName", "-a", "key"), invalidAnnotMsg),
+            (Seq("action", "invoke", "actionName", "-A"), invalidAnnotFileMsg),
+            (Seq("package", "create", "packageName", "-p"), invalidParamMsg),
+            (Seq("package", "create", "packageName", "-p", "key"), invalidParamMsg),
+            (Seq("package", "create", "packageName", "-P"), invalidParamFileMsg),
+            (Seq("package", "update", "packageName", "-p"), invalidParamMsg),
+            (Seq("package", "update", "packageName", "-p", "key"), invalidParamMsg),
+            (Seq("package", "update", "packageName", "-P"), invalidParamFileMsg),
+            (Seq("package", "bind", "packageName", "boundPackageName", "-p"), invalidParamMsg),
+            (Seq("package", "bind", "packageName", "boundPackageName", "-p", "key"), invalidParamMsg),
+            (Seq("package", "bind", "packageName", "boundPackageName", "-P"), invalidParamFileMsg),
+            (Seq("package", "create", "packageName", "-a"), invalidAnnotMsg),
+            (Seq("package", "create", "packageName", "-a", "key"), invalidAnnotMsg),
+            (Seq("package", "create", "packageName", "-A"), invalidAnnotFileMsg),
+            (Seq("package", "update", "packageName", "-a"), invalidAnnotMsg),
+            (Seq("package", "update", "packageName", "-a", "key"), invalidAnnotMsg),
+            (Seq("package", "update", "packageName", "-A"), invalidAnnotFileMsg),
+            (Seq("package", "bind", "packageName", "boundPackageName", "-a"), invalidAnnotMsg),
+            (Seq("package", "bind", "packageName", "boundPackageName", "-a", "key"), invalidAnnotMsg),
+            (Seq("package", "bind", "packageName", "boundPackageName", "-A"), invalidAnnotFileMsg),
+            (Seq("trigger", "create", "triggerName", "-p"), invalidParamMsg),
+            (Seq("trigger", "create", "triggerName", "-p", "key"), invalidParamMsg),
+            (Seq("trigger", "create", "triggerName", "-P"), invalidParamFileMsg),
+            (Seq("trigger", "update", "triggerName", "-p"), invalidParamMsg),
+            (Seq("trigger", "update", "triggerName", "-p", "key"), invalidParamMsg),
+            (Seq("trigger", "update", "triggerName", "-P"), invalidParamFileMsg),
+            (Seq("trigger", "fire", "triggerName", "-p"), invalidParamMsg),
+            (Seq("trigger", "fire", "triggerName", "-p", "key"), invalidParamMsg),
+            (Seq("trigger", "fire", "triggerName", "-P"), invalidParamFileMsg),
+            (Seq("trigger", "create", "triggerName", "-a"), invalidAnnotMsg),
+            (Seq("trigger", "create", "triggerName", "-a", "key"), invalidAnnotMsg),
+            (Seq("trigger", "create", "triggerName", "-A"), invalidAnnotFileMsg),
+            (Seq("trigger", "update", "triggerName", "-a"), invalidAnnotMsg),
+            (Seq("trigger", "update", "triggerName", "-a", "key"), invalidAnnotMsg),
+            (Seq("trigger", "update", "triggerName", "-A"), invalidAnnotFileMsg),
+            (Seq("trigger", "fire", "triggerName", "-a"), invalidAnnotMsg),
+            (Seq("trigger", "fire", "triggerName", "-a", "key"), invalidAnnotMsg),
+            (Seq("trigger", "fire", "triggerName", "-A"), invalidAnnotFileMsg))
+
+        invalidArgs foreach {
+            case (cmd, err) =>
+                val stderr = wsk.cli(cmd, expectedExitCode = ERROR_EXIT).stderr
+                stderr should include(err)
+                stderr should include("Run 'wsk --help' for usage.")
+        }
+    }
+
+    behavior of "Wsk invalid argument handling"
+
+    it should "reject commands that are executed with invalid arguments" in {
+        val invalidArgsMsg = "error: Invalid argument(s)"
+        val tooFewArgsMsg = invalidArgsMsg + "."
+        val tooManyArgsMsg = invalidArgsMsg + ": "
+        val actionNameActionReqMsg = "An action name and action are required."
+        val actionNameReqMsg = "An action name is required."
+        val actionOptMsg = "An action is optional."
+        val packageNameReqMsg = "A package name is required."
+        val packageNameBindingReqMsg = "A package name and binding name are required."
+        val ruleNameReqMsg = "A rule name is required."
+        val ruleTriggerActionReqMsg = "A rule, trigger and action name are required."
+        val activationIdReq = "An activation ID is required."
+        val triggerNameReqMsg = "A trigger name is required."
+        val optNamespaceMsg = "An optional namespace is the only valid argument."
+        val optPayloadMsg = "A payload is optional."
+        val noArgsReqMsg = "No arguments are required."
+        val invalidArg = "invalidArg"
+        val apiCreateReqMsg = "Specify a swagger file or specify an API base path with an API path, an API verb, and an action name."
+        val apiGetReqMsg = "An API base path or API name is required."
+        val apiDeleteReqMsg = "An API base path or API name is required.  An optional API relative path and operation may also be provided."
+        val apiListReqMsg = "Optional parameters are: API base path (or API name), API relative path and operation."
+        val invalidShared = s"Cannot use value '$invalidArg' for shared"
+        val invalidArgs = Seq(
+            (Seq("api-experimental", "create"), s"${tooFewArgsMsg} ${apiCreateReqMsg}"),
+            (Seq("api-experimental", "create", "/basepath", "/path", "GET", "action", invalidArg), s"${tooManyArgsMsg}${invalidArg}. ${apiCreateReqMsg}"),
+            (Seq("api-experimental", "get"), s"${tooFewArgsMsg} ${apiGetReqMsg}"),
+            (Seq("api-experimental", "get", "/basepath", invalidArg), s"${tooManyArgsMsg}${invalidArg}. ${apiGetReqMsg}"),
+            (Seq("api-experimental", "delete"), s"${tooFewArgsMsg} ${apiDeleteReqMsg}"),
+            (Seq("api-experimental", "delete", "/basepath", "/path", "GET", invalidArg), s"${tooManyArgsMsg}${invalidArg}. ${apiDeleteReqMsg}"),
+            (Seq("api-experimental", "list", "/basepath", "/path", "GET", invalidArg), s"${tooManyArgsMsg}${invalidArg}. ${apiListReqMsg}"),
+            (Seq("action", "create"), s"${tooFewArgsMsg} ${actionNameActionReqMsg}"),
+            (Seq("action", "create", "someAction"), s"${tooFewArgsMsg} ${actionNameActionReqMsg}"),
+            (Seq("action", "create", "actionName", "artifactName", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+            (Seq("action", "update"), s"${tooFewArgsMsg} ${actionNameReqMsg} ${actionOptMsg}"),
+            (Seq("action", "update", "actionName", "artifactName", invalidArg),
+                s"${tooManyArgsMsg}${invalidArg}. ${actionNameReqMsg} ${actionOptMsg}"),
+            (Seq("action", "delete"), s"${tooFewArgsMsg} ${actionNameReqMsg}"),
+            (Seq("action", "delete", "actionName", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+            (Seq("action", "get"), s"${tooFewArgsMsg} ${actionNameReqMsg}"),
+            (Seq("action", "get", "actionName", "namespace", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+            (Seq("action", "list", "namespace", invalidArg), s"${tooManyArgsMsg}${invalidArg}. ${optNamespaceMsg}"),
+            (Seq("action", "invoke"), s"${tooFewArgsMsg} ${actionNameReqMsg}"),
+            (Seq("action", "invoke", "actionName", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+            (Seq("activation", "list", "namespace", invalidArg),
+                s"${tooManyArgsMsg}${invalidArg}. ${optNamespaceMsg}"),
+            (Seq("activation", "get"), s"${tooFewArgsMsg} ${activationIdReq}"),
+            (Seq("activation", "get", "activationID", "namespace", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+            (Seq("activation", "logs"), s"${tooFewArgsMsg} ${activationIdReq}"),
+            (Seq("activation", "logs", "activationID", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+            (Seq("activation", "result"), s"${tooFewArgsMsg} ${activationIdReq}"),
+            (Seq("activation", "result", "activationID", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+            (Seq("activation", "poll", "activationID", invalidArg),
+                s"${tooManyArgsMsg}${invalidArg}. ${optNamespaceMsg}"),
+            (Seq("namespace", "list", invalidArg), s"${tooManyArgsMsg}${invalidArg}. ${noArgsReqMsg}"),
+            (Seq("namespace", "get", "namespace", invalidArg),
+                s"${tooManyArgsMsg}${invalidArg}. ${optNamespaceMsg}"),
+            (Seq("package", "create"), s"${tooFewArgsMsg} ${packageNameReqMsg}"),
+            (Seq("package", "create", "packageName", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+            (Seq("package", "create", "packageName", "--shared", invalidArg), invalidShared),
+            (Seq("package", "update"), s"${tooFewArgsMsg} ${packageNameReqMsg}"),
+            (Seq("package", "update", "packageName", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+            (Seq("package", "update", "packageName", "--shared", invalidArg), invalidShared),
+            (Seq("package", "get"), s"${tooFewArgsMsg} ${packageNameReqMsg}"),
+            (Seq("package", "get", "packageName", "namespace", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+            (Seq("package", "bind"), s"${tooFewArgsMsg} ${packageNameBindingReqMsg}"),
+            (Seq("package", "bind", "packageName"), s"${tooFewArgsMsg} ${packageNameBindingReqMsg}"),
+            (Seq("package", "bind", "packageName", "bindingName", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+            (Seq("package", "list", "namespace", invalidArg),
+                s"${tooManyArgsMsg}${invalidArg}. ${optNamespaceMsg}"),
+            (Seq("package", "delete"), s"${tooFewArgsMsg} ${packageNameReqMsg}"),
+            (Seq("package", "delete", "namespace", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+            (Seq("package", "refresh", "namespace", invalidArg),
+                s"${tooManyArgsMsg}${invalidArg}. ${optNamespaceMsg}"),
+            (Seq("rule", "enable"), s"${tooFewArgsMsg} ${ruleNameReqMsg}"),
+            (Seq("rule", "enable", "ruleName", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+            (Seq("rule", "disable"), s"${tooFewArgsMsg} ${ruleNameReqMsg}"),
+            (Seq("rule", "disable", "ruleName", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+            (Seq("rule", "status"), s"${tooFewArgsMsg} ${ruleNameReqMsg}"),
+            (Seq("rule", "status", "ruleName", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+            (Seq("rule", "create"), s"${tooFewArgsMsg} ${ruleTriggerActionReqMsg}"),
+            (Seq("rule", "create", "ruleName"), s"${tooFewArgsMsg} ${ruleTriggerActionReqMsg}"),
+            (Seq("rule", "create", "ruleName", "triggerName"), s"${tooFewArgsMsg} ${ruleTriggerActionReqMsg}"),
+            (Seq("rule", "create", "ruleName", "triggerName", "actionName", invalidArg),
+                s"${tooManyArgsMsg}${invalidArg}."),
+            (Seq("rule", "update"), s"${tooFewArgsMsg} ${ruleTriggerActionReqMsg}"),
+            (Seq("rule", "update", "ruleName"), s"${tooFewArgsMsg} ${ruleTriggerActionReqMsg}"),
+            (Seq("rule", "update", "ruleName", "triggerName"), s"${tooFewArgsMsg} ${ruleTriggerActionReqMsg}"),
+            (Seq("rule", "update", "ruleName", "triggerName", "actionName", invalidArg),
+                s"${tooManyArgsMsg}${invalidArg}."),
+            (Seq("rule", "get"), s"${tooFewArgsMsg} ${ruleNameReqMsg}"),
+            (Seq("rule", "get", "ruleName", "namespace", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+            (Seq("rule", "delete"), s"${tooFewArgsMsg} ${ruleNameReqMsg}"),
+            (Seq("rule", "delete", "ruleName", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+            (Seq("rule", "list", "namespace", invalidArg), s"${tooManyArgsMsg}${invalidArg}. ${optNamespaceMsg}"),
+            (Seq("trigger", "fire"), s"${tooFewArgsMsg} ${triggerNameReqMsg} ${optPayloadMsg}"),
+            (Seq("trigger", "fire", "triggerName", "triggerPayload", invalidArg),
+                s"${tooManyArgsMsg}${invalidArg}. ${triggerNameReqMsg} ${optPayloadMsg}"),
+            (Seq("trigger", "create"), s"${tooFewArgsMsg} ${triggerNameReqMsg}"),
+            (Seq("trigger", "create", "triggerName", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+            (Seq("trigger", "update"), s"${tooFewArgsMsg} ${triggerNameReqMsg}"),
+            (Seq("trigger", "update", "triggerName", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+            (Seq("trigger", "get"), s"${tooFewArgsMsg} ${triggerNameReqMsg}"),
+            (Seq("trigger", "get", "triggerName", "namespace", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+            (Seq("trigger", "delete"), s"${tooFewArgsMsg} ${triggerNameReqMsg}"),
+            (Seq("trigger", "delete", "triggerName", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+            (Seq("trigger", "list", "namespace", invalidArg), s"${tooManyArgsMsg}${invalidArg}. ${optNamespaceMsg}"))
+
+        invalidArgs foreach {
+            case (cmd, err) =>
+                val stderr = wsk.cli(cmd ++ wskprops.overrides, expectedExitCode = ERROR_EXIT).stderr
+                stderr should include(err)
+                stderr should include("Run 'wsk --help' for usage.")
+        }
+    }
+
+    behavior of "Wsk action parameters"
+
+    it should "create an action with different permutations of limits" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val file = Some(TestUtils.getTestActionFilename("hello.js"))
+
+            def testLimit(timeout: Option[Duration] = None, memory: Option[ByteSize] = None, logs: Option[ByteSize] = None, ec: Int = SUCCESS_EXIT) = {
+                // Limits to assert, standard values if CLI omits certain values
+                val limits = JsObject(
+                    "timeout" -> timeout.getOrElse(STD_DURATION).toMillis.toJson,
+                    "memory" -> memory.getOrElse(STD_MEMORY).toMB.toInt.toJson,
+                    "logs" -> logs.getOrElse(STD_LOGSIZE).toMB.toInt.toJson)
+
+                val name = "ActionLimitTests" + Instant.now.toEpochMilli
+                val createResult = assetHelper.withCleaner(wsk.action, name, confirmDelete = (ec == SUCCESS_EXIT)) {
+                    (action, _) =>
+                        val result = action.create(name, file, logsize = logs, memory = memory, timeout = timeout, expectedExitCode = DONTCARE_EXIT)
+                        withClue(s"create failed for parameters: timeout = $timeout, memory = $memory, logsize = $logs:") {
+                            result.exitCode should be(ec)
+                        }
+                        result
+                }
+
+                if (ec == SUCCESS_EXIT) {
+                    val JsObject(parsedAction) = wsk.action.get(name).stdout.split("\n").tail.mkString.parseJson.asJsObject
+                    parsedAction("limits") shouldBe limits
+                } else {
+                    createResult.stderr should include("allowed threshold")
+                }
+            }
+
+            // Assert for valid permutations that the values are set correctly
+            for {
+                time <- Seq(None, Some(MIN_DURATION), Some(MAX_DURATION))
+                mem <- Seq(None, Some(MIN_MEMORY), Some(MAX_MEMORY))
+                log <- Seq(None, Some(MIN_LOGSIZE), Some(MAX_LOGSIZE))
+            } testLimit(time, mem, log)
+
+            // Assert that invalid permutation are rejected
+            testLimit(Some(0.milliseconds), None, None, BAD_REQUEST)
+            testLimit(Some(100.minutes), None, None, BAD_REQUEST)
+            testLimit(None, Some(0.MB), None, BAD_REQUEST)
+            testLimit(None, Some(32768.MB), None, BAD_REQUEST)
+            testLimit(None, None, Some(32768.MB), BAD_REQUEST)
+    }
+
+    it should "create a trigger using property file" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "listTriggers"
+            val tmpProps = File.createTempFile("wskprops", ".tmp")
+            val env = Map("WSK_CONFIG_FILE" -> tmpProps.getAbsolutePath())
+            wsk.cli(Seq("property", "set", "--auth", wp.authKey) ++ wskprops.overrides, env = env)
+            assetHelper.withCleaner(wsk.trigger, name) {
+                (trigger, _) =>
+                    wsk.cli(Seq("-i", "trigger", "create", name), env = env)
+            }
+            tmpProps.delete()
+    }
+}
diff --git a/tests/src/test/scala/whisk/core/cli/test/WskEntitlementTests.scala b/tests/src/test/scala/whisk/core/cli/test/WskEntitlementTests.scala
new file mode 100644
index 0000000..42a34b0
--- /dev/null
+++ b/tests/src/test/scala/whisk/core/cli/test/WskEntitlementTests.scala
@@ -0,0 +1,376 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * 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.
+ */
+
+package whisk.core.cli.test
+
+import org.junit.runner.RunWith
+import org.scalatest.BeforeAndAfterAll
+import org.scalatest.junit.JUnitRunner
+
+import common.RunWskAdminCmd
+import common.TestHelpers
+import common.TestUtils
+import common.TestUtils.FORBIDDEN
+import common.TestUtils.NOT_FOUND
+import common.TestUtils.TIMEOUT
+import common.Wsk
+import common.WskAdmin
+import common.WskProps
+import common.WskTestHelpers
+import spray.json._
+import spray.json.DefaultJsonProtocol._
+import whisk.core.entity.Subject
+import whisk.core.entity.WhiskPackage
+
+@RunWith(classOf[JUnitRunner])
+class WskEntitlementTests
+    extends TestHelpers
+    with WskTestHelpers
+    with BeforeAndAfterAll {
+
+    val wsk = new Wsk
+    lazy val defaultWskProps = WskProps()
+    lazy val guestWskProps = getAdditionalTestSubject()
+
+    override def afterAll() = {
+        disposeAdditionalTestSubject(guestWskProps.namespace)
+    }
+
+    def getAdditionalTestSubject() = {
+        val wskadmin = new RunWskAdminCmd {}
+        val newSubject = Subject().toString
+        WskProps(
+            namespace = newSubject,
+            authKey = wskadmin.cli(Seq("user", "create", newSubject)).stdout.trim)
+    }
+
+    def disposeAdditionalTestSubject(subject: String) = {
+        val wskadmin = new RunWskAdminCmd {}
+        withClue(s"failed to delete temporary subject $subject") {
+            wskadmin.cli(Seq("user", "delete", subject)).stdout should include("Subject deleted")
+        }
+    }
+
+    val samplePackage = "samplePackage"
+    val sampleAction = "sampleAction"
+    val fullSampleActionName = s"$samplePackage/$sampleAction"
+    val guestNamespace = guestWskProps.namespace
+
+    behavior of "Wsk Package Entitlement"
+
+    it should "not allow unauthorized subject to operate on private action" in withAssetCleaner(guestWskProps) {
+        (wp, assetHelper) =>
+            val privateAction = "privateAction"
+
+            assetHelper.withCleaner(wsk.action, privateAction) {
+                (action, name) => action.create(name, Some(TestUtils.getTestActionFilename("hello.js")))(wp)
+            }
+
+            val fullyQualifiedActionName = s"/$guestNamespace/$privateAction"
+            wsk.action.get(fullyQualifiedActionName, expectedExitCode = FORBIDDEN)(defaultWskProps).
+                stderr should include("not authorized")
+
+            withAssetCleaner(defaultWskProps) {
+                (wp, assetHelper) =>
+                    assetHelper.withCleaner(wsk.action, fullyQualifiedActionName, confirmDelete = false) {
+                        (action, name) =>
+                            val rr = action.create(name, None, update = true, expectedExitCode = FORBIDDEN)(wp)
+                            rr.stderr should include("not authorized")
+                            rr
+                    }
+                    assetHelper.withCleaner(wsk.action, "unauthorized sequence", confirmDelete = false) {
+                        (action, name) =>
+                            val rr = action.create(name, Some(fullyQualifiedActionName), kind = Some("sequence"), update = true, expectedExitCode = FORBIDDEN)(wp)
+                            rr.stderr should include("not authorized")
+                            rr
+                    }
+            }
+
+            wsk.action.delete(fullyQualifiedActionName, expectedExitCode = FORBIDDEN)(defaultWskProps).
+                stderr should include("not authorized")
+
+            wsk.action.invoke(fullyQualifiedActionName, expectedExitCode = FORBIDDEN)(defaultWskProps).
+                stderr should include("not authorized")
+    }
+
+    it should "reject deleting action in shared package not owned by authkey" in withAssetCleaner(guestWskProps) {
+        (wp, assetHelper) =>
+            assetHelper.withCleaner(wsk.pkg, samplePackage) {
+                (pkg, _) => pkg.create(samplePackage, shared = Some(true))(wp)
+            }
+
+            assetHelper.withCleaner(wsk.action, fullSampleActionName) {
+                val file = Some(TestUtils.getTestActionFilename("empty.js"))
+                (action, _) => action.create(fullSampleActionName, file)(wp)
+            }
+
+            val fullyQualifiedActionName = s"/$guestNamespace/$fullSampleActionName"
+            wsk.action.get(fullyQualifiedActionName)(defaultWskProps)
+            wsk.action.delete(fullyQualifiedActionName, expectedExitCode = FORBIDDEN)(defaultWskProps)
+    }
+
+    it should "reject create action in shared package not owned by authkey" in withAssetCleaner(guestWskProps) {
+        (wp, assetHelper) =>
+            assetHelper.withCleaner(wsk.pkg, samplePackage) {
+                (pkg, name) => pkg.create(name, shared = Some(true))(wp)
+            }
+
+            val fullyQualifiedActionName = s"/$guestNamespace/notallowed"
+            val file = Some(TestUtils.getTestActionFilename("empty.js"))
+
+            withAssetCleaner(defaultWskProps) {
+                (wp, assetHelper) =>
+                    assetHelper.withCleaner(wsk.action, fullyQualifiedActionName, confirmDelete = false) {
+                        (action, name) => action.create(name, file, expectedExitCode = FORBIDDEN)(wp)
+                    }
+            }
+    }
+
+    it should "reject update action in shared package not owned by authkey" in withAssetCleaner(guestWskProps) {
+        (wp, assetHelper) =>
+            assetHelper.withCleaner(wsk.pkg, samplePackage) {
+                (pkg, _) => pkg.create(samplePackage, shared = Some(true))(wp)
+            }
+
+            assetHelper.withCleaner(wsk.action, fullSampleActionName) {
+                val file = Some(TestUtils.getTestActionFilename("empty.js"))
+                (action, _) => action.create(fullSampleActionName, file)(wp)
+            }
+
+            val fullyQualifiedActionName = s"/$guestNamespace/$fullSampleActionName"
+            wsk.action.create(fullyQualifiedActionName, None, update = true, expectedExitCode = FORBIDDEN)(defaultWskProps)
+    }
+
+    behavior of "Wsk Package Listing"
+
+    it should "list shared packages" in withAssetCleaner(guestWskProps) {
+        (wp, assetHelper) =>
+            assetHelper.withCleaner(wsk.pkg, samplePackage) {
+                (pkg, _) => pkg.create(samplePackage, shared = Some(true))(wp)
+            }
+
+            val fullyQualifiedPackageName = s"/$guestNamespace/$samplePackage"
+            val result = wsk.pkg.list(Some(s"/$guestNamespace"))(defaultWskProps).stdout
+            result should include regex (fullyQualifiedPackageName + """\s+shared""")
+    }
+
+    it should "not list private packages" in withAssetCleaner(guestWskProps) {
+        (wp, assetHelper) =>
+            assetHelper.withCleaner(wsk.pkg, samplePackage) {
+                (pkg, _) => pkg.create(samplePackage)(wp)
+            }
+
+            val fullyQualifiedPackageName = s"/$guestNamespace/$samplePackage"
+            val result = wsk.pkg.list(Some(s"/$guestNamespace"))(defaultWskProps).stdout
+            result should not include regex(fullyQualifiedPackageName)
+    }
+
+    it should "list shared package actions" in withAssetCleaner(guestWskProps) {
+        (wp, assetHelper) =>
+            assetHelper.withCleaner(wsk.pkg, samplePackage) {
+                (pkg, _) => pkg.create(samplePackage, shared = Some(true))(wp)
+            }
+
+            assetHelper.withCleaner(wsk.action, fullSampleActionName) {
+                val file = Some(TestUtils.getTestActionFilename("empty.js"))
+                (action, _) => action.create(fullSampleActionName, file, kind = Some("nodejs"))(wp)
+            }
+
+            val fullyQualifiedPackageName = s"/$guestNamespace/$samplePackage"
+            val fullyQualifiedActionName = s"/$guestNamespace/$fullSampleActionName"
+            val result = wsk.action.list(Some(fullyQualifiedPackageName))(defaultWskProps).stdout
+            result should include regex (fullyQualifiedActionName)
+    }
+
+    behavior of "Wsk Package Binding"
+
+    it should "create a package binding" in withAssetCleaner(guestWskProps) {
+        (wp, assetHelper) =>
+            assetHelper.withCleaner(wsk.pkg, samplePackage) {
+                (pkg, _) => pkg.create(samplePackage, shared = Some(true))(wp)
+            }
+
+            val name = "bindPackage"
+            val annotations = Map("a" -> "A".toJson, WhiskPackage.bindingFieldName -> "xxx".toJson)
+            val provider = s"/$guestNamespace/$samplePackage"
+            withAssetCleaner(defaultWskProps) {
+                (wp, assetHelper) =>
+                    assetHelper.withCleaner(wsk.pkg, name) {
+                        (pkg, _) => pkg.bind(provider, name, annotations = annotations)(wp)
+                    }
+
+                    val stdout = wsk.pkg.get(name)(defaultWskProps).stdout
+                    val annotationString = wsk.parseJsonString(stdout).fields("annotations").toString
+                    annotationString should include regex (""""key":"a"""")
+                    annotationString should include regex (""""value":"A"""")
+                    annotationString should include regex (s""""key":"${WhiskPackage.bindingFieldName}"""")
+                    annotationString should not include regex(""""key":"xxx"""")
+                    annotationString should include regex (s""""name":"${samplePackage}"""")
+            }
+    }
+
+    it should "not create a package binding for private package" in withAssetCleaner(guestWskProps) {
+        (wp, assetHelper) =>
+            assetHelper.withCleaner(wsk.pkg, samplePackage) {
+                (pkg, _) => pkg.create(samplePackage, shared = Some(false))(wp)
+            }
+
+            val name = "bindPackage"
+            val provider = s"/$guestNamespace/$samplePackage"
+            withAssetCleaner(defaultWskProps) {
+                (wp, assetHelper) =>
+                    assetHelper.withCleaner(wsk.pkg, name, confirmDelete = false) {
+                        (pkg, _) => pkg.bind(provider, name, expectedExitCode = FORBIDDEN)(wp)
+                    }
+            }
+    }
+
+    behavior of "Wsk Package Action"
+
+    it should "get and invoke an action from package" in withAssetCleaner(guestWskProps) {
+        (wp, assetHelper) =>
+            assetHelper.withCleaner(wsk.pkg, samplePackage) {
+                (pkg, _) => pkg.create(samplePackage, parameters = Map("a" -> "A".toJson), shared = Some(true))(wp)
+            }
+
+            assetHelper.withCleaner(wsk.action, fullSampleActionName) {
+                val file = Some(TestUtils.getTestActionFilename("hello.js"))
+                (action, _) => action.create(fullSampleActionName, file)(wp)
+            }
+
+            val fullyQualifiedActionName = s"/$guestNamespace/$fullSampleActionName"
+            val stdout = wsk.action.get(fullyQualifiedActionName)(defaultWskProps).stdout
+            stdout should include("name")
+            stdout should include("parameters")
+            stdout should include("limits")
+            stdout should include regex (""""key": "a"""")
+            stdout should include regex (""""value": "A"""")
+
+            val run = wsk.action.invoke(fullyQualifiedActionName)(defaultWskProps)
+
+            withActivation(wsk.activation, run)({
+                _.response.success shouldBe true
+            })(defaultWskProps)
+    }
+
+    it should "invoke an action sequence from package" in withAssetCleaner(guestWskProps) {
+        (wp, assetHelper) =>
+            assetHelper.withCleaner(wsk.pkg, samplePackage) {
+                (pkg, _) => pkg.create(samplePackage, parameters = Map("a" -> "A".toJson), shared = Some(true))(wp)
+            }
+
+            assetHelper.withCleaner(wsk.action, fullSampleActionName) {
+                val file = Some(TestUtils.getTestActionFilename("hello.js"))
+                (action, _) => action.create(fullSampleActionName, file)(wp)
+            }
+
+            withAssetCleaner(defaultWskProps) {
+                (wp, assetHelper) =>
+                    assetHelper.withCleaner(wsk.action, "sequence") {
+                        (action, name) =>
+                            val fullyQualifiedActionName = s"/$guestNamespace/$fullSampleActionName"
+                            action.create(name, Some(fullyQualifiedActionName), kind = Some("sequence"), update = true)(wp)
+                    }
+
+                    val run = wsk.action.invoke("sequence")(defaultWskProps)
+                    withActivation(wsk.activation, run)({
+                        _.response.success shouldBe true
+                    })(defaultWskProps)
+            }
+    }
+
+    it should "not allow invoke an action sequence with more than one component from package after entitlement change" in withAssetCleaner(guestWskProps) {
+        (guestwp, assetHelper) =>
+            val privateSamplePackage = samplePackage + "prv"
+            assetHelper.withCleaner(wsk.pkg, samplePackage) {
+                (pkg, _) =>
+                    pkg.create(samplePackage, parameters = Map("a" -> "A".toJson), shared = Some(true))(guestwp)
+                    pkg.create(privateSamplePackage, parameters = Map("a" -> "A".toJson), shared = Some(true))(guestwp)
+            }
+
+            assetHelper.withCleaner(wsk.action, fullSampleActionName) {
+                val file = Some(TestUtils.getTestActionFilename("hello.js"))
+                (action, _) =>
+                    action.create(fullSampleActionName, file)(guestwp)
+                    action.create(s"$privateSamplePackage/$sampleAction", file)(guestwp)
+            }
+
+            withAssetCleaner(defaultWskProps) {
+                (dwp, assetHelper) =>
+                    assetHelper.withCleaner(wsk.action, "sequence") {
+                        (action, name) =>
+                            val fullyQualifiedActionName = s"/$guestNamespace/$fullSampleActionName"
+                            val fullyQualifiedActionName2 = s"/$guestNamespace/$privateSamplePackage/$sampleAction"
+                            action.create(name, Some(s"$fullyQualifiedActionName,$fullyQualifiedActionName2"),
+                                kind = Some("sequence"))(dwp)
+                    }
+
+                    // change package visibility
+                    wsk.pkg.create(privateSamplePackage, update = true, shared = Some(false))(guestwp)
+                    wsk.action.invoke("sequence", expectedExitCode = FORBIDDEN)(defaultWskProps)
+            }
+    }
+
+    it should "invoke a packaged action not owned by the subject to get the subject's namespace" in withAssetCleaner(guestWskProps) {
+        (_, assetHelper) =>
+            val packageName = "namespacePackage"
+            val actionName = "namespaceAction"
+            val packagedActionName = s"$packageName/$actionName"
+
+            assetHelper.withCleaner(wsk.pkg, packageName) {
+                (pkg, _) => pkg.create(packageName, shared = Some(true))(guestWskProps)
+            }
+
+            assetHelper.withCleaner(wsk.action, packagedActionName) {
+                val file = Some(TestUtils.getTestActionFilename("helloContext.js"))
+                (action, _) => action.create(packagedActionName, file)(guestWskProps)
+            }
+
+            val fullyQualifiedActionName = s"/$guestNamespace/$packagedActionName"
+            val run = wsk.action.invoke(fullyQualifiedActionName)(defaultWskProps)
+
+            withActivation(wsk.activation, run)({ activation =>
+                val (_, namespace) = WskAdmin.getUser(defaultWskProps.authKey)
+                activation.response.success shouldBe true
+                activation.response.result.get.toString should include regex (s""""namespace":\\s*"$namespace"""")
+            })(defaultWskProps)
+    }
+
+    behavior of "Wsk Trigger Feed"
+
+    it should "not create a trigger with timeout error when feed fails to initialize" in withAssetCleaner(guestWskProps) {
+        (wp, assetHelper) =>
+            assetHelper.withCleaner(wsk.pkg, samplePackage) {
+                (pkg, _) => pkg.create(samplePackage, shared = Some(true))(wp)
+            }
+
+            val sampleFeed = s"$samplePackage/sampleFeed"
+            assetHelper.withCleaner(wsk.action, sampleFeed) {
+                val file = Some(TestUtils.getTestActionFilename("empty.js"))
+                (action, _) => action.create(sampleFeed, file, kind = Some("nodejs"))(wp)
+            }
+
+            val fullyQualifiedFeedName = s"/$guestNamespace/$sampleFeed"
+            withAssetCleaner(defaultWskProps) {
+                (wp, assetHelper) =>
+                    assetHelper.withCleaner(wsk.trigger, "badfeed", confirmDelete = false) {
+                        (trigger, name) => trigger.create(name, feed = Some(fullyQualifiedFeedName), expectedExitCode = TIMEOUT)(wp)
+                    }
+                    wsk.trigger.get("badfeed", expectedExitCode = NOT_FOUND)(wp)
+            }
+    }
+
+}
diff --git a/tests/src/test/scala/whisk/core/cli/test/WskWebActionsTests.scala b/tests/src/test/scala/whisk/core/cli/test/WskWebActionsTests.scala
new file mode 100644
index 0000000..bdb75f7
--- /dev/null
+++ b/tests/src/test/scala/whisk/core/cli/test/WskWebActionsTests.scala
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * 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.
+ */
+
+package whisk.core.cli.test
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+import com.jayway.restassured.RestAssured
+
+import common.TestHelpers
+import common.TestUtils
+import common.Wsk
+import common.WskAdmin
+import common.WskProps
+import common.WskTestHelpers
+import spray.json._
+import spray.json.DefaultJsonProtocol._
+import system.rest.RestUtil
+
+/**
+ * Tests web actions.
+ */
+@RunWith(classOf[JUnitRunner])
+class WskWebActionsTests
+    extends TestHelpers
+    with WskTestHelpers
+    with RestUtil {
+
+    val MAX_URL_LENGTH = 8192 // 8K matching nginx default
+
+    implicit val wskprops = WskProps()
+    val wsk = new Wsk
+    val namespace = WskAdmin.getUser(wskprops.authKey)._2
+
+    behavior of "Wsk Web Actions"
+
+    /**
+     * Tests web actions, plus max url limit.
+     */
+    it should "create a web action accessible via HTTPS" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "webaction"
+            val file = Some(TestUtils.getTestActionFilename("echo.js"))
+
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) =>
+                    action.create(name, file, annotations = Map("web-export" -> true.toJson))
+            }
+
+            val host = getServiceURL()
+            val requestPath = host + s"/api/v1/experimental/web/$namespace/default/webaction.text/a?a="
+            val padAmount = MAX_URL_LENGTH - requestPath.length
+            Seq(("A", 200),
+                ("A" * padAmount, 200),
+                // ideally the bad case is just +1 but there's some differences
+                // in how characters are counted i.e., whether these count "https://:443"
+                // or not; it seems sufficient to test right around the boundary
+                ("A" * (padAmount + 100), 414))
+                .foreach {
+                    case (pad, code) =>
+                        val url = (requestPath + pad)
+                        val response = RestAssured.given().config(sslconfig).get(url)
+                        val responseCode = response.statusCode
+
+                        withClue(s"response code: $responseCode, url length: ${url.length}, pad amount: ${pad.length}, url: $url") {
+                            responseCode shouldBe code
+                            if (code == 200) {
+                                response.body().asString() shouldBe pad
+                            } else {
+                                response.body().asString() should include("414 Request-URI Too Large") // from nginx
+                            }
+                        }
+                }
+    }
+
+    /**
+     * Tests web action requiring authentication.
+     */
+    it should "create a web action requiring authentication accessible via HTTPS" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val name = "webaction"
+            val file = Some(TestUtils.getTestActionFilename("echo.js"))
+
+            assetHelper.withCleaner(wsk.action, name) {
+                (action, _) =>
+                    action.create(name, file, annotations = Map("web-export" -> true.toJson, "require-whisk-auth" -> true.toJson))
+            }
+
+            val host = getServiceURL()
+            val url = host + s"/api/v1/experimental/web/$namespace/default/webaction.text/__ow_meta_namespace"
+
+            val unauthorizedResponse = RestAssured.given().config(sslconfig).get(url)
+            unauthorizedResponse.statusCode shouldBe 401
+
+            val authorizedResponse = RestAssured
+                .given()
+                .config(sslconfig)
+                .auth().preemptive().basic(wskprops.authKey.split(":")(0), wskprops.authKey.split(":")(1))
+                .get(url)
+            authorizedResponse.statusCode shouldBe 200
+            authorizedResponse.body().asString() shouldBe namespace
+    }
+}

-- 
To stop receiving notification emails like this one, please contact
"commits@openwhisk.apache.org" <co...@openwhisk.apache.org>.