mod alias;
mod append;
mod autoheal;
mod cli;
mod clonehome;
mod config;
mod config_hub;
mod crosshub;
mod cursor;
#[cfg(feature = "dashboard")]
mod dashboard;
mod doctor;
mod envelope;
mod fleet;
mod ghapp;
mod gitcmd;
mod groups;
mod hooks;
mod identity;
mod inbox;
mod init;
mod join;
mod keygen_release;
mod keyring;
mod knownhubs;
mod machineconfig;
mod presence;
mod projection;
mod reconnect;
mod repos;
mod roster;
mod schema;
mod screen;
mod secrets;
#[cfg(feature = "serve")]
mod serve;
mod skills;
mod store;
mod templates;
mod tiers;
mod transport;
mod trust;
mod verify;
mod version;
mod watch;
mod watchlock;
use anyhow::{anyhow, Result};
use append::{cmd_append, cmd_lifecycle, AppendArgs};
use clap::Parser;
use cli::{Cli, Cmd};
use config_hub::{cmd_config, cmd_hub, cmd_rewatch, cmd_status};
use fleet::{cmd_fleet, cmd_require, cmd_version, update_hint};
use inbox::{cmd_ack, cmd_inbox, cmd_read, cmd_requests, cmd_show, cmd_thread};
use init::{cmd_adopt_clone, cmd_clones, cmd_hubs, cmd_init, cmd_where};
use hooks::{cmd_autoheal, cmd_install_hook, cmd_session_heal, cmd_uninstall_hook};
use identity::{cmd_describe, cmd_identity, cmd_rename, cmd_set_status, cmd_who, cmd_whois};
use join::cmd_join;
use keygen_release::{cmd_changelog, cmd_keygen, cmd_update};
#[cfg(test)]
use reconnect::canonical_hub_id;
use reconnect::{cmd_onboard, cmd_reconnect};
use skills::cmd_install_skill;
#[cfg(test)]
use transport::clone_candidates;
use transport::scheme_from;
use trust::{
cmd_confirm_key, cmd_doctor, cmd_invite, cmd_repos, cmd_screen, cmd_seen, cmd_trust,
cmd_verify,
};
use projection::id_ref_matches;
#[cfg(test)]
use projection::claimants;
#[cfg(test)]
use schema::Frontmatter;
use schema::{is_actionable, Message};
use std::collections::HashSet;
use std::io::Write;
pub(crate) const BUILD_SHA: &str = env!("CONFER_GIT_SHA");
pub(crate) fn warn_safety(msg: impl std::fmt::Display) {
eprintln!("confer: ⚠ {msg}");
}
pub(crate) fn hint(msg: impl std::fmt::Display) {
eprintln!("confer: · {msg}");
}
pub(crate) fn warn_trust(msg: impl std::fmt::Display) {
eprintln!("confer: ‼ {msg}");
}
pub(crate) fn warn_if_watch_should_be_live(root: &std::path::Path, role: &str) {
if role.is_empty() {
return;
}
let clone = root.to_string_lossy();
let armed = autoheal::load()
.targets
.iter()
.any(|t| t.role == role && t.hub == clone);
if !armed {
return; }
let hub = config::hub_key(root);
match watchlock::classify(&watchlock::inspect(&hub, role, 90), BUILD_SHA) {
watchlock::WatchState::Stale | watchlock::WatchState::NotWatching => warn_safety(format!(
"no live watcher for '{role}' on this machine — you are NOT being woken by peer \
messages. Re-arm via /confer-watch (host it under your Monitor tool, never background \
bash); check anytime with `confer watch-status`."
)),
_ => {}
}
}
pub(crate) const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (", env!("CONFER_GIT_SHA"), ")");
const CHANGELOG_MD: &str = include_str!("../CHANGELOG.md");
const TOOL_REPO_SSH: &str = "git@github.com:codeshrew/confer.git";
const TOOL_REPO_HTTPS: &str = "https://github.com/codeshrew/confer.git";
fn check_version(root: &std::path::Path) {
if let Some(pin) = hub_pin(root) {
let a = version::assess(&my_build(), Some(&pin));
if a.outdated && a.grade != "rebuild" {
eprintln!(
"confer: {} — {} (adopt: confer reconnect --role <you>)",
a.grade,
update_hint(a.grade)
);
}
}
}
fn now() -> String {
chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
}
pub(crate) fn truncate(s: &str, n: usize) -> String {
if s.chars().count() <= n {
return s.to_string();
}
let head: String = s.chars().take(n).collect();
match head.rsplit_once(char::is_whitespace) {
Some((keep, _)) if !keep.trim_end().is_empty() => format!("{}…", keep.trim_end()),
_ => format!("{head}…"),
}
}
pub(crate) fn short_id(id: &str) -> &str {
if id.len() > 6 {
&id[id.len() - 6..]
} else {
id
}
}
fn id_matches(full: &str, q: &str) -> bool {
!q.is_empty() && (full == q || full.ends_with(q) || full.starts_with(q))
}
fn is_full_ulid(s: &str) -> bool {
s.len() == 26 && s.chars().all(|c| c.is_ascii_alphanumeric())
}
fn resolve_unique<'a>(msgs: &'a [Message], query: &str) -> Result<&'a str> {
let mut hits: Vec<&str> = msgs
.iter()
.map(|m| m.front.id.as_str())
.filter(|id| id_matches(id, query))
.collect();
hits.sort();
hits.dedup();
match hits.len() {
1 => Ok(hits[0]),
0 => Err(anyhow!("no message matches id '{query}'")),
n => Err(anyhow!(
"id '{query}' is ambiguous — matches {n} messages; use a longer or full id"
)),
}
}
fn valid_slug(s: &str) -> bool {
let ok_first = s
.chars()
.next()
.is_some_and(|c| c.is_ascii_lowercase() || c.is_ascii_digit());
ok_first
&& s.len() <= 64 && s.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
}
fn is_reserved_name(s: &str) -> bool {
s == schema::ALL
}
pub(crate) fn format_line(
roster: &roster::Roster,
m: &Message,
full: bool,
trust: Option<&verify::Trust>,
) -> String {
let kind = m.front.msg_type.to_uppercase();
let ts = m.front.ts.get(11..16).unwrap_or(&m.front.ts);
let who = roster::display(roster, &m.front.from);
let pri = if m.front.priority.as_deref() == Some("high") {
"‼ "
} else {
""
};
let vg = trust.map(|t| format!(" {}", t.glyph())).unwrap_or_default();
let summary = if full {
m.summary_line()
} else {
truncate(&m.summary_line(), 80)
};
schema::sanitize_term(
&format!(
"{pri}{kind} {} | {ts} | {who}{vg}{} — {}{}",
short_id(&m.front.id),
render_targets(roster, &m.front.to),
summary,
render_refs(&m.front.refs),
),
false,
)
}
fn render_refs(refs: &[schema::CodeRef]) -> String {
let Some(first) = refs.first() else {
return String::new();
};
let more = if refs.len() > 1 {
format!(" +{}", refs.len() - 1)
} else {
String::new()
};
format!(" ⟶ {}:{}{more}", first.repo, first.path)
}
fn render_targets(roster: &roster::Roster, targets: &[String]) -> String {
if targets.is_empty() {
return String::new();
}
let names: Vec<String> = targets
.iter()
.map(|t| schema::sanitize_term(roster::display(roster, t), false))
.collect();
format!(" → {}", names.join(", "))
}
pub(crate) fn to_json(m: &Message) -> Result<String> {
let mut v = serde_json::to_value(&m.front)?;
if let serde_json::Value::Object(map) = &mut v {
map.insert("body".into(), serde_json::Value::String(m.body.clone()));
}
Ok(serde_json::to_string(&v)?)
}
fn main() -> Result<()> {
match Cli::parse().cmd {
Cmd::Join {
role,
host,
display,
desc,
signing_key,
force,
} => cmd_join(role, host, display, desc, signing_key, force),
Cmd::Append {
msg_type,
text,
summary,
to,
cc,
priority,
topic,
reply_to,
of,
supersedes,
from,
src,
refs,
allow_empty_body,
resolution,
defer,
allow_secret,
} => cmd_append(AppendArgs {
msg_type,
text,
summary,
to,
cc,
priority,
topic,
reply_to,
of,
supersedes,
from,
src,
refs,
allow_empty_body,
allow_secret,
resolution,
defer,
}),
Cmd::Claim { args } => cmd_lifecycle("claim", args, None),
Cmd::Done { args, resolution } => cmd_lifecycle("done", args, resolution),
Cmd::Error { args } => cmd_lifecycle("error", args, None),
Cmd::Blocked { args } => cmd_lifecycle("blocked", args, None),
Cmd::Defer { args } => cmd_lifecycle("defer", args, None),
Cmd::Poll {
advance,
topic,
hook,
json,
role,
all,
to_me,
..
} => cmd_poll(PollArgs {
advance,
topic,
hook,
json,
role,
all,
to_me,
}),
Cmd::Show { id } => cmd_show(id),
Cmd::Requests {
open,
mine,
role,
json,
backlog,
blocked,
} => cmd_requests(open, mine, role, json, backlog, blocked),
Cmd::Thread { id, full } => cmd_thread(id, full),
Cmd::Init {
url,
dir,
role,
ssh,
https,
display,
desc,
signing_key,
ssh_key,
managed,
} => cmd_init(
url,
dir,
role,
scheme_from(ssh, https),
display,
desc,
signing_key,
ssh_key,
false,
managed,
),
Cmd::Clone {
url,
dir,
role,
ssh,
https,
display,
desc,
signing_key,
ssh_key,
managed,
} => cmd_init(
url,
dir,
role,
scheme_from(ssh, https),
display,
desc,
signing_key,
ssh_key,
true,
managed,
),
Cmd::Clones => cmd_clones(),
Cmd::Hubs => cmd_hubs(),
Cmd::Where => cmd_where(),
Cmd::Keygen { role } => cmd_keygen(role, true),
Cmd::Update { check } => cmd_update(check),
Cmd::AdoptClone { path, force } => cmd_adopt_clone(path, force),
Cmd::Invite {
role,
host,
ssh,
https,
} => cmd_invite(role, host, scheme_from(ssh, https)),
Cmd::Repos { json } => cmd_repos(json),
Cmd::Verify { id } => cmd_verify(id),
Cmd::ConfirmKey { role } => cmd_confirm_key(role),
Cmd::Doctor { dir, fix } => cmd_doctor(dir, fix),
Cmd::Trust { tier } => cmd_trust(tier),
Cmd::Screen { corpus, text } => cmd_screen(corpus, text),
Cmd::Seen { id } => cmd_seen(id),
Cmd::Inbox { role, peek } => cmd_inbox(role, peek),
Cmd::Ack { id, role } => cmd_ack(id, role),
Cmd::Credential { op } => ghapp::credential(&op),
Cmd::AppToken => {
println!("{}", ghapp::token(&ghapp::load_config()?)?);
Ok(())
}
Cmd::AppConfig {
app_id,
key,
installation_id,
find_installation,
} => cmd_app_config(app_id, key, installation_id, find_installation),
Cmd::InstallSkill {
dir,
hub,
role,
no_autoheal,
} => cmd_install_skill(dir, hub, role, no_autoheal),
Cmd::Reconnect {
role,
hub,
dir,
host,
ssh_key,
force,
} => cmd_reconnect(role, hub, dir, host, ssh_key, force),
Cmd::Onboard { role, hub } => cmd_onboard(role, hub),
Cmd::Version { json, check, pin } => cmd_version(json, check, pin),
Cmd::Changelog { since, all } => cmd_changelog(since, all),
Cmd::Fleet { json } => cmd_fleet(json),
Cmd::Require { req, bump } => cmd_require(req, bump),
Cmd::Read {
last,
topic,
full,
json,
} => cmd_read(last, topic, full, json),
Cmd::Watch {
topic,
role,
json,
poll_secs,
no_advance,
replace,
all,
min_priority,
no_version_notice,
delivery,
..
} => {
let min_priority = match min_priority.as_str() {
"low" => 0,
"normal" => 1,
"high" => 2,
other => {
return Err(anyhow!(
"invalid --min-priority '{other}': expected low | normal | high"
))
}
};
watch::run(watch::WatchOpts {
topic,
role,
json,
poll_secs,
advance: !no_advance,
replace,
all,
min_priority,
no_version_notice,
delivery,
})
}
Cmd::WatchStatus { role, json } => watch::cmd_watch_status(role, json),
Cmd::Status => cmd_status(),
#[cfg(feature = "dashboard")]
Cmd::Dashboard { hub } => cmd_dashboard(hub),
#[cfg(feature = "serve")]
Cmd::Serve { hub, bind } => serve::run(resolve_hubs(hub)?, &bind),
Cmd::InstallHook { project } => cmd_install_hook(project),
Cmd::UninstallHook { project } => cmd_uninstall_hook(project),
Cmd::SessionHeal => cmd_session_heal(),
Cmd::Autoheal { action, yes } => cmd_autoheal(action, yes),
Cmd::Config { action, key, value, yes } => cmd_config(action, key, value, yes),
Cmd::Hub { action, yes } => cmd_hub(action, yes),
Cmd::Rewatch { only } => cmd_rewatch(only),
Cmd::Identity { role } => cmd_identity(role),
Cmd::Whois { phrase } => cmd_whois(phrase.join(" ")),
Cmd::Rename { name, role, force } => cmd_rename(name.join(" "), role, force),
Cmd::Describe {
role,
desc,
display,
add_alias,
remove_alias,
force,
} => cmd_describe(role, desc, display, add_alias, remove_alias, force),
Cmd::Retire { role, permanent } => {
cmd_set_status(role, if permanent { "retired" } else { "dormant" })
}
Cmd::Resume { role } => cmd_set_status(role, "active"),
Cmd::Who => cmd_who(),
Cmd::Leave => {
eprintln!(
"confer leave: not yet implemented (planned: release lease + handoff marker)"
);
Ok(())
}
}
}
pub(crate) fn ssh_keygen_path() -> String {
std::process::Command::new("which")
.arg("ssh-keygen")
.output()
.ok()
.filter(|o| o.status.success())
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "ssh-keygen".to_string())
}
#[derive(clap::Args)]
pub(crate) struct LifecycleArgs {
#[arg(long)]
of: String,
#[arg(long)]
summary: Option<String>,
#[arg(long)]
text: Option<String>,
#[arg(long)]
from: Option<String>,
#[arg(long)]
to: Vec<String>,
#[arg(long)]
cc: Vec<String>,
#[arg(long = "reply-to")]
reply_to: Option<String>,
#[arg(long = "ref")]
refs: Vec<String>,
}
struct PollArgs {
advance: bool,
topic: Option<String>,
hook: bool,
json: bool,
role: Option<String>,
all: bool,
to_me: bool,
}
fn cmd_poll(p: PollArgs) -> Result<()> {
let root = config::repo_root()?;
let me = config::resolve_role(p.role.clone(), &root).unwrap_or_default();
warn_if_watch_should_be_live(&root, &me);
if let Err(e) = gitcmd::integrate(&root) {
warn_safety(format!("hub sync failed ({e}); showing local state"));
}
let hub = config::hub_key(&root);
let roster = roster::load(&root);
let since = cursor::load(&hub, &me)?;
let filtered = p.topic.is_some() || p.to_me || p.all;
if p.advance && filtered {
return Err(anyhow!(
"--advance is only allowed on an unfiltered poll (filtered/firehose views must not move the shared cursor)"
));
}
let grps = groups::load(&root);
let msgs = store::messages_since(&root, since.as_deref())?;
let new: Vec<&Message> = msgs
.iter()
.filter(|m| relevant(m, &me, &p, &grps))
.collect();
let mut out: Box<dyn Write> = if p.hook {
Box::new(std::io::stderr())
} else {
Box::new(std::io::stdout())
};
let mut vc = verify::Cache::default();
for m in &new {
let line = if p.json {
to_json(m)?
} else {
let t = verify::status(&root, &hub, &roster, &mut vc, m);
format_line(&roster, m, true, Some(&t))
};
writeln!(out, "{line}")?;
}
drop(out);
if p.advance {
if let Some(anchor) = gitcmd::cursor_anchor(&root) {
cursor::save(&hub, &me, &anchor)?;
}
}
if p.hook && !new.is_empty() {
std::process::exit(2);
}
Ok(())
}
fn relevant(m: &Message, me: &str, p: &PollArgs, groups: &groups::Groups) -> bool {
m.front.from != me
&& p.topic
.as_ref()
.is_none_or(|t| m.front.topic.as_deref() == Some(t.as_str()))
&& (p.all || is_actionable(m) || groups::addressed(m, me, groups))
&& (!p.to_me || groups::addressed(m, me, groups))
}
fn framed_body(
display_md: &str,
m: &Message,
who: &str,
trust: &verify::Trust,
tier: Option<tiers::Tier>,
) -> String {
let v = screen::heuristic(&screen::Input {
body: &m.body,
from_role: &m.front.from,
tier,
refs: vec![],
});
let note = match v.level {
screen::Level::Allow => None,
_ => Some(format!(
"⚠ possible injection ({})",
v.category.unwrap_or("?")
)),
};
envelope::frame(display_md, who, &m.front.from, trust, tier, note.as_deref())
}
fn superseded_set(msgs: &[Message]) -> HashSet<String> {
let mut s = HashSet::new();
for m in msgs {
if let Some(sup) = &m.front.supersedes {
if let Some(t) = msgs.iter().find(|x| id_ref_matches(&x.front.id, sup)) {
s.insert(t.front.id.clone());
}
}
}
s
}
#[cfg(any(feature = "dashboard", feature = "serve"))]
fn resolve_hubs(hub: Vec<String>) -> Result<Vec<std::path::PathBuf>> {
if !hub.is_empty() {
let home = config::home().ok();
return Ok(hub
.into_iter()
.map(|h| match (h.strip_prefix("~/"), &home) {
(Some(rest), Some(home)) => home.join(rest),
_ => std::path::PathBuf::from(h),
})
.collect());
}
match config::repo_root() {
Ok(cwd) => Ok(vec![cwd]),
Err(_) => {
let ds = crosshub::hub_dirs();
if ds.is_empty() {
anyhow::bail!("no hubs found — run inside a hub clone or pass --hub <dir>");
}
Ok(ds)
}
}
}
#[cfg(feature = "dashboard")]
fn cmd_dashboard(hub: Vec<String>) -> Result<()> {
dashboard::run(resolve_hubs(hub)?)
}
fn cmd_app_config(
app_id: Option<String>,
key: Option<String>,
installation_id: Option<u64>,
find_installation: bool,
) -> Result<()> {
let mut c = ghapp::load_config().unwrap_or_default();
let mut changed = false;
if let Some(a) = app_id {
c.app_id = a;
changed = true;
}
if let Some(k) = key {
c.key_path = k;
changed = true;
}
if let Some(i) = installation_id {
c.installation_id = Some(i);
changed = true;
}
if changed {
ghapp::save_config(&c)?;
}
if find_installation {
match ghapp::find_installation(&c) {
Ok(id) => {
println!("found installation id: {id}");
c.installation_id = Some(id);
ghapp::save_config(&c)?;
}
Err(e) => eprintln!(
"confer: {e}\n(config saved; install the App on your repos, then re-run `confer app-config --find-installation`)"
),
}
}
println!(
"app_id: {}\nkey: {}\ninstallation_id: {}",
if c.app_id.is_empty() {
"(unset)"
} else {
&c.app_id
},
if c.key_path.is_empty() {
"(unset)"
} else {
&c.key_path
},
c.installation_id
.map(|i| i.to_string())
.unwrap_or_else(|| "(unset)".into()),
);
if !changed {
println!("\nwire the credential helper: git config credential.\"https://github.com\".helper \"!confer credential\"");
}
Ok(())
}
pub(crate) fn my_build() -> version::BuildId {
version::BuildId {
version: semver::Version::parse(env!("CARGO_PKG_VERSION")).ok(),
sha: BUILD_SHA.to_string(),
}
}
pub(crate) fn hub_pin(root: &std::path::Path) -> Option<version::BuildId> {
let raw = std::fs::read_to_string(root.join(".confer-version")).ok()?;
if raw.trim().is_empty() {
return None;
}
Some(version::BuildId::parse(&raw))
}
pub(crate) fn hub_require(root: &std::path::Path) -> Option<semver::VersionReq> {
let raw = std::fs::read_to_string(root.join(".confer-require")).ok()?;
semver::VersionReq::parse(raw.trim()).ok()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config_hub::short12;
use crate::identity::parse_card;
use crate::join::published_pubkey;
use crate::projection::request_status;
use crate::transport::{clone_url_candidates, parse_remote, Scheme};
#[test]
fn short12_is_char_boundary_safe() {
assert_eq!(short12("0123456789abcdef"), "0123456789ab"); assert_eq!(short12("abc"), "abc"); let multibyte = "aaaaaaaaaaaé"; assert_eq!(short12(multibyte), multibyte); }
#[test]
fn slug_rules() {
for ok in ["carol", "cover-restoration", "a1", "x", "all"] {
assert!(valid_slug(ok), "{ok} should be valid");
}
for bad in [
"",
"-x",
"A",
"a/b",
"../x",
"a b",
"a_b",
"a.",
&"a".repeat(65),
] {
assert!(!valid_slug(bad), "{bad:?} should be invalid");
}
}
#[test]
fn all_is_a_valid_slug_but_reserved_as_an_identity() {
assert!(valid_slug("all"));
assert!(is_reserved_name("all"));
assert!(!is_reserved_name("carol"));
}
#[test]
fn truncate_clips_at_word_boundary_not_midword() {
let s = "alpha beta gamma delta epsilon zeta"; assert_eq!(truncate(s, 100), s); let t = truncate(s, 14); assert!(t.ends_with('…'));
assert!(!t.contains("gam…"), "must not clip mid-word: {t}");
assert!(t.starts_with("alpha beta"));
let big = "supercalifragilisticexpialidocious";
assert_eq!(truncate(big, 5), "super…");
}
#[test]
fn short_id_takes_trailing_six() {
assert_eq!(short_id("01J8Z9K3QH7X4Q9W0C"), "4Q9W0C");
assert_eq!(short_id("abc"), "abc");
}
#[test]
fn id_match_prefix_suffix_and_exact() {
let full = "01J8Z9K3QH7X4Q9W0C";
assert!(id_matches(full, full));
assert!(id_matches(full, "4Q9W0C")); assert!(id_matches(full, "01J8Z9")); assert!(!id_matches(full, "ZZZ"));
}
fn tmsg(msg_type: &str, id: &str, of: Option<&str>) -> Message {
Message {
front: Frontmatter {
id: id.into(),
from: "x".into(),
msg_type: msg_type.into(),
ts: "t".into(),
host: None,
to: vec![],
cc: vec![],
priority: None,
topic: None,
reply_to: None,
of: of.map(String::from),
supersedes: None,
resolution: None,
defer: false,
via: None,
src: None,
summary: Some("s".into()),
refs: vec![],
},
body: String::new(),
}
}
#[test]
fn accessible_to_empty_is_hubwide_else_listed() {
use crate::repos::{accessible_to, Repo};
let open = Repo::default(); assert!(accessible_to(&open, "anyone"));
let restricted = Repo {
access: vec!["bob".into(), "alice".into()],
..Default::default()
};
assert!(accessible_to(&restricted, "bob"));
assert!(!accessible_to(&restricted, "sister-bot"));
let everyone = Repo {
access: vec!["all".into()],
..Default::default()
};
assert!(accessible_to(&everyone, "sister-bot"));
}
#[test]
fn parse_remote_canonicalizes_github_forms() {
let ssh = "git@github.com:codeshrew/team-hub.git";
let https = "https://github.com/codeshrew/team-hub.git";
for input in [
ssh,
https,
"https://github.com/codeshrew/team-hub",
"codeshrew/team-hub",
] {
let r = parse_remote(input);
assert_eq!(r.ssh.as_deref(), Some(ssh), "ssh from {input}");
assert_eq!(r.https.as_deref(), Some(https), "https from {input}");
assert_eq!(
r.shorthand.as_deref(),
Some("codeshrew/team-hub"),
"shorthand from {input}"
);
}
let gl = parse_remote("git@gitlab.com:team/hub.git");
assert_eq!(gl.ssh.as_deref(), Some("git@gitlab.com:team/hub.git"));
assert_eq!(gl.shorthand, None);
let local = parse_remote("/srv/hubs/team-hub.git");
assert_eq!(local.ssh, None);
assert_eq!(local.https, None);
assert_eq!(local.raw, "/srv/hubs/team-hub.git");
}
#[test]
fn parse_card_fails_closed_on_unparsable_frontmatter() {
let (m, body) = parse_card("---\ndisplay: alice\npubkey: ssh-ed25519 AAA\n---\nhello").unwrap();
assert_eq!(m.get("display").and_then(|v| v.as_str()), Some("alice"));
assert_eq!(body, "hello");
let (m, _) = parse_card("just a body, no fence").unwrap();
assert!(m.is_empty());
let (m, _) = parse_card("---\n---\n").unwrap();
assert!(m.is_empty());
assert!(
parse_card("---\ndisplay: a\ndisplay: b\n---\n").is_err(),
"duplicate frontmatter key must fail closed, not read as empty"
);
assert!(
parse_card("---\n: : : not yaml\n---\n").is_err(),
"malformed frontmatter must fail closed"
);
let (m, _) = parse_card("\u{FEFF}---\npubkey: ssh-ed25519 AAA\ndisplay: v\n---\n").unwrap();
assert_eq!(
m.get("pubkey").and_then(|v| v.as_str()),
Some("ssh-ed25519 AAA"),
"a BOM before --- must not hide the pubkey"
);
}
#[test]
fn published_pubkey_classifies_and_fails_closed_on_type_confusion() {
let pk = |yaml: &str| {
let m: serde_yaml::Mapping = serde_yaml::from_str(yaml).unwrap();
published_pubkey(&m)
};
assert_eq!(
pk("pubkey: ssh-ed25519 AAAA").unwrap(),
Some("ssh-ed25519 AAAA".to_string())
);
assert_eq!(pk("display: x").unwrap(), None);
assert_eq!(pk("pubkey: null").unwrap(), None);
assert_eq!(pk("pubkey: \"\"").unwrap(), None);
assert!(pk("pubkey: [a, b]").is_err(), "list pubkey must fail closed");
assert!(pk("pubkey: 123").is_err(), "number pubkey must fail closed");
assert!(pk("pubkey: true").is_err(), "bool pubkey must fail closed");
}
#[test]
fn canonical_hub_id_matches_same_hub_across_scheme_case_host() {
let want = Some("github.com/codeshrew/confer-lab".to_string());
for input in [
"git@github.com:codeshrew/confer-lab.git",
"https://github.com/codeshrew/confer-lab",
"https://github.com/codeshrew/confer-lab.git/",
"codeshrew/confer-lab",
"https://github.com/CodeShrew/Confer-Lab", ] {
assert_eq!(canonical_hub_id(input), want, "canonical of {input}");
}
assert_eq!(
canonical_hub_id("git@git.example.com:org/hub.git"),
canonical_hub_id("https://git.example.com/org/hub"),
"self-hosted ssh vs https must match"
);
assert_eq!(
canonical_hub_id("ssh://git@git.example.com:2222/org/hub.git"),
Some("git.example.com/org/hub".to_string()),
":port and user@ are stripped"
);
}
#[test]
fn canonical_hub_id_does_not_false_match_different_hubs() {
assert_ne!(
canonical_hub_id("orgA/hub"),
canonical_hub_id("orgB/hub"),
"different org must not match"
);
assert_ne!(
canonical_hub_id("git@github.com:o/hub.git"),
canonical_hub_id("git@gitlab.com:o/hub.git"),
"different host must not match"
);
let real = canonical_hub_id("/srv/hubs/myhub.git");
assert_ne!(real, canonical_hub_id("/other/place/myhub.git"), "same basename, different path");
assert_eq!(canonical_hub_id("myhub"), None, "a bare non-owner/repo word is not a hub ref");
assert_ne!(real, canonical_hub_id("/srv/hubs/aaamyhub.git"), "aaamyhub must not match myhub");
assert_ne!(real, canonical_hub_id("/srv/hubs/notmyhub.git"), "notmyhub must not match myhub");
}
#[test]
fn clone_url_candidates_honor_typed_scheme() {
let r = parse_remote("https://github.com/o/repo.git");
let c = clone_url_candidates("https://github.com/o/repo.git", &r, Scheme::Auto);
assert_eq!(c[0], "https://github.com/o/repo.git");
assert_eq!(c[1], "git@github.com:o/repo.git");
let r2 = parse_remote("git@github.com:o/repo.git");
let c2 = clone_url_candidates("git@github.com:o/repo.git", &r2, Scheme::Auto);
assert_eq!(c2[0], "git@github.com:o/repo.git");
assert_eq!(c2[1], "https://github.com/o/repo.git");
let c3 = clone_url_candidates("git@github.com:o/repo.git", &r2, Scheme::Https);
assert_eq!(c3, vec!["https://github.com/o/repo.git".to_string()]);
let r4 = parse_remote("o/repo");
assert_eq!(clone_url_candidates("o/repo", &r4, Scheme::Auto).len(), 2);
}
#[test]
fn clone_candidates_respect_scheme_and_fallback() {
let r = parse_remote("codeshrew/team-hub");
assert_eq!(
clone_candidates(&r, Scheme::Ssh),
vec![r.ssh.clone().unwrap()]
);
assert_eq!(
clone_candidates(&r, Scheme::Https),
vec![r.https.clone().unwrap()]
);
assert_eq!(clone_candidates(&r, Scheme::Auto).len(), 2);
let local = parse_remote("/srv/hubs/x.git");
assert_eq!(
clone_candidates(&local, Scheme::Auto),
vec!["/srv/hubs/x.git".to_string()]
);
}
#[test]
fn empty_reference_folds_against_nothing() {
let a = "01AAAAAAAAAAAAAAAAAAAAAREQ1";
let done = tmsg("done", "01DDDDDDDDDDDDDDDDDDDDDDON1", Some(""));
let mut sup = tmsg("supersede", "01SSSSSSSSSSSSSSSSSSSSSSUP1", None);
sup.front.supersedes = Some(String::new());
let msgs = vec![tmsg("request", a, None), done, sup];
assert_eq!(request_status(&msgs, a), "OPEN");
assert!(claimants(&msgs, a).is_empty());
assert!(superseded_set(&msgs).is_empty());
}
#[test]
fn leading_prefix_does_not_crosscontaminate() {
let a = "01KX2YTCAX0000000000000001";
let b = "01KX2YTCKY0000000000000002";
let full = vec![
tmsg("request", a, None),
tmsg("request", b, None),
tmsg("done", "01DDDDDDDDDDDDDDDDDDDDDDON1", Some(a)),
];
assert_eq!(request_status(&full, a), "DONE");
assert_eq!(request_status(&full, b), "OPEN");
let pfx = vec![
tmsg("request", a, None),
tmsg("request", b, None),
tmsg("done", "01DDDDDDDDDDDDDDDDDDDDDDON2", Some("01KX2YTC")),
];
assert_eq!(request_status(&pfx, a), "OPEN");
assert_eq!(request_status(&pfx, b), "OPEN");
}
#[test]
fn id_ref_matches_is_strict_but_id_matches_is_lenient() {
let full = "01KX2YTCAX0000000000000001";
assert!(id_ref_matches(full, full)); assert!(id_ref_matches(full, "0000000000000001")); assert!(!id_ref_matches(full, "")); assert!(!id_ref_matches(full, "01KX2YTC")); assert!(!id_ref_matches(full, "0001")); assert!(!id_matches(full, "")); assert!(id_matches(full, "01KX2YTC")); }
#[test]
fn resolve_unique_errors_on_ambiguity_and_miss() {
let a = "01KX2YTCAX0000000000000001";
let b = "01KX2YTCKY0000000000000002";
let msgs = vec![tmsg("request", a, None), tmsg("request", b, None)];
assert_eq!(resolve_unique(&msgs, a).unwrap(), a);
assert!(resolve_unique(&msgs, "01KX2YTC").is_err()); assert!(resolve_unique(&msgs, "zzzzzz").is_err()); }
#[test]
fn claimants_lists_distinct_roles_in_fold_order() {
let req = "01AAAAAAAAAAAAAAAAAAAAAREQ1";
let msgs = vec![
tmsg("request", req, None),
{
let mut m = tmsg("claim", "01C1CCCCCCCCCCCCCCCCCCCLM1", Some(req));
m.front.from = "carol".into();
m
},
{
let mut m = tmsg("claim", "01C2CCCCCCCCCCCCCCCCCCCLM2", Some(req));
m.front.from = "bob".into();
m
},
{
let mut m = tmsg("claim", "01C3CCCCCCCCCCCCCCCCCCCLM3", Some(req));
m.front.from = "carol".into();
m
},
];
assert_eq!(claimants(&msgs, req), vec!["carol", "bob"]);
assert!(claimants(&msgs, "01ZZZZZZZZZZZZZZZZZZZZZNONE").is_empty());
}
#[test]
fn request_status_folds_done_over_claim_over_open() {
let r1 = "01AAAAAAAAAAAAAAAAAAAAAREQ1";
let r2 = "01BBBBBBBBBBBBBBBBBBBBBREQ2";
let msgs = vec![
tmsg("request", r1, None),
tmsg("claim", "01C1CCCCCCCCCCCCCCCCCCCLM1", Some(r1)),
tmsg("done", "01D1DDDDDDDDDDDDDDDDDDDON1", Some(r1)),
tmsg("request", r2, None),
tmsg("claim", "01C2CCCCCCCCCCCCCCCCCCCLM2", Some(r2)),
];
assert_eq!(request_status(&msgs, r1), "DONE");
assert_eq!(request_status(&msgs, r2), "CLAIMED");
assert_eq!(request_status(&msgs, "01ZZZZZZZZZZZZZZZZZZZZZREQ3"), "OPEN");
}
}