Use activation db agent if concurrency is not supported (#32)

Fixes Concurrency error when using wskdebug with IBM Cloud Functions #7

if concurrency > 1 is not supported, fallback to activation db agent
diff --git a/src/agentmgr.js b/src/agentmgr.js
index 8b59f28..15ea202 100644
--- a/src/agentmgr.js
+++ b/src/agentmgr.js
@@ -158,7 +158,7 @@
                 console.log("This OpenWhisk does not support action concurrency. Debugging will be a bit slower. Consider using '--ngrok' which might be a faster option.");
 
                 agentName = "polling activation db";
-                agentCode = await this.getPollingActivationRecordAgent();
+                agentCode = await this.getPollingActivationDbAgent();
             }
         }
 
@@ -185,7 +185,17 @@
             });
         }
 
-        await this.pushAgent(action, agentCode, backupName);
+        try {
+            await this.pushAgent(action, agentCode, backupName);
+        } catch (e) {
+            // openwhisk does not support concurrent nodejs actions, try with another
+            if (e.statusCode === 400 && e.error && typeof e.error.error === "string" && e.error.error.includes("concurrency")) {
+                console.log(`The Openwhisk server does not support concurrent actions, using alternative agent. Consider using --ngrok for a possibly faster agent.`);
+                this.concurrency = false;
+                agentCode = await this.getPollingActivationDbAgent();
+                await this.pushAgent(action, agentCode, backupName);
+            }
+        }
 
         if (this.argv.verbose) {
             console.log(`Agent installed.`);
@@ -259,6 +269,11 @@
                             const a = activations[0];
                             if (a.response && a.response.result && !this.activationsSeen[a.activationId]) {
                                 activation = a;
+                                if (!activation.response.success) {
+                                    throw {
+                                        error: activation
+                                    };
+                                }
                                 break;
                             }
                         }
@@ -389,7 +404,7 @@
         return fs.readFileSync(`${__dirname}/../agent/agent-concurrency.js`, {encoding: 'utf8'});
     }
 
-    async getPollingActivationRecordAgent() {
+    async getPollingActivationDbAgent() {
         // this needs 2 helper actions in addition to the agent in place of the action
         await this.createHelperAction(`${this.actionName}_wskdebug_invoked`,   `${__dirname}/../agent/echo.js`);
         await this.createHelperAction(`${this.actionName}_wskdebug_completed`, `${__dirname}/../agent/echo.js`);
diff --git a/test/agentmgr.test.js b/test/agentmgr.test.js
new file mode 100644
index 0000000..cf069b7
--- /dev/null
+++ b/test/agentmgr.test.js
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/* eslint-env mocha */
+
+'use strict';
+
+const Debugger = require("../src/debugger");
+
+const test = require('./test');
+
+describe('agentmgr',  function() {
+    this.timeout(30000);
+
+    before(function() {
+        test.isDockerInstalled();
+    });
+
+    beforeEach(async function() {
+        await test.beforeEach();
+    });
+
+    afterEach(function() {
+        test.afterEach();
+    });
+
+    it("should use non-concurrrent agent if openwhisk does not support concurrency", async function() {
+        const action = "myaction";
+        const code = `const main = () => ({ msg: 'WRONG' });`;
+
+        test.mockAction(action, code);
+
+        test.mockCreateBackupAction(action);
+
+        // wskdebug overwriting the action with the agent
+        test.openwhiskNock()
+            .put(
+                `${test.openwhiskApiUrlActions()}/${action}?overwrite=true`,
+                body => body.annotations.some(v => v.key === "wskdebug" && v.value === true)
+            )
+            .matchHeader("authorization", test.openwhiskApiAuthHeader())
+            .reply(400, {
+                code: 'df940ccf1d076f103c3743685c25d2b2',
+                error: 'The request content was malformed:\nrequirement failed: concurrency 200 exceeds allowed threshold of 1'
+            });
+
+        // another wskdebug with non-concurrent action
+        test.openwhiskNock()
+            .put(
+                `${test.openwhiskApiUrlActions()}/${action}?overwrite=true`,
+                body => body.annotations.some(v => v.key === "wskdebug" && v.value === true)
+            )
+            .matchHeader("authorization", test.openwhiskApiAuthHeader())
+            .reply(200, test.nodejsActionDescription(action));
+
+        // helper actions for non-concurrent action
+        test.openwhiskNock()
+            .put(`${test.openwhiskApiUrlActions()}/${action}_wskdebug_invoked?overwrite=true`)
+            .matchHeader("authorization", test.openwhiskApiAuthHeader())
+            .reply(200, test.nodejsActionDescription(action));
+        test.openwhiskNock()
+            .put(`${test.openwhiskApiUrlActions()}/${action}_wskdebug_completed?overwrite=true`)
+            .matchHeader("authorization", test.openwhiskApiAuthHeader())
+            .reply(200, test.nodejsActionDescription(action));
+
+
+        // invocation
+        test.openwhiskNock()
+            .get(`${test.openwhiskApiUrl()}/activations`)
+            .query(query => query.name === `${action}_wskdebug_invoked`)
+            .matchHeader("authorization", test.openwhiskApiAuthHeader())
+            .reply(200, [{
+                activationId: "dummy-invoked",
+                response: {
+                    success: true,
+                    result: {
+                        $activationId: "1234567890"
+                    }
+                }
+            }]);
+
+        // completion of invocation
+        test.openwhiskNock()
+            .post(`${test.openwhiskApiUrlActions()}/${action}_wskdebug_completed?blocking=true`, {
+                msg: "CORRECT",
+                $activationId: "1234567890"
+            })
+            .matchHeader("authorization", test.openwhiskApiAuthHeader())
+            .reply(200, test.nodejsActionDescription(action));
+
+        // abort polling
+        test.openwhiskNock()
+            .get(`${test.openwhiskApiUrl()}/activations`)
+            .query(query => query.name === `${action}_wskdebug_invoked`)
+            .matchHeader("authorization", test.openwhiskApiAuthHeader())
+            .reply(200, [{
+                activationId: "dummy-invoked-2",
+                response: {
+                    success: false,
+                    result: {
+                        error: {
+                            code: 43
+                        },
+                        $activationId: "99999999999"
+                    }
+                }
+            }]);
+
+        // shutdown/restore process
+        test.mockReadBackupAction(action, code);
+        test.mockRestoreAction(action, code);
+        test.mockRemoveBackupAction(action);
+        test.openwhiskNock()
+            .get(`${test.openwhiskApiUrlActions()}/${action}_wskdebug_invoked?code=false`)
+            .matchHeader("authorization", test.openwhiskApiAuthHeader())
+            .reply(200, {});
+        test.openwhiskNock()
+            .delete(`${test.openwhiskApiUrlActions()}/${action}_wskdebug_invoked`)
+            .matchHeader("authorization", test.openwhiskApiAuthHeader())
+            .reply(200, {});
+        test.openwhiskNock()
+            .get(`${test.openwhiskApiUrlActions()}/${action}_wskdebug_completed?code=false`)
+            .matchHeader("authorization", test.openwhiskApiAuthHeader())
+            .reply(200, {});
+        test.openwhiskNock()
+            .delete(`${test.openwhiskApiUrlActions()}/${action}_wskdebug_completed`)
+            .matchHeader("authorization", test.openwhiskApiAuthHeader())
+            .reply(200, {});
+
+
+        process.chdir("test/nodejs/plain-flat");
+        const argv = {
+            port: test.port,
+            action: "myaction",
+            sourcePath: `${process.cwd()}/action.js`,
+            invokeParams: '{ "key": "invocationOnSourceModification" }'
+        };
+
+        const dbgr = new Debugger(argv);
+        await dbgr.start();
+        dbgr.run();
+
+        // wait a bit
+        await test.sleep(500);
+
+        await dbgr.stop();
+
+        test.assertAllNocksInvoked();
+    });
+});
diff --git a/test/test.js b/test/test.js
index 5a266d7..c01f8c2 100644
--- a/test/test.js
+++ b/test/test.js
@@ -74,6 +74,11 @@
     return openwhisk;
 }
 
+function openwhiskApiUrl() {
+    return `/api/v1/namespaces/${FAKE_OPENWHISK_NAMESPACE}`;
+}
+
+
 function openwhiskApiUrlActions() {
     return `/api/v1/namespaces/${FAKE_OPENWHISK_NAMESPACE}/actions`;
 }
@@ -522,6 +527,7 @@
     mockActionDoubleInvocation,
     // advanced
     openwhiskNock,
+    openwhiskApiUrl,
     openwhiskApiUrlActions,
     openwhiskApiAuthHeader,
     mockAction,