diff --git a/apps/cli/utilities/s_code_project_updater/setup.py b/apps/cli/utilities/s_code_project_updater/setup.py
index fc1c9640874e882157a5d9a5cd6a420c5d92a0e0..2427961859e8a06eaee55fb0771025e1f8cdaecb 100644
--- a/apps/cli/utilities/s_code_project_updater/setup.py
+++ b/apps/cli/utilities/s_code_project_updater/setup.py
@@ -17,6 +17,7 @@ setup(
     url='TBD',
     license="GPL",
     install_requires=['pycapo', 'pymygdala', 'schema', 'sqlalchemy', 'support'],
+    tests_require=['pytest-mock'],
     requires=['sqlalchemy', 'mysqldb'],
     keywords=[],
     packages=['s_code_project_updater'],
diff --git a/apps/cli/utilities/s_code_project_updater/src/s_code_project_updater/commands.py b/apps/cli/utilities/s_code_project_updater/src/s_code_project_updater/commands.py
index e726f4031e8c8225a112a33fea5aec19a43c5412..901eaad404031424e9c3d63eacb83e46d8ae6cc3 100644
--- a/apps/cli/utilities/s_code_project_updater/src/s_code_project_updater/commands.py
+++ b/apps/cli/utilities/s_code_project_updater/src/s_code_project_updater/commands.py
@@ -47,7 +47,26 @@ class ScodeProjectUpdater:
         :param kwargs: the command line arguments or namespace with the arguments to the parser
         """
         self._make_parser()
-        self.args = self.parser.parse_args(**kwargs)
+        try:
+            _LOG.warning('parsing....')
+            self.args = self.parser.parse_args(**kwargs)
+            _LOG.warning('parsed')
+
+        except Exception as exc:
+            _LOG.error(f'parser threw {exc}')
+            self.exit_with_error('Capo profile and project code are '
+                                 'required', 2)
+
+        # at a minimum, Capo profile and project code are required
+        if not self.args.profile or not self.args.project:
+            if not self.args.profile and not self.args.project:
+                self.exit_with_error('Capo profile and project code are '
+                                     'required', 2)
+            if not self.args.profile:
+                self.exit_with_error('Capo profile not specified', 2)
+            if not self.args.project:
+                self.exit_with_error('project code not specified', 2)
+
         args_dict = self.args.__dict__
 
         if args_dict['dry']:
@@ -57,13 +76,16 @@ class ScodeProjectUpdater:
 
         if not args_dict['investigators'] and not args_dict['title'] and not args_dict['abstract']:
             self.set_minimum_properties_from_args(args_dict)
-            return
+            # return
 
         self.project_code = args_dict['project']
         self.stored_project = None
         _LOG.debug(f'{self.args}')
 
-        self.capo_config = get_my_capo_config(profile=self.args.profile)
+        try:
+            self.capo_config = get_my_capo_config(profile=self.args.profile)
+        except Exception as exc:
+            self.exit_with_error(f'Capo configuration error: {exc}', 1)
         try:
             self.archive_context = ArchiveDBSession('SDM', profile=self.capo_config.profile)
             self.pst_context = ArchiveDBSession('PST', profile=self.capo_config.profile)
@@ -243,7 +265,6 @@ class ScodeProjectUpdater:
         # we want the PI's pst_person_id followed by the CoIs' pst_person_ids in numeric order
         pi = investigator_list[0]
         if pi.pst_person_id is not None:
-            self.is_alma = False
             coi_pst_ids = [int(coi.pst_person_id) for coi in investigator_list[1:]]
             coi_pst_ids = sorted(coi_pst_ids)
             author_pst_ids = [int(pi.pst_person_id)]
@@ -251,8 +272,6 @@ class ScodeProjectUpdater:
             authors_to_print = [str(id) for id in author_pst_ids]
             id_list = ' '.join(authors_to_print)
             output.append(f'Authors: {id_list}')
-        else:
-            self.is_alma = True
 
         return output
 
@@ -269,7 +288,11 @@ class ScodeProjectUpdater:
         :return:
         '''
         fetcher = ArchiveProjectFetcher(self.args.profile)
-        self.project = fetcher.fetch_project(self.project_code)
+        try:
+            self.project = fetcher.fetch_project(self.project_code)
+        except AttributeError:
+            self.exit_with_error(f'project code "{self.project_code}" not '
+                                 f'found', 3)
         if self.is_fetch_only():
             output = fetcher.build_project_info()
             try:
@@ -296,8 +319,8 @@ class ScodeProjectUpdater:
                 self.exit_with_error('No project found for the project_code provided', 3)
 
             if self.is_alma():
-                raise ValueError(f'{self.stored_project.project_code} '
-                                 f'is an ALMA project; update not permitted')
+                self.exit_with_error(f'{self.stored_project.project_code} '
+                                 f'is an ALMA project; update not permitted', 2)
 
             if self.args.investigators:
                 proposed_investigators = self.get_pst_users(self.args.investigators)
@@ -445,6 +468,40 @@ class ArchiveProject:
 
         return new_project.investigators
 
+
+class UpdateArgParser:
+    ''' Command-line argument parser for ScodeProjectUpdater '''
+    def __init__(self):
+        self.parser = self._make_parser()
+
+    def _make_parser(self):
+        parser = ap.ArgumentParser(description=_DESCRIPTION.format(version),
+                                   formatter_class=ap.RawTextHelpFormatter,
+                                   epilog=_EPILOG)
+        parser.add_argument('-C', '--project', action='store',
+                            help='project_code to update')
+        parser.add_argument('-P', '--profile', action='store',
+                            help='profile name to use, e.g. test, production')
+        parser.add_argument('-T', '--title', action='store',
+                            help='a quoted string for the new title for the project')
+        parser.add_argument('-A', '--abstract', action='store',
+                            help='a quoted string for the new abstract for the project')
+        parser.add_argument('-I', '--investigators', action='store', type=int, nargs='+',
+                            help='a PST ID, or list of PST IDs, of investigators for the project, '
+                                 'as an unquoted integer or space seperated integer list.  The '
+                                 'first ID in the list will be added as the PI and all subsequenct '
+                                 'IDs will be added as CoIs.')
+        parser.add_argument('-d', '--dry', action='store_true',
+                            help='perform a dry run, going through the motions, but not committing '
+                                 'changes and not performing a re-index of the project.  This may '
+                                 'be useful because it will print the current state of the project '
+                                 'and what the project would look like after the changes.')
+        return parser
+
+    def parse_args(self, **kwargs):
+        return self.parser.parse_args(kwargs)
+
+
 class UpdateException(Exception):
     ''' throw this if there is trouble during the update '''
     pass
diff --git a/apps/cli/utilities/s_code_project_updater/src/s_code_project_updater/project_fetcher.py b/apps/cli/utilities/s_code_project_updater/src/s_code_project_updater/project_fetcher.py
index 1c321e67d96333c698dc98471043749a5bc48ae7..639c0c3040f2febc42f49316462262b70113f318 100644
--- a/apps/cli/utilities/s_code_project_updater/src/s_code_project_updater/project_fetcher.py
+++ b/apps/cli/utilities/s_code_project_updater/src/s_code_project_updater/project_fetcher.py
@@ -31,7 +31,7 @@ class ArchiveProjectFetcher:
 
     def __exit__(self, exc_type, exc_val, exc_tb):
         self.archive_context.close()
-        self.pst_context.close()
+        self.pst_context.session.close()
 
     def fetch_project(self, project_code: str):
 
@@ -101,7 +101,7 @@ class ArchiveProjectFetcher:
 
     def _is_alma(self):
         ''' is this an alma project? '''
-        with warnings.catch_warnings(), self.archive_context, self.pst_context:
+        with warnings.catch_warnings():
             # Suppress SQLAlchemy warnings
             warnings.simplefilter("ignore", category=sa_exc.SAWarning)
 
diff --git a/apps/cli/utilities/s_code_project_updater/test/test_updater.py b/apps/cli/utilities/s_code_project_updater/test/test_updater.py
old mode 100644
new mode 100755
index e0137834b52f273ed0ab0b9df1cf4ccc3b4b6909..aefa05cb64fb3d9802b4c4984e659d875c485bfe
--- a/apps/cli/utilities/s_code_project_updater/test/test_updater.py
+++ b/apps/cli/utilities/s_code_project_updater/test/test_updater.py
@@ -5,7 +5,7 @@ import unittest
 import warnings
 
 import pytest
-from s_code_project_updater.commands import UpdateException
+from s_code_project_updater.commands import UpdateException, ScodeProjectUpdater
 from schema import create_session
 from schema.model import Project
 from schema.pstmodel import Session
@@ -19,7 +19,6 @@ _LOG = get_console_logger("scode_project_updater_tests", logging.DEBUG)
 _UPDATE_COMMAND = 'update_sproj'
 PROFILE = 'local'
 
-
 class UpdaterTestCase(unittest.TestCase):
 
     @classmethod
@@ -27,8 +26,13 @@ class UpdaterTestCase(unittest.TestCase):
         os.environ['CAPO_PROFILE'] = PROFILE
         cls.return_values = build_updater_return_values()
 
-    def setUp(self) -> None:
-        self.initialize_test_data()
+    @classmethod
+    def setUp(cls) -> None:
+        cls.initialize_test_data(cls)
+
+    @classmethod
+    def tearDownClass(cls) -> None:
+        cls.remove_test_data(cls)
 
     def test_dry_run_does_not_update(self):
         fake_project = ScodeTestProject().project
@@ -37,7 +41,9 @@ class UpdaterTestCase(unittest.TestCase):
         return_code = None
         try:
             new_title = 'this is the new title'
-            self.assertNotEqual(fake_project.title, new_title)
+            self.assertNotEqual(fake_project.title, new_title,
+                                f'new title should be {new_title}; got '
+                                f'{fake_project.title}')
             args = [
                 '-C', project_code,
                 '-P', PROFILE,
@@ -53,10 +59,19 @@ class UpdaterTestCase(unittest.TestCase):
             if not return_code:
                 updated = self.get_project_from_db(session, project_code)
                 # nothing should have been updated
-                self.assertEqual(fake_project.title, updated.title)
-                self.assertEqual(fake_project.abstract, updated.abstract)
+                self.assertEqual(fake_project.title, updated.title,
+                                 f'expecting same title, but before is '
+                                 f'{fake_project.title} and after is {updated.title}')
+                self.assertEqual(fake_project.abstract, updated.abstract,
+                                 f'expecting same abstract, but before is '
+                                 f'{fake_project.abstract} and updated is {updated.abstract}')
                 self.assertEqual(len(fake_project.authors),
-                                     len(updated.authors))
+                                     len(updated.authors),
+                                 f'expecting same number of authors, '
+                                 f'but before has {len(fake_project.authors)} '
+                                 f'and after has {len(updated.authors)}')
+            else:
+                pytest.fail(f'unexpected failure; return code ={return_code}')
 
         except Exception as exc:
             pytest.fail(f'{project_code}: {exc}')
@@ -77,17 +92,26 @@ class UpdaterTestCase(unittest.TestCase):
                 session = create_session('SDM')
                 try:
                     updated = self.get_project_from_db(session, project_code)
-                    self.assertEqual(fake_project.title, updated.title)
-                    self.assertEqual(fake_project.abstract, updated.abstract)
+                    self.assertEqual(fake_project.title, updated.title,
+                                     f'expecting same title, but before is '
+                                     f'{fake_project.title} and after is {updated.title}')
+                    self.assertEqual(fake_project.abstract, updated.abstract,
+                                     f'expecting same abstract, but before is '
+                                     f'{fake_project.abstract} and updated is {updated.abstract}')
                     self.assertEqual(len(fake_project.authors),
-                                     len(updated.authors))
+                                     len(updated.authors),
+                                     f'expecting same number of authors, '
+                                     f'but before has {len(fake_project.authors)} '
+                                     f'and after has {len(updated.authors)}')
                     count = 0
                     for orig_author in fake_project.authors:
                         for author in updated.authors:
                             if author.username == orig_author.username:
                                 count += 1
                                 break
-                    self.assertEqual(len(fake_project.authors), count)
+                    self.assertEqual(len(fake_project.authors), count,
+                                     f'before and after projects should have '
+                                     f'same authors')
                 finally:
                     session.close()
         except Exception as exc:
@@ -99,7 +123,8 @@ class UpdaterTestCase(unittest.TestCase):
         project_code = fake_project.project_code
         session = create_session('SDM')
         new_abstract = "Well, here's another nice mess you've gotten us into, Ollie"
-        self.assertNotEqual(fake_project.abstract, new_abstract)
+        self.assertNotEqual(fake_project.abstract, new_abstract,
+                            f'expecting new abstract {new_abstract} but got {fake_project.abstract}')
         args = [
             '-C', project_code,
             '-P', PROFILE,
@@ -119,8 +144,12 @@ class UpdaterTestCase(unittest.TestCase):
                 updated = self.get_project_from_db(session, project_code)
                 # only abstract should have been updated;
                 # all else should be same
-                self.assertEqual(fake_project.title, updated.title)
-                self.assertEqual(new_abstract, updated.abstract)
+                self.assertEqual(fake_project.title, updated.title,
+                                 f'expecting same title, but before is '
+                                 f'{fake_project.title} and after is {updated.title}')
+                self.assertEqual(new_abstract, updated.abstract,
+                                 f'expecting same abstract, but before is '
+                                 f'{fake_project.abstract} and updated is {updated.abstract}')
                 self.assertEqual(len(fake_project.authors),
                                  len(updated.authors))
             else:
@@ -137,8 +166,12 @@ class UpdaterTestCase(unittest.TestCase):
         session = create_session('SDM')
         new_abstract = "I think you ought to know I'm feeling very depressed"
         new_title = 'A Survey of the Mattresses of Sqornshellous Zeta'
-        self.assertNotEqual(fake_project.abstract, new_abstract)
-        self.assertNotEqual(fake_project.title, new_title)
+        self.assertNotEqual(fake_project.abstract, new_abstract,
+                            f'expecting new abstract {new_abstract}, '
+                            f'but abstract was not changed from {fake_project.abstract}')
+        self.assertNotEqual(fake_project.title, new_title,
+                            f'expecting new title {new_title}, '
+                            f'but abstract was not changed from {fake_project.title}')
         args = [
             '-C', project_code,
             '-P', PROFILE,
@@ -159,10 +192,13 @@ class UpdaterTestCase(unittest.TestCase):
                 updated = self.get_project_from_db(session, project_code)
                 # abstract and title should have been updated;
                 # all else should be same
-                self.assertEqual(new_title, updated.title)
-                self.assertEqual(new_abstract, updated.abstract)
+                self.assertEqual(new_title, updated.title,
+                                 'title should  not have changed')
+                self.assertEqual(new_abstract, updated.abstract,
+                                 'abstract should not have changed')
                 self.assertEqual(len(fake_project.authors),
-                                 len(updated.authors))
+                                 len(updated.authors),
+                                 'authors should not have changed')
             else:
                 raise UpdateException()
 
@@ -173,15 +209,22 @@ class UpdaterTestCase(unittest.TestCase):
 
     def test_adds_new_abstract_deletes_author(self):
         fake_project = ScodeTestProject().project
-        new_project = fake_project
         project_code = fake_project.project_code
+        new_project = Project(project_code=project_code,
+                              title=fake_project.title,
+                              abstract=fake_project.abstract)
         new_abstract = "First there is a mountain, then there is no " \
                         "mountain, then there is"
         self.assertNotEqual(new_abstract, fake_project.abstract)
         new_project.abstract = new_abstract
         original_authors = fake_project.authors.copy()
+        self.assertEqual(4, len(original_authors),
+                         'expected 4 authors before update')
+        last_author = original_authors[3]
         new_authors = original_authors[:3]
-        self.assertEqual(len(original_authors) - 1, len(new_authors))
+        self.assertEqual(len(original_authors) - 1, len(new_authors),
+                         f'expecting {len(original_authors) - 1} new authors, '
+                         f'but there are {len(new_authors)}')
         new_project.authors = new_authors
         args = [
             '-C', project_code,
@@ -203,23 +246,31 @@ class UpdaterTestCase(unittest.TestCase):
                 text = self.return_values[return_code] if return_code else ''
                 pytest.fail(f'{exc} {text}')
 
-            if not return_code:
-                updated = self.get_project_from_db(session, project_code)
-                # last author should have been removed and the abstract changed;
-                # title should remain same
-                self.assertEqual(new_abstract, updated.abstract)
-                self.assertEqual(fake_project.title, updated.title)
-                self.assertEqual(len(new_authors), len(updated.authors))
-                self.assertFalse(original_authors[3] in updated.authors)
-                count = 0
-                for orig_author in original_authors[:3]:
-                    for new_author in updated.authors:
-                        if new_author.username == orig_author.username:
-                            count += 1
-                            break
-                self.assertEqual(len(new_authors), count)
-            else:
-                raise UpdateException()
+            self.assertEqual(0, return_code, f'command should have succeeded '
+                                             f'but return code was {return_code}')
+
+            updated = self.get_project_from_db(session, project_code)
+            # last author should have been removed and the abstract changed;
+            # title should remain same
+            self.assertNotEqual(fake_project.abstract, updated.abstract,
+                                'abstract should have changed')
+            self.assertEqual(fake_project.title, updated.title,
+                             'title should not have changed')
+            expected = len(original_authors) - 1
+            actual = len(updated.authors)
+            self.assertEqual(expected, actual,
+                             'one author should have been removed')
+            authors_updated = last_author in updated.authors
+            self.assertFalse(authors_updated, 'THIS IS THE MESSAGE')
+            count = 0
+            for orig_author in original_authors[:3]:
+                for new_author in updated.authors:
+                    if new_author.username == orig_author.username:
+                        count += 1
+                        break
+            self.assertEqual(len(new_authors), count,
+                             f'expected {len(new_authors)} authors in '
+                             f'updated project; there were {count}')
 
         except Exception as exc:
             pytest.fail(f'{project_code}: {exc}')
@@ -227,11 +278,115 @@ class UpdaterTestCase(unittest.TestCase):
         finally:
             session.close()
 
+    def test_output_is_as_expected(self):
+        fake_project = ScodeTestProject().project
+        project_code = fake_project.project_code
+        args = [
+            '-C', project_code,
+            '-P', PROFILE,
+        ]
+
+        runner = CommandLineUpdaterLauncher(args)
+        return_code = runner.run()
+        if return_code:
+            text = self.return_values[return_code]
+            pytest.fail(text)
+
+        stdout = runner.stdout
+        self.assertIsNotNone(stdout, 'program output is expected')
+        self.assertTrue('Title: ' + fake_project.title in stdout,
+                        'title should be in output')
+        self.assertTrue('Abstract: ' + fake_project.abstract in stdout,
+                        'abstract should be in output')
+        pst_ids = [str(id) for id in get_author_pst_ids(fake_project)]
+        pst_id_str = ' '.join(pst_ids)
+        self.assertTrue('Authors: ' + pst_id_str in stdout,
+                        f'output should have PST IDs {pst_ids}')
+
+    def test_copes_with_single_pi(self):
+        project = ScodeTestProject().project
+        args = ['-P', PROFILE, '-C', project.project_code, '-I', '4686']
+        return_code = CommandLineUpdaterLauncher(args=args).run()
+        self.assertEqual(0, return_code,
+                         'update to single author should succeed')
+
+    def test_alma_project_is_rejected(self):
+        project_code = '2018.A.00062.S'
+        args = ['-P', PROFILE, '-C', project_code,
+                '-T', 'Physics at High Angular Resolution in Nearby Galaxies: '
+                      'The Local Galaxy Inventory Continued']
+
+        with pytest.raises(SystemExit) as exc:
+            ScodeProjectUpdater(args=args).update_project()
+            self.assertEqual(2, exc.code, 'ALMA project should be rejected')
+
+    def test_errors_return_expected_codes(self):
+        # minimum required arguments -- profile & project -- omitted
+        return_code = CommandLineUpdaterLauncher([]).run()
+        self.assertEqual(return_code, 2,
+                         'expected return code 2 for no args')
+
+        project_code = ScodeTestProject().project.project_code
+
+        # update failure
+        result = FailingUpdater().update_project()
+        self.assertIsInstance(result, SystemExit)
+        self.assertEqual(5, result.code,
+                         'expecting return code 5 for update failure')
+
+        # profile not specified
+        args = ['-C', project_code,]
+        return_code = CommandLineUpdaterLauncher(args).run()
+        self.assertEqual(return_code, 2,
+                         'expecting return code 2 when profile not specified')
+
+        # project code not specified
+        args = ['-P', PROFILE]
+        self.assertEqual(CommandLineUpdaterLauncher(args).run(), 2,
+                         'expecting return code 2 when project not specified')
+
+        # profile value missing
+        args = ['-P', '-C', project_code]
+        return_code = CommandLineUpdaterLauncher(args).run()
+        self.assertEqual(return_code, 2,
+                         'expecting return code 2 for missing profile')
+
+        # project code missing
+        args = ['-P', PROFILE, '-C']
+        self.assertEqual(CommandLineUpdaterLauncher(args).run(), 2,
+                         'expecting return code 2 for missing project code')
+
+        # bad project code
+        args = ['-P', PROFILE, '-C', 'bogus']
+        self.assertEqual(CommandLineUpdaterLauncher(args).run(), 3,
+                         'expecting return code 3 for invalid project code')
+
+        # bad profile
+        args = ['-P', 'not_a_profile', '-C', project_code]
+        self.assertEqual(CommandLineUpdaterLauncher(args).run(), 1,
+                         'expecting return code 1 for invalid Capo profile')
+
+        # missing title as last argument
+        args = ['-P', PROFILE, '-C', project_code, '-T']
+        self.assertEqual(CommandLineUpdaterLauncher(args).run(), 2,
+                         'expecting return code 2 for missing title')
+
+        # missing title as first argument
+        args = [ '-T', '-P', PROFILE, '-C', project_code,]
+        self.assertEqual(CommandLineUpdaterLauncher(args).run(), 2,
+                         'expecting return code 2 for missing title')
+
+        # nonexistent investigator
+        args = ['-P', PROFILE, '-C', project_code, '-I', '-22']
+        self.assertEqual(CommandLineUpdaterLauncher(args).run(), 4,
+                         'expecting return code 4 for invalid investigator')
+
 
     ### UTILITIES ###
 
     def initialize_test_data(self):
         session = create_session('SDM')
+        num_commits = num_found = 0
         try:
             with warnings.catch_warnings():
                 # Suppress SQLAlchemy warnings
@@ -240,7 +395,6 @@ class UpdaterTestCase(unittest.TestCase):
                 fake_projects = [ScodeTestProject().project,
                                  ScienceTestProject().project,
                                  AlmaTestProject().project]
-                num_commits = 0
                 try:
                     for fake_project in fake_projects:
                         project_code = fake_project.project_code
@@ -249,18 +403,46 @@ class UpdaterTestCase(unittest.TestCase):
                                     project_code) \
                             .first()
                         if existing is not None:
+                            num_found += 1
                             session.delete(existing)
                             session.commit()
                         session.add(fake_project)
                         session.commit()
                         num_commits += 1
 
-                    self.assertEqual(len(fake_projects), num_commits)
+                    if num_commits < num_found:
+                        pytest.fail(f'{num_found} fake projects were found '
+                                    f'and deleted, but {num_commits} were '
+                                    f'added and committed')
                 except Exception as exc:
                     pytest.fail(f'{exc}')
         finally:
             session.close()
 
+    def remove_test_data(self):
+        session = create_session('SDM')
+        try:
+            with warnings.catch_warnings():
+                # Suppress SQLAlchemy warnings
+                warnings.simplefilter("ignore", category=sa_exc.SAWarning)
+
+                fake_projects = [ScodeTestProject().project,
+                                 ScienceTestProject().project,
+                                 AlmaTestProject().project]
+                for fake_project in fake_projects:
+                    project_code = fake_project.project_code
+                    existing = session.query(Project) \
+                        .filter(Project.project_code ==
+                                project_code) \
+                        .first()
+                    if existing is not None:
+                        session.delete(existing)
+                        session.commit()
+        except Exception as exc:
+            pytest.fail(f'{exc}')
+        finally:
+            session.close()
+
     def get_project_from_db(self, session: Session, project_code: str):
         with warnings.catch_warnings():
             # Suppress SQLAlchemy warnings
@@ -270,6 +452,22 @@ class UpdaterTestCase(unittest.TestCase):
                 .filter(Project.project_code == project_code) \
                 .first()
 
+class FailingUpdaterHelper:
+    # def __init__(self, **kwargs):
+    #     pass
+
+    @pytest.fixture()
+    def update_project(self):
+        return SystemExit(5)
+
+class FailingUpdater:
+    def __init__(self):
+        self.helper = FailingUpdaterHelper()
+
+    def update_project(self):
+        return SystemExit(5)
+
+
 class CommandLineUpdaterLauncher:
 
     def __init__(self, args: list):
@@ -292,6 +490,7 @@ class CommandLineUpdaterLauncher:
                                        check=False,
                                        bufsize=1,
                                        universal_newlines=True)
+            self.stdout = proc.stdout
             return proc.returncode
         except Exception as exc:
             _LOG.error(f'{exc}')
diff --git a/apps/cli/utilities/test_data/__init__.py b/apps/cli/utilities/test_data/__init__.py
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/deploy.sh b/deploy.sh
index 9d2d00437e18b6acdea32e4b7be2b33ff47a23ea..7540346c3e19daf7f214bf632c590a217e3f8f5f 100755
--- a/deploy.sh
+++ b/deploy.sh
@@ -32,7 +32,7 @@ if [ "$VIRTUAL_ENV" = "" ]; then
     # Make sure we have an appropriate venv available here: 
     if [ ! -d venv ]; then
 	echo "No virtual environment detected, creating a basic one."
-	python3.6 -m venv venv
+	python3.8 -m venv venv
 	source ./venv/bin/activate
 	pip install --upgrade pip
 	pip install -r deployment/requirements.txt
diff --git a/shared/schema/src/schema/__init__.py b/shared/schema/src/schema/__init__.py
index 18dcffe8ebcbba4dc18e6aecde0e0d1f66a29873..4076356434a624518a67ba5593abf96b5afe214a 100644
--- a/shared/schema/src/schema/__init__.py
+++ b/shared/schema/src/schema/__init__.py
@@ -1,5 +1,7 @@
 # publish our behavior-enhanced table classes
 import sqlalchemy
+from sqlalchemy.sql import ClauseElement
+
 from .model import *
 from sqlalchemy.orm import sessionmaker
 from pycapo import CapoConfig
@@ -47,6 +49,26 @@ def create_session(instrument, **kwargs):
     session = session_mkr()
     return session
 
+def create_model_instance(session, model, defaults=None, commit=True, **kwargs):
+    params = dict((k, v) for k, v in kwargs.items() if not isinstance(v, ClauseElement))
+    params.update(defaults or {})
+    instance = model(**params)
+    session.add(instance)
+    if commit:
+        session.commit()
+        # Need to read it back to get the new PK, etc.
+        session.refresh(instance)
+    return instance
+
+def get_or_create_model_instance(session, model, defaults=None, commit=True,
+                           **kwargs):
+    instance = session.query(model).filter_by(**kwargs).first()
+    if instance:
+        return instance, False
+    else:
+        instance = create_model_instance(session, model, defaults=defaults, commit=commit, **kwargs)
+        return instance, True
+
 
 class ArchiveDBSession:
     """A class to create context manager around an archive connection."""
diff --git a/shared/schema/src/schema/model.py b/shared/schema/src/schema/model.py
index dfa18bb132714475be3e355d100ff5043c857633..bc46a8f583248131eacd30da989bf3f355909b06 100644
--- a/shared/schema/src/schema/model.py
+++ b/shared/schema/src/schema/model.py
@@ -693,7 +693,8 @@ class Project(Base):
     proprietary_duration = Column(Float(53))
 
     science_products = relationship('ScienceProduct', secondary='science_products_projects')
-    authors = relationship('Author')
+    authors = relationship('Author', cascade="all,delete, delete-orphan",
+                           backref='parent')
     execution_blocks = relationship('ExecutionBlock')
     file_groups = relationship('Filegroup')
     alma_ouses = relationship('AlmaOus', backref='projects')