Repository: instructlab/instructlab

Issue:
lab init fails silently in some cases
Given a new container, with git ssh keys _not_ set

```
pip install git+https://<token>@github.com/instruct-lab/cli@main
lab init --non-interactive 
```
fails with an error as expected

but

```
pip install git+https://<token>@github.com/instruct-lab/cli@main
lab init 
```
fails silently- that is to say from the user's POV it appears to succeed, but no `taxonomy` directory is created. 

Another curiosity:
```
pip install git+https://<token>@github.com/instruct-lab/cli@main
lab init 
```

But if you interactively change the directory to one besides `taxonomy` it also fails as expected. 

"Fails as expected" means throwing the error:
```
Failed to clone taxonomy repo: Cmd('git') failed due to: exit code(128)
  cmdline: git clone -v --branch=main --depth=1 -- [git@github.com](mailto:git@github.com):instruct-lab/taxonomy.git taxonomy
  stderr: 'Cloning into 'taxonomy'...
Host key verification failed.
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
'
```




Discussion:
I was able to reproduce this problem with both interactive and non-interactive mode. Although in my case the situation looks like this:
```
$ lab init 
Welcome to InstructLab CLI. This guide will help you to setup your environment.
Please provide the following values to initiate the environment:
Path to taxonomy repo [taxonomy]: 
`taxonomy` seems to not exists or is empty. Should I clone git@github.com:instruct-lab/taxonomy.git for you? [y/N]: y
Cloning git@github.com:instruct-lab/taxonomy.git...
The authenticity of host 'github.com (140.82.113.4)' can't be established.
ED25519 key fingerprint is SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? no
Failed to clone taxonomy repo: Cmd('git') failed due to: exit code(128)
  cmdline: git clone -v --branch=main -- git@github.com:instruct-lab/taxonomy.git taxonomy
  stderr: 'Cloning into 'taxonomy'...
Host key verification failed.
fatal: Could not read from remote repository.
```
similar question appears when I run in non-interactive mode. I've traced this back to `StrictHostKeyChecking ask` configuration option in my `/etc/ssh/ssh_config`, I'm assuming in your case its value is `yes` in which case, quoting `man ssh_config`:
```
If this flag is set to “yes”, ssh will never automatically add host keys to the $HOME/.ssh/known_hosts 
file, and refuses to connect to hosts whose host key has changed.
```
Indeed, when I changed the value to `yes` in my local config I was getting identical error to yours. I'm guessing once we open up this repository for broader public and we switch to `git clone` using `https` this problem will disappear. For now I'll work on adding more intuitive error message, and fallback to instruct user to manually clone the repository. 



Patch:
diff --git a/cli/lab.py b/cli/lab.py
index fae3867ad2..b664d90e47 100755
--- a/cli/lab.py
+++ b/cli/lab.py
@@ -144,16 +144,17 @@ def init(
         taxonomy_path = utils.expand_path(
             click.prompt("Path to taxonomy repo", default=taxonomy_path)
         )
-        try:
-            taxonomy_contents = os.listdir(taxonomy_path)
-        except FileNotFoundError:
-            taxonomy_contents = []
-        if taxonomy_contents:
-            clone_taxonomy_repo = False
-        else:
-            clone_taxonomy_repo = click.confirm(
-                f"`{taxonomy_path}` seems to not exists or is empty. Should I clone {repository} for you?"
-            )
+
+    try:
+        taxonomy_contents = os.listdir(taxonomy_path)
+    except FileNotFoundError:
+        taxonomy_contents = []
+    if taxonomy_contents:
+        clone_taxonomy_repo = False
+    elif interactive:
+        clone_taxonomy_repo = click.confirm(
+            f"`{taxonomy_path}` seems to not exists or is empty. Should I clone {repository} for you?"
+        )
 
     # clone taxonomy repo if it needs to be cloned
     if clone_taxonomy_repo:
@@ -165,11 +166,12 @@ def init(
                 Repo.clone_from(repository, taxonomy_path, branch="main", depth=1)
         except GitError as exc:
             click.secho(f"Failed to clone taxonomy repo: {exc}", fg="red")
+            click.secho(f"Please make sure to manually run `git clone {repository}`")
             raise click.exceptions.Exit(1)
 
     # check if models dir exists, and if so ask for which model to use
     models_dir = dirname(model_path)
-    if exists(models_dir):
+    if interactive and exists(models_dir):
         model_path = utils.expand_path(
             click.prompt("Path to your model", default=model_path)
         )


Test patch:
diff --git a/tests/test_lab_init.py b/tests/test_lab_init.py
index 7e172d73b8..f8f74cdcad 100644
--- a/tests/test_lab_init.py
+++ b/tests/test_lab_init.py
@@ -11,23 +11,32 @@
 
 # First Party
 from cli import lab
+from cli.config import read_config
 from tests.schema import Config
 
 
 class TestLabInit(unittest.TestCase):
-    @patch("git.Repo.clone_from", MagicMock())
-    def test_init(self):
+    # When using `from X import Y` you need to understand that Y becomes part
+    # of your module, so you should use `my_modyle.Y`` to patch.
+    # When using `import X`, you should use `X.Y` to patch.
+    # https://docs.python.org/3/library/unittest.mock.html#where-to-patch?
+    @patch("cli.lab.Repo.clone_from")
+    def test_init_noninteractive(self, mock_clone_from):
         runner = CliRunner()
         with runner.isolated_filesystem():
-            result = runner.invoke(lab.init, ["--non-interactive"])
+            result = runner.invoke(lab.init, args=["--non-interactive"])
             self.assertEqual(result.exit_code, 0)
-            self.assertTrue("config.yaml" in os.listdir())
+            self.assertIn("config.yaml", os.listdir())
+            mock_clone_from.assert_called_once()
 
-    @patch("git.Repo.clone_from", MagicMock())
-    def test_config_pydantic(self):
+    @patch("cli.lab.Repo.clone_from")
+    def test_init_config(self, mock_clone_from):
         runner = CliRunner()
         with runner.isolated_filesystem():
-            runner.invoke(lab.init, ["--non-interactive"])
+            result = runner.invoke(lab.init, args=["--non-interactive"])
+            self.assertEqual(result.exit_code, 0)
+            self.assertIn("config.yaml", os.listdir())
+            mock_clone_from.assert_called_once()
             try:
                 pydantic_yaml.parse_yaml_file_as(model_type=Config, file="config.yaml")
                 self.assertTrue
@@ -38,14 +47,67 @@ def test_config_pydantic(self):
                 print(e)
                 assert self.assertFalse
 
+    def test_init_interactive(self):
+        runner = CliRunner()
+        with runner.isolated_filesystem():
+            result = runner.invoke(lab.init, input="\nn")
+            self.assertEqual(result.exit_code, 0)
+            self.assertIn("config.yaml", os.listdir())
+
     @patch(
-        "git.Repo.clone_from", MagicMock(side_effect=GitError("Authentication failed"))
+        "cli.lab.Repo.clone_from",
+        MagicMock(side_effect=GitError("Authentication failed")),
     )
-    def test_init_git_error(self):
+    def test_init_interactive_git_error(self):
         runner = CliRunner()
         with runner.isolated_filesystem():
-            result = runner.invoke(lab.init, ["--non-interactive"])
-            self.assertEqual(result.exit_code, 1)
-            self.assertTrue(
-                "Failed to clone taxonomy repo: Authentication failed" in result.output
+            result = runner.invoke(lab.init, input="\ny")
+            self.assertEqual(
+                result.exit_code, 1, "command finished with an unexpected exit code"
+            )
+            self.assertIn(
+                "Failed to clone taxonomy repo: Authentication failed", result.output
             )
+            self.assertIn("manually run", result.output)
+
+    @patch("cli.lab.Repo.clone_from")
+    def test_init_interactive_clone(self, mock_clone_from):
+        runner = CliRunner()
+        with runner.isolated_filesystem():
+            result = runner.invoke(lab.init, input="\ny")
+            self.assertEqual(result.exit_code, 0)
+            self.assertIn("config.yaml", os.listdir())
+            mock_clone_from.assert_called_once()
+
+    def test_init_interactive_with_preexisting_nonempty_taxonomy(self):
+        runner = CliRunner()
+        with runner.isolated_filesystem():
+            os.makedirs("taxonomy/contents")
+            result = runner.invoke(lab.init, input="\n")
+            self.assertEqual(result.exit_code, 0)
+            self.assertIn("config.yaml", os.listdir())
+            self.assertIn("taxonomy", os.listdir())
+
+    def test_init_interactive_with_preexisting_config(self):
+        runner = CliRunner()
+        with runner.isolated_filesystem():
+            # first run to prime the config.yaml in current directory
+            result = runner.invoke(lab.init, input="non-default-taxonomy\nn")
+            self.assertEqual(result.exit_code, 0)
+            self.assertIn("config.yaml", os.listdir())
+            config = read_config("config.yaml")
+            self.assertEqual(config.generate.taxonomy_path, "non-default-taxonomy")
+
+            # second invocation should ask if we want to overwrite - yes, and change taxonomy path
+            result = runner.invoke(lab.init, input="y\ndifferent-taxonomy\nn")
+            self.assertEqual(result.exit_code, 0)
+            self.assertIn("config.yaml", os.listdir())
+            config = read_config("config.yaml")
+            self.assertEqual(config.generate.taxonomy_path, "different-taxonomy")
+
+            # third invocation should again ask, but this time don't overwrite
+            result = runner.invoke(lab.init, input="n")
+            self.assertEqual(result.exit_code, 0)
+            self.assertIn("config.yaml", os.listdir())
+            config = read_config("config.yaml")
+            self.assertEqual(config.generate.taxonomy_path, "different-taxonomy")