koala-drift 1.0.4

Wiki ↔ code drift detector.
Documentation
//! Fuzzy-match suggestions for typo'd acceptance paths.
//!
//! When an acceptance line `→ <path>` references a file that doesn't
//! exist but a sibling does (within Levenshtein distance 3), the
//! finding's `fix_hint` should include `did you mean ...?`.

use koala_core::invariant::Context;
use koala_drift::checks::feature_acceptance::FeatureAcceptanceTestRef;
use koala_drift::Check;
use std::fs;
use tempfile::TempDir;

fn write(dir: &std::path::Path, rel: &str, body: &str) {
    let p = dir.join(rel);
    fs::create_dir_all(p.parent().unwrap()).unwrap();
    fs::write(p, body).unwrap();
}

const FEATURE_HEAD: &str =
    "---\nid: x\nstatus: in-progress\n---\n# X\n\n## Acceptance criteria(机械可验证)\n\n";

#[test]
fn fuzzy_suggestion_offered() {
    let tmp = TempDir::new().unwrap();
    // Real test sibling that the typo will land near.
    write(
        tmp.path(),
        "crates/sample/tests/widget_render.rs",
        "// real test\n",
    );
    // Acceptance line points at `widget_rendr.rs` (missing the second `e`).
    let body = format!(
        "{FEATURE_HEAD}- [ ] renders the widget\n  → crates/sample/tests/widget_rendr.rs#happy_path\n"
    );
    write(tmp.path(), "wiki/features/x.md", &body);

    let ctx = Context::new(tmp.path().to_path_buf());
    let findings = FeatureAcceptanceTestRef.run(&ctx);
    assert_eq!(findings.len(), 1, "expected one finding, got {findings:#?}");
    let hint = findings[0].fix_hint.as_deref().unwrap();
    assert!(
        hint.contains("did you mean") && hint.contains("widget_render.rs"),
        "expected fuzzy suggestion in hint, got: {hint}"
    );
}

#[test]
fn no_suggestion_when_distance_too_large() {
    let tmp = TempDir::new().unwrap();
    write(
        tmp.path(),
        "crates/sample/tests/totally_unrelated.rs",
        "// real test\n",
    );
    let body = format!(
        "{FEATURE_HEAD}- [ ] does the thing\n  → crates/sample/tests/widget_render.rs#happy\n"
    );
    write(tmp.path(), "wiki/features/x.md", &body);

    let ctx = Context::new(tmp.path().to_path_buf());
    let findings = FeatureAcceptanceTestRef.run(&ctx);
    assert_eq!(findings.len(), 1);
    let hint = findings[0].fix_hint.as_deref().unwrap();
    assert!(
        !hint.contains("did you mean"),
        "should not suggest distant matches, got: {hint}"
    );
}