#![allow(clippy::unwrap_used)]
use assert_cmd::Command;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn release_body(tag: &str) -> serde_json::Value {
serde_json::json!({ "tag_name": tag, "assets": [] })
}
fn newer_tag() -> String {
let current = env!("CARGO_PKG_VERSION");
let major: u64 = current.split('.').next().unwrap().parse().unwrap();
format!("v{}.0.0", major + 1)
}
fn bb(api: &str) -> (Command, tempfile::TempDir) {
let cfg = tempfile::tempdir().unwrap();
let mut cmd = Command::cargo_bin("bb").unwrap();
cmd.env("HOME", cfg.path())
.env("XDG_CONFIG_HOME", cfg.path())
.env("BB_UPDATE_API_BASE", api)
.env("BB_KEYRING_DISABLE", "1")
.env("BB_SKILL_NO_AUTO_REFRESH", "1")
.env("NO_COLOR", "1");
(cmd, cfg)
}
async fn releases(tag: &str, expect: u64) -> MockServer {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/repos/biokraft/bbcloud/releases/latest"))
.respond_with(ResponseTemplate::new(200).set_body_json(release_body(tag)))
.expect(expect)
.mount(&server)
.await;
server
}
#[tokio::test]
async fn a_newer_release_is_announced_on_stderr() {
let server = releases(&newer_tag(), 1).await;
let (mut cmd, _cfg) = bb(&server.uri());
let out = cmd.args(["skill", "status"]).output().unwrap();
let stderr = String::from_utf8_lossy(&out.stderr);
let expected = newer_tag().trim_start_matches('v').to_string();
assert!(
stderr.contains(&format!("bb {expected} is available")),
"stderr must carry the notice, got: {stderr}"
);
assert!(
stderr.contains("upgrade with:"),
"the notice must name the upgrade command, got: {stderr}"
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
!stdout.contains("is available"),
"stdout must stay clean, got: {stdout}"
);
}
#[tokio::test]
async fn json_mode_keeps_stdout_pure_and_still_notifies() {
let server = releases(&newer_tag(), 1).await;
let (mut cmd, _cfg) = bb(&server.uri());
let out = cmd.args(["skill", "status", "--json"]).output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
serde_json::from_str::<serde_json::Value>(stdout.trim())
.unwrap_or_else(|err| panic!("stdout must be a bare json value ({err}): {stdout}"));
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("is available"),
"an agent reads stderr, so --json must still be told, got: {stderr}"
);
}
#[tokio::test]
async fn the_running_version_is_not_announced() {
let server = releases(&format!("v{}", env!("CARGO_PKG_VERSION")), 1).await;
let (mut cmd, _cfg) = bb(&server.uri());
let out = cmd.args(["skill", "status"]).output().unwrap();
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("is available"),
"an up-to-date install must stay silent, got: {stderr}"
);
}
#[tokio::test]
async fn a_fresh_cache_makes_no_second_request() {
let server = releases(&newer_tag(), 1).await;
let (mut first, cfg) = bb(&server.uri());
first.args(["skill", "status"]).output().unwrap();
let mut second = Command::cargo_bin("bb").unwrap();
let out = second
.env("HOME", cfg.path())
.env("XDG_CONFIG_HOME", cfg.path())
.env("BB_UPDATE_API_BASE", server.uri())
.env("BB_KEYRING_DISABLE", "1")
.env("BB_SKILL_NO_AUTO_REFRESH", "1")
.env("NO_COLOR", "1")
.args(["skill", "status"])
.output()
.unwrap();
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("is available"),
"the cached answer must still be announced, got: {stderr}"
);
assert!(
cfg.path().join("bb").join("update-check.json").exists(),
"the check must record its answer"
);
drop(server);
}
#[tokio::test]
async fn the_kill_switch_makes_no_request_and_prints_nothing() {
let server = releases(&newer_tag(), 0).await;
let (mut cmd, _cfg) = bb(&server.uri());
let out = cmd
.env("BB_NO_UPDATE_CHECK", "1")
.args(["skill", "status"])
.output()
.unwrap();
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("is available"),
"BB_NO_UPDATE_CHECK must silence the notice, got: {stderr}"
);
}
#[tokio::test]
async fn an_unreachable_release_api_is_silent() {
let (mut cmd, _cfg) = bb("http://127.0.0.1:1");
let out = cmd.args(["skill", "status"]).output().unwrap();
assert!(out.status.success(), "the check must never fail a command");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("is available") && !stderr.contains("error"),
"an unreachable release api must be swallowed, got: {stderr}"
);
}
#[tokio::test]
async fn bb_update_does_not_also_run_the_passive_check() {
let server = releases(&format!("v{}", env!("CARGO_PKG_VERSION")), 1).await;
let (mut cmd, _cfg) = bb(&server.uri());
let out = cmd.args(["update", "--json"]).output().unwrap();
assert!(out.status.success(), "update must succeed");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("is available"),
"bb update reports the version itself, got: {stderr}"
);
}