playground: Finish porting shell tests to Go.

Ported default bundle (fortune) verification test to Go.
Changed playground-test target from shell to Go builder tests. This
is temporary; builder tests will be folded into integration-test,
leaving playground-test for more complex and independent http
(storage, Docker) tests.
Minor fixes to Javascript linting in client and pgbundle.

Change-Id: I5eb420cfb95806ee498c4936cf259cdd18ef4cbc
diff --git a/client/.jshintignore b/client/.jshintignore
index ea9a206..13b2f5f 100644
--- a/client/.jshintignore
+++ b/client/.jshintignore
@@ -1,3 +1,4 @@
 node_modules
 build
 public
+bundles
diff --git a/client/Makefile b/client/Makefile
index 417762a..d2267e9 100644
--- a/client/Makefile
+++ b/client/Makefile
@@ -1,8 +1,6 @@
 PATH := bin:node_modules/.bin:$(PATH)
 PATH := $(PATH):$(V23_ROOT)/third_party/cout/node/bin
 SHELL := /bin/bash -euo pipefail
-export GOPATH := $(V23_ROOT)/release/projects/playground/go:$(GOPATH)
-export VDLPATH := $(GOPATH)
 
 js_files := $(shell find browser -name "*.js")
 css_files := $(shell find stylesheets -name "*.css")
@@ -41,6 +39,8 @@
 # This task depends on example_profiles and example_files because we want
 # to re-bundle if any of those change. However, the bundle tool works on
 # directories, so we pass in example_code_bundle_dirs as the path argument.
+# Note: Bundle correctness test was moved to builder integration test.
+# TODO(ivanpi): Move bundles out of client when bootstrap support is ready.
 public/bundles: $(example_profiles) $(example_files) | node_modules
 	$(RM) -rf $@
 	mkdir -p $(dir $(bundle_temp_file))
@@ -85,8 +85,8 @@
 	jshint .
 
 .PHONY: test
-test:
-	v23 run ./test.sh
+test: lint test-client test-browser
+	@true  # silences `watch make`
 
 .PHONY: test-client
 test-client: node_modules
diff --git a/client/lib/shell/pg_test_util.sh b/client/lib/shell/pg_test_util.sh
deleted file mode 100755
index a390da2..0000000
--- a/client/lib/shell/pg_test_util.sh
+++ /dev/null
@@ -1,72 +0,0 @@
-#!/bin/bash
-# Copyright 2015 The Vanadium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file.
-
-# Utilities for testing the playground builder tool.
-# Used by tests in client and go/src/v.io/x/playground.
-
-PLAYGROUND_ROOT="${V23_ROOT}/release/projects/playground"
-
-source "${V23_ROOT}/release/projects/playground/client/lib/shell/shell_test.sh"
-
-# Sets up environment variables required for the tests.
-setup_environment() {
-  export GOPATH="$(pwd):$(v23 env GOPATH)"
-  export VDLPATH="$(pwd):$(v23 env VDLPATH)"
-  export PATH="$(pwd):${shell_test_BIN_DIR}:${V23_ROOT}/third_party/cout/node/bin:${PATH}"
-
-  # We unset all environment variables that supply a principal in order to
-  # simulate production playground setup.
-  unset V23_CREDENTIALS
-  unset VEYRON_AGENT_FD
-}
-
-# Installs the release/javascript/core library and makes it accessible to
-# Javascript files in the Vanadium playground test under the module name
-# 'vanadium'.
-install_vanadium_js() {
-  # TODO(nlacasse): Once release/javascript/core is publicly available in npm, replace this
-  # with "npm install vanadium".
-  npm install --production "${V23_ROOT}/release/javascript/core"
-}
-
-# Installs the pgbundle tool.
-install_pgbundle() {
-  npm install --production "${PLAYGROUND_ROOT}/pgbundle"
-}
-
-# Installs various go binaries.
-build_go_binaries() {
-  shell_test::build_go_binary 'v.io/x/ref/services/wspr/wsprd' -a -tags wspr
-  shell_test::build_go_binary 'v.io/x/ref/cmd/principal'
-  shell_test::build_go_binary 'v.io/x/ref/services/proxy/proxyd'
-  shell_test::build_go_binary 'v.io/x/ref/services/mounttable/mounttabled'
-  shell_test::build_go_binary 'v.io/x/ref/cmd/vdl'
-  shell_test::build_go_binary 'v.io/x/playground/builder'
-}
-
-# Bundles a playground example and tests it using builder.
-# $1: root directory of example to test
-# $2: glob file with file patterns to bundle from $1
-# $3: arguments to call builder with
-test_pg_example() {
-  local -r PGBUNDLE_DIR="$1"
-  local -r PATTERN_FILE="$2"
-  local -r BUILDER_ARGS="$3"
-
-  # Create a fresh dir to save the bundle and run builder in.
-  local -r TEMP_DIR=$(shell::tmp_dir)
-
-  ./node_modules/.bin/pgbundle --verbose "${PATTERN_FILE}" "${PGBUNDLE_DIR}" > "${TEMP_DIR}/test.json" || return
-
-  local -r ORIG_DIR=$(pwd)
-  pushd "${TEMP_DIR}"
-
-  ln -s "${ORIG_DIR}/node_modules" ./  # for release/javascript/core
-  "${shell_test_BIN_DIR}/builder" ${BUILDER_ARGS} < "test.json" 2>&1 | tee builder.out
-  # Move builder output to original dir for verification.
-  mv builder.out "${ORIG_DIR}"
-
-  popd
-}
diff --git a/client/lib/shell/pkg.go b/client/lib/shell/pkg.go
deleted file mode 100644
index 1229481..0000000
--- a/client/lib/shell/pkg.go
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package lib
diff --git a/client/lib/shell/shell.sh b/client/lib/shell/shell.sh
deleted file mode 100755
index 089cd04..0000000
--- a/client/lib/shell/shell.sh
+++ /dev/null
@@ -1,217 +0,0 @@
-#!/bin/bash
-# Copyright 2015 The Vanadium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file.
-
-# The shell library is used to execute veyron shell scripts.
-
-# IMPORTANT: If your script registers its own "trap" handler, that handler must
-# call shell::at_exit to clean up all temporary files and directories created by
-# this library.
-
-set -e
-set -u
-
-TMPDIR="${TMPDIR-/tmp}"
-V23_SHELL_TMP_DIRS=${V23_SHELL_TMP_DIRS-$(mktemp "${TMPDIR}/XXXXXXXX")}
-V23_SHELL_TMP_FILES=${V23_SHELL_TMP_FILES-$(mktemp "${TMPDIR}/XXXXXXXX")}
-V23_SHELL_TMP_PIDS=${V23_SHELL_TMP_PIDS-$(mktemp "${TMPDIR}/XXXXXXXX")}
-
-trap shell::at_exit INT TERM EXIT
-
-# When a user of this library needs cleanup to be performed by root
-# because different processes are running with different system
-# identities, it should set this variable to root to escalate
-# privilege appropriately.
-SUDO_USER="$(whoami)"
-
-# shell::at_exit is executed when the shell script exits. It is used,
-# for example, to garbage collect any temporary files and directories
-# created by invocations of shell::tmp_file and shell:tmp_dir.
-shell::at_exit() {
-  # If the variable V23_SHELL_CMD_LOOP_AT_EXIT is non-empty, accept commands
-  # from the user in a command loop.  This can preserve the state in the logs
-  # directories while the user examines them.
-  case "${V23_SHELL_CMD_LOOP_AT_EXIT-}" in
-  ?*)
-    local cmdline
-    set +u
-    echo 'Entering debug shell.  Type control-D or "exit" to exit.' >&2
-    while echo -n "test> " >&2; read cmdline; do
-      eval "$cmdline"
-    done
-    set -u
-    ;;
-  esac
-  # Unset the trap so that it doesn't run again on exit.
-  trap - INT TERM EXIT
-  for pid in $(cat "${V23_SHELL_TMP_PIDS}"); do
-    sudo -u "${SUDO_USER}" kill "${pid}" &> /dev/null || true
-  done
-  for tmp_dir in $(cat "${V23_SHELL_TMP_DIRS}"); do
-     sudo -u "${SUDO_USER}" rm -rf "${tmp_dir}" &>/dev/null
-  done
-  for tmp_file in $(cat "${V23_SHELL_TMP_FILES}"); do
-     sudo -u "${SUDO_USER}" rm -f "${tmp_file}" &>/dev/null
-  done
-   sudo -u "${SUDO_USER}" rm -f "${V23_SHELL_TMP_DIRS}" "${V23_SHELL_TMP_FILES}" "${V23_SHELL_TMP_PIDS}" &>/dev/null
-}
-
-# shell::kill_child_processes kills all child processes.
-# Note, child processes must set their own "at_exit: kill_child_processes"
-# signal handlers in order for this to kill their descendants.
-shell::kill_child_processes() {
-  # Attempt to shutdown child processes by sending the TERM signal.
-  if [[ -n "$(jobs -p -r)" ]]; then
-    shell::silence pkill -P $$
-    sleep 1
-    # Force shutdown of any remaining child processes using the KILL signal.
-    # Note, we only "sleep 1" and "kill -9" if there were child processes.
-    if [[ -n "$(jobs -p -r)" ]]; then
-      shell::silence sudo -u "${SUDO_USER}"  pkill -9 -P $$
-    fi
-  fi
-}
-
-# shell::check_deps can be used to check which dependencies are
-# missing on the host.
-#
-# Example:
-#   local -r DEPS=$(shell::check_deps git golang-go)
-#   if [[ -n "${DEPS} ]]; then
-#     sudo apt-get install $DEPS
-#   fi
-shell::check_deps() {
-  local RESULT=""
-  case $(uname -s) in
-    "Linux")
-      set +e
-      for pkg in "$@"; do
-        dpkg -s "${pkg}" &> /dev/null
-        if [[ "$?" -ne 0 ]]; then
-          RESULT+=" ${pkg}"
-        fi
-      done
-      set -e
-      ;;
-    "Darwin")
-      set +e
-      for pkg in "$@"; do
-        if [[ -z "$(brew ls --versions ${pkg})" ]]; then
-          RESULT+=" ${pkg}"
-        fi
-      done
-      set -e
-      ;;
-    *)
-      echo "Operating system $(uname -s) is not supported."
-      exit 1
-  esac
-  echo "${RESULT}"
-}
-
-# shell::check_result can be used to obtain the exit status of a bash
-# command. By the semantics of "set -e", a script exits immediately
-# upon encountering a command that fails. This function should be used
-# if instead, a script wants to take an action depending on the exit
-# status.
-#
-# Example:
-#   local -r RESULT=$(shell::check_result <command>)
-#   if [[ "${RESULT}" -ne 0 ]]; then
-#     <handle failure>
-#   else
-#     <handle success>
-#   fi
-shell::check_result() {
-  set +e
-  "$@" &> /dev/null
-  echo "$?"
-  set -e
-}
-
-# shell::tmp_dir can be used to create a temporary directory.
-#
-# Example:
-#   local -R TMP_DIR=$(shell::tmp_dir)
-shell::tmp_dir() {
-  local -r RESULT=$(mktemp -d "${TMPDIR}/XXXXXXXX")
-  echo "${RESULT}" >> "${V23_SHELL_TMP_DIRS}"
-  echo "${RESULT}"
-}
-
-# shell::tmp_file can be used to create a temporary file.
-#
-# Example:
-#   local -R TMP_FILE=$(shell::tmp_file)
-shell::tmp_file() {
-  local -r RESULT=$(mktemp "${TMPDIR}/XXXXXXXX")
-  echo "${RESULT}" >> "${V23_SHELL_TMP_FILES}"
-  echo "${RESULT}"
-}
-
-# shell::run_server is used to start a long running server process and
-# to verify that it is still running after a set timeout period. This
-# is useful for catching cases where the server either fails to start or
-# fails soon after it has started.
-# The script will return 0 if the command is running at that point in time
-# and a non-zero value otherwise. It echos the server's pid.
-#
-# Example:
-#   shell::run_server 5 stdout stderr command arg1 arg2 || exit 1
-shell::run_server() {
-  local -r TIMEOUT="$1"
-  local -r STDOUT="$2"
-  local -r STDERR="$3"
-  shift; shift; shift
-  if [[ "${STDOUT}" = "${STDERR}" ]]; then
-    "$@" > "${STDOUT}" 2>&1 &
-  else
-    "$@" > "${STDOUT}" 2> "${STDERR}" &
-  fi
-  local -r SERVER_PID=$!
-  echo "${SERVER_PID}" >> "${V23_SHELL_TMP_PIDS}"
-  echo "${SERVER_PID}"
-
-  for i in $(seq 1 "${TIMEOUT}"); do
-    sleep 1
-    local RESULT=$(shell::check_result kill -0 "${SERVER_PID}")
-    if [[ "${RESULT}" = "0" ]]; then
-      # server's up, can return early
-      return 0
-    fi
-  done
-  return "${RESULT}"
-}
-
-# shell::timed_wait_for is used to wait until the given pattern appears
-# in the given file within a set timeout. It returns 0 if the pattern
-# was successfully matched and 1 if the timeout expires before the pattern
-# is found.
-#
-# Example:
-#   local -r LOG_FILE=$(shell::tmp_file)
-#   shell::run_server 1 "${LOG_FILE}" "${LOG_FILE}" <server> <args>...
-#   shell::timed_wait_for 1 "${LOG_FILE}" "server started"
-shell::timed_wait_for() {
-  local -r TIMEOUT="$1"
-  local -r FILE="$2"
-  local -r PATTERN="$3"
-  for i in $(seq 1 "${TIMEOUT}"); do
-    sleep 1
-    grep -m 1 "${PATTERN}" "${FILE}" > /dev/null && return 0 || true
-  done
-  # grep has timed out.
-  echo "Timed out. Here's the file:"
-  echo "====BEGIN FILE==="
-  cat "${FILE}"
-  echo "====END FILE==="
-  return 1
-}
-
-# shell::silence runs the given command with the supplied arguments,
-# redirecting all of its output to /dev/null and ignoring its exit
-# code.
-shell::silence() {
-  "$@" &> /dev/null || true
-}
diff --git a/client/lib/shell/shell_test.sh b/client/lib/shell/shell_test.sh
deleted file mode 100755
index 9d8974e..0000000
--- a/client/lib/shell/shell_test.sh
+++ /dev/null
@@ -1,276 +0,0 @@
-#!/bin/bash
-# Copyright 2015 The Vanadium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file.
-
-# The shell_test library is used to execute shell tests.
-
-# IMPORTANT: If your script registers its own "trap" handler, that
-# handler must call shell_test::at_exit to clean up all temporary
-# files and directories created by this library.
-
-source "${V23_ROOT}/release/projects/playground/client/lib/shell/shell.sh"
-
-trap shell_test::at_exit INT TERM EXIT
-
-# ensure that we print a 'FAIL' message unless shell_test::pass is
-# explicitly called.
-shell_test_EXIT_MESSAGE="FAIL_UNEXPECTED_EXIT"
-
-# default timeout values.
-shell_test_DEFAULT_SERVER_TIMEOUT="10"
-shell_test_DEFAULT_MESSAGE_TIMEOUT="30"
-
-# default binary dir and working dir.
-shell_test_BIN_DIR="${shell_test_BIN_DIR:-$(shell::tmp_dir)}"
-shell_test_WORK_DIR="$(shell::tmp_dir)"
-
-# shell_test::assert_eq GOT WANT LINENO checks that GOT == WANT and if
-# not, fails the test, outputting the offending line number.
-shell_test::assert_eq() {
-  local -r GOT="$1"
-  local -r WANT="$2"
-  local -r LINENO="$3"
-  if [[ "${GOT}" != "${WANT}" ]]; then
-    shell_test::fail "line ${LINENO}: expected '${GOT}' == '${WANT}'"
-  fi
-}
-
-# shell_test::assert_ge GOT WANT LINENO checks that GOT >= WANT and if
-# not, fails the test, outputting the given line number and error
-# message.
-shell_test::assert_ge() {
-  local -r GOT="$1"
-  local -r WANT="$2"
-  local -r LINENO="$3"
-  if [[ "${GOT}" -lt "${WANT}" ]]; then
-    shell_test::fail "line ${LINENO}: expected ${GOT} >= ${WANT}"
-  fi
-}
-
-# shell_test::assert_gt GOT WANT LINENO checks that GOT > WANT and if
-# not, fails the test, outputting the given line number and error
-# message.
-shell_test::assert_gt() {
-  local -r GOT="$1"
-  local -r WANT="$2"
-  local -r LINENO="$3"
-  if [[ "${GOT}" -le "${WANT}" ]]; then
-    shell_test::fail "line ${LINENO}: expected ${GOT} > ${WANT}"
-  fi
-}
-
-# shell_test::assert_le GOT WANT LINENO checks that GOT <= WANT and if
-# not, fails the test, outputting the given line number and error
-# message.
-shell_test::assert_le() {
-  local -r GOT="$1"
-  local -r WANT="$2"
-  local -r LINENO="$3"
-  if [[ "${GOT}" -gt "${WANT}" ]]; then
-    shell_test::fail "line ${LINENO}: expected ${GOT} <= ${WANT}"
-  fi
-}
-
-# shell_test::assert_lt GOT WANT LINENO checks that GOT < WANT and if
-# not, fails the test, outputting the given line number and error
-# message.
-shell_test::assert_lt() {
-  local -r GOT="$1"
-  local -r WANT="$2"
-  local -r LINENO="$3"
-  if [[ "${GOT}" -ge "${WANT}" ]]; then
-    shell_test::fail "line ${LINENO}: expected ${GOT} < ${WANT}"
-  fi
-}
-
-# shell_test::assert_ne GOT WANT LINENO checks that GOT != WANT and if
-# not, fails the test, outputting the offending line number.
-shell_test::assert_ne() {
-  local -r GOT="$1"
-  local -r WANT="$2"
-  local -r LINENO="$3"
-  if [[ "${GOT}" == "${WANT}" ]]; then
-    shell_test::fail "line ${LINENO}: expected '${GOT}' != '${WANT}'"
-  fi
-}
-
-# shell_test::assert_contains GOT WANT LINENO checks that GOT contains WANT and
-# if not, fails the test, outputting the offending line number.
-shell_test::assert_contains() {
-  local -r GOT="$1"
-  local -r WANT="$2"
-  local -r LINENO="$3"
-  if [[ ! "${GOT}" =~ "${WANT}" ]]; then
-    shell_test::fail "line ${LINENO}: expected '${GOT}' contains '${WANT}'"
-  fi
-}
-
-# shell_test::at_exit is executed when the shell script exits.
-shell_test::at_exit() {
-  echo "${shell_test_EXIT_MESSAGE}"
-  # Note: shell::at_exit unsets our trap, so it won't run again on exit.
-  shell::at_exit
-  shell::kill_child_processes
-}
-
-# shell_test::fail is used to identify a failing test.
-#
-# Example:
-#   <command> || shell_test::fail "line ${LINENO}: <error message>"
-shell_test::fail() {
-  [[ "$#" -gt 0 ]] && echo "$0 $*"
-  shell_test_EXIT_MESSAGE="FAIL"
-  exit 1
-}
-
-# shell_test::pass is used to identify a passing test.
-shell_test::pass() {
-  shell_test_EXIT_MESSAGE="PASS"
-  exit 0
-}
-
-# shell_test::build_go_binary builds the binary for the given go package ($1) with
-# the given output ($2) in $shell_test_BIN_DIR. Before building a binary, it will
-# check whether it already exists. If so, it will skip it.
-shell_test::build_go_binary() {
-  pushd "${shell_test_BIN_DIR}" > /dev/null
-  local -r PACKAGE="$1"
-  shift
-  local OUTPUT="$(basename ${PACKAGE})"
-  if [[ -f "${OUTPUT}" ]]; then
-    echo "${shell_test_BIN_DIR}/${OUTPUT}"
-    return
-  fi
-  go build -o "${OUTPUT}" "$@" "${PACKAGE}" 2>/dev/null || shell_test::fail "line ${LINENO}: failed to build ${OUTPUT}"
-  echo "${shell_test_BIN_DIR}/${OUTPUT}"
-  popd > /dev/null
-}
-
-# shell_test::setup_server_test is common boilerplate used for testing vanadium
-# servers. In particular, this function sets up an instance of the mount table
-# daemon, and sets the V23_NAMESPACE environment variable accordingly.  It also
-# sets up credentials as needed.
-shell_test::setup_server_test() {
-  if [[ -n ${shell_test_RUNNING_UNDER_AGENT+1} ]]; then
-    shell_test::setup_server_test_agent
-  else
-    shell_test::setup_server_test_no_agent
-  fi
-}
-
-# shell_test::setup_mounttable brings up a mounttable and sets the
-# V23_NAMESPACE to point to it.
-shell_test::setup_mounttable() {
-  # Build the mount table daemon and related tools.
-  MOUNTTABLED_BIN="$(shell_test::build_go_binary 'v.io/x/ref/services/mounttable/mounttabled')"
-
-  # Start the mounttable daemon.
-  local -r MT_LOG=$(shell::tmp_file)
-  if [[ -n ${shell_test_RUNNING_UNDER_AGENT+1} ]]; then
-    shell::run_server "${shell_test_DEFAULT_SERVER_TIMEOUT}" "${MT_LOG}" "${MT_LOG}" "${VRUN}" "${MOUNTTABLED_BIN}" --v23.tcp.address="127.0.0.1:0" &> /dev/null || (cat "${MT_LOG}" && shell_test::fail "line ${LINENO}: failed to start mounttabled")
-  else
-    shell::run_server "${shell_test_DEFAULT_SERVER_TIMEOUT}" "${MT_LOG}" "${MT_LOG}" "${MOUNTTABLED_BIN}" --v23.tcp.address="127.0.0.1:0" &> /dev/null || (cat "${MT_LOG}" && shell_test::fail "line ${LINENO}: failed to start mounttabled")
-  fi
-  shell::timed_wait_for "${shell_test_DEFAULT_MESSAGE_TIMEOUT}" "${MT_LOG}" "Mount table service" || shell_test::fail "line ${LINENO}: failed to find expected output"
-
-  export V23_NAMESPACE=$(grep "Mount table service at:" "${MT_LOG}" | sed -e 's/^.*endpoint: //')
-  [[ -n "${V23_NAMESPACE}" ]] || shell_test::fail "line ${LINENO}: failed to read endpoint from logfile"
-}
-
-# shell_test::setup_server_test_no_agent does the work of setup_server_test when
-# not running under enable_agent (using credentials directories).
-shell_test::setup_server_test_no_agent() {
-  [[ ! -n ${shell_test_RUNNING_UNDER_AGENT+1} ]] \
-    || shell_test::fail "setup_server_test_no_agent called when running under enable_agent"
-  shell_test::setup_mounttable
-
-  # Create and use a new credentials directory /after/ the mount table is
-  # started so that servers or clients started after this point start with different
-  # credentials.
-  local -r TMP=${V23_CREDENTIALS:-}
-  if [[ -z "${TMP}" ]]; then
-    export V23_CREDENTIALS=$(shell::tmp_dir)
-  fi
-}
-
-# shell_test::setup_server_test_agent does the work of setup_server_test when
-# running under enable_agent.
-shell_test::setup_server_test_agent() {
-  [[ -n ${shell_test_RUNNING_UNDER_AGENT+1} ]] \
-    || shell_test::fail "setup_server_test_agent called when not running under enable_agent"
-  shell_test::setup_mounttable
-}
-
-# shell_test::start_server is used to start a server. The server is
-# considered started when it successfully mounts itself into a mount
-# table; an event that is detected using the shell::wait_for
-# function.
-#
-# Example:
-#   shell_test::start_server <server> <args>
-shell_test::start_server() {
-  START_SERVER_LOG_FILE=$(shell::tmp_file)
-  START_SERVER_PID=$(shell::run_server "${shell_test_DEFAULT_SERVER_TIMEOUT}" "${START_SERVER_LOG_FILE}" "${START_SERVER_LOG_FILE}" "$@" -logtostderr -vmodule=publisher=2)
-  shell::timed_wait_for "${shell_test_DEFAULT_MESSAGE_TIMEOUT}" "${START_SERVER_LOG_FILE}" "ipc pub: mount"
-}
-
-# shell_test::credentials is used to create a new VeyronCredentials directory.
-# The directory is initialized with a new principal that has a self-signed
-# blessing of the specified name set as default and shareable with all peers.
-#
-# Example:
-#   shell_test::credentials <self blessing name>
-shell_test::credentials() {
-  [[ ! -n ${shell_test_RUNNING_UNDER_AGENT+1} ]] \
-    || shell_test::fail "credentials called when running under enable_agent"
-  local -r PRINCIPAL_BIN="$(shell_test::build_go_binary 'v.io/x/ref/cmd/principal')"
-  local -r CRED=$(shell::tmp_dir)
-  "${PRINCIPAL_BIN}" create --overwrite=true "${CRED}" "$1" >/dev/null || shell_test::fail "line ${LINENO}: create failed"
-  echo "${CRED}"
-}
-
-# shell_test::forkcredentials is used to create a new VeyronCredentials directory
-# that is initialized with a principal blessed by the provided principal under the
-# provided extension.
-#
-# Example:
-#   shell_test::forkcredentials <blesser principal's VeyronCredentials> <extension>
-shell_test::forkcredentials() {
-  [[ ! -n ${shell_test_RUNNING_UNDER_AGENT+1} ]] \
-    || shell_test::fail "forkcredentials called when running under enable_agent"
-
-  local -r PRINCIPAL_BIN="$(shell_test::build_go_binary 'v.io/x/ref/cmd/principal')"
-  local -r FORKCRED=$(shell::tmp_dir)
-  "${PRINCIPAL_BIN}" create --overwrite=true "${FORKCRED}" self >/dev/null || shell_test::fail "line ${LINENO}: create failed"
-  "${PRINCIPAL_BIN}" --v23.credentials="$1" bless --require-caveats=false "${FORKCRED}" "$2" >blessing || shell_test::fail "line ${LINENO}: bless failed"
-  "${PRINCIPAL_BIN}" --v23.credentials="${FORKCRED}" set default blessing || shell_test::fail "line ${LINENO}: set default failed"
-  "${PRINCIPAL_BIN}" --v23.credentials="${FORKCRED}" set forpeer blessing ... || shell_test::fail "line ${LINENO}: set forpeer failed"
-  echo "${FORKCRED}"
-}
-
-# shell_test::enable_agent causes the test to be executed under the security
-# agent.  It sets up the agent and then runs itself under that agent.
-#
-# Example:
-#   shell_test::enable_agent "$@"
-#   build() {
-#     ...
-#   }
-#   main() {
-#     build
-#     ...
-#     shell_test::pass
-#   }
-#   main "$@"
-shell_test::enable_agent() {
-  if [[ ! -n ${shell_test_RUNNING_UNDER_AGENT+1} ]]; then
-    local -r AGENTD="$(shell_test::build_go_binary 'v.io/x/ref/services/agent/agentd')"
-    local -r WORKDIR="${shell_test_WORK_DIR}"
-    export shell_test_RUNNING_UNDER_AGENT=1
-    V23_CREDENTIALS="${WORKDIR}/credentials" exec ${AGENTD} --no-passphrase --additional-principals="${WORKDIR}/childcredentials" bash -"$-" "$0" "$@"
-    shell_test::fail "failed to run test under agent"
-  else
-    VRUN="$(shell_test::build_go_binary 'v.io/x/ref/cmd/vrun')"
-  fi
-}
diff --git a/client/lib/shell/test.sh b/client/lib/shell/test.sh
deleted file mode 100755
index 972986d..0000000
--- a/client/lib/shell/test.sh
+++ /dev/null
@@ -1,115 +0,0 @@
-#!/bin/bash
-# Copyright 2015 The Vanadium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file.
-
-
-#
-# Unit tests for the shell functions in this directory
-#
-
-source "${V23_ROOT}/release/projects/playground/client/lib/shell/shell_test.sh"
-
-test_assert() {
-  shell_test::assert_eq "foo" "foo" "${LINENO}"
-  shell_test::assert_eq "42" "42" "${LINENO}"
-  shell_test::assert_ge "1" "1" "${LINENO}"
-  shell_test::assert_ge "42" "1" "${LINENO}"
-  shell_test::assert_gt "42" "1" "${LINENO}"
-  shell_test::assert_le "1" "1" "${LINENO}"
-  shell_test::assert_le "1" "42" "${LINENO}"
-  shell_test::assert_lt "1" "42" "${LINENO}"
-  shell_test::assert_ne "1" "42" "${LINENO}"
-  shell_test::assert_ne "foo" "bar" "${LINENO}"
-}
-
-test_run_server() {
-  shell::run_server 1 /dev/null /dev/null foobar > /dev/null 2>&1
-  shell_test::assert_eq "$?" "1" "${LINENO}"
-
-  shell::run_server 1 /dev/null /dev/null sleep 10 > /dev/null 2>&1
-  shell_test::assert_eq "$?" "0" "${LINENO}"
-}
-
-test_timed_wait_for() {
-  local GOT WANT
-  local -r TMPFILE=$(shell::tmp_file)
-  touch "${TMPFILE}"
-
-  shell::timed_wait_for 2 "${TMPFILE}" "doesn't matter, it's all zeros anyway"
-  shell_test::assert_eq "$?" "1" "${LINENO}"
-
-  echo "foo" > "${TMPFILE}"
-  shell::timed_wait_for 2 "${TMPFILE}" "bar"
-  shell_test::assert_eq "$?" "1" "${LINENO}"
-
-  echo "bar" >> "${TMPFILE}"
-  echo "baz" >> "${TMPFILE}"
-  shell::timed_wait_for 2 "${TMPFILE}" "bar"
-  shell_test::assert_eq "$?" "0" "${LINENO}"
-}
-
-# rmpublickey replaces public keys (16 hex bytes, :-separated) with XX:....
-# This substitution enables comparison with golden output even when keys are freshly
-# minted by the "principal create" command.
-rmpublickey() {
-    sed -e "s/\([0-9a-f]\{2\}:\)\{15\}[0-9a-f]\{2\}/XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX/g"
-}
-
-test_credentials() {
-  local -r CRED=$(shell_test::credentials alice)
-
-  local -r PRINCIPAL_BIN="$(shell_test::build_go_binary 'v.io/x/ref/cmd/principal')"
-
-  "${PRINCIPAL_BIN}" --v23.credentials="${CRED}" dump >alice.dump ||  shell_test::fail "line ${LINENO}: ${PRINCIPAL_BIN} dump ${CRED} failed"
-  cat alice.dump | rmpublickey >got || shell_test::fail "line ${LINENO}: cat alice.dump | rmpublickey failed"
-  cat >want <<EOF
-Public key : XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
----------------- BlessingStore ----------------
-Default blessings: alice
-Peer pattern                   : Blessings
-...                            : alice
----------------- BlessingRoots ----------------
-Public key                                      : Pattern
-XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX : [alice]
-EOF
-  if ! diff got want; then
-    shell_test::fail "line ${LINENO}"
-  fi
-}
-
-test_forkcredentials() {
-  local -r CRED=$(shell_test::credentials alice)
-  local -r FORKCRED=$(shell_test::forkcredentials "${CRED}" fork)
-
-  local -r PRINCIPAL_BIN="$(shell_test::build_go_binary 'v.io/x/ref/cmd/principal')"
-
-  "${PRINCIPAL_BIN}" --v23.credentials="${FORKCRED}" dump >alice.dump ||  shell_test::fail "line ${LINENO}: ${PRINCIPAL_BIN} dump ${CRED} failed"
-  cat alice.dump | rmpublickey >got || shell_test::fail "line ${LINENO}: cat alice.dump | rmpublickey failed"
-  cat >want <<EOF
-Public key : XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
----------------- BlessingStore ----------------
-Default blessings: alice/fork
-Peer pattern                   : Blessings
-...                            : alice/fork
----------------- BlessingRoots ----------------
-Public key                                      : Pattern
-XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX : [alice]
-XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX : [self]
-EOF
-  if ! diff got want; then
-    shell_test::fail "line ${LINENO}"
-  fi
-}
-
-main() {
-  test_assert || shell_test::fail "assert"
-  test_run_server || shell_test::fail "test_run_server"
-  test_timed_wait_for  || shell_test::fail "test_run_server"
-  test_credentials  || shell_test::fail "test_credentials"
-  test_forkcredentials  || shell_test::fail "test_forkcredentials"
-
-  shell_test::pass
-}
-
-main "$@"
diff --git a/client/package.json b/client/package.json
index e0689aa..175cdbd 100644
--- a/client/package.json
+++ b/client/package.json
@@ -32,6 +32,7 @@
   },
   "devDependencies": {
     "browserify": "^8.1.1",
+    "dependency-check": "^2.4.0",
     "http-server": "^0.7.4",
     "jshint": "^2.6.0",
     "minimist": "^1.1.1",
diff --git a/client/test.sh b/client/test.sh
deleted file mode 100755
index 1422eeb..0000000
--- a/client/test.sh
+++ /dev/null
@@ -1,46 +0,0 @@
-#!/bin/bash
-# Copyright 2015 The Vanadium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file.
-
-# Tests that default playground examples execute successfully.
-# If any new examples are added, they should be appended to $EXAMPLES below.
-
-# To debug playground compile errors you can build examples locally, e.g.:
-# $ cd bundles/fortune/src
-# $ GOPATH=$(dirname $(pwd)) VDLPATH=$(dirname $(pwd)) v23 go install ./...
-
-# v.io/core/shell/lib/shell_test.sh sourced via
-# playground/client/lib/shell/pg_test_util.sh (shell_test.sh has side
-# effects, should not be sourced again)
-source "${V23_ROOT}/release/projects/playground/client/lib/shell/pg_test_util.sh"
-
-main() {
-  cd "${shell_test_WORK_DIR}"
-
-  setup_environment
-
-  build_go_binaries
-  install_vanadium_js
-  install_pgbundle
-
-  local -r PG_BUNDLES_DIR="${PLAYGROUND_ROOT}/client/bundles"
-
-  local -r EXAMPLES="fortune"
-
-  for e in ${EXAMPLES}; do
-    for p in "${PG_BUNDLES_DIR}"/*.bundle; do
-      local d="${PG_BUNDLES_DIR}/${e}"
-      local description="${e} with $(basename ${p})"
-      echo -e "\n\n>>>>> Test ${description}\n\n"
-      test_pg_example "${d}" "${p}" "-v=true --runTimeout=5s" || shell_test::fail "${description}: failed to run"
-      # TODO(sadovsky): Make this "clean exit" check more robust.
-      grep -q "\"Exited cleanly.\"" builder.out || shell_test::fail "${description}: did not exit cleanly"
-      rm -f builder.out
-    done
-  done
-
-  shell_test::pass
-}
-
-main "$@"
diff --git a/go/src/v.io/x/playground/Makefile b/go/src/v.io/x/playground/Makefile
index 03b9fd3..77c430a 100644
--- a/go/src/v.io/x/playground/Makefile
+++ b/go/src/v.io/x/playground/Makefile
@@ -1,5 +1,3 @@
-PATH := bin:node_modules/.bin:$(PATH)
-PATH := $(PATH):$(V23_ROOT)/third_party/cout/node/bin
 PATH := $(V23_ROOT)/release/projects/playground/go/bin:$(PATH)
 SHELL := /bin/bash -euo pipefail
 export GOPATH := $(V23_ROOT)/release/projects/playground/go:$(GOPATH)
@@ -43,6 +41,7 @@
 	@echo "on the appropriate examples in the config directory."
 	@exit 1;
 
+# Temporary workaround for linking Go tests into Jenkins.
 .PHONY: test
 test:
-	v23 run ./test.sh
+	v23 go test v.io/x/playground/builder -v23.tests
diff --git a/go/src/v.io/x/playground/builder/builder_v23_test.go b/go/src/v.io/x/playground/builder/builder_v23_test.go
new file mode 100644
index 0000000..050e293
--- /dev/null
+++ b/go/src/v.io/x/playground/builder/builder_v23_test.go
@@ -0,0 +1,162 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main_test
+
+import (
+	"bytes"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+
+	_ "v.io/x/ref/profiles"
+	"v.io/x/ref/test/v23tests"
+)
+
+//go:generate v23 test generate
+
+var (
+	vanadiumRoot, nodejsRoot, playgroundRoot string
+)
+
+func initTest(i *v23tests.T) (builder *v23tests.Binary) {
+	vanadiumRoot = os.Getenv("V23_ROOT")
+	if len(vanadiumRoot) == 0 {
+		i.Fatal("V23_ROOT must be set")
+	}
+	nodejsRoot = filepath.Join(vanadiumRoot, "third_party", "cout", "node", "bin")
+
+	i.BuildGoPkg("v.io/x/ref/services/wspr/wsprd", "-a", "-tags", "wspr")
+	i.BuildGoPkg("v.io/x/ref/cmd/principal")
+	i.BuildGoPkg("v.io/x/ref/cmd/vdl")
+	i.BuildGoPkg("v.io/x/ref/services/mounttable/mounttabled")
+	i.BuildGoPkg("v.io/x/ref/services/proxy/proxyd")
+
+	playgroundRoot = filepath.Join(vanadiumRoot, "release", "projects", "playground")
+
+	npmInstall(i, filepath.Join(vanadiumRoot, "release/javascript/core"))
+	npmInstall(i, filepath.Join(playgroundRoot, "pgbundle"))
+
+	return i.BuildGoPkg("v.io/x/playground/builder")
+}
+
+func npmInstall(i *v23tests.T, dir string) {
+	npmBin := i.BinaryFromPath(filepath.Join(nodejsRoot, "npm"))
+	npmBin.Run("install", "--production", dir)
+}
+
+// Bundles a playground example and tests it using builder.
+// - dir is the root directory of example to test
+// - globFile is the path to the glob file with file patterns to use from dir
+// - args are the arguments to call builder with
+func runPGExample(i *v23tests.T, builder *v23tests.Binary, globFile, dir string, args ...string) *v23tests.Invocation {
+	nodeBin := i.BinaryFromPath(filepath.Join(nodejsRoot, "node"))
+	bundle := nodeBin.Run("./node_modules/.bin/pgbundle", "--verbose", globFile, dir)
+
+	tmp := i.NewTempDir("")
+	cwd := i.Pushd(tmp)
+	defer i.Popd()
+	old := filepath.Join(cwd, "node_modules")
+	if err := os.Symlink(old, filepath.Join(".", filepath.Base(old))); err != nil {
+		i.Fatalf("%s: symlink: failed: %v", i.Caller(2), err)
+	}
+
+	PATH := "PATH=" + i.BinDir() + ":" + nodejsRoot
+	if path := os.Getenv("PATH"); len(path) > 0 {
+		PATH += ":" + path
+	}
+	stdin := bytes.NewBufferString(bundle)
+	return builder.WithEnv(PATH).WithStdin(stdin).Start(args...)
+}
+
+// Sets up a glob file with the given files, then runs builder.
+func testWithFiles(i *v23tests.T, builder *v23tests.Binary, testdataDir string, files ...string) *v23tests.Invocation {
+	globFile := filepath.Join(i.NewTempDir(""), "test.bundle")
+	if err := ioutil.WriteFile(globFile, []byte(strings.Join(files, "\n")+"\n"), 0644); err != nil {
+		i.Fatalf("%s: write(%q): failed: %v", i.Caller(1), globFile, err)
+	}
+	return runPGExample(i, builder, globFile, testdataDir, "-verbose=true", "--includeV23Env=true", "--runTimeout=5s")
+}
+
+// Tests the playground builder tool.
+func V23TestPlaygroundBuilder(i *v23tests.T) {
+	i.Pushd(i.NewTempDir(""))
+	defer i.Popd()
+	builderBin := initTest(i)
+
+	cases := []struct {
+		name  string
+		files []string
+	}{
+		{"basic ping (go -> go)",
+			[]string{"src/pong/pong.go", "src/ping/ping.go", "src/pingpong/wire.vdl"}},
+		{"basic ping (js -> js)",
+			[]string{"src/pong/pong.js", "src/ping/ping.js", "src/pingpong/wire.vdl"}},
+		{"basic ping (js -> go)",
+			[]string{"src/pong/pong.go", "src/ping/ping.js", "src/pingpong/wire.vdl"}},
+		{"basic ping (go -> js)",
+			[]string{"src/pong/pong.js", "src/ping/ping.go", "src/pingpong/wire.vdl"}},
+	}
+
+	testdataDir := filepath.Join(playgroundRoot, "go", "src", "v.io", "x", "playground", "testdata")
+
+	runCases := func(authfile string, patterns []string) {
+		for _, c := range cases {
+			files := c.files
+			if len(authfile) > 0 {
+				files = append(files, authfile)
+			}
+			inv := testWithFiles(i, builderBin, testdataDir, files...)
+			i.Logf("test: %s", c.name)
+			inv.ExpectSetEventuallyRE(patterns...)
+		}
+	}
+
+	i.Logf("Test as the same principal")
+	runCases("", []string{"PING", "PONG"})
+
+	i.Logf("Test with authorized blessings")
+	runCases("src/ids/authorized.id", []string{"PING", "PONG"})
+
+	i.Logf("Test with expired blessings")
+	runCases("src/ids/expired.id", []string{"not authorized"})
+
+	i.Logf("Test with unauthorized blessings")
+	runCases("src/ids/unauthorized.id", []string{"not authorized"})
+}
+
+// Tests that default playground examples execute successfully.
+// If any new examples are added, they should be added to bundles below.
+func V23TestPlaygroundBundles(i *v23tests.T) {
+	i.Pushd(i.NewTempDir(""))
+	defer i.Popd()
+	builderBin := initTest(i)
+
+	bundlesDir := filepath.Join(playgroundRoot, "client", "bundles")
+
+	// Add any new examples here.
+	bundles := []string{
+		"fortune",
+	}
+
+	globFiles := []string{
+		"go.bundle",
+		"js.bundle",
+		"go_js.bundle",
+		"js_go.bundle",
+	}
+
+	for _, bundle := range bundles {
+		i.Logf("Test bundle %q", bundle)
+		bundlePath := filepath.Join(bundlesDir, bundle)
+		for _, globFile := range globFiles {
+			globFilePath := filepath.Join(bundlesDir, globFile)
+			inv := runPGExample(i, builderBin, globFilePath, bundlePath, "-verbose=true", "--runTimeout=5s")
+			i.Logf("glob: %q", globFile)
+			// TODO(ivanpi,sadovsky): Make this "clean exit" check more robust.
+			inv.ExpectSetEventuallyRE("Exited cleanly\\.")
+		}
+	}
+}
diff --git a/go/src/v.io/x/playground/builder/main.go b/go/src/v.io/x/playground/builder/main.go
index 7f9559c..6a1670d 100644
--- a/go/src/v.io/x/playground/builder/main.go
+++ b/go/src/v.io/x/playground/builder/main.go
@@ -42,7 +42,7 @@
 )
 
 var (
-	verbose              = flag.Bool("v", true, "Whether to output debug messages.")
+	verbose              = flag.Bool("verbose", true, "Whether to output debug messages.")
 	includeServiceOutput = flag.Bool("includeServiceOutput", false, "Whether to stream service (mounttable, wspr, proxy) output to clients.")
 	includeV23Env        = flag.Bool("includeV23Env", false, "Whether to log the output of \"v23 env\" before compilation.")
 	// TODO(ivanpi): Separate out mounttable, proxy, wspr timeouts. Add compile timeout. Revise default.
diff --git a/go/src/v.io/x/playground/v23_test.go b/go/src/v.io/x/playground/builder/v23_test.go
similarity index 68%
rename from go/src/v.io/x/playground/v23_test.go
rename to go/src/v.io/x/playground/builder/v23_test.go
index 1c4c69c..4ec5654 100644
--- a/go/src/v.io/x/playground/v23_test.go
+++ b/go/src/v.io/x/playground/builder/v23_test.go
@@ -4,7 +4,7 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
-package playground_test
+package main_test
 
 import "testing"
 import "os"
@@ -20,6 +20,10 @@
 	os.Exit(r)
 }
 
-func TestV23Playground(t *testing.T) {
-	v23tests.RunTest(t, V23TestPlayground)
+func TestV23PlaygroundBuilder(t *testing.T) {
+	v23tests.RunTest(t, V23TestPlaygroundBuilder)
+}
+
+func TestV23PlaygroundBundles(t *testing.T) {
+	v23tests.RunTest(t, V23TestPlaygroundBundles)
 }
diff --git a/go/src/v.io/x/playground/playground_v23_test.go b/go/src/v.io/x/playground/playground_v23_test.go
deleted file mode 100644
index 8eb8577..0000000
--- a/go/src/v.io/x/playground/playground_v23_test.go
+++ /dev/null
@@ -1,139 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package playground_test
-
-import (
-	"bytes"
-	"io/ioutil"
-	"os"
-	"path/filepath"
-	"strings"
-
-	_ "v.io/x/ref/profiles"
-	"v.io/x/ref/test/v23tests"
-)
-
-//go:generate v23 test generate
-
-var (
-	vanadiumRoot, nodejsRoot, playgroundRoot string
-)
-
-func init() {
-	vanadiumRoot = os.Getenv("V23_ROOT")
-	if len(vanadiumRoot) == 0 {
-		panic("V23_ROOT must be set")
-	}
-	nodejsRoot = filepath.Join(vanadiumRoot, "third_party", "cout", "node", "bin")
-}
-
-func golist(i *v23tests.T, pkg string) string {
-	v23 := filepath.Join(vanadiumRoot, "bin/v23")
-	return i.Run(v23, "go", "list", "-f", "{{.Dir}}", pkg)
-}
-
-func npmInstall(i *v23tests.T, dir string) {
-	npmBin := i.BinaryFromPath(filepath.Join(nodejsRoot, "npm"))
-	npmBin.Run("install", "--production", dir)
-}
-
-// Bundles a playground example and tests it using builder.
-// - dir is the root directory of example to test
-// - globFile is the path to the glob file with file patterns to use from dir
-// - args are the arguments to call builder with
-func runPGExample(i *v23tests.T, globFile, dir string, args ...string) *v23tests.Invocation {
-	bundle := i.Run("./node_modules/.bin/pgbundle", "--verbose", globFile, dir)
-
-	tmp := i.NewTempDir()
-	cwd := i.Pushd(tmp)
-	defer i.Popd()
-	old := filepath.Join(cwd, "node_modules")
-	if err := os.Symlink(old, filepath.Join(".", filepath.Base(old))); err != nil {
-		i.Fatalf("%s: symlink: failed: %v", i.Caller(2), err)
-	}
-
-	// TODO(ivanpi): move this out so it only gets invoked once even though
-	// the binary is cached.
-	builderBin := i.BuildGoPkg("v.io/x/playground/builder")
-
-	PATH := "PATH=" + i.BinDir() + ":" + nodejsRoot
-	if path := os.Getenv("PATH"); len(path) > 0 {
-		PATH += ":" + path
-	}
-	stdin := bytes.NewBufferString(bundle)
-	return builderBin.WithEnv(PATH).WithStdin(stdin).Start(args...)
-}
-
-// Sets up a glob file with the given files, then runs builder.
-func testWithFiles(i *v23tests.T, pgRoot string, files ...string) *v23tests.Invocation {
-	testdataDir := filepath.Join(pgRoot, "testdata")
-	globFile := filepath.Join(i.NewTempDir(), "test.bundle")
-	if err := ioutil.WriteFile(globFile, []byte(strings.Join(files, "\n")+"\n"), 0644); err != nil {
-		i.Fatalf("%s: write(%q): failed: %v", i.Caller(1), globFile, err)
-	}
-	return runPGExample(i, globFile, testdataDir, "-v=true", "--includeV23Env=true", "--runTimeout=5s")
-}
-
-func V23TestPlayground(i *v23tests.T) {
-	i.Pushd(i.NewTempDir())
-	defer i.Popd()
-
-	v23tests.RunRootMT(i, "--v23.tcp.address=127.0.0.1:0")
-
-	i.BuildGoPkg("v.io/x/ref/services/wspr/wsprd", "-a", "-tags", "wspr")
-	i.BuildGoPkg("v.io/x/ref/cmd/principal")
-	i.BuildGoPkg("v.io/x/ref/cmd/vdl")
-	i.BuildGoPkg("v.io/x/ref/services/proxy/proxyd")
-
-	playgroundPkg := golist(i, "v.io/x/playground")
-	// strip last three directory components, much easier to read in
-	// errors than <path>/../../..
-	playgroundRoot = filepath.Dir(playgroundPkg)
-	playgroundRoot = filepath.Dir(playgroundRoot)
-	playgroundRoot = filepath.Dir(playgroundRoot)
-
-	npmInstall(i, filepath.Join(vanadiumRoot, "release/javascript/core"))
-	npmInstall(i, filepath.Join(playgroundRoot, "pgbundle"))
-
-	cases := []struct {
-		name  string
-		files []string
-	}{
-		{"basic ping (go -> go)",
-			[]string{"src/pong/pong.go", "src/ping/ping.go", "src/pingpong/wire.vdl"}},
-		{"basic ping (js -> js)",
-			[]string{"src/pong/pong.js", "src/ping/ping.js", "src/pingpong/wire.vdl"}},
-		{"basic ping (js -> go)",
-			[]string{"src/pong/pong.go", "src/ping/ping.js", "src/pingpong/wire.vdl"}},
-		{"basic ping (go -> js)",
-			[]string{"src/pong/pong.js", "src/ping/ping.go", "src/pingpong/wire.vdl"}},
-	}
-
-	runCases := func(authfile string, patterns []string) {
-		for _, c := range cases {
-			files := c.files
-			if len(authfile) > 0 {
-				files = append(files, authfile)
-			}
-			inv := testWithFiles(i, playgroundPkg, files...)
-			i.Logf("test: %s", c.name)
-			inv.ExpectSetEventuallyRE(patterns...)
-		}
-	}
-
-	i.Logf("Test as the same principal")
-	runCases("", []string{"PING", "PONG"})
-
-	i.Logf("Test with authorized blessings")
-	runCases("src/ids/authorized.id", []string{"PING", "PONG"})
-
-	// TODO(bprosnitz) Re-enable with issue #986 (once javascript supports expired blessings).
-	//i.Logf("Test with expired blessings")
-	//runCases("src/ids/expired.id", []string{"not authorized"})
-
-	i.Logf("Test with unauthorized blessings")
-	runCases("src/ids/unauthorized.id", []string{"not authorized"})
-
-}
diff --git a/go/src/v.io/x/playground/test.sh b/go/src/v.io/x/playground/test.sh
deleted file mode 100755
index 47dcb5c..0000000
--- a/go/src/v.io/x/playground/test.sh
+++ /dev/null
@@ -1,81 +0,0 @@
-#!/bin/bash
-# Copyright 2015 The Vanadium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file.
-
-# Tests the playground builder tool.
-
-# v.io/core/shell/lib/shell_test.sh sourced via
-# playground/client/lib/shell/pg_test_util.sh (shell_test.sh has side
-# effects, should not be sourced again)
-source "${V23_ROOT}/release/projects/playground/client/lib/shell/pg_test_util.sh"
-
-# Sets up a glob file with the given files, then runs builder.
-test_with_files() {
-  local -r TESTDATA_DIR="${V23_ROOT}/release/projects/playground/go/src/v.io/x/playground/testdata"
-
-  # Write input file paths to the glob file.
-  local -r CONFIG_FILE="$(shell::tmp_dir)/test.bundle"
-  echo "$*" | tr ' ' '\n' > "${CONFIG_FILE}"
-
-  test_pg_example "${TESTDATA_DIR}" "${CONFIG_FILE}" "-v=true --includeV23Env=true --runTimeout=5s"
-}
-
-main() {
-  cd "${shell_test_WORK_DIR}"
-
-  setup_environment
-
-  build_go_binaries
-  install_vanadium_js
-  install_pgbundle
-
-  echo -e "\n\n>>>>> Test as the same principal\n\n"
-
-  test_with_files "src/pong/pong.go" "src/ping/ping.go" "src/pingpong/wire.vdl" || shell_test::fail "line ${LINENO}: basic ping (go -> go)"
-  grep -q PING builder.out || shell_test::fail "line ${LINENO}: no PING"
-  grep -q PONG builder.out || shell_test::fail "line ${LINENO}: no PONG"
-
-  test_with_files "src/pong/pong.js" "src/ping/ping.js" "src/pingpong/wire.vdl" || shell_test::fail "line ${LINENO}: basic ping (js -> js)"
-  grep -q PING builder.out || shell_test::fail "line ${LINENO}: no PING"
-  grep -q PONG builder.out || shell_test::fail "line ${LINENO}: no PONG"
-
-  test_with_files "src/pong/pong.go" "src/ping/ping.js" "src/pingpong/wire.vdl" || shell_test::fail "line ${LINENO}: basic ping (js -> go)"
-  grep -q PING builder.out || shell_test::fail "line ${LINENO}: no PING"
-  grep -q PONG builder.out || shell_test::fail "line ${LINENO}: no PONG"
-
-  test_with_files "src/pong/pong.js" "src/ping/ping.go" "src/pingpong/wire.vdl" || shell_test::fail "line ${LINENO}: basic ping (go -> js)"
-  grep -q PING builder.out || shell_test::fail "line ${LINENO}: no PING"
-  grep -q PONG builder.out || shell_test::fail "line ${LINENO}: no PONG"
-
-  echo -e "\n\n>>>>> Test with authorized blessings\n\n"
-
-  test_with_files "src/pong/pong.go" "src/ping/ping.go" "src/pingpong/wire.vdl" "src/ids/authorized.id" || shell_test::fail "line ${LINENO}: authorized id (go -> go)"
-  grep -q PING builder.out || shell_test::fail "line ${LINENO}: no PING"
-  grep -q PONG builder.out || shell_test::fail "line ${LINENO}: no PONG"
-
-  test_with_files "src/pong/pong.js" "src/ping/ping.js" "src/pingpong/wire.vdl" "src/ids/authorized.id" || shell_test::fail "line ${LINENO}: authorized id (js -> js)"
-  grep -q PING builder.out || shell_test::fail "line ${LINENO}: no PING"
-  grep -q PONG builder.out || shell_test::fail "line ${LINENO}: no PONG"
-
-  echo -e "\n\n>>>>> Test with expired blessings\n\n"
-
-  test_with_files "src/pong/pong.go" "src/ping/ping.go" "src/pingpong/wire.vdl" "src/ids/expired.id" || shell_test::fail  "line ${LINENO}: expired id (go -> go)"
-  grep -q "not authorized" builder.out || shell_test::fail "line ${LINENO}: rpc with expired id succeeded (go -> go)"
-
-# TODO(bprosnitz) Re-enable with issue #986 (once javascript supports expired blessings).
-# test_with_files "src/pong/pong.js" "src/ping/ping.js" "src/pingpong/wire.vdl" "src/ids/expired.id" || shell_test::fail  "line ${LINENO}: expired id (js -> js)"
-#  grep -q "not authorized" builder.out || shell_test::fail "line ${LINENO}: rpc with expired id succeeded (js -> js)"
-
-  echo -e "\n\n>>>>> Test with unauthorized blessings\n\n"
-
-  test_with_files "src/pong/pong.go" "src/ping/ping.go" "src/pingpong/wire.vdl" "src/ids/unauthorized.id" || shell_test::fail  "line ${LINENO}: unauthorized id (go -> go)"
-  grep -q "not authorized" builder.out || shell_test::fail "line ${LINENO}: rpc with unauthorized id succeeded (go -> go)"
-
-  test_with_files "src/pong/pong.js" "src/ping/ping.js" "src/pingpong/wire.vdl" "src/ids/unauthorized.id" || shell_test::fail  "line ${LINENO}: unauthorized id (js -> js)"
-  grep -q "not authorized" builder.out || shell_test::fail "line ${LINENO}: rpc with unauthorized id succeeded (js -> js)"
-
-  shell_test::pass
-}
-
-main "$@"
diff --git a/pgbundle/Makefile b/pgbundle/Makefile
index 424da85..9b1052e 100644
--- a/pgbundle/Makefile
+++ b/pgbundle/Makefile
@@ -1,3 +1,4 @@
+PATH := node_modules/.bin:$(PATH)
 PATH := $(PATH):$(V23_ROOT)/third_party/cout/node/bin
 SHELL := /bin/bash -euo pipefail
 
@@ -11,6 +12,10 @@
 	@$(RM) -rf node_modules
 	@$(RM) -rf npm-debug.log
 
+.PHONY: dependency-check
+dependency-check: package.json node_modules
+	dependency-check $<
+
 .PHONY: lint
-lint:
-	@jshint .
+lint: dependency-check
+	jshint .
diff --git a/pgbundle/package.json b/pgbundle/package.json
index e07967c..dd49453 100644
--- a/pgbundle/package.json
+++ b/pgbundle/package.json
@@ -1,4 +1,5 @@
 {
+  "private": true,
   "name": "pgbundle",
   "description": "Vanadium playground file bundler",
   "version": "0.0.1",
@@ -12,12 +13,13 @@
     "minimist": "^1.1.0"
   },
   "homepage": "https://vanadium.googlesource.com/release.projects.playground/+/master/pgbundle",
-  "private": true,
+  "devDependencies": {
+    "dependency-check": "^2.4.0",
+    "jshint": "^2.6.0"
+  },
   "repository": {
     "type": "git",
     "url": "https://vanadium.googlesource.com/release.projects.playground"
   },
-  "devDependencies": {
-    "jshint": "^2.6.0"
-  }
+  "license": "BSD"
 }