#![warn(clippy::too_many_lines)]
mod alias;
#[cfg(feature = "serve")]
mod api;
mod append;
mod append_ref;
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 patch;
mod pollcmd;
mod presence;
mod projection;
mod reconnect;
mod refcmd;
mod refcode;
mod repomap;
mod repos;
mod reposdiscover;
mod roster;
mod schema;
mod screen;
mod secrets;
mod arm;
mod seen;
#[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_create, cmd_lifecycle, cmd_suggest, 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, cmd_threads};
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,
};
#[cfg(test)]
use projection::claimants;
#[cfg(test)]
use projection::id_ref_matches;
#[cfg(test)]
use schema::Frontmatter;
use schema::Message;
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 => {
if crate::envelope::looks_like_wrapper_paste(query) {
Err(anyhow!(
"'{query}' looks like the `⟦untrusted:…⟧` frame around a peer message, not a \
message id — that token is a per-render random nonce (it changes every time \
the message is shown), not the id. Use the short id shown by `confer inbox \
--peek` / `confer read` instead."
))
} else {
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,
refcmd::render_refs(&m.front.refs),
),
false,
)
}
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 screen_note(m: &Message, tier: Option<tiers::Tier>) -> Option<String> {
let v = screen::heuristic(&screen::Input {
body: &m.body,
from_role: &m.front.from,
tier,
refs: vec![],
});
match v.level {
screen::Level::Allow => None,
_ => Some(format!("⚠ possible injection ({})", v.category.unwrap_or("?"))),
}
}
pub(crate) fn to_json(
m: &Message,
trust: &verify::Trust,
tier: Option<tiers::Tier>,
screen: Option<&str>,
) -> Result<String> {
let mut v = serde_json::to_value(&m.front)?;
if let serde_json::Value::Object(map) = &mut v {
map.insert("event".into(), serde_json::Value::String("message".into()));
map.insert("body".into(), serde_json::Value::String(m.body.clone()));
map.insert(
"trust".into(),
serde_json::json!({
"status": trust.status_str(),
"fpr": trust.fpr(),
"detail": trust.tag(),
}),
);
map.insert(
"tier".into(),
tier.map_or(serde_json::Value::Null, |t| serde_json::Value::String(t.as_str().into())),
);
map.insert(
"screen".into(),
screen.map_or(serde_json::Value::Null, |s| serde_json::Value::String(s.into())),
);
}
Ok(serde_json::to_string(&v)?)
}
#[derive(Debug)]
pub(crate) struct PredicateFalse;
impl std::fmt::Display for PredicateFalse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("predicate not satisfied")
}
}
impl std::error::Error for PredicateFalse {}
#[derive(Debug)]
pub(crate) struct StopHookBlock;
impl std::fmt::Display for StopHookBlock {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("stop-hook: new mail")
}
}
impl std::error::Error for StopHookBlock {}
#[derive(Debug)]
pub(crate) struct AlreadyLanded;
impl std::fmt::Display for AlreadyLanded {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("already landed")
}
}
impl std::error::Error for AlreadyLanded {}
fn main() -> std::process::ExitCode {
use std::process::ExitCode;
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(e) if e.is::<PredicateFalse>() => ExitCode::from(1),
Err(e) if e.is::<StopHookBlock>() => ExitCode::from(2),
Err(e) if e.is::<AlreadyLanded>() => ExitCode::from(2),
Err(e) => {
eprintln!("Error: {e:?}");
ExitCode::from(3)
}
}
}
fn apply_hub_selector(sel: &str) -> Result<()> {
let p = std::path::Path::new(sel);
if p.is_dir() && (p.join(".git").exists() || p.join(".confer-version").exists()) {
std::env::set_var("CONFER_HUB", p.canonicalize()?);
return Ok(());
}
let hub_label = |h: &std::path::Path| -> String {
gitcmd::output(h, &["config", "--get", "remote.origin.url"])
.ok()
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.filter(|u| !u.is_empty())
.map(|u| {
u.rsplit('/')
.next()
.unwrap_or(u.as_str())
.trim_end_matches(".git")
.to_string()
})
.unwrap_or_else(|| {
h.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default()
})
};
let needle = sel.to_lowercase();
let mut by_label: std::collections::BTreeMap<String, std::path::PathBuf> = Default::default();
for h in crosshub::hub_dirs() {
let entry = by_label.entry(hub_label(&h)).or_insert_with(|| h.clone());
if !entry.exists() && h.exists() {
*entry = h; }
}
let matches: Vec<(String, std::path::PathBuf)> = by_label
.into_iter()
.filter(|(l, _)| l.to_lowercase().contains(&needle))
.collect();
match matches.as_slice() {
[] => Err(anyhow!(
"no hub matches '{sel}' — see `confer hubs` for the hubs on this machine"
)),
[(_, path)] => {
std::env::set_var("CONFER_HUB", path);
Ok(())
}
many => Err(anyhow!(
"'{sel}' matches {} hubs ({}) — be more specific",
many.len(),
many.iter().map(|(l, _)| l.as_str()).collect::<Vec<_>>().join(", ")
)),
}
}
fn run() -> Result<()> {
let cli = Cli::parse();
if let Some(sel) = &cli.hub {
apply_hub_selector(sel)?;
}
match cli.cmd {
Cmd::Join {
role,
host,
display,
desc,
signing_key,
force,
} => cmd_join(role, host, display, desc, signing_key, force),
Cmd::Append {
msg_type,
text,
body_file,
summary,
summary_file,
to,
cc,
priority,
topic,
reply_to,
of,
supersedes,
from,
src,
refs,
allow_empty_body,
resolution,
defer,
allow_secret,
ref_from,
allow_dirty,
patch,
patch_repo,
allow_large_patch,
} => cmd_append(AppendArgs {
msg_type,
text,
body_file,
summary,
summary_file,
to,
cc,
priority,
topic,
reply_to,
of,
supersedes,
from,
src,
refs,
allow_empty_body,
allow_secret,
resolution,
defer,
ref_from,
allow_dirty,
patch,
patch_repo,
allow_large_patch,
}),
Cmd::Request { args, reply_to } => cmd_create("request", args, reply_to),
Cmd::Note { args } => cmd_create("note", args, None),
Cmd::Claim { mut args } => {
args.of = args.resolved_of()?;
cmd_lifecycle("claim", args, None)
}
Cmd::Done { mut args, resolution } => {
args.of = args.resolved_of()?;
cmd_lifecycle("done", args, resolution)
}
Cmd::Error { mut args } => {
args.of = args.resolved_of()?;
cmd_lifecycle("error", args, None)
}
Cmd::Blocked { mut args } => {
args.of = args.resolved_of()?;
cmd_lifecycle("blocked", args, None)
}
Cmd::Defer { mut args } => {
args.of = args.resolved_of()?;
cmd_lifecycle("defer", args, None)
}
Cmd::Suggest { args } => cmd_suggest(args),
Cmd::Apply { id, check, repo_dir, force } => patch::cmd_apply(id, check, repo_dir, force),
Cmd::Poll {
advance,
topic,
hook,
json,
role,
all,
to_me,
..
} => {
let r = pollcmd::cmd_poll(pollcmd::PollArgs {
advance,
topic,
hook,
json,
role,
all,
to_me,
});
if hook {
match r {
Err(e) if !e.is::<StopHookBlock>() => {
warn_safety(format!(
"poll --hook: {e:#} — continuing without blocking the stop (fail-open)"
));
Ok(())
}
other => other,
}
} else {
r
}
}
Cmd::Show { id, json } => cmd_show(id, json),
Cmd::Requests {
open,
mine,
role,
json,
backlog,
blocked,
} => cmd_requests(open, mine, role, json, backlog, blocked),
Cmd::Thread { id, full, json } => cmd_thread(id, full, json),
Cmd::Topics {
open,
closed,
stale,
json,
} => cmd_threads(open, closed, stale, json),
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, out } => cmd_keygen(role, out, 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 { action, json } => match action {
Some(cli::ReposAction::Map { slug, path }) => refcmd::cmd_repos_map(slug, path),
Some(cli::ReposAction::Discover { root }) => refcmd::cmd_repos_discover(root),
None => cmd_repos(json),
},
Cmd::Refs { target, check, all_hubs, json } => refcmd::cmd_refs(target, check, all_hubs, json),
Cmd::RefContains { sha, against, repo } => refcmd::cmd_ref_contains(sha, against, repo),
Cmd::Verify { id, strict } => cmd_verify(id, strict),
Cmd::ConfirmKey { role } => cmd_confirm_key(role),
Cmd::Doctor { dir, fix, json, check } => cmd_doctor(dir, fix, json, check),
Cmd::Trust { tier } => cmd_trust(tier),
Cmd::Screen { corpus, text } => cmd_screen(corpus, text),
Cmd::Seen { id, json } => cmd_seen(id, json),
Cmd::Inbox { role, peek, json } => cmd_inbox(role, peek, json),
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,
} => ghapp::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,
wake_on,
no_version_notice,
delivery,
..
} => {
let root = config::repo_root()?;
let hub_key = config::hub_key(&root);
let resolved_role = config::resolve_role(role.clone(), &root).unwrap_or_default();
let (wake_on, min_priority, topic, all) = watch::resolve_watch_prefs(
&hub_key,
&resolved_role,
wake_on.as_deref(),
min_priority.as_deref(),
topic.as_deref(),
all,
)?;
watch::run(watch::WatchOpts {
topic,
role,
json,
poll_secs,
advance: !no_advance,
replace,
all,
min_priority,
wake_on,
no_version_notice,
delivery,
})
}
Cmd::Arm { role, topic, all, min_priority, wake_on } => {
arm::run(role, topic, all, min_priority, wake_on)
}
Cmd::WatchStatus { role, json, check } => watch::cmd_watch_status(role, json, check),
Cmd::Status { json } => cmd_status(json),
#[cfg(feature = "dashboard")]
Cmd::Dashboard { all_hubs } => dashboard::cmd_dashboard(all_hubs),
#[cfg(feature = "serve")]
Cmd::Serve { all_hubs, lan, port, bind } => {
let env_port = std::env::var("CONFER_SERVE_PORT").ok().and_then(|s| s.parse().ok());
let bind = serve::resolve_bind(&serve::BindFlags { bind, lan, port, env_port });
serve::run(crosshub::resolve_hubs(all_hubs)?, &bind, all_hubs)
}
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 { json } => cmd_who(json),
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 {
id: Option<String>,
#[arg(long, default_value = "")]
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>,
#[arg(long = "ref-from")]
ref_from: Option<String>,
#[arg(long = "allow-dirty")]
allow_dirty: bool,
#[arg(long = "body-file")]
body_file: Option<String>,
}
impl LifecycleArgs {
fn resolved_of(&self) -> Result<String> {
let of = self.of.trim();
match (self.id.as_deref().map(str::trim), of) {
(Some(pos), of) if !pos.is_empty() && !of.is_empty() && pos != of => Err(anyhow!(
"conflicting request id: positional '{pos}' vs --of '{of}' — pass just one"
)),
(Some(pos), _) if !pos.is_empty() => Ok(pos.to_string()),
(_, of) if !of.is_empty() => Ok(of.to_string()),
_ => Err(anyhow!("a request id is required: pass it positionally or via --of")),
}
}
}
#[derive(clap::Args)]
pub(crate) struct CreateArgs {
#[arg(long)]
summary: String,
#[arg(long)]
text: Option<String>,
#[arg(long = "to")]
to: Vec<String>,
#[arg(long = "cc")]
cc: Vec<String>,
#[arg(long)]
priority: Option<String>,
#[arg(long)]
topic: Option<String>,
#[arg(long)]
from: Option<String>,
#[arg(long)]
src: Option<String>,
#[arg(long = "ref")]
refs: Vec<String>,
#[arg(long)]
allow_empty_body: bool,
#[arg(long)]
defer: bool,
#[arg(long = "allow-secret")]
allow_secret: bool,
#[arg(long = "ref-from")]
ref_from: Option<String>,
#[arg(long = "allow-dirty")]
allow_dirty: bool,
#[arg(long)]
patch: Option<String>,
#[arg(long = "repo")]
patch_repo: Option<String>,
#[arg(long = "allow-large-patch")]
allow_large_patch: bool,
#[arg(long = "body-file")]
body_file: Option<String>,
}
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, superseded_set};
use crate::transport::{clone_url_candidates, parse_remote, Scheme};
#[test]
fn to_json_is_valid_json_carrying_verified_provenance() {
let m = tmsg("note", "01AAAAAAAAAAAAAAAAAAAAAAAA", None);
let cases = [
(verify::Trust::Verified { fpr: "SHA256:abc".into() }, "verified", true),
(verify::Trust::FirstSight { fpr: "SHA256:def".into() }, "first-sight", true),
(verify::Trust::Unverified { reason: "unsigned".into() }, "unverified", false),
(verify::Trust::Mismatch { reason: "key changed".into() }, "mismatch", false),
];
for (trust, want_status, has_fpr) in cases {
let s = to_json(&m, &trust, Some(tiers::Tier::Foreign), Some("⚠ possible injection (x)"))
.unwrap();
let v: serde_json::Value = serde_json::from_str(&s)
.unwrap_or_else(|e| panic!("to_json must emit parseable JSON ({e}): {s}"));
assert_eq!(v["event"], "message", "stream discriminator");
assert_eq!(v["trust"]["status"], want_status);
assert_eq!(v["trust"]["fpr"].is_string(), has_fpr, "fpr for {want_status}");
assert_eq!(v["tier"], "foreign");
assert_eq!(v["screen"], "⚠ possible injection (x)");
assert!(v["from"].is_string(), "self-declared from still present");
assert!(v["body"].is_string());
}
}
#[test]
fn to_json_mismatch_is_detectable_by_a_machine_consumer() {
let m = tmsg("note", "01BBBBBBBBBBBBBBBBBBBBBBBB", None);
let s = to_json(&m, &verify::Trust::Mismatch { reason: "the role's key changed".into() }, None, None)
.unwrap();
let v: serde_json::Value = serde_json::from_str(&s).unwrap();
assert_eq!(v["trust"]["status"], "mismatch");
assert_eq!(v["tier"], serde_json::Value::Null);
assert_eq!(v["screen"], serde_json::Value::Null);
assert!(v["trust"]["fpr"].is_null(), "a mismatch has no trustworthy fingerprint");
}
#[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");
}
}