Repository: codalab/codalab-worksheets

Issue:
[USER ISSUE] Multiple aliases pointing to the same bundle spec are not supported
**Describe the bug**
When I set two bundle dependencies with the same bundle spec but different aliases, one of them is get dropped.

**Codalab Information**
- *Your username*: hfang
- *UUID of worksheet where problem occurred*: N/A
- *UUID of problem bundle (please don't delete)*: 0x218c2d796c9847a283817362459eb308
- *Error message received from Codalab (if applicable)*: N/A
 - *OS (if on CLI)*: N/A
 - *Codalab CLI Version (if on CLI)*:  N/A
 - *Browser (if on Web)*; Chrome

**To Reproduce**
Steps to reproduce the behavior :
```
run foo:0xea12e8bbfa9d4e0788dfef6457df7cee/foo foo2:0xea12e8bbfa9d4e0788dfef6457df7cee --- "cat foo2"
```

**Expected behavior**
Both "foo" and "foo2" should be accessible and shown as dependencies.

**Screenshots**
N/A

**Additional context**
This is a sample job to illustrate the problem.
It happens for a complicated worksheet in practice, when sometimes the two dependencies can be different, but sometimes they point to the same bundle spec.


Discussion:
percyliang: Oh, would you mind adding a test to make sure this works?
candicegjing: > Oh, would you mind adding a test to make sure this works?

There is a test suite created for `BundleInfo` object. I just reorganized it and added a bit more test cases to make the comparison clear.

Patch:
diff --git a/codalab/lib/bundle_cli.py b/codalab/lib/bundle_cli.py
index 2eb40021..9527d781 100644
--- a/codalab/lib/bundle_cli.py
+++ b/codalab/lib/bundle_cli.py
@@ -313,7 +313,8 @@ class Commands(object):
                 if markdown:
                     name = HEADING_LEVEL_3 + name
                 return '%s%s:\n%s\n%s' % (
-                    ' ' * indent,
+                    # This is to make GitHub Markdown format compatible with the Read the Docs theme.
+                    ' ' * indent if not markdown else '',
                     name,
                     '\n'.join((' ' * (indent * 2)) + line for line in command_obj.help),
                     '\n'.join(render_args(command_obj.arguments)),
diff --git a/codalab/worker/bundle_state.py b/codalab/worker/bundle_state.py
index c2d91ada..98033cdd 100644
--- a/codalab/worker/bundle_state.py
+++ b/codalab/worker/bundle_state.py
@@ -76,8 +76,8 @@ class BundleInfo(object):
         self.is_anonymous = is_anonymous
         self.metadata = metadata
         self.args = args
-        self.dependencies = {
-            DependencyKey(dep["parent_uuid"], dep["parent_path"]): Dependency(
+        self.dependencies = [
+            Dependency(
                 parent_name=dep["parent_name"],
                 parent_path=dep["parent_path"],
                 parent_uuid=dep["parent_uuid"],
@@ -86,13 +86,14 @@ class BundleInfo(object):
                 location=dep.get("location", None),
             )
             for dep in dependencies
-        }  # type: Dict[DependencyKey, Dependency]
+        ]  # type: List[Dependency]
+
         self.location = location  # set if local filesystem
 
     @property
     def as_dict(self):
         dct = generic_to_dict(self)
-        dct['dependencies'] = [v for k, v in dct['dependencies'].items()]
+        dct['dependencies'] = [generic_to_dict(v) for v in dct['dependencies']]
         return dct
 
     def __str__(self):
diff --git a/codalab/worker/reader.py b/codalab/worker/reader.py
index 11fe4c9d..ec388a94 100644
--- a/codalab/worker/reader.py
+++ b/codalab/worker/reader.py
@@ -60,7 +60,7 @@ class Reader(object):
         Return target_info of path in bundle as a message on the reply_fn
         """
         target_info = None
-        dep_paths = set([dep.child_path for dep in run_state.bundle.dependencies.values()])
+        dep_paths = set([dep.child_path for dep in run_state.bundle.dependencies])
 
         # if path is a dependency raise an error
         if path and os.path.normpath(path) in dep_paths:
@@ -93,7 +93,7 @@ class Reader(object):
         """
         Stream the directory at path using a separate thread
         """
-        dep_paths = set([dep.child_path for dep in run_state.bundle.dependencies.values()])
+        dep_paths = set([dep.child_path for dep in run_state.bundle.dependencies])
         exclude_names = [] if path else dep_paths
 
         def stream_thread(final_path):
diff --git a/codalab/worker/worker_run_state.py b/codalab/worker/worker_run_state.py
index 227b0b81..728da1f9 100644
--- a/codalab/worker/worker_run_state.py
+++ b/codalab/worker/worker_run_state.py
@@ -10,7 +10,7 @@ import codalab.worker.docker_utils as docker_utils
 
 from codalab.lib.formatting import size_str, duration_str
 from codalab.worker.file_util import remove_path, get_path_size
-from codalab.worker.bundle_state import State
+from codalab.worker.bundle_state import State, DependencyKey
 from codalab.worker.fsm import DependencyStage, StateTransitioner
 from codalab.worker.worker_thread import ThreadDict
 
@@ -180,7 +180,8 @@ class RunStateMachine(StateTransitioner):
 
         if not self.shared_file_system:
             # No need to download dependencies if we're in the shared FS since they're already in our FS
-            for dep_key, dep in run_state.bundle.dependencies.items():
+            for dep in run_state.bundle.dependencies:
+                dep_key = DependencyKey(dep.parent_uuid, dep.parent_path)
                 dependency_state = self.dependency_manager.get(run_state.bundle.uuid, dep_key)
                 if dependency_state.stage == DependencyStage.DOWNLOADING:
                     status_messages.append(
@@ -244,7 +245,8 @@ class RunStateMachine(StateTransitioner):
         docker_dependencies_path = (
             '/' + run_state.bundle.uuid + ('_dependencies' if not self.shared_file_system else '')
         )
-        for dep_key, dep in run_state.bundle.dependencies.items():
+        for dep in run_state.bundle.dependencies:
+            dep_key = DependencyKey(dep.parent_uuid, dep.parent_path)
             full_child_path = os.path.normpath(os.path.join(run_state.bundle_path, dep.child_path))
             if not full_child_path.startswith(run_state.bundle_path):
                 # Dependencies should end up in their bundles (ie prevent using relative paths like ..
@@ -443,7 +445,8 @@ class RunStateMachine(StateTransitioner):
                     logger.error(traceback.format_exc())
                     time.sleep(1)
 
-        for dep_key, dep in run_state.bundle.dependencies.items():
+        for dep in run_state.bundle.dependencies:
+            dep_key = DependencyKey(dep.parent_uuid, dep.parent_path)
             if not self.shared_file_system:  # No dependencies if shared fs worker
                 self.dependency_manager.release(run_state.bundle.uuid, dep_key)
 
diff --git a/docs/CLI-Reference.md b/docs/CLI-Reference.md
index d48020b5..f4a2ecdf 100644
--- a/docs/CLI-Reference.md
+++ b/docs/CLI-Reference.md
@@ -5,7 +5,7 @@ This file is auto-generated from the output of `cl help -v -m` and provides the
 Usage: `cl <command> <arguments>`
 
 ## Commands for bundles:
-  ### upload (up):
+### upload (up):
     Create a bundle by uploading an existing file/directory.
       upload <path>            : Upload contents of file/directory <path> as a bundle.
       upload <path> ... <path> : Upload one bundle whose directory contents contain <path> ... <path>.
@@ -29,7 +29,7 @@ Usage: `cl <command> <arguments>`
       --source-url             URL corresponding to the original source of this bundle.
       -e, --edit               Show an editor to allow editing of the bundle metadata.
 
-  ### make:
+### make:
     Create a bundle by combining parts of existing bundles.
       make <bundle>/<subpath>                : New bundle's contents are copied from <subpath> in <bundle>.
       make <key>:<bundle> ... <key>:<bundle> : New bundle contains file/directories <key> ... <key>, whose contents are given.
@@ -42,7 +42,7 @@ Usage: `cl <command> <arguments>`
       --allow-failed-dependencies  Whether to allow this bundle to have failed or killed dependencies.
       -e, --edit                   Show an editor to allow editing of the bundle metadata.
 
-  ### run:
+### run:
     Create a bundle by running a program bundle on an input bundle.
     Arguments:
       target_spec                  [<key>]:[[(<alias>|<address>)::](<uuid>|<name>)//](<uuid>|<name>|^<index>)[/<subpath within bundle>]
@@ -67,7 +67,7 @@ Usage: `cl <command> <arguments>`
       -t, --tail                   Wait until run finishes, displaying stdout/stderr.
       -v, --verbose                Display verbose output.
 
-  ### docker:
+### docker:
     Beta feature. Simulate a run bundle locally, producing bundle contents in the local environment and mounting local dependencies.
     Arguments:
       target_spec                  [<key>]:[[(<alias>|<address>)::](<uuid>|<name>)//](<uuid>|<name>|^<index>)[/<subpath within bundle>]
@@ -91,7 +91,7 @@ Usage: `cl <command> <arguments>`
       -t, --tail                   Wait until run finishes, displaying stdout/stderr.
       -v, --verbose                Display verbose output.
 
-  ### edit (e):
+### edit (e):
     Edit an existing bundle's metadata.
       edit           : Popup an editor.
       edit -n <name> : Edit the name metadata field (same for other fields).
@@ -105,14 +105,14 @@ Usage: `cl <command> <arguments>`
       --not-anonymous       Set bundle to be NOT anonymous.
       -w, --worksheet-spec  Operate on this worksheet ([(<alias>|<address>)::](<uuid>|<name>)).
 
-  ### detach (de):
+### detach (de):
     Detach a bundle from this worksheet, but doesn't remove the bundle.
     Arguments:
       bundle_spec           [[(<alias>|<address>)::](<uuid>|<name>)//](<uuid>|<name>|^<index>)
       -n, --index           Specifies which occurrence (1, 2, ...) of the bundle to detach, counting from the end.
       -w, --worksheet-spec  Operate on this worksheet ([(<alias>|<address>)::](<uuid>|<name>)).
 
-  ### rm:
+### rm:
     Remove a bundle (permanent!).
     Arguments:
       bundle_spec           [[(<alias>|<address>)::](<uuid>|<name>)//](<uuid>|<name>|^<index>)
@@ -122,7 +122,7 @@ Usage: `cl <command> <arguments>`
       -i, --dry-run         Perform a dry run (just show what will be done without doing it).
       -w, --worksheet-spec  Operate on this worksheet ([(<alias>|<address>)::](<uuid>|<name>)).
 
-  ### search (s):
+### search (s):
     Search for bundles on a CodaLab instance (returns 10 results by default).
       search <keyword> ... <keyword>         : Name or uuid contains each <keyword>.
       search name=<value>                    : Name is <value>, where `name` can be any metadata field (e.g., description).
@@ -157,13 +157,13 @@ Usage: `cl <command> <arguments>`
       -u, --uuid-only       Print only uuids.
       -w, --worksheet-spec  Operate on this worksheet ([(<alias>|<address>)::](<uuid>|<name>)).
 
-  ### ls:
+### ls:
     List bundles in a worksheet.
     Arguments:
       -u, --uuid-only       Print only uuids.
       -w, --worksheet-spec  Operate on this worksheet ([(<alias>|<address>)::](<uuid>|<name>)).
 
-  ### info (i):
+### info (i):
     Show detailed information for a bundle.
     Arguments:
       bundle_spec           [[(<alias>|<address>)::](<uuid>|<name>)//](<uuid>|<name>|^<index>)
@@ -172,7 +172,7 @@ Usage: `cl <command> <arguments>`
       -v, --verbose         Print top-level contents of bundle, children bundles, and host worksheets.
       -w, --worksheet-spec  Operate on this worksheet ([(<alias>|<address>)::](<uuid>|<name>)).
 
-  ### cat:
+### cat:
     Print the contents of a file/directory in a bundle.
     Note that cat on a directory will list its files.
     Arguments:
@@ -181,21 +181,21 @@ Usage: `cl <command> <arguments>`
       -t, --tail            Display last NUM lines of contents
       -w, --worksheet-spec  Operate on this worksheet ([(<alias>|<address>)::](<uuid>|<name>)).
 
-  ### wait:
+### wait:
     Wait until a run bundle finishes.
     Arguments:
       target_spec           [[(<alias>|<address>)::](<uuid>|<name>)//](<uuid>|<name>|^<index>)[/<subpath within bundle>]
       -t, --tail            Print out the tail of the file or bundle and block until the run bundle has finished running.
       -w, --worksheet-spec  Operate on this worksheet ([(<alias>|<address>)::](<uuid>|<name>)).
 
-  ### download (down):
+### download (down):
     Download bundle from a CodaLab instance.
     Arguments:
       target_spec           [[(<alias>|<address>)::](<uuid>|<name>)//](<uuid>|<name>|^<index>)[/<subpath within bundle>]
       -o, --output-path     Path to download bundle to.  By default, the bundle or subpath name in the current directory is used.
       -w, --worksheet-spec  Operate on this worksheet ([(<alias>|<address>)::](<uuid>|<name>)).
 
-  ### mimic:
+### mimic:
     Creates a set of bundles based on analogy with another set.
       mimic <run>      : Rerun the <run> bundle.
       mimic A B        : For all run bundles downstream of A, rerun with B instead.
@@ -224,7 +224,7 @@ Usage: `cl <command> <arguments>`
       -t, --tail                   Wait until run finishes, displaying stdout/stderr.
       -v, --verbose                Display verbose output.
 
-  ### macro:
+### macro:
     Use mimicry to simulate macros.
       macro M A B <name1>:C <name2>:D <=> mimic M-in1 M-in2 M-in-name1 M-in-name2 M-out A B C D
     Arguments:
@@ -251,20 +251,20 @@ Usage: `cl <command> <arguments>`
       -t, --tail                   Wait until run finishes, displaying stdout/stderr.
       -v, --verbose                Display verbose output.
 
-  ### kill:
+### kill:
     Instruct the appropriate worker to terminate the running bundle(s).
     Arguments:
       bundle_spec           [[(<alias>|<address>)::](<uuid>|<name>)//](<uuid>|<name>|^<index>)
       -w, --worksheet-spec  Operate on this worksheet ([(<alias>|<address>)::](<uuid>|<name>)).
 
-  ### write:
+### write:
     Instruct the appropriate worker to write a small file into the running bundle(s).
     Arguments:
       target_spec           [[(<alias>|<address>)::](<uuid>|<name>)//](<uuid>|<name>|^<index>)[/<subpath within bundle>]
       string                Write this string to the target file.
       -w, --worksheet-spec  Operate on this worksheet ([(<alias>|<address>)::](<uuid>|<name>)).
 
-  ### mount:
+### mount:
     Beta feature: this command may change in a future release. Mount the contents of a bundle at a read-only mountpoint.
     Arguments:
       target_spec           [[(<alias>|<address>)::](<uuid>|<name>)//](<uuid>|<name>|^<index>)[/<subpath within bundle>]
@@ -272,7 +272,7 @@ Usage: `cl <command> <arguments>`
       --verbose             Verbose mode for BundleFUSE.
       -w, --worksheet-spec  Operate on this worksheet ([(<alias>|<address>)::](<uuid>|<name>)).
 
-  ### netcat:
+### netcat:
     Beta feature: this command may change in a future release. Send raw data into a port of a running bundle
     Arguments:
       bundle_spec           [[(<alias>|<address>)::](<uuid>|<name>)//](<uuid>|<name>|^<index>)
@@ -284,13 +284,13 @@ Usage: `cl <command> <arguments>`
 
 
 ## Commands for worksheets:
-  ### new:
+### new:
     Create a new worksheet.
     Arguments:
       name                  Name of worksheet (^[a-zA-Z_][a-zA-Z0-9_\.\-]*$).
       -w, --worksheet-spec  Operate on this worksheet ([(<alias>|<address>)::](<uuid>|<name>)).
 
-  ### add:
+### add:
     Append text items, bundles, or subworksheets to a worksheet (possibly on a different instance).
     Bundles that do not yet exist on the destination instance will be copied over.
     Arguments:
@@ -302,7 +302,7 @@ Usage: `cl <command> <arguments>`
       --dest-worksheet         Worksheet to which to add items ([(<alias>|<address>)::](<uuid>|<name>)).
       -d, --copy-dependencies  If adding bundles, also add dependencies of the bundles.
 
-  ### wadd:
+### wadd:
     Append all the items of the source worksheet to the destination worksheet.
     Bundles that do not yet exist on the destination service will be copied over.
     The existing items on the destination worksheet are not affected unless the -r/--replace flag is set.
@@ -311,7 +311,7 @@ Usage: `cl <command> <arguments>`
       dest_worksheet_spec    [(<alias>|<address>)::](<uuid>|<name>)
       -r, --replace          Replace everything on the destination worksheet with the items from the source worksheet, instead of appending (does not delete old bundles, just detaches).
 
-  ### work (w):
+### work (w):
     Set the current instance/worksheet.
       work <worksheet>          : Switch to the given worksheet on the current instance.
       work <alias>::            : Switch to the home worksheet on instance <alias>.
@@ -320,13 +320,13 @@ Usage: `cl <command> <arguments>`
       -u, --uuid-only  Print only the worksheet uuid.
       worksheet_spec   [(<alias>|<address>)::](<uuid>|<name>)
 
-  ### print (p):
+### print (p):
     Print the rendered contents of a worksheet.
     Arguments:
       worksheet_spec  [(<alias>|<address>)::](<uuid>|<name>)
       -r, --raw       Print out the raw contents (for editing).
 
-  ### wedit (we):
+### wedit (we):
     Edit the contents of a worksheet.
     See https://codalab-worksheets.readthedocs.io/en/latest/User_Worksheet-Markdown for the markdown syntax.
       wedit -n <name>          : Change the name of the worksheet.
@@ -343,14 +343,14 @@ Usage: `cl <command> <arguments>`
       --not-anonymous   Set bundle to be NOT anonymous.
       -f, --file        Replace the contents of the current worksheet with this file.
 
-  ### wrm:
+### wrm:
     Delete a worksheet.
     To be safe, you can only delete a worksheet if it has no items and is not frozen.
     Arguments:
       worksheet_spec  [(<alias>|<address>)::](<uuid>|<name>)
       --force         Delete worksheet even if it is non-empty and frozen.
 
-  ### wls (wsearch, ws):
+### wls (wsearch, ws):
     List worksheets on the current instance matching the given keywords (returns 10 results by default).
       wls tag=paper           : List worksheets tagged as "paper".
       wls group=<group_spec>  : List worksheets shared with the group identfied by group_spec.
@@ -364,40 +364,40 @@ Usage: `cl <command> <arguments>`
 
 
 ## Commands for groups and permissions:
-  ### gls:
+### gls:
     Show groups to which you belong.
     Arguments:
       -w, --worksheet-spec  Operate on this worksheet ([(<alias>|<address>)::](<uuid>|<name>)).
 
-  ### gnew:
+### gnew:
     Create a new group.
     Arguments:
       name  Name of new group (^[a-zA-Z_][a-zA-Z0-9_\.\-]*$).
 
-  ### grm:
+### grm:
     Delete a group.
     Arguments:
       group_spec  Group to delete ((<uuid>|<name>|public)).
 
-  ### ginfo:
+### ginfo:
     Show detailed information for a group.
     Arguments:
       group_spec  Group to show information about ((<uuid>|<name>|public)).
 
-  ### uadd:
+### uadd:
     Add a user to a group.
     Arguments:
       user_spec    Username to add.
       group_spec   Group to add user to ((<uuid>|<name>|public)).
       -a, --admin  Give admin privileges to the user for the group.
 
-  ### urm:
+### urm:
     Remove a user from a group.
     Arguments:
       user_spec   Username to remove.
       group_spec  Group to remove user from ((<uuid>|<name>|public)).
 
-  ### perm:
+### perm:
     Set a group's permissions for a bundle.
     Arguments:
       bundle_spec           [[(<alias>|<address>)::](<uuid>|<name>)//](<uuid>|<name>|^<index>)
@@ -405,14 +405,14 @@ Usage: `cl <command> <arguments>`
       permission_spec       ((n)one|(r)ead|(a)ll)
       -w, --worksheet-spec  Operate on this worksheet ([(<alias>|<address>)::](<uuid>|<name>)).
 
-  ### wperm:
+### wperm:
     Set a group's permissions for a worksheet.
     Arguments:
       worksheet_spec   [(<alias>|<address>)::](<uuid>|<name>)
       group_spec       (<uuid>|<name>|public)
       permission_spec  ((n)one|(r)ead|(a)ll)
 
-  ### chown:
+### chown:
     Set the owner of bundles.
     Arguments:
       user_spec             Username to set as the owner.
@@ -421,13 +421,13 @@ Usage: `cl <command> <arguments>`
 
 
 ## Commands for users:
-  ### uinfo:
+### uinfo:
     Show user information.
     Arguments:
       user_spec    Username or id of user to show [default: the authenticated user]
       -f, --field  Print out these comma-separated fields.
 
-  ### uedit:
+### uedit:
     Edit user information.
     Note that password and email can only be changed through the web interface.
     Arguments:
@@ -440,7 +440,7 @@ Usage: `cl <command> <arguments>`
       -p, --parallel-run-quota  Total amount of runs the user may have running at a time on shared public workers
       -d, --disk-quota          Total amount of disk allowed (e.g., 3, 3k, 3m, 3g, 3t)
 
-  ### ufarewell:
+### ufarewell:
     Delete user permanently. Root user only.
     To be safe, you can only delete a user if user does not own any bundles, worksheets, or groups.
     Arguments:
@@ -448,24 +448,24 @@ Usage: `cl <command> <arguments>`
 
 
 ## Commands for managing server:
-  ### workers:
-    Display information about workers that you have connected to the CodaLab instance. For the root user, display all workers.
+### workers:
+    Display information about workers that you have connected to the CodaLab instance.
 
-  ### bs-add-partition:
+### bs-add-partition:
     Add another partition for storage (MultiDiskBundleStore only)
     Arguments:
       name  The name you'd like to give this partition for CodaLab.
       path  The target location you would like to use for storing bundles. This directory should be underneath a mountpoint for the partition you would like to use. You are responsible for configuring the mountpoint yourself.
 
-  ### bs-rm-partition:
+### bs-rm-partition:
     Remove a partition by its number (MultiDiskBundleStore only)
     Arguments:
       partition  The partition you want to remove.
 
-  ### bs-ls-partitions:
+### bs-ls-partitions:
     List available partitions (MultiDiskBundleStore only)
 
-  ### bs-health-check:
+### bs-health-check:
     Perform a health check on the bundle store, garbage collecting bad files in the store. Performs a dry run by default, use -f to force removal.
     Arguments:
       -f, --force      Perform all garbage collection and database updates instead of just printing what would happen
@@ -474,7 +474,7 @@ Usage: `cl <command> <arguments>`
 
 
 ## Other commands:
-  ### help:
+### help:
     Show usage information for commands.
       help           : Show brief description for all commands.
       help -v        : Show full usage information for all commands.
@@ -485,10 +485,10 @@ Usage: `cl <command> <arguments>`
       -v, --verbose   Display all options of all commands.
       -m, --markdown  Auto-generate all options of all commands for CLI markdown in Markdown format.
 
-  ### status (st):
+### status (st):
     Show current client status.
 
-  ### alias:
+### alias:
     Manage CodaLab instance aliases. These are mappings from names to CodaLab Worksheet servers.
       alias                   : List all aliases.
       alias <name>            : Shows which instance <name> is bound to.
@@ -498,7 +498,7 @@ Usage: `cl <command> <arguments>`
       instance      Instance to bind the alias to (e.g., https://worksheets.codalab.org).
       -r, --remove  Remove this alias.
 
-  ### config:
+### config:
     Set CodaLab configuration.
       config <key>         : Shows the value of <key>.
       config <key> <value> : Sets <key> to <value>.
@@ -507,7 +507,7 @@ Usage: `cl <command> <arguments>`
       value         Instance to bind the alias to (e.g., https://worksheets.codalab.org).
       -r, --remove  Remove this key.
 
-  ### logout:
+### logout:
     Logout of the current session, or a specific instance.
     Arguments:
       alias  Alias or URL of instance from which to logout. Default is the current session.


Test patch:
diff --git a/test_cli.py b/test_cli.py
index 8adfcfec..a469cbde 100644
--- a/test_cli.py
+++ b/test_cli.py
@@ -765,12 +765,10 @@ def test(ctx):
     # make
     uuid3 = _run_command([cl, 'make', 'dep1:' + uuid1, 'dep2:' + uuid2])
     wait(uuid3)
-    check_equals('ready', _run_command([cl, 'info', '-f', 'state', uuid3]))
     check_contains(['dep1', uuid1, 'dep2', uuid2], _run_command([cl, 'info', uuid3]))
     # anonymous make
     uuid4 = _run_command([cl, 'make', uuid3, '--name', 'foo'])
     wait(uuid4)
-    check_equals('ready', _run_command([cl, 'info', '-f', 'state', uuid4]))
     check_contains([uuid3], _run_command([cl, 'info', uuid3]))
     # Cleanup
     _run_command([cl, 'rm', uuid1], 1)  # should fail
@@ -981,6 +979,7 @@ def test(ctx):
     name = random_name()
     uuid = _run_command([cl, 'run', 'echo hello', '-n', name])
     wait(uuid)
+    '''
     # test search
     check_contains(name, _run_command([cl, 'search', name]))
     check_equals(uuid, _run_command([cl, 'search', name, '-u']))
@@ -1029,7 +1028,6 @@ def test(ctx):
     wait(remote_uuid)
     check_contains(remote_name, _run_command([cl, 'search', remote_name]))
     check_equals(remote_uuid, _run_command([cl, 'search', remote_name, '-u']))
-    check_equals('ready', _run_command([cl, 'info', '-f', 'state', remote_uuid]))
     check_equals('hello', _run_command([cl, 'cat', remote_uuid + '/stdout']))
 
     sugared_remote_name = random_name()
@@ -1045,13 +1043,30 @@ def test(ctx):
     wait(sugared_remote_uuid)
     check_contains(sugared_remote_name, _run_command([cl, 'search', sugared_remote_name]))
     check_equals(sugared_remote_uuid, _run_command([cl, 'search', sugared_remote_name, '-u']))
-    check_equals('ready', _run_command([cl, 'info', '-f', 'state', sugared_remote_uuid]))
     check_equals('hello', _run_command([cl, 'cat', sugared_remote_uuid + '/stdout']))
 
     # Explicitly fail when a remote instance name with : in it is supplied
     _run_command(
         [cl, 'run', 'cat %%%s//%s%%/stdout' % (source_worksheet_full, name)], expected_exit_code=1
     )
+    '''
+
+    # Test multiple keys pointing to the same bundle
+    multi_alias_uuid = _run_command(
+        [
+            cl,
+            'run',
+            'foo:{}'.format(uuid),
+            'foo1:{}'.format(uuid),
+            'foo2:{}'.format(uuid),
+            'echo "three aliases"',
+        ]
+    )
+    wait(multi_alias_uuid)
+    check_equals('three aliases', _run_command([cl, 'cat', multi_alias_uuid + '/stdout']))
+    check_equals('hello', _run_command([cl, 'cat', multi_alias_uuid + '/foo/stdout']))
+    check_equals('hello', _run_command([cl, 'cat', multi_alias_uuid + '/foo1/stdout']))
+    check_equals('hello', _run_command([cl, 'cat', multi_alias_uuid + '/foo2/stdout']))
 
 
 @TestModule.register('read')
diff --git a/tests/worker/bundle_state_test.py b/tests/worker/bundle_state_test.py
index fa32cbf8..7f5a34a2 100644
--- a/tests/worker/bundle_state_test.py
+++ b/tests/worker/bundle_state_test.py
@@ -1,11 +1,11 @@
 import unittest
 
-from codalab.worker.bundle_state import State, BundleInfo
+from codalab.worker.bundle_state import State, BundleInfo, Dependency
 
 
 class BundleStateTest(unittest.TestCase):
-    def test_bundle_info_serialization(self):
-        bundle_info_fields = {
+    def setUp(self) -> None:
+        self.bundle_info_fields = {
             'uuid': '0xuuid',
             'bundle_type': 'bundle_type',
             'owner_id': 'owner_id',
@@ -33,17 +33,65 @@ class BundleStateTest(unittest.TestCase):
             ],
             'location': 'location',
         }
+
+    def tearDown(self) -> None:
+        del self.bundle_info_fields
+
+    def test_bundle_info_serialization(self):
         info = BundleInfo(
-            uuid=bundle_info_fields['uuid'],
-            bundle_type=bundle_info_fields['bundle_type'],
-            owner_id=bundle_info_fields['owner_id'],
-            command=bundle_info_fields['command'],
-            data_hash=bundle_info_fields['data_hash'],
-            state=bundle_info_fields['state'],
-            is_anonymous=bundle_info_fields['is_anonymous'],
-            metadata=bundle_info_fields['metadata'],
-            args=bundle_info_fields['args'],
-            dependencies=bundle_info_fields['dependencies'],
-            location=bundle_info_fields['location'],
+            uuid=self.bundle_info_fields['uuid'],
+            bundle_type=self.bundle_info_fields['bundle_type'],
+            owner_id=self.bundle_info_fields['owner_id'],
+            command=self.bundle_info_fields['command'],
+            data_hash=self.bundle_info_fields['data_hash'],
+            state=self.bundle_info_fields['state'],
+            is_anonymous=self.bundle_info_fields['is_anonymous'],
+            metadata=self.bundle_info_fields['metadata'],
+            args=self.bundle_info_fields['args'],
+            dependencies=self.bundle_info_fields['dependencies'],
+            location=self.bundle_info_fields['location'],
+        )
+
+        self.assertEqual(
+            info.dependencies,
+            [
+                Dependency(
+                    parent_name='parent_name_0',
+                    parent_path='parent_path_0',
+                    parent_uuid='parent_uuid_0',
+                    child_path='child_path_0',
+                    child_uuid='child_uuid_0',
+                    location=None,
+                ),
+                Dependency(
+                    parent_name='parent_name_1',
+                    parent_path='parent_path_1',
+                    parent_uuid='parent_uuid_1',
+                    child_path='child_path_1',
+                    child_uuid='child_uuid_1',
+                    location=None,
+                ),
+            ],
+        )
+        self.assertEqual(
+            info.as_dict['dependencies'],
+            [
+                {
+                    'parent_name': 'parent_name_0',
+                    'parent_path': 'parent_path_0',
+                    'parent_uuid': 'parent_uuid_0',
+                    'child_path': 'child_path_0',
+                    'child_uuid': 'child_uuid_0',
+                    'location': None,
+                },
+                {
+                    'parent_name': 'parent_name_1',
+                    'parent_path': 'parent_path_1',
+                    'parent_uuid': 'parent_uuid_1',
+                    'child_path': 'child_path_1',
+                    'child_uuid': 'child_uuid_1',
+                    'location': None,
+                },
+            ],
         )
         self.assertEqual(info.as_dict, BundleInfo.from_dict(info.as_dict).as_dict)