kindling-service 0.3.0

In-process orchestration layer for kindling memory: capsules, observations, retrieval and pins.
Documentation
//! Snapshot tests against fixtures generated by the Node.js filters.
//!
//! `tests/fixtures/fixtures.json` is produced by `generate-fixtures.mjs`
//! running the actual production implementations (the plugin hook filter and
//! the adapter path filter). Every assertion here is byte-for-byte.
//!
//! Regenerate after changing the generator:
//!   node crates/kindling-filter/tests/fixtures/generate-fixtures.mjs

use std::path::PathBuf;

use serde::Deserialize;
use serde_json::Value;

use kindling_service::filter::{
    contains_secrets, filter_content, filter_tool_result, is_excluded_path, mask_secrets,
};

#[derive(Deserialize)]
struct Fixtures {
    content: Vec<ContentCase>,
    tools: Vec<ToolCase>,
    paths: Vec<PathCase>,
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ContentCase {
    name: String,
    input: String,
    contains_secrets: bool,
    masked: String,
    filtered: String,
}

#[derive(Deserialize)]
struct ToolCase {
    name: String,
    tool: String,
    result: Value,
    output: Option<String>,
}

#[derive(Deserialize)]
struct PathCase {
    path: String,
    excluded: bool,
}

fn load_fixtures() -> Fixtures {
    let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("fixtures")
        .join("fixtures.json");
    serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap()
}

#[test]
fn content_cases_match_node_output() {
    for case in load_fixtures().content {
        assert_eq!(
            contains_secrets(&case.input),
            case.contains_secrets,
            "containsSecrets mismatch: {}",
            case.name
        );
        assert_eq!(
            mask_secrets(&case.input),
            case.masked,
            "maskSecrets mismatch: {}",
            case.name
        );
        assert_eq!(
            filter_content(&case.input),
            case.filtered,
            "filterContent mismatch: {}",
            case.name
        );
    }
}

#[test]
fn tool_cases_match_node_output() {
    for case in load_fixtures().tools {
        // JSON has no undefined: the generator writes JS null results as
        // JSON null, which maps to Value::Null (treated as absent).
        let result = if case.result.is_null() {
            None
        } else {
            Some(&case.result)
        };
        assert_eq!(
            filter_tool_result(&case.tool, result),
            case.output,
            "filterToolResult mismatch: {}",
            case.name
        );
    }
}

#[test]
fn path_cases_match_node_output() {
    for case in load_fixtures().paths {
        assert_eq!(
            is_excluded_path(&case.path),
            case.excluded,
            "isExcludedPath mismatch: {}",
            case.path
        );
    }
}

#[test]
fn fixtures_cover_every_branch() {
    let fixtures = load_fixtures();
    // Guard against a future regeneration silently dropping coverage.
    assert!(fixtures.content.iter().any(|c| c.contains_secrets));
    assert!(fixtures.content.iter().any(|c| !c.contains_secrets));
    assert!(fixtures
        .content
        .iter()
        .any(|c| c.filtered.contains("[Truncated")));
    assert!(fixtures
        .tools
        .iter()
        .any(|t| t.output.as_deref() == Some("[Result not captured]")));
    assert!(fixtures.tools.iter().any(|t| t.output.is_none()));
    assert!(fixtures.paths.iter().any(|p| p.excluded));
    assert!(fixtures.paths.iter().any(|p| !p.excluded));
}