#![allow(clippy::large_futures)]
#![allow(clippy::disallowed_methods)]
use std::fs;
use std::path::Path;
use serde_json::Value;
mod test_support;
use test_support::TtyAction;
use test_support::monochange_command;
use test_support::run_in_tty;
use test_support::setup_scenario_workspace;
fn cli() -> std::process::Command {
monochange_command(Some("2026-04-06"))
}
#[cfg(unix)]
fn run_interactive_change_cli(workspace: &Path, output_path: &Path) -> (i32, String) {
let actions = [
TtyAction::Sleep(std::time::Duration::from_millis(500)),
TtyAction::Send {
bytes: b" ",
pause_after: std::time::Duration::from_millis(100),
},
TtyAction::Send {
bytes: b"\r",
pause_after: std::time::Duration::from_millis(500),
},
TtyAction::Send {
bytes: b"\x1b[B",
pause_after: std::time::Duration::from_millis(100),
},
TtyAction::Send {
bytes: b"\r",
pause_after: std::time::Duration::from_millis(500),
},
TtyAction::Send {
bytes: b"\r",
pause_after: std::time::Duration::from_millis(500),
},
TtyAction::Send {
bytes: b"\r",
pause_after: std::time::Duration::from_millis(500),
},
];
let output = output_path.display().to_string();
run_in_tty(
workspace,
&[
"change",
"--interactive",
"--reason",
"interactive reason",
"--details",
"interactive details",
"--output",
output.as_str(),
],
Some("2026-04-06"),
&actions,
)
}
#[test]
fn change_cli_writes_scalar_type_shorthand_when_no_default_bump_is_configured() {
let tempdir = setup_scenario_workspace("changeset-target-metadata/cli-type-only-change");
let output_path = tempdir.path().join(".changeset/core-docs.md");
let output = cli()
.current_dir(tempdir.path())
.arg("run")
.arg("change")
.arg("--package")
.arg("core")
.arg("--type")
.arg("docs")
.arg("--reason")
.arg("clarify migration guide")
.arg("--output")
.arg(&output_path)
.output()
.unwrap_or_else(|error| panic!("change output: {error}"));
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let contents = fs::read_to_string(output_path).unwrap_or_else(|error| panic!("read: {error}"));
assert!(contents.contains("core: docs"));
assert!(!contents.contains("bump:"));
}
#[test]
fn change_cli_rejects_unknown_change_type_for_configured_target() {
let tempdir = setup_scenario_workspace("changeset-target-metadata/cli-type-only-change");
let output = cli()
.current_dir(tempdir.path())
.arg("run")
.arg("change")
.arg("--package")
.arg("core")
.arg("--type")
.arg("security")
.arg("--reason")
.arg("should fail")
.output()
.unwrap_or_else(|error| panic!("change output: {error}"));
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("invalid value 'security'"),
"unexpected stderr: {stderr}"
);
assert!(stderr.contains("[possible values: docs, test]"));
}
#[test]
fn validate_accepts_scalar_type_shorthand_changesets() {
let tempdir = setup_scenario_workspace("changeset-target-metadata/release-workspace");
let output = cli()
.current_dir(tempdir.path())
.arg("step")
.arg("validate")
.output()
.unwrap_or_else(|error| panic!("validate output: {error}"));
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert!(String::from_utf8_lossy(&output.stdout).contains("workspace validation passed"));
}
#[test]
fn release_dry_run_json_supports_scalar_type_default_bumps() {
let tempdir = setup_scenario_workspace("changeset-target-metadata/release-workspace");
let output = cli()
.current_dir(tempdir.path())
.arg("run")
.arg("release")
.arg("--dry-run")
.arg("--format")
.arg("json")
.output()
.unwrap_or_else(|error| panic!("release output: {error}"));
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let json: Value = serde_json::from_slice(&output.stdout)
.unwrap_or_else(|error| panic!("parse json: {error}"));
let decisions = json["plan"]["decisions"]
.as_array()
.unwrap_or_else(|| panic!("decisions array"));
assert!(decisions.iter().any(|decision| {
decision["package"]
.as_str()
.is_some_and(|package| package.contains("crates/core/Cargo.toml"))
&& decision["bump"].as_str() == Some("minor")
}));
assert!(decisions.iter().any(|decision| {
decision["package"]
.as_str()
.is_some_and(|package| package.contains("crates/app/Cargo.toml"))
&& decision["bump"].as_str() == Some("minor")
}));
}
#[cfg(unix)]
#[test]
fn interactive_change_cli_writes_selected_bump() {
let tempdir = setup_scenario_workspace("changeset-target-metadata/render-workspace");
let output_path = tempdir.path().join("interactive.md");
let (status, transcript) = run_interactive_change_cli(tempdir.path(), &output_path);
assert!(status == 0, "{transcript}");
assert!(transcript.contains("wrote change file interactive.md"));
let contents = fs::read_to_string(output_path).unwrap_or_else(|error| panic!("read: {error}"));
assert!(contents.contains("sdk: patch"));
assert!(contents.contains("# interactive reason"));
}
#[test]
fn release_rejects_legacy_reserved_metadata_blocks() {
let tempdir = setup_scenario_workspace("monochange/release-base");
fs::copy(
tempdir.path().join(".changeset/feature.md"),
tempdir.path().join(".changeset/base.md"),
)
.unwrap_or_else(|error| panic!("preserve base changeset: {error}"));
fs::copy(
Path::new(env!("CARGO_MANIFEST_DIR")).join(
"../../fixtures/tests/monochange/release-with-compat-evidence/.changeset/feature.md",
),
tempdir.path().join(".changeset/feature.md"),
)
.unwrap_or_else(|error| panic!("seed legacy-style changeset: {error}"));
let output = cli()
.current_dir(tempdir.path())
.arg("run")
.arg("release")
.arg("--dry-run")
.arg("--format")
.arg("json")
.output()
.unwrap_or_else(|error| panic!("release output: {error}"));
assert!(!output.status.success());
assert!(
String::from_utf8_lossy(&output.stderr)
.contains("target `origin` uses unsupported field(s): core")
);
}