use super::*;
#[test]
fn quickstart_sentinel_reaches_first_commit_from_one_command() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
let out = heddle_output(
&[
"init",
"--quickstart",
"--principal-name",
"CI Sentinel",
"--principal-email",
"ci@example.invalid",
"--no-harness-install",
"--yes",
],
Some(dir),
)
.unwrap();
assert!(
out.status.success(),
"quickstart should succeed: stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
assert!(dir.join(".heddle").is_dir(), "init created .heddle");
let status = status_json(dir);
assert_eq!(
status.get("thread").and_then(Value::as_str),
Some("quickstart"),
"status surfaces the quickstart thread: {status}"
);
let log: Value =
serde_json::from_str(&heddle(&["log", "--output", "json"], Some(dir)).unwrap()).unwrap();
let states = log["states"].as_array().expect("log emits a states array");
assert!(!states.is_empty(), "at least one capture: {log}");
let intent = states[0]["intent"].as_str().unwrap_or_default();
assert!(
intent.contains("quickstart"),
"first capture intent mentions quickstart: {intent:?}"
);
assert_eq!(
states[0]["principal"].as_str(),
Some("CI Sentinel <ci@example.invalid>"),
"capture is attributed to the flag identity: {log}"
);
}
#[test]
fn quickstart_writes_placeholder_when_directory_is_empty() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
heddle(
&[
"init",
"--quickstart",
"--principal-name",
"CI Sentinel",
"--principal-email",
"ci@example.invalid",
"--no-harness-install",
"--yes",
],
Some(dir),
)
.unwrap();
assert!(
dir.join("QUICKSTART.md").is_file(),
"quickstart wrote the root-level placeholder"
);
let log: Value =
serde_json::from_str(&heddle(&["log", "--output", "json"], Some(dir)).unwrap()).unwrap();
assert_eq!(
log["states"].as_array().map(Vec::len),
Some(1),
"exactly one capture: {log}"
);
}
#[test]
fn status_recommends_first_save_not_reinit_on_empty_native_repo() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
heddle(&["init", "--no-harness-install"], Some(dir)).unwrap();
let status = status_json(dir);
assert_eq!(
status["recommended_action"].as_str(),
Some("heddle commit -m \"...\""),
"fresh native repo recommends the first save: {status}"
);
assert!(
!status["recommended_action"]
.as_str()
.unwrap_or_default()
.contains("init --quickstart"),
"status must not suggest re-init on an already-initialized repo: {status}"
);
heddle(
&[
"init",
"--quickstart",
"--principal-name",
"CI Sentinel",
"--principal-email",
"ci@example.invalid",
"--no-harness-install",
"--yes",
],
Some(dir),
)
.unwrap();
let status = status_json(dir);
assert_ne!(
status["recommended_action"].as_str(),
Some("heddle commit -m \"...\""),
"after the first capture the first-save recommendation no longer fires: {status}"
);
}
#[test]
fn quickstart_confirmation_gate_blocks_existing_repo_without_yes() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
heddle(
&[
"init",
"--quickstart",
"--principal-name",
"CI Sentinel",
"--principal-email",
"ci@example.invalid",
"--no-harness-install",
"--yes",
],
Some(dir),
)
.unwrap();
let out = heddle_output(
&[
"init",
"--quickstart",
"--principal-name",
"CI Sentinel",
"--principal-email",
"ci@example.invalid",
"--no-harness-install",
],
Some(dir),
)
.unwrap();
assert!(
!out.status.success(),
"must refuse without --yes on an existing repo"
);
let combined = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(
combined.contains("--yes"),
"the refusal points at --yes: {combined}"
);
}
#[test]
fn quickstart_resolves_identity_from_user_config_without_flags() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
let out = heddle_output(
&["init", "--quickstart", "--no-harness-install", "--yes"],
Some(dir),
)
.unwrap();
assert!(
out.status.success(),
"quickstart should resolve identity from user config: stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
let log: Value =
serde_json::from_str(&heddle(&["log", "--output", "json"], Some(dir)).unwrap()).unwrap();
let states = log["states"].as_array().expect("log emits a states array");
assert_eq!(
states[0]["principal"].as_str(),
Some("Heddle Test <heddle@example.com>"),
"capture is attributed to the seeded user-config identity: {log}"
);
}
#[test]
fn quickstart_identity_failfast_leaves_no_partial_heddle() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
git_hermetic(&["init"], dir);
std::fs::write(dir.join("a.txt"), "hi").unwrap();
git_hermetic(&["add", "."], dir);
git_hermetic(&["commit", "-m", "initial"], dir);
let out = heddle_output_with_env(
&["init", "--quickstart", "--no-harness-install", "--yes"],
Some(dir),
&[
("GIT_CONFIG_GLOBAL", "/dev/null"),
("GIT_CONFIG_SYSTEM", "/dev/null"),
("GIT_CONFIG_NOSYSTEM", "1"),
("HEDDLE_PRINCIPAL_NAME", ""),
("HEDDLE_PRINCIPAL_EMAIL", ""),
],
)
.unwrap();
assert!(
!out.status.success(),
"must refuse without a resolvable identity: stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
assert!(
!dir.join(".heddle").exists(),
"a declined/aborted preflight must leave NO partial .heddle/"
);
}
#[test]
fn quickstart_uses_custom_thread_name() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
heddle(
&[
"init",
"--quickstart",
"--quickstart-thread",
"research",
"--principal-name",
"CI Sentinel",
"--principal-email",
"ci@example.invalid",
"--no-harness-install",
"--yes",
],
Some(dir),
)
.unwrap();
let status = status_json(dir);
assert_eq!(
status.get("thread").and_then(Value::as_str),
Some("research"),
"status surfaces the custom thread: {status}"
);
}
#[test]
fn quickstart_captures_existing_files_without_placeholder() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
std::fs::write(dir.join("notes.txt"), "my work\n").unwrap();
heddle(
&[
"init",
"--quickstart",
"--principal-name",
"CI Sentinel",
"--principal-email",
"ci@example.invalid",
"--no-harness-install",
"--yes",
],
Some(dir),
)
.unwrap();
assert!(
!dir.join("QUICKSTART.md").exists(),
"no placeholder when real files exist to capture"
);
let log: Value =
serde_json::from_str(&heddle(&["log", "--output", "json"], Some(dir)).unwrap()).unwrap();
assert_eq!(
log["states"].as_array().map(Vec::len),
Some(1),
"exactly one capture: {log}"
);
}
#[test]
fn quickstart_checkpoints_on_git_overlay() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
git_hermetic(&["init"], dir);
std::fs::write(dir.join("a.txt"), "hi\n").unwrap();
git_hermetic(&["add", "."], dir);
git_hermetic(&["commit", "-m", "initial"], dir);
let out = heddle_output(
&[
"init",
"--quickstart",
"--principal-name",
"CI Sentinel",
"--principal-email",
"ci@example.invalid",
"--no-harness-install",
"--yes",
],
Some(dir),
)
.unwrap();
assert!(
out.status.success(),
"quickstart should checkpoint on git-overlay: stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("Checkpoint:"),
"git-overlay quickstart reports a checkpoint: {stdout}"
);
}
#[test]
fn quickstart_unborn_git_yields_single_capture_and_checkpoint() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
git_hermetic(&["init"], dir);
let out = heddle_output(
&[
"init",
"--quickstart",
"--principal-name",
"CI Sentinel",
"--principal-email",
"ci@example.invalid",
"--no-harness-install",
"--yes",
],
Some(dir),
)
.unwrap();
assert!(
out.status.success(),
"quickstart should succeed on an unborn Git repo: stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
let log: Value =
serde_json::from_str(&heddle(&["log", "--output", "json"], Some(dir)).unwrap()).unwrap();
let states = log["states"].as_array().expect("log emits a states array");
assert_eq!(
states.len(),
1,
"exactly one capture, no extra bootstrap parent: {log}"
);
let intent = states[0]["intent"].as_str().unwrap_or_default();
assert!(
intent.contains("quickstart"),
"the single state is the quickstart capture, not a bootstrap: {intent:?}"
);
let grepo = open_git(dir).expect("open git repo");
let tip = grepo
.head_id()
.expect("HEAD resolves to a checkpoint commit");
let commit_count = grepo
.rev_walk([tip])
.all()
.expect("rev-walk checkpoint history")
.count();
assert_eq!(
commit_count, 1,
"exactly one checkpoint commit, no extra bootstrap parent"
);
}
#[test]
fn quickstart_preflight_honors_repo_level_principal() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
git_hermetic(&["init"], dir);
let isolate: &[(&str, &str)] = &[
("GIT_CONFIG_GLOBAL", "/dev/null"),
("GIT_CONFIG_SYSTEM", "/dev/null"),
("GIT_CONFIG_NOSYSTEM", "1"),
("HEDDLE_PRINCIPAL_NAME", ""),
("HEDDLE_PRINCIPAL_EMAIL", ""),
];
let init =
heddle_output_with_env(&["init", "--no-harness-install"], Some(dir), isolate).unwrap();
assert!(
init.status.success(),
"plain init should succeed: stdout={} stderr={}",
String::from_utf8_lossy(&init.stdout),
String::from_utf8_lossy(&init.stderr),
);
let cfg_path = dir.join(".heddle").join("config.toml");
let mut cfg = repo::RepoConfig::load(&cfg_path).unwrap_or_default();
cfg.set_principal("Repo Local", "repo@example.invalid");
cfg.save(&cfg_path).unwrap();
let out = heddle_output_with_env(
&["init", "--quickstart", "--no-harness-install", "--yes"],
Some(dir),
isolate,
)
.unwrap();
assert!(
out.status.success(),
"repo-level principal must satisfy the quickstart preflight: stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
let log: Value =
serde_json::from_str(&heddle(&["log", "--output", "json"], Some(dir)).unwrap()).unwrap();
let states = log["states"].as_array().expect("log emits a states array");
assert_eq!(
states[0]["principal"].as_str(),
Some("Repo Local <repo@example.invalid>"),
"capture is attributed to the repo-level principal: {log}"
);
}
#[test]
fn quickstart_rejects_git_sentinel_identity_before_writes() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
git_hermetic(&["init"], dir);
git_hermetic(&["config", "user.name", "Unknown"], dir);
git_hermetic(&["config", "user.email", "unknown@example.com"], dir);
let out = heddle_output_with_env(
&[
"init",
"--quickstart",
"--no-harness-install",
"--yes",
"--output",
"json",
],
Some(dir),
&[
("GIT_CONFIG_GLOBAL", "/dev/null"),
("GIT_CONFIG_SYSTEM", "/dev/null"),
("GIT_CONFIG_NOSYSTEM", "1"),
("HEDDLE_PRINCIPAL_NAME", ""),
("HEDDLE_PRINCIPAL_EMAIL", ""),
],
)
.unwrap();
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success(),
"sentinel-only Git identity must not satisfy the preflight: stdout={} stderr={stderr}",
String::from_utf8_lossy(&out.stdout),
);
assert!(
stderr.contains("quickstart_identity_required"),
"must fail with the quickstart_identity_required advice: stderr={stderr}"
);
assert!(
!dir.join(".heddle").exists(),
"fail-before-writes: a sentinel-only identity must leave NO partial .heddle/"
);
assert!(
!dir.join("QUICKSTART.md").exists(),
"fail-before-writes: no QUICKSTART.md placeholder may be written"
);
}
#[test]
fn quickstart_rejects_sentinel_principal_flags_before_writes() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
let out = heddle_output(
&[
"init",
"--quickstart",
"--principal-name",
"Unknown",
"--principal-email",
"unknown@example.com",
"--no-harness-install",
"--yes",
"--output",
"json",
],
Some(dir),
)
.unwrap();
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success(),
"sentinel principal flags must be rejected: stdout={} stderr={stderr}",
String::from_utf8_lossy(&out.stdout),
);
assert!(
stderr.contains("quickstart_identity_required"),
"must fail with the quickstart_identity_required advice: stderr={stderr}"
);
assert!(
!dir.join(".heddle").exists(),
"fail-before-writes: a sentinel identity must leave NO partial .heddle/"
);
assert!(
!dir.join("QUICKSTART.md").exists(),
"fail-before-writes: no QUICKSTART.md placeholder may be written"
);
}
#[test]
fn quickstart_higher_precedence_sentinel_shadows_valid_lower_source() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
git_hermetic(&["init"], dir);
git_hermetic(&["config", "user.name", "Git Valid"], dir);
git_hermetic(&["config", "user.email", "valid@example.com"], dir);
let isolate: &[(&str, &str)] = &[
("GIT_CONFIG_GLOBAL", "/dev/null"),
("GIT_CONFIG_SYSTEM", "/dev/null"),
("GIT_CONFIG_NOSYSTEM", "1"),
("HEDDLE_PRINCIPAL_NAME", ""),
("HEDDLE_PRINCIPAL_EMAIL", ""),
];
heddle_output_with_env(&["init", "--no-harness-install"], Some(dir), isolate).unwrap();
let cfg_path = dir.join(".heddle").join("config.toml");
let mut cfg = repo::RepoConfig::load(&cfg_path).unwrap_or_default();
cfg.set_principal("Unknown", "unknown@example.com");
cfg.save(&cfg_path).unwrap();
let out = heddle_output_with_env(
&[
"init",
"--quickstart",
"--no-harness-install",
"--yes",
"--output",
"json",
],
Some(dir),
isolate,
)
.unwrap();
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success(),
"a higher-precedence sentinel must shadow the valid lower source and fail: stdout={} stderr={stderr}",
String::from_utf8_lossy(&out.stdout),
);
assert!(
stderr.contains("quickstart_identity_required"),
"must fail with the quickstart_identity_required advice: stderr={stderr}"
);
assert!(
!dir.join("QUICKSTART.md").exists(),
"fail-before-writes: no QUICKSTART.md placeholder may be written"
);
}
#[test]
fn quickstart_resolves_genuine_git_identity_without_flags() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
git_hermetic(&["init"], dir);
git_hermetic(&["config", "user.name", "Git Valid"], dir);
git_hermetic(&["config", "user.email", "valid@example.com"], dir);
let out = heddle_output_with_env(
&["init", "--quickstart", "--no-harness-install", "--yes"],
Some(dir),
&[
("GIT_CONFIG_GLOBAL", "/dev/null"),
("GIT_CONFIG_SYSTEM", "/dev/null"),
("GIT_CONFIG_NOSYSTEM", "1"),
("HEDDLE_PRINCIPAL_NAME", ""),
("HEDDLE_PRINCIPAL_EMAIL", ""),
],
)
.unwrap();
assert!(
out.status.success(),
"a valid Git identity must satisfy the preflight: stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
let log: Value =
serde_json::from_str(&heddle(&["log", "--output", "json"], Some(dir)).unwrap()).unwrap();
let states = log["states"].as_array().expect("log emits a states array");
assert_eq!(
states[0]["principal"].as_str(),
Some("Git Valid <valid@example.com>"),
"capture is attributed to the genuine Git identity: {log}"
);
}
#[test]
fn quickstart_rejects_invalid_thread_name_before_writes() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
let out = heddle_output(
&[
"init",
"--quickstart",
"--quickstart-thread",
"a..b",
"--principal-name",
"CI Sentinel",
"--principal-email",
"ci@example.invalid",
"--no-harness-install",
"--yes",
"--output",
"json",
],
Some(dir),
)
.unwrap();
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success(),
"an invalid thread name must be rejected: stdout={} stderr={stderr}",
String::from_utf8_lossy(&out.stdout),
);
assert!(
stderr.contains("quickstart_thread_name_invalid"),
"must fail with the quickstart_thread_name_invalid advice: stderr={stderr}"
);
assert!(
!dir.join(".heddle").exists(),
"fail-before-writes: an invalid thread name must leave NO partial .heddle/"
);
assert!(
!dir.join("QUICKSTART.md").exists(),
"fail-before-writes: no QUICKSTART.md placeholder may be written"
);
}
#[test]
fn quickstart_rerun_repoints_existing_noncurrent_thread() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
heddle(
&[
"init",
"--quickstart",
"--principal-name",
"CI Sentinel",
"--principal-email",
"ci@example.invalid",
"--no-harness-install",
"--yes",
],
Some(dir),
)
.unwrap();
heddle(&["thread", "create", "work"], Some(dir)).unwrap();
heddle(&["thread", "switch", "work"], Some(dir)).unwrap();
std::fs::write(dir.join("work.txt"), "work\n").unwrap();
heddle(&["capture", "-m", "work state"], Some(dir)).unwrap();
let b_id = state_chain_ids(dir, 1)[0].clone();
std::fs::write(dir.join("more.txt"), "more\n").unwrap();
let out = heddle_output(
&["init", "--quickstart", "--no-harness-install", "--yes"],
Some(dir),
)
.unwrap();
assert!(
out.status.success(),
"rerun quickstart should succeed: stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
let chain = state_chain_ids(dir, 2);
assert_eq!(chain.len(), 2, "the new capture has a parent: {chain:?}");
assert_eq!(
chain[1], b_id,
"new capture's parent is the current state B, not the stale quickstart tip: chain={chain:?} b={b_id}"
);
}
#[test]
fn quickstart_refuses_detached_git_head_before_writes() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
git_hermetic(&["init"], dir);
std::fs::write(dir.join("a.txt"), "hi\n").unwrap();
git_hermetic(&["add", "."], dir);
git_hermetic(&["commit", "-m", "initial"], dir);
git_hermetic(&["checkout", "--detach"], dir);
let out = heddle_output(
&[
"init",
"--quickstart",
"--principal-name",
"CI Sentinel",
"--principal-email",
"ci@example.invalid",
"--no-harness-install",
"--yes",
"--output",
"json",
],
Some(dir),
)
.unwrap();
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success(),
"a detached Git HEAD must be refused: stdout={} stderr={stderr}",
String::from_utf8_lossy(&out.stdout),
);
assert!(
stderr.contains("quickstart_detached_head"),
"must fail with the quickstart_detached_head advice: {stderr}"
);
assert!(
!dir.join(".heddle").exists(),
"fail-before-writes: a detached HEAD must leave NO partial .heddle/"
);
assert!(
!dir.join("QUICKSTART.md").exists(),
"fail-before-writes: no QUICKSTART.md placeholder may be written"
);
}
#[test]
fn quickstart_rejects_git_invalid_thread_name_before_writes() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
git_hermetic(&["init"], dir);
let out = heddle_output(
&[
"init",
"--quickstart",
"--quickstart-thread",
"bad~name",
"--principal-name",
"CI Sentinel",
"--principal-email",
"ci@example.invalid",
"--no-harness-install",
"--yes",
"--output",
"json",
],
Some(dir),
)
.unwrap();
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success(),
"a Git-invalid thread name must be rejected on a Git overlay: stdout={} stderr={stderr}",
String::from_utf8_lossy(&out.stdout),
);
assert!(
stderr.contains("quickstart_thread_name_invalid"),
"must fail with the quickstart_thread_name_invalid advice: {stderr}"
);
assert!(
!dir.join(".heddle").exists(),
"fail-before-writes: a Git-invalid thread name must leave NO partial .heddle/"
);
}
#[test]
fn quickstart_resolves_principal_from_shared_dir_in_materialized_checkout() {
let main = TempDir::new().unwrap();
let checkout = TempDir::new().unwrap();
let empty_cfg = TempDir::new().unwrap();
let empty_cfg_path = empty_cfg.path().join("user.toml");
std::fs::write(&empty_cfg_path, "").unwrap();
heddle(&["init", "--no-harness-install"], Some(main.path())).unwrap();
std::fs::write(main.path().join("file.txt"), "v1\n").unwrap();
heddle(&["capture", "-m", "seed"], Some(main.path())).unwrap();
let shared_cfg = main.path().join(".heddle").join("config.toml");
let mut cfg = repo::RepoConfig::load(&shared_cfg).unwrap_or_default();
cfg.set_principal("Shared Principal", "shared@example.invalid");
cfg.save(&shared_cfg).unwrap();
heddle(
&[
"start",
"feature/mat",
"--workspace",
"materialized",
"--path",
checkout.path().to_str().unwrap(),
],
Some(main.path()),
)
.unwrap();
assert!(
checkout
.path()
.join(".heddle")
.join("objectstore")
.is_file(),
"checkout must be a materialized (objectstore-pointer) checkout"
);
let out = heddle_output_with_env(
&["init", "--quickstart", "--no-harness-install", "--yes"],
Some(checkout.path()),
&[
("HEDDLE_CONFIG", empty_cfg_path.to_str().unwrap()),
("HEDDLE_PRINCIPAL_NAME", ""),
("HEDDLE_PRINCIPAL_EMAIL", ""),
],
)
.unwrap();
assert!(
out.status.success(),
"a materialized checkout must resolve [principal] from the shared dir: stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
let log: Value =
serde_json::from_str(&heddle(&["log", "--output", "json"], Some(checkout.path())).unwrap())
.unwrap();
let states = log["states"].as_array().expect("log emits a states array");
assert_eq!(
states[0]["principal"].as_str(),
Some("Shared Principal <shared@example.invalid>"),
"capture is attributed to the shared-dir principal: {log}"
);
}
#[test]
fn quickstart_confirmation_respects_repo_json_format() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
heddle(&["init", "--no-harness-install"], Some(dir)).unwrap();
let cfg_path = dir.join(".heddle").join("config.toml");
let mut cfg = repo::RepoConfig::load(&cfg_path).unwrap_or_default();
cfg.output.format = Some(repo::OutputFormat::Json);
cfg.save(&cfg_path).unwrap();
let out = heddle_output(
&[
"init",
"--quickstart",
"--principal-name",
"CI Sentinel",
"--principal-email",
"ci@example.invalid",
"--no-harness-install",
],
Some(dir),
)
.unwrap();
assert!(
!out.status.success(),
"the confirmation gate must refuse without --yes"
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
!stdout.contains("would act on a directory that already has work"),
"repo json format must suppress the text confirmation prompt on stdout: stdout={stdout}"
);
assert!(
stdout.trim().is_empty(),
"no stray text output when the repo format is json: stdout={stdout}"
);
}
#[test]
fn quickstart_treats_orphan_branch_repo_as_existing_history() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
git_hermetic(&["init"], dir);
std::fs::write(dir.join("a.txt"), "hi\n").unwrap();
git_hermetic(&["add", "."], dir);
git_hermetic(&["commit", "-m", "initial"], dir);
git_hermetic(&["switch", "--orphan", "scratch"], dir);
let out = heddle_output(
&[
"init",
"--quickstart",
"--principal-name",
"CI Sentinel",
"--principal-email",
"ci@example.invalid",
"--no-harness-install",
"--output",
"json",
],
Some(dir),
)
.unwrap();
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success(),
"an orphan-branch repo must hit the existing-history confirmation gate: stdout={} stderr={stderr}",
String::from_utf8_lossy(&out.stdout),
);
assert!(
stderr.contains("quickstart_needs_confirmation"),
"must be the existing-history confirmation refusal, not a history-free skip: {stderr}"
);
}
#[test]
fn quickstart_orphan_unborn_head_with_history_defers_attachment_and_succeeds() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
git_hermetic(&["init"], dir);
std::fs::write(dir.join("a.txt"), "hi\n").unwrap();
git_hermetic(&["add", "."], dir);
git_hermetic(&["commit", "-m", "initial"], dir);
git_hermetic(&["switch", "--orphan", "scratch"], dir);
let out = heddle_output(
&[
"init",
"--quickstart",
"--principal-name",
"CI Sentinel",
"--principal-email",
"ci@example.invalid",
"--no-harness-install",
"--yes",
"--output",
"json",
],
Some(dir),
)
.unwrap();
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
out.status.success(),
"quickstart should succeed by deferring attachment on an unborn current branch: stdout={} stderr={stderr}",
String::from_utf8_lossy(&out.stdout),
);
assert!(
!stderr.contains("no state to export"),
"the write-through attachment path must not run on an unborn current branch: {stderr}"
);
let status = status_json(dir);
assert_eq!(
status.get("thread").and_then(Value::as_str),
Some("quickstart"),
"quickstart remains the active thread after the first capture: {status}"
);
assert_eq!(
state_chain_ids(dir, 1).len(),
1,
"the first capture established a state for the quickstart thread"
);
}
#[test]
fn quickstart_rejects_env_sentinel_even_with_valid_flags_before_writes() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
let out = heddle_output_with_env(
&[
"init",
"--quickstart",
"--principal-name",
"Valid Name",
"--principal-email",
"valid@example.invalid",
"--no-harness-install",
"--yes",
"--output",
"json",
],
Some(dir),
&[
("HEDDLE_PRINCIPAL_NAME", "Unknown"),
("HEDDLE_PRINCIPAL_EMAIL", "unknown@example.com"),
],
)
.unwrap();
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success(),
"a sentinel env identity must shadow valid flags and fail: stdout={} stderr={stderr}",
String::from_utf8_lossy(&out.stdout),
);
assert!(
stderr.contains("quickstart_identity_required"),
"must fail with the quickstart_identity_required advice: {stderr}"
);
assert!(
!dir.join(".heddle").exists(),
"fail-before-writes: an env sentinel must leave NO partial .heddle/"
);
assert!(
!dir.join("QUICKSTART.md").exists(),
"fail-before-writes: no QUICKSTART.md placeholder may be written"
);
}
#[test]
fn quickstart_installs_selected_harness() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
heddle(
&[
"init",
"--quickstart",
"--principal-name",
"CI Sentinel",
"--principal-email",
"ci@example.invalid",
"--install-harnesses",
"claude-code",
"--harness-install-scope",
"repo",
"--yes",
],
Some(dir),
)
.unwrap();
let settings = dir.join(".claude").join("settings.json");
assert!(settings.is_file(), "claude-code integration was installed");
let contents = std::fs::read_to_string(&settings).unwrap();
assert!(
contents.contains("integration relay claude-code"),
"settings carry the relay hooks: {contents}"
);
}
#[test]
fn quickstart_install_none_installs_nothing() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
heddle(
&[
"init",
"--quickstart",
"--principal-name",
"CI Sentinel",
"--principal-email",
"ci@example.invalid",
"--install-harnesses",
"none",
"--yes",
],
Some(dir),
)
.unwrap();
assert!(
!dir.join(".claude").exists(),
"selecting `none` installs no harness integration"
);
}
#[test]
fn quickstart_native_repo_nested_in_git_checkout_is_not_overlay_refused() {
let outer = TempDir::new().unwrap();
let inner = outer.path().join("project");
std::fs::create_dir(&inner).unwrap();
heddle(&["init", "--no-harness-install"], Some(&inner)).unwrap();
assert!(inner.join(".heddle").is_dir());
git_hermetic(&["init"], outer.path());
std::fs::write(outer.path().join("a.txt"), "hi\n").unwrap();
git_hermetic(&["add", "."], outer.path());
git_hermetic(&["commit", "-m", "initial"], outer.path());
git_hermetic(&["checkout", "--detach"], outer.path());
let out = heddle_output(
&[
"init",
"--quickstart",
"--principal-name",
"CI Sentinel",
"--principal-email",
"ci@example.invalid",
"--no-harness-install",
"--yes",
"--output",
"json",
],
Some(&inner),
)
.unwrap();
let stderr = String::from_utf8_lossy(&out.stderr);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
out.status.success(),
"a native repo nested in a detached-HEAD Git checkout must proceed: stdout={stdout} stderr={stderr}",
);
assert!(
!stderr.contains("quickstart_detached_head"),
"must NOT refuse with the Git-overlay detached-HEAD advice: {stderr}"
);
let init: Value = serde_json::from_str(&stdout).unwrap();
assert_eq!(
init["repository_mode"].as_str(),
Some("native-heddle"),
"the nested repo stays native, not a Git overlay: {init}"
);
assert!(
inner.join(".heddle").is_dir(),
"native .heddle/ was created"
);
}
#[test]
fn quickstart_resolves_shared_checkout_parent_git_identity() {
let main = TempDir::new().unwrap();
let checkout = TempDir::new().unwrap();
let empty_cfg = TempDir::new().unwrap();
let empty_cfg_path = empty_cfg.path().join("user.toml");
std::fs::write(&empty_cfg_path, "").unwrap();
heddle(&["init", "--no-harness-install"], Some(main.path())).unwrap();
std::fs::write(main.path().join("file.txt"), "v1\n").unwrap();
heddle(&["capture", "-m", "seed"], Some(main.path())).unwrap();
heddle(
&[
"start",
"feature/mat",
"--workspace",
"materialized",
"--path",
checkout.path().to_str().unwrap(),
],
Some(main.path()),
)
.unwrap();
assert!(
checkout
.path()
.join(".heddle")
.join("objectstore")
.is_file(),
"checkout must be a materialized (objectstore-pointer) checkout"
);
git_hermetic(&["init"], main.path());
git_hermetic(&["config", "user.name", "Parent Git"], main.path());
git_hermetic(
&["config", "user.email", "parent@example.invalid"],
main.path(),
);
let out = heddle_output_with_env(
&["init", "--quickstart", "--no-harness-install", "--yes"],
Some(checkout.path()),
&[
("HEDDLE_CONFIG", empty_cfg_path.to_str().unwrap()),
("HEDDLE_PRINCIPAL_NAME", ""),
("HEDDLE_PRINCIPAL_EMAIL", ""),
],
)
.unwrap();
assert!(
out.status.success(),
"preflight must resolve identity from the shared dir's parent Git config: stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
let log: Value =
serde_json::from_str(&heddle(&["log", "--output", "json"], Some(checkout.path())).unwrap())
.unwrap();
let states = log["states"].as_array().expect("log emits a states array");
assert_eq!(
states[0]["principal"].as_str(),
Some("Parent Git <parent@example.invalid>"),
"capture is attributed to the shared-checkout parent Git identity: {log}"
);
}
#[test]
fn quickstart_rejects_git_branch_invalid_shorthand_before_writes() {
for bad in ["HEAD", "-leading"] {
let temp = TempDir::new().unwrap();
let dir = temp.path();
git_hermetic(&["init"], dir);
let thread_arg = format!("--quickstart-thread={bad}");
let out = heddle_output(
&[
"init",
"--quickstart",
&thread_arg,
"--principal-name",
"CI Sentinel",
"--principal-email",
"ci@example.invalid",
"--no-harness-install",
"--yes",
"--output",
"json",
],
Some(dir),
)
.unwrap();
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success(),
"'{bad}' is not a usable Git branch name and must be rejected: stdout={} stderr={stderr}",
String::from_utf8_lossy(&out.stdout),
);
assert!(
stderr.contains("quickstart_thread_name_invalid"),
"'{bad}' must fail with the quickstart_thread_name_invalid advice: {stderr}"
);
assert!(
!dir.join(".heddle").exists(),
"fail-before-writes: '{bad}' must leave NO partial .heddle/"
);
}
}
fn snapshot_tree(dir: &std::path::Path) -> Vec<(std::path::PathBuf, Vec<u8>)> {
fn walk(
base: &std::path::Path,
dir: &std::path::Path,
out: &mut Vec<(std::path::PathBuf, Vec<u8>)>,
) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
match entry.file_type() {
Ok(ft) if ft.is_dir() => walk(base, &path, out),
Ok(ft) if ft.is_file() => {
let rel = path.strip_prefix(base).unwrap().to_path_buf();
out.push((rel, std::fs::read(&path).unwrap_or_default()));
}
_ => {}
}
}
}
let mut out = Vec::new();
walk(dir, dir, &mut out);
out.sort();
out
}
#[test]
fn quickstart_from_subdir_targets_discovered_native_repo() {
let temp = TempDir::new().unwrap();
let root = temp.path();
heddle(&["init", "--no-harness-install"], Some(root)).unwrap();
assert!(
root.join(".heddle").is_dir(),
"native repo created at the root"
);
let sub = root.join("nested").join("deeper");
std::fs::create_dir_all(&sub).unwrap();
let out = heddle_output(
&["init", "--quickstart", "--no-harness-install", "--yes"],
Some(&sub),
)
.unwrap();
assert!(
out.status.success(),
"quickstart from a subdir of a native repo must proceed: stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
assert!(
!sub.join(".heddle").exists(),
"must NOT create a nested .heddle/ at the subdirectory"
);
assert!(
root.join(".heddle").is_dir(),
"the discovered repo root stays the single repo"
);
let status = status_json(root);
assert_eq!(
status.get("thread").and_then(Value::as_str),
Some("quickstart"),
"the discovered repo carries the quickstart thread: {status}"
);
assert!(
root.join("QUICKSTART.md").is_file(),
"the capture wrote QUICKSTART.md at the discovered root"
);
assert!(
!sub.join("QUICKSTART.md").exists(),
"nothing was written into the subdirectory"
);
}
#[test]
fn quickstart_from_subdir_targets_discovered_git_root() {
let temp = TempDir::new().unwrap();
let root = temp.path();
git_hermetic(&["init"], root);
std::fs::write(root.join("a.txt"), "hi\n").unwrap();
git_hermetic(&["add", "."], root);
git_hermetic(&["commit", "-m", "initial"], root);
let sub = root.join("src").join("inner");
std::fs::create_dir_all(&sub).unwrap();
let out = heddle_output(
&[
"init",
"--quickstart",
"--principal-name",
"CI Sentinel",
"--principal-email",
"ci@example.invalid",
"--no-harness-install",
"--yes",
"--output",
"json",
],
Some(&sub),
)
.unwrap();
assert!(
out.status.success(),
"quickstart from a subdir of a Git checkout must proceed: stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
assert!(
!sub.join(".heddle").exists(),
"must NOT create a nested .heddle/ at the subdirectory"
);
assert!(
root.join(".heddle").is_dir(),
"Heddle data is created at the discovered Git root"
);
let init: Value = serde_json::from_str(&String::from_utf8_lossy(&out.stdout)).unwrap();
assert_eq!(
init["repository_mode"].as_str(),
Some("git-overlay"),
"the discovered Git root is a Git overlay: {init}"
);
assert_eq!(
init["git_detected"].as_bool(),
Some(true),
"git is detected for the discovered root: {init}"
);
let grepo = open_git(root).expect("open git repo at the discovered root");
let tip = grepo.head_id().expect("HEAD resolves to a commit");
let commit_count = grepo
.rev_walk([tip])
.all()
.expect("rev-walk checkpoint history")
.count();
assert!(
commit_count >= 2,
"the Git-overlay quickstart imported history and added a checkpoint commit: count={commit_count}"
);
}
#[test]
fn quickstart_refusal_on_existing_git_overlay_writes_nothing() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
git_hermetic(&["init"], dir);
std::fs::write(dir.join("a.txt"), "hi\n").unwrap();
git_hermetic(&["add", "."], dir);
git_hermetic(&["commit", "-m", "initial"], dir);
heddle(&["init", "--no-harness-install"], Some(dir)).unwrap();
git_hermetic(&["switch", "-c", "feature"], dir);
let heddle_dir = dir.join(".heddle");
let before = snapshot_tree(&heddle_dir);
let out = heddle_output(
&[
"init",
"--quickstart",
"--principal-name",
"CI Sentinel",
"--principal-email",
"ci@example.invalid",
"--no-harness-install",
"--output",
"json",
],
Some(dir),
)
.unwrap();
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success(),
"must refuse without --yes on an existing repo: stdout={} stderr={stderr}",
String::from_utf8_lossy(&out.stdout),
);
assert!(
stderr.contains("quickstart_needs_confirmation"),
"the refusal is the confirmation gate: {stderr}"
);
let after = snapshot_tree(&heddle_dir);
assert!(
before == after,
"a refused quickstart must not write to .heddle/ (HEAD-sync must not fire): \
the preflight is read-only and must not open the repo"
);
}
#[test]
fn quickstart_captures_before_installing_harness() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
let out = heddle_output(
&[
"init",
"--quickstart",
"--principal-name",
"CI Sentinel",
"--principal-email",
"ci@example.invalid",
"--install-harnesses",
"claude-code",
"--harness-install-scope",
"repo",
"--yes",
],
Some(dir),
)
.unwrap();
assert!(
out.status.success(),
"quickstart with a harness install must succeed: stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
assert!(
dir.join(".claude").join("settings.json").is_file(),
"the selected harness was installed"
);
assert!(
dir.join("QUICKSTART.md").is_file(),
"the capture ran before the install, so the empty dir got QUICKSTART.md"
);
let log: Value =
serde_json::from_str(&heddle(&["log", "--output", "json"], Some(dir)).unwrap()).unwrap();
assert_eq!(
log["states"].as_array().map(Vec::len),
Some(1),
"exactly one capture: {log}"
);
let status = status_json(dir);
let added: Vec<String> = status["changes"]["added"]
.as_array()
.map(|paths| {
paths
.iter()
.filter_map(|p| p.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default();
assert!(
added.iter().any(|p| p.contains(".claude")),
"harness scaffolding is UNTRACKED — it was installed after the capture: {status}"
);
assert!(
!added.iter().any(|p| p.contains("QUICKSTART.md")),
"QUICKSTART.md was the captured first state, not untracked: {status}"
);
}
#[test]
fn quickstart_rejects_invalid_harness_scope_before_writes() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
let out = heddle_output(
&[
"init",
"--quickstart",
"--principal-name",
"CI Sentinel",
"--principal-email",
"ci@example.invalid",
"--install-harnesses",
"claude-code",
"--harness-install-scope",
"bogus",
"--yes",
"--output",
"json",
],
Some(dir),
)
.unwrap();
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success(),
"an invalid harness scope must be rejected before writes: stdout={} stderr={stderr}",
String::from_utf8_lossy(&out.stdout),
);
assert!(
stderr.contains("integration_scope_invalid"),
"must fail with the integration_scope_invalid advice: {stderr}"
);
assert!(
!dir.join(".heddle").exists(),
"fail-before-writes: an invalid harness scope must leave NO partial .heddle/"
);
assert!(
!dir.join(".claude").exists(),
"no harness integration may be installed on a scope error"
);
}
#[test]
fn quickstart_rejects_codex_repo_scope_before_writes() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
let out = heddle_output(
&[
"init",
"--quickstart",
"--principal-name",
"CI Sentinel",
"--principal-email",
"ci@example.invalid",
"--install-harnesses",
"codex",
"--harness-install-scope",
"repo",
"--yes",
"--output",
"json",
],
Some(dir),
)
.unwrap();
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success(),
"codex + repo scope must be rejected before writes: stdout={} stderr={stderr}",
String::from_utf8_lossy(&out.stdout),
);
assert!(
stderr.contains("integration_codex_scope_invalid"),
"must fail with the harness-specific scope advice: {stderr}"
);
assert!(
!dir.join(".heddle").exists(),
"fail-before-writes: a harness/scope mismatch must leave NO partial .heddle/"
);
assert!(
!dir.join("QUICKSTART.md").exists(),
"fail-before-writes: no QUICKSTART.md placeholder may be written"
);
}
#[test]
fn quickstart_rejects_shallow_git_clone_before_writes() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
git_hermetic(&["init"], dir);
std::fs::write(dir.join("a.txt"), "hi\n").unwrap();
git_hermetic(&["add", "."], dir);
git_hermetic(&["commit", "-m", "initial"], dir);
let grepo = open_git(dir).unwrap();
let head = grepo.head_id().unwrap().to_string();
std::fs::write(dir.join(".git").join("shallow"), format!("{head}\n")).unwrap();
let out = heddle_output(
&[
"init",
"--quickstart",
"--principal-name",
"CI Sentinel",
"--principal-email",
"ci@example.invalid",
"--no-harness-install",
"--yes",
"--output",
"json",
],
Some(dir),
)
.unwrap();
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success(),
"a shallow clone must be rejected before writes: stdout={} stderr={stderr}",
String::from_utf8_lossy(&out.stdout),
);
assert!(
stderr.contains("quickstart_shallow_clone"),
"must fail with the quickstart_shallow_clone advice: {stderr}"
);
assert!(
!dir.join(".heddle").exists(),
"fail-before-writes: a shallow clone must leave NO partial .heddle/"
);
assert!(
!dir.join("QUICKSTART.md").exists(),
"fail-before-writes: no QUICKSTART.md placeholder may be written"
);
}
#[test]
fn quickstart_nested_git_below_ancestor_heddle_targets_nested_git() {
let outer = TempDir::new().unwrap();
let ancestor = outer.path();
heddle(&["init", "--no-harness-install"], Some(ancestor)).unwrap();
assert!(ancestor.join(".heddle").is_dir());
let nested = ancestor.join("nested");
std::fs::create_dir(&nested).unwrap();
git_hermetic(&["init"], &nested);
std::fs::write(nested.join("n.txt"), "nested\n").unwrap();
git_hermetic(&["add", "."], &nested);
git_hermetic(&["commit", "-m", "nested initial"], &nested);
let out = heddle_output(
&[
"init",
"--quickstart",
"--principal-name",
"CI Sentinel",
"--principal-email",
"ci@example.invalid",
"--no-harness-install",
"--yes",
"--output",
"json",
],
Some(&nested),
)
.unwrap();
assert!(
out.status.success(),
"quickstart in a nested Git checkout must bootstrap the nested root: stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
assert!(
nested.join(".heddle").is_dir(),
"the nested Git root was bootstrapped with its own .heddle/"
);
let init: Value = serde_json::from_str(&String::from_utf8_lossy(&out.stdout)).unwrap();
assert_eq!(
init["repository_mode"].as_str(),
Some("git-overlay"),
"the nested Git root runs as a Git overlay: {init}"
);
let nested_status = status_json(&nested);
assert_eq!(
nested_status.get("thread").and_then(Value::as_str),
Some("quickstart"),
"the nested repo carries the quickstart thread: {nested_status}"
);
let ancestor_status = status_json(ancestor);
assert_ne!(
ancestor_status.get("thread").and_then(Value::as_str),
Some("quickstart"),
"the ancestor Heddle repo must NOT have been written into: {ancestor_status}"
);
let grepo = open_git(&nested).expect("open nested git repo");
let tip = grepo.head_id().expect("nested HEAD resolves");
let commit_count = grepo
.rev_walk([tip])
.all()
.expect("rev-walk nested history")
.count();
assert!(
commit_count >= 2,
"the nested Git history was imported and a checkpoint added: count={commit_count}"
);
}
#[test]
fn quickstart_git_overlay_capture_and_checkpoint_land_on_quickstart_thread() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
git_hermetic(&["init", "-b", "main"], dir);
std::fs::write(dir.join("a.txt"), "hi\n").unwrap();
git_hermetic(&["add", "."], dir);
git_hermetic(&["commit", "-m", "initial"], dir);
let out = heddle_output(
&[
"init",
"--quickstart",
"--principal-name",
"CI Sentinel",
"--principal-email",
"ci@example.invalid",
"--no-harness-install",
"--yes",
],
Some(dir),
)
.unwrap();
assert!(
out.status.success(),
"quickstart on a Git overlay must succeed: stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
let status = status_json(dir);
assert_eq!(
status.get("thread").and_then(Value::as_str),
Some("quickstart"),
"the active thread is quickstart: {status}"
);
let grepo = open_git(dir).expect("open git repo");
let count_on = |branch: &str| -> usize {
let mut reference =
find_reference(&grepo, branch).unwrap_or_else(|_| panic!("ref {branch} exists"));
let tip = reference.peel_to_id().expect("ref peels");
grepo.rev_walk([tip]).all().expect("rev-walk").count()
};
assert_eq!(count_on("main"), 1, "main stayed at the imported tip");
assert_eq!(
count_on("quickstart"),
2,
"quickstart advanced: imported tip + the checkpoint commit"
);
}
#[test]
fn quickstart_refuses_clobbering_existing_divergent_quickstart_branch() {
let temp = TempDir::new().unwrap();
let dir = temp.path();
git_hermetic(&["init", "-b", "main"], dir);
std::fs::write(dir.join("a.txt"), "hi\n").unwrap();
git_hermetic(&["add", "."], dir);
git_hermetic(&["commit", "-m", "initial"], dir);
git_hermetic(&["checkout", "-b", "quickstart"], dir);
std::fs::write(dir.join("their-work.txt"), "precious\n").unwrap();
git_hermetic(&["add", "."], dir);
git_hermetic(&["commit", "-m", "their quickstart work"], dir);
git_hermetic(&["checkout", "main"], dir);
let tip_before = {
let grepo = open_git(dir).expect("open git repo");
find_reference(&grepo, "quickstart")
.expect("quickstart branch exists")
.peel_to_id()
.expect("peels")
};
let out = heddle_output(
&[
"init",
"--quickstart",
"--principal-name",
"CI Sentinel",
"--principal-email",
"ci@example.invalid",
"--no-harness-install",
"--yes",
"--output",
"json",
],
Some(dir),
)
.unwrap();
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success(),
"a pre-existing divergent quickstart branch must be refused: stdout={} stderr={stderr}",
String::from_utf8_lossy(&out.stdout),
);
assert!(
stderr.contains("quickstart_thread_branch_collision"),
"must fail with the collision advice pointing at --quickstart-thread: {stderr}"
);
let tip_after = {
let grepo = open_git(dir).expect("open git repo");
find_reference(&grepo, "quickstart")
.expect("quickstart branch still exists")
.peel_to_id()
.expect("peels")
};
assert_eq!(
tip_before, tip_after,
"the user's pre-existing quickstart branch must not be moved"
);
assert!(
!dir.join(".heddle").exists(),
"fail-before-writes: a collision must leave NO partial .heddle/"
);
assert!(
!dir.join("QUICKSTART.md").exists(),
"fail-before-writes: no QUICKSTART.md placeholder may be written"
);
}