use super::*;
use crate::cmd::run::read_only_delivery;
#[test]
fn scope_cut_explicit_repository_cannot_alias_report_and_output() {
let home = tempfile::tempdir().unwrap();
let _home = paths::AidHomeGuard::set(home.path());
let repo = home.path().join("repo");
std::fs::create_dir(&repo).unwrap();
assert!(std::process::Command::new("git").args(["init", "-b", "main"])
.arg(&repo).output().unwrap().status.success());
let store = Arc::new(Store::open_memory().unwrap());
let mut args = RunArgs {
agent_name: "codex".into(), prompt: "Read-only audit".into(), read_only: true,
repo: Some(repo.display().to_string()),
output: Some(home.path().join("requested/final.md").display().to_string()),
result_file: Some("../requested/final.md".into()), ..Default::default()
};
let error = match prepare_dispatch_with(&store, &mut args, |_| true) {
Ok(_) => panic!("aliased destinations in the resolved run directory must be refused"),
Err(error) => error.to_string(),
};
assert!(error.contains("same destination"), "{error}");
assert!(!home.path().join("requested").exists());
}
#[test]
fn scope_cut_dispatch_rejects_destinations_in_explicit_repository() {
for output in [false, true] {
let home = tempfile::tempdir().unwrap();
let _home = paths::AidHomeGuard::set(home.path());
let repo = home.path().join("repo");
std::fs::create_dir(&repo).unwrap();
assert!(std::process::Command::new("git").args(["init", "-b", "main"])
.arg(&repo).output().unwrap().status.success());
let destination = repo.join("new/final.md").display().to_string();
let store = Arc::new(Store::open_memory().unwrap());
let mut args = RunArgs {
agent_name: "codex".into(), prompt: "Read-only audit".into(), read_only: true,
repo: Some(repo.display().to_string()),
output: output.then(|| destination.clone()),
result_file: (!output).then_some(destination), ..Default::default()
};
let error = match prepare_dispatch_with(&store, &mut args, |_| true) {
Ok(_) => panic!("destination inside the resolved repository must be refused"),
Err(error) => error.to_string(),
};
assert!(error.contains("artifact destination"), "{error}");
assert!(!repo.join("new").exists());
}
}
#[test]
fn scope_cut_dispatch_creates_fresh_empty_staging_and_saves_its_identity() {
let home = tempfile::tempdir().unwrap();
let _home = paths::AidHomeGuard::set(home.path());
let (first, prepared) = prepare(None, false);
let (second, _) = prepare(None, false);
let staging = std::path::Path::new(first.artifact_staging.as_ref().unwrap());
assert_ne!(first.artifact_staging, second.artifact_staging);
assert_eq!(std::fs::read_dir(staging).unwrap().count(), 0);
assert!(!staging.starts_with(first.dir.as_ref().unwrap()));
assert!(!staging.starts_with(paths::task_dir(prepared.task_id.as_str())));
for name in ["result.md", "output.md", "transcript.md"] {
assert!(!paths::task_dir(prepared.task_id.as_str()).join(name).starts_with(staging));
}
let saved = RunArgs::from_dispatch_args_json(&first.dispatch_args_json().unwrap()).unwrap();
assert_eq!(saved.artifact_staging, first.artifact_staging);
}
#[test]
fn scope_cut_dispatch_destination_validation_rejects_every_writable_root() {
let home = tempfile::tempdir().unwrap();
let _home = paths::AidHomeGuard::set(home.path());
let (mut args, _) = prepare(None, false);
let staging = std::path::PathBuf::from(args.artifact_staging.as_ref().unwrap());
let other = home.path().join("other-root");
std::fs::create_dir(&other).unwrap();
let alias = home.path().join("alias");
std::os::unix::fs::symlink(&staging, &alias).unwrap();
let roots = vec![staging.clone(), other.clone()];
for root in [staging, other, alias] {
for output in [false, true] {
let destination = root.join("new/final.md").display().to_string();
args.output = output.then(|| destination.clone());
args.result_file = (!output).then_some(destination);
let error = read_only_delivery::validate_destinations(&args, &roots, args.dir.as_deref())
.unwrap_err();
assert!(error.to_string().contains("Codex-writable root"), "{error}");
assert!(!root.join("new").exists());
}
}
}
#[test]
fn scope_cut_dispatch_rejects_repository_report_and_output_destinations() {
for output in [false, true] {
let home = tempfile::tempdir().unwrap();
let _home = paths::AidHomeGuard::set(home.path());
let repo = home.path().join("repo");
std::fs::create_dir(&repo).unwrap();
let destination = repo.join("new/final.md").display().to_string();
let store = Arc::new(Store::open_memory().unwrap());
let mut args = RunArgs {
agent_name: "codex".into(), prompt: "Read-only audit".into(), read_only: true,
dir: Some(repo.display().to_string()),
output: output.then(|| destination.clone()),
result_file: (!output).then_some(destination),
existing_task_id: Some(TaskId("t-scope-rejected".into())), ..Default::default()
};
assert!(prepare_dispatch_with(&store, &mut args, |_| true).is_err());
assert_eq!(store.get_task("t-scope-rejected").unwrap().unwrap().status, TaskStatus::Failed);
assert!(!repo.join("new").exists());
}
}
#[test]
fn scope_cut_publication_refuses_symlink_staged_report() {
let home = tempfile::tempdir().unwrap();
let repo = tempfile::tempdir().unwrap();
let _home = paths::AidHomeGuard::set(home.path());
let destination = home.path().join("requested.md");
let (args, _prepared) = prepare(destination.to_str(), false);
let (_, source) = read_only_delivery::agent_paths(&args, AgentKind::Codex);
let source = std::path::Path::new(source.as_ref().unwrap());
std::fs::create_dir_all(source.parent().unwrap()).unwrap();
let target = home.path().join("protected.txt");
std::fs::write(&target, "must not be published").unwrap();
std::os::unix::fs::symlink(&target, source).unwrap();
assert!(read_only_delivery::publish(
&args, AgentKind::Codex, repo.path().to_str(), "t-scope-cut",
).is_err());
assert!(!destination.exists());
assert_eq!(std::fs::read_to_string(target).unwrap(), "must not be published");
}
#[test]
fn scope_cut_publication_refuses_directory_staged_report() {
let home = tempfile::tempdir().unwrap();
let repo = tempfile::tempdir().unwrap();
let _home = paths::AidHomeGuard::set(home.path());
let destination = home.path().join("requested.md");
let (args, _prepared) = prepare(destination.to_str(), false);
let (_, source) = read_only_delivery::agent_paths(&args, AgentKind::Codex);
std::fs::create_dir_all(source.unwrap()).unwrap();
assert!(read_only_delivery::publish(
&args, AgentKind::Codex, repo.path().to_str(), "t-scope-cut",
).is_err());
assert!(!destination.exists());
}
fn prepare(result: Option<&str>, container: bool) -> (RunArgs, PreparedDispatch) {
let repo = tempfile::tempdir().unwrap();
let store = Arc::new(Store::open_memory().unwrap());
let mut args = RunArgs {
agent_name: "codex".into(),
prompt: "Read-only audit of the checkout".into(),
read_only: true,
dir: Some(repo.path().display().to_string()),
result_file: result.map(str::to_string),
container: container.then(|| "test-image".into()),
..Default::default()
};
let prepared = prepare_dispatch_with(&store, &mut args, |_| true).unwrap();
(args, prepared)
}
#[test]
fn read_only_host_dispatch_places_automatic_report_under_task_directory() {
let home = tempfile::tempdir().unwrap();
let _home = paths::AidHomeGuard::set(home.path());
let (args, prepared) = prepare(None, false);
let expected = paths::task_dir(prepared.task_id.as_str()).join("result.md");
assert_eq!(args.result_file.as_deref(), expected.to_str());
assert_eq!(args.result_file_required, Some(false));
}
#[test]
fn read_only_host_dispatch_preserves_explicit_result_file() {
let home = tempfile::tempdir().unwrap();
let _home = paths::AidHomeGuard::set(home.path());
let (args, _) = prepare(Some("../reports/explicit.md"), false);
assert_eq!(args.result_file.as_deref(), Some("../reports/explicit.md"));
assert_eq!(args.result_file_required, Some(true));
}
#[test]
fn read_only_container_dispatch_keeps_checkout_relative_automatic_report() {
let home = tempfile::tempdir().unwrap();
let _home = paths::AidHomeGuard::set(home.path());
let (args, prepared) = prepare(None, true);
assert_eq!(args.result_file, Some(format!("result-{}.md", prepared.task_id)));
}
#[test]
fn fix_round_two_dispatch_rejects_same_report_and_output_destination() {
for alias in ["same", "parent", "symlink", "traversal", "missing"] {
let home = tempfile::tempdir().unwrap();
let _home = paths::AidHomeGuard::set(home.path());
let directory = home.path().join("delivery");
std::fs::create_dir(&directory).unwrap();
let output = directory.join("final.md");
std::fs::write(&output, "original\n").unwrap();
let report = match alias {
"parent" => {
std::os::unix::fs::symlink(&directory, home.path().join("alias")).unwrap();
home.path().join("alias/final.md")
}
"symlink" => {
std::os::unix::fs::symlink(&output, directory.join("alias.md")).unwrap();
directory.join("alias.md")
}
"traversal" => directory.join("missing/../final.md"),
"missing" => directory.join("new/final.md"),
_ => output.clone(),
};
let store = Arc::new(Store::open_memory().unwrap());
let mut args = RunArgs {
agent_name: "codex".into(),
prompt: "Read-only audit of the checkout".into(),
read_only: true,
dir: Some(directory.display().to_string()),
output: Some(if alias == "missing" { report.clone() } else { output.clone() }
.display().to_string()),
result_file: Some(report.display().to_string()),
existing_task_id: Some(TaskId("t-duplicate-delivery".into())),
..Default::default()
};
let error = match prepare_dispatch_with(&store, &mut args, |_| true) {
Ok(_) => panic!("duplicate destinations must be rejected: {alias}"),
Err(error) => error.to_string(),
};
assert!(error.contains("--result-file") && error.contains("-o")
&& error.contains("same destination"), "{error}");
assert!(store.get_task("t-duplicate-delivery").unwrap().is_none());
assert_eq!(std::fs::read_to_string(output).unwrap(), "original\n");
}
}
#[test]
fn prepare_dispatch_uses_task_specific_audit_result_file() {
let home = tempfile::tempdir().unwrap();
let _guard = paths::AidHomeGuard::set(home.path());
let store = Arc::new(Store::open_memory().unwrap());
let mut args = RunArgs {
agent_name: "codex".to_string(),
prompt: "Read-only audit of branch X.".to_string(),
..Default::default()
};
let prepared = prepare_dispatch_with(&store, &mut args, |_| true).unwrap();
assert_eq!(
args.result_file.as_deref(),
Some(crate::cmd::report_mode::task_result_file(prepared.task_id.as_str()).as_str())
);
assert!(!args.audit_report_mode);
}
#[test]
fn prepare_dispatch_skips_auto_result_file_when_output_is_set() {
let home = tempfile::tempdir().unwrap();
let _guard = paths::AidHomeGuard::set(home.path());
let store = Arc::new(Store::open_memory().unwrap());
let mut args = RunArgs {
agent_name: "codex".to_string(),
prompt: "Review the implementation and list findings.".to_string(),
read_only: true,
output: Some(home.path().join("audit.md").display().to_string()),
..Default::default()
};
prepare_dispatch_with(&store, &mut args, |_| true).unwrap();
assert_eq!(args.result_file, None);
}