The test suite for weaveback-tangle lives in src/tests/ and is compiled only under #[cfg(test)]. It is structured as five modules:

  • common — shared TestSetup helper and string constants for test fixtures.

  • utils — low-level helper functions (create_test_writer, write_file).

  • basic — core chunk parsing, expansion, indentation, and delimiter tests.

  • advanced@replace, @reversed, error handling, and recursion tests.

  • safe_writerSafeFileWriter behaviour: write flow, baselines, formatter hooks, modification detection, and path security.

Note

The test fixture strings in common.rs and advanced.rs contain chunk-like syntax (<<…​>>=, # @) because they test the tangle parser itself. The literate adoc files for weaveback-tangle use <[ ]> delimiters with //-only comment markers to avoid parsing conflicts with those fixtures.

Test infrastructure

TestSetup::new(comment_markers) creates a temporary directory, puts a gen/ subdirectory in it, and wires up a Clip with << / >> delimiters and @ chunk-end — the classic noweb defaults that the tests exercise.

create_test_writer() and write_file() provide lower-level access to SafeFileWriter for the safe-writer tests.

// <[@file weaveback-tangle/src/tests/mod.rs]>=
mod advanced;
mod basic;
mod common;
mod safe_writer;
mod utils;

pub(crate) use common::*;
pub(crate) use utils::*;
// @@
// <[@file weaveback-tangle/src/tests/common.rs]>=
// src/tests/common.rs
use crate::*;
use std::fs;
use tempfile::TempDir;

pub(crate) struct TestSetup {
    pub _temp_dir: TempDir,
    pub clip: Clip,
}

impl TestSetup {
    pub fn new(comment_markers: &[&str]) -> Self {
        let temp_dir = TempDir::new().unwrap();
        let gen_path = temp_dir.path().join("gen");
        fs::create_dir_all(&gen_path).unwrap();
        let safe_writer = SafeFileWriter::new(gen_path).unwrap();

        let comment_markers = comment_markers
            .iter()
            .map(|s| s.to_string())
            .collect::<Vec<_>>();

        let clip = Clip::new(safe_writer, "<<", ">>", "@", &comment_markers);

        TestSetup {
            _temp_dir: temp_dir,
            clip,
        }
    }
}

pub(crate) const BASIC_CHUNK: &str = r#"
# <<test>>=
Hello
# @
"#;

pub(crate) const TWO_CHUNKS: &str = r#"
# <<chunk1>>=
First chunk
# @
# <<chunk2>>=
Second chunk
# @
"#;

pub(crate) const NESTED_CHUNKS: &str = r#"
# <<outer>>=
Before
# <<inner>>
After
# @
# <<inner>>=
Nested content
# @
"#;

pub(crate) const INDENTED_CHUNK: &str = r#"
# <<main>>=
    # <<indented>>
# @
# <<indented>>=
some code
# @
"#;

pub(crate) const PYTHON_CODE: &str = r#"
# <<code>>=
def example():
    # <<body>>
# @
# <<body>>=
print('hello')
# @
"#;

pub(crate) const MULTI_COMMENT_CHUNKS: &str = r#"
# <<python_chunk>>=
def hello():
    print("Hello")
# @

// <<rust_chunk>>=
fn main() {
    println!("Hello");
}
// @
"#;

pub(crate) const FILE_CHUNKS: &str = r#"
# <<@file output.txt>>=
content
# @
# <<other>>=
other content
# @
"#;

pub(crate) const SEQUENTIAL_CHUNKS: &str = r#"
# <<main>>=
# <<part1>>
# <<part2>>
# @
# <<part1>>=
First part
# @
# <<part2>>=
Second part
# @
"#;

pub(crate) const EMPTY_CHUNK: &str = r#"
# <<empty>>=
# @
"#;
// @@
// <[@file weaveback-tangle/src/tests/utils.rs]>=
// src/tests/utils.rs
use crate::safe_writer::SafeWriterConfig;
use crate::{WeavebackError, SafeFileWriter};
use std::{fs, io::Write, path::PathBuf};
use tempfile::TempDir;

pub(crate) fn create_test_writer() -> (TempDir, SafeFileWriter) {
    let temp = TempDir::new().unwrap();
    let writer = SafeFileWriter::with_config(
        temp.path().join("gen"),
        SafeWriterConfig::default(),
    ).unwrap();
    (temp, writer)
}

pub(crate) fn write_file(
    writer: &mut SafeFileWriter,
    path: &PathBuf,
    content: &str,
) -> Result<(), WeavebackError> {
    let private_path = writer.before_write(path)?;
    {
        let mut file = fs::File::create(&private_path)?;
        write!(file, "{}", content)?;
    }
    writer.after_write(path)?;
    Ok(())
}
// @@
// <[@file weaveback-tangle/src/tests/basic.rs]>=
// src/tests/basic.rs
use super::*;
use crate::ChunkError;

#[test]
fn test_basic_chunk() {
    let mut setup = TestSetup::new(&["#"]);
    setup.clip.read(BASIC_CHUNK, "test_basic.nw");

    assert!(setup.clip.has_chunk("test"));
    assert_eq!(
        setup.clip.get_chunk_content("test").unwrap(),
        vec!["Hello\n"]
    );
}

#[test]
fn test_multiple_chunks() {
    let mut setup = TestSetup::new(&["#"]);
    setup.clip.read(TWO_CHUNKS, "test_multiple.nw");

    assert!(setup.clip.has_chunk("chunk1"));
    assert!(setup.clip.has_chunk("chunk2"));
    assert_eq!(
        setup.clip.get_chunk_content("chunk1").unwrap(),
        vec!["First chunk\n"]
    );
    assert_eq!(
        setup.clip.get_chunk_content("chunk2").unwrap(),
        vec!["Second chunk\n"]
    );
}

#[test]
fn test_nested_chunk_expansion() -> Result<(), ChunkError> {
    let mut setup = TestSetup::new(&["#"]);
    setup.clip.read(NESTED_CHUNKS, "test_nested.nw");

    let expanded = setup.clip.expand("outer", "")?;
    let expected = vec!["Before\n", "Nested content\n", "After\n"];
    assert_eq!(expanded, expected, "Nested chunks should expand correctly");
    Ok(())
}

#[test]
fn test_indentation_preservation() -> Result<(), ChunkError> {
    let mut setup = TestSetup::new(&["#"]);
    setup.clip.read(INDENTED_CHUNK, "test_indent.nw");

    let expanded = setup.clip.expand("main", "")?;
    assert_eq!(
        expanded,
        vec!["    some code\n"],
        "Indentation should be preserved"
    );
    Ok(())
}

#[test]
fn test_complex_indentation() -> Result<(), ChunkError> {
    let mut setup = TestSetup::new(&["#"]);
    setup.clip.read(PYTHON_CODE, "test_python.nw");

    let expanded = setup.clip.expand("code", "")?;
    let expected = vec!["def example():\n", "    print('hello')\n"];
    assert_eq!(expanded, expected);

    let expanded_indented = setup.clip.expand("code", "  ")?;
    let expected_indented = vec!["  def example():\n", "      print('hello')\n"];
    assert_eq!(expanded_indented, expected_indented);
    Ok(())
}

#[test]
fn test_multi_comment_styles() {
    let mut setup = TestSetup::new(&["#", "//"]);
    setup.clip.read(MULTI_COMMENT_CHUNKS, "test_comments.nw");

    assert!(setup.clip.has_chunk("python_chunk"));
    assert!(setup.clip.has_chunk("rust_chunk"));

    let python_content = setup.clip.get_chunk_content("python_chunk").unwrap();
    assert!(python_content.join("").contains("print(\"Hello\")"));

    let rust_content = setup.clip.get_chunk_content("rust_chunk").unwrap();
    assert!(rust_content.join("").contains("println!(\"Hello\")"));
}

#[test]
fn test_sequential_chunks() -> Result<(), ChunkError> {
    let mut setup = TestSetup::new(&["#"]);
    setup.clip.read(SEQUENTIAL_CHUNKS, "test_sequential.nw");

    let expanded = setup.clip.expand("main", "")?;
    assert_eq!(expanded, vec!["First part\n", "Second part\n"]);
    Ok(())
}

#[test]
fn test_empty_chunk() {
    let mut setup = TestSetup::new(&["#"]);
    setup.clip.read(EMPTY_CHUNK, "test_empty.nw");

    assert!(setup.clip.has_chunk("empty"));
    assert!(
        setup.clip.get_chunk_content("empty").unwrap().is_empty(),
        "empty chunk should have no content"
    );
}
// @@
// <[@file weaveback-tangle/src/tests/advanced.rs]>=
// src/tests/advanced.rs

use super::*;
use crate::{Clip, SafeFileWriter, WeavebackError};
use crate::ChunkError;
use std::fs;

/// Bug fix: duplicate @file chunk without @replace used to silently discard
/// both definitions. Now it reports an error and keeps the first definition.
#[test]
fn test_duplicate_file_chunk_keeps_first_definition() {
    let mut setup = TestSetup::new(&["#"]);
    setup.clip.read(
        r#"
# <<@file out.txt>>=
first definition
# @

# <<@file out.txt>>=
second definition
# @
"#,
        "duplicate.nw",
    );

    // The first definition must survive.
    assert!(
        setup.clip.has_chunk("@file out.txt"),
        "first definition should be kept"
    );
    let content = setup.clip.get_chunk_content("@file out.txt").unwrap();
    assert!(
        content.iter().any(|l| l.contains("first definition")),
        "first definition content should be preserved, got: {:?}",
        content
    );
    assert!(
        !content.iter().any(|l| l.contains("second definition")),
        "second definition should be rejected"
    );
}

#[test]
fn test_file_chunk_detection() {
    let mut setup = TestSetup::new(&["#"]);
    setup.clip.read(FILE_CHUNKS, "test_files.nw");

    let file_chunks = setup.clip.get_file_chunks();
    assert_eq!(file_chunks.len(), 1);
    assert!(file_chunks.contains(&"@file output.txt".to_string()));
}

#[test]
fn test_undefined_chunk_is_error() {
    let mut setup = TestSetup::new(&["#"]);
    setup.clip.read(
        r#"
# <<main>>=
# <<nonexistent>>
# @
"#,
        "undefined.nw",
    );
    setup.clip.set_strict_undefined(true);

    let result = setup.clip.expand("main", "");
    assert!(result.is_err(), "referencing an undefined chunk must be an error");
    let err = result.unwrap_err();
    assert!(
        matches!(err, WeavebackError::Chunk(ChunkError::UndefinedChunk { ref chunk, .. }) if chunk == "nonexistent"),
        "expected UndefinedChunk error, got: {err}",
    );
}

#[test]
fn test_undefined_chunk_is_empty_when_not_strict() {
    let mut setup = TestSetup::new(&["#"]);
    setup.clip.read(
        r#"
# <<main>>=
line before
# <<optional>>
line after
# @
"#,
        "undefined.nw",
    );
    // Default is permissive; no set_strict_undefined call needed.
    let result = setup.clip.expand("main", "");
    assert!(result.is_ok(), "undefined chunk should expand to empty when not strict");
    let lines = result.unwrap();
    assert_eq!(lines, vec!["line before\n", "line after\n"]);
}

#[test]
fn test_recursive_chunk_error() {
    let mut setup = TestSetup::new(&["#"]);
    setup.clip.read(
        r#"
# <<recursive>>=
Start
# <<recursive>>
End
# @
"#,
        "recursive.nw",
    );

    let result = setup.clip.expand("recursive", "");
    match result {
        Err(WeavebackError::Chunk(ChunkError::RecursiveReference {
            chunk,
            cycle,
            file_name,
            location,
        })) => {
            assert_eq!(chunk, "recursive");
            assert_eq!(file_name, "recursive.nw");
            assert_eq!(location.line, 2);
            assert_eq!(cycle, vec!["recursive", "recursive"]);
        }
        _ => panic!("Expected RecursiveReference error"),
    }
}

#[test]
fn test_mutual_recursion_error() {
    let mut setup = TestSetup::new(&["#"]);
    setup.clip.read(
        r#"
# <<chunk-a>>=
Start A
# <<chunk-b>>
End A
# @

# <<chunk-b>>=
Middle B
# <<chunk-a>>
End B
# @
"#,
        "mutual_recursion.nw",
    );

    let result = setup.clip.expand("chunk-a", "");
    match result {
        Err(WeavebackError::Chunk(ChunkError::RecursiveReference {
            chunk,
            cycle,
            file_name,
            location,
        })) => {
            assert_eq!(chunk, "chunk-a");
            assert_eq!(file_name, "mutual_recursion.nw");
            assert_eq!(location.line, 8);
            assert_eq!(cycle, vec!["chunk-a", "chunk-b", "chunk-a"]);
        }
        _ => panic!("Expected RecursiveReference error"),
    }
}

#[test]
fn test_max_recursion_depth() {
    let mut setup = TestSetup::new(&["#"]);

    let mut content = String::from(
        r#"
# <<a-000>>=
# <<a-001>>
# @"#,
    );

    let chain_length = 150; // More than MAX_DEPTH = 100
    for i in 1..chain_length {
        content.push_str(&format!(
            r#"
# <<a-{:03}>>=
# <<a-{:03}>>
# @"#,
            i,     // a-001, a-002, etc.
            i + 1  // a-002, a-003, etc.
        ));
    }

    setup.clip.read(&content, "max_recursion.nw");
    let result = setup.clip.expand("a-000", "");

    // We just match the variant here (less strict). Alternatively, pattern match with { chunk, file_name, location }
    assert!(
        matches!(
            result,
            Err(WeavebackError::Chunk(ChunkError::RecursionLimit { .. }))
        ),
        "Expected RecursionLimit error"
    );
}

#[test]
fn test_error_messages_format() {
    let mut setup = TestSetup::new(&["#"]);
    setup.clip.read(
        r#"
# <<a>>=
# <<a>>
# @
"#,
        "errors.nw",
    );

    let err = setup.clip.expand("a", "").unwrap_err();
    let error_msg = err.to_string();

    assert!(error_msg.contains("Chunk error: errors.nw line 2:"));
    assert!(error_msg.contains("recursive reference detected in chunk 'a'"));
    assert!(error_msg.contains("cycle: a -> a"));
}

#[test]
fn test_dangerous_comment_markers() {
    let markers = &[
        "#",         // normal case
        r".*",       // regex wildcard
        r"[a-z]+",   // regex character class
        r"\d+",      // regex digit
        "<<",        // same as delimiter
        ">>",        // same as delimiter
        "(comment)", // regex group
    ];

    let content = r#"
#<<test1>>=
Content1
@
.*<<test2>>=
Content2
@
[a-z]+<<test3>>=
Content3
@
(comment)<<test4>>=
Content4
@
"#;

    let mut setup = TestSetup::new(markers);
    setup.clip.read(content, "regex_test.nw");

    assert!(setup.clip.has_chunk("test1"), "Basic marker # failed");
    assert!(setup.clip.has_chunk("test2"), "Wildcard marker .* failed");
    assert!(
        setup.clip.has_chunk("test3"),
        "Character class marker [a-z]+ failed"
    );
    assert!(
        setup.clip.has_chunk("test4"),
        "Group marker (comment) failed"
    );

    assert_eq!(
        setup.clip.get_chunk_content("test1").unwrap(),
        vec!["Content1\n"]
    );
}

// ── @replace ─────────────────────────────────────────────────────────────────

/// `@replace` on a regular chunk discards all prior definitions and installs
/// only the new one.
#[test]
fn test_replace_normal_chunk() {
    let mut setup = TestSetup::new(&["#"]);
    setup.clip.read(
        r#"
# <<greet>>=
Hello
# @

# <<@replace greet>>=
Hi
# @
"#,
        "replace_normal.nw",
    );

    let content = setup.clip.get_chunk_content("greet").unwrap();
    assert_eq!(
        content,
        vec!["Hi\n"],
        "only the @replace definition should survive"
    );
}

/// `@replace` on a file chunk replaces the earlier definition.
#[test]
fn test_replace_file_chunk() {
    let mut setup = TestSetup::new(&["#"]);
    setup.clip.read(
        r#"
# <<@file out.txt>>=
old content
# @

# <<@replace @file out.txt>>=
new content
# @
"#,
        "replace_file.nw",
    );

    let content = setup.clip.get_chunk_content("@file out.txt").unwrap();
    assert_eq!(
        content,
        vec!["new content\n"],
        "@replace should replace file chunk"
    );
}

/// Without `@replace`, a second file-chunk definition is an error and the first
/// definition is kept (regression guard).
#[test]
fn test_no_replace_file_chunk_keeps_first() {
    let mut setup = TestSetup::new(&["#"]);
    setup.clip.read(
        r#"
# <<@file out.txt>>=
first
# @

# <<@file out.txt>>=
second
# @
"#,
        "no_replace_file.nw",
    );

    let content = setup.clip.get_chunk_content("@file out.txt").unwrap();
    assert!(
        content.iter().any(|l| l.contains("first")),
        "first definition should be kept without @replace"
    );
    assert!(
        !content.iter().any(|l| l.contains("second")),
        "second definition should be rejected without @replace"
    );
}

// ── @reversed ────────────────────────────────────────────────────────────────

/// A regular chunk may accumulate multiple definitions (without @replace).
/// A plain reference expands them in definition order (first → last).
#[test]
fn test_accumulated_chunks_normal_order() {
    let mut setup = TestSetup::new(&["#"]);
    setup.clip.read(
        r#"
# <<items>>=
alpha
# @

# <<items>>=
beta
# @

# <<items>>=
gamma
# @

# <<list>>=
# <<items>>
# @
"#,
        "normal_order.nw",
    );

    let expanded = setup.clip.expand("list", "").unwrap();
    assert_eq!(expanded, vec!["alpha\n", "beta\n", "gamma\n"]);
}

/// `@reversed` on a reference expands the chunk's accumulated definitions in
/// reverse order (last-defined first).
#[test]
fn test_reversed_reference() {
    let mut setup = TestSetup::new(&["#"]);
    setup.clip.read(
        r#"
# <<items>>=
alpha
# @

# <<items>>=
beta
# @

# <<items>>=
gamma
# @

# <<list>>=
# <<@reversed items>>
# @
"#,
        "reversed.nw",
    );

    let expanded = setup.clip.expand("list", "").unwrap();
    assert_eq!(expanded, vec!["gamma\n", "beta\n", "alpha\n"]);
}

/// `~` in an `@file` path expands to `$HOME` when `--allow-home` is set.
#[test]
fn test_tilde_expansion_in_file_chunk() {
    let fake_home = tempfile::TempDir::new().unwrap();
    // Override HOME for this test
    // TODO: Audit that the environment access only happens in single-threaded code.
    unsafe { std::env::set_var("HOME", fake_home.path()) };

    // Tilde expansion writes outside gen/ and requires allow_home: true.
    let temp_dir = tempfile::TempDir::new().unwrap();
    let gen_path = temp_dir.path().join("gen");
    fs::create_dir_all(&gen_path).unwrap();
    let safe_writer = SafeFileWriter::with_config(
        gen_path,
        crate::safe_writer::SafeWriterConfig {
            allow_home: true,
            ..crate::safe_writer::SafeWriterConfig::default()
        },
    ).unwrap();
    let mut clip = Clip::new(safe_writer, "<<", ">>", "@", &["#".to_string()]);

    clip.read(
        "# <<@file ~/tilde_test.txt>>=\nhello tilde\n# @\n",
        "tilde.nw",
    );
    clip.write_files().unwrap();

    let expected = fake_home.path().join("tilde_test.txt");
    assert!(
        expected.exists(),
        "file should be written to expanded ~ path"
    );
    let content = fs::read_to_string(&expected).unwrap();
    assert_eq!(content, "hello tilde\n");
}

/// Without `--allow-home`, `@file ~/…` is refused rather than silently
/// escaping the gen/ sandbox.
#[test]
fn test_tilde_expansion_blocked_without_allow_home() {
    let mut setup = TestSetup::new(&["#"]);
    setup.clip.read(
        "# <<@file ~/should_not_exist.txt>>=\ndata\n# @\n",
        "tilde_blocked.nw",
    );
    let result = setup.clip.write_files();
    assert!(
        matches!(
            result,
            Err(WeavebackError::SafeWriter(
                crate::safe_writer::SafeWriterError::SecurityViolation(_)
            ))
        ),
        "expected SecurityViolation without --allow-home, got: {:?}",
        result
    );
}
// @@
// <[@file weaveback-tangle/src/tests/safe_writer.rs]>=
// src/tests/safe_writer.rs
use super::*;
use crate::WeavebackError;
use crate::SafeWriterError;
use crate::safe_writer::{SafeFileWriter, SafeWriterConfig};
use std::{collections::HashMap, fs, io::Write, path::PathBuf, thread, time::Duration};

#[test]
fn test_basic_file_writing() -> Result<(), WeavebackError> {
    let (_temp, mut writer) = create_test_writer();
    let test_file = PathBuf::from("test.txt");
    let test_content = "Hello, World!";

    write_file(&mut writer, &test_file, test_content)?;

    let final_path = writer.get_gen_base().join(&test_file);
    let content = fs::read_to_string(&final_path)?;
    assert_eq!(content, test_content);
    Ok(())
}

#[test]
fn test_multiple_file_generation() -> Result<(), WeavebackError> {
    let (_temp, mut writer) = create_test_writer();
    let test_file1 = PathBuf::from("file1.txt");
    let test_file2 = PathBuf::from("file2.txt");

    write_file(&mut writer, &test_file1, "Content 1")?;
    write_file(&mut writer, &test_file2, "Content 2")?;

    let content1 = fs::read_to_string(writer.get_gen_base().join(&test_file1))?;
    let content2 = fs::read_to_string(writer.get_gen_base().join(&test_file2))?;

    assert_eq!(content1.trim(), "Content 1");
    assert_eq!(content2.trim(), "Content 2");
    Ok(())
}

#[test]
fn test_unmodified_file_update() -> Result<(), WeavebackError> {
    let (_temp, mut writer) = create_test_writer();
    let test_file = PathBuf::from("test.txt");

    write_file(&mut writer, &test_file, "Initial content")?;
    write_file(&mut writer, &test_file, "New content")?;

    let content = fs::read_to_string(writer.get_gen_base().join(&test_file))?;
    assert_eq!(content, "New content", "New content should be written");
    Ok(())
}

#[test]
fn test_backup_creation() -> Result<(), WeavebackError> {
    let (_temp, mut writer) = create_test_writer();
    let test_file = PathBuf::from("test.txt");
    let content = "Test content";

    write_file(&mut writer, &test_file, content)?;

    let baseline = writer.get_baseline_for_test("test.txt");
    assert!(baseline.is_some(), "Baseline should be stored in db");
    assert_eq!(
        baseline.as_deref().unwrap(),
        content.as_bytes(),
        "Baseline content should match written content"
    );
    Ok(())
}

#[test]
fn test_nested_directory_creation() -> Result<(), WeavebackError> {
    let (_temp, mut writer) = create_test_writer();
    let nested_path = PathBuf::from("dir1/dir2/test.txt");

    write_file(&mut writer, &nested_path, "Nested content")?;

    let gen_dir = writer.get_gen_base().join("dir1").join("dir2");
    assert!(gen_dir.exists(), "Generated directory structure should exist");

    let baseline = writer.get_baseline_for_test("dir1/dir2/test.txt");
    assert!(
        baseline.is_some(),
        "Baseline for nested file should be stored in db"
    );
    Ok(())
}

#[test]
fn test_modification_detection() -> Result<(), WeavebackError> {
    let (_temp, mut writer) = create_test_writer();
    let test_file = PathBuf::from("test.txt");
    let modified_content = "Modified content";

    write_file(&mut writer, &test_file, "Initial content")?;

    thread::sleep(Duration::from_millis(10));
    let final_path = writer.get_gen_base().join(&test_file);
    {
        let mut file = fs::File::create(&final_path)?;
        write!(file, "{}", modified_content)?;
    }

    let result = write_file(&mut writer, &test_file, "New content");
    match result {
        Err(WeavebackError::SafeWriter(SafeWriterError::ModifiedExternally(_))) => {
            let content = fs::read_to_string(&final_path)?;
            assert_eq!(
                content, modified_content,
                "Modified content should be preserved"
            );
            Ok(())
        }
        Ok(_) => panic!("Expected ModifiedExternally error"),
        Err(e) => panic!("Unexpected error: {}", e),
    }
}

#[test]
fn test_concurrent_modifications() -> Result<(), WeavebackError> {
    let (_temp, mut writer) = create_test_writer();
    let test_file = PathBuf::from("test.txt");
    let modified_content_2 = "Modified 2";

    write_file(&mut writer, &test_file, "Initial")?;

    let final_path = writer.get_gen_base().join(&test_file);
    thread::sleep(Duration::from_millis(10));
    {
        let mut file = fs::File::create(&final_path)?;
        write!(file, "Modified 1")?;
    }
    thread::sleep(Duration::from_millis(10));
    {
        let mut file = fs::File::create(&final_path)?;
        write!(file, "{}", modified_content_2)?;
    }

    let result = write_file(&mut writer, &test_file, "New content");
    match result {
        Err(WeavebackError::SafeWriter(SafeWriterError::ModifiedExternally(_))) => {
            let content = fs::read_to_string(&final_path)?;
            assert_eq!(
                content, modified_content_2,
                "Latest modification should be preserved"
            );
            Ok(())
        }
        Ok(_) => panic!("Expected ModifiedExternally error"),
        Err(e) => panic!("Unexpected error: {}", e),
    }
}

#[test]
fn test_baseline_always_written() -> Result<(), WeavebackError> {
    let (_temp, mut writer) = create_test_writer();
    let test_file = PathBuf::from("test.txt");

    write_file(&mut writer, &test_file, "Test content")?;

    assert!(
        writer.get_baseline_for_test("test.txt").is_some(),
        "Baseline must always be stored in db for modification detection"
    );
    Ok(())
}

#[test]
fn test_validate_filename_relative_path() -> Result<(), WeavebackError> {
    let (_temp, mut writer) = create_test_writer();
    let test_file = PathBuf::from("simple.txt");
    write_file(&mut writer, &test_file, "Allowed")?;
    let final_path = writer.get_gen_base().join(&test_file);
    let content = fs::read_to_string(&final_path)?;
    assert_eq!(content, "Allowed");
    Ok(())
}


#[test]
fn test_path_safety() {
    let (_temp, mut writer) = create_test_writer();

    let test_cases = [
        (
            PathBuf::from("../outside.txt"),
            "Path traversal detected (..)",
        ),
        (
            PathBuf::from("/absolute/path.txt"),
            "Absolute paths are not allowed",
        ),
        (
            PathBuf::from("C:/windows/path.txt"),
            "Windows-style absolute paths are not allowed",
        ),
        (
            PathBuf::from("C:test.txt"),
            "Windows-style absolute paths are not allowed",
        ),
    ];

    for (path, expected_msg) in test_cases {
        let result = write_file(&mut writer, &path, "Should fail");
        match result {
            Err(WeavebackError::SafeWriter(SafeWriterError::SecurityViolation(msg))) => {
                assert!(
                    msg.contains(expected_msg),
                    "Expected message '{}' for path {}",
                    expected_msg,
                    path.display()
                );
            }
            _ => panic!("Expected SecurityViolation for path: {}", path.display()),
        }
    }
}

#[test]
fn test_formatter_is_applied() -> Result<(), WeavebackError> {
    let temp = tempfile::TempDir::new().unwrap();
    let mut formatters = HashMap::new();
    // Shell script that replaces the file content with "FORMATTED\n"
    formatters.insert(
        "txt".to_string(),
        "sh -c echo FORMATTED > \"$1\" && echo FORMATTED > \"$1\"".to_string(),
    );
    // Use a simpler approach: a script file
    let script_path = temp.path().join("fmt.sh");
    fs::write(&script_path, "#!/bin/sh\necho FORMATTED > \"$1\"\n").unwrap();
    std::process::Command::new("chmod")
        .arg("+x")
        .arg(&script_path)
        .status()
        .unwrap();

    let mut formatters2 = HashMap::new();
    formatters2.insert("txt".to_string(), script_path.to_string_lossy().to_string());

    let config = SafeWriterConfig {
        formatters: formatters2,
        ..SafeWriterConfig::default()
    };
    let mut writer =
        SafeFileWriter::with_config(temp.path().join("gen"), config)?;

    let test_file = PathBuf::from("test.txt");
    write_file(&mut writer, &test_file, "original content")?;

    let output = fs::read_to_string(writer.get_gen_base().join(&test_file))?;
    assert!(
        output.contains("FORMATTED"),
        "Formatter should have replaced content, got: {:?}",
        output
    );
    Ok(())
}

#[test]
fn test_formatter_error_propagates() -> Result<(), WeavebackError> {
    let temp = tempfile::TempDir::new().unwrap();
    let mut formatters = HashMap::new();
    formatters.insert("txt".to_string(), "nonexistent-formatter-xyz".to_string());

    let config = SafeWriterConfig {
        formatters,
        ..SafeWriterConfig::default()
    };
    let mut writer =
        SafeFileWriter::with_config(temp.path().join("gen"), config)?;

    let test_file = PathBuf::from("test.txt");
    let result = write_file(&mut writer, &test_file, "some content");
    match result {
        Err(WeavebackError::SafeWriter(SafeWriterError::FormatterError(_))) => Ok(()),
        Ok(_) => panic!("Expected FormatterError but write succeeded"),
        Err(e) => panic!("Unexpected error: {}", e),
    }
}

#[test]
fn test_formatter_prevents_false_positive() -> Result<(), WeavebackError> {
    let temp = tempfile::TempDir::new().unwrap();
    let script_path = temp.path().join("noop.sh");
    // A no-op formatter: copies file to itself (content unchanged)
    fs::write(
        &script_path,
        "#!/bin/sh\ncp \"$1\" \"$1.bak\" && mv \"$1.bak\" \"$1\"\n",
    )
    .unwrap();
    std::process::Command::new("chmod")
        .arg("+x")
        .arg(&script_path)
        .status()
        .unwrap();

    let mut formatters = HashMap::new();
    formatters.insert("txt".to_string(), script_path.to_string_lossy().to_string());

    let config = SafeWriterConfig {
        formatters,
        ..SafeWriterConfig::default()
    };
    let mut writer =
        SafeFileWriter::with_config(temp.path().join("gen"), config)?;

    let test_file = PathBuf::from("test.txt");
    write_file(&mut writer, &test_file, "initial content")?;

    // Simulate formatter running externally on the output (content unchanged)
    let output_path = writer.get_gen_base().join(&test_file);
    let content = fs::read_to_string(&output_path)?;
    fs::write(&output_path, &content)?;

    // Second write should NOT trigger ModifiedExternally (content is the same as baseline)
    let result = write_file(&mut writer, &test_file, "initial content");
    assert!(
        result.is_ok(),
        "Expected success but got: {:?}",
        result.err()
    );
    Ok(())
}
// @@