use std::fs;
use std::path::PathBuf;
use insta::assert_snapshot;
use rstest::rstest;
use crate::common::{TestRepo, repo, setup_snapshot_settings};
fn corrupt_worktree_head(repo: &TestRepo, worktree_name: &str) -> PathBuf {
let feature_path = repo.worktrees.get(worktree_name).unwrap();
let git_dir = feature_path.join(".git");
let git_content = fs::read_to_string(&git_dir).unwrap();
let actual_git_dir = git_content
.strip_prefix("gitdir: ")
.unwrap()
.trim()
.to_string();
let head_path = PathBuf::from(&actual_git_dir).join("HEAD");
fs::write(&head_path, "invalid").unwrap();
head_path
}
#[rstest]
fn test_diagnostic_report_file_format(mut repo: TestRepo) {
repo.add_worktree("feature");
corrupt_worktree_head(&repo, "feature");
let output = repo.wt_command().args(["list", "-vv"]).output().unwrap();
let diagnostic_path = repo
.root_path()
.join(".git")
.join("wt/logs")
.join("diagnostic.md");
assert!(
diagnostic_path.exists(),
"Diagnostic file should be generated at {:?}",
diagnostic_path
);
let content = fs::read_to_string(&diagnostic_path).unwrap();
assert!(
content.contains("<summary>Trace log</summary>"),
"Diagnostic should include trace log section when run with -vv"
);
let settings = setup_snapshot_settings(&repo);
settings.bind(|| {
assert_snapshot!("diagnostic_file_format", normalize_report(&content));
});
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("Diagnostic saved"),
"Output should mention diagnostic was saved. stderr: {}",
stderr
);
}
#[rstest]
fn test_diagnostic_not_created_without_vv(mut repo: TestRepo) {
repo.add_worktree("feature");
corrupt_worktree_head(&repo, "feature");
repo.wt_command().args(["list"]).output().unwrap();
let diagnostic_path = repo
.root_path()
.join(".git")
.join("wt/logs")
.join("diagnostic.md");
assert!(
!diagnostic_path.exists(),
"Diagnostic file should NOT be created without -vv"
);
}
#[rstest]
fn test_diagnostic_hint_without_vv(mut repo: TestRepo) {
repo.add_worktree("feature");
corrupt_worktree_head(&repo, "feature");
let output = repo.wt_command().args(["list"]).output().unwrap();
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("To create a diagnostic file, run with"),
"Should hint to use -vv. stderr: {}",
stderr
);
assert!(
stderr.contains("-vv"),
"Hint should mention -vv flag. stderr: {}",
stderr
);
}
#[rstest]
fn test_diagnostic_contains_required_sections(mut repo: TestRepo) {
repo.add_worktree("feature");
corrupt_worktree_head(&repo, "feature");
repo.wt_command().args(["list", "-vv"]).output().unwrap();
let content = fs::read_to_string(
repo.root_path()
.join(".git")
.join("wt/logs")
.join("diagnostic.md"),
)
.unwrap();
assert!(
content.contains("## Diagnostic Report"),
"Should have header"
);
assert!(content.contains("**Generated:**"), "Should have timestamp");
assert!(content.contains("**Command:**"), "Should have command");
assert!(content.contains("**Result:**"), "Should have result");
assert!(
content.contains("<summary>Environment</summary>"),
"Should have environment section"
);
assert!(content.contains("wt "), "Should have wt version");
assert!(content.contains("git "), "Should have git version");
assert!(
content.contains("Shell integration:"),
"Should have shell integration status"
);
assert!(
content.contains("<summary>Worktrees</summary>"),
"Should have worktrees section"
);
assert!(
content.contains("refs/heads/"),
"Should have branch refs in worktree list"
);
assert!(
content.contains("<summary>Config</summary>"),
"Should have config section"
);
assert!(
content.contains("<summary>Trace log</summary>"),
"Should have trace log section"
);
}
#[rstest]
fn test_diagnostic_context_has_no_ansi_codes(mut repo: TestRepo) {
repo.add_worktree("feature");
corrupt_worktree_head(&repo, "feature");
repo.wt_command().args(["list", "-vv"]).output().unwrap();
let content = fs::read_to_string(
repo.root_path()
.join(".git")
.join("wt/logs")
.join("diagnostic.md"),
)
.unwrap();
assert!(
!content.contains("\x1b["),
"Diagnostic file should not contain ANSI escape codes"
);
assert!(
!content.contains("\u{001b}"),
"Diagnostic file should not contain ANSI escape codes (unicode)"
);
}
#[rstest]
fn test_diagnostic_trace_log_contains_git_commands(mut repo: TestRepo) {
repo.add_worktree("feature");
corrupt_worktree_head(&repo, "feature");
repo.wt_command().args(["list", "-vv"]).output().unwrap();
let content = fs::read_to_string(
repo.root_path()
.join(".git")
.join("wt/logs")
.join("diagnostic.md"),
)
.unwrap();
let trace_start = content
.find("<summary>Trace log</summary>")
.expect("Should have trace log");
let trace_section = &content[trace_start..];
assert!(
trace_section.contains("git worktree list"),
"Trace log should contain git worktree list command"
);
assert!(
trace_section.contains("[wt-trace]"),
"Trace log should contain wt-trace entries"
);
assert!(
trace_section.contains("dur_us="),
"Trace log should contain command durations in microseconds"
);
assert!(
trace_section.contains("ok="),
"Trace log should contain success/failure indicators"
);
}
#[rstest]
fn test_diagnostic_saved_message_with_vv(mut repo: TestRepo) {
repo.add_worktree("feature");
corrupt_worktree_head(&repo, "feature");
let output = repo.wt_command().args(["list", "-vv"]).output().unwrap();
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("Diagnostic saved"),
"Should mention diagnostic was saved. stderr: {}",
stderr
);
}
#[rstest]
fn test_diagnostic_written_to_correct_location(mut repo: TestRepo) {
repo.add_worktree("feature");
corrupt_worktree_head(&repo, "feature");
repo.wt_command().args(["list", "-vv"]).output().unwrap();
let wt_logs_dir = repo.root_path().join(".git").join("wt/logs");
assert!(wt_logs_dir.exists());
let diagnostic_path = wt_logs_dir.join("diagnostic.md");
assert!(
diagnostic_path.exists(),
"diagnostic.md should be in wt/logs"
);
let content = fs::read_to_string(&diagnostic_path).unwrap();
assert!(
content.starts_with("## "),
"Should be a markdown file starting with header"
);
}
#[rstest]
fn test_log_files_created(mut repo: TestRepo) {
repo.add_worktree("feature");
corrupt_worktree_head(&repo, "feature");
repo.wt_command().args(["list", "-vv"]).output().unwrap();
let logs_dir = repo.root_path().join(".git").join("wt/logs");
let trace_log = logs_dir.join("trace.log");
let output_log = logs_dir.join("output.log");
assert!(trace_log.exists(), "trace.log should be created with -vv");
assert!(output_log.exists(), "output.log should be created with -vv");
let trace = fs::read_to_string(&trace_log).unwrap();
assert!(!trace.is_empty(), "trace.log should not be empty");
assert!(
trace.contains("[wt-trace]"),
"trace.log should contain trace entries"
);
}
#[rstest]
fn test_vv_splits_full_and_bounded_output(repo: TestRepo) {
repo.wt_command().args(["list", "-vv"]).output().unwrap();
let logs_dir = repo.root_path().join(".git").join("wt/logs");
let trace = fs::read_to_string(logs_dir.join("trace.log")).unwrap();
let output = fs::read_to_string(logs_dir.join("output.log")).unwrap();
assert!(
trace.contains("[wt-trace]"),
"trace.log should contain [wt-trace] records at -vv"
);
assert!(
output.lines().any(|l| l.contains(" worktree ")),
"output.log should contain `git worktree list --porcelain` stdout lines at -vv"
);
assert!(
!output.contains("[wt-trace]"),
"output.log should not contain [wt-trace] records"
);
}
#[rstest]
fn test_vv_bounded_on_stderr_full_in_output_log(repo: TestRepo) {
let head_out = repo
.git_command()
.args(["rev-parse", "HEAD"])
.run()
.unwrap();
let head = String::from_utf8(head_out.stdout)
.unwrap()
.trim()
.to_string();
let mut content = String::from("# pack-refs with: peeled fully-peeled sorted\n");
for i in 0..250 {
use std::fmt::Write as _;
writeln!(&mut content, "{head} refs/heads/many-branch-{i:03}").unwrap();
}
std::fs::write(repo.root_path().join(".git/packed-refs"), content).unwrap();
let output = repo
.wt_command()
.args(["list", "-vv"])
.env("NO_COLOR", "1")
.output()
.expect("wt list");
let stderr = String::from_utf8_lossy(&output.stderr);
let marker = "more lines, ";
assert!(
stderr.contains(marker),
"stderr should contain elision marker for >200-line subprocess output: {stderr}"
);
let logs_dir = repo.root_path().join(".git").join("wt/logs");
let trace = fs::read_to_string(logs_dir.join("trace.log")).unwrap();
let raw = fs::read_to_string(logs_dir.join("output.log")).unwrap();
assert!(
trace.contains(marker),
"trace.log mirrors stderr and should include the elision marker"
);
assert!(
!raw.contains(marker),
"output.log holds full output and must not contain an elision marker"
);
let tail_ref = "many-branch-249";
assert!(
raw.contains(tail_ref),
"output.log should contain the full for-each-ref stdout (last ref)"
);
assert!(
!stderr.contains(tail_ref),
"stderr preview should be bounded before the last ref"
);
assert!(
!trace.contains(tail_ref),
"trace.log mirrors stderr and should be bounded too"
);
}
#[rstest]
fn test_rust_log_debug_fallback_without_vv(repo: TestRepo) {
let output = repo
.wt_command()
.args(["list"])
.env("RUST_LOG", "debug")
.env("NO_COLOR", "1")
.output()
.expect("wt list");
let stderr = String::from_utf8_lossy(&output.stderr);
let logs_dir = repo.root_path().join(".git").join("wt/logs");
assert!(
!logs_dir.join("trace.log").exists(),
"trace.log should NOT be created without -vv"
);
assert!(
!logs_dir.join("output.log").exists(),
"output.log should NOT be created without -vv"
);
assert!(
stderr.lines().any(|l| l.contains(" worktree ")),
"stderr should contain subprocess stdout via bounded preview: {stderr}"
);
assert!(
!stderr.contains("more lines, "),
"short subprocess output should not trip the elision marker: {stderr}"
);
}
#[rstest]
fn test_vv_writes_diagnostic_on_success(repo: TestRepo) {
let output = repo.wt_command().args(["list", "-vv"]).output().unwrap();
assert!(output.status.success(), "Command should succeed");
let diagnostic_path = repo
.root_path()
.join(".git")
.join("wt/logs")
.join("diagnostic.md");
assert!(
diagnostic_path.exists(),
"Diagnostic file should be created with -vv even on success"
);
let content = fs::read_to_string(&diagnostic_path).unwrap();
assert!(
content.contains("Command completed successfully"),
"Result should indicate success. Content: {}",
content
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("Diagnostic saved"),
"stderr should mention diagnostic was saved. stderr: {}",
stderr
);
}
#[rstest]
fn test_vv_writes_diagnostic_on_error(mut repo: TestRepo) {
repo.add_worktree("feature");
corrupt_worktree_head(&repo, "feature");
let output = repo.wt_command().args(["list", "-vv"]).output().unwrap();
let diagnostic_path = repo
.root_path()
.join(".git")
.join("wt/logs")
.join("diagnostic.md");
assert!(
diagnostic_path.exists(),
"Diagnostic file should be created with -vv on error"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("Diagnostic saved"),
"stderr should mention diagnostic was saved. stderr: {}",
stderr
);
}
#[rstest]
fn test_v_does_not_write_log_files(repo: TestRepo) {
let output = repo.wt_command().args(["list", "-v"]).output().unwrap();
assert!(output.status.success(), "Command should succeed");
let wt_logs = repo.root_path().join(".git").join("wt/logs");
for name in ["diagnostic.md", "trace.log", "output.log"] {
assert!(
!wt_logs.join(name).exists(),
"{name} should NOT be created with just -v (requires -vv)"
);
}
}
#[test]
fn test_vv_outside_repo_no_crash() {
use crate::common::wt_command;
let temp_dir = tempfile::tempdir().unwrap();
let output = wt_command()
.args(["--version", "-vv"])
.current_dir(temp_dir.path())
.output()
.unwrap();
assert!(output.status.success(), "Command should succeed");
let diagnostic_path = temp_dir
.path()
.join(".git")
.join("wt/logs")
.join("diagnostic.md");
assert!(
!diagnostic_path.exists(),
"Diagnostic file should NOT be created outside a git repo"
);
}
#[rstest]
fn test_diagnostic_gh_hint_with_vv(mut repo: TestRepo) {
repo.setup_mock_gh();
let output = repo.wt_command().args(["list", "-vv"]).output().unwrap();
let stderr = String::from_utf8_lossy(&output.stderr);
let hint_line = stderr
.lines()
.find(|line| line.contains("report a bug"))
.expect("Should have hint about reporting a bug");
let normalized = regex::Regex::new(r"--web '?[^' \x1b]*diagnostic\.md'?")
.unwrap()
.replace(hint_line, "--web [DIAGNOSTIC_PATH]");
let settings = setup_snapshot_settings(&repo);
settings.bind(|| {
assert_snapshot!("diagnostic_gh_hint", normalized);
});
}
fn normalize_report(content: &str) -> String {
let mut result = content.to_string();
result = regex::Regex::new(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z")
.unwrap()
.replace_all(&result, "[TIMESTAMP]")
.to_string();
result = regex::Regex::new(r"\*\*Command:\*\* `[^`]+`")
.unwrap()
.replace_all(
&result,
"**Command:** `[PROJECT_ROOT]/target/debug/wt list -vv`",
)
.to_string();
result = regex::Regex::new(r"wt [^ ]+ \([^)]+\)")
.unwrap()
.replace_all(&result, "wt [VERSION] ([OS] [ARCH])")
.to_string();
result = regex::Regex::new(r"git \d+\.\d+[^\n]*")
.unwrap()
.replace_all(&result, "git [GIT_VERSION]")
.to_string();
result = regex::Regex::new(r"worktree (?:/|[A-Za-z]:)[^\n]+")
.unwrap()
.replace_all(&result, "worktree [PATH]")
.to_string();
result = regex::Regex::new(r"HEAD [a-f0-9]{40}")
.unwrap()
.replace_all(&result, "HEAD [COMMIT]")
.to_string();
result = regex::Regex::new(r"\b[a-f0-9]{40}\b")
.unwrap()
.replace_all(&result, "[HASH]")
.to_string();
result = regex::Regex::new(r"User config: [^\n]+")
.unwrap()
.replace_all(&result, "User config: [TEST_CONFIG]")
.to_string();
result = regex::Regex::new(r"Project config: (?:/|[A-Za-z]:)[^\n]+\.config[/\\]wt\.toml")
.unwrap()
.replace_all(&result, "Project config: _REPO_/.config/wt.toml")
.to_string();
result = regex::Regex::new(r"([A-Z]:[^\s)`]+[\\/]repo\.[^\s)`]+|/[^\s)`]+/repo\.[^\s)`]+)")
.unwrap()
.replace_all(&result, "[REPO_PATH]")
.to_string();
result = regex::Regex::new(r"(fatal: not a git repository:)\s*\n\s*(\[REPO_PATH\])")
.unwrap()
.replace_all(&result, "$1 $2")
.to_string();
if let Some(start) = result.find("<summary>Trace log</summary>") {
if let Some(end_offset) = result[start..].find("</details>") {
let end = start + end_offset + "</details>".len();
let before = &result[..start];
let after = &result[end..];
result = format!(
"{}<summary>Trace log</summary>\n\n[TRACE_LOG_CONTENT]\n</details>{}",
before, after
);
}
}
result
}