mod common;
use std::path::PathBuf;
use common::{
FixtureRepo, append_response_log, assert_branch_shape, assert_changelog_shape,
assert_commit_shape, assert_pr_shape, load_env, run_occ, stderr, stdout,
};
fn config_arg(config_path: &PathBuf) -> [&str; 2] {
["--config", config_path.to_str().expect("utf8 config path")]
}
#[test]
fn artifacts_commit_dry_run_generates_valid_output_across_backends() {
let Some(env) = load_env() else { return };
let repo = FixtureRepo::new("e2e-cli-commit");
let diff = repo.staged_diff();
let config = config_arg(&env.config_path);
for backend in &env.active_backends {
for mode in ["adaptive", "conventional"] {
let output = run_occ(
&repo.path,
&[
"commit",
"--stdin",
"--dry-run",
"--text",
"--backend",
backend,
"--mode",
mode,
config[0],
config[1],
],
Some(&diff),
);
assert!(
output.status.success(),
"commit failed for backend={backend} mode={mode}: {}",
stderr(&output)
);
let message = stdout(&output);
assert_commit_shape(&message, mode == "conventional");
append_response_log(
"occ",
&format!("artifacts_commit_{mode}"),
"commit",
backend,
&message,
);
}
}
}
#[test]
fn refine_generates_valid_conventional_output_across_backends() {
let Some(env) = load_env() else { return };
let repo = FixtureRepo::new("e2e-cli-refine");
let diff = repo.staged_diff();
let config = config_arg(&env.config_path);
let seed = "feat: update helper";
for backend in &env.active_backends {
let output = run_occ(
&repo.path,
&[
"commit",
"--stdin",
"--dry-run",
"--text",
"--backend",
backend,
"--mode",
"conventional",
"--refine",
seed,
"--feedback",
"make it shorter and mention subtraction",
config[0],
config[1],
],
Some(&diff),
);
assert!(
output.status.success(),
"refine failed for backend={backend}: {}",
stderr(&output)
);
let message = stdout(&output);
assert_ne!(message, seed, "refine should change the message");
assert_commit_shape(&message, true);
}
}
#[test]
fn artifacts_branch_dry_run_generates_slug_across_backends() {
let Some(env) = load_env() else { return };
let repo = FixtureRepo::new("e2e-cli-branch");
let config = config_arg(&env.config_path);
for backend in &env.active_backends {
let output = run_occ(
&repo.path,
&[
"branch",
"--dry-run",
"--text",
"--backend",
backend,
"--mode",
"conventional",
config[0],
config[1],
],
None,
);
assert!(
output.status.success(),
"branch failed for backend={backend}: {}",
stderr(&output)
);
let branch_name = stdout(&output);
assert_branch_shape(&branch_name);
append_response_log("occ", "artifacts_branch", "branch", backend, &branch_name);
}
}
#[test]
fn artifacts_pr_generation_produces_structured_title_and_body_across_backends() {
let Some(env) = load_env() else { return };
let repo = FixtureRepo::new("e2e-cli-pr");
let config = config_arg(&env.config_path);
for backend in &env.active_backends {
let output = run_occ(
&repo.path,
&["pr", "--text", "--backend", backend, config[0], config[1]],
None,
);
assert!(
output.status.success(),
"pr failed for backend={backend}: {}",
stderr(&output)
);
let draft = stdout(&output);
assert_pr_shape(&draft);
append_response_log("occ", "artifacts_pr", "pr", backend, &draft);
}
}
#[test]
fn artifacts_changelog_generation_produces_sections_across_backends() {
let Some(env) = load_env() else { return };
let repo = FixtureRepo::new("e2e-cli-changelog");
let config = config_arg(&env.config_path);
for backend in &env.active_backends {
let output = run_occ(
&repo.path,
&[
"changelog",
"--text",
"--backend",
backend,
config[0],
config[1],
],
None,
);
assert!(
output.status.success(),
"changelog failed for backend={backend}: {}",
stderr(&output)
);
let entry = stdout(&output);
assert_changelog_shape(&entry);
append_response_log("occ", "artifacts_changelog", "changelog", backend, &entry);
}
}
#[test]
fn hook_install_and_uninstall_touch_the_repo_hook() {
let Some(_env) = load_env() else { return };
let repo = FixtureRepo::new("e2e-cli-hook");
let install = run_occ(&repo.path, &["hook", "install"], None);
assert!(
install.status.success(),
"hook install failed: {}",
stderr(&install)
);
assert!(repo.hook_path().exists(), "hook should exist after install");
let uninstall = run_occ(&repo.path, &["hook", "uninstall"], None);
assert!(
uninstall.status.success(),
"hook uninstall failed: {}",
stderr(&uninstall)
);
assert!(
!repo.hook_path().exists(),
"hook should be removed after uninstall"
);
}
#[test]
fn scan_detects_blocking_secret_from_stdin() {
let Some(_env) = load_env() else { return };
let repo = FixtureRepo::new("e2e-cli-scan");
let diff = "diff --git a/.env b/.env\n--- a/.env\n+++ b/.env\n@@ -0,0 +1 @@\n+AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE\n";
let output = run_occ(
&repo.path,
&[
"scan",
"--stdin",
"--format",
"text",
"--enforcement",
"block-high",
],
Some(diff),
);
assert_eq!(
output.status.code(),
Some(2),
"scan should block the secret"
);
assert!(stdout(&output).contains("AKIAIOSFODNN7EXAMPLE"));
}
#[test]
fn unreachable_custom_endpoint_fails_cleanly() {
let Some(env) = load_env() else { return };
if !env
.active_backends
.iter()
.any(|backend| backend == "custom-api")
{
return;
}
let repo = FixtureRepo::new("e2e-cli-unreachable");
let diff = repo.staged_diff();
let broken_config = repo.path.join("broken.toml");
std::fs::write(
&broken_config,
"backend = \"custom-api\"\nbackend-order = [\"custom-api\"]\n[api.custom]\nmodel = \"test-model\"\nendpoint = \"http://127.0.0.1:1\"\nkey-env = \"\"\n",
)
.expect("write broken config");
let output = run_occ(
&repo.path,
&[
"commit",
"--stdin",
"--dry-run",
"--text",
"--backend",
"custom-api",
"--config",
broken_config.to_str().expect("utf8 config path"),
],
Some(&diff),
);
assert!(!output.status.success(), "broken endpoint should fail");
assert!(
stderr(&output).contains("request failed") || stderr(&output).contains("backend error"),
"unexpected stderr: {}",
stderr(&output)
);
}