sdivi-patterns 0.2.47

Pattern fingerprinting and catalog for sdivi-rust
Documentation
//! Fixture-level integration test: `testing` bucket population vs. `scope_exclude`.
//!
//! Verifies the design choice documented in `testing.rs`:
//! > `patterns.scope_exclude` removes files from the pattern catalog only
//! > (files remain in the graph). The `testing` bucket is non-empty only
//! > when test files are in the pattern scope.
//!
//! The milestone spec (M42) required: "Integration: a fixture with tests
//! in-scope vs excluded, asserting the bucket populates/empties accordingly."

use std::path::PathBuf;

use sdivi_config::PatternsConfig;
use sdivi_parsing::feature_record::{FeatureRecord, PatternHint};
use sdivi_patterns::build_catalog;

// ── Helpers ───────────────────────────────────────────────────────────────────

fn call_hint(text: &str, start_row: usize) -> PatternHint {
    PatternHint {
        node_kind: "call_expression".to_string(),
        start_byte: 0,
        end_byte: text.len(),
        start_row,
        start_col: 0,
        text: text.to_string(),
    }
}

fn record(path: &str, language: &str, hints: Vec<PatternHint>) -> FeatureRecord {
    FeatureRecord {
        path: PathBuf::from(path),
        language: language.to_string(),
        imports: vec![],
        exports: vec![],
        signatures: vec![],
        pattern_hints: hints,
    }
}

fn config_min1(scope_exclude: Vec<String>) -> PatternsConfig {
    PatternsConfig {
        min_pattern_nodes: 1,
        scope_exclude,
        ..PatternsConfig::default()
    }
}

// ── In-scope: bucket is populated ────────────────────────────────────────────

/// Acceptance criterion: `describe('s', fn)` in-scope → `testing` bucket present.
#[test]
fn ts_describe_in_scope_populates_testing_bucket() {
    let records = vec![record(
        "src/app.test.ts",
        "typescript",
        vec![
            call_hint("describe('suite', fn)", 0),
            call_hint("it('does something', fn)", 1),
        ],
    )];

    let catalog = build_catalog(&records, &config_min1(vec![]));

    assert!(
        catalog.entries.contains_key("testing"),
        "testing bucket must be present when describe/it calls are in-scope; \
         got categories: {:?}",
        catalog.entries.keys().collect::<Vec<_>>()
    );
}

/// Acceptance criterion: `expect(x).toBe(1)` in-scope → `testing` bucket present.
#[test]
fn ts_expect_in_scope_populates_testing_bucket() {
    let records = vec![record(
        "src/__tests__/math.test.ts",
        "typescript",
        vec![call_hint("expect(x).toBe(1)", 5)],
    )];

    let catalog = build_catalog(&records, &config_min1(vec![]));

    assert!(
        catalog.entries.contains_key("testing"),
        "testing bucket must be present for expect(x).toBe(1) in-scope; \
         got categories: {:?}",
        catalog.entries.keys().collect::<Vec<_>>()
    );
    let count: u32 = catalog.entries["testing"].values().map(|s| s.count).sum();
    assert!(count >= 1, "testing bucket count must be >= 1, got {count}");
}

/// Go `t.Fatal(err)` in-scope → `testing` bucket present.
#[test]
fn go_t_fatal_in_scope_populates_testing_bucket() {
    let records = vec![record(
        "pkg/auth/auth_test.go",
        "go",
        vec![
            call_hint("t.Run(\"sub\", fn)", 10),
            call_hint("t.Fatal(err)", 15),
        ],
    )];

    let catalog = build_catalog(&records, &config_min1(vec![]));

    assert!(
        catalog.entries.contains_key("testing"),
        "testing bucket must be present for Go t.Run/t.Fatal in-scope; \
         got categories: {:?}",
        catalog.entries.keys().collect::<Vec<_>>()
    );
}

/// Python `self.assertEqual` in-scope → `testing` bucket present.
#[test]
fn python_self_assert_in_scope_populates_testing_bucket() {
    let records = vec![record(
        "tests/test_model.py",
        "python",
        vec![
            call_hint("self.assertEqual(a, b)", 20),
            call_hint("self.assertTrue(x)", 21),
        ],
    )];

    let catalog = build_catalog(&records, &config_min1(vec![]));

    assert!(
        catalog.entries.contains_key("testing"),
        "testing bucket must be present for Python self.assert* in-scope; \
         got categories: {:?}",
        catalog.entries.keys().collect::<Vec<_>>()
    );
}

/// Jest framework helpers in-scope → `testing` bucket present.
#[test]
fn jest_vi_helpers_in_scope_populate_testing_bucket() {
    let records = vec![record(
        "src/mocks/setup.ts",
        "typescript",
        vec![
            call_hint("jest.mock('./module')", 0),
            call_hint("vi.fn()", 1),
        ],
    )];

    let catalog = build_catalog(&records, &config_min1(vec![]));

    assert!(
        catalog.entries.contains_key("testing"),
        "testing bucket must be present for jest.mock / vi.fn in-scope; \
         got categories: {:?}",
        catalog.entries.keys().collect::<Vec<_>>()
    );
}

// ── Excluded: bucket is absent ────────────────────────────────────────────────

/// When the test file is excluded via `scope_exclude`, `testing` bucket must be absent.
#[test]
fn excluded_test_file_yields_empty_testing_bucket() {
    let records = vec![record(
        "src/app.test.ts",
        "typescript",
        vec![
            call_hint("describe('suite', fn)", 0),
            call_hint("expect(x).toBe(1)", 1),
        ],
    )];

    let catalog = build_catalog(&records, &config_min1(vec!["**/*.test.ts".to_string()]));

    assert!(
        !catalog.entries.contains_key("testing"),
        "testing bucket must be absent when test file is excluded; \
         got categories: {:?}",
        catalog.entries.keys().collect::<Vec<_>>()
    );
}

/// Exclude a `__tests__/` directory — `testing` bucket must be absent.
#[test]
fn excluded_tests_dir_yields_empty_testing_bucket() {
    let records = vec![record(
        "src/__tests__/math.test.js",
        "javascript",
        vec![
            call_hint("it('adds', fn)", 0),
            call_hint("expect(1 + 1).toBe(2)", 1),
        ],
    )];

    let catalog = build_catalog(&records, &config_min1(vec!["src/__tests__/**".to_string()]));

    assert!(
        !catalog.entries.contains_key("testing"),
        "testing bucket must be absent when __tests__ dir is excluded; \
         got categories: {:?}",
        catalog.entries.keys().collect::<Vec<_>>()
    );
}

/// Exclude a `*_test.go` pattern — Go `testing` bucket must be absent.
#[test]
fn excluded_go_test_files_yield_empty_testing_bucket() {
    let records = vec![record(
        "pkg/auth/auth_test.go",
        "go",
        vec![call_hint("t.Run(\"case\", fn)", 5)],
    )];

    let catalog = build_catalog(&records, &config_min1(vec!["**/*_test.go".to_string()]));

    assert!(
        !catalog.entries.contains_key("testing"),
        "testing bucket must be absent when *_test.go is excluded; \
         got categories: {:?}",
        catalog.entries.keys().collect::<Vec<_>>()
    );
}

/// Exclude a `tests/` directory — Python `testing` bucket must be absent.
#[test]
fn excluded_python_tests_dir_yields_empty_testing_bucket() {
    let records = vec![record(
        "tests/test_model.py",
        "python",
        vec![call_hint("self.assertEqual(a, b)", 10)],
    )];

    let catalog = build_catalog(&records, &config_min1(vec!["tests/**".to_string()]));

    assert!(
        !catalog.entries.contains_key("testing"),
        "testing bucket must be absent when tests/ dir is excluded; \
         got categories: {:?}",
        catalog.entries.keys().collect::<Vec<_>>()
    );
}

// ── Mixed: in-scope file populates, excluded file does not contribute ─────────

/// When one test file is excluded and another is in-scope, only the in-scope
/// file's patterns appear in the `testing` bucket.
#[test]
fn mixed_in_scope_and_excluded_test_files() {
    let excluded = record(
        "src/vendor/vendor.test.ts",
        "typescript",
        vec![call_hint("describe('vendor', fn)", 0)],
    );
    let in_scope = record(
        "src/app.test.ts",
        "typescript",
        vec![call_hint("describe('app', fn)", 0)],
    );
    let records = vec![excluded, in_scope];

    let catalog = build_catalog(&records, &config_min1(vec!["src/vendor/**".to_string()]));

    // testing bucket is present (in-scope file contributes).
    assert!(
        catalog.entries.contains_key("testing"),
        "testing bucket must be present from in-scope test file; \
         got categories: {:?}",
        catalog.entries.keys().collect::<Vec<_>>()
    );

    // The excluded file's path must not appear in any testing location.
    let excluded_path = PathBuf::from("src/vendor/vendor.test.ts");
    for stats in catalog.entries["testing"].values() {
        for loc in &stats.locations {
            assert_ne!(
                loc.file, excluded_path,
                "excluded file must not appear in testing bucket locations"
            );
        }
    }
}

// ── Records invariant: excluded files remain in the input slice ───────────────

/// `build_catalog` does not mutate the `records` slice — excluded files remain present.
#[test]
fn excluded_file_remains_in_feature_record_slice() {
    let records = vec![record(
        "src/app.test.ts",
        "typescript",
        vec![call_hint("describe('s', fn)", 0)],
    )];

    let _catalog = build_catalog(&records, &config_min1(vec!["**/*.test.ts".to_string()]));

    let still_present = records
        .iter()
        .any(|r| r.path == PathBuf::from("src/app.test.ts"));
    assert!(
        still_present,
        "excluded file must remain in the FeatureRecord slice after build_catalog"
    );
}

// ── Non-test callee in a test file: goes to its own category, not testing ─────

/// A non-test call in a test file resolves to its own category (not testing).
/// Confirms that excluding test files also suppresses their non-test calls.
#[test]
fn non_test_call_in_test_file_is_not_testing() {
    let records = vec![record(
        "src/app.test.ts",
        "typescript",
        vec![
            call_hint("console.log(x)", 0), // → logging, not testing
            call_hint("describe('s', fn)", 1),
        ],
    )];

    let catalog = build_catalog(&records, &config_min1(vec![]));

    // logging from console.log is present (or some other non-testing category)
    // The key assertion: logging must not be in the testing bucket.
    if let Some(testing_entries) = catalog.entries.get("testing") {
        for stats in testing_entries.values() {
            // All instances in the testing bucket must have come from testing callees.
            // There is no direct way to check the callee text post-catalog, but we can
            // assert that logging is separately bucketed (not merged into testing).
            let _ = stats; // structural check: each fingerprint is its own entry
        }
    }
    // The real assertion: console.log goes to logging, not testing.
    assert!(
        catalog.entries.contains_key("logging"),
        "console.log must resolve to logging, not testing"
    );
    assert!(
        catalog.entries.contains_key("testing"),
        "describe('s', fn) must still resolve to testing"
    );
}