use std::{
os::unix::fs::PermissionsExt as _,
path::{Path, PathBuf},
process::{Command, Output},
};
const CRATES: &str = "scv-core, scv-protocol, scv-client, scv-provider-openai, scv-tools, \
scv-channels, scv-server, scv-tui, scv-cli";
struct Checkout {
root: tempfile::TempDir,
}
impl Checkout {
fn new(answer: i32) -> Self {
Self::with_scv(answer, true)
}
fn with_scv(answer: i32, confirms: bool) -> Self {
let root = tempfile::tempdir().unwrap();
let checkout = Self { root };
let bin = checkout.path("bin");
std::fs::create_dir_all(&bin).unwrap();
let log = checkout.path("log");
std::fs::create_dir_all(&log).unwrap();
for (name, script) in [
("curl", "printf 404\n".to_owned()),
(
"scv",
format!(
"if [ \"$1\" = --version ]; then echo 'scv 0.2.1'; exit 0; fi\n\
if [ \"$1\" = confirm ] && [ \"$2\" = --help ]; then {}; fi\n\
printf '%s\\n' \"$@\" > '{}/scv'\nexit {answer}\n",
if confirms {
"exit 0"
} else {
"echo \"error: unrecognized subcommand 'confirm'\" >&2; exit 2"
},
log.display()
),
),
(
"cargo",
format!("printf '%s\\n' \"$*\" >> '{}/cargo'\n", log.display()),
),
] {
let path = bin.join(name);
std::fs::write(&path, format!("#!/bin/sh\n{script}")).unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
}
let repo = checkout.path("repo");
std::fs::create_dir_all(&repo).unwrap();
std::fs::write(
repo.join("Cargo.toml"),
"[workspace.package]\nversion = \"9.9.9\"\n",
)
.unwrap();
checkout.git(&["init", "--bare", "--quiet", "../origin.git"]);
checkout.git(&["init", "--quiet"]);
checkout.git(&["add", "Cargo.toml"]);
checkout.git(&["commit", "--quiet", "-m", "feat: the release"]);
checkout.git(&["remote", "add", "origin", "../origin.git"]);
checkout.git(&["push", "--quiet", "origin", "HEAD:main"]);
checkout.git(&["fetch", "--quiet", "origin"]);
checkout
}
fn path(&self, name: &str) -> PathBuf {
self.root.path().join(name)
}
fn command(&self, program: &str) -> Command {
let mut command = Command::new(program);
let path = std::env::var("PATH").unwrap_or_else(|_| "/usr/bin:/bin".into());
command
.env_clear()
.env("PATH", format!("{}:{path}", self.path("bin").display()))
.env("HOME", self.root.path())
.env("SCV_HOME", self.path("scv-home"))
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_AUTHOR_NAME", "Test")
.env("GIT_AUTHOR_EMAIL", "test@example.com")
.env("GIT_COMMITTER_NAME", "Test")
.env("GIT_COMMITTER_EMAIL", "test@example.com")
.current_dir(self.path("repo"));
command
}
fn git(&self, args: &[&str]) -> String {
let output = self
.command("git")
.args(["-c", "init.defaultBranch=main"])
.args(args)
.output()
.unwrap();
assert!(output.status.success(), "git {args:?}: {output:?}");
String::from_utf8(output.stdout).unwrap()
}
fn publish(&self, args: &[&str], parent: Option<&str>) -> Output {
let script = Path::new(env!("CARGO_MANIFEST_DIR"))
.join(".agents/skills/feature-flow/scripts/publish.sh");
let mut command = self.command("bash");
command.arg(script).args(args);
if let Some(parent) = parent {
command.env("SCV_PARENT", parent);
}
command.output().unwrap()
}
fn logged(&self, name: &str) -> Option<String> {
std::fs::read_to_string(self.path("log").join(name)).ok()
}
}
const PARENT: &str = "0a1b2c3d/session/codex-3f9a2c";
#[test]
fn a_delegated_publish_asks_the_owner_first_and_publishes_on_yes() {
let checkout = Checkout::new(0);
let output = checkout.publish(&[], Some(PARENT));
assert!(output.status.success(), "{output:?}");
let head = checkout.git(&["log", "-1", "--format=%h"]);
let asked = checkout.logged("scv").unwrap();
assert_eq!(
asked,
format!(
"confirm\n--timeout\n1800\n--\nPublish SCV 9.9.9 to crates.io from origin/main {} \
feat: the release?\nCrates: {CRATES}.\nPublishing cannot be undone.\n",
head.trim()
)
);
let published = checkout.logged("cargo").unwrap();
assert_eq!(published.lines().count(), 9, "{published}");
assert_eq!(
published.lines().next(),
Some("publish --locked -p scv-core")
);
}
#[test]
fn a_delegated_publish_stops_unless_the_owner_says_yes() {
for (answer, why) in [
(1, "did not say yes"),
(2, "could not ask the owner"),
(2, "If the running daemon is too old to ask"),
] {
let checkout = Checkout::new(answer);
let output = checkout.publish(&[], Some(PARENT));
assert_eq!(output.status.code(), Some(1), "{output:?}");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains(why), "{stderr}");
assert!(stderr.contains("nothing was published"), "{stderr}");
assert!(checkout.logged("scv").is_some());
assert_eq!(checkout.logged("cargo"), None, "nothing was published");
}
}
#[test]
fn a_terminal_publish_and_a_check_never_ask() {
let checkout = Checkout::new(1);
let output = checkout.publish(&["--check"], Some(PARENT));
assert!(output.status.success(), "{output:?}");
assert_eq!(checkout.logged("scv"), None);
assert_eq!(checkout.logged("cargo"), None);
let output = checkout.publish(&[], None);
assert!(output.status.success(), "{output:?}");
assert_eq!(checkout.logged("scv"), None);
assert_eq!(checkout.logged("cargo").unwrap().lines().count(), 9);
}
#[test]
fn a_delegated_publish_with_an_scv_too_old_to_ask_says_to_publish_from_a_terminal() {
let checkout = Checkout::with_scv(0, false);
let output = checkout.publish(&[], Some(PARENT));
assert_eq!(output.status.code(), Some(1), "{output:?}");
let stderr = String::from_utf8_lossy(&output.stderr);
for why in [
"the installed scv 0.2.1 cannot ask the owner in chat",
"nothing was published",
"(0.3.0) must be published from a terminal",
"Later releases can be published from",
] {
assert!(stderr.contains(why), "{why:?} in {stderr}");
}
assert_eq!(checkout.logged("scv"), None, "no question was asked");
assert_eq!(checkout.logged("cargo"), None, "nothing was published");
}