javascript/core: Allow clients to use trailing undefined args/cb

Fixes and tests: https://github.com/vanadium/issues/issues/715

The goal was to still allow us to have the undefined cb while also
having the undefined trailing arg. This CL addresses the accidental
assumption that a trailing undefined was always a cb and tests that
case in test-js-client-server.js

Change-Id: I1cd97539c8e246a4fb88d4f499607b065639be3e
diff --git a/src/rpc/client.js b/src/rpc/client.js
index 7f6fdea..1b524c4 100644
--- a/src/rpc/client.js
+++ b/src/rpc/client.js
@@ -452,7 +452,7 @@
 
       // Callback is the last function argument, pull it out of the args
       var lastType = typeof args[args.length - 1];
-      if (lastType === 'function' || lastType === 'undefined') {
+      if (lastType === 'function') {
         callback = args.pop();
       }
 
@@ -482,7 +482,16 @@
 
       ctx = vtrace.withNewSpan(ctx, '<jsclient>"'+name+'".'+method);
 
+      // If the last value was undefined, and there is 1 too many args, the
+      // undefined is an undefined cb, not an undefined arg.
+      if (args.length === methodSig.inArgs.length + 1 &&
+        lastType === 'undefined') {
+
+        args.pop();
+      }
+
       if (args.length !== methodSig.inArgs.length) {
+
         var expectedArgs = methodSig.inArgs.map(function(arg) {
           return arg.name;
         });
@@ -500,7 +509,7 @@
 
         // The given arguments exclude the ctx and (optional) cb.
         var givenArgs = Array.prototype.slice.call(arguments, 1);
-        if (typeof givenArgs[givenArgs.length - 1] === 'function') {
+        if (lastType === 'function') {
           givenArgs.pop();
         }
         err = new IncorrectArgCount(
diff --git a/test/integration/test-js-client-server.js b/test/integration/test-js-client-server.js
index d909e9c..7866b17 100644
--- a/test/integration/test-js-client-server.js
+++ b/test/integration/test-js-client-server.js
@@ -353,6 +353,63 @@
     });
   });
 
+  test(namePrefix + 'typeService.isTyped(...) - promise', function(t) {
+    setup(options, function(err, ctx, typeService, end) {
+      t.error(err, 'should not error on setup');
+
+      typeService.isTyped(ctx, 'glue').then(function(res) {
+        t.error(err, 'should not error on isTyped(...)');
+        // Use equal instead of notOk to ensure that res is not wrapped.
+        t.equal(res, false, '\'glue\' is an untyped string');
+
+        var VomStr = vdl.registry.lookupOrCreateConstructor(vdl.types.STRING);
+        var typedString = new VomStr('glued');
+        typeService.isTyped(ctx, typedString, function(err, res) {
+          t.error(err, 'should not error on isTyped(...)');
+          // Use equal instead of ok to ensure that res is not wrapped.
+          t.equal(res, true, 'VomStr(\'glued\') is a typed string');
+          end(t);
+        });
+      }).catch(function error(err) {
+        t.error(err, 'should not error');
+        end(t);
+      });
+    });
+  });
+
+  test(namePrefix + 'typeService.isTyped(undefined) - promise', function(t) {
+    setup(options, function(err, ctx, typeService, end) {
+      t.error(err, 'should not error on setup');
+
+      // Check that you can leave an optional (or any) inArg as undefined.
+      // It should not be mistaken for a callback.
+      typeService.isTyped(ctx, undefined).then(function(res) {
+        t.error(err, 'should not error on isTyped(undefined)');
+        t.equal(res, false, 'undefined is treated like JSValue null');
+
+        // Lenient: Having both arg and cb as undefined is fine too.
+        typeService.isTyped(ctx, undefined, undefined).then(function(res) {
+          t.error(err, 'should not error on isTyped(undefined, undefined)');
+          t.equal(res, false, 'undefined is treated like JSValue null');
+
+          // Having any more undefined's is an error (too many args).
+          typeService.isTyped(ctx, undefined, undefined, undefined).then(
+            function(res) {
+
+            t.fail(err, 'should have errored');
+            end(t);
+          }).catch(function(err) {
+            t.ok(err, 'should error');
+            end(t);
+          });
+        });
+      }).catch(function error(err) {
+        t.error(err, 'should not error');
+        end(t);
+      });
+    });
+  });
+
   // This test ensures that typed values sent between JS server and client are
   // unwrapped when being processed. Further, the client disallows sending the
   // wrong type to the server.