use super::{ACME, deps_at, fire, quiet, script, seed_wall};
use crate::boundary::Action;
use crate::boundary::config::Read;
use crate::boundary::reply::Reply;
use crate::git_tree::tests::fixture::Fixture;
use crate::test_support::TEMPLATE_PROVIDERS;
use std::fs;
use std::path::Path;
use tempfile::tempdir;
#[test]
fn set_marks_answers_with_the_branch_it_read_back_and_logs_the_write() {
let root = tempdir().unwrap();
let deps = quiet(root.path());
let ws = root.path().join("workspaces").join("home");
let deps = super::seeing(&deps, &[ws.as_path()]);
let reply = fire(
&deps,
&Action::SetMarks {
workspace: crate::naming::leaf(&ws),
branch: "balls/agents/home".to_owned(),
},
);
assert_eq!(
reply,
Ok(Reply::Marks {
branch: "balls/agents/home".to_owned(),
})
);
let logged = crate::opslog::tail(&deps.state_root, 10);
assert_eq!(logged.len(), 1);
assert_eq!(logged[0].argv, ["yog-step", "marks", "balls/agents/home"]);
}
#[test]
fn reading_marks_never_refuses_even_for_a_workspace_with_no_project() {
let root = tempdir().unwrap();
let deps = quiet(root.path());
let ws = root.path().join("nowhere");
let deps = super::seeing(&deps, &[ws.as_path()]);
let reply = super::ask(
&deps,
&crate::boundary::Query::Config(Read::Marks {
workspace: crate::naming::leaf(&ws),
}),
);
assert_eq!(
reply,
Ok(Reply::Marks {
branch: crate::world::marks::SHARED_BRANCH.to_owned(),
})
);
}
#[test]
fn set_marks_refuses_an_unlawful_branch_rather_than_writing_one() {
let root = tempdir().unwrap();
let home = root.path().join("workspaces").join("home");
let deps = super::seeing(&quiet(root.path()), &[home.as_path()]);
let err = fire(
&deps,
&Action::SetMarks {
workspace: crate::naming::leaf(&home),
branch: "balls/config".to_owned(),
},
)
.unwrap_err();
assert!(err.contains("landing branch"), "{err}");
}
pub(super) fn workspace() -> Fixture {
let fx = Fixture::new();
fx.commit_other("providers.yaml", TEMPLATE_PROVIDERS);
fx
}
pub(super) fn pick(role: &str, provider: &str, model: &str, ws: &Path) -> Action {
Action::PickModel {
workspace: crate::naming::leaf(ws),
role: role.to_owned(),
provider: provider.to_owned(),
model: model.to_owned(),
}
}
#[test]
fn a_pick_commits_the_assignment_and_writes_no_second_file() {
let root = tempdir().unwrap();
let bin = tempdir().unwrap();
let log = bin.path().join("log");
let litany = script(
bin.path(),
"litany",
&format!(
"cat \"$YOG_EDIT_SRC/providers.yaml\" > {}\nexit 0\n",
log.display()
),
);
let fx = workspace();
let deps = super::seeing(
&deps_at(root.path(), &litany, Path::new("/no/bl")),
&[fx.path.as_path()],
);
seed_wall(&deps, &fx.path, ACME);
let reply = fire(&deps, &pick("worker", "acme", "m-9", &fx.path));
assert!(
matches!(&reply, Ok(Reply::Outcome(o)) if o.ok()),
"{reply:?}"
);
let staged = fs::read_to_string(&log).unwrap();
assert!(staged.contains("provider: acme"), "{staged}");
assert!(staged.contains("model: m-9"), "{staged}");
assert!(
!root.path().join("litany/models.yaml").exists(),
"a pick declares nothing in litany's global config"
);
}
#[test]
fn a_pick_on_a_row_brazen_lacks_writes_nothing() {
let root = tempdir().unwrap();
let fx = workspace();
let deps = super::seeing(&quiet(root.path()), &[fx.path.as_path()]);
let err = fire(&deps, &pick("worker", "nope", "m-9", &fx.path)).unwrap_err();
assert!(err.contains("no provider row `nope`"), "{err}");
assert!(!root.path().join("litany/models.yaml").exists());
}
#[test]
fn a_pick_needs_a_lineage_it_can_read_the_assignment_from() {
let root = tempdir().unwrap();
let deps = quiet(root.path());
let err = fire(&deps, &pick("worker", "acme", "m-9", root.path())).unwrap_err();
assert!(!err.is_empty(), "{err}");
}
#[test]
fn a_pick_whose_models_file_cannot_be_read_refuses_before_planning() {
let root = tempdir().unwrap();
let deps = quiet(root.path());
fs::create_dir_all(root.path().join("litany/models.yaml")).unwrap();
let fx = workspace();
assert!(fire(&deps, &pick("worker", "acme", "m-9", &fx.path)).is_err());
}
#[test]
fn a_pick_on_a_tool_less_protocol_writes_neither_half() {
let root = tempdir().unwrap();
let fx = workspace();
let deps = super::seeing(&quiet(root.path()), &[fx.path.as_path()]);
let err = fire(&deps, &pick("worker", "claude-code", "sonnet", &fx.path)).unwrap_err();
assert!(err.contains("`claude-code` cannot serve a role"), "{err}");
assert!(err.contains("claude_code declares no tools"), "{err}");
assert!(!root.path().join("litany/models.yaml").exists());
}