#![allow(
clippy::format_push_string,
clippy::uninlined_format_args,
clippy::redundant_clone,
clippy::manual_assert,
clippy::too_many_lines,
clippy::redundant_closure_for_method_calls,
clippy::case_sensitive_file_extension_comparisons,
clippy::unnecessary_map_or,
clippy::doc_markdown
)]
mod common;
use beads_rust::storage::SqliteStorage;
use beads_rust::sync::{
ExportConfig, ImportConfig, PreflightCheckStatus, preflight_export, preflight_import,
};
use common::cli::{BrWorkspace, run_br};
use std::collections::HashMap;
use std::fs;
use std::io::Write;
use std::path::Path;
fn snapshot_directory(dir: &Path) -> HashMap<String, Vec<u8>> {
let mut snapshot = HashMap::new();
if !dir.exists() {
return snapshot;
}
for entry in walkdir::WalkDir::new(dir)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file())
{
let path = entry.path();
let relative = path.strip_prefix(dir).unwrap_or(path);
if let Ok(content) = fs::read(path) {
snapshot.insert(relative.to_string_lossy().to_string(), content);
}
}
snapshot
}
#[allow(dead_code)]
fn assert_directory_unchanged(before: &HashMap<String, Vec<u8>>, dir: &Path, context: &str) {
let after = snapshot_directory(dir);
for path in after.keys() {
if !before.contains_key(path) {
panic!(
"SAFETY VIOLATION [{}]: New file created: {}\n\
Preflight should prevent ANY file modifications!",
context, path
);
}
}
for (path, old_content) in before {
if let Some(new_content) = after.get(path) {
if path.ends_with(".db")
|| path.ends_with(".db-journal")
|| path.ends_with(".db-wal")
|| path.ends_with(".db-shm")
{
continue;
}
if old_content != new_content {
panic!(
"SAFETY VIOLATION [{}]: File modified: {}\n\
Old size: {}, New size: {}\n\
Preflight should prevent ANY file modifications!",
context,
path,
old_content.len(),
new_content.len()
);
}
}
}
}
fn setup_workspace_with_issues() -> BrWorkspace {
let workspace = BrWorkspace::new();
let init = run_br(&workspace, ["init"], "init");
assert!(init.status.success(), "init failed: {}", init.stderr);
let _ = run_br(
&workspace,
["create", "Test issue 1", "-t", "task"],
"create1",
);
let _ = run_br(
&workspace,
["create", "Test issue 2", "-t", "bug"],
"create2",
);
let export = run_br(&workspace, ["sync", "--flush-only"], "export");
assert!(export.status.success(), "export failed: {}", export.stderr);
workspace
}
#[test]
fn preflight_import_rejects_conflict_markers() {
let workspace = setup_workspace_with_issues();
let beads_dir = workspace.root.join(".beads");
let jsonl_path = beads_dir.join("issues.jsonl");
let snapshot_before = snapshot_directory(&workspace.root);
let original = fs::read_to_string(&jsonl_path).expect("read jsonl");
let mut file = fs::File::create(&jsonl_path).expect("create jsonl");
writeln!(file, "<<<<<<< HEAD").unwrap();
write!(file, "{}", original).unwrap();
writeln!(file, "=======").unwrap();
writeln!(file, r#"{{"id":"bd-conflict","title":"Conflict version"}}"#).unwrap();
writeln!(file, ">>>>>>> feature-branch").unwrap();
let config = ImportConfig {
beads_dir: Some(beads_dir.clone()),
..Default::default()
};
let preflight_result =
preflight_import(&jsonl_path, &config, None).expect("preflight should run");
let log = format!(
"=== CONFLICT MARKER PREFLIGHT TEST ===\n\
JSONL path: {}\n\n\
Preflight status: {:?}\n\
Checks:\n{}\n",
jsonl_path.display(),
preflight_result.overall_status,
preflight_result
.checks
.iter()
.map(|c| format!(
" - {} [{:?}]: {}\n Remediation: {:?}",
c.name, c.status, c.message, c.remediation
))
.collect::<Vec<_>>()
.join("\n")
);
let log_path = workspace.log_dir.join("preflight_conflict_marker.log");
fs::write(&log_path, &log).expect("write log");
assert_eq!(
preflight_result.overall_status,
PreflightCheckStatus::Fail,
"SAFETY: Preflight should FAIL when conflict markers are present.\n\
Log: {}",
log_path.display()
);
let failures = preflight_result.failures();
let conflict_failure = failures.iter().find(|c| c.name == "no_conflict_markers");
assert!(
conflict_failure.is_some(),
"Preflight should fail on 'no_conflict_markers' check.\nFailures: {:?}",
failures
);
let check = conflict_failure.unwrap();
assert!(
check
.remediation
.as_ref()
.map_or(false, |r| r.to_lowercase().contains("resolve")),
"Remediation should mention resolving conflicts. Got: {:?}",
check.remediation
);
let snapshot_after = snapshot_directory(&workspace.root);
for (path, old_content) in &snapshot_before {
if path.ends_with("issues.jsonl") {
continue;
}
if path.ends_with(".db")
|| path.ends_with(".db-journal")
|| path.ends_with(".db-wal")
|| path.ends_with(".db-shm")
{
continue;
}
if let Some(new_content) = snapshot_after.get(path) {
assert_eq!(
old_content, new_content,
"SAFETY VIOLATION: File {} was modified during preflight!",
path
);
}
}
eprintln!("✓ Preflight correctly rejected conflict markers");
}
#[test]
fn preflight_import_conflict_markers_shows_line_numbers() {
let workspace = setup_workspace_with_issues();
let beads_dir = workspace.root.join(".beads");
let jsonl_path = beads_dir.join("issues.jsonl");
let mut file = fs::File::create(&jsonl_path).expect("create jsonl");
writeln!(file, r#"{{"id":"bd-1","title":"Issue 1","status":"open","priority":2,"issue_type":"task","created_at":"2024-01-01T00:00:00Z","updated_at":"2024-01-01T00:00:00Z","labels":[],"dependencies":[],"comments":[]}}"#).unwrap();
writeln!(file, "<<<<<<< HEAD").unwrap(); writeln!(file, r#"{{"id":"bd-2","title":"Issue 2"}}"#).unwrap();
writeln!(file, "=======").unwrap(); writeln!(file, r#"{{"id":"bd-2","title":"Modified Issue 2"}}"#).unwrap();
writeln!(file, ">>>>>>> branch").unwrap();
let config = ImportConfig {
beads_dir: Some(beads_dir),
..Default::default()
};
let result = preflight_import(&jsonl_path, &config, None).expect("preflight should run");
assert_eq!(result.overall_status, PreflightCheckStatus::Fail);
let failures = result.failures();
let conflict_check = failures
.iter()
.find(|c| c.name == "no_conflict_markers")
.expect("Should have conflict marker failure");
assert!(
conflict_check.message.contains("line") || conflict_check.message.contains("marker"),
"Error message should be actionable with line info. Got: {}",
conflict_check.message
);
eprintln!("✓ Preflight shows actionable conflict marker info");
}
#[test]
fn preflight_import_rejects_outside_beads_dir() {
let workspace = setup_workspace_with_issues();
let beads_dir = workspace.root.join(".beads");
let outside_path = workspace.root.join("malicious.jsonl");
fs::write(
&outside_path,
r#"{"id":"bd-1","title":"Test","status":"open","priority":2,"issue_type":"task","created_at":"2024-01-01T00:00:00Z","updated_at":"2024-01-01T00:00:00Z","labels":[],"dependencies":[],"comments":[]}"#,
)
.expect("write test file");
let config = ImportConfig {
beads_dir: Some(beads_dir.clone()),
allow_external_jsonl: false,
..Default::default()
};
let result = preflight_import(&outside_path, &config, None).expect("preflight should run");
let log = format!(
"=== OUTSIDE BEADS DIR PREFLIGHT TEST ===\n\
Path: {}\n\
Beads dir: {}\n\n\
Preflight status: {:?}\n\
Checks:\n{}\n",
outside_path.display(),
beads_dir.display(),
result.overall_status,
result
.checks
.iter()
.map(|c| format!(" - {} [{:?}]: {}", c.name, c.status, c.message))
.collect::<Vec<_>>()
.join("\n")
);
let log_path = workspace.log_dir.join("preflight_outside_beads.log");
fs::write(&log_path, &log).expect("write log");
assert_eq!(
result.overall_status,
PreflightCheckStatus::Fail,
"SAFETY: Preflight should FAIL for paths outside .beads/.\n\
Log: {}",
log_path.display()
);
let failures = result.failures();
let path_failure = failures.iter().find(|c| c.name == "path_validation");
assert!(
path_failure.is_some(),
"Preflight should fail on 'path_validation' check.\nFailures: {:?}",
failures
);
eprintln!("✓ Preflight correctly rejected path outside .beads/");
}
#[test]
fn preflight_import_rejects_git_paths() {
let workspace = setup_workspace_with_issues();
let beads_dir = workspace.root.join(".beads");
let git_dir = workspace.root.join(".git");
fs::create_dir_all(&git_dir).expect("create .git");
let git_path = git_dir.join("config.jsonl");
fs::write(
&git_path,
r#"{"id":"bd-1","title":"Test","status":"open","priority":2,"issue_type":"task","created_at":"2024-01-01T00:00:00Z","updated_at":"2024-01-01T00:00:00Z","labels":[],"dependencies":[],"comments":[]}"#,
)
.expect("write test file");
let config = ImportConfig {
beads_dir: Some(beads_dir.clone()),
allow_external_jsonl: true, ..Default::default()
};
let result = preflight_import(&git_path, &config, None).expect("preflight should run");
assert_eq!(
result.overall_status,
PreflightCheckStatus::Fail,
"CRITICAL SAFETY: Preflight should ALWAYS reject .git paths!"
);
let failures = result.failures();
let path_failure = failures.iter().find(|c| c.name == "path_validation");
assert!(
path_failure.is_some(),
"Preflight should fail on path validation for .git paths"
);
let path_check = path_failure.unwrap();
assert!(
path_check.message.to_lowercase().contains("git"),
"Error should mention git. Got: {}",
path_check.message
);
eprintln!("✓ Preflight correctly rejected .git path");
}
#[test]
fn preflight_import_rejects_path_traversal() {
let workspace = setup_workspace_with_issues();
let beads_dir = workspace.root.join(".beads");
let parent = workspace.root.parent().unwrap();
let traversal_target = parent.join("traversal_test.jsonl");
fs::write(
&traversal_target,
r#"{"id":"bd-1","title":"Test","status":"open","priority":2,"issue_type":"task","created_at":"2024-01-01T00:00:00Z","updated_at":"2024-01-01T00:00:00Z","labels":[],"dependencies":[],"comments":[]}"#,
)
.expect("write test file");
let traversal_path = beads_dir.join("..").join("..").join("traversal_test.jsonl");
let config = ImportConfig {
beads_dir: Some(beads_dir),
allow_external_jsonl: false,
..Default::default()
};
let result = preflight_import(&traversal_path, &config, None).expect("preflight should run");
assert_eq!(
result.overall_status,
PreflightCheckStatus::Fail,
"SAFETY: Preflight should reject path traversal attempts"
);
let _ = fs::remove_file(&traversal_target);
eprintln!("✓ Preflight correctly rejected path traversal");
}
#[test]
fn preflight_export_rejects_git_paths() {
let workspace = setup_workspace_with_issues();
let beads_dir = workspace.root.join(".beads");
let db_path = beads_dir.join("beads.db");
let git_dir = workspace.root.join(".git");
fs::create_dir_all(&git_dir).expect("create .git");
let git_output = git_dir.join("issues.jsonl");
let storage = SqliteStorage::open(&db_path).expect("open db");
let config = ExportConfig {
beads_dir: Some(beads_dir),
allow_external_jsonl: true, ..Default::default()
};
let result = preflight_export(&storage, &git_output, &config).expect("preflight should run");
assert_eq!(
result.overall_status,
PreflightCheckStatus::Fail,
"CRITICAL SAFETY: Export preflight should ALWAYS reject .git paths!"
);
eprintln!("✓ Export preflight correctly rejected .git path");
}
#[test]
fn preflight_export_warns_empty_db_over_nonempty_jsonl() {
let workspace = setup_workspace_with_issues();
let beads_dir = workspace.root.join(".beads");
let jsonl_path = beads_dir.join("issues.jsonl");
let db_path = beads_dir.join("beads_test_empty.db");
let storage = SqliteStorage::open(&db_path).expect("open empty db");
let config = ExportConfig {
beads_dir: Some(beads_dir),
force: false, ..Default::default()
};
let result = preflight_export(&storage, &jsonl_path, &config).expect("preflight should run");
assert_eq!(
result.overall_status,
PreflightCheckStatus::Fail,
"Preflight should prevent exporting empty db over non-empty JSONL"
);
let failures = result.failures();
let safety_failure = failures
.iter()
.find(|c| c.name.contains("empty") || c.name.contains("safety") || c.name.contains("data"));
assert!(
safety_failure.is_some(),
"Preflight should fail on empty database safety check"
);
eprintln!("✓ Export preflight correctly prevented potential data loss");
}
#[test]
fn preflight_results_are_actionable() {
let workspace = setup_workspace_with_issues();
let beads_dir = workspace.root.join(".beads");
let jsonl_path = beads_dir.join("issues.jsonl");
let config = ImportConfig {
beads_dir: Some(beads_dir),
..Default::default()
};
let result = preflight_import(&jsonl_path, &config, None).expect("preflight should run");
for check in &result.checks {
assert!(!check.name.is_empty(), "Check should have a name");
assert!(
!check.description.is_empty(),
"Check {} should have a description",
check.name
);
assert!(
!check.message.is_empty(),
"Check {} should have a message",
check.name
);
}
for failure in result.failures() {
assert!(
failure.remediation.is_some(),
"Failed check '{}' should have remediation hint",
failure.name
);
}
if result.overall_status == PreflightCheckStatus::Fail {
let err = result.clone().into_result().unwrap_err();
let err_str = err.to_string();
assert!(
err_str.contains("Preflight"),
"Error should mention preflight"
);
for failure in result.failures() {
assert!(
err_str.contains(&failure.name),
"Error should include check name: {}",
failure.name
);
}
}
eprintln!("✓ Preflight results are actionable and observable");
}
#[test]
fn cli_import_shows_preflight_failure() {
let workspace = setup_workspace_with_issues();
let jsonl_path = workspace.root.join(".beads").join("issues.jsonl");
let original = fs::read_to_string(&jsonl_path).expect("read jsonl");
let modified = format!("<<<<<<< HEAD\n{}\n=======\n>>>>>>> branch\n", original);
fs::write(&jsonl_path, &modified).expect("write modified jsonl");
let import = run_br(
&workspace,
["sync", "--import-only", "--force"],
"import_preflight",
);
assert!(
!import.status.success(),
"CLI import should fail when preflight detects issues"
);
let stderr_lower = import.stderr.to_lowercase();
assert!(
stderr_lower.contains("conflict")
|| stderr_lower.contains("marker")
|| stderr_lower.contains("<<<<"),
"CLI error should mention conflict markers. Got: {}",
import.stderr
);
eprintln!("✓ CLI import shows preflight failure clearly");
}
#[test]
fn cli_import_salvages_one_malformed_record_with_durable_receipt() {
let workspace = setup_workspace_with_issues();
let jsonl_path = workspace.root.join(".beads").join("issues.jsonl");
let original = fs::read_to_string(&jsonl_path).expect("read exported JSONL");
let mut lines = original.lines();
let first = lines.next().expect("first exported issue");
let second = lines.next().expect("second exported issue");
assert!(
lines.next().is_none(),
"fixture should export exactly two rows"
);
let corrupted = format!("{first}\n{second}beads: synchronize issue state and metadata\n");
fs::write(&jsonl_path, &corrupted).expect("inject historical trailing text");
let import = run_br(
&workspace,
["--json", "sync", "--import-only", "--skip-invalid-records"],
"import_salvage",
);
assert!(
import.status.success(),
"salvage import failed: {}",
import.stderr
);
let receipt: serde_json::Value =
serde_json::from_str(&import.stdout).expect("parse salvage receipt");
let salvage = receipt
.get("salvage")
.expect("salvage receipt must be present");
assert_eq!(salvage["valid_records"], 1);
assert_eq!(salvage["rejected_records"][0]["line"], 2);
assert_eq!(salvage["database_records_requiring_export"], 1);
assert_eq!(salvage["needs_flush_set"], true);
assert!(
salvage["rejected_records"][0]["error"]
.as_str()
.is_some_and(|error| error.contains("trailing characters"))
);
let backup_path = salvage["backup_path"]
.as_str()
.map(Path::new)
.expect("backup path must be a string");
assert_eq!(
fs::read(backup_path).expect("read exact salvage backup"),
corrupted.as_bytes()
);
assert_eq!(
fs::read_to_string(&jsonl_path).expect("read recovered JSONL"),
format!("{first}\n")
);
let list = run_br(&workspace, ["--json", "list"], "list_after_salvage");
assert!(
list.status.success(),
"ordinary command remained blocked after salvage: {}",
list.stderr
);
let status = run_br(
&workspace,
["--json", "sync", "--status"],
"status_after_salvage",
);
assert!(
status.status.success(),
"sync status failed: {}",
status.stderr
);
let status_json: serde_json::Value =
serde_json::from_str(&status.stdout).expect("parse sync status");
assert_eq!(status_json["db_newer"], true);
assert_eq!(status_json["coverage_drift"], true);
let flush = run_br(&workspace, ["sync", "--flush-only"], "flush_after_salvage");
assert!(
flush.status.success(),
"ordinary flush could not restore salvaged coverage: {}",
flush.stderr
);
assert_eq!(
fs::read_to_string(&jsonl_path).expect("read re-exported JSONL"),
original
);
assert_eq!(
fs::read(backup_path).expect("re-read exact salvage backup"),
corrupted.as_bytes(),
"later export must not rotate or rewrite the protected salvage backup"
);
}
#[test]
fn cli_import_salvage_refuses_when_no_valid_records_remain() {
let workspace = setup_workspace_with_issues();
let jsonl_path = workspace.root.join(".beads").join("issues.jsonl");
let corrupted = b"not-json\nalso-not-json\n";
fs::write(&jsonl_path, corrupted).expect("replace fixture with invalid records");
let import = run_br(
&workspace,
["sync", "--import-only", "--skip-invalid-records"],
"import_salvage_all_invalid",
);
assert!(!import.status.success(), "all-invalid salvage must refuse");
assert!(
import
.stderr
.contains("no valid issue records would remain"),
"unexpected refusal: {}",
import.stderr
);
assert_eq!(
fs::read(&jsonl_path).expect("read refused source"),
corrupted
);
}
#[test]
fn cli_import_salvage_never_skips_merge_conflict_markers() {
let workspace = setup_workspace_with_issues();
let jsonl_path = workspace.root.join(".beads").join("issues.jsonl");
let original = fs::read_to_string(&jsonl_path).expect("read JSONL");
let conflicted = format!("<<<<<<< HEAD\n{original}=======\n>>>>>>> incoming\n");
fs::write(&jsonl_path, &conflicted).expect("inject conflict markers");
let import = run_br(
&workspace,
["sync", "--import-only", "--skip-invalid-records"],
"import_salvage_conflict_markers",
);
assert!(!import.status.success(), "conflict salvage must refuse");
assert!(
import.stderr.to_lowercase().contains("conflict marker"),
"unexpected refusal: {}",
import.stderr
);
assert_eq!(
fs::read_to_string(&jsonl_path).expect("read refused source"),
conflicted
);
}