use super::*;
use crate::test_support::workspace::{
seed_proposal, seed_workspace_config, seed_workspace_lineage,
};
use serde_json::json;
use tempfile::TempDir;
fn workspace() -> TempDir {
let dir = tempfile::tempdir().expect("tmp");
seed_workspace_config(dir.path(), &[("workflow.yaml", "events: {}\n")]);
dir
}
#[test]
fn a_staged_proposal_is_listed_with_the_lineage_it_would_move() {
let ws = workspace();
seed_proposal(
ws.path(),
"20260906T090000Z-r001",
"config/default",
"notes: record what the span taught",
&[("souls/worker.md", "you learned a thing\n")],
);
let view = read(ws.path(), None).expect("a listing");
assert_eq!(view.rows.len(), 1, "{:?}", view.rows);
let row = &view.rows[0];
assert_eq!(row.id, "20260906T090000Z-r001");
assert_eq!(row.lineages, vec!["default".to_owned()]);
assert!(row.fresh, "the lineage still stands where it was read");
assert!(row.diffstat.contains("1 file changed"), "{}", row.diffstat);
assert_eq!(row.subject, "notes: record what the span taught");
assert_eq!(row.parent.len(), 8, "the parent, abbreviated");
assert_eq!(view.whole, None, "the bare read names no proposal");
}
#[test]
fn a_lineage_that_moved_leaves_its_proposal_stale() {
let ws = workspace();
seed_proposal(
ws.path(),
"20260906T090000Z-r001",
"config/default",
"notes: one",
&[("souls/worker.md", "first\n")],
);
assert!(read(ws.path(), None).expect("fresh").rows[0].fresh);
seed_proposal(
ws.path(),
"advance",
"config/default",
"config: move the lineage on",
&[("workflow.yaml", "events: {}\n# moved\n")],
);
let repo = ws.path().join("repo.git").display().to_string();
let status = crate::git_env::status(crate::git_env::git().args([
"-C",
&repo,
"branch",
"-f",
"config/default",
"proposal/advance",
]))
.expect("advance the lineage");
assert!(status.success());
let view = read(ws.path(), None).expect("a listing");
let row = view
.rows
.iter()
.find(|r| r.id == "20260906T090000Z-r001")
.expect("still staged");
assert!(!row.fresh, "its parent is nobody's head now");
assert!(row.lineages.is_empty(), "which is the same fact, as a pool");
}
#[test]
fn a_parent_two_lineages_stand_on_lists_both() {
let ws = workspace();
seed_workspace_lineage(ws.path(), "spare");
seed_proposal(
ws.path(),
"20260906T090000Z-r001",
"config/default",
"notes: one",
&[("souls/worker.md", "first\n")],
);
let mut lineages = read(ws.path(), None).expect("a listing").rows[0]
.lineages
.clone();
lineages.sort();
assert_eq!(lineages, vec!["default".to_owned(), "spare".to_owned()]);
}
#[test]
fn naming_one_answers_it_whole_beside_the_listing() {
let ws = workspace();
seed_proposal(
ws.path(),
"20260906T090000Z-r001",
"config/default",
"notes: record what the span taught",
&[("souls/worker.md", "you learned a thing\n")],
);
let view = read(ws.path(), Some("20260906T090000Z-r001")).expect("one whole");
assert_eq!(view.rows.len(), 1, "the listing rides along");
let whole = view.whole.expect("the proposal itself");
assert!(
whole.contains("notes: record what the span taught"),
"{whole}"
);
assert!(whole.contains("you learned a thing"), "{whole}");
}
#[test]
fn nothing_staged_lists_nothing_and_a_bad_id_refuses() {
let ws = workspace();
assert_eq!(
read(ws.path(), None).expect("empty"),
ProposalView::default()
);
assert!(read(ws.path(), Some("nobody")).is_err());
}
#[test]
fn the_verdict_vocabulary_round_trips_and_refuses_a_third_word() {
for verdict in [Verdict::Accept, Verdict::Reject] {
assert_eq!(Verdict::parse(verdict.word()), Some(verdict));
assert!(verdict.flag().starts_with("--"));
assert!(verdict.flag().ends_with(verdict.word()));
}
assert_eq!(Verdict::parse("maybe"), None);
assert_eq!(Verdict::parse(""), None);
}
#[test]
fn the_answer_round_trips_and_absence_stays_absence() {
let listing = ProposalView {
rows: vec![ProposalRow {
id: "20260906T090000Z-r001".into(),
lineages: vec!["default".into()],
parent: "9f2c1ab4".into(),
fresh: true,
diffstat: "1 file changed, 4 insertions(+)".into(),
subject: "notes: record what the span taught".into(),
}],
whole: None,
};
let value = wire::reply(&listing);
assert_eq!(value["kind"], wire::KIND);
assert!(
value.get("whole").is_none(),
"the bare listing names no proposal"
);
let o = value.as_object().expect("an object");
assert_eq!(wire::view_of(o).expect("decodes"), listing);
let whole = ProposalView {
whole: Some("commit …\n+ a line\n".into()),
..listing
};
let value = wire::reply(&whole);
let o = value.as_object().expect("an object");
assert_eq!(wire::view_of(o).expect("decodes"), whole);
}
#[test]
fn the_decoder_refuses_a_row_it_cannot_read() {
let bad = json!({"kind": wire::KIND, "ok": true, "rows": ["not an object"]});
assert!(wire::view_of(bad.as_object().expect("an object")).is_err());
let missing = json!({"kind": wire::KIND, "ok": true,
"rows": [{"id": "a", "lineages": [], "parent": "p", "fresh": true}]});
assert!(wire::view_of(missing.as_object().expect("an object")).is_err());
let nothing = json!({"kind": wire::KIND, "ok": true});
assert!(wire::view_of(nothing.as_object().expect("an object")).is_err());
}