use std::{fs, path::Path};
use serde_json::Value;
use tempfile::TempDir;
use super::heddle;
fn set_repo_default_visibility(repo: &Path, tier_toml: &str) {
let config_path = repo.join(".heddle/config.toml");
let contents = format!(
"[repository]\nversion = 1\n\n[review.discussion]\ndefault_visibility = {tier_toml}\n"
);
fs::write(&config_path, contents).expect("write repo config");
}
fn init_and_capture(label: &str) -> (TempDir, String) {
let temp = TempDir::new().unwrap();
heddle(&["init"], Some(temp.path())).unwrap();
fs::write(temp.path().join("note.txt"), label.as_bytes()).unwrap();
(temp, String::new())
}
fn capture_state(temp: &Path, message: &str) -> String {
heddle(&["capture", "-m", message], Some(temp)).expect("capture");
let raw = heddle(&["--output", "json", "log", "--limit", "1"], Some(temp)).unwrap();
let value: Value = serde_json::from_str(&raw).unwrap();
value["states"][0]["change_id"]
.as_str()
.expect("log --output json should expose change_id")
.to_string()
}
fn latest_state(temp: &Path) -> String {
let raw = heddle(&["--output", "json", "log", "--limit", "1"], Some(temp)).unwrap();
let value: Value = serde_json::from_str(&raw).unwrap();
value["states"][0]["change_id"]
.as_str()
.expect("log --output json should expose change_id")
.to_string()
}
fn show_json(temp: &Path, state: &str) -> Value {
let raw = heddle(
&["--output", "json", "visibility", "show", state],
Some(temp),
)
.expect("visibility show");
serde_json::from_str(&raw).expect("visibility show output should be JSON")
}
#[test]
fn invariant_a_captured_tier_unchanged_when_default_drifts_public() {
let (temp, _) = init_and_capture("secret");
set_repo_default_visibility(
temp.path(),
"{ Restricted = { scope_label = \"embargo\" } }",
);
let state = capture_state(temp.path(), "captured under embargo");
let before = show_json(temp.path(), &state);
assert_eq!(before["output_kind"], "visibility_show");
assert_eq!(
before["tier"], "restricted",
"state captured under a restricted default must resolve restricted: {before}"
);
assert_eq!(before["label"], "embargo");
assert_eq!(before["effective_public"], false);
set_repo_default_visibility(temp.path(), "\"Public\"");
let after = show_json(temp.path(), &state);
assert_eq!(
after["tier"], "restricted",
"drifting the default to public must NOT retroactively expose a captured state: {after}"
);
assert_eq!(after["label"], "embargo");
assert_eq!(after["effective_public"], false);
}
#[test]
fn capture_still_applies_default_visibility() {
let (temp, _) = init_and_capture("secret");
set_repo_default_visibility(temp.path(), "\"Internal\"");
let state = capture_state(temp.path(), "captured internal");
let show = show_json(temp.path(), &state);
assert_eq!(
show["tier"], "internal",
"capture must inherit the configured non-public default: {show}"
);
assert_eq!(show["effective_public"], false);
}
#[test]
fn cherry_pick_state_gets_default_visibility() {
let temp = TempDir::new().unwrap();
heddle(&["init"], Some(temp.path())).unwrap();
fs::write(temp.path().join("note.txt"), b"base").unwrap();
heddle(&["capture", "-m", "first"], Some(temp.path())).expect("capture first");
let first = latest_state(temp.path());
fs::write(temp.path().join("note.txt"), b"modified").unwrap();
heddle(&["capture", "-m", "second"], Some(temp.path())).expect("capture second");
set_repo_default_visibility(
temp.path(),
"{ Restricted = { scope_label = \"embargo\" } }",
);
heddle(&["cherry-pick", &first], Some(temp.path())).expect("cherry-pick");
let new_state = latest_state(temp.path());
let show = show_json(temp.path(), &new_state);
assert_eq!(
show["tier"], "restricted",
"cherry-picked state must inherit the restricted default via the chokepoint: {show}"
);
assert_eq!(show["effective_public"], false);
}
#[test]
fn revert_state_gets_default_visibility() {
let temp = TempDir::new().unwrap();
heddle(&["init"], Some(temp.path())).unwrap();
fs::write(temp.path().join("note.txt"), b"base").unwrap();
heddle(&["capture", "-m", "first"], Some(temp.path())).expect("capture first");
fs::write(temp.path().join("note.txt"), b"modified").unwrap();
heddle(&["capture", "-m", "second"], Some(temp.path())).expect("capture second");
let second = latest_state(temp.path());
set_repo_default_visibility(temp.path(), "\"Internal\"");
heddle(&["revert", &second], Some(temp.path())).expect("revert");
let new_state = latest_state(temp.path());
let show = show_json(temp.path(), &new_state);
assert_eq!(
show["tier"], "internal",
"reverted state must inherit the internal default via the chokepoint: {show}"
);
assert_eq!(show["effective_public"], false);
}
#[test]
fn undo_after_capture_with_nonpublic_default_reverts_snapshot_and_visibility_in_one_undo() {
let temp = TempDir::new().unwrap();
heddle(&["init"], Some(temp.path())).unwrap();
fs::write(temp.path().join("note.txt"), b"base").unwrap();
heddle(&["capture", "-m", "first"], Some(temp.path())).expect("capture first");
let first = latest_state(temp.path());
set_repo_default_visibility(temp.path(), "\"Internal\"");
fs::write(temp.path().join("note.txt"), b"secret").unwrap();
heddle(&["capture", "-m", "second"], Some(temp.path())).expect("capture second");
let second = latest_state(temp.path());
assert_ne!(first, second, "second capture is a distinct state");
assert_eq!(
show_json(temp.path(), &second)["tier"],
"internal",
"second capture inherits the non-public default"
);
heddle(&["undo"], Some(temp.path())).expect("undo capture");
assert_eq!(
latest_state(temp.path()),
first,
"one undo must revert the snapshot itself — HEAD back to the pre-capture state, \
not just the sidecar"
);
let after = show_json(temp.path(), &second);
assert_eq!(
after["effective_public"], true,
"the same single undo must also revert the auto-applied default visibility: {after}"
);
assert_eq!(after["tier"], "public");
}
#[test]
fn explicit_visibility_set_remains_its_own_undoable_batch() {
let (temp, _) = init_and_capture("ordinary"); let state = capture_state(temp.path(), "ordinary capture");
assert_eq!(
show_json(temp.path(), &state)["effective_public"],
true,
"capture under the public default writes no visibility record"
);
heddle(
&["visibility", "set", &state, "--tier", "internal"],
Some(temp.path()),
)
.expect("visibility set");
assert_eq!(show_json(temp.path(), &state)["tier"], "internal");
heddle(&["undo"], Some(temp.path())).expect("undo set");
assert_eq!(
latest_state(temp.path()),
state,
"undo of the explicit set must NOT revert the capture — the set is its own batch"
);
let after = show_json(temp.path(), &state);
assert_eq!(
after["effective_public"], true,
"the explicit set is reverted by its own undo: {after}"
);
assert_eq!(after["tier"], "public");
}
#[test]
fn public_default_capture_stays_record_free() {
let (temp, _) = init_and_capture("ordinary");
let state = capture_state(temp.path(), "ordinary capture");
let show = show_json(temp.path(), &state);
assert_eq!(show["tier"], "public");
assert_eq!(show["effective_public"], true);
}
#[test]
fn visibility_set_then_show_reports_tier() {
let (temp, _) = init_and_capture("ordinary");
let state = capture_state(temp.path(), "ordinary capture");
let raw = heddle(
&[
"--output",
"json",
"visibility",
"set",
&state,
"--tier",
"internal",
],
Some(temp.path()),
)
.expect("visibility set");
let set: Value = serde_json::from_str(&raw).unwrap();
assert_eq!(set["output_kind"], "visibility_set");
assert_eq!(set["tier"], "internal");
let show = show_json(temp.path(), &state);
assert_eq!(show["tier"], "internal");
assert_eq!(show["effective_public"], false);
}
#[test]
fn visibility_promote_supersedes_to_less_restrictive() {
let (temp, _) = init_and_capture("ordinary");
let state = capture_state(temp.path(), "ordinary capture");
heddle(
&[
"visibility",
"set",
&state,
"--tier",
"restricted",
"--label",
"embargo",
],
Some(temp.path()),
)
.expect("visibility set restricted");
let raw = heddle(
&[
"--output",
"json",
"visibility",
"promote",
&state,
"--tier",
"internal",
],
Some(temp.path()),
)
.expect("visibility promote");
let promote: Value = serde_json::from_str(&raw).unwrap();
assert_eq!(promote["output_kind"], "visibility_promote");
assert_eq!(promote["tier"], "internal");
let show = show_json(temp.path(), &state);
assert_eq!(
show["tier"], "internal",
"promotion should be the effective tier"
);
}
#[test]
fn undo_visibility_set_restores_prior_sidecar() {
let (temp, _) = init_and_capture("ordinary");
let state = capture_state(temp.path(), "ordinary capture");
assert_eq!(
show_json(temp.path(), &state)["effective_public"],
true,
"state starts public-by-absence"
);
heddle(
&["visibility", "set", &state, "--tier", "internal"],
Some(temp.path()),
)
.expect("visibility set");
assert_eq!(show_json(temp.path(), &state)["tier"], "internal");
heddle(&["undo"], Some(temp.path())).expect("undo visibility set");
let after_undo = show_json(temp.path(), &state);
assert_eq!(
after_undo["effective_public"], true,
"undo must restore public-by-absence (sidecar removed): {after_undo}"
);
assert_eq!(after_undo["tier"], "public");
heddle(&["undo", "--redo"], Some(temp.path())).expect("redo visibility set");
let after_redo = show_json(temp.path(), &state);
assert_eq!(
after_redo["tier"], "internal",
"redo must reapply the set tier: {after_redo}"
);
assert_eq!(after_redo["effective_public"], false);
}
#[test]
fn undo_visibility_set_restores_previous_nonpublic() {
let (temp, _) = init_and_capture("ordinary");
let state = capture_state(temp.path(), "ordinary capture");
heddle(
&[
"visibility",
"set",
&state,
"--tier",
"team-scoped",
"--label",
"infra",
],
Some(temp.path()),
)
.expect("set team-scoped");
assert_eq!(show_json(temp.path(), &state)["tier"], "team_scoped");
heddle(
&["visibility", "set", &state, "--tier", "internal"],
Some(temp.path()),
)
.expect("set internal");
assert_eq!(show_json(temp.path(), &state)["tier"], "internal");
heddle(&["undo"], Some(temp.path())).expect("undo second set");
let after = show_json(temp.path(), &state);
assert_eq!(
after["tier"], "team_scoped",
"undo must restore the previous non-public tier, not absence: {after}"
);
assert_eq!(after["effective_public"], false);
assert_eq!(after["label"], "infra");
}
#[test]
fn undo_visibility_promote_reverts_tier() {
let (temp, _) = init_and_capture("ordinary");
let state = capture_state(temp.path(), "ordinary capture");
heddle(
&[
"visibility",
"set",
&state,
"--tier",
"restricted",
"--label",
"embargo",
],
Some(temp.path()),
)
.expect("set restricted");
assert_eq!(show_json(temp.path(), &state)["tier"], "restricted");
heddle(
&["visibility", "promote", &state, "--tier", "internal"],
Some(temp.path()),
)
.expect("promote to internal");
assert_eq!(show_json(temp.path(), &state)["tier"], "internal");
heddle(&["undo"], Some(temp.path())).expect("undo promote");
let after_undo = show_json(temp.path(), &state);
assert_eq!(
after_undo["tier"], "restricted",
"undo of promote must revert to the pre-promote tier: {after_undo}"
);
assert_eq!(after_undo["label"], "embargo");
heddle(&["undo", "--redo"], Some(temp.path())).expect("redo promote");
assert_eq!(
show_json(temp.path(), &state)["tier"],
"internal",
"redo of promote must re-apply the promoted tier"
);
}
#[test]
fn visibility_list_enumerates_tiered_states() {
let (temp, _) = init_and_capture("ordinary");
let state = capture_state(temp.path(), "ordinary capture");
heddle(
&["visibility", "set", &state, "--tier", "internal"],
Some(temp.path()),
)
.expect("visibility set");
let raw = heddle(
&["--output", "json", "visibility", "list"],
Some(temp.path()),
)
.expect("visibility list");
let listing: Value = serde_json::from_str(&raw).unwrap();
assert_eq!(listing["output_kind"], "visibility_list");
assert_eq!(listing["count"], 1);
assert_eq!(listing["states"][0]["tier"], "internal");
}