Skip to content

Add option to skip ansi escape codes #103

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Oct 27, 2023

Conversation

phaer
Copy link
Contributor

@phaer phaer commented Apr 23, 2023

This allows users to ignore ansi escape codes, such as terminal colors and would therefore close #25.

As you've noticed, i needed to add the new parameter skip_ansi_escape_codes in quite a few places. Maybe it would be a good idea to introduce an Options struct for timeout and skip_ansi_escape_codes, with default values? I could do that, but wanted to keep this PR focused.

I've considered using strip_ansi_escapes, mentioned in the issue. But that uses VTE which uses a state machine to interpret the escape codes.

But as we are not interested in interpreting them, just skipping them and ansi escape codes are just the byte 27 inserted in a string, followed by "[", zero or more numbers separated by ";" and ending with a letter;
We optionally just skip those in our non-blocking reader in order to efficently ignore them during matching without having to traverse the whole buffer a second time. A minimal test case is also added

@phaer phaer force-pushed the skip-ansi-escape-codes branch from 3847d78 to 1c1cd35 Compare April 23, 2023 20:08
@petreeftime
Copy link
Collaborator

Adding an optional Options or Config generated by a ConfigBuilder would be ideal, so that we don't have to change the API for every field.

I would also add a spawn_with_config function instead of modifing the API of the current spawn function.

@phaer phaer force-pushed the skip-ansi-escape-codes branch from eca1f4a to b0b521b Compare April 23, 2023 21:03
@phaer
Copy link
Contributor Author

phaer commented Apr 23, 2023

Thanks for the quick review @petreeftime. I've added Options, which makes the diff much more readable.

The spawn* API stays the same, but gains spawn_with_options. Just saw that the test failed again, same failure as in my other PR - but I can't reproduce locally with cargo test yet 🤔

@phaer phaer force-pushed the skip-ansi-escape-codes branch from b0b521b to b0cdd41 Compare April 23, 2023 21:09
@matthiasbeyer
Copy link
Member

Yeah that CI failure is attempted to be fixed in #101 - it is some kind of sporadic failure which we did not find yet.

@phaer
Copy link
Contributor Author

phaer commented Apr 24, 2023

Ah, thanks for the explanation! So I'll just leave this PR-as is, as bors should re-run the checks before an eventual merge anyways, right?

LMK if there's something left to improve or fix :)

@matthiasbeyer
Copy link
Member

Yeah I think this is fine... we'll ping you if there's anything to fix up, I guess.

@phaer phaer force-pushed the skip-ansi-escape-codes branch 4 times, most recently from 883b178 to b1b6044 Compare April 27, 2023 10:53
Copy link
Collaborator

@ekarlsn ekarlsn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, nice work!

@phaer
Copy link
Contributor Author

phaer commented Jun 27, 2023

@matthiasbeyer Friendly ping :) could we merge this?

@matthiasbeyer
Copy link
Member

Yes we could, but we're getting the sporadic CI error again and for whatever reason I cannot re-run the CI.

Could you @phaer please rebase this PR to latest master, or, if it is based on latest master: git commit --amend --no-edit && git push <your github remote> <this branch> --force please? This way CI should re-run and I hope we do not get the error this time.

@matthiasbeyer
Copy link
Member

This CI failure is really annoying 😠

phaer added 4 commits October 27, 2023 12:20
ansi escape codes are just the byte 27 inserted in a string,
followed by "[", zero or more numbers separated by ";" and ending
with a letter.
We optionally just skip those in our non-blocking reader in order
to efficently ignore them during matching.

Signed-off-by: phaer <[email protected]>
because handling of Err(_) and Ok(0) might be tricky otherwise,
so we just use a boolean flag

Signed-off-by: phaer <[email protected]>
@phaer phaer force-pushed the skip-ansi-escape-codes branch from f099dc4 to e992f99 Compare October 27, 2023 10:21
@phaer
Copy link
Contributor Author

phaer commented Oct 27, 2023

@matthiasbeyer Just remembered this, rebased and we've got a passing CI now 🥳

@matthiasbeyer
Copy link
Member

bors merge=souze

@bors
Copy link
Contributor

bors bot commented Oct 27, 2023

Build succeeded!

The publicly hosted instance of bors-ng is deprecated and will go away soon.

If you want to self-host your own instance, instructions are here.
For more help, visit the forum.

If you want to switch to GitHub's built-in merge queue, visit their help page.

@bors bors bot merged commit 9eb61dd into rust-cli:master Oct 27, 2023
@phaer phaer deleted the skip-ansi-escape-codes branch October 27, 2023 10:44
@hirrolot hirrolot mentioned this pull request Jan 12, 2024
RandyMcMillan added a commit to RandyMcMillan/gnostr that referenced this pull request Jan 9, 2025
new file mode 100644
index 00000000..43f36291
--- /dev/null
+++ b/test_utils/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "test_utils"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+anyhow = "1.0.75"
+assert_cmd = "2.0.12"
+dialoguer = "0.10.4"
+directories = "5.0.1"
+git2 = "0.18.1"
+nostr = "0.32.0"
+nostr-sdk = "0.32.0"
+once_cell = "1.18.0"
+rand = "0.8"
+rexpect = { git = "https://github.com/rust-cli/rexpect.git", rev = "9eb61dd" }
+simple-websockets = { git = "https://github.com/DanConwayDev/simple-websockets", branch= "auto-release-port" }
+strip-ansi-escapes = "0.2.0"
+tungstenite = "0.20.1"
diff --git a/test_utils/config.toml b/test_utils/config.toml
new file mode 100644
index 00000000..519f44ff
--- /dev/null
+++ b/test_utils/config.toml
@@ -0,0 +1,2 @@
+[env]
+NGITTEST = true
\ No newline at end of file
diff --git a/test_utils/src/git.rs b/test_utils/src/git.rs
new file mode 100644
index 00000000..76656df6
--- /dev/null
+++ b/test_utils/src/git.rs
@@ -0,0 +1,183 @@
+//create
+
+// implement drop?
+use std::{env::current_dir, fs, path::PathBuf};
+
+use anyhow::{Context, Result};
+use git2::{Oid, RepositoryInitOptions, Signature, Time};
+
+pub struct GitTestRepo {
+    pub dir: PathBuf,
+    pub git_repo: git2::Repository,
+}
+
+impl Default for GitTestRepo {
+    fn default() -> Self {
+        Self::new("main").unwrap()
+    }
+}
+impl GitTestRepo {
+    pub fn new(main_branch_name: &str) -> Result<Self> {
+        let path = current_dir()?.join(format!("tmpgit-{}", rand::random::<u64>()));
+        let git_repo = git2::Repository::init_opts(
+            &path,
+            RepositoryInitOptions::new()
+                .initial_head(main_branch_name)
+                .mkpath(true),
+        )?;
+        Ok(Self {
+            dir: path,
+            git_repo,
+        })
+    }
+
+    pub fn initial_commit(&self) -> Result<Oid> {
+        let oid = self.git_repo.index()?.write_tree()?;
+        let tree = self.git_repo.find_tree(oid)?;
+        let commit_oid = self.git_repo.commit(
+            Some("HEAD"),
+            &joe_signature(),
+            &joe_signature(),
+            "Initial commit",
+            &tree,
+            &[],
+        )?;
+        Ok(commit_oid)
+    }
+
+    pub fn populate(&self) -> Result<Oid> {
+        self.initial_commit()?;
+        fs::write(self.dir.join("t1.md"), "some content")?;
+        self.stage_and_commit("add t1.md")?;
+        fs::write(self.dir.join("t2.md"), "some content1")?;
+        self.stage_and_commit("add t2.md")
+    }
+
+    pub fn populate_with_test_branch(&self) -> Result<Oid> {
+        self.populate()?;
+        self.create_branch("add-example-feature")?;
+        self.checkout("add-example-feature")?;
+        fs::write(self.dir.join("f1.md"), "some content")?;
+        self.stage_and_commit("add f1.md")?;
+        fs::write(self.dir.join("f2.md"), "some content")?;
+        self.stage_and_commit("add f2.md")?;
+        fs::write(self.dir.join("f3.md"), "some content1")?;
+        self.stage_and_commit("add f3.md")
+    }
+
+    pub fn stage_and_commit(&self, message: &str) -> Result<Oid> {
+        self.stage_and_commit_custom_signature(message, None, None)
+    }
+
+    pub fn stage_and_commit_custom_signature(
+        &self,
+        message: &str,
+        author: Option<&git2::Signature>,
+        commiter: Option<&git2::Signature>,
+    ) -> Result<Oid> {
+        let prev_oid = self.git_repo.head().unwrap().peel_to_commit()?;
+
+        let mut index = self.git_repo.index()?;
+        index.add_all(["."], git2::IndexAddOption::DEFAULT, None)?;
+        index.write()?;
+
+        let oid = self.git_repo.commit(
+            Some("HEAD"),
+            author.unwrap_or(&joe_signature()),
+            commiter.unwrap_or(&joe_signature()),
+            message,
+            &self.git_repo.find_tree(index.write_tree()?)?,
+            &[&prev_oid],
+        )?;
+
+        Ok(oid)
+    }
+
+    pub fn create_branch(&self, branch_name: &str) -> Result<()> {
+        self.git_repo
+            .branch(branch_name, &self.git_repo.head()?.peel_to_commit()?, false)?;
+        Ok(())
+    }
+
+    pub fn checkout(&self, ref_name: &str) -> Result<Oid> {
+        let (object, reference) = self.git_repo.revparse_ext(ref_name)?;
+
+        self.git_repo.checkout_tree(&object, None)?;
+
+        match reference {
+            // gref is an actual reference like branches or tags
+            Some(gref) => self.git_repo.set_head(gref.name().unwrap()),
+            // this is a commit, not a reference
+            None => self.git_repo.set_head_detached(object.id()),
+        }?;
+        let oid = self.git_repo.head()?.peel_to_commit()?.id();
+        Ok(oid)
+    }
+
+    pub fn get_local_branch_names(&self) -> Result<Vec<String>> {
+        let local_branches = self
+            .git_repo
+            .branches(Some(git2::BranchType::Local))
+            .context("getting GitRepo branches should not error even for a blank repository")?;
+
+        let mut branch_names = vec![];
+
+        for iter in local_branches {
+            let branch = iter?.0;
+            if let Some(name) = branch.name()? {
+                branch_names.push(name.to_string());
+            }
+        }
+        Ok(branch_names)
+    }
+
+    pub fn get_checked_out_branch_name(&self) -> Result<String> {
+        Ok(self
+            .git_repo
+            .head()?
+            .shorthand()
+            .context("an object without a shorthand is checked out")?
+            .to_string())
+    }
+
+    pub fn get_tip_of_local_branch(&self, branch_name: &str) -> Result<Oid> {
+        let branch = self
+            .git_repo
+            .find_branch(branch_name, git2::BranchType::Local)
+            .context(format!("cannot find branch {branch_name}"))?;
+        Ok(branch.into_reference().peel_to_commit()?.id())
+    }
+
+    pub fn add_remote(&self, name: &str, url: &str) -> Result<()> {
+        self.git_repo.remote(name, url)?;
+        Ok(())
+    }
+}
+
+impl Drop for GitTestRepo {
+    fn drop(&mut self) {
+        let _ = fs::remove_dir_all(&self.dir);
+    }
+}
+pub fn joe_signature() -> Signature<'static> {
+    Signature::new("Joe Bloggs", "[email protected]", &Time::new(0, 0)).unwrap()
+}
+
+#[cfg(test)]
+mod tests {
+
+    use super::*;
+
+    #[test]
+    fn methods_do_not_throw() -> Result<()> {
+        let repo = GitTestRepo::new("main")?;
+
+        repo.populate()?;
+        repo.create_branch("feature")?;
+        repo.checkout("feature")?;
+        fs::write(repo.dir.join("t3.md"), "some content")?;
+        repo.stage_and_commit("add t3.md")?;
+
+        Ok(())
+    }
+}
diff --git a/test_utils/src/lib.rs b/test_utils/src/lib.rs
new file mode 100644
index 00000000..1d881cfe
--- /dev/null
+++ b/test_utils/src/lib.rs
@@ -0,0 +1,978 @@
+use std::{ffi::OsStr, path::PathBuf, str::FromStr};
+
+use anyhow::{bail, ensure, Context, Result};
+use dialoguer::theme::{ColorfulTheme, Theme};
+use directories::ProjectDirs;
+use nostr::{self, nips::nip65::RelayMetadata, Kind, Tag};
+use nostr_sdk::{serde_json, TagStandard};
+use once_cell::sync::Lazy;
+use rexpect::session::{Options, PtySession};
+use strip_ansi_escapes::strip_str;
+
+pub mod git;
+pub mod relay;
+
+pub static PATCH_KIND: u16 = 1617;
+pub static REPOSITORY_KIND: u16 = 30617;
+
+pub static TEST_KEY_1_NSEC: &str =
+    "nsec1ppsg5sm2aexq06juxmu9evtutr6jkwkhp98exxxvwamhru9lyx9s3rwseq";
+pub static TEST_KEY_1_SK_HEX: &str =
+    "08608a436aee4c07ea5c36f85cb17c58f52b3ad7094f9318cc777771f0bf218b";
+pub static TEST_KEY_1_NPUB: &str =
+    "npub175lyhnt6nn00qjw0v3navw9pxgv43txnku0tpxprl4h6mvpr6a5qlphudg";
+pub static TEST_KEY_1_PUBKEY_HEX: &str =
+    "f53e4bcd7a9cdef049cf6467d638a1321958acd3b71eb09823fd6fadb023d768";
+pub static TEST_KEY_1_DISPLAY_NAME: &str = "bob";
+pub static TEST_KEY_1_ENCRYPTED: &str = "ncryptsec1qgq77e3uftz8dh3jkjxwdms3v6gwqaqduxyzld82kskas8jcs5xup3sf2pc5tr0erqkqrtu0ptnjgjlgvx8lt7c0d7laryq2u7psfa6zm7mk7ln3ln58468shwatm7cx5wy5wvm7yk74ksrngygwxg74";
+pub static TEST_KEY_1_ENCRYPTED_WEAK: &str = "ncryptsec1qg835almhlrmyxqtqeva44d5ugm9wk2ccmwspxrqv4wjsdpdlud9es5hsrvs0pas7dvsretm0mc26qwfc7v8986mqngnjshcplnqzj62lxf44a0kkdv788f6dh20x2eum96l2j8v37s5grrheu2hgrkf";
+pub static TEST_KEY_1_KEYS: Lazy<nostr::Keys> =
+    Lazy::new(|| nostr::Keys::from_str(TEST_KEY_1_NSEC).unwrap());
+
+pub fn generate_test_key_1_metadata_event(name: &str) -> nostr::Event {
+    nostr::event::EventBuilder::metadata(&nostr::Metadata::new().name(name))
+        .to_event(&TEST_KEY_1_KEYS)
+        .unwrap()
+}
+
+pub fn generate_test_key_1_metadata_event_old(name: &str) -> nostr::Event {
+    make_event_old_or_change_user(
+        generate_test_key_1_metadata_event(name),
+        &TEST_KEY_1_KEYS,
+        10000,
+    )
+}
+
+pub fn generate_test_key_1_kind_event(kind: Kind) -> nostr::Event {
+    nostr::event::EventBuilder::new(kind, "", [])
+        .to_event(&TEST_KEY_1_KEYS)
+        .unwrap()
+}
+
+pub fn generate_test_key_1_relay_list_event() -> nostr::Event {
+    nostr::event::EventBuilder::new(
+        nostr::Kind::RelayList,
+        "",
+        [
+            nostr::Tag::from_standardized(nostr::TagStandard::RelayMetadata {
+                relay_url: nostr::Url::from_str("ws://localhost:8053").unwrap(),
+                metadata: Some(RelayMetadata::Write),
+            }),
+            nostr::Tag::from_standardized(nostr::TagStandard::RelayMetadata {
+                relay_url: nostr::Url::from_str("ws://localhost:8054").unwrap(),
+                metadata: Some(RelayMetadata::Read),
+            }),
+            nostr::Tag::from_standardized(nostr::TagStandard::RelayMetadata {
+                relay_url: nostr::Url::from_str("ws://localhost:8055").unwrap(),
+                metadata: None,
+            }),
+        ],
+    )
+    .to_event(&TEST_KEY_1_KEYS)
+    .unwrap()
+}
+
+pub fn generate_test_key_1_relay_list_event_same_as_fallback() -> nostr::Event {
+    nostr::event::EventBuilder::new(
+        nostr::Kind::RelayList,
+        "",
+        [
+            nostr::Tag::from_standardized(nostr::TagStandard::RelayMetadata {
+                relay_url: nostr::Url::from_str("ws://localhost:8051").unwrap(),
+                metadata: Some(RelayMetadata::Write),
+            }),
+            nostr::Tag::from_standardized(nostr::TagStandard::RelayMetadata {
+                relay_url: nostr::Url::from_str("ws://localhost:8052").unwrap(),
+                metadata: Some(RelayMetadata::Write),
+            }),
+        ],
+    )
+    .to_event(&TEST_KEY_1_KEYS)
+    .unwrap()
+}
+
+pub static TEST_KEY_2_NSEC: &str =
+    "nsec1ypglg6nj6ep0g2qmyfqcv2al502gje3jvpwye6mthmkvj93tqkesknv6qm";
+pub static TEST_KEY_2_NPUB: &str =
+    "npub1h2yz2eh0798nh25hvypenrz995nla9dktfuk565ljf3ghnkhdljsul834e";
+
+pub static TEST_KEY_2_DISPLAY_NAME: &str = "carole";
+pub static TEST_KEY_2_ENCRYPTED: &str = "...2";
+pub static TEST_KEY_2_KEYS: Lazy<nostr::Keys> =
+    Lazy::new(|| nostr::Keys::from_str(TEST_KEY_2_NSEC).unwrap());
+
+pub fn generate_test_key_2_metadata_event(name: &str) -> nostr::Event {
+    nostr::event::EventBuilder::metadata(&nostr::Metadata::new().name(name))
+        .to_event(&TEST_KEY_2_KEYS)
+        .unwrap()
+}
+
+pub static TEST_INVALID_NSEC: &str = "nsec1ppsg5sm2aex";
+pub static TEST_PASSWORD: &str = "769dfd£pwega8SHGv3!#Bsfd5t";
+pub static TEST_INVALID_PASSWORD: &str = "INVALID769dfd£pwega8SHGv3!";
+pub static TEST_WEAK_PASSWORD: &str = "fhaiuhfwe";
+pub static TEST_RANDOM_TOKEN: &str = "lkjh2398HLKJ43hrweiJ6FaPfdssgtrg";
+
+pub fn make_event_old_or_change_user(
+    event: nostr::Event,
+    keys: &nostr::Keys,
+    how_old_in_secs: u64,
+) -> nostr::Event {
+    let mut unsigned =
+        nostr::event::EventBuilder::new(event.kind, event.content.clone(), event.tags.clone())
+            .to_unsigned_event(keys.public_key());
+
+    unsigned.created_at =
+        nostr::types::Timestamp::from(nostr::types::Timestamp::now().as_u64() - how_old_in_secs);
+    unsigned.id = Some(nostr::EventId::new(
+        &keys.public_key(),
+        &unsigned.created_at,
+        &unsigned.kind,
+        &unsigned.tags,
+        &unsigned.content,
+    ));
+
+    unsigned.sign(keys).unwrap()
+}
+
+pub fn generate_repo_ref_event() -> nostr::Event {
+    // taken from test git_repo
+    // TODO - this may not be consistant across computers as it might take the
+    // author and committer from global git config
+    let root_commit = "9ee507fc4357d7ee16a5d8901bedcd103f23c17d";
+    nostr::event::EventBuilder::new(
+        nostr::Kind::Custom(REPOSITORY_KIND),
+        "",
+        [
+            Tag::identifier(
+                // root_commit.to_string()
+                format!("{}-consider-it-random", root_commit),
+            ),
+            Tag::from_standardized(TagStandard::Reference(root_commit.to_string())),
+            Tag::from_standardized(TagStandard::Name("example name".into())),
+            Tag::from_standardized(TagStandard::Description("example description".into())),
+            Tag::custom(
+                nostr::TagKind::Custom(std::borrow::Cow::Borrowed("clone")),
+                vec!["git:://123.gitexample.com/test".to_string()],
+            ),
+            Tag::custom(
+                nostr::TagKind::Custom(std::borrow::Cow::Borrowed("web")),
+                vec![
+                    "https://exampleproject.xyz".to_string(),
+                    "https://gitworkshop.dev/123".to_string(),
+                ],
+            ),
+            Tag::custom(
+                nostr::TagKind::Custom(std::borrow::Cow::Borrowed("relays")),
+                vec![
+                    "ws://localhost:8055".to_string(),
+                    "ws://localhost:8056".to_string(),
+                ],
+            ),
+            Tag::custom(
+                nostr::TagKind::Custom(std::borrow::Cow::Borrowed("maintainers")),
+                vec![
+                    TEST_KEY_1_KEYS.public_key().to_string(),
+                    TEST_KEY_2_KEYS.public_key().to_string(),
+                ],
+            ),
+        ],
+    )
+    .to_event(&TEST_KEY_1_KEYS)
+    .unwrap()
+}
+
+/// enough to fool event_is_patch_set_root
+pub fn get_pretend_proposal_root_event() -> nostr::Event {
+    serde_json::from_str(r#"{"id":"8cb75aa4cda10a3a0f3242dc49d36159d30b3185bf63414cf6ce17f5c14a73b1","pubkey":"f53e4bcd7a9cdef049cf6467d638a1321958acd3b71eb09823fd6fadb023d768","created_at":1714984571,"kind":1617,"tags":[["t","root"]],"content":"","sig":"6c197314b8c4c61da696dff888198333004d1ecc5d7bae2c554857f2f2b0d3ecc09369a5d8ba089c1bf89e3c6f5be40ade873fd698438ef8b303ffc6df35eb3f"}"#).unwrap()
+}
+
+/// wrapper for a cli testing tool - currently wraps rexpect and dialoguer
+///
+/// 1. allow more accurate articulation of expected behaviour
+/// 2. provide flexibility to swap rexpect for a tool that better maps to
+///    expected behaviour
+/// 3. provides flexability to swap dialoguer with another cli interaction tool
+pub struct CliTester {
+    rexpect_session: PtySession,
+    formatter: ColorfulTheme,
+}
+
+impl CliTester {
+    pub fn expect_input(&mut self, prompt: &str) -> Result<CliTesterInputPrompt> {
+        let mut i = CliTesterInputPrompt {
+            tester: self,
+            prompt: prompt.to_string(),
+        };
+        i.prompt(false).context("initial input prompt")?;
+        Ok(i)
+    }
+
+    pub fn expect_input_eventually(&mut self, prompt: &str) -> Result<CliTesterInputPrompt> {
+        let mut i = CliTesterInputPrompt {
+            tester: self,
+            prompt: prompt.to_string(),
+        };
+        i.prompt(true).context("initial input prompt")?;
+        Ok(i)
+    }
+
+    pub fn expect_password(&mut self, prompt: &str) -> Result<CliTesterPasswordPrompt> {
+        let mut i = CliTesterPasswordPrompt {
+            tester: self,
+            prompt: prompt.to_string(),
+            confirmation_prompt: "".to_string(),
+        };
+        i.prompt().context("initial password prompt")?;
+        Ok(i)
+    }
+
+    pub fn expect_confirm(
+        &mut self,
+        prompt: &str,
+        default: Option<bool>,
+    ) -> Result<CliTesterConfirmPrompt> {
+        let mut i = CliTesterConfirmPrompt {
+            tester: self,
+            prompt: prompt.to_string(),
+            default,
+        };
+        i.prompt(false, default).context("initial confirm prompt")?;
+        Ok(i)
+    }
+
+    pub fn expect_confirm_eventually(
+        &mut self,
+        prompt: &str,
+        default: Option<bool>,
+    ) -> Result<CliTesterConfirmPrompt> {
+        let mut i = CliTesterConfirmPrompt {
+            tester: self,
+            prompt: prompt.to_string(),
+            default,
+        };
+        i.prompt(true, default).context("initial confirm prompt")?;
+        Ok(i)
+    }
+
+    pub fn expect_choice(
+        &mut self,
+        prompt: &str,
+        choices: Vec<String>,
+    ) -> Result<CliTesterChoicePrompt> {
+        let mut i = CliTesterChoicePrompt {
+            tester: self,
+            prompt: prompt.to_string(),
+            choices,
+        };
+        i.prompt(false).context("initial confirm prompt")?;
+        Ok(i)
+    }
+
+    pub fn expect_multi_select(
+        &mut self,
+        prompt: &str,
+        choices: Vec<String>,
+    ) -> Result<CliTesterMultiSelectPrompt> {
+        let mut i = CliTesterMultiSelectPrompt {
+            tester: self,
+            prompt: prompt.to_string(),
+            choices,
+        };
+        i.prompt(false).context("initial confirm prompt")?;
+        Ok(i)
+    }
+}
+
+pub struct CliTesterInputPrompt<'a> {
+    tester: &'a mut CliTester,
+    prompt: String,
+}
+
+impl CliTesterInputPrompt<'_> {
+    fn prompt(&mut self, eventually: bool) -> Result<&mut Self> {
+        let mut s = String::new();
+        self.tester
+            .formatter
+            .format_prompt(&mut s, self.prompt.as_str())
+            .expect("diagluer theme formatter should succeed");
+        s.push(' ');
+
+        ensure!(
+            s.contains(self.prompt.as_str()),
+            "dialoguer must be broken as formatted prompt success doesnt contain prompt"
+        );
+
+        if eventually {
+            self.tester
+                .expect_eventually(sanatize(s).as_str())
+                .context("expect input prompt eventually")?;
+        } else {
+            self.tester
+                .expect(sanatize(s).as_str())
+                .context("expect input prompt")?;
+        }
+
+        Ok(self)
+    }
+
+    pub fn succeeds_with(&mut self, input: &str) -> Result<&mut Self> {
+        self.tester.send_line(input)?;
+        self.tester
+            .expect(input)
+            .context("expect input to be printed")?;
+        self.tester
+            .expect("\r")
+            .context("expect new line after input to be printed")?;
+
+        let mut s = String::new();
+        self.tester
+            .formatter
+            .format_input_prompt_selection(&mut s, self.prompt.as_str(), input)
+            .expect("diagluer theme formatter should succeed");
+        if !s.contains(self.prompt.as_str()) {
+            panic!("dialoguer must be broken as formatted prompt success doesnt contain prompt");
+        }
+        let formatted_success = format!("{}\r\n", sanatize(s));
+
+        self.tester
+            .expect(formatted_success.as_str())
+            .context("expect immediate prompt success")?;
+        Ok(self)
+    }
+}
+
+pub struct CliTesterPasswordPrompt<'a> {
+    tester: &'a mut CliTester,
+    prompt: String,
+    confirmation_prompt: String,
+}
+
+impl CliTesterPasswordPrompt<'_> {
+    fn prompt(&mut self) -> Result<&mut Self> {
+        let p = match self.confirmation_prompt.is_empty() {
+            true => self.prompt.as_str(),
+            false => self.confirmation_prompt.as_str(),
+        };
+
+        let mut s = String::new();
+        self.tester
+            .formatter
+            .format_password_prompt(&mut s, p)
+            .expect("diagluer theme formatter should succeed");
+
+        ensure!(s.contains(p), "dialoguer must be broken");
+
+        self.tester
+            .expect(format!("\r{}", sanatize(s)).as_str())
+            .context("expect password input prompt")?;
+        Ok(self)
+    }
+
+    pub fn with_confirmation(&mut self, prompt: &str) -> Result<&mut Self> {
+        self.confirmation_prompt = prompt.to_string();
+        Ok(self)
+    }
+
+    pub fn succeeds_with(&mut self, password: &str) -> Result<&mut Self> {
+        self.tester.send_line(password)?;
+
+        self.tester
+            .expect("\r\n")
+            .context("expect new lines after password input")?;
+
+        if !self.confirmation_prompt.is_empty() {
+            self.prompt()
+                .context("expect password confirmation prompt")?;
+            self.tester.send_line(password)?;
+            self.tester
+                .expect("\r\n\r")
+                .context("expect new lines after password confirmation input")?;
+        }
+
+        let mut s = String::new();
+        self.tester
+            .formatter
+            .format_password_prompt_selection(&mut s, self.prompt.as_str())
+            .expect("diagluer theme formatter should succeed");
+
+        ensure!(s.contains(self.prompt.as_str()), "dialoguer must be broken");
+
+        self.tester
+            .expect(format!("\r{}\r\n", sanatize(s)).as_str())
+            .context("expect password prompt success")?;
+
+        Ok(self)
+    }
+}
+
+pub struct CliTesterConfirmPrompt<'a> {
+    tester: &'a mut CliTester,
+    prompt: String,
+    default: Option<bool>,
+}
+
+impl CliTesterConfirmPrompt<'_> {
+    fn prompt(&mut self, eventually: bool, default: Option<bool>) -> Result<&mut Self> {
+        let mut s = String::new();
+        self.tester
+            .formatter
+            .format_confirm_prompt(&mut s, self.prompt.as_str(), default)
+            .expect("diagluer theme formatter should succeed");
+        ensure!(
+            s.contains(self.prompt.as_str()),
+            "dialoguer must be broken as formatted prompt success doesnt contain prompt"
+        );
+
+        if eventually {
+            self.tester
+                .expect_eventually(sanatize(s).as_str())
+                .context("expect input prompt eventually")?;
+        } else {
+            self.tester
+                .expect(sanatize(s).as_str())
+                .context("expect confirm prompt")?;
+        }
+
+        Ok(self)
+    }
+
+    pub fn succeeds_with(&mut self, input: Option<bool>) -> Result<&mut Self> {
+        self.tester.send_line(match input {
+            None => "",
+            Some(true) => "y",
+            Some(false) => "n",
+        })?;
+        self.tester
+            .expect("\r")
+            .context("expect new line after confirm input to be printed")?;
+
+        let mut s = String::new();
+        self.tester
+            .formatter
+            .format_confirm_prompt_selection(
+                &mut s,
+                self.prompt.as_str(),
+                match input {
+                    None => self.default,
+                    Some(_) => input,
+                },
+            )
+            .expect("diagluer theme formatter should succeed");
+        if !s.contains(self.prompt.as_str()) {
+            panic!("dialoguer must be broken as formatted prompt success doesnt contain prompt");
+        }
+        let formatted_success = format!("{}\r\n", sanatize(s));
+
+        self.tester
+            .expect(formatted_success.as_str())
+            .context("expect immediate prompt success")?;
+        Ok(self)
+    }
+}
+
+pub struct CliTesterMultiSelectPrompt<'a> {
+    tester: &'a mut CliTester,
+    prompt: String,
+    choices: Vec<String>,
+}
+
+impl CliTesterMultiSelectPrompt<'_> {
+    fn prompt(&mut self, eventually: bool) -> Result<&mut Self> {
+        if eventually {
+            self.tester
+                .expect_eventually(format!("{}:\r\n", self.prompt))
+                .context("expect multi-select prompt eventually")?;
+        } else {
+            self.tester
+                .expect(format!("{}:\r\n", self.prompt))
+                .context("expect multi-select prompt")?;
+        }
+        Ok(self)
+    }
+
+    pub fn succeeds_with(
+        &mut self,
+        chosen_indexes: Vec<usize>,
+        report: bool,
+        default_indexes: Vec<usize>,
+    ) -> Result<&mut Self> {
+        if report {
+            bail!("TODO: add support for report")
+        }
+
+        fn show_options(
+            tester: &mut CliTester,
+            choices: &[String],
+            active_index: usize,
+            selected_indexes: &[usize],
+        ) -> Result<()> {
+            for (index, item) in choices.iter().enumerate() {
+                tester.expect(format!(
+                    "{}{}{}\r\n",
+                    if active_index.eq(&index) { "> " } else { "  " },
+                    if selected_indexes.iter().any(|i| i.eq(&index)) {
+                        "[x] "
+                    } else {
+                        "[ ] "
+                    },
+                    item,
+                ))?;
+            }
+            Ok(())
+        }
+
+        show_options(self.tester, &self.choices, 0, &default_indexes)?;
+
+        if default_indexes.eq(&chosen_indexes) {
+            self.tester.send("\r\n")?;
+        } else {
+            bail!("TODO: add support changing options");
+        }
+
+        for _ in self.choices.iter() {
+            self.tester.expect("\r")?;
+        }
+        // one for removing prompt maybe?
+        self.tester.expect("\r")?;
+
+        Ok(self)
+    }
+}
+
+pub struct CliTesterChoicePrompt<'a> {
+    tester: &'a mut CliTester,
+    prompt: String,
+    choices: Vec<String>,
+}
+
+impl CliTesterChoicePrompt<'_> {
+    fn prompt(&mut self, eventually: bool) -> Result<&mut Self> {
+        let mut s = String::new();
+        self.tester
+            .formatter
+            .format_select_prompt(&mut s, self.prompt.as_str())
+            .expect("diagluer theme formatter should succeed");
+        ensure!(
+            s.contains(self.prompt.as_str()),
+            "dialoguer must be broken as formatted prompt success doesnt contain prompt"
+        );
+
+        if eventually {
+            self.tester
+                .expect_eventually(sanatize(s).as_str())
+                .context("expect input prompt eventually")?;
+        } else {
+            self.tester
+                .expect(sanatize(s).as_str())
+                .context("expect confirm prompt")?;
+        }
+
+        Ok(self)
+    }
+
+    pub fn succeeds_with(
+        &mut self,
+        chosen_index: u64,
+        report: bool,
+        default_index: Option<u64>,
+    ) -> Result<&mut Self> {
+        if default_index.is_some() {
+            println!("TODO: add support for default choice")
+        }
+
+        fn show_options(
+            tester: &mut CliTester,
+            choices: &[String],
+            selected_index: Option<usize>,
+        ) -> Result<()> {
+            if selected_index.is_some() {
+                for _ in 0..choices.len() {
+                    tester.expect("\r").context("expect new line per choice")?;
+                }
+            } else {
+                tester
+                    .expect("\r\n")
+                    .context("expect new line before choices")?;
+            }
+
+            for (index, item) in choices.iter().enumerate() {
+                let mut s = String::new();
+                tester
+                    .formatter
+                    .format_select_prompt_item(
+                        &mut s,
+                        item.as_str(),
+                        if let Some(i) = selected_index {
+                            index == i
+                        } else {
+                            false
+                        },
+                    )
+                    .expect("diagluer theme formatter should succeed");
+                ensure!(
+                    s.contains(item.as_str()),
+                    "dialoguer must be broken as formatted prompt success doesnt contain prompt"
+                );
+                tester.expect(sanatize(s)).context("expect choice item")?;
+
+                tester
+                    .expect(if choices.len() == index {
+                        "\r\r"
+                    } else {
+                        "\r\n"
+                    })
+                    .context("expect new line after choice item")?;
+            }
+            Ok(())
+        }
+        fn show_selected(
+            tester: &mut CliTester,
+            prompt: &str,
+            choices: &[String],
+            selected_index: u64,
+        ) -> Result<()> {
+            let mut s = String::new();
+
+            let selected = choices[usize::try_from(selected_index)?].clone();
+            tester
+                .formatter
+                .format_select_prompt_selection(&mut s, prompt, selected.as_str())
+                .expect("diagluer theme formatter should succeed");
+            ensure!(
+                s.contains(selected.as_str()),
+                "dialoguer must be broken as formatted prompt success doesnt contain prompt"
+            );
+            tester.expect(sanatize(s)).context("expect choice item")?;
+            Ok(())
+        }
+
+        show_options(self.tester, &self.choices, None)?;
+
+        for _ in 0..(chosen_index + 1) {
+            self.tester.send("j")?;
+        }
+
+        self.tester.send(" ")?;
+
+        for index in 0..(chosen_index + 1) {
+            show_options(self.tester, &self.choices, Some(usize::try_from(index)?))?;
+        }
+
+        for _ in 0..self.choices.len() {
+            self.tester
+                .expect("\r")
+                .context("expect new line per option")?;
+        }
+
+        self.tester
+            .expect("\r")
+            .context("expect new line after options")?;
+
+        if report {
+            show_selected(self.tester, &self.prompt, &self.choices, chosen_index)?;
+            self.tester
+                .expect("\r\n")
+                .context("expect new line at end")?;
+        }
+
+        Ok(self)
+    }
+}
+
+impl CliTester {
+    pub fn new<I, S>(args: I) -> Self
+    where
+        I: IntoIterator<Item = S>,
+        S: AsRef<OsStr>,
+    {
+        Self {
+            rexpect_session: rexpect_with(args, 2000).expect("rexpect to spawn new process"),
+            formatter: ColorfulTheme::default(),
+        }
+    }
+    pub fn new_from_dir<I, S>(dir: &PathBuf, args: I) -> Self
+    where
+        I: IntoIterator<Item = S>,
+        S: AsRef<OsStr>,
+    {
+        Self {
+            rexpect_session: rexpect_with_from_dir(dir, args, 2000)
+                .expect("rexpect to spawn new process"),
+            formatter: ColorfulTheme::default(),
+        }
+    }
+    pub fn new_with_timeout<I, S>(timeout_ms: u64, args: I) -> Self
+    where
+        I: IntoIterator<Item = S>,
+        S: AsRef<OsStr>,
+    {
+        Self {
+            rexpect_session: rexpect_with(args, timeout_ms).expect("rexpect to spawn new process"),
+            formatter: ColorfulTheme::default(),
+        }
+    }
+
+    pub fn restart_with<I, S>(&mut self, args: I) -> &mut Self
+    where
+        I: IntoIterator<Item = S>,
+        S: AsRef<OsStr>,
+    {
+        self.rexpect_session
+            .process
+            .exit()
+            .expect("process to exit");
+        self.rexpect_session = rexpect_with(args, 2000).expect("rexpect to spawn new process");
+        self
+    }
+
+    pub fn exit(&mut self) -> Result<()> {
+        match self
+            .rexpect_session
+            .process
+            .exit()
+            .context("expect proccess to exit")
+        {
+            Ok(_) => Ok(()),
+            Err(e) => Err(e),
+        }
+    }
+
+    fn exp_string(&mut self, message: &str) -> Result<String> {
+        match self
+            .rexpect_session
+            .exp_string(message)
+            .context("expected immediate end but got timed out")
+        {
+            Ok(before) => Ok(before),
+            Err(e) => {
+                for p in [51, 52, 53, 55, 56, 57] {
+                    let _ = relay::shutdown_relay(8000 + p);
+                }
+                Err(e)
+            }
+        }
+    }
+
+    /// returns what came before expected message
+    pub fn expect_eventually<S>(&mut self, message: S) -> Result<String>
+    where
+        S: Into<String>,
+    {
+        let message_string = message.into();
+        let message = message_string.as_str();
+        let before = self.exp_string(message).context("exp_string failed")?;
+        Ok(before)
+    }
+
+    pub fn expect_after_whitespace<S>(&mut self, message: S) -> Result<&mut Self>
+    where
+        S: Into<String>,
+    {
+        assert_eq!("", self.expect_eventually(message)?.trim());
+        Ok(self)
+    }
+
+    pub fn expect<S>(&mut self, message: S) -> Result<&mut Self>
+    where
+        S: Into<String>,
+    {
+        let message_string = message.into();
+        let message = message_string.as_str();
+        let before = self.expect_eventually(message)?;
+        if !before.is_empty() {
+            std::fs::write("test-cli-expect-output.txt", before.clone())?;
+
+            // let mut output = std::fs::File::create("aaaaaaaaaaa.txt")?;
+            // write!(output, "{}", *before);
+        }
+        ensure!(
+            before.is_empty(),
+            format!(
+                "expected message \"{}\". but got \"{}\" first.",
+                message.replace('\n', "\\n").replace('\r', "\\r"),
+                before.replace('\n', "\\n").replace('\r', "\\r"),
+            ),
+        );
+        Ok(self)
+    }
+
+    fn exp_eof(&mut self) -> Result<String> {
+        match self
+            .rexpect_session
+            .exp_eof()
+            .context("expected end but got timed out")
+        {
+            Ok(before) => Ok(before),
+            Err(e) => {
+                for p in [51, 52, 53, 55, 56, 57] {
+                    let _ = relay::shutdown_relay(8000 + p);
+                }
+                Err(e)
+            }
+        }
+    }
+
+    pub fn expect_end(&mut self) -> Result<()> {
+        let before = self
+            .exp_eof()
+            .context("expected immediate end but got timed out")?;
+        ensure!(
+            before.is_empty(),
+            format!(
+                "expected immediate end but got '{}' first.",
+                before.replace('\n', "\\n").replace('\r', "\\r"),
+            ),
+        );
+        Ok(())
+    }
+
+    pub fn expect_end_with(&mut self, message: &str) -> Result<()> {
+        let before = self
+            .exp_eof()
+            .context("expected immediate end but got timed out")?;
+        assert_eq!(before, message);
+        Ok(())
+    }
+
+    pub fn expect_end_eventually_and_print(&mut self) -> Result<()> {
+        let before = self.exp_eof().context("expected end but got timed out")?;
+        println!("ended eventually with:");
+        println!("{}", &before);
+        Ok(())
+    }
+
+    pub fn expect_end_with_whitespace(&mut self) -> Result<()> {
+        let before = self
+            .exp_eof()
+            .context("expected immediate end but got timed out")?;
+        assert_eq!(before.trim(), "");
+        Ok(())
+    }
+
+    pub fn expect_end_eventually(&mut self) -> Result<String> {
+        self.exp_eof()
+            .context("expected end eventually but got timed out")
+    }
+
+    pub fn expect_end_eventually_with(&mut self, message: &str) -> Result<()> {
+        self.expect_eventually(message)?;
+        self.expect_end()
+    }
+
+    fn send_line(&mut self, line: &str) -> Result<()> {
+        self.rexpect_session
+            .send_line(line)
+            .context("send_line failed")?;
+        Ok(())
+    }
+
+    fn send(&mut self, s: &str) -> Result<()> {
+        self.rexpect_session.send(s).context("send failed")?;
+        self.rexpect_session.flush()?;
+        Ok(())
+    }
+}
+
+/// sanatize unicode string for rexpect
+fn sanatize(s: String) -> String {
+    // remove ansi codes as they don't work with rexpect
+    strip_str(s)
+        // sanatize unicode rexpect issue 105 is resolved https://github.com/rust-cli/rexpect/issues/105
+        .as_bytes()
+        .iter()
+        .map(|c| *c as char)
+        .collect::<String>()
+}
+
+pub fn rexpect_with<I, S>(args: I, timeout_ms: u64) -> Result<PtySession, rexpect::error::Error>
+where
+    I: IntoIterator<Item = S>,
+    S: AsRef<std::ffi::OsStr>,
+{
+    let mut cmd = std::process::Command::new(assert_cmd::cargo::cargo_bin("ngit"));
+    cmd.env("NGITTEST", "TRUE");
+    cmd.env("RUST_BACKTRACE", "0");
+    cmd.args(args);
+    // using branch for PR https://github.com/rust-cli/rexpect/pull/103 to strip ansi escape codes
+    rexpect::session::spawn_with_options(
+        cmd,
+        Options {
+            timeout_ms: Some(timeout_ms),
+            strip_ansi_escape_codes: true,
+        },
+    )
+}
+
+pub fn rexpect_with_from_dir<I, S>(
+    dir: &PathBuf,
+    args: I,
+    timeout_ms: u64,
+) -> Result<PtySession, rexpect::error::Error>
+where
+    I: IntoIterator<Item = S>,
+    S: AsRef<std::ffi::OsStr>,
+{
+    let mut cmd = std::process::Command::new(assert_cmd::cargo::cargo_bin("ngit"));
+    cmd.env("NGITTEST", "TRUE");
+    cmd.env("RUST_BACKTRACE", "0");
+    cmd.current_dir(dir);
+    cmd.args(args);
+    // using branch for PR https://github.com/rust-cli/rexpect/pull/103 to strip ansi escape codes
+    rexpect::session::spawn_with_options(
+        cmd,
+        Options {
+            timeout_ms: Some(timeout_ms),
+            strip_ansi_escape_codes: true,
+        },
+    )
+}
+
+/// backup and remove application config and data
+pub fn before() -> Result<()> {
+    backup_existing_config()
+}
+
+/// restore backuped application config and data
+pub fn after() -> Result<()> {
+    restore_config_backup()
+}
+
+/// run func between before and after scripts which backup, reset and restore
+/// application config
+///
+/// TODO: fix issue: if func panics, after() is not run.
+pub fn with_fresh_config<F>(func: F) -> Result<()>
+where
+    F: Fn() -> Result<()>,
+{
+    before()?;
+    func()?;
+    after()
+}
+
+fn backup_existing_config() -> Result<()> {
+    let config_path = get_dirs().config_dir().join("config.json");
+    let backup_config_path = get_dirs().config_dir().join("config-backup.json");
+    if config_path.exists() {
+        std::fs::rename(config_path, backup_config_path)?;
+    }
+    Ok(())
+}
+
+fn restore_config_backup() -> Result<()> {
+    let config_path = get_dirs().config_dir().join("config.json");
+    let backup_config_path = get_dirs().config_dir().join("config-backup.json");
+    if config_path.exists() {
+        std::fs::remove_file(&config_path)?;
+    }
+    if backup_config_path.exists() {
+        std::fs::rename(backup_config_path, config_path)?;
+    }
+    Ok(())
+}
+
+fn get_dirs() -> ProjectDirs {
+    ProjectDirs::from("", "CodeCollaboration", "ngit")
+        .expect("rust directories crate should return ProjectDirs")
+}
diff --git a/test_utils/src/relay.rs b/test_utils/src/relay.rs
new file mode 100644
index 00000000..82a8f8dd
--- /dev/null
+++ b/test_utils/src/relay.rs
@@ -0,0 +1,347 @@
+use std::collections::HashMap;
+
+use anyhow::{bail, Result};
+use nostr::{ClientMessage, JsonUtil, RelayMessage};
+
+use crate::CliTester;
+
+type ListenerEventFunc<'a> = &'a dyn Fn(&mut Relay, u64, nostr::Event) -> Result<()>;
+pub type ListenerReqFunc<'a> =
+    &'a dyn Fn(&mut Relay, u64, nostr::SubscriptionId, Vec<nostr::Filter>) -> Result<()>;
+
+pub struct Relay<'a> {
+    port: u16,
+    event_hub: simple_websockets::EventHub,
+    clients: HashMap<u64, simple_websockets::Responder>,
+    pub events: Vec<nostr::Event>,
+    pub reqs: Vec<Vec<nostr::Filter>>,
+    event_listener: Option<ListenerEventFunc<'a>>,
+    req_listener: Option<ListenerReqFunc<'a>>,
+}
+
+impl<'a> Relay<'a> {
+    pub fn new(
+        port: u16,
+        event_listener: Option<ListenerEventFunc<'a>>,
+        req_listener: Option<ListenerReqFunc<'a>>,
+    ) -> Self {
+        let event_hub = simple_websockets::launch(port)
+            .unwrap_or_else(|_| panic!("failed to listen on port {port}"));
+        Self {
+            port,
+            events: vec![],
+            reqs: vec![],
+            event_hub,
+            clients: HashMap::new(),
+            event_listener,
+            req_listener,
+        }
+    }
+    pub fn respond_ok(
+        &self,
+        client_id: u64,
+        event: nostr::Event,
+        error: Option<&str>,
+    ) -> Result<bool> {
+        let responder = self.clients.get(&client_id).unwrap();
+
+        let ok_json = RelayMessage::Ok {
+            event_id: event.id,
+            status: error.is_none(),
+            message: error.unwrap_or("").to_string(),
+        }
+        .as_json();
+        // bail!(format!("{}", &ok_json));
+        Ok(responder.send(simple_websockets::Message::Text(ok_json)))
+    }
+
+    pub fn respond_eose(
+        &self,
+        client_id: u64,
+        subscription_id: nostr::SubscriptionId,
+    ) -> Result<bool> {
+        let responder = self.clients.get(&client_id).unwrap();
+
+        Ok(responder.send(simple_websockets::Message::Text(
+            RelayMessage::EndOfStoredEvents(subscription_id).as_json(),
+        )))
+    }
+
+    /// send events and eose
+    pub fn respond_events(
+        &self,
+        client_id: u64,
+        subscription_id: &nostr::SubscriptionId,
+        events: &Vec<nostr::Event>,
+    ) -> Result<bool> {
+        let responder = self.clients.get(&client_id).unwrap();
+
+        for event in events {
+            let res = responder.send(simple_websockets::Message::Text(
+                RelayMessage::Event {
+                    subscription_id: subscription_id.clone(),
+                    event: Box::new(event.clone()),
+                }
+                .as_json(),
+            ));
+            if !res {
+                return Ok(false);
+            }
+        }
+        self.respond_eose(client_id, subscription_id.clone())
+    }
+
+    /// send collected events, filtered by filters, and eose
+    pub fn respond_standard_req(
+        &self,
+        client_id: u64,
+        subscription_id: &nostr::SubscriptionId,
+        // TODO: enable filters
+        _filters: &[nostr::Filter],
+    ) -> Result<bool> {
+        // let t: Vec<nostr::Kind> = self.events.iter().map(|e| e.kind).collect();
+        // .filter(|e| filters.iter().any(|filter| filter.match_event(e)))
+        // println!("letsgo{:?}", t);
+
+        self.respond_events(
+            client_id,
+            subscription_id,
+            &self
+                .events
+                .iter()
+                // FIXME:
+                // `filter.match_events` does not exist anymore
+                // it has been moved to `nostr_database_::FilterIndex`
+                // but it's private now
+                // .filter(|e| filters.iter().any(|filter|filter.match_event(e)))
+                .filter(|_| true)
+                .cloned()
+                .collect(),
+        )
+    }
+    /// listen, collect events and responds with event_listener to events or
+    /// Ok(eventid) if event_listner is None
+    pub async fn listen_until_close(&mut self) -> Result<()> {
+        loop {
+            println!("{} polling", self.port);
+            match self.event_hub.poll_async().await {
+                simple_websockets::Event::Connect(client_id, responder) => {
+                    // add their Responder to our `clients` map:
+                    self.clients.insert(client_id, responder);
+                }
+                simple_websockets::Event::Disconnect(client_id) => {
+                    // remove the disconnected client from the clients map:
+                    println!("{} disconnected", self.port);
+                    self.clients.remove(&client_id);
+                    // break;
+                }
+                simple_websockets::Event::Message(client_id, message) => {
+                    // println!("bla{:?}", &message);
+
+                    println!(
+                        "{} Received a message from client #{}: {:?}",
+                        self.port, client_id, message
+                    );
+                    if let simple_websockets::Message::Text(s) = message.clone() {
+                        if s.eq("shut me down") {
+                            println!("{} recieved shut me down", self.port);
+                            break;
+                        }
+                    }
+                    // println!("{:?}", &message);
+                    if let Ok(event) = get_nevent(&message) {
+                        // println!("{:?}", &event);
+                        // let t: Vec<nostr::Kind> = self.events.iter().map(|e| e.kind).collect();
+                        // println!("before{:?}", t);
+                        self.events.push(event.clone());
+                        // let t: Vec<nostr::Kind> = self.events.iter().map(|e| e.kind).collect();
+                        // println!("after{:?}", t);
+
+                        if let Some(listner) = self.event_listener {
+                            listner(self, client_id, event)?;
+                        } else {
+                            self.respond_ok(client_id, event, None)?;
+                        }
+                    }
+
+                    if let Ok((subscription_id, filters)) = get_nreq(&message) {
+                        self.reqs.push(filters.clone());
+                        if let Some(listner) = self.req_listener {
+                            listner(self, client_id, subscription_id, filters)?;
+                        } else {
+                            self.respond_standard_req(client_id, &subscription_id, &filters)?;
+                            // self.respond_eose(client_id, subscription_id)?;
+                        }
+                        // respond with events
+                        // respond with EOSE
+                    }
+                    if is_nclose(&message) {
+                        println!("{} recieved nostr close", self.port);
+                        // break;
+                    }
+                }
+            }
+        }
+        println!(
+            "{} stop polling. we may not be polling but the tcplistner is still listening",
+            self.port
+        );
+        Ok(())
+    }
+}
+
+pub fn shutdown_relay(port: u64) -> Result<()> {
+    let mut counter = 0;
+    while let Ok((mut socket, _)) = tungstenite::connect(format!("ws://localhost:{}", port)) {
+        counter += 1;
+        if counter == 1 {
+            socket.write(tungstenite::Message::text("shut me down"))?;
+        }
+        socket.close(None)?;
+    }
+    Ok(())
+}
+
+fn get_nevent(message: &simple_websockets::Message) -> Result<nostr::Event> {
+    if let simple_websockets::Message::Text(s) = message.clone() {
+        let cm_result = ClientMessage::from_json(s);
+        if let Ok(ClientMessage::Event(event)) = cm_result {
+            let e = *event;
+            return Ok(e.clone());
+        }
+    }
+    bail!("not nostr event")
+}
+
+fn get_nreq(
+    message: &simple_websockets::Message,
+) -> Result<(nostr::SubscriptionId, Vec<nostr::Filter>)> {
+    if let simple_websockets::Message::Text(s) = message.clone() {
+        let cm_result = ClientMessage::from_json(s);
+        if let Ok(ClientMessage::Req {
+            subscription_id,
+            filters,
+        }) = cm_result
+        {
+            return Ok((subscription_id, filters));
+        }
+    }
+    bail!("not nostr event")
+}
+
+fn is_nclose(message: &simple_websockets::Message) -> bool {
+    if let simple_websockets::Message::Text(s) = message.clone() {
+        let cm_result = ClientMessage::from_json(s);
+        if let Ok(ClientMessage::Close(_)) = cm_result {
+            return true;
+        }
+    }
+    false
+}
+
+pub enum Message {
+    Event,
+    // Request,
+}
+
+/// leaves trailing whitespace and only compatible with --no-cli-spinners flag
+/// relays tuple: (title,successful,message)
+pub fn expect_send_with_progress(
+    p: &mut CliTester,
+    relays: Vec<(&str, bool, &str)>,
+    event_count: u16,
+) -> Result<()> {
+    p.expect(format!(
+        " - {} -------------------- 0/{event_count}",
+        &relays[0].0
+    ))?;
+    let last_relay_outcome = outcome_message(relays.last().unwrap());
+    let mut s = String::new();
+    loop {
+        s.push_str(&p.expect_eventually(&last_relay_outcome)?);
+        s.push_str(&last_relay_outcome);
+        if relays.iter().all(|r| s.contains(&outcome_message(r))) {
+            // all responses have been received with correct outcome
+            break;
+        }
+    }
+    Ok(())
+}
+
+fn outcome_message(relay: &(&str, bool, &str)) -> String {
+    if relay.1 {
+        format!(" y {}", relay.0)
+    } else {
+        format!(" x {} {}", relay.0, relay.2)
+    }
+}
+
+pub fn expect_send_with_progress_exact_interaction(
+    p: &mut CliTester,
+    titles: Vec<&str>,
+    count: u16,
+) -> Result<()> {
+    let whitespace_mid = " \r\n";
+    let whitespace_end = "                   \r\r\r";
+
+    p.expect(format!(
+        " - {} -------------------- 0/{count}        \r",
+        titles[0]
+    ))?;
+    p.expect(format!(
+        " - {} -------------------- 0/{count}{whitespace_mid}",
+        titles[0]
+    ))?;
+    p.expect(format!(
+        " - {} -------------------- 0/{count}                     \r\r",
+        titles[1]
+    ))?;
+
+    let generate_text = |title: &str, num: u16, confirmed_complete: bool| -> String {
+        let symbol = if confirmed_complete && num.eq(&count) {
+            "â"
+        } else {
+            "-"
+        };
+        let bar = match (num, count) {
+            (0, _) => "--------------------",
+            (1, 2) => "###########---------",
+            (x, y) => {
+                if x.eq(&y) {
+                    "####################"
+                } else {
+                    "--unknown--"
+                }
+            }
+        };
+        format!(
+            " {symbol} {title} {bar} {num}/{count}{}",
+            if (&title).eq(titles.last().unwrap()) {
+                whitespace_end
+            } else {
+                whitespace_mid
+            }
+        )
+    };
+    let mut nums: HashMap<&str, u16> = HashMap::new();
+    for title in &titles {
+        nums.insert(title, 0);
+        p.expect(generate_text(title, 0, false))?;
+    }
+    loop {
+        for selected_title in &titles {
+            for title in &titles {
+                if title.eq(selected_title) {
+                    let new_num = nums.get(title).unwrap() + 1;
+                    if new_num.gt(&count) {
+                        return Ok(());
+                    }
+                    nums.insert(title, new_num);
+                    p.expect(generate_text(title, *nums.get(title).unwrap(), false))?;
+                } else {
+                    p.expect(generate_text(title, *nums.get(title).unwrap(), true))?;
+                }
+            }
+        }
+    }
+}
RandyMcMillan added a commit to RandyMcMillan/gnostr that referenced this pull request Jan 9, 2025
…interactor.rs

index 4cf6357f..0b9c5f86 100644
--- a/crates/ngit/src/cli_interactor.rs
+++ b/crates/ngit/src/cli_interactor.rs
@@ -1,5 +1,5 @@
 use anyhow::{Context, Result};
-use dialoguer::{theme::ColorfulTheme, Confirm, Input, Password};
+use dialoguer::{Confirm, Input, Password, theme::ColorfulTheme};
 #[cfg(test)]
 use mockall::*;

diff --git a/crates/ngit/src/client.rs b/crates/ngit/src/client.rs
index 56f0e168..5eeac469 100644
--- a/crates/ngit/src/client.rs
+++ b/crates/ngit/src/client.rs
@@ -12,7 +12,7 @@
 // want to inadvertlty use other features of nightly that might be removed.
 use std::{fmt::Write, time::Duration};

-use anyhow::{bail, Context, Result};
+use anyhow::{Context, Result, bail};
 use async_trait::async_trait;
 use futures::stream::{self, StreamExt};
 use indicatif::{MultiProgress, ProgressBar, ProgressState, ProgressStyle};
diff --git a/crates/ngit/src/config.rs b/crates/ngit/src/config.rs
index 7fca446b..9b7d431b 100644
--- a/crates/ngit/src/config.rs
+++ b/crates/ngit/src/config.rs
@@ -1,6 +1,6 @@
 use std::{fs::File, io::BufReader};

-use anyhow::{anyhow, Context, Result};
+use anyhow::{Context, Result, anyhow};
 use directories::ProjectDirs;
 #[cfg(test)]
 use mockall::*;
diff --git a/crates/ngit/src/git.rs b/crates/ngit/src/git.rs
index fb3b3537..61c57dd8 100644
--- a/crates/ngit/src/git.rs
+++ b/crates/ngit/src/git.rs
@@ -2,9 +2,9 @@
 use std::path::PathBuf;
 use std::{env::current_dir, path::Path};

-use anyhow::{bail, Context, Result};
+use anyhow::{Context, Result, bail};
 use git2::{DiffOptions, Oid, Revwalk};
-use nostr_sdk::hashes::{sha1::Hash as Sha1Hash, Hash};
+use nostr_sdk::hashes::{Hash, sha1::Hash as Sha1Hash};

 use crate::sub_commands::list::{get_commit_id_from_patch, tag_value};

@@ -790,7 +790,7 @@ fn extract_sig_from_patch_tags<'a>(
 mod tests {
     use std::fs;

-    use test_utils::{generate_repo_ref_event, git::GitTestRepo, TEST_KEY_1_KEYS};
+    use test_utils::{TEST_KEY_1_KEYS, generate_repo_ref_event, git::GitTestRepo};

     use super::*;

@@ -1300,10 +1300,10 @@ mod tests {
                     &oid_to_sha1(&feature_oid),
                 )?;
                 assert_eq!(ahead, vec![]);
-                assert_eq!(
-                    behind,
-                    vec![oid_to_sha1(&behind_2_oid), oid_to_sha1(&behind_1_oid),],
-                );
+                assert_eq!(behind, vec![
+                    oid_to_sha1(&behind_2_oid),
+                    oid_to_sha1(&behind_1_oid),
+                ],);
                 Ok(())
             }

@@ -1325,10 +1325,10 @@ mod tests {
                     &oid_to_sha1(&main_oid),
                     &oid_to_sha1(&ahead_2_oid),
                 )?;
-                assert_eq!(
-                    ahead,
-                    vec![oid_to_sha1(&ahead_2_oid), oid_to_sha1(&ahead_1_oid),],
-                );
+                assert_eq!(ahead, vec![
+                    oid_to_sha1(&ahead_2_oid),
+                    oid_to_sha1(&ahead_1_oid),
+                ],);
                 assert_eq!(behind, vec![]);
                 Ok(())
             }
@@ -1357,14 +1357,14 @@ mod tests {
                     &oid_to_sha1(&behind_2_oid),
                     &oid_to_sha1(&ahead_2_oid),
                 )?;
-                assert_eq!(
-                    ahead,
-                    vec![oid_to_sha1(&ahead_2_oid), oid_to_sha1(&ahead_1_oid)],
-                );
-                assert_eq!(
-                    behind,
-                    vec![oid_to_sha1(&behind_2_oid), oid_to_sha1(&behind_1_oid)],
-                );
+                assert_eq!(ahead, vec![
+                    oid_to_sha1(&ahead_2_oid),
+                    oid_to_sha1(&ahead_1_oid)
+                ],);
+                assert_eq!(behind, vec![
+                    oid_to_sha1(&behind_2_oid),
+                    oid_to_sha1(&behind_1_oid)
+                ],);
                 Ok(())
             }
         }
@@ -2026,10 +2026,9 @@ mod tests {
                 test_repo.populate_with_test_branch()?;
                 test_repo.checkout("main")?;

-                assert_eq!(
-                    git_repo.parse_starting_commits("HEAD~1")?,
-                    vec![str_to_sha1("431b84edc0d2fa118d63faa3c2db9c73d630a5ae")?],
-                );
+                assert_eq!(git_repo.parse_starting_commits("HEAD~1")?, vec![
+                    str_to_sha1("431b84edc0d2fa118d63faa3c2db9c73d630a5ae")?
+                ],);
                 Ok(())
             }

@@ -2039,10 +2038,9 @@ mod tests {
                 let git_repo = Repo::from_path(&test_repo.dir)?;
                 test_repo.populate_with_test_branch()?;

-                assert_eq!(
-                    git_repo.parse_starting_commits("HEAD~1")?,
-                    vec![str_to_sha1("82ff2bcc9aa94d1bd8faee723d4c8cc190d6061c")?],
-                );
+                assert_eq!(git_repo.parse_starting_commits("HEAD~1")?, vec![
+                    str_to_sha1("82ff2bcc9aa94d1bd8faee723d4c8cc190d6061c")?
+                ],);
                 Ok(())
             }
         }
@@ -2056,13 +2054,10 @@ mod tests {
                 test_repo.populate_with_test_branch()?;
                 test_repo.checkout("main")?;

-                assert_eq!(
-                    git_repo.parse_starting_commits("HEAD~2")?,
-                    vec![
-                        str_to_sha1("431b84edc0d2fa118d63faa3c2db9c73d630a5ae")?,
-                        str_to_sha1("af474d8d271490e5c635aad337abdc050034b16a")?,
-                    ],
-                );
+                assert_eq!(git_repo.parse_starting_commits("HEAD~2")?, vec![
+                    str_to_sha1("431b84edc0d2fa118d63faa3c2db9c73d630a5ae")?,
+                    str_to_sha1("af474d8d271490e5c635aad337abdc050034b16a")?,
+                ],);
                 Ok(())
             }
         }
@@ -2075,14 +2070,11 @@ mod tests {
                 let git_repo = Repo::from_path(&test_repo.dir)?;
                 test_repo.populate_with_test_branch()?;

-                assert_eq!(
-                    git_repo.parse_starting_commits("HEAD~3")?,
-                    vec![
-                        str_to_sha1("82ff2bcc9aa94d1bd8faee723d4c8cc190d6061c")?,
-                        str_to_sha1("a23e6b05aaeb7d1471b4a838b51f337d5644eeb0")?,
-                        str_to_sha1("7ab82116068982671a8111f27dc10599172334b2")?,
-                    ],
-                );
+                assert_eq!(git_repo.parse_starting_commits("HEAD~3")?, vec![
+                    str_to_sha1("82ff2bcc9aa94d1bd8faee723d4c8cc190d6061c")?,
+                    str_to_sha1("a23e6b05aaeb7d1471b4a838b51f337d5644eeb0")?,
+                    str_to_sha1("7ab82116068982671a8111f27dc10599172334b2")?,
+                ],);
                 Ok(())
             }
         }
@@ -2096,14 +2088,11 @@ mod tests {
                 test_repo.populate_with_test_branch()?;
                 test_repo.checkout("main")?;

-                assert_eq!(
-                    git_repo.parse_starting_commits("af474d8..a23e6b0")?,
-                    vec![
-                        str_to_sha1("a23e6b05aaeb7d1471b4a838b51f337d5644eeb0")?,
-                        str_to_sha1("7ab82116068982671a8111f27dc10599172334b2")?,
-                        str_to_sha1("431b84edc0d2fa118d63faa3c2db9c73d630a5ae")?,
-                    ],
-                );
+                assert_eq!(git_repo.parse_starting_commits("af474d8..a23e6b0")?, vec![
+                    str_to_sha1("a23e6b05aaeb7d1471b4a838b51f337d5644eeb0")?,
+                    str_to_sha1("7ab82116068982671a8111f27dc10599172334b2")?,
+                    str_to_sha1("431b84edc0d2fa118d63faa3c2db9c73d630a5ae")?,
+                ],);
                 Ok(())
             }
         }
diff --git a/crates/ngit/src/key_handling/encryption.rs b/crates/ngit/src/key_handling/encryption.rs
index 3f4ee41f..38f03e1f 100644
--- a/crates/ngit/src/key_handling/encryption.rs
+++ b/crates/ngit/src/key_handling/encryption.rs
@@ -1,7 +1,7 @@
 use anyhow::Result;
 #[cfg(test)]
 use mockall::*;
-use nostr::{prelude::*, Keys};
+use nostr::{Keys, prelude::*};

 #[derive(Default)]
 pub struct Encryptor;
diff --git a/crates/ngit/src/key_handling/users.rs b/crates/ngit/src/key_handling/users.rs
index a79a9779..ac2f26c5 100644
--- a/crates/ngit/src/key_handling/users.rs
+++ b/crates/ngit/src/key_handling/users.rs
@@ -185,19 +185,16 @@ impl UserManagement for UserManager {
             }

             let events: Vec<Event> = match client
-                .get_events(
-                    relays_to_search,
-                    vec![
-                        nostr::Filter::default()
-                            .author(*public_key)
-                            .since(nostr::Timestamp::from(user_ref.metadata.created_at + 1))
-                            .kind(Kind::Metadata),
-                        nostr::Filter::default()
-                            .author(*public_key)
-                            .since(nostr::Timestamp::from(user_ref.relays.created_at + 1))
-                            .kind(Kind::RelayList),
-                    ],
-                )
+                .get_events(relays_to_search, vec![
+                    nostr::Filter::default()
+                        .author(*public_key)
+                        .since(nostr::Timestamp::from(user_ref.metadata.created_at + 1))
+                        .kind(Kind::Metadata),
+                    nostr::Filter::default()
+                        .author(*public_key)
+                        .since(nostr::Timestamp::from(user_ref.relays.created_at + 1))
+                        .kind(Kind::RelayList),
+                ])
                 .await
             {
                 Ok(events) => events,
@@ -563,47 +560,39 @@ mod tests {
         use crate::client::MockConnect;

         fn generate_relaylist_event() -> nostr::Event {
-            nostr::event::EventBuilder::new(
-                nostr::Kind::RelayList,
-                "",
-                [
-                    nostr::Tag::from_standardized(nostr::TagStandard::RelayMetadata {
-                        relay_url: nostr::Url::from_str("wss://fredswrite1.relay/").unwrap(),
-                        metadata: Some(RelayMetadata::Write),
-                    }),
-                    nostr::Tag::from_standardized(nostr::TagStandard::RelayMetadata {
-                        relay_url: nostr::Url::from_str("wss://fredsread1.relay/").unwrap(),
-                        metadata: Some(RelayMetadata::Read),
-                    }),
-                    nostr::Tag::from_standardized(nostr::TagStandard::RelayMetadata {
-                        relay_url: nostr::Url::from_str("wss://fredsreadwrite.relay/").unwrap(),
-                        metadata: None,
-                    }),
-                ],
-            )
+            nostr::event::EventBuilder::new(nostr::Kind::RelayList, "", [
+                nostr::Tag::from_standardized(nostr::TagStandard::RelayMetadata {
+                    relay_url: nostr::Url::from_str("wss://fredswrite1.relay/").unwrap(),
+                    metadata: Some(RelayMetadata::Write),
+                }),
+                nostr::Tag::from_standardized(nostr::TagStandard::RelayMetadata {
+                    relay_url: nostr::Url::from_str("wss://fredsread1.relay/").unwrap(),
+                    metadata: Some(RelayMetadata::Read),
+                }),
+                nostr::Tag::from_standardized(nostr::TagStandard::RelayMetadata {
+                    relay_url: nostr::Url::from_str("wss://fredsreadwrite.relay/").unwrap(),
+                    metadata: None,
+                }),
+            ])
             .to_event(&TEST_KEY_1_KEYS)
             .unwrap()
         }

         fn generate_relaylist_event_user_2() -> nostr::Event {
-            nostr::event::EventBuilder::new(
-                nostr::Kind::RelayList,
-                "",
-                [
-                    nostr::Tag::from_standardized(nostr::TagStandard::RelayMetadata {
-                        relay_url: nostr::Url::from_str("wss://carolswrite1.relay/").unwrap(),
-                        metadata: Some(RelayMetadata::Write),
-                    }),
-                    nostr::Tag::from_standardized(nostr::TagStandard::RelayMetadata {
-                        relay_url: nostr::Url::from_str("wss://carolsread1.relay/").unwrap(),
-                        metadata: Some(RelayMetadata::Read),
-                    }),
-                    nostr::Tag::from_standardized(nostr::TagStandard::RelayMetadata {
-                        relay_url: nostr::Url::from_str("wss://carolsreadwrite.relay/").unwrap(),
-                        metadata: None,
-                    }),
-                ],
-            )
+            nostr::event::EventBuilder::new(nostr::Kind::RelayList, "", [
+                nostr::Tag::from_standardized(nostr::TagStandard::RelayMetadata {
+                    relay_url: nostr::Url::from_str("wss://carolswrite1.relay/").unwrap(),
+                    metadata: Some(RelayMetadata::Write),
+                }),
+                nostr::Tag::from_standardized(nostr::TagStandard::RelayMetadata {
+                    relay_url: nostr::Url::from_str("wss://carolsread1.relay/").unwrap(),
+                    metadata: Some(RelayMetadata::Read),
+                }),
+                nostr::Tag::from_standardized(nostr::TagStandard::RelayMetadata {
+                    relay_url: nostr::Url::from_str("wss://carolsreadwrite.relay/").unwrap(),
+                    metadata: None,
+                }),
+            ])
             .to_event(&TEST_KEY_2_KEYS)
             .unwrap()
         }
diff --git a/crates/ngit/src/login.rs b/crates/ngit/src/login.rs
index 4cdf3c1b..e290c3eb 100644
--- a/crates/ngit/src/login.rs
+++ b/crates/ngit/src/login.rs
@@ -1,6 +1,6 @@
 use std::str::FromStr;

-use anyhow::{bail, Context, Result};
+use anyhow::{Context, Result, bail};
 use nostr::PublicKey;
 use zeroize::Zeroize;

diff --git a/crates/ngit/src/repo_ref.rs b/crates/ngit/src/repo_ref.rs
index 9fb1f0c4..3188a5d1 100644
--- a/crates/ngit/src/repo_ref.rs
+++ b/crates/ngit/src/repo_ref.rs
@@ -1,7 +1,7 @@
 use std::{fs::File, io::BufReader, str::FromStr};

-use anyhow::{bail, Context, Result};
-use nostr::{nips::nip19::Nip19, FromBech32, PublicKey, Tag, TagStandard, ToBech32};
+use anyhow::{Context, Result, bail};
+use nostr::{FromBech32, PublicKey, Tag, TagStandard, ToBech32, nips::nip19::Nip19};
 use serde::{Deserialize, Serialize};

 #[cfg(not(test))]
@@ -289,13 +289,10 @@ pub fn save_repo_config_to_yaml(
                 .context("cannot convert public key into npub")?,
         );
     }
-    serde_yaml::to_writer(
-        file,
-        &RepoConfigYaml {
-            maintainers: maintainers_npubs,
-            relays,
-        },
-    )
+    serde_yaml::to_writer(file, &RepoConfigYaml {
+        maintainers: maintainers_npubs,
+        relays,
+    })
     .context("cannot write maintainers to maintainers.yaml file serde_yaml")
 }

@@ -400,37 +397,33 @@ mod tests {

         #[test]
         fn git_server() {
-            assert_eq!(
-                RepoRef::try_from(create()).unwrap().git_server,
-                vec!["https://localhost:1000"],
-            )
+            assert_eq!(RepoRef::try_from(create()).unwrap().git_server, vec![
+                "https://localhost:1000"
+            ],)
         }

         #[test]
         fn web() {
-            assert_eq!(
-                RepoRef::try_from(create()).unwrap().web,
-                vec![
-                    "https://exampleproject.xyz".to_string(),
-                    "https://gitworkshop.dev/123".to_string()
-                ],
-            )
+            assert_eq!(RepoRef::try_from(create()).unwrap().web, vec![
+                "https://exampleproject.xyz".to_string(),
+                "https://gitworkshop.dev/123".to_string()
+            ],)
         }

         #[test]
         fn relays() {
-            assert_eq!(
-                RepoRef::try_from(create()).unwrap().relays,
-                vec!["ws://relay1.io".to_string(), "ws://relay2.io".to_string()],
-            )
+            assert_eq!(RepoRef::try_from(create()).unwrap().relays, vec![
+                "ws://relay1.io".to_string(),
+                "ws://relay2.io".to_string()
+            ],)
         }

         #[test]
         fn maintainers() {
-            assert_eq!(
-                RepoRef::try_from(create()).unwrap().maintainers,
-                vec![TEST_KEY_1_KEYS.public_key(), TEST_KEY_2_KEYS.public_key()],
-            )
+            assert_eq!(RepoRef::try_from(create()).unwrap().maintainers, vec![
+                TEST_KEY_1_KEYS.public_key(),
+                TEST_KEY_2_KEYS.public_key()
+            ],)
         }
     }

diff --git a/crates/ngit/src/sub_commands/init.rs b/crates/ngit/src/sub_commands/init.rs
index ac81096c..b7506339 100644
--- a/crates/ngit/src/sub_commands/init.rs
+++ b/crates/ngit/src/sub_commands/init.rs
@@ -7,12 +7,12 @@ use crate::client::Client;
 #[cfg(test)]
 use crate::client::MockConnect;
 use crate::{
+    Cli,
     cli_interactor::{Interactor, InteractorPrompt, PromptInputParms},
     client::Connect,
     git::{Repo, RepoActions},
     login,
-    repo_ref::{self, extract_pks, get_repo_config_from_yaml, save_repo_config_to_yaml, RepoRef},
-    Cli,
+    repo_ref::{self, RepoRef, extract_pks, get_repo_config_from_yaml, save_repo_config_to_yaml},
 };

 #[derive(Debug, clap::Args)]
diff --git a/crates/ngit/src/sub_commands/list.rs b/crates/ngit/src/sub_commands/list.rs
index 24979fe0..cdc83912 100644
--- a/crates/ngit/src/sub_commands/list.rs
+++ b/crates/ngit/src/sub_commands/list.rs
@@ -1,6 +1,6 @@
 use std::{io::Write, ops::Add};

-use anyhow::{bail, Context, Result};
+use anyhow::{Context, Result, bail};

 use super::send::event_is_patch_set_root;
 #[cfg(not(test))]
@@ -10,11 +10,11 @@ use crate::client::MockConnect;
 use crate::{
     cli_interactor::{Interactor, InteractorPrompt, PromptChoiceParms, PromptConfirmParms},
     client::Connect,
-    git::{str_to_sha1, Repo, RepoActions},
-    repo_ref::{self, RepoRef, REPO_REF_KIND},
+    git::{Repo, RepoActions, str_to_sha1},
+    repo_ref::{self, REPO_REF_KIND, RepoRef},
     sub_commands::send::{
-        commit_msg_from_patch_oneliner, event_is_cover_letter, event_is_revision_root,
-        event_to_cover_letter, patch_supports_commit_ids, PATCH_KIND,
+        PATCH_KIND, commit_msg_from_patch_oneliner, event_is_cover_letter, event_is_revision_root,
+        event_to_cover_letter, patch_supports_commit_ids,
     },
 };

@@ -725,33 +725,28 @@ pub async fn find_proposal_events(
     root_commit: &str,
 ) -> Result<Vec<nostr::Event>> {
     let mut proposals = client
-        .get_events(
-            repo_ref.relays.clone(),
-            vec![
-                nostr::Filter::default()
-                    .kind(nostr::Kind::Custom(PATCH_KIND))
-                    .custom_tag(
-                        nostr::SingleLetterTag::lowercase(nostr::Alphabet::T),
-                        vec!["root"],
-                    )
-                    .custom_tag(
-                        nostr::SingleLetterTag::lowercase(nostr::Alphabet::A),
-                        repo_ref
-                            .maintainers
-                            .iter()
-                            .map(|m| format!("{REPO_REF_KIND}:{m}:{}", repo_ref.identifier)),
-                    ),
-                // also pick up proposals from the same repo but no target at our maintainers repo
-                // events
-                nostr::Filter::default()
-                    .kind(nostr::Kind::Custom(PATCH_KIND))
-                    .custom_tag(
-                        nostr::SingleLetterTag::lowercase(nostr::Alphabet::T),
-                        vec!["root"],
-                    )
-                    .reference(root_commit),
-            ],
-        )
+        .get_events(repo_ref.relays.clone(), vec![
+            nostr::Filter::default()
+                .kind(nostr::Kind::Custom(PATCH_KIND))
+                .custom_tag(nostr::SingleLetterTag::lowercase(nostr::Alphabet::T), vec![
+                    "root",
+                ])
+                .custom_tag(
+                    nostr::SingleLetterTag::lowercase(nostr::Alphabet::A),
+                    repo_ref
+                        .maintainers
+                        .iter()
+                        .map(|m| format!("{REPO_REF_KIND}:{m}:{}", repo_ref.identifier)),
+                ),
+            // also pick up proposals from the same repo but no target at our maintainers repo
+            // events
+            nostr::Filter::default()
+                .kind(nostr::Kind::Custom(PATCH_KIND))
+                .custom_tag(nostr::SingleLetterTag::lowercase(nostr::Alphabet::T), vec![
+                    "root",
+                ])
+                .reference(root_commit),
+        ])
         .await
         .context("cannot get proposal events")?
         .iter()
@@ -784,19 +779,16 @@ pub async fn find_commits_for_proposal_root_events(
     repo_ref: &RepoRef,
 ) -> Result<Vec<nostr::Event>> {
     let mut patch_events: Vec<nostr::Event> = client
-        .get_events(
-            repo_ref.relays.clone(),
-            vec![
-                nostr::Filter::default()
-                    .kind(nostr::Kind::Custom(PATCH_KIND))
-                    .events(
-                        proposal_root_events
-                            .iter()
-                            .map(|e| e.id)
-                            .collect::<Vec<nostr::EventId>>(),
-                    ),
-            ],
-        )
+        .get_events(repo_ref.relays.clone(), vec![
+            nostr::Filter::default()
+                .kind(nostr::Kind::Custom(PATCH_KIND))
+                .events(
+                    proposal_root_events
+                        .iter()
+                        .map(|e| e.id)
+                        .collect::<Vec<nostr::EventId>>(),
+                ),
+        ])
         .await
         .context("cannot fetch patch events")?
         .iter()
diff --git a/crates/ngit/src/sub_commands/login.rs b/crates/ngit/src/sub_commands/login.rs
index 1bd19c1f..5c054e70 100644
--- a/crates/ngit/src/sub_commands/login.rs
+++ b/crates/ngit/src/sub_commands/login.rs
@@ -5,7 +5,7 @@ use clap;
 use crate::client::Client;
 #[cfg(test)]
 use crate::client::MockConnect;
-use crate::{client::Connect, login, Cli};
+use crate::{Cli, client::Connect, login};

 #[derive(Debug, clap::Args)]
 pub struct LoginSubCommandArgs {
diff --git a/crates/ngit/src/sub_commands/pull.rs b/crates/ngit/src/sub_commands/pull.rs
index 716e6c53..aa791368 100644
--- a/crates/ngit/src/sub_commands/pull.rs
+++ b/crates/ngit/src/sub_commands/pull.rs
@@ -1,4 +1,4 @@
-use anyhow::{bail, Context, Result};
+use anyhow::{Context, Result, bail};

 use super::list::{get_commit_id_from_patch, tag_value};
 #[cfg(not(test))]
@@ -7,7 +7,7 @@ use crate::client::Client;
 use crate::client::MockConnect;
 use crate::{
     client::Connect,
-    git::{str_to_sha1, Repo, RepoActions},
+    git::{Repo, RepoActions, str_to_sha1},
     repo_ref,
     sub_commands::{
         list::get_most_recent_patch_with_ancestors,
diff --git a/crates/ngit/src/sub_commands/push.rs b/crates/ngit/src/sub_commands/push.rs
index 6b681e3f..d4266c03 100644
--- a/crates/ngit/src/sub_commands/push.rs
+++ b/crates/ngit/src/sub_commands/push.rs
@@ -1,4 +1,4 @@
-use anyhow::{bail, Context, Result};
+use anyhow::{Context, Result, bail};
 use nostr_sdk::hashes::sha1::Hash as Sha1Hash;

 #[cfg(not(test))]
@@ -6,8 +6,9 @@ use crate::client::Client;
 #[cfg(test)]
 use crate::client::MockConnect;
 use crate::{
+    Cli,
     client::Connect,
-    git::{str_to_sha1, Repo, RepoActions},
+    git::{Repo, RepoActions, str_to_sha1},
     login,
     repo_ref::{self, RepoRef},
     sub_commands::{
@@ -18,7 +19,6 @@ use crate::{
         },
         send::{event_is_revision_root, event_to_cover_letter, generate_patch_event, send_events},
     },
-    Cli,
 };

 #[derive(Debug, clap::Args)]
@@ -104,16 +104,13 @@ pub async fn launch(cli_args: &Cli, args: &PushSubCommandArgs) -> Result<()> {

     if args.force {
         println!("preparing to force push proposal revision...");
-        sub_commands::send::launch(
-            cli_args,
-            &sub_commands::send::SendSubCommandArgs {
-                since_or_range: String::new(),
-                in_reply_to: vec![proposal_root_event.id.to_string()],
-                title: None,
-                description: None,
-                no_cover_letter: args.no_cover_letter,
-            },
-        )
+        sub_commands::send::launch(cli_args, &sub_commands::send::SendSubCommandArgs {
+            since_or_range: String::new(),
+            in_reply_to: vec![proposal_root_event.id.to_string()],
+            title: None,
+            description: None,
+            no_cover_letter: args.no_cover_letter,
+        })
         .await?;
         println!("force pushed proposal revision");
         return Ok(());
diff --git a/crates/ngit/src/sub_commands/send.rs b/crates/ngit/src/sub_commands/send.rs
index 5246819a..45b4c62e 100644
--- a/crates/ngit/src/sub_commands/send.rs
+++ b/crates/ngit/src/sub_commands/send.rs
@@ -1,14 +1,14 @@
 use std::{str::FromStr, time::Duration};

-use anyhow::{bail, Context, Result};
+use anyhow::{Context, Result, bail};
 use console::Style;
 use futures::future::join_all;
 use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
 use nostr::{
-    nips::{nip01::Coordinate, nip10::Marker, nip19::Nip19},
     EventBuilder, FromBech32, Tag, TagKind, ToBech32, UncheckedUrl,
+    nips::{nip01::Coordinate, nip10::Marker, nip19::Nip19},
 };
-use nostr_sdk::{hashes::sha1::Hash as Sha1Hash, TagStandard};
+use nostr_sdk::{TagStandard, hashes::sha1::Hash as Sha1Hash};

 use super::list::tag_value;
 #[cfg(not(test))]
@@ -16,14 +16,14 @@ use crate::client::Client;
 #[cfg(test)]
 use crate::client::MockConnect;
 use crate::{
+    Cli,
     cli_interactor::{
         Interactor, InteractorPrompt, PromptConfirmParms, PromptInputParms, PromptMultiChoiceParms,
     },
     client::Connect,
     git::{Repo, RepoActions},
     login,
-    repo_ref::{self, RepoRef, REPO_REF_KIND},
-    Cli,
+    repo_ref::{self, REPO_REF_KIND, RepoRef},
 };

 #[derive(Debug, clap::Args)]
@@ -34,16 +34,16 @@ pub struct SendSubCommandArgs {
     #[clap(long, value_parser, num_args = 0.., value_delimiter = ' ')]
     /// references to an existing proposal for which this is a new
     /// version and/or events / npubs to tag as mentions
-    pub  in_reply_to: Vec<String>,
+    pub in_reply_to: Vec<String>,
     /// don't prompt for a cover letter
     #[arg(long, action)]
-    pub  no_cover_letter: bool,
+    pub no_cover_letter: bool,
     /// optional cover letter title
     #[clap(short, long)]
-    pub  title: Option<String>,
+    pub title: Option<String>,
     #[clap(short, long)]
     /// optional cover letter description
-    pub  description: Option<String>,
+    pub description: Option<String>,
 }

 #[allow(clippy::too_many_lines)]
@@ -526,10 +526,9 @@ async fn get_root_proposal_id_and_mentions_from_in_reply_to(
                 public_key: _,
             }) => {
                 let events = client
-                    .get_events(
-                        repo_relays.to_vec(),
-                        vec![nostr::Filter::new().id(*event_id)],
-                    )
+                    .get_events(repo_relays.to_vec(), vec![
+                        nostr::Filter::new().id(*event_id),
+                    ])
                     .await
                     .context("whilst getting events specified in --in-reply-to")?;
                 if let Some(first) = events.iter().find(|e| e.id.eq(event_id)) {
@@ -1185,10 +1184,10 @@ mod tests {
                 identify_ahead_behind(&git_repo, &Some("feature".to_string()), &None)?;

             assert_eq!(from_branch, "feature");
-            assert_eq!(
-                ahead,
-                vec![oid_to_sha1(&feature_oid), oid_to_sha1(&dev_oid_first)]
-            );
+            assert_eq!(ahead, vec![
+                oid_to_sha1(&feature_oid),
+                oid_to_sha1(&dev_oid_first)
+            ]);
             assert_eq!(to_branch, "main");
             assert_eq!(behind, vec![]);

diff --git a/crates/ngit/test_utils/src/lib.rs b/crates/ngit/test_utils/src/lib.rs
index 1d881cfe..150113b0 100644
--- a/crates/ngit/test_utils/src/lib.rs
+++ b/crates/ngit/test_utils/src/lib.rs
@@ -1,10 +1,10 @@
 use std::{ffi::OsStr, path::PathBuf, str::FromStr};

-use anyhow::{bail, ensure, Context, Result};
+use anyhow::{Context, Result, bail, ensure};
 use dialoguer::theme::{ColorfulTheme, Theme};
 use directories::ProjectDirs;
-use nostr::{self, nips::nip65::RelayMetadata, Kind, Tag};
-use nostr_sdk::{serde_json, TagStandard};
+use nostr::{self, Kind, Tag, nips::nip65::RelayMetadata};
+use nostr_sdk::{TagStandard, serde_json};
 use once_cell::sync::Lazy;
 use rexpect::session::{Options, PtySession};
 use strip_ansi_escapes::strip_str;
@@ -50,43 +50,35 @@ pub fn generate_test_key_1_kind_event(kind: Kind) -> nostr::Event {
 }

 pub fn generate_test_key_1_relay_list_event() -> nostr::Event {
-    nostr::event::EventBuilder::new(
-        nostr::Kind::RelayList,
-        "",
-        [
-            nostr::Tag::from_standardized(nostr::TagStandard::RelayMetadata {
-                relay_url: nostr::Url::from_str("ws://localhost:8053").unwrap(),
-                metadata: Some(RelayMetadata::Write),
-            }),
-            nostr::Tag::from_standardized(nostr::TagStandard::RelayMetadata {
-                relay_url: nostr::Url::from_str("ws://localhost:8054").unwrap(),
-                metadata: Some(RelayMetadata::Read),
-            }),
-            nostr::Tag::from_standardized(nostr::TagStandard::RelayMetadata {
-                relay_url: nostr::Url::from_str("ws://localhost:8055").unwrap(),
-                metadata: None,
-            }),
-        ],
-    )
+    nostr::event::EventBuilder::new(nostr::Kind::RelayList, "", [
+        nostr::Tag::from_standardized(nostr::TagStandard::RelayMetadata {
+            relay_url: nostr::Url::from_str("ws://localhost:8053").unwrap(),
+            metadata: Some(RelayMetadata::Write),
+        }),
+        nostr::Tag::from_standardized(nostr::TagStandard::RelayMetadata {
+            relay_url: nostr::Url::from_str("ws://localhost:8054").unwrap(),
+            metadata: Some(RelayMetadata::Read),
+        }),
+        nostr::Tag::from_standardized(nostr::TagStandard::RelayMetadata {
+            relay_url: nostr::Url::from_str("ws://localhost:8055").unwrap(),
+            metadata: None,
+        }),
+    ])
     .to_event(&TEST_KEY_1_KEYS)
     .unwrap()
 }

 pub fn generate_test_key_1_relay_list_event_same_as_fallback() -> nostr::Event {
-    nostr::event::EventBuilder::new(
-        nostr::Kind::RelayList,
-        "",
-        [
-            nostr::Tag::from_standardized(nostr::TagStandard::RelayMetadata {
-                relay_url: nostr::Url::from_str("ws://localhost:8051").unwrap(),
-                metadata: Some(RelayMetadata::Write),
-            }),
-            nostr::Tag::from_standardized(nostr::TagStandard::RelayMetadata {
-                relay_url: nostr::Url::from_str("ws://localhost:8052").unwrap(),
-                metadata: Some(RelayMetadata::Write),
-            }),
-        ],
-    )
+    nostr::event::EventBuilder::new(nostr::Kind::RelayList, "", [
+        nostr::Tag::from_standardized(nostr::TagStandard::RelayMetadata {
+            relay_url: nostr::Url::from_str("ws://localhost:8051").unwrap(),
+            metadata: Some(RelayMetadata::Write),
+        }),
+        nostr::Tag::from_standardized(nostr::TagStandard::RelayMetadata {
+            relay_url: nostr::Url::from_str("ws://localhost:8052").unwrap(),
+            metadata: Some(RelayMetadata::Write),
+        }),
+    ])
     .to_event(&TEST_KEY_1_KEYS)
     .unwrap()
 }
@@ -140,44 +132,40 @@ pub fn generate_repo_ref_event() -> nostr::Event {
     // TODO - this may not be consistant across computers as it might take the
     // author and committer from global git config
     let root_commit = "9ee507fc4357d7ee16a5d8901bedcd103f23c17d";
-    nostr::event::EventBuilder::new(
-        nostr::Kind::Custom(REPOSITORY_KIND),
-        "",
-        [
-            Tag::identifier(
-                // root_commit.to_string()
-                format!("{}-consider-it-random", root_commit),
-            ),
-            Tag::from_standardized(TagStandard::Reference(root_commit.to_string())),
-            Tag::from_standardized(TagStandard::Name("example name".into())),
-            Tag::from_standardized(TagStandard::Description("example description".into())),
-            Tag::custom(
-                nostr::TagKind::Custom(std::borrow::Cow::Borrowed("clone")),
-                vec!["git:://123.gitexample.com/test".to_string()],
-            ),
-            Tag::custom(
-                nostr::TagKind::Custom(std::borrow::Cow::Borrowed("web")),
-                vec![
-                    "https://exampleproject.xyz".to_string(),
-                    "https://gitworkshop.dev/123".to_string(),
-                ],
-            ),
-            Tag::custom(
-                nostr::TagKind::Custom(std::borrow::Cow::Borrowed("relays")),
-                vec![
-                    "ws://localhost:8055".to_string(),
-                    "ws://localhost:8056".to_string(),
-                ],
-            ),
-            Tag::custom(
-                nostr::TagKind::Custom(std::borrow::Cow::Borrowed("maintainers")),
-                vec![
-                    TEST_KEY_1_KEYS.public_key().to_string(),
-                    TEST_KEY_2_KEYS.public_key().to_string(),
-                ],
-            ),
-        ],
-    )
+    nostr::event::EventBuilder::new(nostr::Kind::Custom(REPOSITORY_KIND), "", [
+        Tag::identifier(
+            // root_commit.to_string()
+            format!("{}-consider-it-random", root_commit),
+        ),
+        Tag::from_standardized(TagStandard::Reference(root_commit.to_string())),
+        Tag::from_standardized(TagStandard::Name("example name".into())),
+        Tag::from_standardized(TagStandard::Description("example description".into())),
+        Tag::custom(
+            nostr::TagKind::Custom(std::borrow::Cow::Borrowed("clone")),
+            vec!["git:://123.gitexample.com/test".to_string()],
+        ),
+        Tag::custom(
+            nostr::TagKind::Custom(std::borrow::Cow::Borrowed("web")),
+            vec![
+                "https://exampleproject.xyz".to_string(),
+                "https://gitworkshop.dev/123".to_string(),
+            ],
+        ),
+        Tag::custom(
+            nostr::TagKind::Custom(std::borrow::Cow::Borrowed("relays")),
+            vec![
+                "ws://localhost:8055".to_string(),
+                "ws://localhost:8056".to_string(),
+            ],
+        ),
+        Tag::custom(
+            nostr::TagKind::Custom(std::borrow::Cow::Borrowed("maintainers")),
+            vec![
+                TEST_KEY_1_KEYS.public_key().to_string(),
+                TEST_KEY_2_KEYS.public_key().to_string(),
+            ],
+        ),
+    ])
     .to_event(&TEST_KEY_1_KEYS)
     .unwrap()
 }
@@ -450,14 +438,10 @@ impl CliTesterConfirmPrompt<'_> {
         let mut s = String::new();
         self.tester
             .formatter
-            .format_confirm_prompt_selection(
-                &mut s,
-                self.prompt.as_str(),
-                match input {
-                    None => self.default,
-                    Some(_) => input,
-                },
-            )
+            .format_confirm_prompt_selection(&mut s, self.prompt.as_str(), match input {
+                None => self.default,
+                Some(_) => input,
+            })
             .expect("diagluer theme formatter should succeed");
         if !s.contains(self.prompt.as_str()) {
             panic!("dialoguer must be broken as formatted prompt success doesnt contain prompt");
@@ -895,13 +879,10 @@ where
     cmd.env("RUST_BACKTRACE", "0");
     cmd.args(args);
     // using branch for PR https://github.com/rust-cli/rexpect/pull/103 to strip ansi escape codes
-    rexpect::session::spawn_with_options(
-        cmd,
-        Options {
-            timeout_ms: Some(timeout_ms),
-            strip_ansi_escape_codes: true,
-        },
-    )
+    rexpect::session::spawn_with_options(cmd, Options {
+        timeout_ms: Some(timeout_ms),
+        strip_ansi_escape_codes: true,
+    })
 }

 pub fn rexpect_with_from_dir<I, S>(
@@ -919,13 +900,10 @@ where
     cmd.current_dir(dir);
     cmd.args(args);
     // using branch for PR https://github.com/rust-cli/rexpect/pull/103 to strip ansi escape codes
-    rexpect::session::spawn_with_options(
-        cmd,
-        Options {
-            timeout_ms: Some(timeout_ms),
-            strip_ansi_escape_codes: true,
-        },
-    )
+    rexpect::session::spawn_with_options(cmd, Options {
+        timeout_ms: Some(timeout_ms),
+        strip_ansi_escape_codes: true,
+    })
 }

 /// backup and remove application config and data
diff --git a/crates/ngit/test_utils/src/relay.rs b/crates/ngit/test_utils/src/relay.rs
index 82a8f8dd..5a1708b7 100644
--- a/crates/ngit/test_utils/src/relay.rs
+++ b/crates/ngit/test_utils/src/relay.rs
@@ -1,6 +1,6 @@
 use std::collections::HashMap;

-use anyhow::{bail, Result};
+use anyhow::{Result, bail};
 use nostr::{ClientMessage, JsonUtil, RelayMessage};

 use crate::CliTester;
diff --git a/crates/ngit/tests/init.rs b/crates/ngit/tests/init.rs
index 43d56379..5b1862d3 100644
--- a/crates/ngit/tests/init.rs
+++ b/crates/ngit/tests/init.rs
@@ -83,14 +83,10 @@ mod when_repo_not_previously_claimed {
                     8051,
                     None,
                     Some(&|relay, client_id, subscription_id, _| -> Result<()> {
-                        relay.respond_events(
-                            client_id,
-                            &subscription_id,
-                            &vec![
-                                generate_test_key_1_metadata_event("fred"),
-                                generate_test_key_1_relay_list_event(),
-                            ],
-                        )?;
+                        relay.respond_events(client_id, &subscription_id, &vec![
+                            generate_test_key_1_metadata_event("fred"),
+                            generate_test_key_1_relay_list_event(),
+                        ])?;
                         Ok(())
                     }),
                 ),
@@ -206,14 +202,10 @@ mod when_repo_not_previously_claimed {
                         8051,
                         None,
                         Some(&|relay, client_id, subscription_id, _| -> Result<()> {
-                            relay.respond_events(
-                                client_id,
-                                &subscription_id,
-                                &vec![
-                                    generate_test_key_1_metadata_event("fred"),
-                                    generate_test_key_1_relay_list_event(),
-                                ],
-                            )?;
+                            relay.respond_events(client_id, &subscription_id, &vec![
+                                generate_test_key_1_metadata_event("fred"),
+                                generate_test_key_1_relay_list_event(),
+                            ])?;
                             Ok(())
                         }),
                     ),
@@ -469,14 +461,10 @@ mod when_repo_not_previously_claimed {
                         8051,
                         None,
                         Some(&|relay, client_id, subscription_id, _| -> Result<()> {
-                            relay.respond_events(
-                                client_id,
-                                &subscription_id,
-                                &vec![
-                                    generate_test_key_1_metadata_event("fred"),
-                                    generate_test_key_1_relay_list_event(),
-                                ],
-                            )?;
+                            relay.respond_events(client_id, &subscription_id, &vec![
+                                generate_test_key_1_metadata_event("fred"),
+                                generate_test_key_1_relay_list_event(),
+                            ])?;
                             Ok(())
                         }),
                     ),
diff --git a/crates/ngit/tests/list.rs b/crates/ngit/tests/list.rs
index cd071a98..11e8ce2d 100644
--- a/crates/ngit/tests/list.rs
+++ b/crates/ngit/tests/list.rs
@@ -76,54 +76,45 @@ fn cli_tester_create_proposal(
     create_and_populate_branch(test_repo, branch_name, prefix, false)?;
     std::thread::sleep(std::time::Duration::from_millis(1000));
     if let Some(in_reply_to) = in_reply_to {
-        let mut p = CliTester::new_from_dir(
-            &test_repo.dir,
-            [
-                "--nsec",
-                TEST_KEY_1_NSEC,
-                "--password",
-                TEST_PASSWORD,
-                "--disable-cli-spinners",
-                "send",
-                "HEAD~2",
-                "--no-cover-letter",
-                "--in-reply-to",
-                in_reply_to.as_str(),
-            ],
-        );
+        let mut p = CliTester::new_from_dir(&test_repo.dir, [
+            "--nsec",
+            TEST_KEY_1_NSEC,
+            "--password",
+            TEST_PASSWORD,
+            "--disable-cli-spinners",
+            "send",
+            "HEAD~2",
+            "--no-cover-letter",
+            "--in-reply-to",
+            in_reply_to.as_str(),
+        ]);
         p.expect_end_eventually()?;
     } else if let Some((title, description)) = cover_letter_title_and_description {
-        let mut p = CliTester::new_from_dir(
-            &test_repo.dir,
-            [
-                "--nsec",
-                TEST_KEY_1_NSEC,
-                "--password",
-                TEST_PASSWORD,
-                "--disable-cli-spinners",
-                "send",
-                "HEAD~2",
-                "--title",
-                format!("\"{title}\"").as_str(),
-                "--description",
-                format!("\"{description}\"").as_str(),
-            ],
-        );
+        let mut p = CliTester::new_from_dir(&test_repo.dir, [
+            "--nsec",
+            TEST_KEY_1_NSEC,
+            "--password",
+            TEST_PASSWORD,
+            "--disable-cli-spinners",
+            "send",
+            "HEAD~2",
+            "--title",
+            format!("\"{title}\"").as_str(),
+            "--description",
+            format!("\"{description}\"").as_str(),
+        ]);
         p.expect_end_eventually()?;
     } else {
-        let mut p = CliTester::new_from_dir(
-            &test_repo.dir,
-            [
-                "--nsec",
-                TEST_KEY_1_NSEC,
-                "--password",
-                TEST_PASSWORD,
-                "--disable-cli-spinners",
-                "send",
-                "HEAD~2",
-                "--no-cover-letter",
-            ],
-        );
+        let mut p = CliTester::new_from_dir(&test_repo.dir, [
+            "--nsec",
+            TEST_KEY_1_NSEC,
+            "--password",
+            TEST_PASSWORD,
+            "--disable-cli-spinners",
+            "send",
+            "HEAD~2",
+            "--no-cover-letter",
+        ]);
         p.expect_end_eventually()?;
     }
     Ok(())
@@ -133,8 +124,8 @@ mod cannot_find_repo_event {
     use super::*;
     mod cli_prompts {
         use nostr::{
-            nips::{nip01::Coordinate, nip19::Nip19Event},
             ToBech32,
+            nips::{nip01::Coordinate, nip19::Nip19Event},
         };

         use super::*;
@@ -273,8 +264,8 @@ mod when_main_branch_is_uptodate {
                     r55.events.push(generate_test_key_1_metadata_event("fred"));
                     r55.events.push(generate_test_key_1_relay_list_event());

-                    let cli_tester_handle = std::thread::spawn(
-                        move || -> Result<(GitTestRepo, GitTestRepo)> {
+                    let cli_tester_handle =
+                        std::thread::spawn(move || -> Result<(GitTestRepo, GitTestRepo)> {
                             let originating_repo = cli_tester_create_proposals()?;

                             let test_repo = GitTestRepo::default();
@@ -282,27 +273,22 @@ mod when_main_branch_is_uptodate {
                             let mut p = CliTester::new_from_dir(&test_repo.dir, ["list"]);

                             p.expect("finding proposals...\r\n")?;
-                            let mut c = p.expect_choice(
-                                "all proposals",
-                                vec![
-                                    format!("\"{PROPOSAL_TITLE_3}\""),
-                                    format!("\"{PROPOSAL_TITLE_2}\""),
-                                    format!("\"{PROPOSAL_TITLE_1}\""),
-                                ],
-                            )?;
+                            let mut c = p.expect_choice("all proposals", vec![
+                                format!("\"{PROPOSAL_TITLE_3}\""),
+                                format!("\"{PROPOSAL_TITLE_2}\""),
+                                format!("\"{PROPOSAL_TITLE_1}\""),
+                            ])?;
                             c.succeeds_with(2, true, None)?;

                             p.expect("finding commits...\r\n")?;
-                            let mut c = p.expect_choice(
-                                "",
-                                vec![
-                                    format!(
-                                        "create and checkout proposal branch (2 ahead 0 behind 'main')" ),
-                                    format!("apply to current branch with `git am`"),
-                                    format!("download to ./patches"),
-                                    format!("back"),
-                                ],
-                            )?;
+                            let mut c = p.expect_choice("", vec![
+                                format!(
+                                    "create and checkout proposal branch (2 ahead 0 behind 'main')"
+                                ),
+                                format!("apply to current branch with `git am`"),
+                                format!("download to ./patches"),
+                                format!("back"),
+                            ])?;
                             c.succeeds_with(0, false, Some(0))?;
                             p.expect(format!(
                                 "checked out proposal as '{FEATURE_BRANCH_NAME_1}' branch\r\n"
@@ -312,8 +298,7 @@ mod when_main_branch_is_uptodate {
                                 relay::shutdown_relay(8000 + p)?;
                             }
                             Ok((originating_repo, test_repo))
-                        },
-                    );
+                        });

                     // launch relay
                     let _ = join!(
@@ -357,26 +342,21 @@ mod when_main_branch_is_uptodate {
                             let mut p = CliTester::new_from_dir(&test_repo.dir, ["list"]);

                             p.expect("finding proposals...\r\n")?;
-                            let mut c = p.expect_choice(
-                                "all proposals",
-                                vec![
-                                    format!("\"{PROPOSAL_TITLE_3}\""),
-                                    format!("\"{PROPOSAL_TITLE_2}\""),
-                                    format!("\"{PROPOSAL_TITLE_1}\""),
-                                ],
-                            )?;
+                            let mut c = p.expect_choice("all proposals", vec![
+                                format!("\"{PROPOSAL_TITLE_3}\""),
+                                format!("\"{PROPOSAL_TITLE_2}\""),
+                                format!("\"{PROPOSAL_TITLE_1}\""),
+                            ])?;
                             c.succeeds_with(2, true, None)?;
                             p.expect("finding commits...\r\n")?;
-                            let mut c = p.expect_choice(
-                                "",
-                                vec![
-                                    format!(
-                                        "create and checkout proposal branch (2 ahead 0 behind 'main')" ),
-                                    format!("apply to current branch with `git am`"),
-                                    format!("download to ./patches"),
-                                    format!("back"),
-                                ],
-                            )?;
+                            let mut c = p.expect_choice("", vec![
+                                format!(
+                                    "create and checkout proposal branch (2 ahead 0 behind 'main')"
+                                ),
+                                format!("apply to current branch with `git am`"),
+                                format!("download to ./patches"),
+                                format!("back"),
+                            ])?;
                             c.succeeds_with(0, false, None)?;
                             p.expect(format!(
                                 "checked out proposal as '{FEATURE_BRANCH_NAME_1}' branch\r\n"
@@ -457,8 +437,8 @@ mod when_main_branch_is_uptodate {
                     r55.events.push(generate_test_key_1_metadata_event("fred"));
                     r55.events.push(generate_test_key_1_relay_list_event());

-                    let cli_tester_handle = std::thread::spawn(
-                        move || -> Result<(GitTestRepo, GitTestRepo)> {
+                    let cli_tester_handle =
+                        std::thread::spawn(move || -> Result<(GitTestRepo, GitTestRepo)> {
                             let originating_repo = cli_tester_create_proposals()?;

                             let test_repo = GitTestRepo::default();
@@ -466,27 +446,22 @@ mod when_main_branch_is_uptodate {
                             let mut p = CliTester::new_from_dir(&test_repo.dir, ["list"]);

                             p.expect("finding proposals...\r\n")?;
-                            let mut c = p.expect_choice(
-                                "all proposals",
-                                vec![
-                                    format!("\"{PROPOSAL_TITLE_3}\""),
-                                    format!("\"{PROPOSAL_TITLE_2}\""),
-                                    format!("\"{PROPOSAL_TITLE_1}\""),
-                                ],
-                            )?;
+                            let mut c = p.expect_choice("all proposals", vec![
+                                format!("\"{PROPOSAL_TITLE_3}\""),
+                                format!("\"{PROPOSAL_TITLE_2}\""),
+                                format!("\"{PROPOSAL_TITLE_1}\""),
+                            ])?;
                             c.succeeds_with(0, true, None)?;

                             p.expect("finding commits...\r\n")?;
-                            let mut c = p.expect_choice(
-                                "",
-                                vec![
-                                    format!(
-                                        "create and checkout proposal branch (2 ahead 0 behind 'main')" ),
-                                    format!("apply to current branch with `git am`"),
-                                    format!("download to ./patches"),
-                                    format!("back"),
-                                ],
-                            )?;
+                            let mut c = p.expect_choice("", vec![
+                                format!(
+                                    "create and checkout proposal branch (2 ahead 0 behind 'main')"
+                                ),
+                                format!("apply to current branch with `git am`"),
+                                format!("download to ./patches"),
+                                format!("back"),
+                            ])?;
                             c.succeeds_with(0, false, Some(0))?;
                             p.expect(format!(
                                 "checked out proposal as '{FEATURE_BRANCH_NAME_3}' branch\r\n"
@@ -497,8 +472,7 @@ mod when_main_branch_is_uptodate {
                                 relay::shutdown_relay(8000 + p)?;
                             }
                             Ok((originating_repo, test_repo))
-                        },
-                    );
+                        });

                     // launch relay
                     let _ = join!(
@@ -543,26 +517,21 @@ mod when_main_branch_is_uptodate {
                             let mut p = CliTester::new_from_dir(&test_repo.dir, ["list"]);

                             p.expect("finding proposals...\r\n")?;
-                            let mut c = p.expect_choice(
-                                "all proposals",
-                                vec![
-                                    format!("\"{PROPOSAL_TITLE_3}\""),
-                                    format!("\"{PROPOSAL_TITLE_2}\""),
-                                    format!("\"{PROPOSAL_TITLE_1}\""),
-                                ],
-                            )?;
+                            let mut c = p.expect_choice("all proposals", vec![
+                                format!("\"{PROPOSAL_TITLE_3}\""),
+                                format!("\"{PROPOSAL_TITLE_2}\""),
+                                format!("\"{PROPOSAL_TITLE_1}\""),
+                            ])?;
                             c.succeeds_with(0, true, None)?;
                             p.expect("finding commits...\r\n")?;
-                            let mut c = p.expect_choice(
-                                "",
-                                vec![
-                                    format!(
-                                        "create and checkout proposal branch (2 ahead 0 behind 'main')" ),
-                                    format!("apply to current branch with `git am`"),
-                                    format!("download to ./patches"),
-                                    format!("back"),
-                                ],
-                            )?;
+                            let mut c = p.expect_choice("", vec![
+                                format!(
+                                    "create and checkout proposal branch (2 ahead 0 behind 'main')"
+                                ),
+                                format!("apply to current branch with `git am`"),
+                                format!("download to ./patches"),
+                                format!("back"),
+                            ])?;
                             c.succeeds_with(0, false, Some(0))?;
                             p.expect(format!(
                                 "checked out proposal as '{FEATURE_BRANCH_NAME_3}' branch\r\n"
@@ -643,8 +612,8 @@ mod when_main_branch_is_uptodate {
                     r55.events.push(generate_test_key_1_metadata_event("fred"));
                     r55.events.push(generate_test_key_1_relay_list_event());

-                    let cli_tester_handle = std::thread::spawn(
-                        move || -> Result<(GitTestRepo, GitTestRepo)> {
+                    let cli_tester_handle =
+                        std::thread::spawn(move || -> Result<(GitTestRepo, GitTestRepo)> {
                             let originating_repo = cli_tester_create_proposals()?;
                             cli_tester_create_proposal(
                                 &originating_repo,
@@ -658,27 +627,22 @@ mod when_main_branch_is_uptodate {
                             let mut p = CliTester::new_from_dir(&test_repo.dir, ["list"]);

                             p.expect("finding proposals...\r\n")?;
-                            let mut c = p.expect_choice(
-                                "all proposals",
-                                vec![
-                                    format!("add d3.md"), // commit msg title
-                                    format!("\"{PROPOSAL_TITLE_3}\""),
-                                    format!("\"{PROPOSAL_TITLE_2}\""),
-                                    format!("\"{PROPOSAL_TITLE_1}\""),
-                                ],
-                            )?;
+                            let mut c = p.expect_choice("all proposals", vec![
+                                format!("add d3.md"), // commit msg title
+                                format!("\"{PROPOSAL_TITLE_3}\""),
+                                format!("\"{PROPOSAL_TITLE_2}\""),
+                                format!("\"{PROPOSAL_TITLE_1}\""),
+                            ])?;
                             c.succeeds_with(0, true, None)?;
                             p.expect("finding commits...\r\n")?;
-                            let mut c = p.expect_choice(
-                                "",
-                                vec![
-                                    format!(
-                                        "create and checkout proposal branch (2 ahead 0 behind 'main')" ),
-                                    format!("apply to current branch with `git am`"),
-                                    format!("download to ./patches"),
-                                    format!("back"),
-                                ],
-                            )?;
+                            let mut c = p.expect_choice("", vec![
+                                format!(
+                                    "create and checkout proposal branch (2 ahead 0 behind 'main')"
+                                ),
+                                format!("apply to current branch with `git am`"),
+                                format!("download to ./patches"),
+                                format!("back"),
+                            ])?;
                             c.succeeds_with(0, false, Some(0))?;
                             p.expect(format!(
                                 "checked out proposal as '{FEATURE_BRANCH_NAME_4}' branch\r\n"
@@ -689,8 +653,7 @@ mod when_main_branch_is_uptodate {
                                 relay::shutdown_relay(8000 + p)?;
                             }
                             Ok((originating_repo, test_repo))
-                        },
-                    );
+                        });

                     // launch relay
                     let _ = join!(
@@ -742,27 +705,22 @@ mod when_main_branch_is_uptodate {
                             let mut p = CliTester::new_from_dir(&test_repo.dir, ["list"]);

                             p.expect("finding proposals...\r\n")?;
-                            let mut c = p.expect_choice(
-                                "all proposals",
-                                vec![
-                                    format!("add d3.md"), // commit msg title
-                                    format!("\"{PROPOSAL_TITLE_3}\""),
-                                    format!("\"{PROPOSAL_TITLE_2}\""),
-                                    format!("\"{PROPOSAL_TITLE_1}\""),
-                                ],
-                            )?;
+                            let mut c = p.expect_choice("all proposals", vec![
+                                format!("add d3.md"), // commit msg title
+                                format!("\"{PROPOSAL_TITLE_3}\""),
+                                format!("\"{PROPOSAL_TITLE_2}\""),
+                                format!("\"{PROPOSAL_TITLE_1}\""),
+                            ])?;
                             c.succeeds_with(0, true, None)?;
                             p.expect("finding commits...\r\n")?;
-                            let mut c = p.expect_choice(
-                                "",
-                                vec![
-                                    format!(
-                                        "create and checkout proposal branch (2 ahead 0 behind 'main')" ),
-                                    format!("apply to current branch with `git am`"),
-                                    format!("download to ./patches"),
-                                    format!("back"),
-                                ],
-                            )?;
+                            let mut c = p.expect_choice("", vec![
+                                format!(
+                                    "create and checkout proposal branch (2 ahead 0 behind 'main')"
+                                ),
+                                format!("apply to current branch with `git am`"),
+                                format!("download to ./patches"),
+                                format!("back"),
+                            ])?;
                             c.succeeds_with(0, false, Some(0))?;
                             p.expect(format!(
                                 "checked out proposal as '{FEATURE_BRANCH_NAME_4}' branch\r\n"
@@ -867,25 +825,19 @@ mod when_main_branch_is_uptodate {
                             )?;
                             test_repo.checkout("main")?;
                             p.expect("finding proposals...\r\n")?;
-                            let mut c = p.expect_choice(
-                                "all proposals",
-                                vec![
-                                    format!("\"{PROPOSAL_TITLE_3}\""),
-                                    format!("\"{PROPOSAL_TITLE_2}\""),
-                                    format!("\"{PROPOSAL_TITLE_1}\""),
-                                ],
-                            )?;
+                            let mut c = p.expect_choice("all proposals", vec![
+                                format!("\"{PROPOSAL_TITLE_3}\""),
+                                format!("\"{PROPOSAL_TITLE_2}\""),
+                                format!("\"{PROPOSAL_TITLE_1}\""),
+                            ])?;
                             c.succeeds_with(2, true, None)?;
                             p.expect("finding commits...\r\n")?;
-                            let mut c = p.expect_choice(
-                                "",
-                                vec![
-                                    format!("checkout proposal branch (2 ahead 0 behind 'main')"),
-                                    format!("apply to current branch with `git am`"),
-                                    format!("download to ./patches"),
-                                    format!("back"),
-                                ],
-                            )?;
+                            let mut c = p.expect_choice("", vec![
+                                format!("checkout proposal branch (2 ahead 0 behind 'main')"),
+                                format!("apply to current branch with `git am`"),
+                                format!("download to ./patches"),
+                                format!("back"),
+                            ])?;
                             c.succeeds_with(0, false, Some(0))?;
                             p.expect(format!(
                                 "checked out proposal as '{FEATURE_BRANCH_NAME_1}' branch\r\n"
@@ -949,25 +901,19 @@ mod when_main_branch_is_uptodate {
                             test_repo.checkout("main")?;

                             p.expect("finding proposals...\r\n")?;
-                            let mut c = p.expect_choice(
-                                "all proposals",
-                                vec![
-                                    format!("\"{PROPOSAL_TITLE_3}\""),
-                                    format!("\"{PROPOSAL_TITLE_2}\""),
-                                    format!("\"{PROPOSAL_TITLE_1}\""),
-                                ],
-                            )?;
+                            let mut c = p.expect_choice("all proposals", vec![
+                                format!("\"{PROPOSAL_TITLE_3}\""),
+                                format!("\"{PROPOSAL_TITLE_2}\""),
+                                format!("\"{PROPOSAL_TITLE_1}\""),
+                            ])?;
                             c.succeeds_with(2, true, None)?;
                             p.expect("finding commits...\r\n")?;
-                            let mut c = p.expect_choice(
-                                "",
-                                vec![
-                                    format!("checkout proposal branch (2 ahead 0 behind 'main')"),
-                                    format!("apply to current branch with `git am`"),
-                                    format!("download to ./patches"),
-                                    format!("back"),
-                                ],
-                            )?;
+                            let mut c = p.expect_choice("", vec![
+           …
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Strip ANSI escape codes
4 participants