use crate::stack::spec::{expand_path, parse_git_ref, GitRef, StackPrEntry, StackSpec};
const STUDIO_FIXTURE: &str = r#"{
"id": "studio-combined",
"description": "dev/combined-fixes for Studio",
"component": "studio",
"component_path": "~/Developer/studio",
"base": { "remote": "origin", "branch": "trunk" },
"target": { "remote": "fork", "branch": "dev/combined-fixes" },
"prs": [
{ "repo": "Automattic/studio", "number": 3057, "note": "native spawn" },
{ "repo": "Automattic/studio", "number": 3095 }
]
}"#;
#[test]
fn parses_canonical_studio_fixture() {
let spec: StackSpec = serde_json::from_str(STUDIO_FIXTURE).expect("parse");
assert_eq!(spec.id, "studio-combined");
assert_eq!(spec.component, "studio");
assert_eq!(spec.base.remote, "origin");
assert_eq!(spec.base.branch, "trunk");
assert_eq!(spec.target.remote, "fork");
assert_eq!(spec.target.branch, "dev/combined-fixes");
assert_eq!(spec.prs.len(), 2);
assert_eq!(spec.prs[0].repo, "Automattic/studio");
assert_eq!(spec.prs[0].number, 3057);
assert_eq!(spec.prs[0].note.as_deref(), Some("native spawn"));
assert!(spec.prs[1].note.is_none());
}
#[test]
fn round_trips_via_serde() {
let spec: StackSpec = serde_json::from_str(STUDIO_FIXTURE).expect("parse");
let serialized = serde_json::to_string(&spec).expect("serialize");
let again: StackSpec = serde_json::from_str(&serialized).expect("re-parse");
assert_eq!(again.id, spec.id);
assert_eq!(again.prs.len(), spec.prs.len());
assert_eq!(again.prs[0].number, spec.prs[0].number);
}
#[test]
fn missing_required_field_errors() {
let bad = r#"{
"component": "studio",
"component_path": "~/x",
"target": { "remote": "fork", "branch": "dev" }
}"#;
let err = serde_json::from_str::<StackSpec>(bad).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("base"),
"expected missing-field error mentioning `base`, got: {}",
msg
);
}
#[test]
fn empty_id_filled_from_loader_not_serde() {
let no_id = r#"{
"component": "studio",
"component_path": "~/Developer/studio",
"base": { "remote": "origin", "branch": "trunk" },
"target": { "remote": "fork", "branch": "dev" }
}"#;
let spec: StackSpec = serde_json::from_str(no_id).expect("parse");
assert_eq!(spec.id, "");
}
#[test]
fn empty_prs_array_is_valid() {
let no_prs = r#"{
"component": "studio",
"component_path": "~/x",
"base": { "remote": "o", "branch": "trunk" },
"target": { "remote": "f", "branch": "dev" }
}"#;
let spec: StackSpec = serde_json::from_str(no_prs).expect("parse");
assert_eq!(spec.prs.len(), 0);
}
#[test]
fn parse_git_ref_splits_on_first_slash_only() {
let r = parse_git_ref("origin/trunk", "base").expect("parse");
assert_eq!(r.remote, "origin");
assert_eq!(r.branch, "trunk");
let r = parse_git_ref("fork/dev/combined-fixes", "target").expect("parse");
assert_eq!(r.remote, "fork");
assert_eq!(r.branch, "dev/combined-fixes");
}
#[test]
fn parse_git_ref_rejects_no_slash() {
let err = parse_git_ref("trunk", "base").unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("remote") && msg.contains("branch"),
"expected slash-format error, got: {}",
msg
);
}
#[test]
fn parse_git_ref_rejects_empty_components() {
assert!(parse_git_ref("/trunk", "base").is_err());
assert!(parse_git_ref("origin/", "base").is_err());
assert!(parse_git_ref("", "base").is_err());
}
#[test]
fn git_ref_display_round_trip_format() {
let r = GitRef {
remote: "origin".into(),
branch: "trunk".into(),
};
assert_eq!(r.display(), "origin/trunk");
let r = GitRef {
remote: "fork".into(),
branch: "dev/combined-fixes".into(),
};
assert_eq!(r.display(), "fork/dev/combined-fixes");
}
#[test]
fn expand_path_expands_tilde() {
let home = std::env::var("HOME").unwrap_or_default();
let expanded = expand_path("~/Developer/foo");
assert!(
expanded.starts_with(&home),
"expected tilde to expand to $HOME, got: {}",
expanded
);
assert!(expanded.ends_with("/Developer/foo"));
}
#[test]
fn expand_path_substitutes_env_vars() {
std::env::set_var("HOMEBOY_STACK_TEST_VAR", "magic-value");
let out = expand_path("/prefix/${env.HOMEBOY_STACK_TEST_VAR}/suffix");
assert_eq!(out, "/prefix/magic-value/suffix");
std::env::remove_var("HOMEBOY_STACK_TEST_VAR");
}
#[test]
fn expand_path_unknown_token_left_literal() {
let out = expand_path("/prefix/${unknown.thing}/suffix");
assert_eq!(out, "/prefix/${unknown.thing}/suffix");
}
#[test]
fn expand_path_unset_env_var_becomes_empty() {
std::env::remove_var("HOMEBOY_STACK_TEST_NEVER_SET_XYZ");
let out = expand_path("/a/${env.HOMEBOY_STACK_TEST_NEVER_SET_XYZ}/b");
assert_eq!(out, "/a//b");
}
#[test]
fn pr_entry_serializes_without_optional_fields() {
let entry = StackPrEntry {
repo: "Automattic/studio".into(),
number: 1,
note: None,
};
let json = serde_json::to_string(&entry).unwrap();
assert!(!json.contains("note"));
assert!(json.contains("\"number\":1"));
}