use crate::common::{TestRepo, repo, wt_command};
use insta::assert_snapshot;
use rstest::rstest;
use serde_json::Value;
use std::io::Write;
use std::process::Stdio;
fn run_statusline_from_dir(
repo: &TestRepo,
args: &[&str],
stdin_json: Option<&str>,
cwd: &std::path::Path,
) -> String {
let mut cmd = wt_command();
cmd.current_dir(cwd);
cmd.args(["list", "statusline"]);
cmd.args(args);
repo.configure_wt_cmd(&mut cmd);
if stdin_json.is_some() {
cmd.stdin(Stdio::piped());
}
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let mut child = cmd.spawn().expect("failed to spawn command");
if let Some(json) = stdin_json {
let mut stdin = child.stdin.take().expect("failed to get stdin");
stdin
.write_all(json.as_bytes())
.expect("failed to write stdin");
drop(stdin);
}
let output = child.wait_with_output().expect("failed to wait for output");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
if !stdout.is_empty() {
stdout.to_string()
} else {
stderr.to_string()
}
}
fn run_statusline(repo: &TestRepo, args: &[&str], stdin_json: Option<&str>) -> String {
run_statusline_from_dir(repo, args, stdin_json, repo.root_path())
}
fn add_uncommitted_changes(repo: &TestRepo) {
std::fs::write(repo.root_path().join("modified.txt"), "modified content").unwrap();
}
fn add_commits_ahead(repo: &mut TestRepo) {
let feature_path = repo.add_worktree("feature");
std::fs::write(feature_path.join("feature.txt"), "feature content").unwrap();
repo.git_command()
.args(["add", "."])
.current_dir(&feature_path)
.run()
.unwrap();
repo.git_command()
.args(["commit", "-m", "Feature commit 1"])
.current_dir(&feature_path)
.run()
.unwrap();
std::fs::write(feature_path.join("feature2.txt"), "more content").unwrap();
repo.git_command()
.args(["add", "."])
.current_dir(&feature_path)
.run()
.unwrap();
repo.git_command()
.args(["commit", "-m", "Feature commit 2"])
.current_dir(&feature_path)
.run()
.unwrap();
}
#[rstest]
fn test_statusline_basic(repo: TestRepo) {
let output = run_statusline(&repo, &[], None);
assert_snapshot!(output, @"[0m main [2m^[22m[2m|[22m");
}
#[rstest]
fn test_statusline_with_changes(repo: TestRepo) {
add_uncommitted_changes(&repo);
let output = run_statusline(&repo, &[], None);
assert_snapshot!(output, @"[0m main [36m?[0m[2m^[22m[2m|[22m");
}
#[rstest]
fn test_statusline_commits_ahead(mut repo: TestRepo) {
add_commits_ahead(&mut repo);
let feature_path = repo.worktree_path("feature");
let output = run_statusline_from_dir(&repo, &[], None, feature_path);
assert_snapshot!(output, @"[0m feature [2m↑[22m [32m↑2[0m ^[32m+2");
}
fn claude_code_snapshot_settings() -> insta::Settings {
let mut settings = insta::Settings::clone_current();
settings.add_filter(r"_REPO_", "[PATH]");
settings.add_filter(r"/[a-zA-Z0-9/._-]+/repo", "[PATH]");
settings.add_filter(r"^\x1b\[0m ", "");
settings
}
fn escape_path_for_json(path: &std::path::Path) -> String {
path.display().to_string().replace('\\', r"\\")
}
#[rstest]
fn test_statusline_claude_code_full_context(repo: TestRepo) {
add_uncommitted_changes(&repo);
let escaped_path = escape_path_for_json(repo.root_path());
let json = format!(
r#"{{
"hook_event_name": "Status",
"session_id": "test-session",
"model": {{
"id": "claude-opus-4-1",
"display_name": "Opus"
}},
"workspace": {{
"current_dir": "{escaped_path}",
"project_dir": "{escaped_path}"
}},
"version": "1.0.80"
}}"#,
);
let output = run_statusline(&repo, &["--format=claude-code"], Some(&json));
claude_code_snapshot_settings().bind(|| {
assert_snapshot!(output, @"[PATH] main [36m?[0m[2m^[22m[2m|[22m | Opus");
});
}
#[rstest]
fn test_statusline_claude_code_minimal(repo: TestRepo) {
let escaped_path = escape_path_for_json(repo.root_path());
let json = format!(r#"{{"workspace": {{"current_dir": "{escaped_path}"}}}}"#,);
let output = run_statusline(&repo, &["--format=claude-code"], Some(&json));
claude_code_snapshot_settings().bind(|| {
assert_snapshot!(output, @"[PATH] main [2m^[22m[2m|[22m");
});
}
#[rstest]
fn test_statusline_claude_code_with_model(repo: TestRepo) {
let escaped_path = escape_path_for_json(repo.root_path());
let json = format!(
r#"{{
"workspace": {{"current_dir": "{escaped_path}"}},
"model": {{"display_name": "Haiku"}}
}}"#,
);
let output = run_statusline(&repo, &["--format=claude-code"], Some(&json));
claude_code_snapshot_settings().bind(|| {
assert_snapshot!(output, @"[PATH] main [2m^[22m[2m|[22m | Haiku");
});
}
#[rstest]
fn test_statusline_claude_code_with_context_gauge(repo: TestRepo) {
let escaped_path = escape_path_for_json(repo.root_path());
let json = format!(
r#"{{
"workspace": {{"current_dir": "{escaped_path}"}},
"model": {{"display_name": "Opus"}},
"context_window": {{"used_percentage": 42}}
}}"#,
);
let output = run_statusline(&repo, &["--format=claude-code"], Some(&json));
claude_code_snapshot_settings().bind(|| {
assert_snapshot!(output, @"[PATH] main [2m^[22m[2m|[22m | Opus 🌕 42%");
});
}
#[rstest]
fn test_statusline_claude_code_context_gauge_low(repo: TestRepo) {
let escaped_path = escape_path_for_json(repo.root_path());
let json = format!(
r#"{{
"workspace": {{"current_dir": "{escaped_path}"}},
"model": {{"display_name": "Opus"}},
"context_window": {{"used_percentage": 5}}
}}"#,
);
let output = run_statusline(&repo, &["--format=claude-code"], Some(&json));
claude_code_snapshot_settings().bind(|| {
assert_snapshot!(output, @"[PATH] main [2m^[22m[2m|[22m | Opus 🌕 5%");
});
}
#[rstest]
fn test_statusline_claude_code_context_gauge_high(repo: TestRepo) {
let escaped_path = escape_path_for_json(repo.root_path());
let json = format!(
r#"{{
"workspace": {{"current_dir": "{escaped_path}"}},
"model": {{"display_name": "Opus"}},
"context_window": {{"used_percentage": 98}}
}}"#,
);
let output = run_statusline(&repo, &["--format=claude-code"], Some(&json));
claude_code_snapshot_settings().bind(|| {
assert_snapshot!(output, @"[PATH] main [2m^[22m[2m|[22m | Opus 🌑 98%");
});
}
#[rstest]
fn test_statusline_claude_code_missing_context_window(repo: TestRepo) {
let escaped_path = escape_path_for_json(repo.root_path());
let json = format!(
r#"{{
"workspace": {{"current_dir": "{escaped_path}"}},
"model": {{"display_name": "Opus"}}
}}"#,
);
let output = run_statusline(&repo, &["--format=claude-code"], Some(&json));
claude_code_snapshot_settings().bind(|| {
assert!(
!output.contains('○')
&& !output.contains('◔')
&& !output.contains('◑')
&& !output.contains('◕')
&& !output.contains('●'),
"No gauge should appear when context_window is missing: {output}"
);
assert_snapshot!(output, @"[PATH] main [2m^[22m[2m|[22m | Opus");
});
}
#[rstest]
fn test_statusline_reflects_checked_out_branch(mut repo: TestRepo) {
let feature_path = repo.add_worktree("feature");
let output = run_statusline_from_dir(&repo, &[], None, &feature_path);
assert!(
output.contains("feature"),
"statusline should show 'feature' for feature worktree, got: {output}"
);
repo.git_command().args(["branch", "other"]).run().unwrap();
let checkout_output = repo
.git_command()
.args(["checkout", "other"])
.current_dir(&feature_path)
.run()
.unwrap();
assert!(
checkout_output.status.success(),
"checkout should succeed: {}",
String::from_utf8_lossy(&checkout_output.stderr)
);
let output = run_statusline_from_dir(&repo, &[], None, &feature_path);
assert!(
output.contains("other"),
"statusline should show 'other' after checkout, got: {output}"
);
assert!(
!output.contains("feature"),
"statusline should not show 'feature' after checkout, got: {output}"
);
}
#[rstest]
fn test_statusline_detached_head(mut repo: TestRepo) {
let feature_path = repo.add_worktree("feature");
repo.git_command()
.args(["checkout", "--detach"])
.current_dir(&feature_path)
.run()
.unwrap();
let output = run_statusline_from_dir(&repo, &[], None, &feature_path);
assert!(
output.contains("HEAD") || !output.contains("feature"),
"statusline should not show 'feature' in detached HEAD, got: {output}"
);
}
#[rstest]
fn test_statusline_with_url(repo: TestRepo) {
repo.write_project_config(
r#"[list]
url = "http://{{ branch }}.localhost:3000"
"#,
);
let output = run_statusline(&repo, &[], None);
assert_snapshot!(output, @"[0m main [36m?[0m[2m^[22m[2m|[22m http://main.localhost:3000");
}
#[rstest]
fn test_statusline_url_in_feature_worktree(mut repo: TestRepo) {
repo.write_project_config(
r#"[list]
url = "http://{{ branch }}.localhost:3000"
"#,
);
repo.run_git(&["add", ".config/wt.toml"]);
repo.run_git(&["commit", "-m", "Add project config"]);
let feature_path = repo.add_worktree("feature");
let output = run_statusline_from_dir(&repo, &[], None, &feature_path);
assert_snapshot!(output, @"[0m feature [2m_[22m http://feature.localhost:3000");
}
#[rstest]
fn test_statusline_json_basic(repo: TestRepo) {
let output = run_statusline(&repo, &["--format=json"], None);
let parsed: Value = serde_json::from_str(&output).expect("should be valid JSON");
let items = parsed.as_array().expect("should be an array");
assert_eq!(
items.len(),
1,
"should have exactly one item (current worktree)"
);
let item = &items[0];
assert_eq!(item["branch"], "main");
assert_eq!(item["kind"], "worktree");
assert!(item["is_current"].as_bool().unwrap());
assert!(item["is_main"].as_bool().unwrap());
assert!(item["commit"]["sha"].is_string());
assert!(item["commit"]["short_sha"].is_string());
}
#[rstest]
fn test_statusline_json_with_changes(repo: TestRepo) {
std::fs::write(repo.root_path().join("modified.txt"), "modified content").unwrap();
let output = run_statusline(&repo, &["--format=json"], None);
let parsed: Value = serde_json::from_str(&output).expect("should be valid JSON");
let item = &parsed[0];
assert_eq!(item["branch"], "main");
let working_tree = &item["working_tree"];
assert!(
working_tree["untracked"].as_bool().unwrap(),
"should show untracked file"
);
}
#[rstest]
fn test_statusline_json_feature_branch(mut repo: TestRepo) {
let feature_path = repo.add_worktree("feature");
std::fs::write(feature_path.join("feature.txt"), "content").unwrap();
repo.git_command()
.args(["add", "."])
.current_dir(&feature_path)
.run()
.unwrap();
repo.git_command()
.args(["commit", "-m", "Feature commit"])
.current_dir(&feature_path)
.run()
.unwrap();
let output = run_statusline_from_dir(&repo, &["--format=json"], None, &feature_path);
let parsed: Value = serde_json::from_str(&output).expect("should be valid JSON");
let item = &parsed[0];
assert_eq!(item["branch"], "feature");
assert!(item["is_current"].as_bool().unwrap());
assert!(!item["is_main"].as_bool().unwrap());
assert!(
item["main"]["ahead"].as_u64().unwrap() >= 1,
"should be ahead of main"
);
}
#[rstest]
fn test_statusline_json_ignores_claude_code(repo: TestRepo) {
let escaped_path = escape_path_for_json(repo.root_path());
let json = format!(
r#"{{
"workspace": {{"current_dir": "{escaped_path}"}},
"model": {{"display_name": "Opus"}}
}}"#,
);
let output = run_statusline(&repo, &["--format=json", "--claude-code"], Some(&json));
let parsed: Value = serde_json::from_str(&output).expect("should be valid JSON");
assert!(parsed.is_array(), "should produce JSON array output");
let item = &parsed[0];
assert_eq!(item["branch"], "main");
}
#[rstest]
fn test_statusline_nested_worktree(mut repo: TestRepo) {
let nested_path = repo.root_path().join(".worktrees").join("feature");
let nested_worktree = repo.add_worktree_at_path("feature", &nested_path);
let output = repo
.wt_command()
.current_dir(&nested_worktree)
.args(["list", "statusline"])
.output()
.expect("statusline should succeed");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("feature"),
"Nested worktree should show 'feature' branch, got: {stdout}"
);
assert!(
!stdout.contains("main"),
"Nested worktree should NOT show 'main' branch, got: {stdout}"
);
}
#[rstest]
fn test_statusline_json_nested_worktree(mut repo: TestRepo) {
let nested_path = repo.root_path().join(".worktrees").join("feature");
let nested_worktree = repo.add_worktree_at_path("feature", &nested_path);
let output = repo
.wt_command()
.current_dir(&nested_worktree)
.args(["list", "statusline", "--format=json"])
.output()
.expect("statusline should succeed");
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: Value = serde_json::from_str(&stdout).expect("should be valid JSON");
assert!(parsed.is_array(), "should produce JSON array");
let items = parsed.as_array().unwrap();
assert_eq!(items.len(), 1, "should have exactly one item");
assert_eq!(
items[0]["branch"], "feature",
"Nested worktree should report 'feature' branch, not parent"
);
}